426 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|