Files
VirtualC64-Core/KeyboardController.swift
2020-02-29 11:49:23 -07:00

426 lines
13 KiB
Swift

//
// This file is part of VirtualC64 - A cycle accurate Commodore 64 emulator
//
// Copyright (C) Dirk W. Hoffmann. www.dirkwhoffmann.de
// Licensed under the GNU General Public License v3
//
// See https://www.gnu.org for license information
//
import Foundation
import Carbon.HIToolbox
struct Defaults {
static let keyMap = KeyboardController.standardKeyMap
static let mapKeysByPosition = false
}
//!@ brief Keyboard event handler
@objc
class KeyboardController: NSObject {
var c64: C64Proxy
/// Determines whether the joystick emulation keys should be uncoupled from
// the keyboard.
var disconnectJoyKeys = true
/**
Key mapping mode
The user can choose between a symbolic and a positional key assignment.
Symbolic assignment tries to assign the keys according to their meaning
while positional assignment establishes a one-to-one mapping between Mac
keys and C64 keys.
*/
var mapKeysByPosition = Defaults.mapKeysByPosition
/// Used key map if keys are mapped by position
var keyMap: [MacKey: C64Key] = Defaults.keyMap
// Delete when Objective-C code is gone
func getDisconnectEmulationKeys() -> Bool { return disconnectJoyKeys }
func setDisconnectEmulationKeys(_ b: Bool) { disconnectJoyKeys = b }
func getMapKeysByPosition() -> Bool { return mapKeysByPosition }
func setMapKeysByPosition(_ b: Bool) { mapKeysByPosition = b }
/// Remembers the currently pressed key modifiers
var leftShift: Bool = false
var rightShift: Bool = false
var control: Bool = false
var option: Bool = false
var command: Bool = false
/**
Remembers the currently pressed keys and their assigned C64 key list
This variable is only used when keys are mapped symbolically. It's written
in keyDown and picked up in keyUp.
*/
var pressedKeys: [MacKey: [C64Key]] = [:]
/**
Checks if the internal values are consistent with the provides flags.
There should never be an insonsistency. But if there is, we release the
suspicous key. Otherwise, we risk to block the C64's keyboard matrix
for good.
*/
func checkConsistency(withFlags flags: NSEvent.ModifierFlags) {
if (leftShift || rightShift) != flags.contains(.shift) {
keyUp(with: MacKey.leftShift)
keyUp(with: MacKey.rightShift)
Swift.print("*** SHIFT inconsistency detected *** \(leftShift) \(rightShift)")
}
if control != flags.contains(.control) {
keyUp(with: MacKey.control)
Swift.print("*** SHIFT inconsistency *** \(control)")
}
if option != flags.contains(.option) {
keyUp(with: MacKey.option)
Swift.print("*** SHIFT inconsistency *** \(option)")
}
}
@objc
init(c64: C64Proxy) {
self.c64 = c64
super.init()
}
@objc
func keyDown(keyCode: UInt16, characters: String?, flags: NSEvent.ModifierFlags) {
// Ignore keys that are pressed in combination with the command key
if flags.contains(.command) {
// track("Ignoring the command key")
return
}
// Create and press MacKey
let macKey = MacKey(keyCode: keyCode, characters: characters)
checkConsistency(withFlags: flags)
keyDown(with: macKey)
}
@objc
func keyUp(keyCode: UInt16, characters: String?, flags: NSEvent.ModifierFlags) {
// Create and release macKey
let macKey = MacKey(keyCode: keyCode, characters: characters)
checkConsistency(withFlags: flags)
keyUp(with: macKey)
}
func flagsChanged(with event: NSEvent) {
// track("\(event)")
let mod = event.modifierFlags
let keyCode = event.keyCode
if keyCode == kVK_Shift {
if !leftShift {
leftShift = true
keyDown(with: MacKey.leftShift)
} else {
leftShift = false
keyUp(with: MacKey.leftShift)
}
}
if keyCode == kVK_RightShift {
if !rightShift {
rightShift = true
keyDown(with: MacKey.rightShift)
} else {
rightShift = false
keyUp(with: MacKey.rightShift)
}
}
if mod.contains(.control) && !control {
control = true
keyDown(with: MacKey.control)
}
if !mod.contains(.control) && control {
control = false
keyUp(with: MacKey.control)
}
if mod.contains(.option) && !option {
option = true
keyDown(with: MacKey.option)
}
if !mod.contains(.option) && option {
option = false
keyUp(with: MacKey.option)
}
command = mod.contains(.command)
}
func keyDown(with macKey: MacKey) {
// track("\(macKey)")
// Check if this key is used for joystick emulation
// if controller.gamePadManager.keyDown(with: macKey) && disconnectJoyKeys {
// return
// }
if mapKeysByPosition {
keyDown(with: macKey, keyMap: keyMap)
return
}
// Translate MacKey to a list of C64Keys
let c64Keys = translate(macKey: macKey)
if c64Keys != [] {
// Store key combination for later use in keyUp
pressedKeys[macKey] = c64Keys
// Press all required keys
for key in c64Keys {
c64.keyboard.pressKey(atRow: key.row, col: key.col)
}
}
}
func keyDown(with macKey: MacKey, keyMap: [MacKey: C64Key]) {
if let key = keyMap[macKey] {
c64.keyboard.pressKey(atRow: key.row, col: key.col)
}
}
func keyUp(with macKey: MacKey) {
// Check if this key is used for joystick emulation
// if controller.gamePadManager.keyUp(with: macKey) && disconnectJoyKeys {
// return
// }
if mapKeysByPosition {
keyUp(with: macKey, keyMap: keyMap)
return
}
// Lookup keys to be released
if let c64Keys = pressedKeys[macKey] {
for key in c64Keys {
c64.keyboard.releaseKey(atRow: key.row, col: key.col)
}
}
}
func keyUp(with macKey: MacKey, keyMap: [MacKey: C64Key]) {
if let key = keyMap[macKey] {
// track("Releasing row: \(key.row) col: \(key.col)\n")
c64.keyboard.releaseKey(atRow: key.row, col: key.col)
}
}
/// Standard physical key map
/// Keys are matched based on their position on the keyboard
static let standardKeyMap: [MacKey: C64Key] = [
// First row of C64 keyboard
MacKey.Ansi.grave: C64Key.leftArrow,
MacKey.Iso.hat: C64Key.leftArrow,
MacKey.Ansi.digit0: C64Key.digit0,
MacKey.Ansi.digit1: C64Key.digit1,
MacKey.Ansi.digit2: C64Key.digit2,
MacKey.Ansi.digit3: C64Key.digit3,
MacKey.Ansi.digit4: C64Key.digit4,
MacKey.Ansi.digit5: C64Key.digit5,
MacKey.Ansi.digit6: C64Key.digit6,
MacKey.Ansi.digit7: C64Key.digit7,
MacKey.Ansi.digit8: C64Key.digit8,
MacKey.Ansi.digit9: C64Key.digit9,
MacKey.Ansi.minus: C64Key.minus,
MacKey.Ansi.equal: C64Key.plus,
MacKey.delete: C64Key.delete,
MacKey.F1: C64Key.F1F2,
// Second row of C64 keyboard
MacKey.tab: C64Key.control,
MacKey.Ansi.Q: C64Key.Q,
MacKey.Ansi.W: C64Key.W,
MacKey.Ansi.E: C64Key.E,
MacKey.Ansi.R: C64Key.R,
MacKey.Ansi.T: C64Key.T,
MacKey.Ansi.Y: C64Key.Y,
MacKey.Ansi.U: C64Key.U,
MacKey.Ansi.I: C64Key.I,
MacKey.Ansi.O: C64Key.O,
MacKey.Ansi.P: C64Key.P,
MacKey.Ansi.leftBracket: C64Key.at,
MacKey.Ansi.rightBracket: C64Key.asterisk,
MacKey.F3: C64Key.F3F4,
// Third row of C64 keyboard
MacKey.control: C64Key.runStop,
MacKey.Ansi.A: C64Key.A,
MacKey.Ansi.S: C64Key.S,
MacKey.Ansi.D: C64Key.D,
MacKey.Ansi.F: C64Key.F,
MacKey.Ansi.G: C64Key.G,
MacKey.Ansi.H: C64Key.H,
MacKey.Ansi.J: C64Key.J,
MacKey.Ansi.K: C64Key.K,
MacKey.Ansi.L: C64Key.L,
MacKey.Ansi.semicolon: C64Key.semicolon,
MacKey.Ansi.quote: C64Key.colon,
MacKey.Ansi.backSlash: C64Key.equal,
MacKey.ret: C64Key.ret,
MacKey.F5: C64Key.F5F6,
// Fourth row of C64 keyboard
MacKey.option: C64Key.commodore,
MacKey.leftShift: C64Key.shift,
MacKey.rightShift: C64Key.rightShift,
MacKey.Ansi.Z: C64Key.Z,
MacKey.Ansi.X: C64Key.X,
MacKey.Ansi.C: C64Key.C,
MacKey.Ansi.V: C64Key.V,
MacKey.Ansi.B: C64Key.B,
MacKey.Ansi.N: C64Key.N,
MacKey.Ansi.M: C64Key.M,
MacKey.Ansi.comma: C64Key.comma,
MacKey.Ansi.period: C64Key.period,
MacKey.Ansi.slash: C64Key.slash,
MacKey.curRight: C64Key.curLeftRight,
MacKey.curLeft: C64Key.curLeftRight,
MacKey.curDown: C64Key.curUpDown,
MacKey.curUp: C64Key.curUpDown,
MacKey.F7: C64Key.F7F8,
// Fifth row of C64 keyboard
MacKey.space: C64Key.space
]
/// Logical key mapping
/// Keys are mapped based on their meaning or the characters they represent
func translate(macKey: MacKey) -> [C64Key] {
switch macKey {
// First row of C64 keyboard
case MacKey.home: return [C64Key.home]
case MacKey.clear: return [C64Key.home, C64Key.shift]
case MacKey.delete: return [C64Key.delete]
// Second row of C64 keyboard
case MacKey.tab: return [C64Key.control]
// Third row of C64 keyboard
case MacKey.control: return [C64Key.runStop]
case MacKey.ret: return [C64Key.ret]
// Fourth row of C64 keyboard
case MacKey.option: return [C64Key.commodore]
case MacKey.curLeft: return [C64Key.curLeftRight, C64Key.shift]
case MacKey.curRight: return [C64Key.curLeftRight]
case MacKey.curUp: return [C64Key.curUpDown, C64Key.shift]
case MacKey.curDown: return [C64Key.curUpDown]
// Fifth row of C64 keyboard
case MacKey.space: return [C64Key.space]
// Function keys
case MacKey.F1: return [C64Key.F1F2]
case MacKey.F2: return [C64Key.F1F2, C64Key.shift]
case MacKey.F3: return [C64Key.F3F4]
case MacKey.F4: return [C64Key.F3F4, C64Key.shift]
case MacKey.F5: return [C64Key.F5F6]
case MacKey.F6: return [C64Key.F5F6, C64Key.shift]
case MacKey.F7: return [C64Key.F7F8]
case MacKey.F8: return [C64Key.F7F8, C64Key.shift]
default:
// Translate symbolically
return C64Key.translate(char: macKey.description)
}
}
func _type(keyList: [C64Key]) {
for key in keyList {
if key == .restore {
c64.keyboard.pressRestoreKey()
} else {
c64.keyboard.pressKey(atRow: key.row, col: key.col)
}
}
usleep(useconds_t(50000))
for key in keyList {
if key == .restore {
c64.keyboard.releaseRestoreKey()
} else {
c64.keyboard.releaseKey(atRow: key.row, col: key.col)
}
}
}
func _type(key: C64Key) {
type(keyList: [key])
}
func type(keyList: [C64Key]) {
DispatchQueue.global().async {
self._type(keyList: keyList)
}
}
func type(key: C64Key) {
DispatchQueue.global().async {
self._type(key: key)
}
}
@objc
func type(string: String?,
initialDelay: useconds_t = 0,
completion: (() -> Void)? = nil) {
if var truncated = string {
// Shorten string if it is too large
if truncated.count > 255 {
truncated = truncated.prefix(256) + "..."
}
// Type string ...
DispatchQueue.global().async {
usleep(initialDelay)
for c in truncated.lowercased() {
let c64Keys = C64Key.translate(char: String(c))
self._type(keyList: c64Keys)
usleep(useconds_t(20000))
}
guard let fn = completion else { return }
DispatchQueue.main.async {
fn()
}
}
}
}
func type(_ string: String?, initialDelay seconds: Double = 0.0) {
let uSeconds = useconds_t(1000000 * seconds)
type(string: string, initialDelay: uSeconds)
}
func typeOnKeyboardAndPressPlay(string: String?) {
type(string: string, completion: c64.datasette.pressPlay)
}
}