diff --git a/CHANGES.md b/CHANGES.md index 3f1d5ee0..cf130293 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +3.2 (2020-04-xx) +--- + +Improvements: + +- Added support for modifier-only shortcuts +- The `*ShortcutMonitor` family of classes considers the `isEnabled` property of its actions before installing any handlers +- The `SRAXGlobalShortcutMonitor` uses Quartz Services to install an event tap via the `CGEvent*` family of functions. +Unlike `SRGlobalShortcutMonitor`, it can alter handled events but requires the user to grant the Accessibility permission + +Fixes: + +- The control now shifts the label off the center to avoid clipping if there is enough space +- Better invalidation for re-draws +- Handle and warn when AppKit throws exception because NSEvent's `characters*` properties are accessed from a non-main thread + 3.1 (2019-10-19) --- diff --git a/Documentation.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift b/Documentation.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..1dd18c48 --- /dev/null +++ b/Documentation.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift @@ -0,0 +1,259 @@ +/*: + - Important: + Playground uses Live View. + */ +import AppKit +import PlaygroundSupport +import ShortcutRecorder + +PlaygroundPage.current.needsIndefiniteExecution = true +let mainView = NSView(frame: NSRect(x: 0, y: 0, width: 150, height: 50)) +PlaygroundPage.current.liveView = mainView + +let control = RecorderControl() +mainView.addSubview(control) +NSLayoutConstraint.activate([ + control.centerXAnchor.constraint(equalTo: mainView.centerXAnchor), + control.centerYAnchor.constraint(equalTo: mainView.centerYAnchor) +]) +/*: + ### Configuring Modifier Flags Requirements + `RecorderControl` allows you to forbid some modifier flags while require other. + + There are 3 properties that govern this behavior: + - `allowedModifierFlags` controls what flags *can* be set + - `requiredModifierFlags` controls what flags *must* be set + - `allowsEmptyModifierFlags` controls whether no modifier flags are allowed + + - Important: + The control will validate the settings raising an exception for conflicts like marking the flag both disallowed and required. + */ +//control.set(allowedModifierFlags: [.command, .shift, .control], // ⌥ is not allowed +// requiredModifierFlags: [.command, .shift], // ⌘ and ⇧ are required +// allowsEmptyModifierFlags: false) // at least one modifier flag must be set +/*: + ### Modifier flags only shortcut + `RecorderControl` can be configured to record a shortcut that has modifier flags (e.g. ⌘) but no key code. It may be useful for apps such as graphic editors as they often alter the behavior based on + modifier flags. + */ +//control.allowsModifierFlagsOnlyShortcut = true +/*: + ### Cancelling recording with Esc + The control is configured by default to cancel recording when the Esc key is pressed with no modifier flags. As a side effect it is therefore impossibe to record the Esc key. + */ +//control.allowsEscapeToCancelRecording = false +/*: + ### Clearning the recorded value with Delete + Similarly the control is configured by default to clear the recorded value when the Delete key is pressed with no modifier flags. It has exactly the same side effect but for the Delete key this time. + */ +//control.allowsDeleteToClearShortcutAndEndRecording = false + +/*: + ### Communicating change to the controller + */ +class Controller: NSObject { + @objc var objectValue: Shortcut? +} +/*: + Change can be communicated via Target-Action + */ +//extension Controller { +// @objc func action(sender: RecorderControl) { +// objectValue = sender.objectValue +// print("action: \(sender.stringValue)") +// } +//} +//let target = Controller() +//control.target = target +//control.action = #selector(target.action(sender:)) +/*: + As well as via Cocoa Bindings and NSEditorRegistration + */ +//extension Controller: NSEditorRegistration { +// func objectDidBeginEditing(_ editor: NSEditor) { +// print("editor: did begin editing") +// } +// +// func objectDidEndEditing(_ editor: NSEditor) { +// print("editor: did end editing with \((editor as! RecorderControl).stringValue)") +// } +//} +//let controller = Controller() +//control.bind(.value, to: controller, withKeyPath: "objectValue", options: nil) +/*: + And via a delegate + */ +//extension Controller: RecorderControlDelegate { +// func recorderControlShouldBeginRecording(_ aControl: RecorderControl) -> Bool { +// print("delegate: should begin editing") +// return true +// } +// +// func recorderControlDidBeginRecording(_ aControl: RecorderControl) { +// print("delegate: did begin editing") +// } +// +// func recorderControl(_ aControl: RecorderControl, shouldUnconditionallyAllowModifierFlags aFlags: Bool, forKeyCode aKeyCode: KeyCode) -> Bool { +// print("delegate: should unconditionally allow modifier flags") +// return true +// } +// +// func recorderControl(_ aControl: RecorderControl, canRecord aShortcut: Shortcut) -> Bool { +// print("delegate: can record shortcut") +// return true +// } +// +// func recorderControlDidEndRecording(_ aControl: RecorderControl) { +// objectValue = aControl.objectValue +// print("delegate: did end editing with \(aControl.stringValue)") +// } +//} +//let controller = Controller() +//control.delegate = controller + +/*: + ### Shortcut + The result of recording is an instance of `Shortcut`, a model class that represents recorded modifier flags and a key code. + */ +//let shortcut = Shortcut(keyEquivalent: "⌥⇧⌘A")! +//assert(shortcut.keyCode == .ansiA) +//assert(shortcut.modifierFlags == [.option, .shift, .command]) +/*: + The `characters` and `charactersIgnoringModifiers` are similar to those of `NSEvent`, and return string-representation of the key code and modifier flags, if available. + */ +//print("Shortcut Characters: \(shortcut.characters!)") +//print("Shortcut Characters Ignoring Modifiers: \(shortcut.charactersIgnoringModifiers!)") +/*: + Since some of the underlying API is using Carbon, there are properties to get Carbon-representation of the `keyCode` and `modifierFlags`: + */ +//print("Carbon Key Code: \(shortcut.carbonKeyCode)") +//print("Carbon Modifier Flags: \(shortcut.carbonModifierFlags)") + +/*: + ### Shortcut Validation + The recorded shortcut is often used as either a key equivalent or a global shortcut. In either case you want to avoid assigning the same shortcut to multiple actions. `ShortcutValidator` helps to prevent these conflicts by checking against Main Menu and System Global Shortcuts for you. + */ +//let validator = ShortcutValidator() +//do { +// try validator.validate(shortcut: Shortcut(keyEquivalent: "⌘Q")) +//} +//catch let error as NSError { +// print(error.localizedDescription) +//} +/*: + For convenience the validator implements the `RecorderControlDelegate/recorderControl(_:,canRecord:)`. + */ +//control.delegate = validator + +/*: + ### Cocoa Transformers + Sometimes it's useful to display a shortcut outside of the recorder control. E.g. in a tooltip or in a label. + + `ShortcutFormatter`, a subclass of `NSFormatter`, can be used in standard Cocoa controls. + */ +//let textField = NSTextField(labelWithString: "") +//textField.formatter = ShortcutFormatter() +//textField.objectValue = Shortcut(keyEquivalent: "⇧⌘A")! +//print(textField.stringValue) +/*: + A number of transformers, subclasses of `NSValueTransformer`, are available for custom alterations. + + #### KeyCodeTransformer + `KeyCodeTransformer` is a class-cluster that transforms numeric key codes into `String`. + + Translation of a key code varies across combinations of keyboards and input sources. E.g. `KeyCode.ansiA` corresponds to "a" in the U.S. English input source but to "ф" in the Russian input source. In addition, some keys, like `KeyCode.tab`, have dual representation: as an input character (`\u{9}`) and as a drawable glyph (`⇥`). Some glyphs may be sensitive to layout direction, e.g. `KeyCode.tab` glyph for right-to-left languages is `⇤`. + + - Note: + The ASCII-capable group is recommended as it provides consistent behavior for all users. It's what `RecorderControl` uses unless `drawsASCIIEquivalentOfShortcut` is set to `false`. + + There are 4 subclasses in the cluster: + + - `SymbolicKeyCodeTransformer`: translates a key code into an input character using current input source + - `LiteralKeyCodeTransformer`: translates a key code into a drawable glyph using current input source + - `ASCIISymbolicKeyCodeTransformer`: translates a key code into an input character using ASCII-capable input source + - `ASCIILiteralKeyCodeTransformer`: translates a key code into a drawable glyph using ASCII-capable input source + this is the only class in the cluster that *allows reverse transformation* + */ +//print("Symbolic Key Code: \"\(ASCIISymbolicKeyCodeTransformer.shared.transformedValue(KeyCode.tab) as! String)\"") +//print("Literal Key Code: \"\(ASCIILiteralKeyCodeTransformer.shared.transformedValue(KeyCode.tab) as! String)\"") +/*: + #### ModifierFlagsTransformer + `ModifierFlagsTransformer` is a class-cluster that transforms of modifier flags into a `String`. + + There are 2 subclasses in the cluster: + - `SymbolicModifierFlagsTransformer` translates modifier flags into readable words, e.g. Shift-Command + - `LiteralModifierFlagsTransformer` translates modifier flags into drawable glyphs, e.g. ⇧⌘ + */ +//let flags: NSEvent.ModifierFlags = [.shift, .command] +//print("Symbolic Modifier Flags: \"\(SymbolicModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"") +//print("Literal Modifier Flags: \"\(LiteralModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"") +/*: + #### Transformers + Both are helper classes that can transform instances of `Shortcut` into Cocoa's `keyEquivalent` and `keyEquivalentModifierMask`. This allows to bind key paths leading to a `Shortcut` to Cocoa controls directly from Interface Builder. + */ +//print("Key Equivalent: \"\(KeyEquivalentTransformer.shared.transformedValue(shortcut) as! String)\"") +//print("Key Equivalent Modifier Mask: \"\(KeyEquivalentModifierMaskTransformer.shared.transformedValue(shortcut) as! UInt)\"") + +/*: + ### Shortcut Monitoring + `GlobalShortcutMonitor` and `LocalShortcutMonitor` allows to perform actions in response to key events. Instance of either class can associate a shortcut (an object or a KVO path) with an action (a selector or a block). + + `GlobalShortcutMonitor` tries to register a system-wide hot key that can be triggered from any app. +*/ +//let shortcut = Shortcut(keyEquivalent: "⌘A") +//let action = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌥⇧⌘A")!) { action in +// print("Handle global shortcut") +// return true +//} +//let globalMonitor = GlobalShortcutMonitor() +//globalMonitor.addAction(action, forKeyEvent: .down) +/*: + `LocalShortcutMonitor` requires you to call the `handle(_:, withTarget:)` method with a key event and an optional target (for selector). + + `LocalShortcutMonitor` is designed to be used from: + - `NSResponder/keyDown(with:)` + - `NSResponder/keyUp(with:)` + - `NSResponder/performKeyEquivalent(with:)` + - `NSResponder/flagsChanged(with:)` + - `NSEvent/addLocalMonitorForEvents(matching:handler:)` + - `NSEvent/addGlobalMonitorForEvents(matching:handler:)` + */ +//let shortcut = Shortcut(keyEquivalent: "⌥⇧⌘A")! +//let action = ShortcutAction(shortcut: shortcut) { action in +// print("Handle local shortcut") +// return true +//} +//let event = NSEvent.keyEvent(with: .keyDown, +// location: NSPoint(x: 0, y: 0), +// modifierFlags: shortcut.modifierFlags, +// timestamp: 0, +// windowNumber: 0, +// context: nil, +// characters: "A", +// charactersIgnoringModifiers: "a", +// isARepeat: false, +// keyCode: UInt16(shortcut.keyCode.rawValue))! +//let localMonitor = LocalShortcutMonitor() +//localMonitor.addAction(action, forKeyEvent: .down) +//localMonitor.handle(event, withTarget: nil) +/*: + It can be used to recognize and handle `keyCode`-less shortcuts + */ +//let event = NSEvent.keyEvent(with: .flagsChanged, +// location: NSPoint(x: 0, y: 0), +// modifierFlags: [.shift, .command], +// timestamp: 0, +// windowNumber: 0, +// context: nil, +// characters: "A", +// charactersIgnoringModifiers: "a", +// isARepeat: false, +// keyCode: UInt16(kVK_Command))! +//let shortcut = Shortcut(event: event)! +//let action = ShortcutAction(shortcut: shortcut) { action in +// print("Handle local shortcut") +// return true +//} +//let localMonitor = LocalShortcutMonitor() +//localMonitor.addAction(action, forKeyEvent: .down) +//localMonitor.handle(event, withTarget: nil) diff --git a/Documentation.playground/Pages/Shortcut, Validation and Monitoring.xcplaygroundpage/Contents.swift b/Documentation.playground/Pages/Shortcut, Validation and Monitoring.xcplaygroundpage/Contents.swift deleted file mode 100644 index 106b70fb..00000000 --- a/Documentation.playground/Pages/Shortcut, Validation and Monitoring.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,120 +0,0 @@ -//#-hidden-code -import AppKit -import Carbon -import PlaygroundSupport -import ShortcutRecorder -PlaygroundPage.current.needsIndefiniteExecution = true -//#-end-hidden-code -//: [Previous](@previous) -/*: - ## Shortcut - Captured key code and modifier flags are represented by an instance of the `Shortcut` model class. - - The easiest way to create one in code is by using an ASCII key equivalent: - */ -let shortcut = Shortcut(keyEquivalent: "⌥⇧⌘A")! -assert(shortcut.keyCode == kVK_ANSI_A) -assert(shortcut.modifierFlags == [.option, .shift, .command]) -/*: - The `characters` and `charactersIgnoringModifiers` are convenience properties similar to those of `NSEvent` - and return translation of the key code and modifier flags, if available. They do not participate in equality tests. - */ -print("Shortcut Characters: \(shortcut.characters!)") -print("Shortcut Characters Ignoring Modifiers: \(shortcut.charactersIgnoringModifiers!)") -/*: - Since some of the underlying API is using Carbon, there are convenience properties to get Carbon-representation - of the `keyCode` and `modifierFlags`: - */ -print("Carbon Key Code: \(shortcut.carbonKeyCode)") -print("Carbon Modifier Flags: \(shortcut.carbonModifierFlags)") -/*: - Shortcut can be checked for equality directly against a Cocoa-like key equivalent: - */ -shortcut.isEqual(keyEquivalent: "a", modifierFlags: [.shift]) -/*: - It conforms to `NSSecureCoding`: - */ -var encodedShortcutData = NSKeyedArchiver.archivedData(withRootObject: shortcut, requiringSecureCoding: true) -let decodedShortcut = try! NSKeyedUnarchiver.unarchivedObject(ofClass: Shortcut.self, from: encodedShortcutData) -assert(shortcut == decodedShortcut) -/*: - ## Shortcut Validation - The recorded shortcut is often used as either a key equivalent or a global shortcut. In either case you want to avoid - assigning the same shortcut to multiple actions. `ShortcutValidator` helps to prevent these conflicts by checking - against Main Menu and System Global Shortcuts for you. - */ -let validator = ShortcutValidator() -do { - try validator.validate(shortcut: shortcut) -} -catch let error as NSError { - print(error.localizedDescription) -} -/*: - ## Shortcut Actions - The `ShortcutAction` class connects shortcuts to actions. - - Shortcut can be set directly: - */ -let action = ShortcutAction(shortcut: shortcut) { action in - print("Handle global shortcut") - return true -} -/*: - Or it can be observed from another object: - */ -let autoupdatingAction = ShortcutAction(keyPath: "shortcut", of: UserDefaults.standard) { action in - print("Handle autoupdating global shortcut") - return true -} -encodedShortcutData = NSKeyedArchiver.archivedData(withRootObject: Shortcut(keyEquivalent: "⌃⇧⌘B")!, requiringSecureCoding: true) -UserDefaults.standard.set(encodedShortcutData, forKey: "shortcut") -/*: - Action can be a closure, as seen above, or it can be a target/action: - */ -let targetAction = ShortcutAction(shortcut: shortcut, target: nil, action: #selector(NSResponder.selectAll(_:)), tag: 0) -/*: - If target is not specified it defaults to `NSApp`. See the class documentation for more configuration options. - - ## Shortcut Monitoring - The `GlobalShortcutMonitor` and `LocalShortcutMonitor` use actions to observe and react to user's keyboard events. - - `GlobalShortcutMonitor` registers a global shortcut that can be activated regardless of an application that - currently has the keyboard focus. - - - Note: - ⌥⇧⌘A and ⌃⇧⌘B will be globally overridden until you terminate the Playground - */ -let globalMonitor = GlobalShortcutMonitor.shared -globalMonitor.addAction(action, forKeyEvent: .down) -globalMonitor.addAction(autoupdatingAction, forKeyEvent: .down) -/*: - `LocalShortcutMonitor` organizes actions into a collection that can later by used for event dispatch e.g. - in the subclasses of `NSResponder` such as `NSView` and `NSViewController`. It's a convenient alternative - to `NSMenu`, e.g. when there are too many shortcuts to specify or when the app is headless and lacks - the main menu altogether. - */ -class MyController: NSViewController { - var localMonitor = LocalShortcutMonitor() - - override func viewDidLoad() { - super.viewDidLoad() - localMonitor.addAction(#selector(MyController.selectNextTab(_:)), forKeyEquivalent: "⇧⌘]", tag: 0) - localMonitor.addAction(#selector(MyController.selectPreviousTab(_:)), forKeyEquivalent: "⇧⌘[", tag: 0) - } - - override func keyDown(with event: NSEvent) { - if (!localMonitor.handle(event, withTarget: self)) { - super.keyDown(with: event) - } - } - - @objc func selectNextTab(_ sender: Any?) { - print("selectNextTab") - } - - @objc func selectPreviousTab(_ sender: Any?) { - print("selectPreviousTab") - } -} -//: [Next](@next) diff --git a/Documentation.playground/Pages/Transformers and Formatters.xcplaygroundpage/Contents.swift b/Documentation.playground/Pages/Transformers and Formatters.xcplaygroundpage/Contents.swift deleted file mode 100644 index d694e1fa..00000000 --- a/Documentation.playground/Pages/Transformers and Formatters.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,99 +0,0 @@ -//#-hidden-code -import AppKit -import PlaygroundSupport -import ShortcutRecorder - -PlaygroundPage.current.needsIndefiniteExecution = true -let mainView = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 50)) -PlaygroundPage.current.liveView = mainView -//#-end-hidden-code -//: [Previous](@previous) -/*: - - Important: - Playground uses Live View. - - ## Formatters - Sometimes it's useful to display a shortcut outside of the recorder control. E.g. in a tooltip or in a label. - - `ShortcutFormatter`, a subclass of `NSFormatter`, can be used for that: - */ -let label = NSTextField(labelWithString: "") -label.translatesAutoresizingMaskIntoConstraints = false -mainView.addSubview(label) -NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: mainView.centerXAnchor), - label.centerYAnchor.constraint(equalTo: mainView.centerYAnchor), - label.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), - label.trailingAnchor.constraint(equalTo: mainView.trailingAnchor), - ]) - -label.formatter = ShortcutFormatter() -let shortcut = Shortcut(keyEquivalent: "⇧⌘A")! -label.objectValue = shortcut -/*: - ## Transformers - A number of transformers, subclasses of `NSValueTransformer`, are available for custom alternations. - - ### KeyCodeTransformer - `KeyCodeTransformer` is a class-cluster that transforms the given numeric key code into a `String`. - - Translation of a key code varies across combinations of keyboards and input sources. E.g. `kVK_ANSI_A` corresponds - to "a" in the U.S. English input source but to "ф" in the Russian input source. In addition, some keys, like - `kVK_Tab`, have dual representation: as an input character (`\u{9}`) and as a drawable glyph (`⇥`). Some glyphs may be - sensitive to layout direction, e.g. `kVK_Tab` glyph for right-to-left languages is `⇤`. - - The class-cluster is split into 2 main groups: - - `*` uses current input source - - `ASCII*` uses ASCII-capable input source. - - - Note: - The ASCII-capable group is recommended as it provides consistent behavior for all users. It's what `RecorderControl` - uses unless `drawsASCIIEquivalentOfShortcut` is unset. - - Each group is then split into 2 more subgroups: - - `Symbolic` translates a key code into an input character - - `Literal` translates a key code into a drawable glyph - - All in all there are 4 subclasses: - - - `SymbolicKeyCodeTransformer`: translates a key code into an input character using current input source - - `LiteralKeyCodeTransformer`: translates a key code into a drawable glyph using current input source - - `ASCIISymbolicKeyCodeTransformer`: translates a key code into an input character using ASCII-capable input source - - `ASCIILiteralKeyCodeTransformer`: translates a key code into a drawable glyph using ASCII-capable input source; - this is the only class in the cluster that *allows reverse transformation* - - The `transformedValue(_:,withImplicitModifierFlags:,explicitModifierFlags:,layoutDirection:)` designated method - performs translation. - - Implicit modifier flags are the flags that are incorporated into visual appearance of the key code, - e.g. ⌥a → å in the U.S. English input source. - - Explicit modifier may alter environment settings, e.g. ⇧ is sometimes used to alter the input direction. - - Layout direction can alter appearance of key codes like `kVK_Tab`. -*/ -print("Symbolic Key Code: \"\(ASCIISymbolicKeyCodeTransformer.shared.transformedValue(kVK_Tab) as! String)\"") -print("Literal Key Code: \"\(ASCIILiteralKeyCodeTransformer.shared.transformedValue(kVK_Tab) as! String)\"") -/*: - ### ModifierFlagsTransformer - `ModifierFlagsTransformer` is a class-cluster that transforms a combination of modifier flags into a `String`. - - There are 2 subclasses in the cluster. - - `SymbolicModifierFlagsTransformer` translates a key code into an input sentance, e.g. Shift-Command - - `LiteralModifierFlagsTransformer` translates a key code into an drawable glyph, e.g. ⇧⌘; - this is the only class in the cluster that *allows reverse transformation* - - The `transformedValue(_, layoutDirection:)` designated method performs the translation. Layout direction alters the order - of modifier flags, e.g. ⇧⌘ → ⌘⇧ / Shift-Command → Command-Shift -*/ -let flags: NSEvent.ModifierFlags = [.shift, .command] -print("Symbolic Modifier Flags: \"\(SymbolicModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"") -print("Literal Modifier Flags: \"\(LiteralModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"") -/*: - ### KeyEquivalentTransformer and KeyEquivalentModifierMaskTransformer - Both are helper classes that can transform instances of `Shortcut` into Cocoa's - `keyEquivalent` and `keyEquivalentModifierMask` properties of classes like `NSMenuItem` and `NSButton`. -*/ -print("Key Equivalent: \"\(KeyEquivalentTransformer.shared.transformedValue(shortcut) as! String)\"") -print("Key Equivalent Modifier Mask: \"\(KeyEquivalentModifierMaskTransformer.shared.transformedValue(shortcut) as! UInt)\"") - //: [Next](@next) diff --git a/Documentation.playground/Pages/Using the Control.xcplaygroundpage/Contents.swift b/Documentation.playground/Pages/Using the Control.xcplaygroundpage/Contents.swift deleted file mode 100644 index 06ae656d..00000000 --- a/Documentation.playground/Pages/Using the Control.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,361 +0,0 @@ -//#-hidden-code -import AppKit -import PlaygroundSupport -import ShortcutRecorder - -PlaygroundPage.current.needsIndefiniteExecution = true -let mainView = NSView(frame: NSRect(x: 0, y: 0, width: 500, height: 500)) -PlaygroundPage.current.liveView = mainView - -let stackView = NSStackView() -stackView.orientation = .vertical -stackView.translatesAutoresizingMaskIntoConstraints = false -mainView.addSubview(stackView) -NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), - stackView.topAnchor.constraint(equalTo: mainView.topAnchor), - stackView.widthAnchor.constraint(equalTo: mainView.widthAnchor) -]) -//#-end-hidden-code -/*: - - Important: - Playground uses Live View. - - ## Adding the Control to View Hierarchy - `RecorderControl` is native to Auto Layout and can handle its own intrinsic size. - */ -let targetActionLabel = NSTextField(labelWithString: "Target-Action:") -let targetActionRecorder = RecorderControl() - -let bindingsLabel = NSTextField(labelWithString: "Bindings:") -let bindingsRecorder = RecorderControl() - -let delegateLabel = NSTextField(labelWithString: "Delegate:") -let delegateRecorder = RecorderControl() - -let views = [ - (targetActionLabel, targetActionRecorder), - (bindingsLabel, bindingsRecorder), - (delegateLabel, delegateRecorder) -] -for (label, recorder) in views { - label.translatesAutoresizingMaskIntoConstraints = false - - let containerView = NSView() - containerView.addSubview(label) - containerView.addSubview(recorder) - NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "|-(>=20)-[label]-[recorder]-(>=20)-|", - options: [.alignAllFirstBaseline], - metrics: nil, - views: ["label": label, "recorder": recorder])) - NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[recorder]|", - options: [], - metrics: nil, - views: ["recorder": recorder])) - NSLayoutConstraint.activate([ - recorder.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) - ]) - - stackView.addView(containerView, in: .center) -} -assert(!stackView.hasAmbiguousLayout) -/*: - - Note: translatesAutoresizingMaskIntoConstraints is *off* by default. - - When clicked, the control beings recording whose product is an instance of `Shortcut`. - The value can also be set directly via the `objectValue` property or the `NSValueBinding` binding. - */ -targetActionRecorder.objectValue = Shortcut(keyEquivalent: "⇧⌘A")! -// bindingsRecorder.objectValue is set by the corresponding binding below -delegateRecorder.objectValue = Shortcut(keyEquivalent: "⇧⌘B")! -/*: - ## Configuring Modifier Flags Requirements - `RecorderControl` allows you to forbid some modifier flags while require other. - - There are 3 properties that govern this behavior: - - `allowedModifierFlags` controls what flags *can* be set - - `requiredModifierFlags` controls what flags *must* be set - - `allowsEmptyModifierFlags` controls whether no modifier flags are allowed - - - Important: - The control will validate the settings raising an exception for conflicts like marking the flag both disallowed - and required. -*/ -targetActionRecorder.set(allowedModifierFlags: [.command, .shift, .control], // the option flag is not allowed - requiredModifierFlags: [.command, .shift], // ⇧ and ⌘ are required - allowsEmptyModifierFlags: false) // at least one modifier flag must be set -delegateRecorder.set(allowedModifierFlags: CocoaModifierFlagsMask, - requiredModifierFlags: [], - allowsEmptyModifierFlags: true) -bindingsRecorder.set(allowedModifierFlags: [.command, .option], - requiredModifierFlags: [.option], - allowsEmptyModifierFlags: false) -/*: - The requirements can be bypassed by implementing either the `recorderControl(_:,shouldUnconditionallyAllow:,forKeyCode:)` delegate method or by setting `objectValue` directly. - - ## Configuring Key Code Handling - Some keys are natural shortcuts with consistent actions assigned to them throughout the system and well-designed apps. - The `RecorderControl` recognizes Escape to cancel the recording and Delete to end the recording by clearing current value. - This behavior can be altered with `allowsEscapeToCancelRecording` and `allowsDeleteToClearShortcutAndEndRecording` respectively. - - ## View ↔︎ Controller Communication - In Cocoa there is a number of ways how a view can communicate value changes to the controller. `RecorderControl` - supports all of them. - - ### Target-Action - The `target` and `action` properties inherited from `NSControl` are used to deliver a notification whenever recording ends. - */ -/// Target will print control's value whenever recording ends. -class Target: NSObject { - @objc func action(sender: RecorderControl) { - print("action: \(sender.stringValue)") - } -} -let target = Target() -targetActionRecorder.target = target -targetActionRecorder.action = #selector(target.action(sender:)) -/*: - ### Bindings - `RecorderControl` implements `NSValueBinding` and supports transformers. - - If the observed object also adopts the `NSEditorRegistration` protocol (typically a subclass of `NSDocument` and `NSController`) - the control will notify it by using the appropriate methods. - */ -class Editor: NSObject, NSEditorRegistration { - @objc var objectValue: Shortcut? = Shortcut(keyEquivalent: "⌥⌘C")! - - func objectDidBeginEditing(_ editor: NSEditor) { - print("editor: did begin editing") - } - - func objectDidEndEditing(_ editor: NSEditor) { - print("editor: did end editing with \((editor as! RecorderControl).stringValue)") - } -} -let editor = Editor() -bindingsRecorder.bind(.value, to: editor, withKeyPath: "objectValue", options: nil) -/*: - ### Delegate - The delegate may opt in to receive notifications whenever recording begins and ends. - */ -class Delegate: NSObject, RecorderControlDelegate { - func recorderControlDidBeginRecording(_ aControl: RecorderControl) { - print("delegate: did begin editing") - } - - func recorderControlDidEndRecording(_ aControl: RecorderControl) { - print("delegate did end editing with \(aControl.stringValue)") - } -} -let delegate = Delegate() -delegateRecorder.delegate = delegate -/*: - ## Styling - Appearance of the control is controller by the `style` property which can be any object conforming to the `RecorderControlStyling` protocol. - - Here is an example of customizing the appearance to replicate XCode-alike Key Bindings preferences. - */ -class TableCellRecorderControl: RecorderControl { -//: `NSTableRowView` will automatically propagate its background style that is later used to alter label's color. - @objc var backgroundStyle: NSView.BackgroundStyle = .normal { - didSet { - self.setNeedsDisplay(self.style.labelDrawingGuide.frame) - } - } - -//: When row is selected, alter text color to match the behavior of `NSTextField`. - override var drawingLabelAttributes: [NSAttributedString.Key : Any]? { - var attributes = super.drawingLabelAttributes! - - if !isRecording { - attributes[.foregroundColor] = backgroundStyle == .normal ? NSColor.controlTextColor : NSColor.alternateSelectedControlTextColor - } - - return attributes - } - -//: Indicate recording visually by drawing the standard control background color. - override func drawBackground(_ aDirtyRect: NSRect) { - if isRecording { - NSColor.controlBackgroundColor.setFill() - } - else { - NSColor.clear.setFill() - } - - aDirtyRect.fill() - } - -//: Do not draw "Click to Record Shortcut". - override var drawingLabel: String { - if isRecording { - return super.drawingLabel - } - else { - return self.stringValue - } - } -} - -class RecorderControlTableViewStyle: NSObject, RecorderControlStyling { -//: Override required properties. - let identifier = "sr-tableview" - let allowsVibrancy = false - let isOpaque = false - let baselineDrawingOffsetFromBottom: CGFloat = 3.0 - let alignmentRectInsets = NSEdgeInsetsZero - let intrinsicContentSize = NSSize(width: 20.0, height: 17.0) - lazy var alignmentGuide = NSLayoutGuide() - lazy var labelDrawingGuide = NSLayoutGuide() - var alwaysConstraints = [NSLayoutConstraint]() - var displayingConstraints = [NSLayoutConstraint]() - var recordingWithNoValueConstraints = [NSLayoutConstraint]() - var recordingWithValueConstraints = [NSLayoutConstraint]() -//: And provide attributes for the label. - let normalLabelAttributes: [NSAttributedString.Key : Any] = [.font: NSFont.labelFont(ofSize: 13.0)] - let recordingLabelAttributes: [NSAttributedString.Key : Any] = [ - .font: NSFont.labelFont(ofSize: 13.0), - .foregroundColor: NSColor.disabledControlTextColor - ] - let disabledLabelAttributes: [NSAttributedString.Key : Any] = [.font: NSFont.labelFont(ofSize: 13.0)] -//: Attach guides after the style is added to the control. - func prepareForRecorderControl(_ aControl: RecorderControl) { - aControl.addLayoutGuide(alignmentGuide) - aControl.addLayoutGuide(labelDrawingGuide) - - alwaysConstraints = [ - alignmentGuide.topAnchor.constraint(equalTo: aControl.topAnchor), - alignmentGuide.leftAnchor.constraint(equalTo: aControl.leftAnchor), - alignmentGuide.bottomAnchor.constraint(equalTo: aControl.bottomAnchor), - alignmentGuide.rightAnchor.constraint(equalTo: aControl.rightAnchor), - - labelDrawingGuide.topAnchor.constraint(equalTo: alignmentGuide.topAnchor), - labelDrawingGuide.leftAnchor.constraint(equalTo: alignmentGuide.leftAnchor), - labelDrawingGuide.bottomAnchor.constraint(equalTo: alignmentGuide.bottomAnchor), - labelDrawingGuide.rightAnchor.constraint(equalTo: alignmentGuide.rightAnchor) - ] - - displayingConstraints = alwaysConstraints - recordingWithValueConstraints = alwaysConstraints - recordingWithValueConstraints = alwaysConstraints - } -//: Detach guides when the style is about to be removed. - func prepareForRemoval() { - alignmentGuide.owningView?.removeLayoutGuide(alignmentGuide) - labelDrawingGuide.owningView?.removeLayoutGuide(labelDrawingGuide) - } -//: Boilerplate code to support `NSCopying`. - func copy(with zone: NSZone? = nil) -> Any { - return type(of: self).init() - } - - override required init() { super.init() } -} -//: Each row in the table represents a Command and an associated Key Binding. Commands can have a default value -struct KeyBinding: Hashable { - let name: String - let defaultShortcut: Shortcut? - var currentShortcut: Shortcut? -} -//: The table view has 2 columns: Command and Key. The Command column displays name of the command and the Key column displays either current or default shortcut. -extension NSUserInterfaceItemIdentifier { - static let commandColumn = NSUserInterfaceItemIdentifier("CommandColumn") - static let commandCell = NSUserInterfaceItemIdentifier("CommandCell") - static let keyColumn = NSUserInterfaceItemIdentifier("KeyColumn") - static let keyCell = NSUserInterfaceItemIdentifier("KeyCell") -} - -class TableViewOwner: NSObject -{ -//: Key Combinations displayed in the table. - var keyBindings = [ - KeyBinding(name: "Undo", defaultShortcut: Shortcut(keyEquivalent: "⌘Z"), currentShortcut: nil), - KeyBinding(name: "Redo", defaultShortcut: Shortcut(keyEquivalent: "⇧⌘Z"), currentShortcut: nil), - KeyBinding(name: "Cut", defaultShortcut: Shortcut(keyEquivalent: "⌘X"), currentShortcut: nil), - KeyBinding(name: "Copy", defaultShortcut: Shortcut(keyEquivalent: "⌘C"), currentShortcut: nil), - KeyBinding(name: "Paste", defaultShortcut: Shortcut(keyEquivalent: "⌘V"), currentShortcut: nil), - KeyBinding(name: "Paste Special", defaultShortcut: Shortcut(keyEquivalent: "⌥⌘V"), currentShortcut: nil), - KeyBinding(name: "Paste and Preserve Formatting", defaultShortcut: Shortcut(keyEquivalent: "⌥⇧⌘V"), currentShortcut: nil) - ] -//: Boilerplate code to set up the table view. - var tableView: NSTableView! - lazy var view: NSView = { - tableView = NSTableView() - tableView.usesAutomaticRowHeights = true - tableView.dataSource = self - tableView.delegate = self - - let commandColumn = NSTableColumn(identifier: .commandColumn) - commandColumn.title = "Command" - commandColumn.resizingMask = [] - commandColumn.isEditable = false - commandColumn.width = 200.0 - - let keyColumn = NSTableColumn(identifier: .keyColumn) - keyColumn.title = "Key" - - tableView.addTableColumn(commandColumn) - tableView.addTableColumn(keyColumn) - - let scrollView = NSScrollView() - scrollView.borderType = .bezelBorder - scrollView.documentView = tableView - NSLayoutConstraint.activate([ - scrollView.widthAnchor.constraint(equalToConstant: 400.0), - scrollView.heightAnchor.constraint(equalToConstant: 200.0) - ]) - - return scrollView - }() -} -//: Implement `NSTableViewDelegate` and `NSTableViewDataSource` to populate the table view. -extension TableViewOwner: NSTableViewDelegate, NSTableViewDataSource { - func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - let columnIdentifier = tableColumn!.identifier - - if columnIdentifier == .commandColumn { - var view = tableView.makeView(withIdentifier: .commandCell, owner: self) as! NSTextField? - if view == nil { - view = NSTextField(labelWithString: "") - view!.identifier = .commandCell - } - - view!.stringValue = keyBindings[row].name - return view - } - else if columnIdentifier == .keyColumn { - var view = tableView.makeView(withIdentifier: .keyCell, owner: self) as! TableCellRecorderControl? - if view == nil { - view = TableCellRecorderControl() - view!.style = RecorderControlTableViewStyle() - view!.delegate = self - view!.identifier = .keyCell - } - - view!.objectValue = keyBindings[row].currentShortcut ?? keyBindings[row].defaultShortcut - return view - } - - return nil - } - - func numberOfRows(in tableView: NSTableView) -> Int { - return keyBindings.count - } -} -//: Select table's row visually when editing starts and save value when it ends. -extension TableViewOwner: RecorderControlDelegate { - func recorderControlDidBeginRecording(_ aControl: RecorderControl) { - tableView.selectRowIndexes(IndexSet(integer: tableView.row(for: aControl)), byExtendingSelection: false) - } - - func recorderControlDidEndRecording(_ aControl: RecorderControl) { - let index = tableView.row(for: aControl) - keyBindings[index].currentShortcut = aControl.objectValue - tableView.deselectRow(index) - } -} -//: Add the table view to the playground. -let tableViewOwner = TableViewOwner() -stackView.addView(tableViewOwner.view, in: .center) -//: [Next](@next) diff --git a/Documentation.playground/Pages/Utility.xcplaygroundpage/Contents.swift b/Documentation.playground/Pages/Utility.xcplaygroundpage/Contents.swift deleted file mode 100644 index cb84ddf2..00000000 --- a/Documentation.playground/Pages/Utility.xcplaygroundpage/Contents.swift +++ /dev/null @@ -1,46 +0,0 @@ -//: [Previous](@previous) -//#-hidden-code -import AppKit -import Carbon -import ShortcutRecorder -//#-end-hidden-code - -/*: - ## Modifier Flags - - Under the hood the framework relies on Carbon to register frameworks since there is no modern replacement in Cocoa.\ - Hence there are methods to convert between Cocoa and Carbom modifier flags: - */ -var cocoaFlags: NSEvent.ModifierFlags = [.command, .shift] -var carbonFlags = UInt32(cmdKey | shiftKey) -assert(cocoaToCarbonFlags(cocoaFlags) == carbonFlags) -assert(carbonToCocoaFlags(carbonFlags) == cocoaFlags) -/*: - Since modifier flags may have other values than command, option, shift and control, there are masks to remove them: - */ -cocoaFlags = NSEvent.ModifierFlags([.command, .shift, .function]).intersection(CocoaModifierFlagsMask) -assert(cocoaFlags == [.command, .shift]) - -carbonFlags = UInt32(cmdKey | shiftKey | alphaLock) & CarbonModifierFlagsMask -assert(carbonFlags == UInt32(cmdKey | shiftKey)) -/*: - ## Glyphs - - A number of constants are present for rendering of key codes and modifier flags whose raw values do not\ - map into a representable glyph in the font:` - - `SRKeyCodeGlyph` / `SRKeyCodeString` - - `SRModifierFlagGlyph` / `SRModifierFlagString` -*/ -assert(KeyCodeString.tabRight.rawValue == "⇥") -assert(String(format: "%C", KeyCodeGlyph.tabRight.rawValue) == "⇥") - -assert(ModifierFlagString.command.rawValue == "⌘") -assert(String(format: "%C", ModifierFlagGlyph.command.rawValue) == "⌘") -/*: - ## Resources - - ShortcutRecorder is a framework and therefore comes with a number of methods to locate and load its own resources - when distributed as part of the bundle. - - `SRBundle()`, `SRLoc(_)` and `SRImage(_)` will locate bundle, localized string and image respectively. - */ diff --git a/Documentation.playground/contents.xcplayground b/Documentation.playground/contents.xcplayground index 3a8adcc6..ce767fe3 100644 --- a/Documentation.playground/contents.xcplayground +++ b/Documentation.playground/contents.xcplayground @@ -1,11 +1,8 @@ - - + - - \ No newline at end of file diff --git a/Inspector/App.swift b/Inspector/App.swift index fce4959f..f7613030 100644 --- a/Inspector/App.swift +++ b/Inspector/App.swift @@ -16,7 +16,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ShortcutActionTarget { let purrSound = NSSound(named: "Purr")! override func awakeFromNib() { - let shortcut = Shortcut(code: 0, modifierFlags: [.shift, .control, .option, .command], characters: "A", charactersIgnoringModifiers: "a") + + let shortcut = Shortcut(code: KeyCode.ansiA, modifierFlags: [.shift, .control, .option, .command], characters: "A", charactersIgnoringModifiers: "a") let shortcutData = NSKeyedArchiver.archivedData(withRootObject: shortcut) UserDefaults.standard.register(defaults: [ diff --git a/Inspector/Views.swift b/Inspector/Views.swift index 1eb9362a..e0bc069a 100644 --- a/Inspector/Views.swift +++ b/Inspector/Views.swift @@ -34,7 +34,7 @@ extension NSView { static var scaleToken = "scale" - @IBInspectable var drawsChessboard: Bool { + @objc var drawsChessboard: Bool { get { return objc_getAssociatedObject(self, &NSView.drawsChessboardToken) as? Bool ?? false } @@ -45,7 +45,7 @@ extension NSView { } } - @IBInspectable var chessboardPrimaryColor: NSColor { + @objc var chessboardPrimaryColor: NSColor { get { return objc_getAssociatedObject(self, &NSView.chessboardPrimaryColorToken) as? NSColor ?? NSColor.textBackgroundColor } @@ -56,7 +56,7 @@ extension NSView { } } - @IBInspectable var chessboardSecondaryColor: NSColor { + @objc var chessboardSecondaryColor: NSColor { get { return objc_getAssociatedObject(self, &NSView.chessboardSecondaryColorToken) as? NSColor ?? NSColor.tertiaryLabelColor } @@ -67,7 +67,7 @@ extension NSView { } } - @IBInspectable var drawsBaseline: Bool { + @objc var drawsBaseline: Bool { get { return objc_getAssociatedObject(self, &NSView.drawsBaselineToken) as? Bool ?? false } @@ -78,7 +78,7 @@ extension NSView { } } - @IBInspectable var baselinePrimaryColor: NSColor { + @objc var baselinePrimaryColor: NSColor { get { return objc_getAssociatedObject(self, &NSView.baselinePrimaryColorToken) as? NSColor ?? NSColor.red } @@ -89,7 +89,7 @@ extension NSView { } } - @IBInspectable var baselineSecondaryColor: NSColor { + @objc var baselineSecondaryColor: NSColor { get { return objc_getAssociatedObject(self, &NSView.baselineSecondaryColorToken) as? NSColor ?? NSColor.blue } @@ -100,7 +100,7 @@ extension NSView { } } - @IBInspectable var drawsAlignmentRect: Bool { + @objc var drawsAlignmentRect: Bool { get { return objc_getAssociatedObject(self, &NSView.drawsAlignmentRectToken) as? Bool ?? false } @@ -111,7 +111,7 @@ extension NSView { } } - @IBInspectable var alignmentRectColor: NSColor { + @objc var alignmentRectColor: NSColor { get { return objc_getAssociatedObject(self, &NSView.alignmentRectColorToken) as? NSColor ?? NSColor.red } @@ -122,7 +122,7 @@ extension NSView { } } - var scale: CGFloat { + @objc var scale: CGFloat { get { return objc_getAssociatedObject(self, &NSView.scaleToken) as? CGFloat ?? 2.0 } diff --git a/Library/Info.plist b/Library/Info.plist index 2b6a8fb2..b42a4a84 100644 --- a/Library/Info.plist +++ b/Library/Info.plist @@ -13,11 +13,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.1 + 3.2 CFBundleSignature ???? CFBundleVersion - 3.1 + 3.2 NSHumanReadableCopyright Copyright © 2019 ShortcutRecorder attributors diff --git a/Library/SRCommon.h b/Library/SRCommon.h index efba63ec..aef1f095 100644 --- a/Library/SRCommon.h +++ b/Library/SRCommon.h @@ -5,6 +5,7 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -110,6 +111,128 @@ typedef NS_ENUM(unichar, SRModifierFlagGlyph) } NS_SWIFT_NAME(ModifierFlagGlyph); +/*! + Known key codes. + */ +typedef NS_ENUM(uint16_t, SRKeyCode) { + SRKeyCodeNone = UINT16_MAX, + + SRKeyCodeA NS_SWIFT_NAME(ansiA) = kVK_ANSI_A, + SRKeyCodeS NS_SWIFT_NAME(ansiS) = kVK_ANSI_S, + SRKeyCodeD NS_SWIFT_NAME(ansiD) = kVK_ANSI_D, + SRKeyCodeF NS_SWIFT_NAME(ansiF) = kVK_ANSI_F, + SRKeyCodeH NS_SWIFT_NAME(ansiH) = kVK_ANSI_H, + SRKeyCodeG NS_SWIFT_NAME(ansiG) = kVK_ANSI_G, + SRKeyCodeZ NS_SWIFT_NAME(ansiZ) = kVK_ANSI_Z, + SRKeyCodeX NS_SWIFT_NAME(ansiX) = kVK_ANSI_X, + SRKeyCodeC NS_SWIFT_NAME(ansiC) = kVK_ANSI_C, + SRKeyCodeV NS_SWIFT_NAME(ansiV) = kVK_ANSI_V, + SRKeyCodeB NS_SWIFT_NAME(ansiB) = kVK_ANSI_B, + SRKeyCodeQ NS_SWIFT_NAME(ansiQ) = kVK_ANSI_Q, + SRKeyCodeW NS_SWIFT_NAME(ansiW) = kVK_ANSI_W, + SRKeyCodeE NS_SWIFT_NAME(ansiE) = kVK_ANSI_E, + SRKeyCodeR NS_SWIFT_NAME(ansiR) = kVK_ANSI_R, + SRKeyCodeY NS_SWIFT_NAME(ansiY) = kVK_ANSI_Y, + SRKeyCodeT NS_SWIFT_NAME(ansiT) = kVK_ANSI_T, + SRKeyCode1 NS_SWIFT_NAME(ansi1) = kVK_ANSI_1, + SRKeyCode2 NS_SWIFT_NAME(ansi2) = kVK_ANSI_2, + SRKeyCode3 NS_SWIFT_NAME(ansi3) = kVK_ANSI_3, + SRKeyCode4 NS_SWIFT_NAME(ansi4) = kVK_ANSI_4, + SRKeyCode6 NS_SWIFT_NAME(ansi6) = kVK_ANSI_6, + SRKeyCode5 NS_SWIFT_NAME(ansi5) = kVK_ANSI_5, + SRKeyCodeEqual NS_SWIFT_NAME(ansiEqual) = kVK_ANSI_Equal, + SRKeyCode9 NS_SWIFT_NAME(ansi9) = kVK_ANSI_9, + SRKeyCode7 NS_SWIFT_NAME(ansi7) = kVK_ANSI_7, + SRKeyCodeMinus NS_SWIFT_NAME(ansiMinus) = kVK_ANSI_Minus, + SRKeyCode8 NS_SWIFT_NAME(ansi8) = kVK_ANSI_8, + SRKeyCode0 NS_SWIFT_NAME(ansi0) = kVK_ANSI_0, + SRKeyCodeRightBracket NS_SWIFT_NAME(ansiRightBracket) = kVK_ANSI_RightBracket, + SRKeyCodeO NS_SWIFT_NAME(ansiO) = kVK_ANSI_O, + SRKeyCodeU NS_SWIFT_NAME(ansiU) = kVK_ANSI_U, + SRKeyCodeLeftBracket NS_SWIFT_NAME(ansiLeftBracket) = kVK_ANSI_LeftBracket, + SRKeyCodeI NS_SWIFT_NAME(ansiI) = kVK_ANSI_I, + SRKeyCodeP NS_SWIFT_NAME(ansiP) = kVK_ANSI_P, + SRKeyCodeL NS_SWIFT_NAME(ansiL) = kVK_ANSI_L, + SRKeyCodeJ NS_SWIFT_NAME(ansiJ) = kVK_ANSI_J, + SRKeyCodeQuote NS_SWIFT_NAME(ansiQuote) = kVK_ANSI_Quote, + SRKeyCodeK NS_SWIFT_NAME(ansiK) = kVK_ANSI_K, + SRKeyCodeSemicolon NS_SWIFT_NAME(ansiSemicolon) = kVK_ANSI_Semicolon, + SRKeyCodeBackslash NS_SWIFT_NAME(ansiBackslash) = kVK_ANSI_Backslash, + SRKeyCodeComma NS_SWIFT_NAME(ansiComma) = kVK_ANSI_Comma, + SRKeyCodeSlash NS_SWIFT_NAME(ansiSlash) = kVK_ANSI_Slash, + SRKeyCodeN NS_SWIFT_NAME(ansiN) = kVK_ANSI_N, + SRKeyCodeM NS_SWIFT_NAME(ansiM) = kVK_ANSI_M, + SRKeyCodePeriod NS_SWIFT_NAME(ansiPeriod) = kVK_ANSI_Period, + SRKeyCodeGrave NS_SWIFT_NAME(ansiGrave) = kVK_ANSI_Grave, + SRKeyCodeKeypadDecimal NS_SWIFT_NAME(ansiKeypadDecimal) = kVK_ANSI_KeypadDecimal, + SRKeyCodeKeypadMultiply NS_SWIFT_NAME(ansiKeypadMultiply) = kVK_ANSI_KeypadMultiply, + SRKeyCodeKeypadPlus NS_SWIFT_NAME(ansiKeypadPlus) = kVK_ANSI_KeypadPlus, + SRKeyCodeKeypadClear NS_SWIFT_NAME(ansiKeypadClear) = kVK_ANSI_KeypadClear, + SRKeyCodeKeypadDivide NS_SWIFT_NAME(ansiKeypadDivide) = kVK_ANSI_KeypadDivide, + SRKeyCodeKeypadEnter NS_SWIFT_NAME(ansiKeypadEnter) = kVK_ANSI_KeypadEnter, + SRKeyCodeKeypadMinus NS_SWIFT_NAME(ansiKeypadMinus) = kVK_ANSI_KeypadMinus, + SRKeyCodeKeypadEquals NS_SWIFT_NAME(ansiKeypadEquals) = kVK_ANSI_KeypadEquals, + SRKeyCodeKeypad0 NS_SWIFT_NAME(ansiKeypad0) = kVK_ANSI_Keypad0, + SRKeyCodeKeypad1 NS_SWIFT_NAME(ansiKeypad1) = kVK_ANSI_Keypad1, + SRKeyCodeKeypad2 NS_SWIFT_NAME(ansiKeypad2) = kVK_ANSI_Keypad2, + SRKeyCodeKeypad3 NS_SWIFT_NAME(ansiKeypad3) = kVK_ANSI_Keypad3, + SRKeyCodeKeypad4 NS_SWIFT_NAME(ansiKeypad4) = kVK_ANSI_Keypad4, + SRKeyCodeKeypad5 NS_SWIFT_NAME(ansiKeypad5) = kVK_ANSI_Keypad5, + SRKeyCodeKeypad6 NS_SWIFT_NAME(ansiKeypad6) = kVK_ANSI_Keypad6, + SRKeyCodeKeypad7 NS_SWIFT_NAME(ansiKeypad7) = kVK_ANSI_Keypad7, + SRKeyCodeKeypad8 NS_SWIFT_NAME(ansiKeypad8) = kVK_ANSI_Keypad8, + SRKeyCodeKeypad9 NS_SWIFT_NAME(ansiKeypad9) = kVK_ANSI_Keypad9, + + SRKeyCodeReturn NS_SWIFT_NAME(return) = kVK_Return, + SRKeyCodeTab NS_SWIFT_NAME(tab) = kVK_Tab, + SRKeyCodeSpace NS_SWIFT_NAME(space) = kVK_Space, + SRKeyCodeDelete NS_SWIFT_NAME(delete) = kVK_Delete, + SRKeyCodeEscape NS_SWIFT_NAME(escape) = kVK_Escape, + SRKeyCodeCapsLock NS_SWIFT_NAME(capslock) = kVK_CapsLock, + SRKeyCodeF17 NS_SWIFT_NAME(f17) = kVK_F17, + SRKeyCodeVolumeUp NS_SWIFT_NAME(volumeUp) = kVK_VolumeUp, + SRKeyCodeVolumeDown NS_SWIFT_NAME(volumeDown) = kVK_VolumeDown, + SRKeyCodeMute NS_SWIFT_NAME(mute) = kVK_Mute, + SRKeyCodeF18 NS_SWIFT_NAME(f18) = kVK_F18, + SRKeyCodeF19 NS_SWIFT_NAME(f19) = kVK_F19, + SRKeyCodeF20 NS_SWIFT_NAME(f20) = kVK_F20, + SRKeyCodeF5 NS_SWIFT_NAME(f5) = kVK_F5, + SRKeyCodeF6 NS_SWIFT_NAME(f6) = kVK_F6, + SRKeyCodeF7 NS_SWIFT_NAME(f7) = kVK_F7, + SRKeyCodeF3 NS_SWIFT_NAME(f3) = kVK_F3, + SRKeyCodeF8 NS_SWIFT_NAME(f8) = kVK_F8, + SRKeyCodeF9 NS_SWIFT_NAME(f9) = kVK_F9, + SRKeyCodeF11 NS_SWIFT_NAME(f11) = kVK_F11, + SRKeyCodeF13 NS_SWIFT_NAME(f13) = kVK_F13, + SRKeyCodeF16 NS_SWIFT_NAME(f16) = kVK_F16, + SRKeyCodeF14 NS_SWIFT_NAME(f14) = kVK_F14, + SRKeyCodeF10 NS_SWIFT_NAME(f10) = kVK_F10, + SRKeyCodeF12 NS_SWIFT_NAME(f12) = kVK_F12, + SRKeyCodeF15 NS_SWIFT_NAME(f15) = kVK_F15, + SRKeyCodeHelp NS_SWIFT_NAME(help) = kVK_Help, + SRKeyCodeHome NS_SWIFT_NAME(home) = kVK_Home, + SRKeyCodePageUp NS_SWIFT_NAME(pageUp) = kVK_PageUp, + SRKeyCodeForwardDelete NS_SWIFT_NAME(forwardDelete) = kVK_ForwardDelete, + SRKeyCodeF4 NS_SWIFT_NAME(f4) = kVK_F4, + SRKeyCodeEnd NS_SWIFT_NAME(end) = kVK_End, + SRKeyCodeF2 NS_SWIFT_NAME(f2) = kVK_F2, + SRKeyCodePageDown NS_SWIFT_NAME(pageDown) = kVK_PageDown, + SRKeyCodeF1 NS_SWIFT_NAME(f1) = kVK_F1, + SRKeyCodeLeftArrow NS_SWIFT_NAME(leftArrow) = kVK_LeftArrow, + SRKeyCodeRightArrow NS_SWIFT_NAME(rightArrow) = kVK_RightArrow, + SRKeyCodeDownArrow NS_SWIFT_NAME(downArrow) = kVK_DownArrow, + SRKeyCodeUpArrow NS_SWIFT_NAME(upArrow) = kVK_UpArrow, + + SRKeyCodeISOSection NS_SWIFT_NAME(isoSection) = kVK_ISO_Section, + + SRKeyCodeJISYen NS_SWIFT_NAME(jisYen) = kVK_JIS_Yen, + SRKeyCodeJISUnderscore NS_SWIFT_NAME(jisUnderscore) = kVK_JIS_Underscore, + SRKeyCodeJISKeypadComma NS_SWIFT_NAME(jisKeypadComma) = kVK_JIS_KeypadComma, + SRKeyCodeJISEisu NS_SWIFT_NAME(jisEisu) = kVK_JIS_Eisu, + SRKeyCodeJISKana NS_SWIFT_NAME(jisKana) = kVK_JIS_Kana +} NS_SWIFT_NAME(KeyCode); + + /*! NSString version of SRModifierFlagGlyph diff --git a/Library/SRKeyBindingTransformer.m b/Library/SRKeyBindingTransformer.m index 2f217bb8..12aca514 100644 --- a/Library/SRKeyBindingTransformer.m +++ b/Library/SRKeyBindingTransformer.m @@ -85,42 +85,43 @@ - (SRShortcut *)transformedValue:(NSString *)aValue BOOL isNumPad = [modifierFlagsString containsString:@"#"]; if (isNumPad) { - switch (keyCode.unsignedShortValue) { - case kVK_ANSI_0: - keyCode = @(kVK_ANSI_Keypad0); + switch (keyCode.unsignedShortValue) + { + case SRKeyCode0: + keyCode = @(SRKeyCodeKeypad0); break; - case kVK_ANSI_1: - keyCode = @(kVK_ANSI_Keypad1); + case SRKeyCode1: + keyCode = @(SRKeyCodeKeypad1); break; - case kVK_ANSI_2: - keyCode = @(kVK_ANSI_Keypad2); + case SRKeyCode2: + keyCode = @(SRKeyCodeKeypad2); break; - case kVK_ANSI_3: - keyCode = @(kVK_ANSI_Keypad3); + case SRKeyCode3: + keyCode = @(SRKeyCodeKeypad3); break; - case kVK_ANSI_4: - keyCode = @(kVK_ANSI_Keypad4); + case SRKeyCode4: + keyCode = @(SRKeyCodeKeypad4); break; - case kVK_ANSI_5: - keyCode = @(kVK_ANSI_Keypad5); + case SRKeyCode5: + keyCode = @(SRKeyCodeKeypad5); break; - case kVK_ANSI_6: - keyCode = @(kVK_ANSI_Keypad6); + case SRKeyCode6: + keyCode = @(SRKeyCodeKeypad6); break; - case kVK_ANSI_7: - keyCode = @(kVK_ANSI_Keypad7); + case SRKeyCode7: + keyCode = @(SRKeyCodeKeypad7); break; - case kVK_ANSI_8: - keyCode = @(kVK_ANSI_Keypad8); + case SRKeyCode8: + keyCode = @(SRKeyCodeKeypad8); break; - case kVK_ANSI_9: - keyCode = @(kVK_ANSI_Keypad9); + case SRKeyCode9: + keyCode = @(SRKeyCodeKeypad9); break; - case kVK_ANSI_Minus: - keyCode = @(kVK_ANSI_KeypadMinus); + case SRKeyCodeMinus: + keyCode = @(SRKeyCodeKeypadMinus); break; - case kVK_ANSI_Equal: - keyCode = @(kVK_ANSI_KeypadEquals); + case SRKeyCodeEqual: + keyCode = @(SRKeyCodeKeypadEquals); break; default: break; @@ -170,30 +171,30 @@ - (NSString *)reverseTransformedValue:(SRShortcut *)aValue if (![modifierFlags isKindOfClass:NSNumber.class]) modifierFlags = @(0); - unsigned short keyCodeValue = keyCode.unsignedShortValue; + SRKeyCode keyCodeValue = keyCode.unsignedShortValue; NSEventModifierFlags modifierFlagsValue = modifierFlags.unsignedIntegerValue; BOOL isNumPad = NO; switch (keyCodeValue) { - case kVK_ANSI_Keypad0: - case kVK_ANSI_Keypad1: - case kVK_ANSI_Keypad2: - case kVK_ANSI_Keypad3: - case kVK_ANSI_Keypad4: - case kVK_ANSI_Keypad5: - case kVK_ANSI_Keypad6: - case kVK_ANSI_Keypad7: - case kVK_ANSI_Keypad8: - case kVK_ANSI_Keypad9: - case kVK_ANSI_KeypadDecimal: - case kVK_ANSI_KeypadMultiply: - case kVK_ANSI_KeypadPlus: - case kVK_ANSI_KeypadClear: - case kVK_ANSI_KeypadDivide: - case kVK_ANSI_KeypadEnter: - case kVK_ANSI_KeypadMinus: - case kVK_ANSI_KeypadEquals: + case SRKeyCodeKeypad0: + case SRKeyCodeKeypad1: + case SRKeyCodeKeypad2: + case SRKeyCodeKeypad3: + case SRKeyCodeKeypad4: + case SRKeyCodeKeypad5: + case SRKeyCodeKeypad6: + case SRKeyCodeKeypad7: + case SRKeyCodeKeypad8: + case SRKeyCodeKeypad9: + case SRKeyCodeKeypadDecimal: + case SRKeyCodeKeypadMultiply: + case SRKeyCodeKeypadPlus: + case SRKeyCodeKeypadClear: + case SRKeyCodeKeypadDivide: + case SRKeyCodeKeypadEnter: + case SRKeyCodeKeypadMinus: + case SRKeyCodeKeypadEquals: isNumPad = YES; default: break; diff --git a/Library/SRKeyCodeTransformer.h b/Library/SRKeyCodeTransformer.h index 9549a6c2..00fd5353 100644 --- a/Library/SRKeyCodeTransformer.h +++ b/Library/SRKeyCodeTransformer.h @@ -4,7 +4,7 @@ // #import -#import +#import NS_ASSUME_NONNULL_BEGIN @@ -45,7 +45,7 @@ NS_SWIFT_UNAVAILABLE("use SRLiteralKeyCodeTransformer / SRSymbolicKeyCodeTransfo /*! Return literal string for the given key code, modifier flags and layout direction. */ -- (nullable NSString *)literalForKeyCode:(unsigned short)aValue +- (nullable NSString *)literalForKeyCode:(SRKeyCode)aValue withImplicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags layoutDirection:(NSUserInterfaceLayoutDirection)aDirection; @@ -53,7 +53,7 @@ NS_SWIFT_UNAVAILABLE("use SRLiteralKeyCodeTransformer / SRSymbolicKeyCodeTransfo /*! Return symbolic string for the given key code, modifier flags and layout direction. */ -- (nullable NSString *)symbolForKeyCode:(unsigned short)aValue +- (nullable NSString *)symbolForKeyCode:(SRKeyCode)aValue withImplicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags layoutDirection:(NSUserInterfaceLayoutDirection)aDirection; @@ -126,7 +126,7 @@ NS_SWIFT_NAME(ASCIISymbolicKeyCodeTransformer) + (SRKeyCodeTransformer *)sharedPlainASCIITransformer __attribute__((deprecated("", "SRASCIILiteralKeyCodeTransformer/sharedTransformer"))); - (instancetype)initWithASCIICapableKeyboardInputSource:(BOOL)aUsesASCII plainStrings:(BOOL)aUsesPlainStrings __attribute__((deprecated)); -- (BOOL)isKeyCodeSpecial:(unsigned short)aKeyCode __attribute__((deprecated)); +- (BOOL)isKeyCodeSpecial:(SRKeyCode)aKeyCode __attribute__((deprecated)); - (NSString *)transformedValue:(NSNumber *)aValue withModifierFlags:(NSNumber *)aModifierFlags __attribute__((deprecated)); - (NSString *)transformedSpecialKeyCode:(NSNumber *)aKeyCode diff --git a/Library/SRKeyCodeTransformer.m b/Library/SRKeyCodeTransformer.m index b2fbcbbf..650aed19 100644 --- a/Library/SRKeyCodeTransformer.m +++ b/Library/SRKeyCodeTransformer.m @@ -21,11 +21,11 @@ @interface _SRKeyCodeTranslatorCacheKey : NSObject @property (copy, readonly) NSString *identifier; @property (readonly) NSEventModifierFlags implicitModifierFlags; @property (readonly) NSEventModifierFlags explicitModifierFlags; -@property (readonly) unsigned short keyCode; +@property (readonly) SRKeyCode keyCode; - (instancetype)initWithIdentifier:(NSString *)anIdentifier implicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags - keyCode:(unsigned short)aKeyCode; + keyCode:(SRKeyCode)aKeyCode; @end @@ -34,7 +34,7 @@ @implementation _SRKeyCodeTranslatorCacheKey - (instancetype)initWithIdentifier:(NSString *)anIdentifier implicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags - keyCode:(unsigned short)aKeyCode + keyCode:(SRKeyCode)aKeyCode { self = [super init]; @@ -66,7 +66,7 @@ - (NSUInteger)hash { NSUInteger implicitFlagsBitSize = 4; NSUInteger explicitFlagsBitSize = 4; - NSUInteger keyCodeBitSize = sizeof(unsigned short) * CHAR_BIT; + NSUInteger keyCodeBitSize = sizeof(SRKeyCode) * CHAR_BIT; NSUInteger identifierHash = _identifier.hash; NSUInteger implicitFlagsHash = _implicitModifierFlags >> 17; @@ -86,19 +86,16 @@ - (NSUInteger)hash Cache of the key code translation with respect to input source identifier. */ @interface _SRKeyCodeTranslator : NSObject -{ - NSCache<_SRKeyCodeTranslatorCacheKey *, NSString *> *_translationCache; - _SRKeyCodeTransformerCacheInputSourceCreate _inputSourceCreator; - id _inputSource; -} + @property (class, readonly) _SRKeyCodeTranslator *shared; +@property (readonly) _SRKeyCodeTransformerCacheInputSourceCreate inputSourceCreator; @property (readonly) id inputSource; /*! @param aCreator Lazily instantiates an instance of input source. */ - (instancetype)initWithInputSourceCreator:(_SRKeyCodeTransformerCacheInputSourceCreate)aCreator NS_DESIGNATED_INITIALIZER; - (instancetype)initWithInputSource:(id)anInputSource NS_DESIGNATED_INITIALIZER; -- (nullable NSString *)translateKeyCode:(unsigned short)aKeyCode +- (nullable NSString *)translateKeyCode:(SRKeyCode)aKeyCode implicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags usingCache:(BOOL)anIsUsingCache; @@ -106,6 +103,10 @@ - (nullable NSString *)translateKeyCode:(unsigned short)aKeyCode @implementation _SRKeyCodeTranslator +{ + NSCache<_SRKeyCodeTranslatorCacheKey *, NSString *> *_translationCache; + id _inputSource; +} + (_SRKeyCodeTranslator *)shared { @@ -156,11 +157,14 @@ - (id)inputSource return (__bridge_transfer id)_inputSourceCreator(); } -- (nullable NSString *)translateKeyCode:(unsigned short)aKeyCode +- (nullable NSString *)translateKeyCode:(SRKeyCode)aKeyCode implicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags usingCache:(BOOL)anIsUsingCache { + if (aKeyCode == SRKeyCodeNone) + return @""; + anImplicitModifierFlags &= SRCocoaModifierFlagsMask; anExplicitModifierFlags &= SRCocoaModifierFlagsMask; @@ -232,7 +236,7 @@ - (nullable NSString *)translateKeyCode:(unsigned short)aKeyCode } else if (actualLength == 0) { - os_trace("#Error No translation exists for keyCode %hu and modifierFlags %lu", + os_trace_debug("#Error No translation exists for keyCode %hu and modifierFlags %lu", aKeyCode, anImplicitModifierFlags); return nil; @@ -279,7 +283,7 @@ - (NSNumber *)keyCodeForTranslation:(NSString *)aTranslation { NSAssert([aTranslation.lowercaseString isEqualToString:aTranslation], @"aTranslation must be a lowercase string"); - TISInputSourceRef inputSource = _inputSourceCreator(); + TISInputSourceRef inputSource = self.inputSourceCreator(); if (!inputSource) { @@ -373,109 +377,109 @@ - (instancetype)initWithInputSource:(id)anInputSource static dispatch_once_t OnceToken; dispatch_once(&OnceToken, ^{ KnownKeyCodes = @[ - @(kVK_ANSI_0), - @(kVK_ANSI_1), - @(kVK_ANSI_2), - @(kVK_ANSI_3), - @(kVK_ANSI_4), - @(kVK_ANSI_5), - @(kVK_ANSI_6), - @(kVK_ANSI_7), - @(kVK_ANSI_8), - @(kVK_ANSI_9), - @(kVK_ANSI_A), - @(kVK_ANSI_B), - @(kVK_ANSI_Backslash), - @(kVK_ANSI_C), - @(kVK_ANSI_Comma), - @(kVK_ANSI_D), - @(kVK_ANSI_E), - @(kVK_ANSI_Equal), - @(kVK_ANSI_F), - @(kVK_ANSI_G), - @(kVK_ANSI_Grave), - @(kVK_ANSI_H), - @(kVK_ANSI_I), - @(kVK_ANSI_J), - @(kVK_ANSI_K), - @(kVK_ANSI_Keypad0), - @(kVK_ANSI_Keypad1), - @(kVK_ANSI_Keypad2), - @(kVK_ANSI_Keypad3), - @(kVK_ANSI_Keypad4), - @(kVK_ANSI_Keypad5), - @(kVK_ANSI_Keypad6), - @(kVK_ANSI_Keypad7), - @(kVK_ANSI_Keypad8), - @(kVK_ANSI_Keypad9), - @(kVK_ANSI_KeypadDecimal), - @(kVK_ANSI_KeypadDivide), - @(kVK_ANSI_KeypadEnter), - @(kVK_ANSI_KeypadEquals), - @(kVK_ANSI_KeypadMinus), - @(kVK_ANSI_KeypadMultiply), - @(kVK_ANSI_KeypadPlus), - @(kVK_ANSI_L), - @(kVK_ANSI_LeftBracket), - @(kVK_ANSI_M), - @(kVK_ANSI_Minus), - @(kVK_ANSI_N), - @(kVK_ANSI_O), - @(kVK_ANSI_P), - @(kVK_ANSI_Period), - @(kVK_ANSI_Q), - @(kVK_ANSI_Quote), - @(kVK_ANSI_R), - @(kVK_ANSI_RightBracket), - @(kVK_ANSI_S), - @(kVK_ANSI_Semicolon), - @(kVK_ANSI_Slash), - @(kVK_ANSI_T), - @(kVK_ANSI_U), - @(kVK_ANSI_V), - @(kVK_ANSI_W), - @(kVK_ANSI_X), - @(kVK_ANSI_Y), - @(kVK_ANSI_Z), - @(kVK_Delete), - @(kVK_DownArrow), - @(kVK_End), - @(kVK_Escape), - @(kVK_F1), - @(kVK_F2), - @(kVK_F3), - @(kVK_F4), - @(kVK_F5), - @(kVK_F6), - @(kVK_F7), - @(kVK_F8), - @(kVK_F9), - @(kVK_F10), - @(kVK_F11), - @(kVK_F12), - @(kVK_F13), - @(kVK_F14), - @(kVK_F15), - @(kVK_F16), - @(kVK_F17), - @(kVK_F18), - @(kVK_F19), - @(kVK_F20), - @(kVK_ForwardDelete), - @(kVK_Help), - @(kVK_Home), - @(kVK_ISO_Section), - @(kVK_JIS_KeypadComma), - @(kVK_JIS_Underscore), - @(kVK_JIS_Yen), - @(kVK_LeftArrow), - @(kVK_PageDown), - @(kVK_PageUp), - @(kVK_Return), - @(kVK_RightArrow), - @(kVK_Space), - @(kVK_Tab), - @(kVK_UpArrow) + @(SRKeyCode0), + @(SRKeyCode1), + @(SRKeyCode2), + @(SRKeyCode3), + @(SRKeyCode4), + @(SRKeyCode5), + @(SRKeyCode6), + @(SRKeyCode7), + @(SRKeyCode8), + @(SRKeyCode9), + @(SRKeyCodeA), + @(SRKeyCodeB), + @(SRKeyCodeBackslash), + @(SRKeyCodeC), + @(SRKeyCodeComma), + @(SRKeyCodeD), + @(SRKeyCodeE), + @(SRKeyCodeEqual), + @(SRKeyCodeF), + @(SRKeyCodeG), + @(SRKeyCodeGrave), + @(SRKeyCodeH), + @(SRKeyCodeI), + @(SRKeyCodeJ), + @(SRKeyCodeK), + @(SRKeyCodeKeypad0), + @(SRKeyCodeKeypad1), + @(SRKeyCodeKeypad2), + @(SRKeyCodeKeypad3), + @(SRKeyCodeKeypad4), + @(SRKeyCodeKeypad5), + @(SRKeyCodeKeypad6), + @(SRKeyCodeKeypad7), + @(SRKeyCodeKeypad8), + @(SRKeyCodeKeypad9), + @(SRKeyCodeKeypadDecimal), + @(SRKeyCodeKeypadDivide), + @(SRKeyCodeKeypadEnter), + @(SRKeyCodeKeypadEquals), + @(SRKeyCodeKeypadMinus), + @(SRKeyCodeKeypadMultiply), + @(SRKeyCodeKeypadPlus), + @(SRKeyCodeL), + @(SRKeyCodeLeftBracket), + @(SRKeyCodeM), + @(SRKeyCodeMinus), + @(SRKeyCodeN), + @(SRKeyCodeO), + @(SRKeyCodeP), + @(SRKeyCodePeriod), + @(SRKeyCodeQ), + @(SRKeyCodeQuote), + @(SRKeyCodeR), + @(SRKeyCodeRightBracket), + @(SRKeyCodeS), + @(SRKeyCodeSemicolon), + @(SRKeyCodeSlash), + @(SRKeyCodeT), + @(SRKeyCodeU), + @(SRKeyCodeV), + @(SRKeyCodeW), + @(SRKeyCodeX), + @(SRKeyCodeY), + @(SRKeyCodeZ), + @(SRKeyCodeDelete), + @(SRKeyCodeDownArrow), + @(SRKeyCodeEnd), + @(SRKeyCodeEscape), + @(SRKeyCodeF1), + @(SRKeyCodeF2), + @(SRKeyCodeF3), + @(SRKeyCodeF4), + @(SRKeyCodeF5), + @(SRKeyCodeF6), + @(SRKeyCodeF7), + @(SRKeyCodeF8), + @(SRKeyCodeF9), + @(SRKeyCodeF10), + @(SRKeyCodeF11), + @(SRKeyCodeF12), + @(SRKeyCodeF13), + @(SRKeyCodeF14), + @(SRKeyCodeF15), + @(SRKeyCodeF16), + @(SRKeyCodeF17), + @(SRKeyCodeF18), + @(SRKeyCodeF19), + @(SRKeyCodeF20), + @(SRKeyCodeForwardDelete), + @(SRKeyCodeHelp), + @(SRKeyCodeHome), + @(SRKeyCodeISOSection), + @(SRKeyCodeJISKeypadComma), + @(SRKeyCodeJISUnderscore), + @(SRKeyCodeJISYen), + @(SRKeyCodeLeftArrow), + @(SRKeyCodePageDown), + @(SRKeyCodePageUp), + @(SRKeyCodeReturn), + @(SRKeyCodeRightArrow), + @(SRKeyCodeSpace), + @(SRKeyCodeTab), + @(SRKeyCodeUpArrow) ]; }); @@ -494,96 +498,97 @@ - (id)inputSource #pragma mark Methods -- (NSString *)literalForKeyCode:(unsigned short)aValue +- (NSString *)literalForKeyCode:(SRKeyCode)aValue withImplicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags layoutDirection:(NSUserInterfaceLayoutDirection)aDirection { - switch (aValue) { - case kVK_F1: + switch (aValue) + { + case SRKeyCodeF1: return @"F1"; - case kVK_F2: + case SRKeyCodeF2: return @"F2"; - case kVK_F3: + case SRKeyCodeF3: return @"F3"; - case kVK_F4: + case SRKeyCodeF4: return @"F4"; - case kVK_F5: + case SRKeyCodeF5: return @"F5"; - case kVK_F6: + case SRKeyCodeF6: return @"F6"; - case kVK_F7: + case SRKeyCodeF7: return @"F7"; - case kVK_F8: + case SRKeyCodeF8: return @"F8"; - case kVK_F9: + case SRKeyCodeF9: return @"F9"; - case kVK_F10: + case SRKeyCodeF10: return @"F10"; - case kVK_F11: + case SRKeyCodeF11: return @"F11"; - case kVK_F12: + case SRKeyCodeF12: return @"F12"; - case kVK_F13: + case SRKeyCodeF13: return @"F13"; - case kVK_F14: + case SRKeyCodeF14: return @"F14"; - case kVK_F15: + case SRKeyCodeF15: return @"F15"; - case kVK_F16: + case SRKeyCodeF16: return @"F16"; - case kVK_F17: + case SRKeyCodeF17: return @"F17"; - case kVK_F18: + case SRKeyCodeF18: return @"F18"; - case kVK_F19: + case SRKeyCodeF19: return @"F19"; - case kVK_F20: + case SRKeyCodeF20: return @"F20"; - case kVK_Space: + case SRKeyCodeSpace: return SRLoc(@"Space"); - case kVK_Delete: + case SRKeyCodeDelete: return aDirection == NSUserInterfaceLayoutDirectionRightToLeft ? SRKeyCodeStringDeleteRight : SRKeyCodeStringDeleteLeft; - case kVK_ForwardDelete: + case SRKeyCodeForwardDelete: return aDirection == NSUserInterfaceLayoutDirectionRightToLeft ? SRKeyCodeStringDeleteLeft : SRKeyCodeStringDeleteRight; - case kVK_ANSI_KeypadClear: + case SRKeyCodeKeypadClear: return SRKeyCodeStringPadClear; - case kVK_LeftArrow: + case SRKeyCodeLeftArrow: return SRKeyCodeStringLeftArrow; - case kVK_RightArrow: + case SRKeyCodeRightArrow: return SRKeyCodeStringRightArrow; - case kVK_UpArrow: + case SRKeyCodeUpArrow: return SRKeyCodeStringUpArrow; - case kVK_DownArrow: + case SRKeyCodeDownArrow: return SRKeyCodeStringDownArrow; - case kVK_End: + case SRKeyCodeEnd: return SRKeyCodeStringSoutheastArrow; - case kVK_Home: + case SRKeyCodeHome: return SRKeyCodeStringNorthwestArrow; - case kVK_Escape: + case SRKeyCodeEscape: return SRKeyCodeStringEscape; - case kVK_PageDown: + case SRKeyCodePageDown: return SRKeyCodeStringPageDown; - case kVK_PageUp: + case SRKeyCodePageUp: return SRKeyCodeStringPageUp; - case kVK_Return: + case SRKeyCodeReturn: return SRKeyCodeStringReturnR2L; - case kVK_ANSI_KeypadEnter: + case SRKeyCodeKeypadEnter: return SRKeyCodeStringReturn; - case kVK_Tab: + case SRKeyCodeTab: { if (anImplicitModifierFlags & NSEventModifierFlagShift) return aDirection == NSUserInterfaceLayoutDirectionRightToLeft ? SRKeyCodeStringTabRight : SRKeyCodeStringTabLeft; else return aDirection == NSUserInterfaceLayoutDirectionRightToLeft ? SRKeyCodeStringTabLeft : SRKeyCodeStringTabRight; } - case kVK_Help: + case SRKeyCodeHelp: return SRKeyCodeStringHelp; - case kVK_JIS_Underscore: + case SRKeyCodeJISUnderscore: return SRKeyCodeStringJISUnderscore; - case kVK_JIS_KeypadComma: + case SRKeyCodeJISKeypadComma: return SRKeyCodeStringJISComma; - case kVK_JIS_Yen: + case SRKeyCodeJISYen: return SRKeyCodeStringJISYen; default: return [_translator translateKeyCode:aValue @@ -593,91 +598,92 @@ - (NSString *)literalForKeyCode:(unsigned short)aValue } } -- (NSString *)symbolForKeyCode:(unsigned short)aValue +- (NSString *)symbolForKeyCode:(SRKeyCode)aValue withImplicitModifierFlags:(NSEventModifierFlags)anImplicitModifierFlags explicitModifierFlags:(NSEventModifierFlags)anExplicitModifierFlags layoutDirection:(NSUserInterfaceLayoutDirection)aDirection { - switch (aValue) { - case kVK_F1: + switch (aValue) + { + case SRKeyCodeF1: return SRUnicharToString(NSF1FunctionKey); - case kVK_F2: + case SRKeyCodeF2: return SRUnicharToString(NSF2FunctionKey); - case kVK_F3: + case SRKeyCodeF3: return SRUnicharToString(NSF3FunctionKey); - case kVK_F4: + case SRKeyCodeF4: return SRUnicharToString(NSF4FunctionKey); - case kVK_F5: + case SRKeyCodeF5: return SRUnicharToString(NSF5FunctionKey); - case kVK_F6: + case SRKeyCodeF6: return SRUnicharToString(NSF6FunctionKey); - case kVK_F7: + case SRKeyCodeF7: return SRUnicharToString(NSF7FunctionKey); - case kVK_F8: + case SRKeyCodeF8: return SRUnicharToString(NSF8FunctionKey); - case kVK_F9: + case SRKeyCodeF9: return SRUnicharToString(NSF9FunctionKey); - case kVK_F10: + case SRKeyCodeF10: return SRUnicharToString(NSF10FunctionKey); - case kVK_F11: + case SRKeyCodeF11: return SRUnicharToString(NSF11FunctionKey); - case kVK_F12: + case SRKeyCodeF12: return SRUnicharToString(NSF12FunctionKey); - case kVK_F13: + case SRKeyCodeF13: return SRUnicharToString(NSF13FunctionKey); - case kVK_F14: + case SRKeyCodeF14: return SRUnicharToString(NSF14FunctionKey); - case kVK_F15: + case SRKeyCodeF15: return SRUnicharToString(NSF15FunctionKey); - case kVK_F16: + case SRKeyCodeF16: return SRUnicharToString(NSF16FunctionKey); - case kVK_F17: + case SRKeyCodeF17: return SRUnicharToString(NSF17FunctionKey); - case kVK_F18: + case SRKeyCodeF18: return SRUnicharToString(NSF18FunctionKey); - case kVK_F19: + case SRKeyCodeF19: return SRUnicharToString(NSF19FunctionKey); - case kVK_F20: + case SRKeyCodeF20: return SRUnicharToString(NSF20FunctionKey); - case kVK_Space: + case SRKeyCodeSpace: return SRUnicharToString(' '); - case kVK_Delete: + case SRKeyCodeDelete: return SRUnicharToString(NSBackspaceCharacter); - case kVK_ForwardDelete: + case SRKeyCodeForwardDelete: return SRUnicharToString(NSDeleteCharacter); - case kVK_ANSI_KeypadClear: + case SRKeyCodeKeypadClear: return SRUnicharToString(NSClearLineFunctionKey); - case kVK_LeftArrow: + case SRKeyCodeLeftArrow: return SRUnicharToString(NSLeftArrowFunctionKey); - case kVK_RightArrow: + case SRKeyCodeRightArrow: return SRUnicharToString(NSRightArrowFunctionKey); - case kVK_UpArrow: + case SRKeyCodeUpArrow: return SRUnicharToString(NSUpArrowFunctionKey); - case kVK_DownArrow: + case SRKeyCodeDownArrow: return SRUnicharToString(NSDownArrowFunctionKey); - case kVK_End: + case SRKeyCodeEnd: return SRUnicharToString(NSEndFunctionKey); - case kVK_Home: + case SRKeyCodeHome: return SRUnicharToString(NSHomeFunctionKey); - case kVK_Escape: + case SRKeyCodeEscape: return SRUnicharToString('\e'); - case kVK_PageDown: + case SRKeyCodePageDown: return SRUnicharToString(NSPageDownFunctionKey); - case kVK_PageUp: + case SRKeyCodePageUp: return SRUnicharToString(NSPageUpFunctionKey); - case kVK_Return: + case SRKeyCodeReturn: return SRUnicharToString(NSCarriageReturnCharacter); - case kVK_ANSI_KeypadEnter: + case SRKeyCodeKeypadEnter: return SRUnicharToString(NSEnterCharacter); - case kVK_Tab: + case SRKeyCodeTab: return SRUnicharToString(NSTabCharacter); - case kVK_Help: + case SRKeyCodeHelp: return SRUnicharToString(NSHelpFunctionKey); - case kVK_JIS_Underscore: + case SRKeyCodeJISUnderscore: return SRKeyCodeStringJISUnderscore; - case kVK_JIS_KeypadComma: + case SRKeyCodeJISKeypadComma: return SRKeyCodeStringJISComma; - case kVK_JIS_Yen: + case SRKeyCodeJISYen: return SRKeyCodeStringJISYen; default: return [_translator translateKeyCode:aValue @@ -723,43 +729,43 @@ + (SRKeyCodeTransformer *)sharedPlainASCIITransformer static dispatch_once_t OnceToken; dispatch_once(&OnceToken, ^{ Mapping = @{ - @(kVK_F1): SRUnicharToString(NSF1FunctionKey), - @(kVK_F2): SRUnicharToString(NSF2FunctionKey), - @(kVK_F3): SRUnicharToString(NSF3FunctionKey), - @(kVK_F4): SRUnicharToString(NSF4FunctionKey), - @(kVK_F5): SRUnicharToString(NSF5FunctionKey), - @(kVK_F6): SRUnicharToString(NSF6FunctionKey), - @(kVK_F7): SRUnicharToString(NSF7FunctionKey), - @(kVK_F8): SRUnicharToString(NSF8FunctionKey), - @(kVK_F9): SRUnicharToString(NSF9FunctionKey), - @(kVK_F10): SRUnicharToString(NSF10FunctionKey), - @(kVK_F11): SRUnicharToString(NSF11FunctionKey), - @(kVK_F12): SRUnicharToString(NSF12FunctionKey), - @(kVK_F13): SRUnicharToString(NSF13FunctionKey), - @(kVK_F14): SRUnicharToString(NSF14FunctionKey), - @(kVK_F15): SRUnicharToString(NSF15FunctionKey), - @(kVK_F16): SRUnicharToString(NSF16FunctionKey), - @(kVK_F17): SRUnicharToString(NSF17FunctionKey), - @(kVK_F18): SRUnicharToString(NSF18FunctionKey), - @(kVK_F19): SRUnicharToString(NSF19FunctionKey), - @(kVK_F20): SRUnicharToString(NSF20FunctionKey), - @(kVK_Space): SRUnicharToString(' '), - @(kVK_Delete): SRUnicharToString(NSBackspaceCharacter), - @(kVK_ForwardDelete): SRUnicharToString(NSDeleteCharacter), - @(kVK_ANSI_KeypadClear): SRUnicharToString(NSClearLineFunctionKey), - @(kVK_LeftArrow): SRUnicharToString(NSLeftArrowFunctionKey), - @(kVK_RightArrow): SRUnicharToString(NSRightArrowFunctionKey), - @(kVK_UpArrow): SRUnicharToString(NSUpArrowFunctionKey), - @(kVK_DownArrow): SRUnicharToString(NSDownArrowFunctionKey), - @(kVK_End): SRUnicharToString(NSEndFunctionKey), - @(kVK_Home): SRUnicharToString(NSHomeFunctionKey), - @(kVK_Escape): SRUnicharToString('\e'), - @(kVK_PageDown): SRUnicharToString(NSPageDownFunctionKey), - @(kVK_PageUp): SRUnicharToString(NSPageUpFunctionKey), - @(kVK_Return): SRUnicharToString(NSCarriageReturnCharacter), - @(kVK_ANSI_KeypadEnter): SRUnicharToString(NSEnterCharacter), - @(kVK_Tab): SRUnicharToString(NSTabCharacter), - @(kVK_Help): SRUnicharToString(NSHelpFunctionKey) + @(SRKeyCodeF1): SRUnicharToString(NSF1FunctionKey), + @(SRKeyCodeF2): SRUnicharToString(NSF2FunctionKey), + @(SRKeyCodeF3): SRUnicharToString(NSF3FunctionKey), + @(SRKeyCodeF4): SRUnicharToString(NSF4FunctionKey), + @(SRKeyCodeF5): SRUnicharToString(NSF5FunctionKey), + @(SRKeyCodeF6): SRUnicharToString(NSF6FunctionKey), + @(SRKeyCodeF7): SRUnicharToString(NSF7FunctionKey), + @(SRKeyCodeF8): SRUnicharToString(NSF8FunctionKey), + @(SRKeyCodeF9): SRUnicharToString(NSF9FunctionKey), + @(SRKeyCodeF10): SRUnicharToString(NSF10FunctionKey), + @(SRKeyCodeF11): SRUnicharToString(NSF11FunctionKey), + @(SRKeyCodeF12): SRUnicharToString(NSF12FunctionKey), + @(SRKeyCodeF13): SRUnicharToString(NSF13FunctionKey), + @(SRKeyCodeF14): SRUnicharToString(NSF14FunctionKey), + @(SRKeyCodeF15): SRUnicharToString(NSF15FunctionKey), + @(SRKeyCodeF16): SRUnicharToString(NSF16FunctionKey), + @(SRKeyCodeF17): SRUnicharToString(NSF17FunctionKey), + @(SRKeyCodeF18): SRUnicharToString(NSF18FunctionKey), + @(SRKeyCodeF19): SRUnicharToString(NSF19FunctionKey), + @(SRKeyCodeF20): SRUnicharToString(NSF20FunctionKey), + @(SRKeyCodeSpace): SRUnicharToString(' '), + @(SRKeyCodeDelete): SRUnicharToString(NSBackspaceCharacter), + @(SRKeyCodeForwardDelete): SRUnicharToString(NSDeleteCharacter), + @(SRKeyCodeKeypadClear): SRUnicharToString(NSClearLineFunctionKey), + @(SRKeyCodeLeftArrow): SRUnicharToString(NSLeftArrowFunctionKey), + @(SRKeyCodeRightArrow): SRUnicharToString(NSRightArrowFunctionKey), + @(SRKeyCodeUpArrow): SRUnicharToString(NSUpArrowFunctionKey), + @(SRKeyCodeDownArrow): SRUnicharToString(NSDownArrowFunctionKey), + @(SRKeyCodeEnd): SRUnicharToString(NSEndFunctionKey), + @(SRKeyCodeHome): SRUnicharToString(NSHomeFunctionKey), + @(SRKeyCodeEscape): SRUnicharToString('\e'), + @(SRKeyCodePageDown): SRUnicharToString(NSPageDownFunctionKey), + @(SRKeyCodePageUp): SRUnicharToString(NSPageUpFunctionKey), + @(SRKeyCodeReturn): SRUnicharToString(NSCarriageReturnCharacter), + @(SRKeyCodeKeypadEnter): SRUnicharToString(NSEnterCharacter), + @(SRKeyCodeTab): SRUnicharToString(NSTabCharacter), + @(SRKeyCodeHelp): SRUnicharToString(NSHelpFunctionKey) }; }); return Mapping; @@ -771,43 +777,43 @@ + (SRKeyCodeTransformer *)sharedPlainASCIITransformer static dispatch_once_t OnceToken; dispatch_once(&OnceToken, ^{ Mapping = @{ - @(kVK_F1): @"F1", - @(kVK_F2): @"F2", - @(kVK_F3): @"F3", - @(kVK_F4): @"F4", - @(kVK_F5): @"F5", - @(kVK_F6): @"F6", - @(kVK_F7): @"F7", - @(kVK_F8): @"F8", - @(kVK_F9): @"F9", - @(kVK_F10): @"F10", - @(kVK_F11): @"F11", - @(kVK_F12): @"F12", - @(kVK_F13): @"F13", - @(kVK_F14): @"F14", - @(kVK_F15): @"F15", - @(kVK_F16): @"F16", - @(kVK_F17): @"F17", - @(kVK_F18): @"F18", - @(kVK_F19): @"F19", - @(kVK_F20): @"F20", - @(kVK_Space): SRLoc(@"Space"), - @(kVK_Delete): SRKeyCodeStringDeleteLeft, - @(kVK_ForwardDelete): SRKeyCodeStringDeleteRight, - @(kVK_ANSI_KeypadClear): SRKeyCodeStringPadClear, - @(kVK_LeftArrow): SRKeyCodeStringLeftArrow, - @(kVK_RightArrow): SRKeyCodeStringRightArrow, - @(kVK_UpArrow): SRKeyCodeStringUpArrow, - @(kVK_DownArrow): SRKeyCodeStringDownArrow, - @(kVK_End): SRKeyCodeStringSoutheastArrow, - @(kVK_Home): SRKeyCodeStringNorthwestArrow, - @(kVK_Escape): SRKeyCodeStringEscape, - @(kVK_PageDown): SRKeyCodeStringPageDown, - @(kVK_PageUp): SRKeyCodeStringPageUp, - @(kVK_Return): SRKeyCodeStringReturnR2L, - @(kVK_ANSI_KeypadEnter): SRKeyCodeStringReturn, - @(kVK_Tab): SRKeyCodeStringTabRight, - @(kVK_Help): @"?⃝" + @(SRKeyCodeF1): @"F1", + @(SRKeyCodeF2): @"F2", + @(SRKeyCodeF3): @"F3", + @(SRKeyCodeF4): @"F4", + @(SRKeyCodeF5): @"F5", + @(SRKeyCodeF6): @"F6", + @(SRKeyCodeF7): @"F7", + @(SRKeyCodeF8): @"F8", + @(SRKeyCodeF9): @"F9", + @(SRKeyCodeF10): @"F10", + @(SRKeyCodeF11): @"F11", + @(SRKeyCodeF12): @"F12", + @(SRKeyCodeF13): @"F13", + @(SRKeyCodeF14): @"F14", + @(SRKeyCodeF15): @"F15", + @(SRKeyCodeF16): @"F16", + @(SRKeyCodeF17): @"F17", + @(SRKeyCodeF18): @"F18", + @(SRKeyCodeF19): @"F19", + @(SRKeyCodeF20): @"F20", + @(SRKeyCodeSpace): SRLoc(@"Space"), + @(SRKeyCodeDelete): SRKeyCodeStringDeleteLeft, + @(SRKeyCodeForwardDelete): SRKeyCodeStringDeleteRight, + @(SRKeyCodeKeypadClear): SRKeyCodeStringPadClear, + @(SRKeyCodeLeftArrow): SRKeyCodeStringLeftArrow, + @(SRKeyCodeRightArrow): SRKeyCodeStringRightArrow, + @(SRKeyCodeUpArrow): SRKeyCodeStringUpArrow, + @(SRKeyCodeDownArrow): SRKeyCodeStringDownArrow, + @(SRKeyCodeEnd): SRKeyCodeStringSoutheastArrow, + @(SRKeyCodeHome): SRKeyCodeStringNorthwestArrow, + @(SRKeyCodeEscape): SRKeyCodeStringEscape, + @(SRKeyCodePageDown): SRKeyCodeStringPageDown, + @(SRKeyCodePageUp): SRKeyCodeStringPageUp, + @(SRKeyCodeReturn): SRKeyCodeStringReturnR2L, + @(SRKeyCodeKeypadEnter): SRKeyCodeStringReturn, + @(SRKeyCodeTab): SRKeyCodeStringTabRight, + @(SRKeyCodeHelp): @"?⃝" }; }); return Mapping; @@ -862,7 +868,7 @@ - (NSString *)transformedSpecialKeyCode:(NSNumber *)aKeyCode layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight]; } -- (BOOL)isKeyCodeSpecial:(unsigned short)aKeyCode +- (BOOL)isKeyCodeSpecial:(SRKeyCode)aKeyCode { return self.class.specialKeyCodeToSymbolMapping[@(aKeyCode)] != nil; } @@ -1076,103 +1082,103 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue { case SRKeyCodeGlyphTabRight: case SRKeyCodeGlyphTabLeft: - result = @(kVK_Tab); + result = @(SRKeyCodeTab); break; case SRKeyCodeGlyphReturn: - result = @(kVK_ANSI_KeypadEnter); + result = @(SRKeyCodeKeypadEnter); break; case SRKeyCodeGlyphReturnR2L: - result = @(kVK_Return); + result = @(SRKeyCodeReturn); break; case SRKeyCodeGlyphDeleteLeft: - result = @(kVK_Delete); + result = @(SRKeyCodeDelete); break; case SRKeyCodeGlyphDeleteRight: - result = @(kVK_ForwardDelete); + result = @(SRKeyCodeForwardDelete); break; case SRKeyCodeGlyphPadClear: - result = @(kVK_ANSI_KeypadClear); + result = @(SRKeyCodeKeypadClear); break; case SRKeyCodeGlyphLeftArrow: - result = @(kVK_LeftArrow); + result = @(SRKeyCodeLeftArrow); break; case SRKeyCodeGlyphRightArrow: - result = @(kVK_RightArrow); + result = @(SRKeyCodeRightArrow); break; case SRKeyCodeGlyphUpArrow: - result = @(kVK_UpArrow); + result = @(SRKeyCodeUpArrow); break; case SRKeyCodeGlyphDownArrow: - result = @(kVK_DownArrow); + result = @(SRKeyCodeDownArrow); break; case SRKeyCodeGlyphPageDown: - result = @(kVK_PageDown); + result = @(SRKeyCodePageDown); break; case SRKeyCodeGlyphPageUp: - result = @(kVK_PageUp); + result = @(SRKeyCodePageUp); break; case SRKeyCodeGlyphNorthwestArrow: - result = @(kVK_Home); + result = @(SRKeyCodeHome); break; case SRKeyCodeGlyphSoutheastArrow: - result = @(kVK_End); + result = @(SRKeyCodeEnd); break; case SRKeyCodeGlyphEscape: - result = @(kVK_Escape); + result = @(SRKeyCodeEscape); break; case SRKeyCodeGlyphSpace: - result = @(kVK_Space); + result = @(SRKeyCodeSpace); break; case SRKeyCodeGlyphJISUnderscore: - result = @(kVK_JIS_Underscore); + result = @(SRKeyCodeJISUnderscore); break; case SRKeyCodeGlyphJISComma: - result = @(kVK_JIS_KeypadComma); + result = @(SRKeyCodeJISKeypadComma); break; case SRKeyCodeGlyphJISYen: - result = @(kVK_JIS_Yen); + result = @(SRKeyCodeJISYen); break; case SRKeyCodeGlyphANSI0: - result = @(kVK_ANSI_0); + result = @(SRKeyCode0); break; case SRKeyCodeGlyphANSI1: - result = @(kVK_ANSI_1); + result = @(SRKeyCode1); break; case SRKeyCodeGlyphANSI2: - result = @(kVK_ANSI_2); + result = @(SRKeyCode2); break; case SRKeyCodeGlyphANSI3: - result = @(kVK_ANSI_3); + result = @(SRKeyCode3); break; case SRKeyCodeGlyphANSI4: - result = @(kVK_ANSI_4); + result = @(SRKeyCode4); break; case SRKeyCodeGlyphANSI5: - result = @(kVK_ANSI_5); + result = @(SRKeyCode5); break; case SRKeyCodeGlyphANSI6: - result = @(kVK_ANSI_6); + result = @(SRKeyCode6); break; case SRKeyCodeGlyphANSI7: - result = @(kVK_ANSI_7); + result = @(SRKeyCode7); break; case SRKeyCodeGlyphANSI8: - result = @(kVK_ANSI_8); + result = @(SRKeyCode8); break; case SRKeyCodeGlyphANSI9: - result = @(kVK_ANSI_9); + result = @(SRKeyCode9); break; case SRKeyCodeGlyphANSIEqual: - result = @(kVK_ANSI_Equal); + result = @(SRKeyCodeEqual); break; case SRKeyCodeGlyphANSIMinus: - result = @(kVK_ANSI_Minus); + result = @(SRKeyCodeMinus); break; case SRKeyCodeGlyphANSISlash: - result = @(kVK_ANSI_Slash); + result = @(SRKeyCodeSlash); break; case SRKeyCodeGlyphANSIPeriod: - result = @(kVK_ANSI_Period); + result = @(SRKeyCodePeriod); break; default: break; @@ -1186,64 +1192,64 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue switch (fNumber) { case 1: - result = @(kVK_F1); + result = @(SRKeyCodeF1); break; case 2: - result = @(kVK_F2); + result = @(SRKeyCodeF2); break; case 3: - result = @(kVK_F3); + result = @(SRKeyCodeF3); break; case 4: - result = @(kVK_F4); + result = @(SRKeyCodeF4); break; case 5: - result = @(kVK_F5); + result = @(SRKeyCodeF5); break; case 6: - result = @(kVK_F6); + result = @(SRKeyCodeF6); break; case 7: - result = @(kVK_F7); + result = @(SRKeyCodeF7); break; case 8: - result = @(kVK_F8); + result = @(SRKeyCodeF8); break; case 9: - result = @(kVK_F9); + result = @(SRKeyCodeF9); break; case 10: - result = @(kVK_F10); + result = @(SRKeyCodeF10); break; case 11: - result = @(kVK_F11); + result = @(SRKeyCodeF11); break; case 12: - result = @(kVK_F12); + result = @(SRKeyCodeF12); break; case 13: - result = @(kVK_F13); + result = @(SRKeyCodeF13); break; case 14: - result = @(kVK_F14); + result = @(SRKeyCodeF14); break; case 15: - result = @(kVK_F15); + result = @(SRKeyCodeF15); break; case 16: - result = @(kVK_F16); + result = @(SRKeyCodeF16); break; case 17: - result = @(kVK_F17); + result = @(SRKeyCodeF17); break; case 18: - result = @(kVK_F18); + result = @(SRKeyCodeF18); break; case 19: - result = @(kVK_F19); + result = @(SRKeyCodeF19); break; case 20: - result = @(kVK_F20); + result = @(SRKeyCodeF20); break; default: break; @@ -1255,16 +1261,16 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue if ([lowercaseValue caseInsensitiveCompare:SRLoc(@"Space")] == NSOrderedSame || [lowercaseValue isEqualToString:@"space"]) { - result = @(kVK_Space); + result = @(SRKeyCodeSpace); } else if ([lowercaseValue isEqualToString:@"esc"] || [lowercaseValue isEqualToString:@"escape"]) - result = @(kVK_Escape); + result = @(SRKeyCodeEscape); else if ([lowercaseValue isEqualToString:@"tab"]) - result = @(kVK_Tab); + result = @(SRKeyCodeTab); else if ([lowercaseValue isEqualToString:@"help"] || [lowercaseValue isEqualToString:@"?⃝"]) - result = @(kVK_Help); + result = @(SRKeyCodeHelp); else if ([lowercaseValue isEqualToString:@"enter"]) - result = @(kVK_Return); + result = @(SRKeyCodeReturn); } if (result == nil) @@ -1351,145 +1357,145 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue switch (glyph) { case NSF1FunctionKey: - result = @(kVK_F1); + result = @(SRKeyCodeF1); break; case NSF2FunctionKey: - result = @(kVK_F2); + result = @(SRKeyCodeF2); break; case NSF3FunctionKey: - result = @(kVK_F3); + result = @(SRKeyCodeF3); break; case NSF4FunctionKey: - result = @(kVK_F4); + result = @(SRKeyCodeF4); break; case NSF5FunctionKey: - result = @(kVK_F5); + result = @(SRKeyCodeF5); break; case NSF6FunctionKey: - result = @(kVK_F6); + result = @(SRKeyCodeF6); break; case NSF7FunctionKey: - result = @(kVK_F7); + result = @(SRKeyCodeF7); break; case NSF8FunctionKey: - result = @(kVK_F8); + result = @(SRKeyCodeF8); break; case NSF9FunctionKey: - result = @(kVK_F9); + result = @(SRKeyCodeF9); break; case NSF10FunctionKey: - result = @(kVK_F10); + result = @(SRKeyCodeF10); break; case NSF11FunctionKey: - result = @(kVK_F11); + result = @(SRKeyCodeF11); break; case NSF12FunctionKey: - result = @(kVK_F12); + result = @(SRKeyCodeF12); break; case NSF13FunctionKey: - result = @(kVK_F13); + result = @(SRKeyCodeF13); break; case NSF14FunctionKey: - result = @(kVK_F14); + result = @(SRKeyCodeF14); break; case NSF15FunctionKey: - result = @(kVK_F15); + result = @(SRKeyCodeF15); break; case NSF16FunctionKey: - result = @(kVK_F16); + result = @(SRKeyCodeF16); break; case NSF17FunctionKey: - result = @(kVK_F17); + result = @(SRKeyCodeF17); break; case NSF18FunctionKey: - result = @(kVK_F18); + result = @(SRKeyCodeF18); break; case NSF19FunctionKey: - result = @(kVK_F19); + result = @(SRKeyCodeF19); break; case NSF20FunctionKey: - result = @(kVK_F20); + result = @(SRKeyCodeF20); break; case NSUpArrowFunctionKey: - result = @(kVK_UpArrow); + result = @(SRKeyCodeUpArrow); break; case NSDownArrowFunctionKey: - result = @(kVK_DownArrow); + result = @(SRKeyCodeDownArrow); break; case NSLeftArrowFunctionKey: - result = @(kVK_LeftArrow); + result = @(SRKeyCodeLeftArrow); break; case NSRightArrowFunctionKey: - result = @(kVK_RightArrow); + result = @(SRKeyCodeRightArrow); break; case NSEndFunctionKey: - result = @(kVK_End); + result = @(SRKeyCodeEnd); break; case NSHelpFunctionKey: - result = @(kVK_Help); + result = @(SRKeyCodeHelp); break; case NSHomeFunctionKey: - result = @(kVK_Home); + result = @(SRKeyCodeHome); break; case NSPageDownFunctionKey: - result = @(kVK_PageDown); + result = @(SRKeyCodePageDown); break; case NSPageUpFunctionKey: - result = @(kVK_PageUp); + result = @(SRKeyCodePageUp); break; case NSBackTabCharacter: - result = @(kVK_Tab); + result = @(SRKeyCodeTab); break; case SRKeyCodeGlyphJISUnderscore: - result = @(kVK_JIS_Underscore); + result = @(SRKeyCodeJISUnderscore); break; case SRKeyCodeGlyphJISComma: - result = @(kVK_JIS_KeypadComma); + result = @(SRKeyCodeJISKeypadComma); break; case SRKeyCodeGlyphJISYen: - result = @(kVK_JIS_Yen); + result = @(SRKeyCodeJISYen); break; case SRKeyCodeGlyphANSI0: - result = @(kVK_ANSI_0); + result = @(SRKeyCode0); break; case SRKeyCodeGlyphANSI1: - result = @(kVK_ANSI_1); + result = @(SRKeyCode1); break; case SRKeyCodeGlyphANSI2: - result = @(kVK_ANSI_2); + result = @(SRKeyCode2); break; case SRKeyCodeGlyphANSI3: - result = @(kVK_ANSI_3); + result = @(SRKeyCode3); break; case SRKeyCodeGlyphANSI4: - result = @(kVK_ANSI_4); + result = @(SRKeyCode4); break; case SRKeyCodeGlyphANSI5: - result = @(kVK_ANSI_5); + result = @(SRKeyCode5); break; case SRKeyCodeGlyphANSI6: - result = @(kVK_ANSI_6); + result = @(SRKeyCode6); break; case SRKeyCodeGlyphANSI7: - result = @(kVK_ANSI_7); + result = @(SRKeyCode7); break; case SRKeyCodeGlyphANSI8: - result = @(kVK_ANSI_8); + result = @(SRKeyCode8); break; case SRKeyCodeGlyphANSI9: - result = @(kVK_ANSI_9); + result = @(SRKeyCode9); break; case SRKeyCodeGlyphANSIEqual: - result = @(kVK_ANSI_Equal); + result = @(SRKeyCodeEqual); break; case SRKeyCodeGlyphANSIMinus: - result = @(kVK_ANSI_Minus); + result = @(SRKeyCodeMinus); break; case SRKeyCodeGlyphANSISlash: - result = @(kVK_ANSI_Slash); + result = @(SRKeyCodeSlash); break; case SRKeyCodeGlyphANSIPeriod: - result = @(kVK_ANSI_Period); + result = @(SRKeyCodePeriod); break; default: result = [(_SRKeyCodeASCIITranslator *)self->_translator keyCodeForTranslation:aValue.lowercaseString]; diff --git a/Library/SRRecorderControl.h b/Library/SRRecorderControl.h index 50281c24..cad49e7a 100644 --- a/Library/SRRecorderControl.h +++ b/Library/SRRecorderControl.h @@ -13,6 +13,11 @@ NS_ASSUME_NONNULL_BEGIN +/*! + Priority assigned to the constraint that controls intrinsic label width. + */ +extern const NSLayoutPriority SRRecorderControlLabelWidthPriority NS_SWIFT_NAME(SRRecorderControl.LabelWidthPriority); + /*! SRRecorderControl is a control that can record keyboard shortcuts. @@ -39,9 +44,6 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(RecorderControl) IB_DESIGNABLE @interface SRRecorderControl : NSControl /* */ -{ - BOOL _isCompatibilityModeEnabled; -} /*! Called by a designated initializer to set up internal state. @@ -61,6 +63,8 @@ IB_DESIGNABLE Return an integer bit field indicating allowed modifier flags. @discussion Defaults to SRCocoaModifierFlagsMask. + + @see -setAllowedModifierFlags:requiredModifierFlags:allowsEmptyModifierFlags: */ @property (readonly) IBInspectable NSEventModifierFlags allowedModifierFlags; @@ -68,6 +72,8 @@ IB_DESIGNABLE Return an integer bit field indicating required modifier flags. @discussion Defaults to 0. + + @see -setAllowedModifierFlags:requiredModifierFlags:allowsEmptyModifierFlags: */ @property (readonly) IBInspectable NSEventModifierFlags requiredModifierFlags; @@ -75,6 +81,8 @@ IB_DESIGNABLE Whether shortcuts without modifier flags are allowed. @discussion Defaults to NO. + + @see -setAllowedModifierFlags:requiredModifierFlags:allowsEmptyModifierFlags: */ @property (readonly) IBInspectable BOOL allowsEmptyModifierFlags; @@ -126,10 +134,24 @@ IB_DESIGNABLE The control fully supports right-to-left layouts but macOS is inconsistent. Parts of the system draw key equivalents fully right-to-left, that is flags in reverse order and properly altered directional keys such as Tab. Yet other parts either support it only partially or not at all. + The most visible key equivalents, those that appear in NSMenuItem, do not respect right-to-left at all. */ @property IBInspectable BOOL stringValueRespectsUserInterfaceLayoutDirection; +/*! + Whether the control allows to record shortcuts without a key code. + + @discussion + Defaults to NO. + + When YES changes the control behavior to allow recording of a shortcut without a key code. + In this mode recording ends either when key code is pressed (as usual) or when all modifier flags are relased. + Instead of capturing only currently pressed modifier flags, the control XORs its internal value whenever + a modifier key is pressed. I.e. whenever a modifier key is pressed it either added or removed. + */ +@property IBInspectable BOOL allowsModifierFlagsOnlyShortcut; + /*! Configure allowed and required modifier flags for user interaction. @@ -150,36 +172,6 @@ IB_DESIGNABLE requiredModifierFlags:(NSEventModifierFlags)newRequiredModifierFlags allowsEmptyModifierFlags:(BOOL)newAllowsEmptyModifierFlags NS_SWIFT_NAME(set(allowedModifierFlags:requiredModifierFlags:allowsEmptyModifierFlags:)); -/*! - Check whether a given combination is valid. - - @param aModifierFlags Proposed modifier flags. - - @param aKeyCode Code of the pressed key. - - @seealso allowedModifierFlags - - @seealso allowsEmptyModifierFlags - - @seealso requiredModifierFlags - */ -- (BOOL)areModifierFlagsValid:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode; - -/*! - Check whether a given combination is allowed. - - @param aModifierFlags Proposed modifier flags. - - @param aKeyCode Code of the pressed key. - - @seealso allowedModifierFlags - - @seealso allowsEmptyModifierFlags - - @seealso requiredModifierFlags - */ -- (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode; - /*! Called whenever the control needs to inform a user about misuse, like pressing invalid modifier flags. @@ -226,6 +218,59 @@ IB_DESIGNABLE */ @property (getter=isClearButtonHighlighted, readonly) BOOL clearButtonHighlighted; +/*! + Check whether a given combination can be recorded. + + @discussion + Subclasses may override to provide custom verfication logic for a proposed shortcut. + + @param aModifierFlags Proposed modifier flags. + + @param aKeyCode Code of the pressed key. + + @seealso requiredModifierFlags + + @seealso -areModifierFlagsAllowed:forKeyCode: + */ +- (BOOL)areModifierFlagsValid:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode; + +/*! + Check whether given modifier flags are allowed by control's configuration and delegate. + + @discussion + Subclasses may override to provide custom verification logic for allowed modifier flags. + + @param aModifierFlags Proposed modifier flags. + + @param aKeyCode Code of the pressed key. + + @seealso allowedModifierFlags + + @seealso allowsEmptyModifierFlags + + @seealso -[SRRecorderControlDelegate recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:]; + */ +- (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode; + +/*! + Check whether the control is in a state to to capture key events. + + @discussion + To avoid "stray" events the control must be: + 1. Enabled + 2. The first responder + 3. Not tracking mouse events + */ +- (BOOL)canCaptureKeyEvent; + +/*! + Check if recording can be ended by capturing a given shortcut. + + @discussion + Recording must be in progress and other criteria such as being a first responder must be satisfied too. + */ +- (BOOL)canEndRecordingWithObjectValue:(nullable SRShortcut *)aShortcut; + /*! Called when a user begins recording. */ @@ -256,6 +301,8 @@ IB_DESIGNABLE enter compatibility mode where objectValue and NSValueBinding accessors will accept and return instances of NSDictionary. + To check whether the control has compatibility mode enabled use KVC with the "isCompatibilityModeEnabled" key. + @seealso SRShortcutKey */ @property (nullable, copy) SRShortcut *objectValue; @@ -341,6 +388,11 @@ IB_DESIGNABLE */ - (void)updateActiveConstraints; +/*! + Called when control's state changes in a way that may affect label's constraints. + */ +- (void)updateLabelConstraints; + /*! Schedules performSelector to notify style that view's appearance did change. @@ -387,13 +439,13 @@ NS_SWIFT_NAME(RecorderControlDelegate) @param aModifierFlags Proposed modifier flags. - @param aKeyCode Code of the pressed key. + @param aKeyCode Code of the pressed key, if any. @return YES if the control should ignore the rules; otherwise, NO. @discussion - Normally, you wouldn't allow a user to record a shourcut without modifier flags set: disallow 'a', but allow cmd-'a'. - However, some keys are designed to be key shortcuts by itself, e.g. functional keys. + Normally, you wouldn't allow a user to record a shortcut without modifier flags set. + However, some keys, like functional keys, are designed to be key shortcuts by itself. By implementing this method the delegate can allow these special keys to be set without modifier flags even when the control is configured to disallow empty modifier flags. @@ -403,7 +455,7 @@ NS_SWIFT_NAME(RecorderControlDelegate) @seealso requiredModifierFlags */ -- (BOOL)recorderControl:(SRRecorderControl *)aControl shouldUnconditionallyAllowModifierFlags:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode; +- (BOOL)recorderControl:(SRRecorderControl *)aControl shouldUnconditionallyAllowModifierFlags:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode; /*! Ask the delegate if the shortcut can be set. @@ -427,7 +479,7 @@ NS_SWIFT_NAME(RecorderControlDelegate) - (BOOL)shortcutRecorder:(SRRecorderControl *)aRecorder canRecordShortcut:(NSDictionary *)aShortcut __attribute__((deprecated("", "recorderControl:canRecordShortcut:"))); -- (BOOL)shortcutRecorder:(SRRecorderControl *)aRecorder shouldUnconditionallyAllowModifierFlags:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode __attribute__((deprecated("", "recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:"))); +- (BOOL)shortcutRecorder:(SRRecorderControl *)aRecorder shouldUnconditionallyAllowModifierFlags:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode __attribute__((deprecated("", "recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:"))); - (BOOL)shortcutRecorderShouldBeginRecording:(SRRecorderControl *)aRecorder __attribute__((deprecated("", "recorderControlShouldBeginRecording:"))); diff --git a/Library/SRRecorderControl.m b/Library/SRRecorderControl.m index 0f02441a..8ab5e6b9 100644 --- a/Library/SRRecorderControl.m +++ b/Library/SRRecorderControl.m @@ -14,6 +14,14 @@ #import "SRModifierFlagsTransformer.h" +#ifndef SR_DEBUG_DRAWING +#define SR_DEBUG_DRAWING DEBUG && 0 +#endif // SR_DEBUG_DRAWING + + +const NSLayoutPriority SRRecorderControlLabelWidthPriority = NSLayoutPriorityDefaultHigh + 1; + + typedef NS_ENUM(NSUInteger, _SRRecorderControlButtonTag) { _SRRecorderControlInvalidButtonTag = -1, @@ -23,8 +31,9 @@ typedef NS_ENUM(NSUInteger, _SRRecorderControlButtonTag) }; -static NSInteger _SRStyleUserInterfaceLayoutDirectionObservingContext; -static NSInteger _SRStyleAppearanceObservingContext; +static void *_SRStyleUserInterfaceLayoutDirectionObservingContext = &_SRStyleUserInterfaceLayoutDirectionObservingContext; +static void *_SRStyleAppearanceObservingContext = &_SRStyleAppearanceObservingContext; +static void *_SRStyleGuideObservingContext = &_SRStyleGuideObservingContext; #define _SRIfRespondsGet(obj, sel, default) [obj respondsToSelector:@selector(sel)] ? [obj sel] : (default) @@ -33,6 +42,8 @@ typedef NS_ENUM(NSUInteger, _SRRecorderControlButtonTag) @implementation SRRecorderControl { + BOOL _isCompatibilityModeEnabled; + SRRecorderControlStyle *_style; NSInvocation *_notifyStyle; @@ -48,10 +59,13 @@ @implementation SRRecorderControl // +NSEvent.modifierFlags may change across run loop calls // Extra care is needed to ensure that all methods will see the same flags. - NSEventModifierFlags _currentlyDrawnRecordingModifierFlags; - NSEventModifierFlags _accessibilityRecordingModifierFlags; + NSEventModifierFlags _lastSeenModifierFlags; BOOL _isLazilyInitializingStyle; + BOOL _didPauseGlobalShortcutMonitor; + + // Controls intrinsic width of the label. + NSLayoutConstraint *_labelWidthConstraint; } - (instancetype)initWithFrame:(NSRect)aFrameRect @@ -102,6 +116,27 @@ - (void)dealloc [NSNotificationCenter.defaultCenter removeObserver:self]; [NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self]; [NSObject cancelPreviousPerformRequestsWithTarget:_notifyStyle]; + + if ([_style respondsToSelector:@selector(preferredComponents)]) + { + [_style removeObserver:self + forKeyPath:@"preferredComponents.userInterfaceLayoutDirection" + context:_SRStyleUserInterfaceLayoutDirectionObservingContext]; + [_style removeObserver:self + forKeyPath:@"preferredComponents.appearance" + context:_SRStyleAppearanceObservingContext]; + } + + [_style removeObserver:self forKeyPath:@"labelDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(backgroundDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"backgroundDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"cancelButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"clearButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext]; } #pragma mark Properties @@ -226,6 +261,7 @@ - (void)setObjectValue:(SRShortcut *)newObjectValue NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification); NSAccessibilityPostNotification(self, NSAccessibilityValueChangedNotification); [self setNeedsDisplayInRect:self.style.labelDrawingGuide.frame]; + [self updateLabelConstraints]; } } @@ -285,25 +321,73 @@ - (void)_setStyle:(SRRecorderControlStyle *)newStyle if ([_style respondsToSelector:@selector(preferredComponents)]) { - [_style removeObserver:self forKeyPath:@"preferredComponents.userInterfaceLayoutDirection" context:&_SRStyleUserInterfaceLayoutDirectionObservingContext]; - [_style removeObserver:self forKeyPath:@"preferredComponents.appearance" context:&_SRStyleAppearanceObservingContext]; + [_style removeObserver:self + forKeyPath:@"preferredComponents.userInterfaceLayoutDirection" + context:_SRStyleUserInterfaceLayoutDirectionObservingContext]; + [_style removeObserver:self + forKeyPath:@"preferredComponents.appearance" + context:_SRStyleAppearanceObservingContext]; } + [_style removeObserver:self forKeyPath:@"labelDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(backgroundDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"backgroundDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"cancelButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)]) + [_style removeObserver:self forKeyPath:@"clearButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext]; + _style = newStyle; if ([_style respondsToSelector:@selector(prepareForRecorderControl:)]) [_style prepareForRecorderControl:self]; + _labelWidthConstraint = [_style.labelDrawingGuide.widthAnchor constraintEqualToConstant:0.0]; + _labelWidthConstraint.priority = NSLayoutPriorityDefaultHigh + 1; + _labelWidthConstraint.active = YES; + if ([_style respondsToSelector:@selector(preferredComponents)]) { [_style addObserver:self forKeyPath:@"preferredComponents.userInterfaceLayoutDirection" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior - context:&_SRStyleUserInterfaceLayoutDirectionObservingContext]; + context:_SRStyleUserInterfaceLayoutDirectionObservingContext]; [_style addObserver:self forKeyPath:@"preferredComponents.appearance" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:&_SRStyleAppearanceObservingContext]; + context:_SRStyleAppearanceObservingContext]; + } + + [_style addObserver:self + forKeyPath:@"labelDrawingGuide.frame" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:_SRStyleGuideObservingContext]; + + if ([_style respondsToSelector:@selector(backgroundDrawingGuide)]) + { + [_style addObserver:self + forKeyPath:@"backgroundDrawingGuide.frame" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:_SRStyleGuideObservingContext]; + } + + if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)]) + { + [_style addObserver:self + forKeyPath:@"cancelButtonDrawingGuide.frame" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:_SRStyleGuideObservingContext]; + } + + if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)]) + { + [_style addObserver:self + forKeyPath:@"clearButtonDrawingGuide.frame" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:_SRStyleGuideObservingContext]; } if (self.isRecording) @@ -388,12 +472,10 @@ - (NSString *)drawingLabel if (self.isRecording) { - _currentlyDrawnRecordingModifierFlags = NSEvent.modifierFlags & self.allowedModifierFlags; - - if (_currentlyDrawnRecordingModifierFlags) + if (_lastSeenModifierFlags) { __auto_type layoutDirection = self.stringValueRespectsUserInterfaceLayoutDirection ? self.userInterfaceLayoutDirection : NSUserInterfaceLayoutDirectionLeftToRight; - label = [SRSymbolicModifierFlagsTransformer.sharedTransformer transformedValue:@(_currentlyDrawnRecordingModifierFlags) + label = [SRSymbolicModifierFlagsTransformer.sharedTransformer transformedValue:@(_lastSeenModifierFlags) layoutDirection:layoutDirection]; } else @@ -449,7 +531,7 @@ - (BOOL)beginRecording }; #pragma clang diagnostic pop - os_activity_initiate("beginRecording", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRRecorderControl beginRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{ if (!self.enabled) { result = NO; @@ -478,18 +560,23 @@ - (BOOL)beginRecording return; } - self.needsDisplay = YES; + self->_lastSeenModifierFlags = NSEvent.modifierFlags & self.allowedModifierFlags; [self willChangeValueForKey:@"isRecording"]; self->_isRecording = YES; [self didChangeValueForKey:@"isRecording"]; + self.needsDisplay = YES; [self updateActiveConstraints]; + [self updateLabelConstraints]; [self updateTrackingAreas]; self.toolTip = _SRIfRespondsGet(self.style, recordingTooltip, SRLoc(@"Type shortcut")); if (self.pausesGlobalShortcutMonitorWhileRecording) + { + _didPauseGlobalShortcutMonitor = YES; [SRGlobalShortcutMonitor.sharedMonitor pause]; + } NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding]; if (bindingInfo) @@ -518,7 +605,7 @@ - (void)endRecording if (!self.isRecording) return; - os_activity_initiate("endRecording via cancel", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRRecorderControl endRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{ [self endRecordingWithObjectValue:self->_objectValue]; }); } @@ -528,7 +615,7 @@ - (void)clearAndEndRecording if (!self.isRecording) return; - os_activity_initiate("endRecording via clear", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRRecorderControl clearAndEndRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{ [self endRecordingWithObjectValue:nil]; }); } @@ -548,22 +635,25 @@ - (void)endRecordingWithObjectValue:(SRShortcut *)anObjectValue }; #pragma clang diagnostic pop - os_activity_initiate("endRecording explicitly", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{ + os_activity_initiate("-[SRRecorderControl endRecordingWithObjectValue:]", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{ [self willChangeValueForKey:@"isRecording"]; self->_isRecording = NO; [self didChangeValueForKey:@"isRecording"]; self.objectValue = anObjectValue; - self->_currentlyDrawnRecordingModifierFlags = 0; - self->_accessibilityRecordingModifierFlags = 0; + self->_lastSeenModifierFlags = 0; + self.needsDisplay = YES; [self updateActiveConstraints]; + [self updateLabelConstraints]; [self updateTrackingAreas]; self.toolTip = _SRIfRespondsGet(self.style, normalTooltip, SRLoc(@"Click to record shortcut")); - self.needsDisplay = YES; - if (self.pausesGlobalShortcutMonitorWhileRecording) + if (_didPauseGlobalShortcutMonitor) + { + _didPauseGlobalShortcutMonitor = NO; [SRGlobalShortcutMonitor.sharedMonitor resume]; + } NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding]; if (bindingInfo) @@ -611,6 +701,19 @@ - (void)updateActiveConstraints } } +- (void)updateLabelConstraints +{ + NSString *label = self.drawingLabel; + NSDictionary *labelAttributes = self.drawingLabelAttributes; + CGFloat labelWidth = NSWidth([label boundingRectWithSize:NSMakeSize(9999.0, self.intrinsicContentSize.height) + options:0 + attributes:labelAttributes + context:nil]); + // Extra 2 points to avoid clipping of smoothing pixels. + _labelWidthConstraint.constant = ceil(MAX(labelWidth, + [labelAttributes[SRMinimalDrawableWidthAttributeName] doubleValue]) + 2.0); +} + - (void)drawBackground:(NSRect)aDirtyRect { NSRect backgroundFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, backgroundDrawingGuide, frame, self.bounds)]; @@ -689,13 +792,17 @@ - (void)drawLabel:(NSRect)aDirtyRect { NSRect labelFrame = self.style.labelDrawingGuide.frame; +#if SR_DEBUG_DRAWING + [NSColor.systemRedColor set]; + NSRectFill(labelFrame); +#endif + if (NSIsEmptyRect(labelFrame) || ![self needsToDrawRect:labelFrame]) return; NSString *label = self.drawingLabel; NSDictionary *labelAttributes = self.drawingLabelAttributes; - [NSGraphicsContext saveGraphicsState]; CGFloat baselineOffset = _SRIfRespondsGet(self.style, baselineLayoutOffsetFromBottom, self.style.baselineDrawingOffsetFromBottom); labelFrame.origin.y = NSMaxY(labelFrame) - baselineOffset; labelFrame = [self backingAlignedRect:labelFrame options:NSAlignRectFlipped | @@ -704,9 +811,21 @@ - (void)drawLabel:(NSRect)aDirtyRect NSAlignMaxXInward | NSAlignMaxYInward]; + [NSGraphicsContext saveGraphicsState]; + +#if SR_DEBUG_DRAWING + [[NSColor.systemRedColor highlightWithLevel:0.5] set]; + NSRectFill(labelFrame); +#endif + CGFloat minWidth = [labelAttributes[SRMinimalDrawableWidthAttributeName] doubleValue]; if (labelFrame.size.width >= minWidth) + { + if (!self.isOpaque && self.style.isLabelDrawingFrameOpaque) + CGContextSetShouldSmoothFonts(NSGraphicsContext.currentContext.CGContext, true); + [label drawWithRect:labelFrame options:0 attributes:labelAttributes context:nil]; + } [NSGraphicsContext restoreGraphicsState]; } @@ -715,6 +834,14 @@ - (void)drawCancelButton:(NSRect)aDirtyRect { NSRect cancelButtonFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, cancelButtonDrawingGuide, frame, NSZeroRect)]; +#if SR_DEBUG_DRAWING + [NSColor.systemBlueColor set]; + NSRectFill([self centerScanRect:_SRIfRespondsGetProp(self.style, cancelButtonLayoutGuide, frame, NSZeroRect)]); + + [[NSColor.systemBlueColor highlightWithLevel:0.5] set]; + NSRectFill(cancelButtonFrame); +#endif + if (NSIsEmptyRect(cancelButtonFrame) || ![self needsToDrawRect:cancelButtonFrame]) return; @@ -730,6 +857,15 @@ - (void)drawCancelButton:(NSRect)aDirtyRect - (void)drawClearButton:(NSRect)aDirtyRect { NSRect clearButtonFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, clearButtonDrawingGuide, frame, NSZeroRect)]; + +#if SR_DEBUG_DRAWING + [NSColor.systemGreenColor set]; + NSRectFill([self centerScanRect:_SRIfRespondsGetProp(self.style, clearButtonLayoutGuide, frame, NSZeroRect)]); + + [[NSColor.systemGreenColor highlightWithLevel:0.5] set]; + NSRectFill(clearButtonFrame); +#endif + if (NSIsEmptyRect(clearButtonFrame) || ![self needsToDrawRect:clearButtonFrame]) return; @@ -742,22 +878,22 @@ - (void)drawClearButton:(NSRect)aDirtyRect [NSGraphicsContext restoreGraphicsState]; } -- (BOOL)areModifierFlagsValid:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode +- (BOOL)areModifierFlagsValid:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode { aModifierFlags &= SRCocoaModifierFlagsMask; __block BOOL allowModifierFlags = YES; - os_activity_initiate("areModifierFlagsValid:forKeyCode:", OS_ACTIVITY_FLAG_DEFAULT, ^{ - allowModifierFlags = [self areModifierFlagsAllowed:aModifierFlags forKeyCode:aKeyCode]; - + os_activity_initiate("-[SRRecorderControl areModifierFlagsValid:forKeyCode:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ if ((aModifierFlags & self.requiredModifierFlags) != self.requiredModifierFlags) allowModifierFlags = NO; + else + allowModifierFlags = [self areModifierFlagsAllowed:aModifierFlags forKeyCode:aKeyCode]; }); return allowModifierFlags; } -- (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode:(unsigned short)aKeyCode +- (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode { aModifierFlags &= SRCocoaModifierFlagsMask; __block BOOL allowModifierFlags = YES; @@ -765,26 +901,25 @@ - (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" __auto_type DelegateShouldUnconditionallyAllowModifierFlags = ^{ - if (!allowModifierFlags && [self.delegate respondsToSelector:@selector(recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:)]) + if ([self.delegate respondsToSelector:@selector(recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:)]) { return [self.delegate recorderControl:self shouldUnconditionallyAllowModifierFlags:aModifierFlags forKeyCode:aKeyCode]; } - else if (!allowModifierFlags && [self.delegate respondsToSelector:@selector(shortcutRecorder:shouldUnconditionallyAllowModifierFlags:forKeyCode:)]) + else if ([self.delegate respondsToSelector:@selector(shortcutRecorder:shouldUnconditionallyAllowModifierFlags:forKeyCode:)]) { return [self.delegate shortcutRecorder:self shouldUnconditionallyAllowModifierFlags:aModifierFlags forKeyCode:aKeyCode]; } else - return YES; + return NO; }; #pragma clang diagnostic pop - os_activity_initiate("areModifierFlagsAllowed:forKeyCode:", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{ - if (aModifierFlags == 0 && !self.allowsEmptyModifierFlags) - allowModifierFlags = NO; - else if ((aModifierFlags & self.allowedModifierFlags) != aModifierFlags) - allowModifierFlags = NO; - - allowModifierFlags = DelegateShouldUnconditionallyAllowModifierFlags(); + os_activity_initiate("-[SRRecorderControl areModifierFlagsAllowed:forKeyCode:]", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{ + if ((aModifierFlags == 0 && !self.allowsEmptyModifierFlags) || + ((aModifierFlags & self.allowedModifierFlags) != aModifierFlags)) + { + allowModifierFlags = DelegateShouldUnconditionallyAllowModifierFlags(); + } }); return allowModifierFlags; @@ -872,6 +1007,69 @@ - (void)scheduleControlViewAppearanceDidChange:(nullable id)aReason [_notifyStyle performSelector:@selector(invokeWithTarget:) withObject:_style afterDelay:0.0 inModes:@[NSRunLoopCommonModes]]; } +- (BOOL)canCaptureKeyEvent +{ + if (!self.enabled) + { + os_trace_debug("The control is disabled"); + return NO; + } + else if (self.window.firstResponder != self) + { + os_trace_debug("The control is not the first responder"); + return NO; + } + else if (self->_mouseTrackingButtonTag != _SRRecorderControlInvalidButtonTag) + { + os_trace_debug("The control is tracking %lu", self->_mouseTrackingButtonTag); + return NO; + } + else + return YES; +} + +- (BOOL)canEndRecordingWithObjectValue:(nullable SRShortcut *)aShortcut +{ + __block BOOL result = NO; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + __auto_type DelegateCanRecordShortcut = ^(SRShortcut *aShortcut){ + if ([self.delegate respondsToSelector:@selector(recorderControl:canRecordShortcut:)]) + return [self.delegate recorderControl:self canRecordShortcut:aShortcut]; + else if ([self.delegate respondsToSelector:@selector(shortcutRecorder:canRecordShortcut:)]) + return [self.delegate shortcutRecorder:self canRecordShortcut:aShortcut.dictionaryRepresentation]; + else if ([self.delegate respondsToSelector:@selector(control:isValidObject:)]) + return [self.delegate control:self isValidObject:aShortcut]; + else + return YES; + }; +#pragma clang diagnostic pop + + os_activity_initiate("-[SRRecorderControl canEndRecordingWithObjectValue:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ + if ([self areModifierFlagsValid:aShortcut.modifierFlags forKeyCode:aShortcut.keyCode]) + { + if (DelegateCanRecordShortcut(aShortcut)) + { + os_trace_debug("Valid and accepted shortcut"); + result = YES; + } + else + { + os_trace_debug("Delegate rejected"); + result = NO; + } + } + else + { + os_trace_debug("Modifier flags %lu rejected", aShortcut.modifierFlags); + result = NO; + } + }); + + return result; +} + #pragma mark NSAccessibility - (BOOL)isAccessibilityElement @@ -888,8 +1086,7 @@ - (NSString *)accessibilityLabel { if (self.isRecording) { - _accessibilityRecordingModifierFlags = _currentlyDrawnRecordingModifierFlags; - return [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_accessibilityRecordingModifierFlags) + return [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_lastSeenModifierFlags) layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight]; } else @@ -905,19 +1102,25 @@ - (id)accessibilityValue if (!_objectValue) return SRLoc(@"Empty"); - NSString *f = [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_objectValue.modifierFlags) - layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight]; - NSString *c = nil; + NSString *flags = [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_objectValue.modifierFlags) + layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight]; + + SRKeyCodeTransformer *transformer = nil; if (self.drawsASCIIEquivalentOfShortcut) - c = [SRASCIILiteralKeyCodeTransformer.sharedTransformer transformedValue:@(_objectValue.keyCode)]; + transformer = SRASCIILiteralKeyCodeTransformer.sharedTransformer; else - c = [SRLiteralKeyCodeTransformer.sharedTransformer transformedValue:@(_objectValue.keyCode)]; + transformer = SRLiteralKeyCodeTransformer.sharedTransformer; - if (f.length > 0) - return [NSString stringWithFormat:@"%@-%@", f, c]; + NSString *code = [transformer transformedValue:@(_objectValue.keyCode)]; + + if (!code) + code = [NSString stringWithFormat:@"%hu", _objectValue.keyCode]; + + if (flags.length > 0) + return [NSString stringWithFormat:@"%@-%@", flags, code]; else - return [NSString stringWithFormat:@"%@", c]; + return [NSString stringWithFormat:@"%@", code]; } } @@ -1070,7 +1273,7 @@ - (void)setAttributedStringValue:(NSAttributedString *)newAttributedStringValue - (NSString *)stringValue { if (!_objectValue) - return @""; + return SRLoc(@""); __auto_type layoutDirection = self.stringValueRespectsUserInterfaceLayoutDirection ? self.userInterfaceLayoutDirection : NSUserInterfaceLayoutDirectionLeftToRight; NSString *flags = [SRSymbolicModifierFlagsTransformer.sharedTransformer transformedValue:@(_objectValue.modifierFlags) @@ -1087,6 +1290,9 @@ - (NSString *)stringValue explicitModifierFlags:@(_objectValue.modifierFlags) layoutDirection:layoutDirection]; + if (!code) + code = [NSString stringWithFormat:@"<%hu>", _objectValue.keyCode]; + if (layoutDirection == NSUserInterfaceLayoutDirectionRightToLeft) return [NSString stringWithFormat:@"%@%@", code, flags]; else @@ -1167,37 +1373,6 @@ - (NSUserInterfaceLayoutDirection)userInterfaceLayoutDirection return super.userInterfaceLayoutDirection; } -- (void)layout -{ - NSRect oldLabelFrame = self.style.labelDrawingGuide.frame; - NSRect oldCancelButtonFrame = _SRIfRespondsGetProp(self.style, cancelButtonDrawingGuide, frame, NSZeroRect); - NSRect oldClearButtonFrame = _SRIfRespondsGetProp(self.style, clearButtonDrawingGuide, frame, NSZeroRect); - - [super layout]; - - NSRect newLabelFrame = self.style.labelDrawingGuide.frame; - NSRect newCancelButtonFrame = _SRIfRespondsGetProp(self.style, cancelButtonDrawingGuide, frame, NSZeroRect); - NSRect newClearButtonFrame = _SRIfRespondsGetProp(self.style, clearButtonDrawingGuide, frame, NSZeroRect); - - if (!NSEqualRects(oldLabelFrame, newLabelFrame)) - { - [self setNeedsDisplayInRect:oldLabelFrame]; - [self setNeedsDisplayInRect:newLabelFrame]; - } - - if (!NSEqualRects(oldCancelButtonFrame, newCancelButtonFrame)) - { - [self setNeedsDisplayInRect:oldCancelButtonFrame]; - [self setNeedsDisplayInRect:newCancelButtonFrame]; - } - - if (!NSEqualRects(oldClearButtonFrame, newClearButtonFrame)) - { - [self setNeedsDisplayInRect:oldClearButtonFrame]; - [self setNeedsDisplayInRect:newClearButtonFrame]; - } -} - - (void)drawRect:(NSRect)aDirtyRect { [self drawBackground:aDirtyRect]; @@ -1321,6 +1496,7 @@ - (void)updateTrackingAreas - (void)updateConstraints { [self updateActiveConstraints]; + [self updateLabelConstraints]; [super updateConstraints]; } @@ -1449,7 +1625,7 @@ - (void)mouseDown:(NSEvent *)anEvent else if ([self mouse:locationInView inRect:self.bounds]) { _mouseTrackingButtonTag = _SRRecorderControlMainButtonTag; - [self setNeedsDisplay:YES]; + self.needsDisplay = YES; } else [super mouseDown:anEvent]; @@ -1484,7 +1660,7 @@ - (void)mouseUp:(NSEvent *)anEvent { // It's possible to receive this event after window resigned its key status // e.g. when shortcut brings new window and makes it key. - [self setNeedsDisplay:YES]; + self.needsDisplay = YES; } else { @@ -1515,17 +1691,14 @@ - (void)mouseUp:(NSEvent *)anEvent - (void)mouseEntered:(NSEvent *)anEvent { - if (!self.enabled) - { - [super mouseEntered:anEvent]; - return; - } - - if ((_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea) || - (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea) || - (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea)) + if (self.enabled) { - [self setNeedsDisplayInRect:anEvent.trackingArea.rect]; + if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.backgroundDrawingGuide.frame]; + else if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.cancelButtonDrawingGuide.frame]; + else if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.clearButtonDrawingGuide.frame]; } [super mouseEntered:anEvent]; @@ -1533,17 +1706,14 @@ - (void)mouseEntered:(NSEvent *)anEvent - (void)mouseExited:(NSEvent *)anEvent { - if (!self.enabled) - { - [super mouseExited:anEvent]; - return; - } - - if ((_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea) || - (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea) || - (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea)) + if (self.enabled) { - [self setNeedsDisplayInRect:anEvent.trackingArea.rect]; + if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.backgroundDrawingGuide.frame]; + else if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.cancelButtonDrawingGuide.frame]; + else if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea) + [self setNeedsDisplayInRect:self.style.clearButtonDrawingGuide.frame]; } [super mouseExited:anEvent]; @@ -1559,41 +1729,9 @@ - (BOOL)performKeyEquivalent:(NSEvent *)anEvent { __block BOOL result = NO; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - __auto_type DelegateCanRecordShortcut = ^(SRShortcut *aShortcut){ - if ([self.delegate respondsToSelector:@selector(recorderControl:canRecordShortcut:)]) - return [self.delegate recorderControl:self canRecordShortcut:aShortcut]; - else if ([self.delegate respondsToSelector:@selector(shortcutRecorder:canRecordShortcut:)]) - return [self.delegate shortcutRecorder:self canRecordShortcut:aShortcut.dictionaryRepresentation]; - else if ([self.delegate respondsToSelector:@selector(control:isValidObject:)]) - return [self.delegate control:self isValidObject:aShortcut]; - else - return YES; - }; -#pragma clang diagnostic pop - - os_activity_initiate("performKeyEquivalent:", OS_ACTIVITY_FLAG_DEFAULT, ^{ - if (!self.enabled) - { - os_trace_debug("The control is disabled -> NO"); - result = NO; - return; - } - - if (self.window.firstResponder != self) - { - os_trace_debug("The control is not the first responder -> NO"); - result = NO; + os_activity_initiate("-[SRRecorderControl performKeyEquivalent:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ + if (![self canCaptureKeyEvent]) return; - } - - if (self->_mouseTrackingButtonTag != _SRRecorderControlInvalidButtonTag) - { - os_trace_debug("The control is tracking %lu -> NO", self->_mouseTrackingButtonTag); - result = NO; - return; - } if (self.isRecording) { @@ -1601,56 +1739,43 @@ - (BOOL)performKeyEquivalent:(NSEvent *)anEvent { // This shouldn't really happen ever, but was rarely observed. // See https://github.com/Kentzo/ShortcutRecorder/issues/40 - os_trace_debug("Invalid keyCode -> NO"); + os_trace_debug("Invalid key code"); result = NO; } else if (self.allowsEscapeToCancelRecording && - anEvent.keyCode == kVK_Escape && + anEvent.keyCode == SRKeyCodeEscape && (anEvent.modifierFlags & SRCocoaModifierFlagsMask) == 0) { - os_trace_debug("Cancel via Esc -> YES"); + os_trace_debug("Cancel via Esc"); [self endRecording]; result = YES; } else if (self.allowsDeleteToClearShortcutAndEndRecording && - (anEvent.keyCode == kVK_Delete || anEvent.keyCode == kVK_ForwardDelete) && + (anEvent.keyCode == SRKeyCodeDelete || anEvent.keyCode == SRKeyCodeForwardDelete) && (anEvent.modifierFlags & SRCocoaModifierFlagsMask) == 0) { - os_trace_debug("Clear via Delete -> YES"); + os_trace_debug("Clear via Delete"); [self clearAndEndRecording]; result = YES; } - else if ([self areModifierFlagsValid:anEvent.modifierFlags forKeyCode:anEvent.keyCode]) + else { - SRShortcut *newObjectValue = [SRShortcut shortcutWithCode:anEvent.keyCode - modifierFlags:anEvent.modifierFlags - characters:anEvent.characters - charactersIgnoringModifiers:anEvent.charactersIgnoringModifiers]; - - BOOL canRecordShortcut = DelegateCanRecordShortcut(newObjectValue); + SRShortcut *newObjectValue = [SRShortcut shortcutWithEvent:anEvent]; - if (canRecordShortcut) - { - os_trace_debug("Valid and accepted shortcut -> YES"); + if ([self canEndRecordingWithObjectValue:newObjectValue]) [self endRecordingWithObjectValue:newObjectValue]; - result = YES; - } else { // Do not end editing and allow the client to make another attempt. - os_trace_debug("Valid but rejected shortcut -> YES"); - result = YES; + [self playAlert]; } - } - else - { - os_trace_debug("Modifier flags %lu rejected -> NO", anEvent.modifierFlags); - result = NO; + + result = YES; } } - else if (anEvent.keyCode == kVK_Space) + else if (anEvent.keyCode == SRKeyCodeSpace) { - os_trace_debug("Begin recording via Space -> YES"); + os_trace_debug("Begin recording via Space"); result = [self beginRecording]; } else @@ -1662,13 +1787,50 @@ - (BOOL)performKeyEquivalent:(NSEvent *)anEvent - (void)flagsChanged:(NSEvent *)anEvent { - if (self.isRecording) + if (self.isRecording && [self canCaptureKeyEvent]) { - NSEventModifierFlags modifierFlags = anEvent.modifierFlags & SRCocoaModifierFlagsMask; - if (modifierFlags != 0 && ![self areModifierFlagsAllowed:modifierFlags forKeyCode:anEvent.keyCode]) - [self playAlert]; + __auto_type modifierFlags = anEvent.modifierFlags & SRCocoaModifierFlagsMask; + + if (self.allowsModifierFlagsOnlyShortcut) + { + __auto_type keyCode = anEvent.keyCode; + __auto_type nextModifierFlags = _lastSeenModifierFlags; + + // Only XOR when flag is added. + if ((modifierFlags & NSEventModifierFlagCommand) && (keyCode == kVK_Command || keyCode == kVK_RightCommand)) + nextModifierFlags ^= NSEventModifierFlagCommand; + else if ((modifierFlags & NSEventModifierFlagOption) && (keyCode == kVK_Option || keyCode == kVK_RightOption)) + nextModifierFlags ^= NSEventModifierFlagOption; + else if ((modifierFlags & NSEventModifierFlagShift) && (keyCode == kVK_Shift || keyCode == kVK_RightShift)) + nextModifierFlags ^= NSEventModifierFlagShift; + else if ((modifierFlags & NSEventModifierFlagControl) && (keyCode == kVK_Control || keyCode == kVK_RightControl)) + nextModifierFlags ^= NSEventModifierFlagControl; + else if (modifierFlags == 0 && _lastSeenModifierFlags != 0) + { + SRShortcut *newObjectValue = [SRShortcut shortcutWithCode:SRKeyCodeNone + modifierFlags:_lastSeenModifierFlags + characters:nil + charactersIgnoringModifiers:nil]; + + if ([self canEndRecordingWithObjectValue:newObjectValue]) + [self endRecordingWithObjectValue:newObjectValue]; + } + + if (nextModifierFlags != _lastSeenModifierFlags && ![self areModifierFlagsAllowed:nextModifierFlags forKeyCode:SRKeyCodeNone]) + [self playAlert]; + else + _lastSeenModifierFlags = nextModifierFlags; + } + else + { + if (![self areModifierFlagsAllowed:modifierFlags forKeyCode:SRKeyCodeNone]) + [self playAlert]; + else + _lastSeenModifierFlags = modifierFlags; + } [self setNeedsDisplayInRect:self.style.labelDrawingGuide.frame]; + [self updateLabelConstraints]; } [super flagsChanged:anEvent]; @@ -1713,16 +1875,19 @@ - (Class)valueClassForBinding:(NSBindingName)aBinding return [super optionDescriptionsForBinding:aBinding]; } -- (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject change:(NSDictionary *)aChange context:(void *)aContext +- (void)observeValueForKeyPath:(NSString *)aKeyPath + ofObject:(id)anObject + change:(NSDictionary *)aChange + context:(void *)aContext { - if (aContext == &_SRStyleUserInterfaceLayoutDirectionObservingContext) + if (aContext == _SRStyleUserInterfaceLayoutDirectionObservingContext) { if ([aChange objectForKey:NSKeyValueChangeNotificationIsPriorKey]) [self willChangeValueForKey:@"userInterfaceLayoutDirection"]; else [self didChangeValueForKey:@"userInterfaceLayoutDirection"]; } - else if (aContext == &_SRStyleAppearanceObservingContext) + else if (aContext == _SRStyleAppearanceObservingContext) { __auto_type appearance = [aChange[NSKeyValueChangeNewKey] unsignedIntegerValue]; @@ -1731,6 +1896,11 @@ - (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject change else self.appearance = nil; } + else if (aContext == _SRStyleGuideObservingContext) + { + [self setNeedsDisplayInRect:[aChange[NSKeyValueChangeOldKey] rectValue]]; + [self setNeedsDisplayInRect:[aChange[NSKeyValueChangeNewKey] rectValue]]; + } else [super observeValueForKeyPath:aKeyPath ofObject:anObject change:aChange context:aContext]; } diff --git a/Library/SRRecorderControlStyle.h b/Library/SRRecorderControlStyle.h index 675b92ee..f7662835 100644 --- a/Library/SRRecorderControlStyle.h +++ b/Library/SRRecorderControlStyle.h @@ -213,6 +213,20 @@ NS_SWIFT_NAME(RecorderControlStyling) */ @property (getter=isOpaque, readonly) BOOL opaque; +/*! + Whether area under the label drawing frame is opaque. + + @discussion + AppKit disables LCD smoothing for non-opaque layer-backed views. Styles that guarantee that area under + the label is opaque want to enable LCD smoothing. + + @note Target context may still elect to disallow LCD smoothing + + @see CGContextSetShouldSmoothFonts + @see CGContextSetAllowsFontSmoothing + */ +@property (getter=isLabelDrawingFrameOpaque, readonly) BOOL labelDrawingFrameOpaque; + /*! Unlike baselineLayoutOffsetFromBottom this is the true baseline where label is actually drawn. diff --git a/Library/SRRecorderControlStyle.m b/Library/SRRecorderControlStyle.m index fbb0894d..7c4f4505 100644 --- a/Library/SRRecorderControlStyle.m +++ b/Library/SRRecorderControlStyle.m @@ -89,7 +89,8 @@ SRRecorderControlStyleComponentsTint SRRecorderControlStyleComponentsTintFromSys NSControlTint SRRecorderControlStyleComponentsTintToSystem(SRRecorderControlStyleComponentsTint aTint) { - switch (aTint) { + switch (aTint) + { case SRRecorderControlStyleComponentsTintBlue: return NSBlueControlTint; break; @@ -683,7 +684,10 @@ - (instancetype)init p.alignment = NSTextAlignmentCenter; p.lineBreakMode = NSLineBreakByTruncatingMiddle; - NSFont *font = [NSFont fontWithName:anObject[@"fontName"] size:[anObject[@"fontSize"] doubleValue]]; + NSString *fontName = anObject[@"fontName"]; + CGFloat fontSize = [anObject[@"fontSize"] doubleValue]; + NSFont *font = [fontName isEqual:@".AppleSystemUIFont"] ? [NSFont systemFontOfSize:fontSize] : [NSFont fontWithName:fontName size:fontSize]; + NSColor *fontColor = [NSColor colorWithCatalogName:anObject[@"fontColorCatalogName"] colorName:anObject[@"fontColorName"]]; NSMutableDictionary *attributes = @{ @@ -713,7 +717,7 @@ - (instancetype)init }; __block NSDictionary *info = nil; - os_activity_initiate("infoForStyle:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRRecorderControlStyleResourceLoader infoForStyle:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ os_trace_debug_with_payload("Fetching info", ^(xpc_object_t d) { xpc_dictionary_set_string(d, "identifier", aStyle.identifier.UTF8String); }); @@ -792,7 +796,7 @@ - (instancetype)init - (NSArray *)lookupPrefixesForStyle:(SRRecorderControlStyle *)aStyle { __block NSArray *lookupPrefixes = nil; - os_activity_initiate("lookupPrefixesForStyle:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRRecorderControlStyleResourceLoader lookupPrefixesForStyle:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ os_trace_debug_with_payload("Fetching lookup prefixes", ^(xpc_object_t d) { xpc_dictionary_set_string(d, "identifier", aStyle.identifier.UTF8String); }); @@ -833,7 +837,7 @@ - (instancetype)init - (NSImage *)imageNamed:(NSString *)aName forStyle:(SRRecorderControlStyle *)aStyle { __block NSImage *image = nil; - os_activity_initiate("imageNamed:forStyle:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRRecorderControlStyleResourceLoader imageNamed:forStyle:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ os_trace_debug_with_payload("Fetching image name", ^(xpc_object_t d) { xpc_dictionary_set_string(d, "identifier", aStyle.identifier.UTF8String); xpc_dictionary_set_string(d, "image", aName.UTF8String); @@ -906,7 +910,6 @@ @implementation SRRecorderControlStyle NSLayoutConstraint *_backgroundBottomConstraint; NSLayoutConstraint *_backgroundRightConstraint; - NSLayoutConstraint *_alignmentSuggestedWidthConstraint; NSLayoutConstraint *_alignmentWidthConstraint; NSLayoutConstraint *_alignmentHeightConstraint; NSLayoutConstraint *_alignmentToLabelConstraint; @@ -948,6 +951,7 @@ - (instancetype)initWithIdentifier:(NSString *)anIdentifier components:(SRRecord _allowsVibrancy = NO; _opaque = NO; + _labelDrawingFrameOpaque = YES; _alwaysConstraints = @[]; _displayingConstraints = @[]; _recordingWithValueConstraints = @[]; @@ -982,13 +986,14 @@ + (SRRecorderControlStyleResourceLoader *)resourceLoader - (void)addConstraints { - [self.recorderControl addLayoutGuide:self.alignmentGuide]; - [self.recorderControl addLayoutGuide:self.backgroundDrawingGuide]; - [self.recorderControl addLayoutGuide:self.labelDrawingGuide]; - [self.recorderControl addLayoutGuide:self.cancelButtonDrawingGuide]; - [self.recorderControl addLayoutGuide:self.clearButtonDrawingGuide]; - [self.recorderControl addLayoutGuide:self.cancelButtonLayoutGuide]; - [self.recorderControl addLayoutGuide:self.clearButtonLayoutGuide]; + __auto_type strongRecorderControl = self.recorderControl; + [strongRecorderControl addLayoutGuide:self.alignmentGuide]; + [strongRecorderControl addLayoutGuide:self.backgroundDrawingGuide]; + [strongRecorderControl addLayoutGuide:self.labelDrawingGuide]; + [strongRecorderControl addLayoutGuide:self.cancelButtonDrawingGuide]; + [strongRecorderControl addLayoutGuide:self.clearButtonDrawingGuide]; + [strongRecorderControl addLayoutGuide:self.cancelButtonLayoutGuide]; + [strongRecorderControl addLayoutGuide:self.clearButtonLayoutGuide]; __auto_type SetConstraint = ^(NSLayoutConstraint * __strong *var, NSLayoutConstraint *value) { *var = value; @@ -1053,16 +1058,16 @@ - (void)addConstraints _alwaysConstraints = @[ MakeEqConstraint(self.alignmentGuide.topAnchor, - self.recorderControl.topAnchor, + strongRecorderControl.topAnchor, @"SR_alignmentGuide_topToView"), MakeEqConstraint(self.alignmentGuide.leftAnchor, - self.recorderControl.leftAnchor, + strongRecorderControl.leftAnchor, @"SR_alignmentGuide_leftToView"), MakeEqConstraint(self.alignmentGuide.rightAnchor, - self.recorderControl.rightAnchor, + strongRecorderControl.rightAnchor, @"SR_alignmentGuide_rightToView"), MakeConstraint(self.alignmentGuide.bottomAnchor, - self.recorderControl.bottomAnchor, + strongRecorderControl.bottomAnchor, 0.0, NSLayoutPriorityDefaultHigh, NSLayoutRelationEqual, @@ -1073,12 +1078,6 @@ - (void)addConstraints SetConstraint(&_alignmentWidthConstraint, MakeGteConstraint(self.alignmentGuide.widthAnchor, nil, @"SR_alignmentGuide_width")), - SetConstraint(&_alignmentSuggestedWidthConstraint, MakeConstraint(self.alignmentGuide.widthAnchor, - nil, - 0.0, - NSLayoutPriorityDefaultLow, - NSLayoutRelationEqual, - @"SR_alignmentGuide_suggestedWidth")), SetConstraint(&_backgroundTopConstraint, MakeEqConstraint(self.alignmentGuide.topAnchor, self.backgroundDrawingGuide.topAnchor, @@ -1105,21 +1104,21 @@ - (void)addConstraints MakeConstraint(self.labelDrawingGuide.centerXAnchor, self.alignmentGuide.centerXAnchor, 0.0, - NSLayoutPriorityDefaultHigh, + SRRecorderControlLabelWidthPriority - 1, NSLayoutRelationEqual, @"SR_labelDrawingGuide_centerXToAlignment") ]; _displayingConstraints = @[ - SetConstraint(&_labelToAlignmentConstraint, MakeEqConstraint(self.alignmentGuide.trailingAnchor, - self.labelDrawingGuide.trailingAnchor, - @"SR_labelDrawingGuide_trailingToAlignment")), + SetConstraint(&_labelToAlignmentConstraint, MakeGteConstraint(self.alignmentGuide.trailingAnchor, + self.labelDrawingGuide.trailingAnchor, + @"SR_labelDrawingGuide_trailingToAlignment")), ]; _recordingWithNoValueConstraints = @[ - SetConstraint(&_labelToCancelConstraint, MakeEqConstraint(self.cancelButtonDrawingGuide.leadingAnchor, - self.labelDrawingGuide.trailingAnchor, - @"SR_labelDrawingGuide_trailingToCancel")), + SetConstraint(&_labelToCancelConstraint, MakeGteConstraint(self.cancelButtonDrawingGuide.leadingAnchor, + self.labelDrawingGuide.trailingAnchor, + @"SR_labelDrawingGuide_trailingToCancel")), SetConstraint(&_cancelToAlignmentConstraint, MakeEqConstraint(self.alignmentGuide.trailingAnchor, self.cancelButtonDrawingGuide.trailingAnchor, @@ -1200,13 +1199,14 @@ - (void)addConstraints @"SR_clearButtonLayoutGuide_trailingToAlignment"), ]; - self.recorderControl.needsUpdateConstraints = YES; + strongRecorderControl.needsUpdateConstraints = YES; } #pragma mark SRRecorderControlStyling @synthesize identifier = _identifier; @synthesize allowsVibrancy = _allowsVibrancy; @synthesize opaque = _opaque; +@synthesize labelDrawingFrameOpaque = _labelDrawingFrameOpaque; @synthesize normalLabelAttributes = _normalLabelAttributes; @synthesize recordingLabelAttributes = _recordingLabelAttributes; @synthesize disabledLabelAttributes = _disabledLabelAttributes; @@ -1268,29 +1268,30 @@ - (void)prepareForRecorderControl:(SRRecorderControl *)aControl _recorderControl = aControl; [self didChangeValueForKey:@"recorderControl"]; - if (!_recorderControl) + if (!aControl) return; [self addConstraints]; [self recorderControlAppearanceDidChange:nil]; - _recorderControl.needsDisplay = YES; + aControl.needsDisplay = YES; } - (void)prepareForRemoval { - NSAssert(_recorderControl != nil, @"Style was not applied properly."); + __auto_type strongRecorderControl = _recorderControl; + NSAssert(strongRecorderControl != nil, @"Style was not applied properly."); - [_recorderControl removeLayoutGuide:_alignmentGuide]; - [_recorderControl removeLayoutGuide:_backgroundDrawingGuide]; - [_recorderControl removeLayoutGuide:_labelDrawingGuide]; - [_recorderControl removeLayoutGuide:_cancelButtonDrawingGuide]; - [_recorderControl removeLayoutGuide:_clearButtonDrawingGuide]; - [_recorderControl removeLayoutGuide:_cancelButtonLayoutGuide]; - [_recorderControl removeLayoutGuide:_clearButtonLayoutGuide]; + [strongRecorderControl removeLayoutGuide:_alignmentGuide]; + [strongRecorderControl removeLayoutGuide:_backgroundDrawingGuide]; + [strongRecorderControl removeLayoutGuide:_labelDrawingGuide]; + [strongRecorderControl removeLayoutGuide:_cancelButtonDrawingGuide]; + [strongRecorderControl removeLayoutGuide:_clearButtonDrawingGuide]; + [strongRecorderControl removeLayoutGuide:_cancelButtonLayoutGuide]; + [strongRecorderControl removeLayoutGuide:_clearButtonLayoutGuide]; [self willChangeValueForKey:@"recorderControl"]; - _recorderControl = nil; + strongRecorderControl = nil; [self didChangeValueForKey:@"recorderControl"]; } @@ -1354,7 +1355,8 @@ - (void)recorderControlAppearanceDidChange:(nullable id)aReason [self.recorderControl setNeedsDisplayInRect:frame]; }; - NSRect controlBounds = self.recorderControl.bounds; + __auto_type strongRecorderControl = self.recorderControl; + NSRect controlBounds = strongRecorderControl.bounds; UpdateImage(@"bezel-normal-left", @selector(bezelNormalLeft), controlBounds); UpdateImage(@"bezel-normal-center", @selector(bezelNormalCenter), controlBounds); @@ -1426,13 +1428,12 @@ - (void)recorderControlAppearanceDidChange:(nullable id)aReason _clearButtonWidthConstraint.constant + _clearToAlignmentConstraint.constant); - _alignmentSuggestedWidthConstraint.constant = maxExpectedLeadingLabelOffset + maxExpectedLabelWidth + maxExpectedTrailingLabelOffset; - - _intrinsicContentSize = NSMakeSize(_alignmentSuggestedWidthConstraint.constant, _alignmentHeightConstraint.constant); + _intrinsicContentSize = NSMakeSize(maxExpectedLeadingLabelOffset + maxExpectedLabelWidth + maxExpectedTrailingLabelOffset, + _alignmentHeightConstraint.constant); - [self.recorderControl noteFocusRingMaskChanged]; - [self.recorderControl invalidateIntrinsicContentSize]; - self.recorderControl.needsDisplay = YES; + [strongRecorderControl noteFocusRingMaskChanged]; + [strongRecorderControl invalidateIntrinsicContentSize]; + strongRecorderControl.needsDisplay = YES; } _currentLookupPrefixes = newLookupPrefixes; diff --git a/Library/SRShortcut.h b/Library/SRShortcut.h index 5ad14c8d..c8e6fbfc 100644 --- a/Library/SRShortcut.h +++ b/Library/SRShortcut.h @@ -55,7 +55,7 @@ NS_SWIFT_NAME(Shortcut) /*! @seealso SRShortcut/initWithCode:modifierFlags:characters:charactersIgnoringModifiers: */ -+ (instancetype)shortcutWithCode:(unsigned short)aKeyCode ++ (instancetype)shortcutWithCode:(SRKeyCode)aKeyCode modifierFlags:(NSEventModifierFlags)aModifierFlags characters:(nullable NSString *)aCharacters charactersIgnoringModifiers:(nullable NSString *)aCharactersIgnoringModifiers; @@ -103,7 +103,7 @@ NS_SWIFT_NAME(Shortcut) If aCharacters is nil, an attempt is made to translate the given key code and modifier flags using SRASCIISymbolicKeyCodeTransformer. Similarly for aCharactersIgnoringModifiers. */ -- (instancetype)initWithCode:(unsigned short)aKeyCode +- (instancetype)initWithCode:(SRKeyCode)aKeyCode modifierFlags:(NSEventModifierFlags)aModifierFlags characters:(nullable NSString *)aCharacters charactersIgnoringModifiers:(nullable NSString *)aCharactersIgnoringModifiers NS_DESIGNATED_INITIALIZER; @@ -113,7 +113,7 @@ NS_SWIFT_NAME(Shortcut) /*! A key code such as 0 ('a'). */ -@property (readonly) unsigned short keyCode; +@property (readonly) SRKeyCode keyCode; /*! Modifier flags such as NSEventModifierFlagCommand | NSEventModifierFlagOption. @@ -223,8 +223,8 @@ NS_INLINE BOOL SRShortcutEqualToShortcut(NSDictionary *a, NSDictionary *b) /*! Create ShortcutRecorder 2 shortcut. */ -NS_INLINE NSDictionary *SRShortcutWithCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut"))); -NS_INLINE NSDictionary *SRShortcutWithCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) +NS_INLINE NSDictionary *SRShortcutWithCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut"))); +NS_INLINE NSDictionary *SRShortcutWithCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) { return @{SRShortcutKeyKeyCode: @(aKeyCode), SRShortcutKeyModifierFlags: @(aModifierFlags)}; } @@ -234,14 +234,14 @@ NS_INLINE NSDictionary *SRShortcutWithCocoaModifierFlagsAndKeyCode(NSEventModifi Return string representation of a shortcut with modifier flags replaced with their localized readable equivalents (e.g. ⌥ -> Option). */ -NSString * _Nonnull SRReadableStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut/readableStringRepresentation:"))); +NSString * _Nonnull SRReadableStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut/readableStringRepresentation:"))); /*! Return string representation of a shortcut with modifier flags replaced with their localized readable equivalents (e.g. ⌥ -> Option) and ASCII character with a key code. */ -NSString * _Nonnull SRReadableASCIIStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut/readableStringRepresentation:"))); +NSString * _Nonnull SRReadableASCIIStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut/readableStringRepresentation:"))); /*! @@ -251,7 +251,7 @@ NSString * _Nonnull SRReadableASCIIStringForCocoaModifierFlagsAndKeyCode(NSEvent @discussion On macOS some key combinations can have "alternates". E.g. option-A can be represented both as "option-A" and "å". */ -BOOL SRKeyCodeWithFlagsEqualToKeyEquivalentWithFlags(unsigned short aKeyCode, +BOOL SRKeyCodeWithFlagsEqualToKeyEquivalentWithFlags(SRKeyCode aKeyCode, NSEventModifierFlags aKeyCodeFlags, NSString * _Nullable aKeyEquivalent, NSEventModifierFlags aKeyEquivalentModifierFlags) __attribute__((deprecated("Deprecated in 3.0", "SRShortcut/isEqualToKeyEquivalent:withModifierFlags:"))); diff --git a/Library/SRShortcut.m b/Library/SRShortcut.m index 070d83eb..64982a57 100644 --- a/Library/SRShortcut.m +++ b/Library/SRShortcut.m @@ -27,7 +27,7 @@ @implementation SRShortcut -+ (instancetype)shortcutWithCode:(unsigned short)aKeyCode ++ (instancetype)shortcutWithCode:(SRKeyCode)aKeyCode modifierFlags:(NSEventModifierFlags)aModifierFlags characters:(NSString *)aCharacters charactersIgnoringModifiers:(NSString *)aCharactersIgnoringModifiers @@ -40,29 +40,60 @@ + (instancetype)shortcutWithCode:(unsigned short)aKeyCode + (instancetype)shortcutWithEvent:(NSEvent *)aKeyboardEvent { - if (((1 << aKeyboardEvent.type) & (NSEventMaskKeyDown | NSEventMaskKeyUp)) == 0) + __auto_type eventType = aKeyboardEvent.type; + if (((1 << eventType) & (NSEventMaskKeyDown | NSEventMaskKeyUp | NSEventMaskFlagsChanged)) == 0) { - os_trace_error("aKeyboardEvent must be either NSEventTypeKeyUp or NSEventTypeKeyDown, but got %lu", aKeyboardEvent.type); + os_trace_error("#Error aKeyboardEvent must be either NSEventTypeKeyUp, NSEventTypeKeyDown or NSEventTypeFlagsChanged, but got %lu", aKeyboardEvent.type); return nil; } - return [self shortcutWithCode:aKeyboardEvent.keyCode - modifierFlags:aKeyboardEvent.modifierFlags - characters:aKeyboardEvent.characters - charactersIgnoringModifiers:aKeyboardEvent.charactersIgnoringModifiers]; + __auto_type keyCode = aKeyboardEvent.keyCode; + __auto_type modifierFlags = aKeyboardEvent.modifierFlags; + if (eventType == NSEventTypeFlagsChanged) + { + if (keyCode == kVK_Command || keyCode == kVK_RightCommand) + modifierFlags |= NSEventModifierFlagCommand; + else if (keyCode == kVK_Option || keyCode == kVK_RightOption) + modifierFlags |= NSEventModifierFlagOption; + else if (keyCode == kVK_Shift || keyCode == kVK_RightShift) + modifierFlags |= NSEventModifierFlagShift; + else if (keyCode == kVK_Control || keyCode == kVK_RightControl) + modifierFlags |= NSEventModifierFlagControl; + + keyCode = SRKeyCodeNone; + } + + NSString *characters = @""; + NSString *charactersIgnoringModifiers = @""; + if (eventType != NSEventTypeFlagsChanged) + { + @try + { + characters = aKeyboardEvent.characters; + charactersIgnoringModifiers = aKeyboardEvent.charactersIgnoringModifiers; + } + @catch (NSException *e) + { + if (!NSThread.isMainThread) + { + NSParameterAssert(NO); + os_trace_error("#Error #Developer AppKit failed to extract characters because it is used in non-main thread"); + } + else + @throw; + } + } + + return [self shortcutWithCode:keyCode + modifierFlags:modifierFlags + characters:characters + charactersIgnoringModifiers:charactersIgnoringModifiers]; } + (instancetype)shortcutWithDictionary:(NSDictionary *)aDictionary { NSNumber *keyCode = aDictionary[SRShortcutKeyKeyCode]; - - if (![keyCode isKindOfClass:NSNumber.class]) - { - os_trace_error("aDictionary must have a key code"); - return nil; - } - - unsigned short keyCodeValue = keyCode.unsignedShortValue; + SRKeyCode keyCodeValue = [keyCode isKindOfClass:NSNumber.class] ? keyCode.unsignedShortValue : SRKeyCodeNone; NSUInteger modifierFlagsValue = 0; NSString *charactersValue = nil; NSString *charactersIgnoringModifiersValue = nil; @@ -104,8 +135,16 @@ + (instancetype)shortcutWithKeyEquivalent:(NSString *)aKeyEquivalent [parser scanCharactersFromSet:PossibleFlags intoString:&modifierFlagsString]; NSString *keyCodeString = [aKeyEquivalent substringFromIndex:parser.scanLocation]; - NSNumber *modifierFlags = [SRSymbolicModifierFlagsTransformer.sharedTransformer reverseTransformedValue:modifierFlagsString]; - NSNumber *keyCode = [SRASCIILiteralKeyCodeTransformer.sharedTransformer reverseTransformedValue:keyCodeString]; + if (!modifierFlagsString.length && !keyCodeString.length) + return nil; + + NSNumber *modifierFlags = @0; + if (modifierFlagsString.length) + modifierFlags = [SRSymbolicModifierFlagsTransformer.sharedTransformer reverseTransformedValue:modifierFlagsString]; + + NSNumber *keyCode = @(SRKeyCodeNone); + if (keyCodeString.length) + keyCode = [SRASCIILiteralKeyCodeTransformer.sharedTransformer reverseTransformedValue:keyCodeString]; if (!modifierFlags || !keyCode) return nil; @@ -129,7 +168,7 @@ + (nullable instancetype)shortcutWithKeyBinding:(NSString *)aKeyBinding return [SRKeyBindingTransformer.sharedTransformer transformedValue:aKeyBinding]; } -- (instancetype)initWithCode:(unsigned short)aKeyCode +- (instancetype)initWithCode:(SRKeyCode)aKeyCode modifierFlags:(NSEventModifierFlags)aModifierFlags characters:(NSString *)aCharacters charactersIgnoringModifiers:(NSString *)aCharactersIgnoringModifiers @@ -143,7 +182,7 @@ - (instancetype)initWithCode:(unsigned short)aKeyCode if (aCharacters) _characters = [aCharacters copy]; - else + else if (aKeyCode != SRKeyCodeNone) _characters = [SRASCIISymbolicKeyCodeTransformer.sharedTransformer transformedValue:@(aKeyCode) withImplicitModifierFlags:@(aModifierFlags) explicitModifierFlags:nil @@ -151,7 +190,7 @@ - (instancetype)initWithCode:(unsigned short)aKeyCode if (aCharactersIgnoringModifiers) _charactersIgnoringModifiers = [aCharactersIgnoringModifiers copy]; - else + else if (aKeyCode != SRKeyCodeNone) _charactersIgnoringModifiers = [SRASCIISymbolicKeyCodeTransformer.sharedTransformer transformedValue:@(aKeyCode) withImplicitModifierFlags:nil explicitModifierFlags:@(aModifierFlags) @@ -211,13 +250,15 @@ - (BOOL)isEqualToDictionary:(NSDictionary *)aDictionary { if ([aDictionary[SRShortcutKeyKeyCode] isKindOfClass:NSNumber.class]) return [aDictionary[SRShortcutKeyKeyCode] unsignedShortValue] == self.keyCode && ([aDictionary[SRShortcutKeyModifierFlags] unsignedIntegerValue] & SRCocoaModifierFlagsMask) == self.modifierFlags; + else if (!aDictionary[SRShortcutKeyKeyCode] && self.keyCode == SRKeyCodeNone) + return ([aDictionary[SRShortcutKeyModifierFlags] unsignedIntegerValue] & SRCocoaModifierFlagsMask) == self.modifierFlags; else return NO; } - (BOOL)isEqualToKeyEquivalent:(nullable NSString *)aKeyEquivalent withModifierFlags:(NSEventModifierFlags)aModifierFlags { - if (!aKeyEquivalent.length) + if (!aKeyEquivalent.length || self.keyCode == SRKeyCodeNone) return NO; if ([self isEqualToKeyEquivalent:aKeyEquivalent @@ -236,13 +277,13 @@ - (BOOL)isEqualToKeyEquivalent:(NSString *)aKeyEquivalent withModifierFlags:(NSEventModifierFlags)aModifierFlags usingTransformer:(SRKeyCodeTransformer *)aTransformer { - if (!aKeyEquivalent.length) + if (!aKeyEquivalent.length || self.keyCode == SRKeyCodeNone) return NO; aModifierFlags &= SRCocoaModifierFlagsMask; - // Special case: Both ⇤ and ⇥ key equivalents respond to kVK_Tab. - if (self.keyCode == kVK_Tab && + // Special case: Both ⇤ and ⇥ key equivalents respond to SRKeyCodeTab. + if (self.keyCode == SRKeyCodeTab && self.modifierFlags == aModifierFlags && aKeyEquivalent.length == 1 && ([aKeyEquivalent characterAtIndex:0] == NSTabCharacter || @@ -401,6 +442,9 @@ @implementation SRShortcut (Carbon) - (UInt32)carbonKeyCode { + if (self.keyCode == SRKeyCodeNone) + os_trace_error("#Critical SRKeyCodeNone has no representation in Carbon"); + return self.keyCode; } @@ -408,26 +452,26 @@ - (UInt32)carbonModifierFlags { switch (self.carbonKeyCode) { - case kVK_F1: - case kVK_F2: - case kVK_F3: - case kVK_F4: - case kVK_F5: - case kVK_F6: - case kVK_F7: - case kVK_F8: - case kVK_F9: - case kVK_F10: - case kVK_F11: - case kVK_F12: - case kVK_F13: - case kVK_F14: - case kVK_F15: - case kVK_F16: - case kVK_F17: - case kVK_F18: - case kVK_F19: - case kVK_F20: + case SRKeyCodeF1: + case SRKeyCodeF2: + case SRKeyCodeF3: + case SRKeyCodeF4: + case SRKeyCodeF5: + case SRKeyCodeF6: + case SRKeyCodeF7: + case SRKeyCodeF8: + case SRKeyCodeF9: + case SRKeyCodeF10: + case SRKeyCodeF11: + case SRKeyCodeF12: + case SRKeyCodeF13: + case SRKeyCodeF14: + case SRKeyCodeF15: + case SRKeyCodeF16: + case SRKeyCodeF17: + case SRKeyCodeF18: + case SRKeyCodeF19: + case SRKeyCodeF20: return SRCocoaToCarbonFlags(self.modifierFlags) | NSFunctionKeyMask; default: return SRCocoaToCarbonFlags(self.modifierFlags); @@ -437,11 +481,14 @@ - (UInt32)carbonModifierFlags @end -NSString *SRReadableStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) +NSString *SRReadableStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) { SRKeyCodeTransformer *t = [SRKeyCodeTransformer sharedPlainTransformer]; NSString *c = [t transformedValue:@(aKeyCode)]; + if (!c) + c = [NSString stringWithFormat:@"<%hu>", aKeyCode]; + return [NSString stringWithFormat:@"%@%@%@%@%@", (aModifierFlags & NSEventModifierFlagCommand ? SRLoc(@"Command-") : @""), (aModifierFlags & NSEventModifierFlagOption ? SRLoc(@"Option-") : @""), @@ -451,11 +498,14 @@ - (UInt32)carbonModifierFlags } -NSString *SRReadableASCIIStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, unsigned short aKeyCode) +NSString *SRReadableASCIIStringForCocoaModifierFlagsAndKeyCode(NSEventModifierFlags aModifierFlags, SRKeyCode aKeyCode) { SRKeyCodeTransformer *t = [SRKeyCodeTransformer sharedPlainASCIITransformer]; NSString *c = [t transformedValue:@(aKeyCode)]; + if (!c) + c = [NSString stringWithFormat:@"<%hu>", aKeyCode]; + return [NSString stringWithFormat:@"%@%@%@%@%@", (aModifierFlags & NSEventModifierFlagCommand ? SRLoc(@"Command-") : @""), (aModifierFlags & NSEventModifierFlagOption ? SRLoc(@"Option-") : @""), @@ -465,7 +515,7 @@ - (UInt32)carbonModifierFlags } -BOOL SRKeyCodeWithFlagsEqualToKeyEquivalentWithFlags(unsigned short aKeyCode, +BOOL SRKeyCodeWithFlagsEqualToKeyEquivalentWithFlags(SRKeyCode aKeyCode, NSEventModifierFlags aKeyCodeFlags, NSString *aKeyEquivalent, NSEventModifierFlags aKeyEquivalentModifierFlags) diff --git a/Library/SRShortcutAction.h b/Library/SRShortcutAction.h index 5a450f8c..e6210bb3 100644 --- a/Library/SRShortcutAction.h +++ b/Library/SRShortcutAction.h @@ -183,6 +183,29 @@ NS_SWIFT_NAME(ShortcutAction) @end +/*! + Type of the keyboard event. + + @const SRKeyEventTypeUp Keyboard key is released. + @const SRKeyEventTypeDown Keyboard key is pressed. + */ +typedef NS_CLOSED_ENUM(NSUInteger, SRKeyEventType) +{ + SRKeyEventTypeUp = NSEventTypeKeyUp, + SRKeyEventTypeDown = NSEventTypeKeyDown +} NS_SWIFT_NAME(KeyEventType); + + +@interface NSEvent (SRShortcutAction) + +/*! + Keyboard event type as recognized by the shortcut recorder. + */ +@property (readonly) SRKeyEventType SR_keyEventType; + +@end + + /*! Base class for the SRGlobalShortcutMonitor and SRLocalShortcutMonitor. @@ -205,14 +228,6 @@ NS_SWIFT_NAME(ShortcutAction) NS_SWIFT_NAME(ShortcutMonitor) @interface SRShortcutMonitor : NSObject - -typedef NS_CLOSED_ENUM(NSUInteger, SRKeyEventType) -{ - SRKeyEventTypeUp = NSEventTypeKeyUp, - SRKeyEventTypeDown = NSEventTypeKeyDown -} NS_SWIFT_NAME(KeyEventType); - - /*! All shortcut actions. */ @@ -229,24 +244,20 @@ typedef NS_CLOSED_ENUM(NSUInteger, SRKeyEventType) - (NSArray *)actionsForKeyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(actions(forKeyEvent:)); /*! - All actions for a given shortcut and key event. + Enabled actions for a given shortcut and key event. @return Order is determined by the time of association such as that the last object is the most recently associated. - If the shortcut has no associated actions, returns an empty set. - */ -- (NSArray *)actionsForShortcut:(SRShortcut *)aShortcut - keyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(actions(forShortcut:keyEvent:)); - -/*! - The most recent action associated with a given shortcut and key event. + If the shortcut has no associated actions, returns an empty array. */ -- (nullable SRShortcutAction *)actionForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent; +- (NSArray *)enabledActionsForShortcut:(SRShortcut *)aShortcut + keyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(enabledActions(forShortcut:keyEvent:)); /*! Add an action to the monitor for a key event. - @note Adding the same action twice for the same key event makes it the most recent. + @discussion + Adding the same action for the same event type again only changes its order by making it the most recent. */ - (void)addAction:(SRShortcutAction *)anAction forKeyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(addAction(_:forKeyEvent:)); @@ -256,37 +267,40 @@ typedef NS_CLOSED_ENUM(NSUInteger, SRKeyEventType) - (void)removeAction:(SRShortcutAction *)anAction forKeyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(removeAction(_:forKeyEvent:)); /*! - Remove an action from the monitor. + Remove an action, if present, from the monitor. */ - (void)removeAction:(SRShortcutAction *)anAction NS_SWIFT_NAME(removeAction(_:)); /*! - Remove all actions for a given shortcut and key event. + Remove all actions from the monitor. */ -- (void)removeAllActionsForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(removeAllActions(forShortcut:keyEvent:)); +- (void)removeAllActions; /*! - Remove all actions for a given key event. - */ -- (void)removeAllActionsForKeyEvent:(SRKeyEventType)aKeyEvent NS_SWIFT_NAME(removeAllActions(forKeyEvent:)); + Called before the shortcut gets its first associated enabled action. -/*! - Remove all actions for a given shortcut. + @note Do not mutate actions within the callback. */ -- (void)removeAllActionsForShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(removeAllActions(forShortcut:)); +- (void)willAddShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(willAddShortcut(_:)); /*! - Remove all actions from the monitor. + Called after the shortcut gets its first associated enabled action. + + @note Do not mutate actions within the callback. */ -- (void)removeAllActions; +- (void)didAddShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(didAddShortcut(_:)); /*! - Called after the shortcut gets its first associated action. + Called before the shortcuts loses its last associated enabled action. + + @note Do not mutate actions within the callback. */ -- (void)didAddShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(didAddShortcut(_:)); +- (void)willRemoveShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(willRemoveShortcut(_:)); /*! - Called after the shortcut loses its last associated action. + Called after the shortcut loses its last associated enabled action. + + @note Do not mutate actions within the callback. */ - (void)didRemoveShortcut:(SRShortcut *)aShortcut NS_SWIFT_NAME(didRemoveShortcut(_:)); @@ -303,29 +317,21 @@ typedef NS_CLOSED_ENUM(NSUInteger, SRKeyEventType) @end +extern const OSType SRShortcutActionSignature; + + /*! - Handle shortcuts regardless of the currently active application. + Handle shortcuts regardless of the currently active application via Carbon Hot Key API. - @discussion - Action that corresponds to the shortcut is performed asyncrhonoysly in the specified dispatch queue. + @note Does not support shortcuts with the SRKeyCodeNone key code. + + @see SRAXGlobalShortcutMonitor */ NS_SWIFT_NAME(GlobalShortcutMonitor) @interface SRGlobalShortcutMonitor : SRShortcutMonitor @property (class, readonly) SRGlobalShortcutMonitor *sharedMonitor NS_SWIFT_NAME(shared); -/*! - Target dispatch queue for the action. - - @discussion: - Defaults to the main queue. - - The action block is detached and submitted asynchronously to the given queue. - - @seealso DISPATCH_BLOCK_NO_QOS_CLASS - */ -@property dispatch_queue_t dispatchQueue; - /*! Enable system-wide shortcut monitoring. @@ -355,7 +361,7 @@ NS_SWIFT_NAME(GlobalShortcutMonitor) If there is more than one action associated with the event, they are performed one by one either until one of them returns YES or the iteration is exhausted. */ -- (OSStatus)handleEvent:(nullable EventRef)anEvent; +- (OSStatus)handleEvent:(EventRef)anEvent; /*! Called after the carbon event handler is installed. @@ -370,6 +376,75 @@ NS_SWIFT_NAME(GlobalShortcutMonitor) @end +/*! + Handle shortcuts regardless of the currently active application via Quartz Event Service API. + + @discussion + Unlike SRGlobalShortcutMonitor it can handle shortcuts with the SRKeyCodeNone key code. But it has + security implications as this API requires the app to either run under the root user or been allowed + the Accessibility permission. + + The monitor automatically enables and disables the tap when needed. + + @see SRGlobalShortcutMonitor + @see AXIsProcessTrustedWithOptions + @see NSAppleEventsUsageDescription + */ +@interface SRAXGlobalShortcutMonitor : SRShortcutMonitor + +/*! + Mach port that corresponds to the event tap used under the hood. + */ +@property (readonly) CFMachPortRef eventTap; +- (CFMachPortRef)eventTap NS_RETURNS_INNER_POINTER CF_RETURNS_NOT_RETAINED; + +/*! + Run loop source that corresponds to the eventTap. + */ +@property (readonly) CFRunLoopSourceRef eventTapSource; +- (CFRunLoopSourceRef)eventTapSource NS_RETURNS_INNER_POINTER CF_RETURNS_NOT_RETAINED; + +/*! + Run loop that corresponds to the eventTap. + */ +@property (readonly) NSRunLoop *eventTapRunLoop; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability" +/*! + Initialize the monitor by installing the event tap in the current run loop. + */ +- (nullable instancetype)init; +#pragma clang diagnostic pop + +/*! + Initialize the monitor by installing the event tap in a given run loop. + + @param aRunLoop Run loop for the event tap. + + @discussion + Initialization may fail if it's impossible to create the event tap. + + @see https://stackoverflow.com/q/52738506/188530 + */ +- (nullable instancetype)initWithRunLoop:(NSRunLoop *)aRunLoop NS_DESIGNATED_INITIALIZER; + +/*! + Perform the action associated with a given event. + + @param anEvent A Quartz keyboard event. + + @return nil if event is handled; unchanged anEvent otherwise. + + @discussion + If there is more than one action associated with the event, they are performed one by one + either until one of them returns YES or the iteration is exhausted. + */ +- (nullable CGEventRef)handleEvent:(CGEventRef)anEvent; + +@end + + /*! Handle AppKit's keyboard events. @@ -386,12 +461,12 @@ NS_SWIFT_NAME(LocalShortcutMonitor) @seealso NSStandardKeyBindingResponding */ -@property (class, readonly) SRLocalShortcutMonitor *standardShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *standardShortcuts; /*! Shortcuts that mimic default main menu for a new Cocoa Applications. */ -@property (class, readonly) SRLocalShortcutMonitor *mainMenuShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *mainMenuShortcuts; /*! Shortcuts associated with the clipboard. @@ -403,7 +478,7 @@ NS_SWIFT_NAME(LocalShortcutMonitor) - redo: - undo: */ -@property (class, readonly) SRLocalShortcutMonitor *clipboardShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *clipboardShortcuts; /*! Shortcuts associated with window management. @@ -412,7 +487,7 @@ NS_SWIFT_NAME(LocalShortcutMonitor) - performMiniaturize: - toggleFullScreen: */ -@property (class, readonly) SRLocalShortcutMonitor *windowShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *windowShortcuts; /*! Key bindings associated with document management. @@ -425,7 +500,7 @@ NS_SWIFT_NAME(LocalShortcutMonitor) - duplicateDocument: - openDocument: */ -@property (class, readonly) SRLocalShortcutMonitor *documentShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *documentShortcuts; /*! Key bindings associated with application management. @@ -434,7 +509,7 @@ NS_SWIFT_NAME(LocalShortcutMonitor) - hideOtherApplications: - terminate: */ -@property (class, readonly) SRLocalShortcutMonitor *appShortcuts; +@property (class, readonly, copy) SRLocalShortcutMonitor *appShortcuts; /*! Perform the action associated with the event, if any. @@ -447,7 +522,7 @@ NS_SWIFT_NAME(LocalShortcutMonitor) If there are more than one action associated with the event, they are performed one by one either until one of them returns YES or the iteration is exhausted. */ -- (BOOL)handleEvent:(nullable NSEvent *)anEvent withTarget:(nullable id)aTarget; +- (BOOL)handleEvent:(NSEvent *)anEvent withTarget:(nullable id)aTarget; /*! Update the monitor with system-wide and user-specific Cocoa Text System key bindings. diff --git a/Library/SRShortcutAction.m b/Library/SRShortcutAction.m index 0e78faa1..44bd1b85 100644 --- a/Library/SRShortcutAction.m +++ b/Library/SRShortcutAction.m @@ -111,7 +111,7 @@ - (SRShortcut *)shortcut - (void)setShortcut:(SRShortcut *)aShortcut { - os_activity_initiate("Setting raw shortcut", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRShortcutAction setShortcut:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ @synchronized (self) { [self willChangeValueForKey:@"observedObject"]; @@ -135,7 +135,7 @@ - (void)setShortcut:(SRShortcut *)aShortcut - (void)setObservedObject:(id)newObservedObject withKeyPath:(NSString *)newKeyPath { - os_activity_initiate("Setting autoupdating shortcut", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRShortcutAction setObservedObject:withKeyPath:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ @synchronized (self) { if (newObservedObject == self->_observedObject && [self->_observedKeyPath isEqualToString:newKeyPath]) @@ -186,9 +186,11 @@ - (void)setActionHandler:(SRShortcutActionHandler)newActionHandler - (id)target { + id strongTarget = _target; + @synchronized (self) { - return _target != nil ? _target : NSApplication.sharedApplication; + return strongTarget != nil ? strongTarget : NSApplication.sharedApplication; } } @@ -196,13 +198,17 @@ - (void)setTarget:(id)newTarget { @synchronized (self) { - if (newTarget == _target) + id strongTarget = _target; + + if (newTarget == strongTarget) return; + strongTarget = newTarget; + [self willChangeValueForKey:@"target"]; - _target = newTarget; + _target = strongTarget; - if (_target && _actionHandler) + if (strongTarget && _actionHandler) { [self willChangeValueForKey:@"actionHandler"]; _actionHandler = nil; @@ -218,7 +224,8 @@ - (void)setTarget:(id)newTarget - (BOOL)performActionOnTarget:(id)aTarget { __block BOOL isPerformed = NO; - os_activity_initiate("Performing shortcut action", OS_ACTIVITY_FLAG_DEFAULT, ^{ + + os_activity_initiate("-[SRShortcutAction performActionOnTarget:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ if (!self.isEnabled) { os_trace_debug("Not performed: disabled"); @@ -297,6 +304,7 @@ - (BOOL)performActionOnTarget:(id)aTarget } } }); + return isPerformed; } @@ -304,8 +312,10 @@ - (BOOL)performActionOnTarget:(id)aTarget - (void)_invalidateObserving { - if (_observedObject) - [_observedObject removeObserver:self forKeyPath:_observedKeyPath context:_SRShortcutActionContext]; + id strongObservedObject = _observedObject; + + if (strongObservedObject) + [strongObservedObject removeObserver:self forKeyPath:_observedKeyPath context:_SRShortcutActionContext]; _observedObject = nil; _observedKeyPath = nil; @@ -324,7 +334,7 @@ - (void)observeValueForKeyPath:(NSString *)aKeyPath return; } - os_activity_initiate("Observing new shortcut", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRShortcutAction observeValueForKeyPath:ofObject:change:context:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ SRShortcut *newShortcut = aChange[NSKeyValueChangeNewKey]; // NSController subclasses are notable for not setting the New and Old keys of the change dictionary. @@ -356,18 +366,59 @@ - (void)observeValueForKeyPath:(NSString *)aKeyPath #pragma mark - +@implementation NSEvent (SRShortcutAction) + +- (SRKeyEventType)SR_keyEventType +{ + SRKeyEventType eventType = 0; + + switch (self.type) + { + case NSEventTypeKeyDown: + eventType = SRKeyEventTypeDown; + break; + case NSEventTypeKeyUp: + eventType = SRKeyEventTypeUp; + break; + case NSEventTypeFlagsChanged: + { + __auto_type keyCode = self.keyCode; + if (keyCode == kVK_Command || keyCode == kVK_RightCommand) + eventType = self.modifierFlags & NSEventModifierFlagCommand ? SRKeyEventTypeDown : SRKeyEventTypeUp; + else if (keyCode == kVK_Option || keyCode == kVK_RightOption) + eventType = self.modifierFlags & NSEventModifierFlagOption ? SRKeyEventTypeDown : SRKeyEventTypeUp; + else if (keyCode == kVK_Shift || keyCode == kVK_RightShift) + eventType = self.modifierFlags & NSEventModifierFlagShift ? SRKeyEventTypeDown : SRKeyEventTypeUp; + else if (keyCode == kVK_Control || keyCode == kVK_RightControl) + eventType = self.modifierFlags & NSEventModifierFlagControl ? SRKeyEventTypeDown : SRKeyEventTypeUp; + else + os_trace("#Error Unexpected key code %hu for the FlagsChanged event", keyCode); + break; + } + default: + os_trace("#Error Unexpected key event of type %lu", self.type); + break; + } + + return eventType; +} + +@end + + static void *_SRShortcutMonitorContext = &_SRShortcutMonitorContext; @interface SRShortcutMonitor () { @protected - NSMutableDictionary *> *_shortcutToKeyDownActions; - NSMutableDictionary *> *_shortcutToKeyUpActions; - NSCountedSet *_shortcuts; NSCountedSet *_actions; + NSMutableSet *_enabledActions; NSMutableSet *_keyUpActions; NSMutableSet *_keyDownActions; + NSMutableDictionary *> *_shortcutToEnabledKeyDownActions; + NSMutableDictionary *> *_shortcutToEnabledKeyUpActions; + NSCountedSet *_shortcuts; // count increased for every enabled action } @end @@ -380,12 +431,13 @@ - (instancetype)init if (self) { - _shortcutToKeyDownActions = [NSMutableDictionary new]; - _shortcutToKeyUpActions = [NSMutableDictionary new]; - _shortcuts = [NSCountedSet new]; _actions = [NSCountedSet new]; + _enabledActions = [NSMutableSet new]; + _shortcutToEnabledKeyDownActions = [NSMutableDictionary new]; + _shortcutToEnabledKeyUpActions = [NSMutableDictionary new]; _keyUpActions = [NSMutableSet new]; _keyDownActions = [NSMutableSet new]; + _shortcuts = [NSCountedSet new]; } return self; @@ -393,10 +445,11 @@ - (instancetype)init - (void)dealloc { - for (SRShortcutAction *action in _actions) - { - [action removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; - } + for (SRShortcutAction *a in _actions) + [a removeObserver:self forKeyPath:@"enabled" context:_SRShortcutMonitorContext]; + + for (SRShortcutAction *a in _enabledActions) + [a removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; } #pragma mark Properties @@ -427,49 +480,53 @@ - (void)dealloc } } -- (NSArray *)actionsForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent +- (NSArray *)enabledActionsForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent { @synchronized (_actions) { - __auto_type result = [self _actionsForShortcut:aShortcut keyEvent:aKeyEvent]; + __auto_type result = [self _enabledActionsForShortcut:aShortcut keyEvent:aKeyEvent]; return result != nil ? [NSArray arrayWithArray:result.array] : [NSArray new]; } } -- (SRShortcutAction *)actionForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent -{ - @synchronized (_actions) - { - return [[self _actionsForShortcut:aShortcut keyEvent:aKeyEvent] lastObject]; - } -} - - (void)addAction:(SRShortcutAction *)anAction forKeyEvent:(SRKeyEventType)aKeyEvent { @synchronized (_actions) { + NSAssert([_actions countForObject:anAction] < 2, @"Action is added too many times"); + __auto_type keyEventActions = [self _actionsForKeyEvent:aKeyEvent]; + BOOL isFirstActionForKeyEvent = ![keyEventActions containsObject:anAction]; - if (![keyEventActions containsObject:anAction]) + if (isFirstActionForKeyEvent) { + BOOL isFirstAction = ![_actions countForObject:anAction]; + + if (isFirstAction) + [self willChangeValueForKey:@"actions"]; + [_actions addObject:anAction]; - NSAssert([_actions countForObject:anAction] <= 2, @"Action is added too many times"); [keyEventActions addObject:anAction]; - if ([_actions countForObject:anAction] == 1) + if (isFirstAction) { [anAction addObserver:self - forKeyPath:@"shortcut" - options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + forKeyPath:@"enabled" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial context:_SRShortcutMonitorContext]; } + else if ([_enabledActions containsObject:anAction]) + { + __auto_type shortcut = [self _shortcutForEnabledAction:anAction hint:nil]; + [self _addEnabledAction:anAction toShortcut:shortcut forKeyEvent:aKeyEvent]; + } - if (anAction.shortcut) - [self _addAction:anAction toShortcut:anAction.shortcut forKeyEvent:aKeyEvent]; + if (isFirstAction) + [self didChangeValueForKey:@"actions"]; } else if (anAction.shortcut) { - __auto_type shortcutActions = [self _actionsForShortcut:anAction.shortcut keyEvent:aKeyEvent]; + __auto_type shortcutActions = [self _enabledActionsForShortcut:anAction.shortcut keyEvent:aKeyEvent]; NSAssert(shortcutActions.count, @"Action was not added to the shortcut"); NSUInteger fromIndex = [shortcutActions indexOfObject:anAction]; NSAssert(fromIndex != NSNotFound, @"Action was not added to the shortcut"); @@ -486,14 +543,48 @@ - (void)removeAction:(SRShortcutAction *)anAction forKeyEvent:(SRKeyEventType)aK if (![keyEventActions containsObject:anAction]) return; + BOOL isLastAction = [_actions countForObject:anAction] == 1; + + if (isLastAction) + { + [self willChangeValueForKey:@"actions"]; + [anAction removeObserver:self forKeyPath:@"enabled" context:_SRShortcutMonitorContext]; + } + + BOOL isLastActionForShortcut = NO; + SRShortcut *shortcut = nil; + + if ([_enabledActions containsObject:anAction]) + { + if (isLastAction) + [anAction removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; + + shortcut = [self _shortcutForEnabledAction:anAction hint:nil]; + isLastActionForShortcut = [_shortcuts countForObject:shortcut] == 1; + + if (isLastActionForShortcut) + { + [self willChangeValueForKey:@"shortcuts"]; + [self willRemoveShortcut:shortcut]; + } + + [self _removeEnabledAction:anAction fromShortcut:shortcut forKeyEvent:aKeyEvent]; + + if (isLastAction) + [_enabledActions removeObject:anAction]; + } + [keyEventActions removeObject:anAction]; [_actions removeObject:anAction]; - if (![_actions countForObject:anAction]) - [anAction removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; + if (isLastActionForShortcut) + { + [self didRemoveShortcut:shortcut]; + [self didChangeValueForKey:@"shortcuts"]; + } - if (anAction.shortcut) - [self _removeAction:anAction fromShortcut:anAction.shortcut forKeyEvent:aKeyEvent]; + if (isLastAction) + [self didChangeValueForKey:@"actions"]; } } @@ -506,66 +597,54 @@ - (void)removeAction:(SRShortcutAction *)anAction } } -- (void)removeAllActionsForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent +- (void)removeAllActions { @synchronized (_actions) { - for (SRShortcutAction *action in [self actionsForShortcut:aShortcut keyEvent:aKeyEvent]) - { - [self removeAction:action forKeyEvent:aKeyEvent]; - } - } -} + for (SRShortcutAction *a in _actions) + [a removeObserver:self forKeyPath:@"enabled" context:_SRShortcutMonitorContext]; -- (void)removeAllActionsForKeyEvent:(SRKeyEventType)aKeyEvent -{ - @synchronized (_actions) - { - for (SRShortcutAction *action in [self actionsForKeyEvent:aKeyEvent]) - { - [self removeAction:action forKeyEvent:aKeyEvent]; - } - } -} + for (SRShortcutAction *a in _enabledActions) + [a removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; -- (void)removeAllActionsForShortcut:(SRShortcut *)aShortcut -{ - @synchronized (_actions) - { - [self removeAllActionsForShortcut:aShortcut keyEvent:SRKeyEventTypeDown]; - [self removeAllActionsForShortcut:aShortcut keyEvent:SRKeyEventTypeUp]; - } -} + [self willChangeValueForKey:@"actions"]; + [self willChangeValueForKey:@"shortcuts"]; -- (void)removeAllActions -{ - @synchronized (_actions) - { - for (SRShortcutAction *action in _actions) - { - [action removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; - } + __auto_type oldShortcuts = _shortcuts.allObjects; + for (SRShortcut *s in oldShortcuts) + [self willRemoveShortcut:s]; - [_shortcutToKeyDownActions removeAllObjects]; - [_shortcutToKeyUpActions removeAllObjects]; + _shortcuts = [NSCountedSet new]; [_actions removeAllObjects]; + [_enabledActions removeAllObjects]; [_keyUpActions removeAllObjects]; [_keyDownActions removeAllObjects]; + [_shortcutToEnabledKeyDownActions removeAllObjects]; + [_shortcutToEnabledKeyUpActions removeAllObjects]; - __auto_type oldShortcuts = _shortcuts; - _shortcuts = [NSCountedSet new]; - - for (SRShortcut *shortcut in oldShortcuts) + [oldShortcuts enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(SRShortcut * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [self didRemoveShortcut:shortcut]; - } + [self didRemoveShortcut:obj]; + }]; + + [self didChangeValueForKey:@"shortcuts"]; + [self didChangeValueForKey:@"actions"]; } } +- (void)willAddShortcut:(SRShortcut *)aShortcut +{ +} + - (void)didAddShortcut:(SRShortcut *)aShortcut { } +- (void)willRemoveShortcut:(SRShortcut *)aShortcut +{ +} + - (void)didRemoveShortcut:(SRShortcut *)aShortcut { } @@ -586,94 +665,144 @@ - (void)didRemoveShortcut:(SRShortcut *)aShortcut } } -- (NSMutableDictionary *> *)_shortcutToActionsForKeyEvent:(SRKeyEventType)aKeyEvent +- (NSMutableDictionary *> *)_shortcutToEnabledActionsForKeyEvent:(SRKeyEventType)aKeyEvent { switch (aKeyEvent) { case SRKeyEventTypeDown: - return _shortcutToKeyDownActions; + return _shortcutToEnabledKeyDownActions; case SRKeyEventTypeUp: - return _shortcutToKeyUpActions; + return _shortcutToEnabledKeyUpActions; default: [NSException raise:NSInvalidArgumentException format:@"Unexpected keyboard event type %lu", aKeyEvent]; return nil; } } -- (nullable NSMutableOrderedSet *)_actionsForShortcut:(SRShortcut *)aShortcut keyEvent:(SRKeyEventType)aKeyEvent +- (nullable NSMutableOrderedSet *)_enabledActionsForShortcut:(nonnull SRShortcut *)aShortcut + keyEvent:(SRKeyEventType)aKeyEvent { - return [[self _shortcutToActionsForKeyEvent:aKeyEvent] objectForKey:aShortcut]; + return [[self _shortcutToEnabledActionsForKeyEvent:aKeyEvent] objectForKey:aShortcut]; +} + +- (nonnull SRShortcut *)_shortcutForEnabledAction:(nonnull SRShortcutAction *)anAction hint:(nullable SRShortcut *)aShortcut +{ + NSParameterAssert([_enabledActions containsObject:anAction]); + + __auto_type checkShortcut = ^(SRShortcut * _Nullable aShortcut) { + if (!aShortcut) + return NO; + + if (self->_shortcutToEnabledKeyDownActions[aShortcut] != nil && + [self->_shortcutToEnabledKeyDownActions[aShortcut] containsObject:anAction]) + { + return YES; + } + else if (self->_shortcutToEnabledKeyUpActions[aShortcut] != nil && + [self->_shortcutToEnabledKeyUpActions[aShortcut] containsObject:anAction]) + { + return YES; + } + + return NO; + }; + + if (checkShortcut(aShortcut)) + return aShortcut; + else if (checkShortcut(anAction.shortcut)) + return anAction.shortcut; + else + { + for (SRShortcut *s in _shortcuts) + { + if (checkShortcut(s)) + return s; + } + } + + __builtin_unreachable(); } -- (void)_actionDidChangeShortcut:(SRShortcutAction *)anAction from:(SRShortcut *)oldShortcut to:(SRShortcut *)newShortcut +- (void)_enabledActionDidChangeShortcut:(nonnull SRShortcutAction *)anAction + from:(nullable SRShortcut *)anOldShortcut + to:(nullable SRShortcut *)aNewShortcut { + NSParameterAssert(![anOldShortcut isEqual:aNewShortcut]); + BOOL isKeyDownAction = [_keyDownActions containsObject:anAction]; BOOL isKeyUpAction = [_keyUpActions containsObject:anAction]; + BOOL isLastActionForOldShortcut = anOldShortcut && [_shortcuts countForObject:anOldShortcut] == 1; + BOOL isFirstActionForNewShortcut = aNewShortcut && [_shortcuts countForObject:aNewShortcut] == 0; + + if (isLastActionForOldShortcut || isFirstActionForNewShortcut) + [self willChangeValueForKey:@"shortcuts"]; + + if (isLastActionForOldShortcut) + [self willRemoveShortcut:anOldShortcut]; - if (oldShortcut) + if (anOldShortcut) { if (isKeyDownAction) - [self _removeAction:anAction fromShortcut:oldShortcut forKeyEvent:SRKeyEventTypeDown]; + [self _removeEnabledAction:anAction fromShortcut:anOldShortcut forKeyEvent:SRKeyEventTypeDown]; if (isKeyUpAction) - [self _removeAction:anAction fromShortcut:oldShortcut forKeyEvent:SRKeyEventTypeUp]; + [self _removeEnabledAction:anAction fromShortcut:anOldShortcut forKeyEvent:SRKeyEventTypeUp]; } - if (newShortcut) + if (isLastActionForOldShortcut) + [self didRemoveShortcut:anOldShortcut]; + + if (isFirstActionForNewShortcut) + [self willAddShortcut:aNewShortcut]; + + if (aNewShortcut) { if (isKeyDownAction) - [self _addAction:anAction toShortcut:newShortcut forKeyEvent:SRKeyEventTypeDown]; + [self _addEnabledAction:anAction toShortcut:aNewShortcut forKeyEvent:SRKeyEventTypeDown]; if (isKeyUpAction) - [self _addAction:anAction toShortcut:newShortcut forKeyEvent:SRKeyEventTypeUp]; + [self _addEnabledAction:anAction toShortcut:aNewShortcut forKeyEvent:SRKeyEventTypeUp]; } + + if (isFirstActionForNewShortcut) + [self didAddShortcut:aNewShortcut]; + + if (isLastActionForOldShortcut || isFirstActionForNewShortcut) + [self didChangeValueForKey:@"shortcuts"]; } -/*! - Add the action to the shortcut, optionally calling the hook. - */ -- (void)_addAction:(SRShortcutAction *)anAction toShortcut:(SRShortcut *)aShortcut forKeyEvent:(SRKeyEventType)aKeyEvent +- (void)_addEnabledAction:(nonnull SRShortcutAction *)anAction + toShortcut:(nonnull SRShortcut *)aShortcut + forKeyEvent:(SRKeyEventType)aKeyEvent { - __auto_type shortcutToActions = [self _shortcutToActionsForKeyEvent:aKeyEvent]; - __auto_type shortcutActions = shortcutToActions[aShortcut]; - NSParameterAssert(![shortcutActions containsObject:anAction]); + __auto_type shortcutToActions = [self _shortcutToEnabledActionsForKeyEvent:aKeyEvent]; + __auto_type actions = shortcutToActions[aShortcut]; + NSParameterAssert(![actions containsObject:anAction]); - BOOL isNewShortcut = [_shortcuts countForObject:aShortcut] == 0; + [_shortcuts addObject:aShortcut]; - if (!shortcutActions) + if (!actions) { - shortcutActions = [NSMutableOrderedSet orderedSetWithObject:anAction]; - shortcutToActions[aShortcut] = shortcutActions; - [_shortcuts addObject:aShortcut]; + actions = [NSMutableOrderedSet orderedSetWithObject:anAction]; + shortcutToActions[aShortcut] = actions; } else - [shortcutActions addObject:anAction]; - - if (isNewShortcut) - [self didAddShortcut:aShortcut]; + [actions addObject:anAction]; } -/*! - Remove the action from the shortcut, optionally calling the hook. - */ -- (void)_removeAction:(SRShortcutAction *)anAction fromShortcut:(SRShortcut *)aShortcut forKeyEvent:(SRKeyEventType)aKeyEvent +- (void)_removeEnabledAction:(nonnull SRShortcutAction *)anAction + fromShortcut:(SRShortcut *)aShortcut + forKeyEvent:(SRKeyEventType)aKeyEvent { - NSParameterAssert([_shortcuts containsObject:aShortcut]); - - __auto_type shortcutToActions = [self _shortcutToActionsForKeyEvent:aKeyEvent]; - __auto_type shortcutActions = shortcutToActions[aShortcut]; - NSParameterAssert([shortcutActions containsObject:anAction]); + __auto_type shortcutToActions = [self _shortcutToEnabledActionsForKeyEvent:aKeyEvent]; + __auto_type actions = shortcutToActions[aShortcut]; + NSParameterAssert([actions containsObject:anAction]); - [shortcutActions removeObject:anAction]; + [_shortcuts removeObject:aShortcut]; + [actions removeObject:anAction]; - if (!shortcutActions.count) - { + if (!actions.count) shortcutToActions[aShortcut] = nil; - [_shortcuts removeObject:aShortcut]; - } - - if (![_shortcuts countForObject:aShortcut]) - [self didRemoveShortcut:aShortcut]; } #pragma mark NSObject @@ -685,20 +814,110 @@ - (void)observeValueForKeyPath:(NSString *)aKeyPath { if (aContext == _SRShortcutMonitorContext) { - SRShortcut *oldShortcut = aChange[NSKeyValueChangeOldKey]; - SRShortcut *newShortcut = aChange[NSKeyValueChangeNewKey]; + __auto_type action = (SRShortcutAction *)anObject; - @synchronized (_actions) + if ([aKeyPath isEqualToString:@"enabled"]) { - [self _actionDidChangeShortcut:(SRShortcutAction *)anObject - from:((id)oldShortcut == NSNull.null) ? nil : oldShortcut - to:((id)newShortcut == NSNull.null) ? nil : newShortcut]; + BOOL wasEnabled = [aChange[NSKeyValueChangeOldKey] boolValue]; // NO for NSKeyValueObservingOptionInitial + BOOL isEnabled = [aChange[NSKeyValueChangeNewKey] boolValue]; + + if (wasEnabled == isEnabled) + return; + + if (isEnabled) + { + [_enabledActions addObject:action]; + [action addObserver:self + forKeyPath:@"shortcut" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial + context:_SRShortcutMonitorContext]; + } + else + { + [action removeObserver:self forKeyPath:@"shortcut" context:_SRShortcutMonitorContext]; + + @synchronized (_actions) + { + __auto_type shortcut = [self _shortcutForEnabledAction:action hint:nil]; + BOOL isLastActionForShortcut = [_shortcuts countForObject:shortcut] == 1; + + if (isLastActionForShortcut) + { + [self willChangeValueForKey:@"shortcuts"]; + [self willRemoveShortcut:shortcut]; + } + + if ([_keyDownActions containsObject:action]) + [self _removeEnabledAction:action fromShortcut:shortcut forKeyEvent:SRKeyEventTypeDown]; + + if ([_keyUpActions containsObject:action]) + [self _removeEnabledAction:action fromShortcut:shortcut forKeyEvent:SRKeyEventTypeUp]; + + if (isLastActionForShortcut) + { + [self didRemoveShortcut:shortcut]; + [self didChangeValueForKey:@"shortcuts"]; + } + + [_enabledActions removeObject:action]; + } + } + } + else if ([aKeyPath isEqualToString:@"shortcut"]) + { + SRShortcut *oldShortcut = aChange[NSKeyValueChangeOldKey]; // nil for NSKeyValueObservingOptionInitial + SRShortcut *newShortcut = aChange[NSKeyValueChangeNewKey]; + + if ([oldShortcut isEqual:newShortcut]) + return; + + @synchronized (_actions) + { + [self _enabledActionDidChangeShortcut:action + from:((id)oldShortcut == NSNull.null) ? nil : oldShortcut + to:((id)newShortcut == NSNull.null) ? nil : newShortcut]; + } } } else [super observeValueForKeyPath:aKeyPath ofObject:anObject change:aChange context:aContext]; } +- (NSString *)debugDescription +{ + NSMutableString *d = [NSMutableString new]; + __auto_type formatActions = ^(NSMutableDictionary *> *aShortcutToActions) { + for (SRShortcut *s in aShortcutToActions) + { + [d appendFormat:@"\t%@: {\n", s]; + + for (SRShortcutAction *a in aShortcutToActions[s]) + [d appendFormat:@"\t\t%@\n", a]; + + [d appendString:@"\t}\n"]; + } + }; + + if (_shortcutToEnabledKeyDownActions.count) + { + [d appendString:@"Key Down Shortcuts: {\n"]; + formatActions(_shortcutToEnabledKeyDownActions); + [d appendString:@"}\n"]; + } + + if (_shortcutToEnabledKeyUpActions.count) + { + [d appendString:@"Key Up Shortcuts: {\n"]; + formatActions(_shortcutToEnabledKeyUpActions); + [d appendString:@"}\n"]; + } + + if (d.length) + return d; + else + return @"No Shortcuts"; +} + @end @@ -723,11 +942,6 @@ - (SRShortcutAction *)addAction:(SEL)anAction forKeyEquivalent:(NSString *)aKeyE static const UInt32 _SRInvalidHotKeyID = 0; -static OSStatus SRCarbonEventHandler(EventHandlerCallRef aHandler, EventRef anEvent, void *aUserData) -{ - return [(__bridge SRGlobalShortcutMonitor *)aUserData handleEvent:anEvent]; -} - @implementation SRGlobalShortcutMonitor { @@ -738,6 +952,22 @@ @implementation SRGlobalShortcutMonitor NSInteger _disableCounter; } +static OSStatus _SRCarbonEventHandler(EventHandlerCallRef aHandler, EventRef anEvent, void *aUserData) +{ + if (!anEvent) + { + os_trace_error("#Error Event is NULL"); + return eventNotHandledErr; + } + else if (GetEventClass(anEvent) != kEventClassKeyboard) + { + os_trace_error("#Error Not a keyboard event"); + return eventNotHandledErr; + } + else + return [(__bridge SRGlobalShortcutMonitor *)aUserData handleEvent:anEvent]; +} + + (SRGlobalShortcutMonitor *)sharedMonitor { static SRGlobalShortcutMonitor *Shared = nil; @@ -758,7 +988,6 @@ - (instancetype)init _shortcutToHotKeyRef = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality]; _shortcutToHotKeyId = [NSMutableDictionary new]; - _dispatchQueue = dispatch_get_main_queue(); } return self; @@ -781,13 +1010,13 @@ - (void)resume os_trace_debug("Global Shortcut Monitor counter: %ld -> %ld", _disableCounter, _disableCounter - 1); _disableCounter -= 1; - [self _installEventHandlerIfNeeded]; - if (_disableCounter == 0) { for (SRShortcut *shortcut in _shortcuts) [self _registerHotKeyForShortcutIfNeeded:shortcut]; } + + [self _installEventHandlerIfNeeded]; } } @@ -810,47 +1039,24 @@ - (void)pause - (OSStatus)handleEvent:(EventRef)anEvent { - __block OSStatus error = noErr; + __block OSStatus error = eventNotHandledErr; - os_activity_initiate("Handling Carbon event", OS_ACTIVITY_FLAG_DETACHED, ^{ + os_activity_initiate("-[SRGlobalShortcutMonitor handleEvent:]", OS_ACTIVITY_FLAG_DETACHED, ^{ if (self->_disableCounter > 0) { os_trace_debug("Monitoring is currently disabled"); - error = eventNotHandledErr; - return; - } - else if (!anEvent) - { - os_trace_error("#Error Event is NULL"); - error = eventNotHandledErr; - return; - } - else if (GetEventClass(anEvent) != kEventClassKeyboard) - { - os_trace_error("#Error Not a keyboard event"); - error = eventNotHandledErr; return; } EventHotKeyID hotKeyID; - error = GetEventParameter(anEvent, - kEventParamDirectObject, - typeEventHotKeyID, - NULL, - sizeof(hotKeyID), - NULL, - &hotKeyID); - - if (error != noErr) + if (GetEventParameter(anEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID) != noErr) { os_trace_error("#Critical Failed to get hot key ID: %d", error); - error = eventNotHandledErr; return; } else if (hotKeyID.id == 0 || hotKeyID.signature != SRShortcutActionSignature) { os_trace_error("#Error Unexpected hot key with id %u and signature: %u", hotKeyID.id, hotKeyID.signature); - error = eventNotHandledErr; return; } @@ -861,7 +1067,6 @@ - (OSStatus)handleEvent:(EventRef)anEvent if (!shortcut) { os_trace("Unregistered hot key with id %u and signature %u", hotKeyID.id, hotKeyID.signature); - error = eventNotHandledErr; return; } @@ -876,24 +1081,27 @@ - (OSStatus)handleEvent:(EventRef)anEvent break; default: os_trace("#Error Unexpected key event of type %u", GetEventKind(anEvent)); - error = eventNotHandledErr; return; } - __auto_type actions = [self actionsForShortcut:shortcut keyEvent:eventType]; + __auto_type actions = [self enabledActionsForShortcut:shortcut keyEvent:eventType]; if (!actions.count) { os_trace("No actions for the shortcut"); - error = eventNotHandledErr; return; } - dispatch_async(self.dispatchQueue, dispatch_block_create(DISPATCH_BLOCK_NO_QOS_CLASS, ^{ - [actions enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) { - *stop = [obj performActionOnTarget:nil]; - }]; - })); + __block BOOL isHandled = NO; + + [actions enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) + { + *stop = isHandled = [obj performActionOnTarget:nil]; + }]; + + if (isHandled) + error = noErr; } }); @@ -902,10 +1110,12 @@ - (OSStatus)handleEvent:(EventRef)anEvent - (void)didAddEventHandler { + os_trace_debug("Added Carbon HotKey Event Handler"); } - (void)didRemoveEventHandler { + os_trace_debug("Removed Carbon HotKey Event Handler"); } #pragma mark Private @@ -915,19 +1125,18 @@ - (void)_installEventHandlerIfNeeded if (_carbonEventHandler) return; - // _shortcuts is checked instead of _shortcutToHotKeyRef because the handler is added before the registration. - if (_disableCounter > 0 || !_shortcuts.count) + if (_disableCounter > 0 || !_shortcutToHotKeyRef.count) return; - static const EventTypeSpec eventSpec[] = { + static const EventTypeSpec EventSpec[] = { { kEventClassKeyboard, kEventHotKeyPressed }, { kEventClassKeyboard, kEventHotKeyReleased } }; os_trace("Installing Carbon hot key event handler"); OSStatus error = InstallEventHandler(GetEventDispatcherTarget(), - (EventHandlerProcPtr)SRCarbonEventHandler, - sizeof(eventSpec) / sizeof(EventTypeSpec), - eventSpec, + _SRCarbonEventHandler, + sizeof(EventSpec) / sizeof(EventTypeSpec), + EventSpec, (__bridge void *)self, &_carbonEventHandler); @@ -945,7 +1154,6 @@ - (void)_removeEventHandlerIfNeeded if (!_carbonEventHandler) return; - // _shortcutToHotKeyRef is checked instead of _shortcuts because the handler is removed after the registrations. if (_disableCounter <= 0 && _shortcutToHotKeyRef.count) return; @@ -955,7 +1163,7 @@ - (void)_removeEventHandlerIfNeeded if (error != noErr) os_trace_error("#Error Failed to remove event handler: %d", error); - // Assume that an error to remove the handler is due to the latter being invalid. + // Assume that an error happened due to _carbonEventHandler being invalid. _carbonEventHandler = NULL; [self didRemoveEventHandler]; } @@ -967,6 +1175,12 @@ - (void)_registerHotKeyForShortcutIfNeeded:(SRShortcut *)aShortcut if (hotKey) return; + if (aShortcut.keyCode == SRKeyCodeNone) + { + os_trace_error("#Error Shortcut without a key code cannot be registered as Carbon hot key"); + return; + } + static UInt32 CarbonID = _SRInvalidHotKeyID; EventHotKeyID hotKeyID = {SRShortcutActionSignature, ++CarbonID}; os_trace("Registering Carbon hot key"); @@ -974,7 +1188,7 @@ - (void)_registerHotKeyForShortcutIfNeeded:(SRShortcut *)aShortcut aShortcut.carbonModifierFlags, hotKeyID, GetEventDispatcherTarget(), - 0, + kEventHotKeyNoOptions, &hotKey); if (error != noErr || !hotKey) @@ -1037,7 +1251,7 @@ - (void)didAddShortcut:(SRShortcut *)aShortcut [self _installEventHandlerIfNeeded]; } -- (void)didRemoveShortcut:(SRShortcut *)aShortcut +- (void)willRemoveShortcut:(SRShortcut *)aShortcut { [self _unregisterHotKeyForShortcutIfNeeded:aShortcut]; [self _removeEventHandlerIfNeeded]; @@ -1046,6 +1260,125 @@ - (void)didRemoveShortcut:(SRShortcut *)aShortcut @end +@implementation SRAXGlobalShortcutMonitor + +CGEventRef _Nullable _SRQuartzEventHandler(CGEventTapProxy aProxy, CGEventType aType, CGEventRef anEvent, void * _Nullable aUserInfo) +{ + __auto_type self = (__bridge SRAXGlobalShortcutMonitor *)aUserInfo; + + if (aType == kCGEventTapDisabledByTimeout || aType == kCGEventTapDisabledByUserInput) + { + os_trace("#Error #Developer The system disabled event tap due to %u", aType); + CGEventTapEnable(self.eventTap, true); + return anEvent; + } + else if (aType != kCGEventKeyDown && aType != kCGEventKeyUp && aType != kCGEventFlagsChanged) + { + os_trace("#Error #Developer Unexpected event of type %u", aType); + return anEvent; + } + else + return [self handleEvent:anEvent]; +} + +- (instancetype)init +{ + return [self initWithRunLoop:NSRunLoop.currentRunLoop]; +} + +- (instancetype)initWithRunLoop:(NSRunLoop *)aRunLoop +{ + static const CGEventMask Mask = (CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged)); + __auto_type eventTap = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + Mask, + _SRQuartzEventHandler, + (__bridge void *)self); + if (!eventTap) + { + os_trace_error("#Critical Unable to create event tap: make sure Accessibility is enabled"); + return nil; + } + + self = [super init]; + + if (self) + { + _eventTap = eventTap; + _eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); + CFRunLoopAddSource(aRunLoop.getCFRunLoop, _eventTapSource, kCFRunLoopDefaultMode); + } + + return self; +} + +- (void)dealloc +{ + if (_eventTap) + CFRelease(_eventTap); + + CFRelease(_eventTapSource); +} + +#pragma mark Methods + +- (CGEventRef)handleEvent:(CGEventRef)anEvent +{ + __block __auto_type result = anEvent; + + os_activity_initiate("-[SRAXGlobalShortcutMonitor handleEvent:]", OS_ACTIVITY_FLAG_DETACHED, ^{ + __auto_type nsEvent = [NSEvent eventWithCGEvent:anEvent]; + if (!nsEvent) + { + os_trace_error("#Error Unexpected event"); + return; + } + + __auto_type shortcut = [SRShortcut shortcutWithEvent:nsEvent]; + if (!shortcut) + { + os_trace_error("#Error Not a keyboard event"); + return; + } + + SRKeyEventType eventType = nsEvent.SR_keyEventType; + if (eventType == 0) + return; + + __auto_type actions = [self enabledActionsForShortcut:shortcut keyEvent:eventType]; + __block BOOL isHandled = NO; + [actions enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) + { + *stop = isHandled = [obj performActionOnTarget:nil]; + }]; + + result = isHandled ? nil : anEvent; + }); + + return result; +} + +#pragma mark SRShortcutMonitor + +- (void)didAddShortcut:(SRShortcut *)aShortcut +{ + if (_shortcuts.count) + CGEventTapEnable(_eventTap, true); +} + +- (void)willRemoveShortcut:(SRShortcut *)aShortcut +{ + if (_shortcuts.count == 1 && [_shortcuts countForObject:aShortcut] == 1) + CGEventTapEnable(_eventTap, false); +} + +@end + + @interface NSObject (_SRShortcutAction) - (void)undo:(id)aSender; - (void)redo:(id)aSender; @@ -1220,7 +1553,7 @@ + (SRLocalShortcutMonitor *)appShortcuts #pragma mark Methods -- (BOOL)handleEvent:(nullable NSEvent *)anEvent withTarget:(nullable id)aTarget +- (BOOL)handleEvent:(NSEvent *)anEvent withTarget:(nullable id)aTarget { SRShortcut *shortcut = [SRShortcut shortcutWithEvent:anEvent]; if (!shortcut) @@ -1229,24 +1562,18 @@ - (BOOL)handleEvent:(nullable NSEvent *)anEvent withTarget:(nullable id)aTarget return NO; } - SRKeyEventType eventType = 0; - switch (anEvent.type) { - case NSEventTypeKeyDown: - eventType = SRKeyEventTypeDown; - break; - case NSEventTypeKeyUp: - eventType = SRKeyEventTypeUp; - break; - default: - os_trace("#Error Unexpected key event of type %lu", anEvent.type); - return NO; - } + SRKeyEventType eventType = anEvent.SR_keyEventType; + if (eventType == 0) + return NO; - __auto_type actions = [self actionsForShortcut:shortcut keyEvent:eventType]; + __auto_type actions = [self enabledActionsForShortcut:shortcut keyEvent:eventType]; __block BOOL isHandled = NO; - [actions enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) { + [actions enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) + { *stop = isHandled = [obj performActionOnTarget:aTarget]; }]; + return isHandled; } @@ -1258,7 +1585,8 @@ - (void)updateWithCocoaTextKeyBindings NSMutableDictionary *keyBindings = [systemKeyBindings mutableCopy]; [keyBindings addEntriesFromDictionary:userKeyBindings]; - @synchronized (_actions) { + @synchronized (_actions) + { [keyBindings enumerateKeysAndObjectsUsingBlock:^(NSString *aKey, id aValue, BOOL *aStop) { if (![aKey isKindOfClass:NSString.class] || !aKey.length) return; @@ -1277,7 +1605,7 @@ - (void)updateWithCocoaTextKeyBindings else if (!keyBinding.length || [keyBinding isEqualToString:@"noop:"]) { // Only remove actions with static shortcuts. - __auto_type actions = [self->_shortcutToKeyDownActions objectForKey:shortcut]; + __auto_type actions = [self->_shortcutToEnabledKeyDownActions objectForKey:shortcut]; NSIndexSet *actionsToRemove = [actions indexesOfObjectsPassingTest:^BOOL(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) { return obj.observedObject == nil; }]; diff --git a/Library/SRShortcutController.m b/Library/SRShortcutController.m index 61ae7d2b..f6349249 100644 --- a/Library/SRShortcutController.m +++ b/Library/SRShortcutController.m @@ -245,7 +245,9 @@ - (void)updateComputedKeyPaths - (void)_updateRecorderControlValueBinding { - if (!_recorderControl) + __auto_type strongRecorderControl = _recorderControl; + + if (!strongRecorderControl) return; NSDictionary *contentBindingInfo = [self infoForBinding:NSContentObjectBinding]; @@ -253,10 +255,10 @@ - (void)_updateRecorderControlValueBinding return; NSDictionary *bindingOptions = [contentBindingInfo[NSOptionsKey] dictionaryWithValuesForKeys:@[NSValueTransformerBindingOption, NSValueTransformerNameBindingOption]]; - [_recorderControl bind:NSValueBinding - toObject:contentBindingInfo[NSObservedObjectKey] - withKeyPath:contentBindingInfo[NSObservedKeyPathKey] - options:bindingOptions]; + [strongRecorderControl bind:NSValueBinding + toObject:contentBindingInfo[NSObservedObjectKey] + withKeyPath:contentBindingInfo[NSObservedKeyPathKey] + options:bindingOptions]; } diff --git a/Library/SRShortcutFormatter.m b/Library/SRShortcutFormatter.m index 39a5bd5c..8db4968b 100644 --- a/Library/SRShortcutFormatter.m +++ b/Library/SRShortcutFormatter.m @@ -68,7 +68,12 @@ - (NSString *)stringForObjectValue:(SRShortcut *)aShortcut withImplicitModifierFlags:nil explicitModifierFlags:@(aShortcut.modifierFlags) layoutDirection:self.layoutDirection]; + + if (!key) + key = [NSString stringWithFormat:@"<%hu>", aShortcut.keyCode]; + NSString *flags = [flagsTransformer transformedValue:@(aShortcut.modifierFlags)]; + return [NSString stringWithFormat:@"%@%@", flags, key]; } diff --git a/Library/SRShortcutValidator.h b/Library/SRShortcutValidator.h index 6525dea7..78eb03db 100644 --- a/Library/SRShortcutValidator.h +++ b/Library/SRShortcutValidator.h @@ -70,10 +70,10 @@ NS_SWIFT_NAME(ShortcutValidator) @interface SRShortcutValidator(Deprecated) -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcut:error:"))) NS_SWIFT_UNAVAILABLE("validateShortcut(_:)"); -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagTakenInDelegate:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcutAgainstDelegate:error:"))) NS_SWIFT_UNAVAILABLE("validateShortcutAgainstDelegate(_:)"); -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagsTakenInSystemShortcuts:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcutAgainstSystemShortcuts:error:"))) NS_SWIFT_UNAVAILABLE("Use validateShortcutAgainstSystemShortcuts(_:)"); -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlags:(NSEventModifierFlags)aFlags takenInMenu:(NSMenu *)aMenu error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcut:againstMenu:error:"))) NS_SWIFT_UNAVAILABLE("Use validateShortcut(_:againstMenu:)"); +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcut:error:"))) NS_SWIFT_UNAVAILABLE("validateShortcut(_:)"); +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagTakenInDelegate:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcutAgainstDelegate:error:"))) NS_SWIFT_UNAVAILABLE("validateShortcutAgainstDelegate(_:)"); +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagsTakenInSystemShortcuts:(NSEventModifierFlags)aFlags error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcutAgainstSystemShortcuts:error:"))) NS_SWIFT_UNAVAILABLE("Use validateShortcutAgainstSystemShortcuts(_:)"); +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlags:(NSEventModifierFlags)aFlags takenInMenu:(NSMenu *)aMenu error:(NSError * _Nullable *)outError __attribute__((deprecated("", "validateShortcut:againstMenu:error:"))) NS_SWIFT_UNAVAILABLE("Use validateShortcut(_:againstMenu:)"); @end @@ -98,7 +98,7 @@ NS_SWIFT_NAME(ShortcutValidator) /*! Same as -shortcutValidator:isShortcutValid:reason: but return value is flipped. I.e. YES means shortcut is invalid. */ -- (BOOL)shortcutValidator:(SRShortcutValidator *)aValidator isKeyCode:(unsigned short)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags reason:(NSString * _Nullable * _Nonnull)outReason __attribute__((deprecated("", "shortcutValidator:isShortcutValid:reason:"))); +- (BOOL)shortcutValidator:(SRShortcutValidator *)aValidator isKeyCode:(SRKeyCode)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags reason:(NSString * _Nullable * _Nonnull)outReason __attribute__((deprecated("", "shortcutValidator:isShortcutValid:reason:"))); /*! Ask the delegate whether validator should check key equivalents of app's menu items. diff --git a/Library/SRShortcutValidator.m b/Library/SRShortcutValidator.m index 206fde93..fd7bf0b9 100644 --- a/Library/SRShortcutValidator.m +++ b/Library/SRShortcutValidator.m @@ -38,19 +38,21 @@ - (instancetype)init - (BOOL)validateShortcut:(SRShortcut *)aShortcut error:(NSError * __autoreleasing *)outError { __block BOOL result = NO; - os_activity_initiate("validateShortcut:error:", OS_ACTIVITY_FLAG_DEFAULT, ^{ + os_activity_initiate("-[SRShortcutValidator validateShortcut:error:]", OS_ACTIVITY_FLAG_DEFAULT, ^{ + __auto_type strongDelegate = self.delegate; + if (![self validateShortcutAgainstDelegate:aShortcut error:outError]) { result = NO; } - else if ((![self.delegate respondsToSelector:@selector(shortcutValidatorShouldCheckSystemShortcuts:)] || - [self.delegate shortcutValidatorShouldCheckSystemShortcuts:self]) && + else if ((![strongDelegate respondsToSelector:@selector(shortcutValidatorShouldCheckSystemShortcuts:)] || + [strongDelegate shortcutValidatorShouldCheckSystemShortcuts:self]) && ![self validateShortcutAgainstSystemShortcuts:aShortcut error:outError]) { result = NO; } - else if ((![self.delegate respondsToSelector:@selector(shortcutValidatorShouldCheckMenu:)] || - [self.delegate shortcutValidatorShouldCheckMenu:self]) && + else if ((![strongDelegate respondsToSelector:@selector(shortcutValidatorShouldCheckMenu:)] || + [strongDelegate shortcutValidatorShouldCheckMenu:self]) && NSApp.mainMenu && ![self validateShortcut:aShortcut againstMenu:NSApp.mainMenu error:outError]) { @@ -75,28 +77,37 @@ - (BOOL)validateShortcutAgainstDelegate:(SRShortcut *)aShortcut error:(NSError * #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" __auto_type DelegateIsShortcutValid = ^(NSString * __autoreleasing * aReason) { - if ([self.delegate respondsToSelector:@selector(shortcutValidator:isShortcutValid:reason:)]) - return [self.delegate shortcutValidator:self isShortcutValid:aShortcut reason:aReason]; - else if ([self.delegate respondsToSelector:@selector(shortcutValidator:isKeyCode:andFlagsTaken:reason:)]) - return (BOOL)![self.delegate shortcutValidator:self - isKeyCode:aShortcut.keyCode - andFlagsTaken:aShortcut.modifierFlags - reason:aReason]; + __auto_type strongDelegate = self.delegate; + + if ([strongDelegate respondsToSelector:@selector(shortcutValidator:isShortcutValid:reason:)]) + { + return [strongDelegate shortcutValidator:self + isShortcutValid:aShortcut + reason:aReason]; + } + else if ([strongDelegate respondsToSelector:@selector(shortcutValidator:isKeyCode:andFlagsTaken:reason:)]) + { + return (BOOL)![strongDelegate shortcutValidator:self + isKeyCode:aShortcut.keyCode + andFlagsTaken:aShortcut.modifierFlags + reason:aReason]; + } else return YES; }; #pragma clang diagnostic pop - os_activity_initiate("validateShortcutAgainstDelegate:error:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRShortcutValidator validateShortcutAgainstDelegate:error:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ NSString *delegateReason = nil; if (!DelegateIsShortcutValid(&delegateReason)) { if (outError) { BOOL isASCIIOnly = YES; + __auto_type strongDelegate = self.delegate; - if ([self.delegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) - isASCIIOnly = [self.delegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; + if ([strongDelegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) + isASCIIOnly = [strongDelegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; NSString *shortcut = [aShortcut readableStringRepresentation:isASCIIOnly]; NSString *failureReason = [NSString stringWithFormat:SRLoc(@"The \"%@\" shortcut can't be used!"), shortcut]; @@ -125,7 +136,7 @@ - (BOOL)validateShortcutAgainstDelegate:(SRShortcut *)aShortcut error:(NSError * - (BOOL)validateShortcutAgainstSystemShortcuts:(SRShortcut *)aShortcut error:(NSError * __autoreleasing *)outError { __block BOOL result = NO; - os_activity_initiate("validateShortcutAgainstSystemShortcuts:error:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRShortcutValidator validateShortcutAgainstSystemShortcuts:error:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ CFArrayRef s = NULL; OSStatus err = CopySymbolicHotKeys(&s); @@ -155,9 +166,10 @@ - (BOOL)validateShortcutAgainstSystemShortcuts:(SRShortcut *)aShortcut error:(NS if (outError) { BOOL isASCIIOnly = YES; + __auto_type strongDelegate = self.delegate; - if ([self.delegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) - isASCIIOnly = [self.delegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; + if ([strongDelegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) + isASCIIOnly = [strongDelegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; NSString *shortcut = [aShortcut readableStringRepresentation:isASCIIOnly]; NSString *failureReason = [NSString stringWithFormat: @@ -189,7 +201,7 @@ - (BOOL)validateShortcut:(SRShortcut *)aShortcut againstMenu:(NSMenu *)aMenu err { __block BOOL result = NO; - os_activity_initiate("validateShortcut:againstMenu:error:", OS_ACTIVITY_FLAG_DEFAULT, (^{ + os_activity_initiate("-[SRShortcutValidator validateShortcut:againstMenu:error:]", OS_ACTIVITY_FLAG_DEFAULT, (^{ for (NSMenuItem *menuItem in aMenu.itemArray) { if (menuItem.hasSubmenu && ![self validateShortcut:aShortcut againstMenu:menuItem.submenu error:outError]) @@ -210,9 +222,10 @@ - (BOOL)validateShortcut:(SRShortcut *)aShortcut againstMenu:(NSMenu *)aMenu err if (outError) { BOOL isASCIIOnly = YES; + __auto_type strongDelegate = self.delegate; - if ([self.delegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) - isASCIIOnly = [self.delegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; + if ([strongDelegate respondsToSelector:@selector(shortcutValidatorShouldUseASCIIStringForKeyCodes:)]) + isASCIIOnly = [strongDelegate shortcutValidatorShouldUseASCIIStringForKeyCodes:self]; NSString *shortcut = [aShortcut readableStringRepresentation:isASCIIOnly]; NSString *failureReason = [NSString stringWithFormat:SRLoc(@"The \"%@\" shortcut can't be used!"), shortcut]; @@ -266,22 +279,22 @@ - (BOOL)recorderControl:(SRRecorderControl *)aRecorder canRecordShortcut:(SRShor #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError; +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagsTaken:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError; { return ![self validateShortcut:[SRShortcut shortcutWithCode:aKeyCode modifierFlags:aFlags characters:nil charactersIgnoringModifiers:nil] error:outError]; } -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagTakenInDelegate:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagTakenInDelegate:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError { return ![self validateShortcutAgainstDelegate:[SRShortcut shortcutWithCode:aKeyCode modifierFlags:aFlags characters:nil charactersIgnoringModifiers:nil] error:outError]; } -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlagsTakenInSystemShortcuts:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlagsTakenInSystemShortcuts:(NSEventModifierFlags)aFlags error:(NSError * __autoreleasing *)outError { return ![self validateShortcutAgainstSystemShortcuts:[SRShortcut shortcutWithCode:aKeyCode modifierFlags:aFlags characters:nil charactersIgnoringModifiers:nil] error:outError]; } -- (BOOL)isKeyCode:(unsigned short)aKeyCode andFlags:(NSEventModifierFlags)aFlags takenInMenu:(NSMenu *)aMenu error:(NSError * __autoreleasing *)outError +- (BOOL)isKeyCode:(SRKeyCode)aKeyCode andFlags:(NSEventModifierFlags)aFlags takenInMenu:(NSMenu *)aMenu error:(NSError * __autoreleasing *)outError { return ![self validateShortcut:[SRShortcut shortcutWithCode:aKeyCode modifierFlags:aFlags characters:nil charactersIgnoringModifiers:nil] againstMenu:aMenu error:outError]; } diff --git a/README.md b/README.md index 8c3542e3..96decf6b 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,11 @@ The framework comes with: - `SRRecorderControlStyle` for custom styling - `SRShortcut` that represents a shortcut model - `SRGlobalShortcutMonitor` to turn the shortcut into an action by registering a global hot key +- `SRAXGlobalShortcutMonitor` to handle any kind of keyboard event via Accessibility - `SRLocalShortcutMonitor` for manual handling in the responder chain and `NSEvent` monitors - `SRShortcutController` for smooth Cocoa Bindings and seamless Interface Builder integration - `SRShortcutValidator` to check validity of the shortcut against Cocoa key equivalents and global hot keys -- `NSValueTransformer` and `NSFormatter` subclasses for custom alternations +- `NSValueTransformer` and `NSFormatter` subclasses for custom alterations ```swift import ShortcutRecorder @@ -60,11 +61,11 @@ The framework supports [module maps](https://clang.llvm.org/docs/Modules.html), ### CocoaPods - pod 'ShortcutRecorder', '~> 3.1' + pod 'ShortcutRecorder', '~> 3.2' ### Carthage - github "Kentzo/ShortcutRecorder" ~> 3.1 + github "Kentzo/ShortcutRecorder" ~> 3.2 Prebuilt frameworks are available. diff --git a/ShortcutRecorder.podspec b/ShortcutRecorder.podspec index 75ecdca1..200cc2bc 100644 --- a/ShortcutRecorder.podspec +++ b/ShortcutRecorder.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.8' s.name = 'ShortcutRecorder' - s.version = '3.1' + s.version = '3.2' s.summary = 'The best control to record shortcuts on macOS' s.homepage = 'https://github.com/Kentzo/ShortcutRecorder' s.license = { :type => 'CC BY 4.0', :file => 'LICENSE.txt' } diff --git a/ShortcutRecorder.xcodeproj/project.pbxproj b/ShortcutRecorder.xcodeproj/project.pbxproj index c48d3303..dbf59695 100644 --- a/ShortcutRecorder.xcodeproj/project.pbxproj +++ b/ShortcutRecorder.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -289,6 +289,7 @@ ); name = ShortcutRecorder; sourceTree = ""; + usesTabs = 0; }; 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; @@ -509,7 +510,7 @@ }; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "ShortcutRecorder" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( @@ -714,11 +715,15 @@ COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 3.1; - DYLIB_CURRENT_VERSION = 3.1; + DYLIB_CURRENT_VERSION = 3.2; FRAMEWORK_VERSION = A; INFOPLIST_FILE = Library/Info.plist; INSTALL_PATH = "@rpath"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.kulakov.ShortcutRecorder; PRODUCT_NAME = ShortcutRecorder; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -737,11 +742,15 @@ COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 3.1; - DYLIB_CURRENT_VERSION = 3.1; + DYLIB_CURRENT_VERSION = 3.2; FRAMEWORK_VERSION = A; INFOPLIST_FILE = Library/Info.plist; INSTALL_PATH = "@rpath"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.kulakov.ShortcutRecorder; PRODUCT_NAME = ShortcutRecorder; SWIFT_VERSION = 5.0; @@ -770,7 +779,11 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -800,13 +813,18 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.kulakov.ShortcutRecorder.UnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; @@ -833,7 +851,10 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = Inspector/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.kulakov.ShortcutRecorder.Inspector; @@ -862,7 +883,10 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = Inspector/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.kulakov.ShortcutRecorder.Inspector; @@ -895,7 +919,11 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -927,7 +955,11 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -961,7 +993,9 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_INTERFACE_IVARS = YES_ERROR; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES_AGGRESSIVE; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; @@ -986,6 +1020,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-fstack-protector"; + RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; SKIP_INSTALL = YES; WARNING_CFLAGS = "-Wall"; @@ -1014,7 +1049,9 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_INTERFACE_IVARS = YES_ERROR; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES_AGGRESSIVE; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; @@ -1035,6 +1072,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; OTHER_CFLAGS = "-fstack-protector"; + RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Unit Tests/SRKeyCodeTransformerTests.swift b/Unit Tests/SRKeyCodeTransformerTests.swift index 173d2d08..dafd062f 100644 --- a/Unit Tests/SRKeyCodeTransformerTests.swift +++ b/Unit Tests/SRKeyCodeTransformerTests.swift @@ -15,11 +15,11 @@ class SRKeyCodeTransformerTests: XCTestCase { c.drawsASCIIEquivalentOfShortcut = true c.userInterfaceLayoutDirection = .leftToRight - c.objectValue = Shortcut(code: UInt16(kVK_Tab), modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil) + c.objectValue = Shortcut(code: KeyCode.tab, modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil) XCTAssertEqual(c.stringValue, "\u{21E5}") c.userInterfaceLayoutDirection = .rightToLeft - c.objectValue = Shortcut(code: UInt16(kVK_Tab), modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil) + c.objectValue = Shortcut(code: KeyCode.tab, modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil) XCTAssertEqual(c.stringValue, "\u{21E4}") } } @@ -27,436 +27,436 @@ class SRKeyCodeTransformerTests: XCTestCase { class SRASCIILiteralKeyCodeTransformerTests: XCTestCase { func testTransform() { - func AssertEqual(code: Int, literal: String) { + func AssertEqual(code: KeyCode, literal: String) { XCTContext.runActivity(named: literal) { (_) in - XCTAssertEqual(ASCIILiteralKeyCodeTransformer.shared.transformedValue(code as NSNumber), literal) + XCTAssertEqual(ASCIILiteralKeyCodeTransformer.shared.transformedValue(code.rawValue as NSNumber), literal) } } - AssertEqual(code: kVK_ANSI_0, literal: "0") - AssertEqual(code: kVK_ANSI_1, literal: "1") - AssertEqual(code: kVK_ANSI_2, literal: "2") - AssertEqual(code: kVK_ANSI_3, literal: "3") - AssertEqual(code: kVK_ANSI_4, literal: "4") - AssertEqual(code: kVK_ANSI_5, literal: "5") - AssertEqual(code: kVK_ANSI_6, literal: "6") - AssertEqual(code: kVK_ANSI_7, literal: "7") - AssertEqual(code: kVK_ANSI_8, literal: "8") - AssertEqual(code: kVK_ANSI_9, literal: "9") - AssertEqual(code: kVK_ANSI_A, literal: "A") - AssertEqual(code: kVK_ANSI_B, literal: "B") - AssertEqual(code: kVK_ANSI_Backslash, literal: #"\"#) - AssertEqual(code: kVK_ANSI_C, literal: "C") - AssertEqual(code: kVK_ANSI_Comma, literal: ",") - AssertEqual(code: kVK_ANSI_D, literal: "D") - AssertEqual(code: kVK_ANSI_E, literal: "E") - AssertEqual(code: kVK_ANSI_Equal, literal: "=") - AssertEqual(code: kVK_ANSI_F, literal: "F") - AssertEqual(code: kVK_ANSI_G, literal: "G") - AssertEqual(code: kVK_ANSI_Grave, literal: "`") - AssertEqual(code: kVK_ANSI_H, literal: "H") - AssertEqual(code: kVK_ANSI_I, literal: "I") - AssertEqual(code: kVK_ANSI_J, literal: "J") - AssertEqual(code: kVK_ANSI_K, literal: "K") - AssertEqual(code: kVK_ANSI_Keypad0, literal: "0") - AssertEqual(code: kVK_ANSI_Keypad1, literal: "1") - AssertEqual(code: kVK_ANSI_Keypad2, literal: "2") - AssertEqual(code: kVK_ANSI_Keypad3, literal: "3") - AssertEqual(code: kVK_ANSI_Keypad4, literal: "4") - AssertEqual(code: kVK_ANSI_Keypad5, literal: "5") - AssertEqual(code: kVK_ANSI_Keypad6, literal: "6") - AssertEqual(code: kVK_ANSI_Keypad7, literal: "7") - AssertEqual(code: kVK_ANSI_Keypad8, literal: "8") - AssertEqual(code: kVK_ANSI_Keypad9, literal: "9") - AssertEqual(code: kVK_ANSI_KeypadDecimal, literal: ".") - AssertEqual(code: kVK_ANSI_KeypadDivide, literal: "/") - AssertEqual(code: kVK_ANSI_KeypadEnter, literal: KeyCodeString.return.rawValue) - AssertEqual(code: kVK_ANSI_KeypadEquals, literal: "=") - AssertEqual(code: kVK_ANSI_KeypadMinus, literal: "-") - AssertEqual(code: kVK_ANSI_KeypadMultiply, literal: "*") - AssertEqual(code: kVK_ANSI_KeypadPlus, literal: "+") - AssertEqual(code: kVK_ANSI_L, literal: "L") - AssertEqual(code: kVK_ANSI_LeftBracket, literal: "[") - AssertEqual(code: kVK_ANSI_M, literal: "M") - AssertEqual(code: kVK_ANSI_Minus, literal: "-") - AssertEqual(code: kVK_ANSI_N, literal: "N") - AssertEqual(code: kVK_ANSI_O, literal: "O") - AssertEqual(code: kVK_ANSI_P, literal: "P") - AssertEqual(code: kVK_ANSI_Period, literal: ".") - AssertEqual(code: kVK_ANSI_Q, literal: "Q") - AssertEqual(code: kVK_ANSI_Quote, literal: "'") - AssertEqual(code: kVK_ANSI_R, literal: "R") - AssertEqual(code: kVK_ANSI_RightBracket, literal: "]") - AssertEqual(code: kVK_ANSI_S, literal: "S") - AssertEqual(code: kVK_ANSI_Semicolon, literal: ";") - AssertEqual(code: kVK_ANSI_Slash, literal: "/") - AssertEqual(code: kVK_ANSI_T, literal: "T") - AssertEqual(code: kVK_ANSI_U, literal: "U") - AssertEqual(code: kVK_ANSI_V, literal: "V") - AssertEqual(code: kVK_ANSI_W, literal: "W") - AssertEqual(code: kVK_ANSI_X, literal: "X") - AssertEqual(code: kVK_ANSI_Y, literal: "Y") - AssertEqual(code: kVK_ANSI_Z, literal: "Z") - AssertEqual(code: kVK_Delete, literal: KeyCodeString.deleteLeft.rawValue) - AssertEqual(code: kVK_DownArrow, literal: KeyCodeString.downArrow.rawValue) - AssertEqual(code: kVK_End, literal: KeyCodeString.southeastArrow.rawValue) - AssertEqual(code: kVK_Escape, literal: KeyCodeString.escape.rawValue) - AssertEqual(code: kVK_F1, literal: "F1") - AssertEqual(code: kVK_F2, literal: "F2") - AssertEqual(code: kVK_F3, literal: "F3") - AssertEqual(code: kVK_F4, literal: "F4") - AssertEqual(code: kVK_F5, literal: "F5") - AssertEqual(code: kVK_F6, literal: "F6") - AssertEqual(code: kVK_F7, literal: "F7") - AssertEqual(code: kVK_F8, literal: "F8") - AssertEqual(code: kVK_F9, literal: "F9") - AssertEqual(code: kVK_F10, literal: "F10") - AssertEqual(code: kVK_F11, literal: "F11") - AssertEqual(code: kVK_F12, literal: "F12") - AssertEqual(code: kVK_F13, literal: "F13") - AssertEqual(code: kVK_F14, literal: "F14") - AssertEqual(code: kVK_F15, literal: "F15") - AssertEqual(code: kVK_F16, literal: "F16") - AssertEqual(code: kVK_F17, literal: "F17") - AssertEqual(code: kVK_F18, literal: "F18") - AssertEqual(code: kVK_F19, literal: "F19") - AssertEqual(code: kVK_F20, literal: "F20") - AssertEqual(code: kVK_ForwardDelete, literal: KeyCodeString.deleteRight.rawValue) - AssertEqual(code: kVK_Help, literal: KeyCodeString.help.rawValue) - AssertEqual(code: kVK_Home, literal: KeyCodeString.northwestArrow.rawValue) - AssertEqual(code: kVK_ISO_Section, literal: "§") - AssertEqual(code: kVK_JIS_KeypadComma, literal: KeyCodeString.jisComma.rawValue) - AssertEqual(code: kVK_JIS_Underscore, literal: KeyCodeString.jisUnderscore.rawValue) - AssertEqual(code: kVK_JIS_Yen, literal: KeyCodeString.jisYen.rawValue) - AssertEqual(code: kVK_LeftArrow, literal: KeyCodeString.leftArrow.rawValue) - AssertEqual(code: kVK_PageDown, literal: KeyCodeString.pageDown.rawValue) - AssertEqual(code: kVK_PageUp, literal: KeyCodeString.pageUp.rawValue) - AssertEqual(code: kVK_Return, literal: KeyCodeString.returnR2L.rawValue) - AssertEqual(code: kVK_RightArrow, literal: KeyCodeString.rightArrow.rawValue) - AssertEqual(code: kVK_Space, literal: shortcutRecorderLocalizedString(forKey: "Space")) - AssertEqual(code: kVK_Tab, literal: KeyCodeString.tabRight.rawValue) - AssertEqual(code: kVK_UpArrow, literal: KeyCodeString.upArrow.rawValue) + AssertEqual(code: KeyCode.ansi0, literal: "0") + AssertEqual(code: KeyCode.ansi1, literal: "1") + AssertEqual(code: KeyCode.ansi2, literal: "2") + AssertEqual(code: KeyCode.ansi3, literal: "3") + AssertEqual(code: KeyCode.ansi4, literal: "4") + AssertEqual(code: KeyCode.ansi5, literal: "5") + AssertEqual(code: KeyCode.ansi6, literal: "6") + AssertEqual(code: KeyCode.ansi7, literal: "7") + AssertEqual(code: KeyCode.ansi8, literal: "8") + AssertEqual(code: KeyCode.ansi9, literal: "9") + AssertEqual(code: KeyCode.ansiA, literal: "A") + AssertEqual(code: KeyCode.ansiB, literal: "B") + AssertEqual(code: KeyCode.ansiBackslash, literal: #"\"#) + AssertEqual(code: KeyCode.ansiC, literal: "C") + AssertEqual(code: KeyCode.ansiComma, literal: ",") + AssertEqual(code: KeyCode.ansiD, literal: "D") + AssertEqual(code: KeyCode.ansiE, literal: "E") + AssertEqual(code: KeyCode.ansiEqual, literal: "=") + AssertEqual(code: KeyCode.ansiF, literal: "F") + AssertEqual(code: KeyCode.ansiG, literal: "G") + AssertEqual(code: KeyCode.ansiGrave, literal: "`") + AssertEqual(code: KeyCode.ansiH, literal: "H") + AssertEqual(code: KeyCode.ansiI, literal: "I") + AssertEqual(code: KeyCode.ansiJ, literal: "J") + AssertEqual(code: KeyCode.ansiK, literal: "K") + AssertEqual(code: KeyCode.ansiKeypad0, literal: "0") + AssertEqual(code: KeyCode.ansiKeypad1, literal: "1") + AssertEqual(code: KeyCode.ansiKeypad2, literal: "2") + AssertEqual(code: KeyCode.ansiKeypad3, literal: "3") + AssertEqual(code: KeyCode.ansiKeypad4, literal: "4") + AssertEqual(code: KeyCode.ansiKeypad5, literal: "5") + AssertEqual(code: KeyCode.ansiKeypad6, literal: "6") + AssertEqual(code: KeyCode.ansiKeypad7, literal: "7") + AssertEqual(code: KeyCode.ansiKeypad8, literal: "8") + AssertEqual(code: KeyCode.ansiKeypad9, literal: "9") + AssertEqual(code: KeyCode.ansiKeypadDecimal, literal: ".") + AssertEqual(code: KeyCode.ansiKeypadDivide, literal: "/") + AssertEqual(code: KeyCode.ansiKeypadEnter, literal: KeyCodeString.return.rawValue) + AssertEqual(code: KeyCode.ansiKeypadEquals, literal: "=") + AssertEqual(code: KeyCode.ansiKeypadMinus, literal: "-") + AssertEqual(code: KeyCode.ansiKeypadMultiply, literal: "*") + AssertEqual(code: KeyCode.ansiKeypadPlus, literal: "+") + AssertEqual(code: KeyCode.ansiL, literal: "L") + AssertEqual(code: KeyCode.ansiLeftBracket, literal: "[") + AssertEqual(code: KeyCode.ansiM, literal: "M") + AssertEqual(code: KeyCode.ansiMinus, literal: "-") + AssertEqual(code: KeyCode.ansiN, literal: "N") + AssertEqual(code: KeyCode.ansiO, literal: "O") + AssertEqual(code: KeyCode.ansiP, literal: "P") + AssertEqual(code: KeyCode.ansiPeriod, literal: ".") + AssertEqual(code: KeyCode.ansiQ, literal: "Q") + AssertEqual(code: KeyCode.ansiQuote, literal: "'") + AssertEqual(code: KeyCode.ansiR, literal: "R") + AssertEqual(code: KeyCode.ansiRightBracket, literal: "]") + AssertEqual(code: KeyCode.ansiS, literal: "S") + AssertEqual(code: KeyCode.ansiSemicolon, literal: ";") + AssertEqual(code: KeyCode.ansiSlash, literal: "/") + AssertEqual(code: KeyCode.ansiT, literal: "T") + AssertEqual(code: KeyCode.ansiU, literal: "U") + AssertEqual(code: KeyCode.ansiV, literal: "V") + AssertEqual(code: KeyCode.ansiW, literal: "W") + AssertEqual(code: KeyCode.ansiX, literal: "X") + AssertEqual(code: KeyCode.ansiY, literal: "Y") + AssertEqual(code: KeyCode.ansiZ, literal: "Z") + AssertEqual(code: KeyCode.delete, literal: KeyCodeString.deleteLeft.rawValue) + AssertEqual(code: KeyCode.downArrow, literal: KeyCodeString.downArrow.rawValue) + AssertEqual(code: KeyCode.end, literal: KeyCodeString.southeastArrow.rawValue) + AssertEqual(code: KeyCode.escape, literal: KeyCodeString.escape.rawValue) + AssertEqual(code: KeyCode.f1, literal: "F1") + AssertEqual(code: KeyCode.f2, literal: "F2") + AssertEqual(code: KeyCode.f3, literal: "F3") + AssertEqual(code: KeyCode.f4, literal: "F4") + AssertEqual(code: KeyCode.f5, literal: "F5") + AssertEqual(code: KeyCode.f6, literal: "F6") + AssertEqual(code: KeyCode.f7, literal: "F7") + AssertEqual(code: KeyCode.f8, literal: "F8") + AssertEqual(code: KeyCode.f9, literal: "F9") + AssertEqual(code: KeyCode.f10, literal: "F10") + AssertEqual(code: KeyCode.f11, literal: "F11") + AssertEqual(code: KeyCode.f12, literal: "F12") + AssertEqual(code: KeyCode.f13, literal: "F13") + AssertEqual(code: KeyCode.f14, literal: "F14") + AssertEqual(code: KeyCode.f15, literal: "F15") + AssertEqual(code: KeyCode.f16, literal: "F16") + AssertEqual(code: KeyCode.f17, literal: "F17") + AssertEqual(code: KeyCode.f18, literal: "F18") + AssertEqual(code: KeyCode.f19, literal: "F19") + AssertEqual(code: KeyCode.f20, literal: "F20") + AssertEqual(code: KeyCode.forwardDelete, literal: KeyCodeString.deleteRight.rawValue) + AssertEqual(code: KeyCode.help, literal: KeyCodeString.help.rawValue) + AssertEqual(code: KeyCode.home, literal: KeyCodeString.northwestArrow.rawValue) + AssertEqual(code: KeyCode.isoSection, literal: "§") + AssertEqual(code: KeyCode.jisKeypadComma, literal: KeyCodeString.jisComma.rawValue) + AssertEqual(code: KeyCode.jisUnderscore, literal: KeyCodeString.jisUnderscore.rawValue) + AssertEqual(code: KeyCode.jisYen, literal: KeyCodeString.jisYen.rawValue) + AssertEqual(code: KeyCode.leftArrow, literal: KeyCodeString.leftArrow.rawValue) + AssertEqual(code: KeyCode.pageDown, literal: KeyCodeString.pageDown.rawValue) + AssertEqual(code: KeyCode.pageUp, literal: KeyCodeString.pageUp.rawValue) + AssertEqual(code: KeyCode.return, literal: KeyCodeString.returnR2L.rawValue) + AssertEqual(code: KeyCode.rightArrow, literal: KeyCodeString.rightArrow.rawValue) + AssertEqual(code: KeyCode.space, literal: shortcutRecorderLocalizedString(forKey: "Space")) + AssertEqual(code: KeyCode.tab, literal: KeyCodeString.tabRight.rawValue) + AssertEqual(code: KeyCode.upArrow, literal: KeyCodeString.upArrow.rawValue) } func testReverseTransform() { - func AssertEqual(literal: String, code: Int) { + func AssertEqual(literal: String, code: KeyCode) { XCTContext.runActivity(named: literal) { (_) in - XCTAssertEqual(ASCIILiteralKeyCodeTransformer.shared.reverseTransformedValue(literal) as! Int, code) + XCTAssertEqual(ASCIILiteralKeyCodeTransformer.shared.reverseTransformedValue(literal) as! UInt16, code.rawValue) } } - AssertEqual(literal: "0", code: kVK_ANSI_0) - AssertEqual(literal: "1", code: kVK_ANSI_1) - AssertEqual(literal: "2", code: kVK_ANSI_2) - AssertEqual(literal: "3", code: kVK_ANSI_3) - AssertEqual(literal: "4", code: kVK_ANSI_4) - AssertEqual(literal: "5", code: kVK_ANSI_5) - AssertEqual(literal: "6", code: kVK_ANSI_6) - AssertEqual(literal: "7", code: kVK_ANSI_7) - AssertEqual(literal: "8", code: kVK_ANSI_8) - AssertEqual(literal: "9", code: kVK_ANSI_9) - AssertEqual(literal: "a", code: kVK_ANSI_A) - AssertEqual(literal: "A", code: kVK_ANSI_A) - AssertEqual(literal: "b", code: kVK_ANSI_B) - AssertEqual(literal: #"\"#, code: kVK_ANSI_Backslash) - AssertEqual(literal: "c", code: kVK_ANSI_C) - AssertEqual(literal: ",", code: kVK_ANSI_Comma) - AssertEqual(literal: "d", code: kVK_ANSI_D) - AssertEqual(literal: "e", code: kVK_ANSI_E) - AssertEqual(literal: "=", code: kVK_ANSI_Equal) - AssertEqual(literal: "f", code: kVK_ANSI_F) - AssertEqual(literal: "g", code: kVK_ANSI_G) - AssertEqual(literal: "`", code: kVK_ANSI_Grave) - AssertEqual(literal: "h", code: kVK_ANSI_H) - AssertEqual(literal: "i", code: kVK_ANSI_I) - AssertEqual(literal: "j", code: kVK_ANSI_J) - AssertEqual(literal: "k", code: kVK_ANSI_K) - AssertEqual(literal: "⌅", code: kVK_ANSI_KeypadEnter) - AssertEqual(literal: "l", code: kVK_ANSI_L) - AssertEqual(literal: "[", code: kVK_ANSI_LeftBracket) - AssertEqual(literal: "m", code: kVK_ANSI_M) - AssertEqual(literal: "-", code: kVK_ANSI_Minus) - AssertEqual(literal: "n", code: kVK_ANSI_N) - AssertEqual(literal: "o", code: kVK_ANSI_O) - AssertEqual(literal: "p", code: kVK_ANSI_P) - AssertEqual(literal: ".", code: kVK_ANSI_Period) - AssertEqual(literal: "q", code: kVK_ANSI_Q) - AssertEqual(literal: "'", code: kVK_ANSI_Quote) - AssertEqual(literal: "r", code: kVK_ANSI_R) - AssertEqual(literal: "]", code: kVK_ANSI_RightBracket) - AssertEqual(literal: "s", code: kVK_ANSI_S) - AssertEqual(literal: ";", code: kVK_ANSI_Semicolon) - AssertEqual(literal: "/", code: kVK_ANSI_Slash) - AssertEqual(literal: "t", code: kVK_ANSI_T) - AssertEqual(literal: "u", code: kVK_ANSI_U) - AssertEqual(literal: "v", code: kVK_ANSI_V) - AssertEqual(literal: "w", code: kVK_ANSI_W) - AssertEqual(literal: "x", code: kVK_ANSI_X) - AssertEqual(literal: "y", code: kVK_ANSI_Y) - AssertEqual(literal: "z", code: kVK_ANSI_Z) - AssertEqual(literal: "⌫", code: kVK_Delete) - AssertEqual(literal: "↓", code: kVK_DownArrow) - AssertEqual(literal: "↘", code: kVK_End) - AssertEqual(literal: "⎋", code: kVK_Escape) - AssertEqual(literal: "Esc", code: kVK_Escape) - AssertEqual(literal: "Escape", code: kVK_Escape) - AssertEqual(literal: "F1", code: kVK_F1) - AssertEqual(literal: "f1", code: kVK_F1) - AssertEqual(literal: "F2", code: kVK_F2) - AssertEqual(literal: "F3", code: kVK_F3) - AssertEqual(literal: "F4", code: kVK_F4) - AssertEqual(literal: "F5", code: kVK_F5) - AssertEqual(literal: "F6", code: kVK_F6) - AssertEqual(literal: "F7", code: kVK_F7) - AssertEqual(literal: "F8", code: kVK_F8) - AssertEqual(literal: "F9", code: kVK_F9) - AssertEqual(literal: "F10", code: kVK_F10) - AssertEqual(literal: "F11", code: kVK_F11) - AssertEqual(literal: "F12", code: kVK_F12) - AssertEqual(literal: "F13", code: kVK_F13) - AssertEqual(literal: "F14", code: kVK_F14) - AssertEqual(literal: "F15", code: kVK_F15) - AssertEqual(literal: "F16", code: kVK_F16) - AssertEqual(literal: "F17", code: kVK_F17) - AssertEqual(literal: "F18", code: kVK_F18) - AssertEqual(literal: "F19", code: kVK_F19) - AssertEqual(literal: "F20", code: kVK_F20) - AssertEqual(literal: "⌦", code: kVK_ForwardDelete) - AssertEqual(literal: "?⃝", code: kVK_Help) - AssertEqual(literal: "Help", code: kVK_Help) - AssertEqual(literal: "↖", code: kVK_Home) - AssertEqual(literal: "§", code: kVK_ISO_Section) - AssertEqual(literal: "←", code: kVK_LeftArrow) - AssertEqual(literal: "⇟", code: kVK_PageDown) - AssertEqual(literal: "⇞", code: kVK_PageUp) - AssertEqual(literal: "↩", code: kVK_Return) - AssertEqual(literal: "Enter", code: kVK_Return) - AssertEqual(literal: "→", code: kVK_RightArrow) - AssertEqual(literal: " ", code: kVK_Space) - AssertEqual(literal: "Space", code: kVK_Space) - AssertEqual(literal: "⇥", code: kVK_Tab) - AssertEqual(literal: "⇤", code: kVK_Tab) - AssertEqual(literal: "Tab", code: kVK_Tab) - AssertEqual(literal: "↑", code: kVK_UpArrow) - AssertEqual(literal: "_", code: kVK_JIS_Underscore) - AssertEqual(literal: "、", code: kVK_JIS_KeypadComma) - AssertEqual(literal: "¥", code: kVK_JIS_Yen) - AssertEqual(literal: "*", code: kVK_ANSI_KeypadMultiply); - AssertEqual(literal: "+", code: kVK_ANSI_KeypadPlus); + AssertEqual(literal: "0", code: KeyCode.ansi0) + AssertEqual(literal: "1", code: KeyCode.ansi1) + AssertEqual(literal: "2", code: KeyCode.ansi2) + AssertEqual(literal: "3", code: KeyCode.ansi3) + AssertEqual(literal: "4", code: KeyCode.ansi4) + AssertEqual(literal: "5", code: KeyCode.ansi5) + AssertEqual(literal: "6", code: KeyCode.ansi6) + AssertEqual(literal: "7", code: KeyCode.ansi7) + AssertEqual(literal: "8", code: KeyCode.ansi8) + AssertEqual(literal: "9", code: KeyCode.ansi9) + AssertEqual(literal: "a", code: KeyCode.ansiA) + AssertEqual(literal: "A", code: KeyCode.ansiA) + AssertEqual(literal: "b", code: KeyCode.ansiB) + AssertEqual(literal: #"\"#, code: KeyCode.ansiBackslash) + AssertEqual(literal: "c", code: KeyCode.ansiC) + AssertEqual(literal: ",", code: KeyCode.ansiComma) + AssertEqual(literal: "d", code: KeyCode.ansiD) + AssertEqual(literal: "e", code: KeyCode.ansiE) + AssertEqual(literal: "=", code: KeyCode.ansiEqual) + AssertEqual(literal: "f", code: KeyCode.ansiF) + AssertEqual(literal: "g", code: KeyCode.ansiG) + AssertEqual(literal: "`", code: KeyCode.ansiGrave) + AssertEqual(literal: "h", code: KeyCode.ansiH) + AssertEqual(literal: "i", code: KeyCode.ansiI) + AssertEqual(literal: "j", code: KeyCode.ansiJ) + AssertEqual(literal: "k", code: KeyCode.ansiK) + AssertEqual(literal: "⌅", code: KeyCode.ansiKeypadEnter) + AssertEqual(literal: "l", code: KeyCode.ansiL) + AssertEqual(literal: "[", code: KeyCode.ansiLeftBracket) + AssertEqual(literal: "m", code: KeyCode.ansiM) + AssertEqual(literal: "-", code: KeyCode.ansiMinus) + AssertEqual(literal: "n", code: KeyCode.ansiN) + AssertEqual(literal: "o", code: KeyCode.ansiO) + AssertEqual(literal: "p", code: KeyCode.ansiP) + AssertEqual(literal: ".", code: KeyCode.ansiPeriod) + AssertEqual(literal: "q", code: KeyCode.ansiQ) + AssertEqual(literal: "'", code: KeyCode.ansiQuote) + AssertEqual(literal: "r", code: KeyCode.ansiR) + AssertEqual(literal: "]", code: KeyCode.ansiRightBracket) + AssertEqual(literal: "s", code: KeyCode.ansiS) + AssertEqual(literal: ";", code: KeyCode.ansiSemicolon) + AssertEqual(literal: "/", code: KeyCode.ansiSlash) + AssertEqual(literal: "t", code: KeyCode.ansiT) + AssertEqual(literal: "u", code: KeyCode.ansiU) + AssertEqual(literal: "v", code: KeyCode.ansiV) + AssertEqual(literal: "w", code: KeyCode.ansiW) + AssertEqual(literal: "x", code: KeyCode.ansiX) + AssertEqual(literal: "y", code: KeyCode.ansiY) + AssertEqual(literal: "z", code: KeyCode.ansiZ) + AssertEqual(literal: "⌫", code: KeyCode.delete) + AssertEqual(literal: "↓", code: KeyCode.downArrow) + AssertEqual(literal: "↘", code: KeyCode.end) + AssertEqual(literal: "⎋", code: KeyCode.escape) + AssertEqual(literal: "Esc", code: KeyCode.escape) + AssertEqual(literal: "Escape", code: KeyCode.escape) + AssertEqual(literal: "F1", code: KeyCode.f1) + AssertEqual(literal: "f1", code: KeyCode.f1) + AssertEqual(literal: "F2", code: KeyCode.f2) + AssertEqual(literal: "F3", code: KeyCode.f3) + AssertEqual(literal: "F4", code: KeyCode.f4) + AssertEqual(literal: "F5", code: KeyCode.f5) + AssertEqual(literal: "F6", code: KeyCode.f6) + AssertEqual(literal: "F7", code: KeyCode.f7) + AssertEqual(literal: "F8", code: KeyCode.f8) + AssertEqual(literal: "F9", code: KeyCode.f9) + AssertEqual(literal: "F10", code: KeyCode.f10) + AssertEqual(literal: "F11", code: KeyCode.f11) + AssertEqual(literal: "F12", code: KeyCode.f12) + AssertEqual(literal: "F13", code: KeyCode.f13) + AssertEqual(literal: "F14", code: KeyCode.f14) + AssertEqual(literal: "F15", code: KeyCode.f15) + AssertEqual(literal: "F16", code: KeyCode.f16) + AssertEqual(literal: "F17", code: KeyCode.f17) + AssertEqual(literal: "F18", code: KeyCode.f18) + AssertEqual(literal: "F19", code: KeyCode.f19) + AssertEqual(literal: "F20", code: KeyCode.f20) + AssertEqual(literal: "⌦", code: KeyCode.forwardDelete) + AssertEqual(literal: "?⃝", code: KeyCode.help) + AssertEqual(literal: "Help", code: KeyCode.help) + AssertEqual(literal: "↖", code: KeyCode.home) + AssertEqual(literal: "§", code: KeyCode.isoSection) + AssertEqual(literal: "←", code: KeyCode.leftArrow) + AssertEqual(literal: "⇟", code: KeyCode.pageDown) + AssertEqual(literal: "⇞", code: KeyCode.pageUp) + AssertEqual(literal: "↩", code: KeyCode.return) + AssertEqual(literal: "Enter", code: KeyCode.return) + AssertEqual(literal: "→", code: KeyCode.rightArrow) + AssertEqual(literal: " ", code: KeyCode.space) + AssertEqual(literal: "Space", code: KeyCode.space) + AssertEqual(literal: "⇥", code: KeyCode.tab) + AssertEqual(literal: "⇤", code: KeyCode.tab) + AssertEqual(literal: "Tab", code: KeyCode.tab) + AssertEqual(literal: "↑", code: KeyCode.upArrow) + AssertEqual(literal: "_", code: KeyCode.jisUnderscore) + AssertEqual(literal: "、", code: KeyCode.jisKeypadComma) + AssertEqual(literal: "¥", code: KeyCode.jisYen) + AssertEqual(literal: "*", code: KeyCode.ansiKeypadMultiply); + AssertEqual(literal: "+", code: KeyCode.ansiKeypadPlus); } } class SRASCIISymbolicKeyCodeTransformerTests: XCTestCase { func testTransform() { - func AssertEqual(code: Int, symbol: String) { + func AssertEqual(code: KeyCode, symbol: String) { XCTContext.runActivity(named: symbol) { (_) in - XCTAssertEqual(ASCIISymbolicKeyCodeTransformer.shared.transformedValue(code as NSNumber), symbol) + XCTAssertEqual(ASCIISymbolicKeyCodeTransformer.shared.transformedValue(code.rawValue as NSNumber), symbol) } } - AssertEqual(code: kVK_ANSI_0, symbol: "0") - AssertEqual(code: kVK_ANSI_1, symbol: "1") - AssertEqual(code: kVK_ANSI_2, symbol: "2") - AssertEqual(code: kVK_ANSI_3, symbol: "3") - AssertEqual(code: kVK_ANSI_4, symbol: "4") - AssertEqual(code: kVK_ANSI_5, symbol: "5") - AssertEqual(code: kVK_ANSI_6, symbol: "6") - AssertEqual(code: kVK_ANSI_7, symbol: "7") - AssertEqual(code: kVK_ANSI_8, symbol: "8") - AssertEqual(code: kVK_ANSI_9, symbol: "9") - AssertEqual(code: kVK_ANSI_A, symbol: "a") - AssertEqual(code: kVK_ANSI_B, symbol: "b") - AssertEqual(code: kVK_ANSI_Backslash, symbol: #"\"#) - AssertEqual(code: kVK_ANSI_C, symbol: "c") - AssertEqual(code: kVK_ANSI_Comma, symbol: ",") - AssertEqual(code: kVK_ANSI_D, symbol: "d") - AssertEqual(code: kVK_ANSI_E, symbol: "e") - AssertEqual(code: kVK_ANSI_Equal, symbol: "=") - AssertEqual(code: kVK_ANSI_F, symbol: "f") - AssertEqual(code: kVK_ANSI_G, symbol: "g") - AssertEqual(code: kVK_ANSI_Grave, symbol: "`") - AssertEqual(code: kVK_ANSI_H, symbol: "h") - AssertEqual(code: kVK_ANSI_I, symbol: "i") - AssertEqual(code: kVK_ANSI_J, symbol: "j") - AssertEqual(code: kVK_ANSI_K, symbol: "k") - AssertEqual(code: kVK_ANSI_Keypad0, symbol: "0") - AssertEqual(code: kVK_ANSI_Keypad1, symbol: "1") - AssertEqual(code: kVK_ANSI_Keypad2, symbol: "2") - AssertEqual(code: kVK_ANSI_Keypad3, symbol: "3") - AssertEqual(code: kVK_ANSI_Keypad4, symbol: "4") - AssertEqual(code: kVK_ANSI_Keypad5, symbol: "5") - AssertEqual(code: kVK_ANSI_Keypad6, symbol: "6") - AssertEqual(code: kVK_ANSI_Keypad7, symbol: "7") - AssertEqual(code: kVK_ANSI_Keypad8, symbol: "8") - AssertEqual(code: kVK_ANSI_Keypad9, symbol: "9") - AssertEqual(code: kVK_ANSI_KeypadDecimal, symbol: ".") - AssertEqual(code: kVK_ANSI_KeypadDivide, symbol: "/") - AssertEqual(code: kVK_ANSI_KeypadEnter, symbol: unicharToString(unichar(NSEnterCharacter))) - AssertEqual(code: kVK_ANSI_KeypadEquals, symbol: "=") - AssertEqual(code: kVK_ANSI_KeypadMinus, symbol: "-") - AssertEqual(code: kVK_ANSI_KeypadMultiply, symbol: "*") - AssertEqual(code: kVK_ANSI_KeypadPlus, symbol: "+") - AssertEqual(code: kVK_ANSI_L, symbol: "l") - AssertEqual(code: kVK_ANSI_LeftBracket, symbol: "[") - AssertEqual(code: kVK_ANSI_M, symbol: "m") - AssertEqual(code: kVK_ANSI_Minus, symbol: "-") - AssertEqual(code: kVK_ANSI_N, symbol: "n") - AssertEqual(code: kVK_ANSI_O, symbol: "o") - AssertEqual(code: kVK_ANSI_P, symbol: "p") - AssertEqual(code: kVK_ANSI_Period, symbol: ".") - AssertEqual(code: kVK_ANSI_Q, symbol: "q") - AssertEqual(code: kVK_ANSI_Quote, symbol: "'") - AssertEqual(code: kVK_ANSI_R, symbol: "r") - AssertEqual(code: kVK_ANSI_RightBracket, symbol: "]") - AssertEqual(code: kVK_ANSI_S, symbol: "s") - AssertEqual(code: kVK_ANSI_Semicolon, symbol: ";") - AssertEqual(code: kVK_ANSI_Slash, symbol: "/") - AssertEqual(code: kVK_ANSI_T, symbol: "t") - AssertEqual(code: kVK_ANSI_U, symbol: "u") - AssertEqual(code: kVK_ANSI_V, symbol: "v") - AssertEqual(code: kVK_ANSI_W, symbol: "w") - AssertEqual(code: kVK_ANSI_X, symbol: "x") - AssertEqual(code: kVK_ANSI_Y, symbol: "y") - AssertEqual(code: kVK_ANSI_Z, symbol: "z") - AssertEqual(code: kVK_Delete, symbol: unicharToString(unichar(NSBackspaceCharacter))) - AssertEqual(code: kVK_DownArrow, symbol: unicharToString(unichar(NSDownArrowFunctionKey))) - AssertEqual(code: kVK_End, symbol: unicharToString(unichar(NSEndFunctionKey))) - AssertEqual(code: kVK_Escape, symbol: "\u{1b}") - AssertEqual(code: kVK_F1, symbol: unicharToString(unichar(NSF1FunctionKey))) - AssertEqual(code: kVK_F2, symbol: unicharToString(unichar(NSF2FunctionKey))) - AssertEqual(code: kVK_F3, symbol: unicharToString(unichar(NSF3FunctionKey))) - AssertEqual(code: kVK_F4, symbol: unicharToString(unichar(NSF4FunctionKey))) - AssertEqual(code: kVK_F5, symbol: unicharToString(unichar(NSF5FunctionKey))) - AssertEqual(code: kVK_F6, symbol: unicharToString(unichar(NSF6FunctionKey))) - AssertEqual(code: kVK_F7, symbol: unicharToString(unichar(NSF7FunctionKey))) - AssertEqual(code: kVK_F8, symbol: unicharToString(unichar(NSF8FunctionKey))) - AssertEqual(code: kVK_F9, symbol: unicharToString(unichar(NSF9FunctionKey))) - AssertEqual(code: kVK_F10, symbol: unicharToString(unichar(NSF10FunctionKey))) - AssertEqual(code: kVK_F11, symbol: unicharToString(unichar(NSF11FunctionKey))) - AssertEqual(code: kVK_F12, symbol: unicharToString(unichar(NSF12FunctionKey))) - AssertEqual(code: kVK_F13, symbol: unicharToString(unichar(NSF13FunctionKey))) - AssertEqual(code: kVK_F14, symbol: unicharToString(unichar(NSF14FunctionKey))) - AssertEqual(code: kVK_F15, symbol: unicharToString(unichar(NSF15FunctionKey))) - AssertEqual(code: kVK_F16, symbol: unicharToString(unichar(NSF16FunctionKey))) - AssertEqual(code: kVK_F17, symbol: unicharToString(unichar(NSF17FunctionKey))) - AssertEqual(code: kVK_F18, symbol: unicharToString(unichar(NSF18FunctionKey))) - AssertEqual(code: kVK_F19, symbol: unicharToString(unichar(NSF19FunctionKey))) - AssertEqual(code: kVK_F20, symbol: unicharToString(unichar(NSF20FunctionKey))) - AssertEqual(code: kVK_ForwardDelete, symbol: unicharToString(unichar(NSDeleteCharacter))) - AssertEqual(code: kVK_Help, symbol: unicharToString(unichar(NSHelpFunctionKey))) - AssertEqual(code: kVK_Home, symbol: unicharToString(unichar(NSHomeFunctionKey))) - AssertEqual(code: kVK_ISO_Section, symbol: "§") - AssertEqual(code: kVK_JIS_KeypadComma, symbol: KeyCodeString.jisComma.rawValue) - AssertEqual(code: kVK_JIS_Underscore, symbol: KeyCodeString.jisUnderscore.rawValue) - AssertEqual(code: kVK_JIS_Yen, symbol: KeyCodeString.jisYen.rawValue) - AssertEqual(code: kVK_LeftArrow, symbol: unicharToString(unichar(NSLeftArrowFunctionKey))) - AssertEqual(code: kVK_PageDown, symbol: unicharToString(unichar(NSPageDownFunctionKey))) - AssertEqual(code: kVK_PageUp, symbol: unicharToString(unichar(NSPageUpFunctionKey))) - AssertEqual(code: kVK_Return, symbol: unicharToString(unichar(NSCarriageReturnCharacter))) - AssertEqual(code: kVK_RightArrow, symbol: unicharToString(unichar(NSRightArrowFunctionKey))) - AssertEqual(code: kVK_Space, symbol: " ") - AssertEqual(code: kVK_Tab, symbol: unicharToString(unichar(NSTabCharacter))) - AssertEqual(code: kVK_UpArrow, symbol: unicharToString(unichar(NSUpArrowFunctionKey))) + AssertEqual(code: KeyCode.ansi0, symbol: "0") + AssertEqual(code: KeyCode.ansi1, symbol: "1") + AssertEqual(code: KeyCode.ansi2, symbol: "2") + AssertEqual(code: KeyCode.ansi3, symbol: "3") + AssertEqual(code: KeyCode.ansi4, symbol: "4") + AssertEqual(code: KeyCode.ansi5, symbol: "5") + AssertEqual(code: KeyCode.ansi6, symbol: "6") + AssertEqual(code: KeyCode.ansi7, symbol: "7") + AssertEqual(code: KeyCode.ansi8, symbol: "8") + AssertEqual(code: KeyCode.ansi9, symbol: "9") + AssertEqual(code: KeyCode.ansiA, symbol: "a") + AssertEqual(code: KeyCode.ansiB, symbol: "b") + AssertEqual(code: KeyCode.ansiBackslash, symbol: #"\"#) + AssertEqual(code: KeyCode.ansiC, symbol: "c") + AssertEqual(code: KeyCode.ansiComma, symbol: ",") + AssertEqual(code: KeyCode.ansiD, symbol: "d") + AssertEqual(code: KeyCode.ansiE, symbol: "e") + AssertEqual(code: KeyCode.ansiEqual, symbol: "=") + AssertEqual(code: KeyCode.ansiF, symbol: "f") + AssertEqual(code: KeyCode.ansiG, symbol: "g") + AssertEqual(code: KeyCode.ansiGrave, symbol: "`") + AssertEqual(code: KeyCode.ansiH, symbol: "h") + AssertEqual(code: KeyCode.ansiI, symbol: "i") + AssertEqual(code: KeyCode.ansiJ, symbol: "j") + AssertEqual(code: KeyCode.ansiK, symbol: "k") + AssertEqual(code: KeyCode.ansiKeypad0, symbol: "0") + AssertEqual(code: KeyCode.ansiKeypad1, symbol: "1") + AssertEqual(code: KeyCode.ansiKeypad2, symbol: "2") + AssertEqual(code: KeyCode.ansiKeypad3, symbol: "3") + AssertEqual(code: KeyCode.ansiKeypad4, symbol: "4") + AssertEqual(code: KeyCode.ansiKeypad5, symbol: "5") + AssertEqual(code: KeyCode.ansiKeypad6, symbol: "6") + AssertEqual(code: KeyCode.ansiKeypad7, symbol: "7") + AssertEqual(code: KeyCode.ansiKeypad8, symbol: "8") + AssertEqual(code: KeyCode.ansiKeypad9, symbol: "9") + AssertEqual(code: KeyCode.ansiKeypadDecimal, symbol: ".") + AssertEqual(code: KeyCode.ansiKeypadDivide, symbol: "/") + AssertEqual(code: KeyCode.ansiKeypadEnter, symbol: unicharToString(unichar(NSEnterCharacter))) + AssertEqual(code: KeyCode.ansiKeypadEquals, symbol: "=") + AssertEqual(code: KeyCode.ansiKeypadMinus, symbol: "-") + AssertEqual(code: KeyCode.ansiKeypadMultiply, symbol: "*") + AssertEqual(code: KeyCode.ansiKeypadPlus, symbol: "+") + AssertEqual(code: KeyCode.ansiL, symbol: "l") + AssertEqual(code: KeyCode.ansiLeftBracket, symbol: "[") + AssertEqual(code: KeyCode.ansiM, symbol: "m") + AssertEqual(code: KeyCode.ansiMinus, symbol: "-") + AssertEqual(code: KeyCode.ansiN, symbol: "n") + AssertEqual(code: KeyCode.ansiO, symbol: "o") + AssertEqual(code: KeyCode.ansiP, symbol: "p") + AssertEqual(code: KeyCode.ansiPeriod, symbol: ".") + AssertEqual(code: KeyCode.ansiQ, symbol: "q") + AssertEqual(code: KeyCode.ansiQuote, symbol: "'") + AssertEqual(code: KeyCode.ansiR, symbol: "r") + AssertEqual(code: KeyCode.ansiRightBracket, symbol: "]") + AssertEqual(code: KeyCode.ansiS, symbol: "s") + AssertEqual(code: KeyCode.ansiSemicolon, symbol: ";") + AssertEqual(code: KeyCode.ansiSlash, symbol: "/") + AssertEqual(code: KeyCode.ansiT, symbol: "t") + AssertEqual(code: KeyCode.ansiU, symbol: "u") + AssertEqual(code: KeyCode.ansiV, symbol: "v") + AssertEqual(code: KeyCode.ansiW, symbol: "w") + AssertEqual(code: KeyCode.ansiX, symbol: "x") + AssertEqual(code: KeyCode.ansiY, symbol: "y") + AssertEqual(code: KeyCode.ansiZ, symbol: "z") + AssertEqual(code: KeyCode.delete, symbol: unicharToString(unichar(NSBackspaceCharacter))) + AssertEqual(code: KeyCode.downArrow, symbol: unicharToString(unichar(NSDownArrowFunctionKey))) + AssertEqual(code: KeyCode.end, symbol: unicharToString(unichar(NSEndFunctionKey))) + AssertEqual(code: KeyCode.escape, symbol: "\u{1b}") + AssertEqual(code: KeyCode.f1, symbol: unicharToString(unichar(NSF1FunctionKey))) + AssertEqual(code: KeyCode.f2, symbol: unicharToString(unichar(NSF2FunctionKey))) + AssertEqual(code: KeyCode.f3, symbol: unicharToString(unichar(NSF3FunctionKey))) + AssertEqual(code: KeyCode.f4, symbol: unicharToString(unichar(NSF4FunctionKey))) + AssertEqual(code: KeyCode.f5, symbol: unicharToString(unichar(NSF5FunctionKey))) + AssertEqual(code: KeyCode.f6, symbol: unicharToString(unichar(NSF6FunctionKey))) + AssertEqual(code: KeyCode.f7, symbol: unicharToString(unichar(NSF7FunctionKey))) + AssertEqual(code: KeyCode.f8, symbol: unicharToString(unichar(NSF8FunctionKey))) + AssertEqual(code: KeyCode.f9, symbol: unicharToString(unichar(NSF9FunctionKey))) + AssertEqual(code: KeyCode.f10, symbol: unicharToString(unichar(NSF10FunctionKey))) + AssertEqual(code: KeyCode.f11, symbol: unicharToString(unichar(NSF11FunctionKey))) + AssertEqual(code: KeyCode.f12, symbol: unicharToString(unichar(NSF12FunctionKey))) + AssertEqual(code: KeyCode.f13, symbol: unicharToString(unichar(NSF13FunctionKey))) + AssertEqual(code: KeyCode.f14, symbol: unicharToString(unichar(NSF14FunctionKey))) + AssertEqual(code: KeyCode.f15, symbol: unicharToString(unichar(NSF15FunctionKey))) + AssertEqual(code: KeyCode.f16, symbol: unicharToString(unichar(NSF16FunctionKey))) + AssertEqual(code: KeyCode.f17, symbol: unicharToString(unichar(NSF17FunctionKey))) + AssertEqual(code: KeyCode.f18, symbol: unicharToString(unichar(NSF18FunctionKey))) + AssertEqual(code: KeyCode.f19, symbol: unicharToString(unichar(NSF19FunctionKey))) + AssertEqual(code: KeyCode.f20, symbol: unicharToString(unichar(NSF20FunctionKey))) + AssertEqual(code: KeyCode.forwardDelete, symbol: unicharToString(unichar(NSDeleteCharacter))) + AssertEqual(code: KeyCode.help, symbol: unicharToString(unichar(NSHelpFunctionKey))) + AssertEqual(code: KeyCode.home, symbol: unicharToString(unichar(NSHomeFunctionKey))) + AssertEqual(code: KeyCode.isoSection, symbol: "§") + AssertEqual(code: KeyCode.jisKeypadComma, symbol: KeyCodeString.jisComma.rawValue) + AssertEqual(code: KeyCode.jisUnderscore, symbol: KeyCodeString.jisUnderscore.rawValue) + AssertEqual(code: KeyCode.jisYen, symbol: KeyCodeString.jisYen.rawValue) + AssertEqual(code: KeyCode.leftArrow, symbol: unicharToString(unichar(NSLeftArrowFunctionKey))) + AssertEqual(code: KeyCode.pageDown, symbol: unicharToString(unichar(NSPageDownFunctionKey))) + AssertEqual(code: KeyCode.pageUp, symbol: unicharToString(unichar(NSPageUpFunctionKey))) + AssertEqual(code: KeyCode.return, symbol: unicharToString(unichar(NSCarriageReturnCharacter))) + AssertEqual(code: KeyCode.rightArrow, symbol: unicharToString(unichar(NSRightArrowFunctionKey))) + AssertEqual(code: KeyCode.space, symbol: " ") + AssertEqual(code: KeyCode.tab, symbol: unicharToString(unichar(NSTabCharacter))) + AssertEqual(code: KeyCode.upArrow, symbol: unicharToString(unichar(NSUpArrowFunctionKey))) } func testReverseTransform() { - func AssertEqual(symbol: String, code: Int) { + func AssertEqual(symbol: String, code: KeyCode) { XCTContext.runActivity(named: symbol) { (_) in - XCTAssertEqual(ASCIISymbolicKeyCodeTransformer.shared.reverseTransformedValue(symbol) as! Int, code) + XCTAssertEqual(ASCIISymbolicKeyCodeTransformer.shared.reverseTransformedValue(symbol) as! UInt16, code.rawValue) } } - AssertEqual(symbol: "0", code: kVK_ANSI_0) - AssertEqual(symbol: "1", code: kVK_ANSI_1) - AssertEqual(symbol: "2", code: kVK_ANSI_2) - AssertEqual(symbol: "3", code: kVK_ANSI_3) - AssertEqual(symbol: "4", code: kVK_ANSI_4) - AssertEqual(symbol: "5", code: kVK_ANSI_5) - AssertEqual(symbol: "6", code: kVK_ANSI_6) - AssertEqual(symbol: "7", code: kVK_ANSI_7) - AssertEqual(symbol: "8", code: kVK_ANSI_8) - AssertEqual(symbol: "9", code: kVK_ANSI_9) - AssertEqual(symbol: "a", code: kVK_ANSI_A) - AssertEqual(symbol: "A", code: kVK_ANSI_A) - AssertEqual(symbol: "b", code: kVK_ANSI_B) - AssertEqual(symbol: #"\"#, code: kVK_ANSI_Backslash) - AssertEqual(symbol: "c", code: kVK_ANSI_C) - AssertEqual(symbol: ",", code: kVK_ANSI_Comma) - AssertEqual(symbol: "d", code: kVK_ANSI_D) - AssertEqual(symbol: "e", code: kVK_ANSI_E) - AssertEqual(symbol: "=", code: kVK_ANSI_Equal) - AssertEqual(symbol: "f", code: kVK_ANSI_F) - AssertEqual(symbol: "g", code: kVK_ANSI_G) - AssertEqual(symbol: "`", code: kVK_ANSI_Grave) - AssertEqual(symbol: "h", code: kVK_ANSI_H) - AssertEqual(symbol: "i", code: kVK_ANSI_I) - AssertEqual(symbol: "j", code: kVK_ANSI_J) - AssertEqual(symbol: "k", code: kVK_ANSI_K) - AssertEqual(symbol: unicharToString(unichar(NSEnterCharacter)), code: kVK_ANSI_KeypadEnter) - AssertEqual(symbol: "l", code: kVK_ANSI_L) - AssertEqual(symbol: "[", code: kVK_ANSI_LeftBracket) - AssertEqual(symbol: "m", code: kVK_ANSI_M) - AssertEqual(symbol: "-", code: kVK_ANSI_Minus) - AssertEqual(symbol: "n", code: kVK_ANSI_N) - AssertEqual(symbol: "o", code: kVK_ANSI_O) - AssertEqual(symbol: "p", code: kVK_ANSI_P) - AssertEqual(symbol: ".", code: kVK_ANSI_Period) - AssertEqual(symbol: "q", code: kVK_ANSI_Q) - AssertEqual(symbol: "'", code: kVK_ANSI_Quote) - AssertEqual(symbol: "r", code: kVK_ANSI_R) - AssertEqual(symbol: "]", code: kVK_ANSI_RightBracket) - AssertEqual(symbol: "s", code: kVK_ANSI_S) - AssertEqual(symbol: ";", code: kVK_ANSI_Semicolon) - AssertEqual(symbol: "/", code: kVK_ANSI_Slash) - AssertEqual(symbol: "t", code: kVK_ANSI_T) - AssertEqual(symbol: "u", code: kVK_ANSI_U) - AssertEqual(symbol: "v", code: kVK_ANSI_V) - AssertEqual(symbol: "w", code: kVK_ANSI_W) - AssertEqual(symbol: "x", code: kVK_ANSI_X) - AssertEqual(symbol: "y", code: kVK_ANSI_Y) - AssertEqual(symbol: "z", code: kVK_ANSI_Z) - AssertEqual(symbol: unicharToString(unichar(NSBackspaceCharacter)), code: kVK_Delete) - AssertEqual(symbol: unicharToString(unichar(NSDownArrowFunctionKey)), code: kVK_DownArrow) - AssertEqual(symbol: unicharToString(unichar(NSEndFunctionKey)), code: kVK_End) - AssertEqual(symbol: "\u{1b}", code: kVK_Escape) - AssertEqual(symbol: unicharToString(unichar(NSF1FunctionKey)), code: kVK_F1) - AssertEqual(symbol: unicharToString(unichar(NSF2FunctionKey)), code: kVK_F2) - AssertEqual(symbol: unicharToString(unichar(NSF3FunctionKey)), code: kVK_F3) - AssertEqual(symbol: unicharToString(unichar(NSF4FunctionKey)), code: kVK_F4) - AssertEqual(symbol: unicharToString(unichar(NSF5FunctionKey)), code: kVK_F5) - AssertEqual(symbol: unicharToString(unichar(NSF6FunctionKey)), code: kVK_F6) - AssertEqual(symbol: unicharToString(unichar(NSF7FunctionKey)), code: kVK_F7) - AssertEqual(symbol: unicharToString(unichar(NSF8FunctionKey)), code: kVK_F8) - AssertEqual(symbol: unicharToString(unichar(NSF9FunctionKey)), code: kVK_F9) - AssertEqual(symbol: unicharToString(unichar(NSF10FunctionKey)), code: kVK_F10) - AssertEqual(symbol: unicharToString(unichar(NSF11FunctionKey)), code: kVK_F11) - AssertEqual(symbol: unicharToString(unichar(NSF12FunctionKey)), code: kVK_F12) - AssertEqual(symbol: unicharToString(unichar(NSF13FunctionKey)), code: kVK_F13) - AssertEqual(symbol: unicharToString(unichar(NSF14FunctionKey)), code: kVK_F14) - AssertEqual(symbol: unicharToString(unichar(NSF15FunctionKey)), code: kVK_F15) - AssertEqual(symbol: unicharToString(unichar(NSF16FunctionKey)), code: kVK_F16) - AssertEqual(symbol: unicharToString(unichar(NSF17FunctionKey)), code: kVK_F17) - AssertEqual(symbol: unicharToString(unichar(NSF18FunctionKey)), code: kVK_F18) - AssertEqual(symbol: unicharToString(unichar(NSF19FunctionKey)), code: kVK_F19) - AssertEqual(symbol: unicharToString(unichar(NSF20FunctionKey)), code: kVK_F20) - AssertEqual(symbol: unicharToString(unichar(NSDeleteCharacter)), code: kVK_ForwardDelete) - AssertEqual(symbol: unicharToString(unichar(NSHelpFunctionKey)), code: kVK_Help) - AssertEqual(symbol: unicharToString(unichar(NSHomeFunctionKey)), code: kVK_Home) - AssertEqual(symbol: "§", code: kVK_ISO_Section) - AssertEqual(symbol: unicharToString(unichar(NSLeftArrowFunctionKey)), code: kVK_LeftArrow) - AssertEqual(symbol: unicharToString(unichar(NSPageDownFunctionKey)), code: kVK_PageDown) - AssertEqual(symbol: unicharToString(unichar(NSPageUpFunctionKey)), code: kVK_PageUp) - AssertEqual(symbol: unicharToString(unichar(NSCarriageReturnCharacter)), code: kVK_Return) - AssertEqual(symbol: unicharToString(unichar(NSRightArrowFunctionKey)), code: kVK_RightArrow) - AssertEqual(symbol: " ", code: kVK_Space) - AssertEqual(symbol: unicharToString(unichar(NSTabCharacter)), code: kVK_Tab) - AssertEqual(symbol: unicharToString(unichar(NSBackTabCharacter)), code: kVK_Tab) - AssertEqual(symbol: unicharToString(unichar(NSUpArrowFunctionKey)), code: kVK_UpArrow) - AssertEqual(symbol: "_", code: kVK_JIS_Underscore) - AssertEqual(symbol: "、", code: kVK_JIS_KeypadComma) - AssertEqual(symbol: "¥", code: kVK_JIS_Yen) - AssertEqual(symbol: "*", code: kVK_ANSI_KeypadMultiply); - AssertEqual(symbol: "+", code: kVK_ANSI_KeypadPlus); + AssertEqual(symbol: "0", code: KeyCode.ansi0) + AssertEqual(symbol: "1", code: KeyCode.ansi1) + AssertEqual(symbol: "2", code: KeyCode.ansi2) + AssertEqual(symbol: "3", code: KeyCode.ansi3) + AssertEqual(symbol: "4", code: KeyCode.ansi4) + AssertEqual(symbol: "5", code: KeyCode.ansi5) + AssertEqual(symbol: "6", code: KeyCode.ansi6) + AssertEqual(symbol: "7", code: KeyCode.ansi7) + AssertEqual(symbol: "8", code: KeyCode.ansi8) + AssertEqual(symbol: "9", code: KeyCode.ansi9) + AssertEqual(symbol: "a", code: KeyCode.ansiA) + AssertEqual(symbol: "A", code: KeyCode.ansiA) + AssertEqual(symbol: "b", code: KeyCode.ansiB) + AssertEqual(symbol: #"\"#, code: KeyCode.ansiBackslash) + AssertEqual(symbol: "c", code: KeyCode.ansiC) + AssertEqual(symbol: ",", code: KeyCode.ansiComma) + AssertEqual(symbol: "d", code: KeyCode.ansiD) + AssertEqual(symbol: "e", code: KeyCode.ansiE) + AssertEqual(symbol: "=", code: KeyCode.ansiEqual) + AssertEqual(symbol: "f", code: KeyCode.ansiF) + AssertEqual(symbol: "g", code: KeyCode.ansiG) + AssertEqual(symbol: "`", code: KeyCode.ansiGrave) + AssertEqual(symbol: "h", code: KeyCode.ansiH) + AssertEqual(symbol: "i", code: KeyCode.ansiI) + AssertEqual(symbol: "j", code: KeyCode.ansiJ) + AssertEqual(symbol: "k", code: KeyCode.ansiK) + AssertEqual(symbol: unicharToString(unichar(NSEnterCharacter)), code: KeyCode.ansiKeypadEnter) + AssertEqual(symbol: "l", code: KeyCode.ansiL) + AssertEqual(symbol: "[", code: KeyCode.ansiLeftBracket) + AssertEqual(symbol: "m", code: KeyCode.ansiM) + AssertEqual(symbol: "-", code: KeyCode.ansiMinus) + AssertEqual(symbol: "n", code: KeyCode.ansiN) + AssertEqual(symbol: "o", code: KeyCode.ansiO) + AssertEqual(symbol: "p", code: KeyCode.ansiP) + AssertEqual(symbol: ".", code: KeyCode.ansiPeriod) + AssertEqual(symbol: "q", code: KeyCode.ansiQ) + AssertEqual(symbol: "'", code: KeyCode.ansiQuote) + AssertEqual(symbol: "r", code: KeyCode.ansiR) + AssertEqual(symbol: "]", code: KeyCode.ansiRightBracket) + AssertEqual(symbol: "s", code: KeyCode.ansiS) + AssertEqual(symbol: ";", code: KeyCode.ansiSemicolon) + AssertEqual(symbol: "/", code: KeyCode.ansiSlash) + AssertEqual(symbol: "t", code: KeyCode.ansiT) + AssertEqual(symbol: "u", code: KeyCode.ansiU) + AssertEqual(symbol: "v", code: KeyCode.ansiV) + AssertEqual(symbol: "w", code: KeyCode.ansiW) + AssertEqual(symbol: "x", code: KeyCode.ansiX) + AssertEqual(symbol: "y", code: KeyCode.ansiY) + AssertEqual(symbol: "z", code: KeyCode.ansiZ) + AssertEqual(symbol: unicharToString(unichar(NSBackspaceCharacter)), code: KeyCode.delete) + AssertEqual(symbol: unicharToString(unichar(NSDownArrowFunctionKey)), code: KeyCode.downArrow) + AssertEqual(symbol: unicharToString(unichar(NSEndFunctionKey)), code: KeyCode.end) + AssertEqual(symbol: "\u{1b}", code: KeyCode.escape) + AssertEqual(symbol: unicharToString(unichar(NSF1FunctionKey)), code: KeyCode.f1) + AssertEqual(symbol: unicharToString(unichar(NSF2FunctionKey)), code: KeyCode.f2) + AssertEqual(symbol: unicharToString(unichar(NSF3FunctionKey)), code: KeyCode.f3) + AssertEqual(symbol: unicharToString(unichar(NSF4FunctionKey)), code: KeyCode.f4) + AssertEqual(symbol: unicharToString(unichar(NSF5FunctionKey)), code: KeyCode.f5) + AssertEqual(symbol: unicharToString(unichar(NSF6FunctionKey)), code: KeyCode.f6) + AssertEqual(symbol: unicharToString(unichar(NSF7FunctionKey)), code: KeyCode.f7) + AssertEqual(symbol: unicharToString(unichar(NSF8FunctionKey)), code: KeyCode.f8) + AssertEqual(symbol: unicharToString(unichar(NSF9FunctionKey)), code: KeyCode.f9) + AssertEqual(symbol: unicharToString(unichar(NSF10FunctionKey)), code: KeyCode.f10) + AssertEqual(symbol: unicharToString(unichar(NSF11FunctionKey)), code: KeyCode.f11) + AssertEqual(symbol: unicharToString(unichar(NSF12FunctionKey)), code: KeyCode.f12) + AssertEqual(symbol: unicharToString(unichar(NSF13FunctionKey)), code: KeyCode.f13) + AssertEqual(symbol: unicharToString(unichar(NSF14FunctionKey)), code: KeyCode.f14) + AssertEqual(symbol: unicharToString(unichar(NSF15FunctionKey)), code: KeyCode.f15) + AssertEqual(symbol: unicharToString(unichar(NSF16FunctionKey)), code: KeyCode.f16) + AssertEqual(symbol: unicharToString(unichar(NSF17FunctionKey)), code: KeyCode.f17) + AssertEqual(symbol: unicharToString(unichar(NSF18FunctionKey)), code: KeyCode.f18) + AssertEqual(symbol: unicharToString(unichar(NSF19FunctionKey)), code: KeyCode.f19) + AssertEqual(symbol: unicharToString(unichar(NSF20FunctionKey)), code: KeyCode.f20) + AssertEqual(symbol: unicharToString(unichar(NSDeleteCharacter)), code: KeyCode.forwardDelete) + AssertEqual(symbol: unicharToString(unichar(NSHelpFunctionKey)), code: KeyCode.help) + AssertEqual(symbol: unicharToString(unichar(NSHomeFunctionKey)), code: KeyCode.home) + AssertEqual(symbol: "§", code: KeyCode.isoSection) + AssertEqual(symbol: unicharToString(unichar(NSLeftArrowFunctionKey)), code: KeyCode.leftArrow) + AssertEqual(symbol: unicharToString(unichar(NSPageDownFunctionKey)), code: KeyCode.pageDown) + AssertEqual(symbol: unicharToString(unichar(NSPageUpFunctionKey)), code: KeyCode.pageUp) + AssertEqual(symbol: unicharToString(unichar(NSCarriageReturnCharacter)), code: KeyCode.return) + AssertEqual(symbol: unicharToString(unichar(NSRightArrowFunctionKey)), code: KeyCode.rightArrow) + AssertEqual(symbol: " ", code: KeyCode.space) + AssertEqual(symbol: unicharToString(unichar(NSTabCharacter)), code: KeyCode.tab) + AssertEqual(symbol: unicharToString(unichar(NSBackTabCharacter)), code: KeyCode.tab) + AssertEqual(symbol: unicharToString(unichar(NSUpArrowFunctionKey)), code: KeyCode.upArrow) + AssertEqual(symbol: "_", code: KeyCode.jisUnderscore) + AssertEqual(symbol: "、", code: KeyCode.jisKeypadComma) + AssertEqual(symbol: "¥", code: KeyCode.jisYen) + AssertEqual(symbol: "*", code: KeyCode.ansiKeypadMultiply); + AssertEqual(symbol: "+", code: KeyCode.ansiKeypadPlus); } } diff --git a/Unit Tests/SRRecorderControlTests.swift b/Unit Tests/SRRecorderControlTests.swift index a0cc16c0..52df72a5 100644 --- a/Unit Tests/SRRecorderControlTests.swift +++ b/Unit Tests/SRRecorderControlTests.swift @@ -92,11 +92,11 @@ class SRRecorderControlTests: XCTestCase { func testComaptibilityBindingAndViewChangeWithShortcut() { let v = RecorderControl() v.bind(NSBindingName.value, to: NSUserDefaultsController.shared, withKeyPath: "values.shortcut", options: nil) - let keyCode: UInt16 = 0 + let keyCode = KeyCode.ansiA let modifierFlags: NSEvent.ModifierFlags = [.command, .option] let objectValue = Shortcut(code: keyCode, modifierFlags: modifierFlags, characters: nil, charactersIgnoringModifiers: nil) v.setValue(objectValue, forKey: "objectValue") - XCTAssertEqual(UserDefaults.standard.value(forKeyPath: "shortcut.keyCode") as! UInt16, keyCode) + XCTAssertEqual(UserDefaults.standard.value(forKeyPath: "shortcut.keyCode") as! UInt16, keyCode.rawValue) XCTAssertEqual(UserDefaults.standard.value(forKeyPath: "shortcut.modifierFlags") as! UInt, modifierFlags.rawValue) XCTAssertTrue(v.value(forKey: "isCompatibilityModeEnabled") as! Bool) } @@ -124,12 +124,12 @@ class SRRecorderControlTests: XCTestCase { observation.invalidate() } - let s1 = Shortcut(code: UInt16(kVK_ANSI_A), + let s1 = Shortcut(code: KeyCode.ansiA, modifierFlags: .command, characters: "A", charactersIgnoringModifiers: "a") v.objectValue = s1 - let s2 = Shortcut(code: UInt16(kVK_ANSI_B), + let s2 = Shortcut(code: KeyCode.ansiB, modifierFlags: .command, characters: "B", charactersIgnoringModifiers: "b") @@ -248,4 +248,12 @@ class SRRecorderControlTests: XCTestCase { control.userInterfaceLayoutDirection = .leftToRight wait(for: [expectation], timeout: 0) } + + func testDisallowedEmptyModifierFlags() { + let control = RecorderControl() + control.set(allowedModifierFlags: CocoaModifierFlagsMask, + requiredModifierFlags: [], + allowsEmptyModifierFlags: false) + XCTAssertFalse(control.areModifierFlagsAllowed([], for: .ansiA)) + } } diff --git a/Unit Tests/SRShortcutActionTests.swift b/Unit Tests/SRShortcutActionTests.swift index bdc2c98c..b4162c34 100644 --- a/Unit Tests/SRShortcutActionTests.swift +++ b/Unit Tests/SRShortcutActionTests.swift @@ -8,8 +8,10 @@ import XCTest import ShortcutRecorder +/// Utility class to test Target-Action. fileprivate class Target: NSObject, ShortcutActionTarget, NSUserInterfaceValidations { - let expectation = XCTestExpectation() + let expectation = XCTestExpectation(description: "Target-Action", assertForOverFulfill: true) + func perform(shortcutAction anAction: ShortcutAction) -> Bool { expectation.fulfill() return true @@ -332,49 +334,137 @@ class SRShortcutActionTests: XCTestCase { class SRShortcutMonitorTests: XCTestCase { /** - A subclass of ShortcutMonitor that tracks addition and removal of shortcuts - as well as verifies invariants in the hooks. + A subclass of ShortcutMonitor that tracks changes and verifies invariants. */ class TrackingMonitor: ShortcutMonitor { enum Change: Equatable { - case add(Shortcut) - case remove(Shortcut) + case willChangeActions(Set) + case didChangeActions(Set, Set) + case willChangeShortcuts(Set) + case didChangeShortcuts(Set, Set) + case willAddShortcut(Shortcut) + case didAddShortcut(Shortcut) + case willRemoveShortcut(Shortcut) + case didRemoveShortcut(Shortcut) } var changes: [Change] = [] + var actionsObserver: NSKeyValueObservation! + var shortcutsObserver: NSKeyValueObservation! + + override init() { + super.init() + + actionsObserver = self.observe(\.actions, options: [.new, .old, .prior]) { monitor, change in + let oldActions = Set(change.oldValue!) + let allActions = Set(self.actions) + + if change.isPrior { + self.changes.append(.willChangeActions(Set(change.oldValue ?? []))) + XCTAssertEqual(oldActions, allActions) + } + else { + self.changes.append(.didChangeActions(Set(change.oldValue ?? []), Set(change.newValue ?? []))) + let newActions = Set(change.newValue!) + XCTAssertEqual(newActions, allActions) + } + } + + shortcutsObserver = self.observe(\.shortcuts, options: [.new, .old, .prior]) { monitor, change in + let oldShortcuts = Set(change.oldValue!) + let allShortcuts = Set(self.shortcuts) + + if change.isPrior { + self.changes.append(.willChangeShortcuts(Set(change.oldValue ?? []))) + XCTAssertEqual(oldShortcuts, allShortcuts) + } + else { + self.changes.append(.didChangeShortcuts(Set(change.oldValue ?? []), Set(change.newValue ?? []))) + let newShortcuts = Set(change.newValue!) + XCTAssertEqual(newShortcuts, allShortcuts) + } + } + } + + override func willAddShortcut(_ aShortcut: Shortcut) { + changes.append(.willAddShortcut(aShortcut)) + + let allShortcuts = Set(shortcuts) + + let allActions = Set(actions) + let allKeyDownActions = Set(actions(forKeyEvent: .down)) + let allKeyUpActions = Set(actions(forKeyEvent: .up)) + + let enabledKeyDownActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .down)) + let enabledKeyUpActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .up)) + let enabledActionsForShortcut = enabledKeyDownActionsForShortcut.union(enabledKeyUpActionsForShortcut) + + XCTAssertEqual(allActions, allKeyDownActions.union(allKeyUpActions)) + XCTAssertFalse(allShortcuts.contains(aShortcut)) + XCTAssertTrue(enabledActionsForShortcut.isEmpty) + XCTAssertTrue(allKeyDownActions.isSuperset(of: enabledKeyDownActionsForShortcut)) + XCTAssertTrue(allKeyUpActions.isSuperset(of: enabledKeyUpActionsForShortcut)) + } + override func didAddShortcut(_ aShortcut: Shortcut) { - changes.append(Change.add(aShortcut)) + changes.append(.didAddShortcut(aShortcut)) + + let allShortcuts = Set(shortcuts) + + let allActions = Set(actions) + let allKeyDownActions = Set(actions(forKeyEvent: .down)) + let allKeyUpActions = Set(actions(forKeyEvent: .up)) - let allActions = actions.filter { (action) in action.shortcut == aShortcut } - let keyDownActions = actions(forKeyEvent: .down).filter { (action) in action.shortcut == aShortcut } - let keyUpActions = actions(forKeyEvent: .up).filter { (action) in action.shortcut == aShortcut } - let shortcutKeyDownActions = actions(forShortcut: aShortcut, keyEvent: .down) - let shortcutKeyUpActions = actions(forShortcut: aShortcut, keyEvent: .up) + let enabledKeyDownActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .down)) + let enabledKeyUpActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .up)) + let enabledActionsForShortcut = enabledKeyDownActionsForShortcut.union(enabledKeyUpActionsForShortcut) - XCTAssertTrue(shortcuts.contains(aShortcut)) - XCTAssertFalse(allActions.isEmpty) - XCTAssertEqual(Set(allActions), Set(keyDownActions).union(keyUpActions)) - XCTAssertEqual(Set(allActions), Set(shortcutKeyDownActions).union(shortcutKeyUpActions)) - XCTAssertEqual(action(for: aShortcut, keyEvent: .down), shortcutKeyDownActions.last as ShortcutAction?) - XCTAssertEqual(action(for: aShortcut, keyEvent: .up), shortcutKeyUpActions.last as ShortcutAction?) + XCTAssertEqual(allActions, allKeyDownActions.union(allKeyUpActions)) + XCTAssertTrue(allShortcuts.contains(aShortcut)) + XCTAssertFalse(enabledActionsForShortcut.isEmpty) + XCTAssertTrue(allKeyDownActions.isSuperset(of: enabledKeyDownActionsForShortcut)) + XCTAssertTrue(allKeyUpActions.isSuperset(of: enabledKeyUpActionsForShortcut)) + } + + override func willRemoveShortcut(_ aShortcut: Shortcut) { + changes.append(.willRemoveShortcut(aShortcut)) + + let allShortcuts = Set(shortcuts) + + let allActions = Set(actions) + let allKeyDownActions = Set(actions(forKeyEvent: .down)) + let allKeyUpActions = Set(actions(forKeyEvent: .up)) + + let enabledKeyDownActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .down)) + let enabledKeyUpActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .up)) + let enabledActionsForShortcut = enabledKeyDownActionsForShortcut.union(enabledKeyUpActionsForShortcut) + + XCTAssertEqual(allActions, allKeyDownActions.union(allKeyUpActions)) + XCTAssertTrue(allShortcuts.contains(aShortcut)) + XCTAssertFalse(enabledActionsForShortcut.isEmpty) + XCTAssertTrue(allKeyDownActions.isSuperset(of: enabledKeyDownActionsForShortcut)) + XCTAssertTrue(allKeyUpActions.isSuperset(of: enabledKeyUpActionsForShortcut)) } override func didRemoveShortcut(_ aShortcut: Shortcut) { - changes.append(Change.remove(aShortcut)) + changes.append(.didRemoveShortcut(aShortcut)) + + let allShortcuts = Set(shortcuts) - let allActions = actions.filter { (action) in action.shortcut == aShortcut } - let keyDownActions = actions(forKeyEvent: .down).filter { (action) in action.shortcut == aShortcut } - let keyUpActions = actions(forKeyEvent: .up).filter { (action) in action.shortcut == aShortcut } - let shortcutKeyDownActions = actions(forShortcut: aShortcut, keyEvent: .down) - let shortcutKeyUpActions = actions(forShortcut: aShortcut, keyEvent: .up) + let allActions = Set(actions) + let allKeyDownActions = Set(actions(forKeyEvent: .down)) + let allKeyUpActions = Set(actions(forKeyEvent: .up)) - XCTAssertFalse(shortcuts.contains(aShortcut)) - XCTAssertTrue(allActions.isEmpty) - XCTAssertEqual(Set(allActions), Set(keyDownActions).union(keyUpActions)) - XCTAssertEqual(Set(allActions), Set(shortcutKeyDownActions).union(shortcutKeyUpActions)) - XCTAssertEqual(action(for: aShortcut, keyEvent: .down), shortcutKeyDownActions.last as ShortcutAction?) - XCTAssertEqual(action(for: aShortcut, keyEvent: .up), shortcutKeyUpActions.last as ShortcutAction?) + let enabledKeyDownActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .down)) + let enabledKeyUpActionsForShortcut = Set(enabledActions(forShortcut: aShortcut, keyEvent: .up)) + let enabledActionsForShortcut = enabledKeyDownActionsForShortcut.union(enabledKeyUpActionsForShortcut) + + XCTAssertEqual(allActions, allKeyDownActions.union(allKeyUpActions)) + XCTAssertFalse(allShortcuts.contains(aShortcut)) + XCTAssertTrue(enabledActionsForShortcut.isEmpty) + XCTAssertTrue(allKeyDownActions.isSuperset(of: enabledKeyDownActionsForShortcut)) + XCTAssertTrue(allKeyUpActions.isSuperset(of: enabledKeyUpActionsForShortcut)) } } @@ -385,7 +475,20 @@ class SRShortcutMonitorTests: XCTestCase { let monitor = TrackingMonitor() monitor.addAction(action, forKeyEvent: .down) monitor.removeAction(action) - XCTAssertEqual(monitor.changes, [.add(.default), .remove(.default)]) + XCTAssertEqual(monitor.changes, [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])), + .didChangeActions(Set(), Set([action])), + .willChangeActions(Set([action])), + .willChangeShortcuts(Set([.default])), + .willRemoveShortcut(.default), + .didRemoveShortcut(.default), + .didChangeShortcuts(Set([.default]), Set()), + .didChangeActions(Set([action]), Set()) + ]) } XCTContext.runActivity(named: "down key event") { _ in test(.down) } @@ -398,17 +501,43 @@ class SRShortcutMonitorTests: XCTestCase { let cmd_a = Shortcut(keyEquivalent: "⌘A")! let cmd_b = Shortcut(keyEquivalent: "⌘B")! + var changes: [TrackingMonitor.Change] = [] + monitor.addAction(action, forKeyEvent: .down) - XCTAssertEqual(monitor.changes, []) + changes.append(contentsOf: [ + .willChangeActions(Set()), + .didChangeActions(Set(), Set([action])) + ]) + XCTAssertEqual(monitor.changes, changes) action.shortcut = cmd_a - XCTAssertEqual(monitor.changes, [.add(cmd_a)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set()), + .willAddShortcut(cmd_a), + .didAddShortcut(cmd_a), + .didChangeShortcuts(Set(), Set([cmd_a])) + ]) + XCTAssertEqual(monitor.changes, changes) action.shortcut = cmd_b - XCTAssertEqual(monitor.changes, [.add(cmd_a), .remove(cmd_a), .add(cmd_b)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_a])), + .willRemoveShortcut(cmd_a), + .didRemoveShortcut(cmd_a), + .willAddShortcut(cmd_b), + .didAddShortcut(cmd_b), + .didChangeShortcuts(Set([cmd_a]), Set([cmd_b])) + ]) + XCTAssertEqual(monitor.changes, changes) action.shortcut = nil - XCTAssertEqual(monitor.changes, [.add(cmd_a), .remove(cmd_a), .add(cmd_b), .remove(cmd_b)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_b])), + .willRemoveShortcut(cmd_b), + .didRemoveShortcut(cmd_b), + .didChangeShortcuts(Set([cmd_b]), Set([])) + ]) + XCTAssertEqual(monitor.changes, changes) } func testShortcutObservationOfMultipleActionsAdditionAndRemoval() { @@ -421,9 +550,32 @@ class SRShortcutMonitorTests: XCTestCase { monitor.addAction(action1, forKeyEvent: keyEvent1) monitor.addAction(action2, forKeyEvent: keyEvent2) monitor.removeAction(action1) - XCTAssertEqual(monitor.changes, [.add(cmd_a)]) + var changes: [TrackingMonitor.Change] = [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(cmd_a), + .didAddShortcut(cmd_a), + .didChangeShortcuts(Set(), Set([cmd_a])), + .didChangeActions(Set(), Set([action1])), + + .willChangeActions(Set([action1])), + .didChangeActions(Set([action1]), Set([action1, action2])), + + .willChangeActions(Set([action1, action2])), + .didChangeActions(Set([action1, action2]), Set([action2])) + ] + XCTAssertEqual(monitor.changes, changes) + monitor.removeAction(action2) - XCTAssertEqual(monitor.changes, [.add(cmd_a), .remove(cmd_a)]) + changes.append(contentsOf: [ + .willChangeActions(Set([action2])), + .willChangeShortcuts(Set([cmd_a])), + .willRemoveShortcut(cmd_a), + .didRemoveShortcut(cmd_a), + .didChangeShortcuts(Set([cmd_a]), Set()), + .didChangeActions(Set([action2]), Set()) + ]) + XCTAssertEqual(monitor.changes, changes) } XCTContext.runActivity(named: "down key event") { _ in test(.down, .down) } @@ -433,63 +585,82 @@ class SRShortcutMonitorTests: XCTestCase { } func testShortcutChangeObservationOfMultipleActions() { - let cmd_a = Shortcut(keyEquivalent: "⌘A")! - let cmd_b = Shortcut(keyEquivalent: "⌘B")! - let action1 = ShortcutAction() - let action2 = ShortcutAction() - func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { + let cmd_a = Shortcut(keyEquivalent: "⌘A")! + let cmd_b = Shortcut(keyEquivalent: "⌘B")! + let action1 = ShortcutAction() + let action2 = ShortcutAction() let monitor = TrackingMonitor() monitor.addAction(action1, forKeyEvent: keyEvent1) monitor.addAction(action2, forKeyEvent: keyEvent2) - XCTAssertEqual(monitor.changes, []) + var changes: [TrackingMonitor.Change] = [ + .willChangeActions(Set()), + .didChangeActions(Set(), Set([action1])), + .willChangeActions(Set([action1])), + .didChangeActions(Set([action1]), Set([action1, action2])) + ] + XCTAssertEqual(monitor.changes, changes) action1.shortcut = cmd_a - XCTAssertEqual(monitor.changes, [.add(cmd_a)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set()), + .willAddShortcut(cmd_a), + .didAddShortcut(cmd_a), + .didChangeShortcuts(Set(), Set([cmd_a])) + ]) + XCTAssertEqual(monitor.changes, changes) action2.shortcut = cmd_a - XCTAssertEqual(monitor.changes, [.add(cmd_a)]) + XCTAssertEqual(monitor.changes, changes) action1.shortcut = cmd_b - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_a])), + .willAddShortcut(cmd_b), + .didAddShortcut(cmd_b), + .didChangeShortcuts(Set([cmd_a]), Set([cmd_a, cmd_b])) + ]) + XCTAssertEqual(monitor.changes, changes) action2.shortcut = cmd_b - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b), .remove(cmd_a)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_a, cmd_b])), + .willRemoveShortcut(cmd_a), + .didRemoveShortcut(cmd_a), + .didChangeShortcuts(Set([cmd_a, cmd_b]), Set([cmd_b])) + ]) + XCTAssertEqual(monitor.changes, changes) action1.shortcut = cmd_a - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b), .remove(cmd_a), .add(cmd_a)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_b])), + .willAddShortcut(cmd_a), + .didAddShortcut(cmd_a), + .didChangeShortcuts(Set([cmd_b]), Set([cmd_a, cmd_b])) + ]) + XCTAssertEqual(monitor.changes, changes) action2.shortcut = cmd_a - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b), .remove(cmd_a), .add(cmd_a), .remove(cmd_b)]) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_a, cmd_b])), + .willRemoveShortcut(cmd_b), + .didRemoveShortcut(cmd_b), + .didChangeShortcuts(Set([cmd_a, cmd_b]), Set([cmd_a])) + ]) + XCTAssertEqual(monitor.changes, changes) action1.shortcut = nil - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b), .remove(cmd_a), .add(cmd_a), .remove(cmd_b)]) + XCTAssertEqual(monitor.changes, changes) action2.shortcut = nil - XCTAssertEqual(monitor.changes, [.add(cmd_a), .add(cmd_b), .remove(cmd_a), .add(cmd_a), .remove(cmd_b), .remove(cmd_a)]) - } - - XCTContext.runActivity(named: "down key event") { _ in test(.down, .down) } - XCTContext.runActivity(named: "up key event") { _ in test(.up, .up) } - XCTContext.runActivity(named: "down & up key events") { _ in test(.down, .up) } - XCTContext.runActivity(named: "up & down key events") { _ in test(.up, .down) } - } - - func testRemovalOfAllActionsForShortcut() { - let action1 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘A")!) {_ in true} - let action2 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘B")!) {_ in true} - - func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { - let monitor = TrackingMonitor() - monitor.addAction(action1, forKeyEvent: keyEvent1) - monitor.addAction(action1, forKeyEvent: keyEvent2) - monitor.addAction(action2, forKeyEvent: keyEvent1) - monitor.addAction(action2, forKeyEvent: keyEvent2) - monitor.removeAllActions(forShortcut: action1.shortcut!) - XCTAssertEqual(monitor.changes, [.add(action1.shortcut!), .add(action2.shortcut!), .remove(action1.shortcut!)]) - XCTAssertEqual(monitor.actions(forShortcut: action1.shortcut!, keyEvent: .down), []) - XCTAssertEqual(monitor.actions(forShortcut: action1.shortcut!, keyEvent: .up), []) + changes.append(contentsOf: [ + .willChangeShortcuts(Set([cmd_a])), + .willRemoveShortcut(cmd_a), + .didRemoveShortcut(cmd_a), + .didChangeShortcuts(Set([cmd_a]), Set()) + ]) + XCTAssertEqual(monitor.changes, changes) } XCTContext.runActivity(named: "down key event") { _ in test(.down, .down) } @@ -498,21 +669,6 @@ class SRShortcutMonitorTests: XCTestCase { XCTContext.runActivity(named: "up & down key events") { _ in test(.up, .down) } } - func testRemovalOfAllActionsForKeyEvent() { - let action1 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘A")!) {_ in true} - let action2 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘B")!) {_ in true} - let monitor = TrackingMonitor() - monitor.addAction(action1, forKeyEvent: .down) - monitor.addAction(action2, forKeyEvent: .up) - monitor.removeAllActions(forKeyEvent: .down) - XCTAssertEqual(monitor.changes, [.add(action1.shortcut!), .add(action2.shortcut!), .remove(action1.shortcut!)]) - XCTAssertEqual(monitor.actions(forKeyEvent: .down), []) - monitor.removeAllActions(forKeyEvent: .up) - XCTAssertEqual(monitor.changes, [.add(action1.shortcut!), .add(action2.shortcut!), .remove(action1.shortcut!), .remove(action2.shortcut!)]) - XCTAssertEqual(monitor.actions(forKeyEvent: .up), []) - XCTAssertEqual(monitor.actions, []) - } - func testRemovalOfAllActions() { let action1 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘A")!) {_ in true} let action2 = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌘B")!) {_ in true} @@ -520,8 +676,48 @@ class SRShortcutMonitorTests: XCTestCase { monitor.addAction(action1, forKeyEvent: .down) monitor.addAction(action2, forKeyEvent: .up) monitor.removeAllActions() - XCTAssertEqual(monitor.changes[..<2], [.add(action1.shortcut!), .add(action2.shortcut!)]) - XCTAssertTrue(monitor.changes[2...].allSatisfy([.remove(action1.shortcut!), .remove(action2.shortcut!)].contains)) + XCTAssertEqual(monitor.changes[..<14], [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(action1.shortcut!), + .didAddShortcut(action1.shortcut!), + .didChangeShortcuts(Set(), Set([action1.shortcut!])), + .didChangeActions(Set(), Set([action1])), + + .willChangeActions(Set([action1])), + .willChangeShortcuts(Set([action1.shortcut!])), + .willAddShortcut(action2.shortcut!), + .didAddShortcut(action2.shortcut!), + .didChangeShortcuts(Set([action1.shortcut!]), Set([action1.shortcut!, action2.shortcut!])), + .didChangeActions(Set([action1]), Set([action1, action2])), + + .willChangeActions(Set([action1, action2])), + .willChangeShortcuts(Set([action1.shortcut!, action2.shortcut!])), + ]) + + // shortcuts may be removed in any order + let action1RemovalChanges: [TrackingMonitor.Change] = [ + .willRemoveShortcut(action1.shortcut!), + .willRemoveShortcut(action2.shortcut!), + .didRemoveShortcut(action2.shortcut!), + .didRemoveShortcut(action1.shortcut!) + ] + let action2RemovalChanges: [TrackingMonitor.Change] = [ + .willRemoveShortcut(action2.shortcut!), + .willRemoveShortcut(action1.shortcut!), + .didRemoveShortcut(action1.shortcut!), + .didRemoveShortcut(action2.shortcut!) + ] + XCTAssertTrue( + (Array(monitor.changes[14...17]) == action1RemovalChanges) || + (Array(monitor.changes[14...17]) == action2RemovalChanges) + ) + + XCTAssertEqual(monitor.changes[18...], [ + .didChangeShortcuts(Set([action1.shortcut!, action2.shortcut!]), Set()), + .didChangeActions(Set([action1, action2]), Set()) + ]) + XCTAssertEqual(monitor.actions(forKeyEvent: .down), []) XCTAssertEqual(monitor.actions(forKeyEvent: .up), []) XCTAssertEqual(monitor.actions, []) @@ -535,12 +731,10 @@ class SRShortcutMonitorTests: XCTestCase { let monitor = TrackingMonitor() monitor.addAction(action1, forKeyEvent: keyEvent) monitor.addAction(action2, forKeyEvent: keyEvent) - XCTAssertEqual(monitor.action(for: .default, keyEvent: keyEvent), action2) - XCTAssertEqual(monitor.actions(forShortcut: .default, keyEvent: keyEvent), [action1, action2]) + XCTAssertEqual(monitor.enabledActions(forShortcut: .default, keyEvent: keyEvent), [action1, action2]) monitor.addAction(action1, forKeyEvent: keyEvent) - XCTAssertEqual(monitor.action(for: .default, keyEvent: keyEvent), action1) - XCTAssertEqual(monitor.actions(forShortcut: .default, keyEvent: keyEvent), [action2, action1]) + XCTAssertEqual(monitor.enabledActions(forShortcut: .default, keyEvent: keyEvent), [action2, action1]) } XCTContext.runActivity(named: "down key event") { _ in test(.down) } @@ -553,12 +747,16 @@ class SRShortcutMonitorTests: XCTestCase { let removeObserverExpectation = XCTestExpectation(description: "remove observer", assertForOverFulfill: true) override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) { - addObserverExpectation.fulfill() + if keyPath == "enabled" { + addObserverExpectation.fulfill() + } super.addObserver(observer, forKeyPath: keyPath, options: options, context: context) } override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) { - removeObserverExpectation.fulfill() + if keyPath == "enabled" { + removeObserverExpectation.fulfill() + } super.removeObserver(observer, forKeyPath: keyPath, context: context) } } @@ -566,6 +764,7 @@ class SRShortcutMonitorTests: XCTestCase { func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { let action = ObservedAction(shortcut: .default) { _ in true } let monitor = TrackingMonitor() + monitor.addAction(action, forKeyEvent: keyEvent1) monitor.addAction(action, forKeyEvent: keyEvent2) monitor.removeAction(action, forKeyEvent: keyEvent1) @@ -580,14 +779,28 @@ class SRShortcutMonitorTests: XCTestCase { } func testActionAddedTwiceForTheSameKeyEventNeedsToBeRemovedOnce() { - let action = ShortcutAction(shortcut: .default) { _ in true } - func test(_ keyEvent: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } let monitor = TrackingMonitor() + monitor.addAction(action, forKeyEvent: keyEvent) monitor.addAction(action, forKeyEvent: keyEvent) monitor.removeAction(action, forKeyEvent: keyEvent) - XCTAssertEqual(monitor.changes, [.add(action.shortcut!), .remove(action.shortcut!)]) + XCTAssertEqual(monitor.changes, [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])), + .didChangeActions(Set(), Set([action])), + + .willChangeActions(Set([action])), + .willChangeShortcuts(Set([.default])), + .willRemoveShortcut(.default), + .didRemoveShortcut(.default), + .didChangeShortcuts(Set([.default]), Set()), + .didChangeActions(Set([action]), Set()) + ]) } XCTContext.runActivity(named: "down key event") { _ in test(.down) } @@ -595,16 +808,33 @@ class SRShortcutMonitorTests: XCTestCase { } func testActionAddedTwiceForDifferentKeyEventsNeedsToBeRemovedTwice() { - let action = ShortcutAction(shortcut: .default) { _ in true } - func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } let monitor = TrackingMonitor() + monitor.addAction(action, forKeyEvent: keyEvent1) monitor.addAction(action, forKeyEvent: keyEvent2) monitor.removeAction(action, forKeyEvent: keyEvent1) - XCTAssertEqual(monitor.changes, [.add(action.shortcut!)]) + var changes: [TrackingMonitor.Change] = [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])), + .didChangeActions(Set(), Set([action])) + ] + XCTAssertEqual(monitor.changes, changes) + monitor.removeAction(action, forKeyEvent: keyEvent2) - XCTAssertEqual(monitor.changes, [.add(action.shortcut!), .remove(action.shortcut!)]) + changes.append(contentsOf: [ + .willChangeActions(Set([action])), + .willChangeShortcuts(Set([.default])), + .willRemoveShortcut(.default), + .didRemoveShortcut(.default), + .didChangeShortcuts(Set([.default]), Set()), + .didChangeActions(Set([action]), Set()) + ]) + XCTAssertEqual(monitor.changes, changes) } XCTContext.runActivity(named: "down & up key events") { _ in test(.down, .up) } @@ -612,9 +842,8 @@ class SRShortcutMonitorTests: XCTestCase { } func testActionAddedTwiceForTheSameKeyEventInvariants() { - let action = ShortcutAction(shortcut: .default) { _ in true } - func test(_ keyEvent: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } let monitor = TrackingMonitor() let oppositeKeyEvent: KeyEventType = keyEvent == .down ? .up : .down @@ -635,9 +864,8 @@ class SRShortcutMonitorTests: XCTestCase { } func testActionAddedTwiceForDifferentKeyEventsInvariants() { - let action = ShortcutAction(shortcut: .default) { _ in true } - func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } let monitor = TrackingMonitor() monitor.addAction(action, forKeyEvent: keyEvent1) @@ -660,6 +888,114 @@ class SRShortcutMonitorTests: XCTestCase { XCTContext.runActivity(named: "down & up key events") { _ in test(.down, .up) } XCTContext.runActivity(named: "up & down key events") { _ in test(.up, .down) } } + + func testAddingDisabledActionDoesNotChangeShortcuts() { + func test(_ keyEvent: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } + action.isEnabled = false + let monitor = TrackingMonitor() + + monitor.addAction(action, forKeyEvent: keyEvent) + monitor.removeAction(action) + XCTAssertEqual(monitor.changes, [ + .willChangeActions(Set()), + .didChangeActions(Set(), Set([action])), + + .willChangeActions(Set([action])), + .didChangeActions(Set([action]), Set()) + ]) + } + + XCTContext.runActivity(named: "down key event") { _ in test(.down) } + XCTContext.runActivity(named: "up key event") { _ in test(.up) } + } + + func testMonitorTracksWhetherActionIsEnabled() { + func test(_ keyEvent: KeyEventType) { + let action = ShortcutAction(shortcut: .default) { _ in true } + let monitor = TrackingMonitor() + + monitor.addAction(action, forKeyEvent: keyEvent) + var changes: [TrackingMonitor.Change] = [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])), + .didChangeActions(Set(), Set([action])), + ] + XCTAssertEqual(monitor.changes, changes) + + action.isEnabled = false + changes.append(contentsOf: [ + .willChangeShortcuts(Set([.default])), + .willRemoveShortcut(.default), + .didRemoveShortcut(.default), + .didChangeShortcuts(Set([.default]), Set()) + ]) + XCTAssertEqual(monitor.changes, changes) + + action.isEnabled = true + changes.append(contentsOf: [ + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])) + ]) + XCTAssertEqual(monitor.changes, changes) + } + + XCTContext.runActivity(named: "down key event") { _ in test(.down) } + XCTContext.runActivity(named: "up key event") { _ in test(.up) } + } + + func testMonitorWhetherAllActionsForShortcutAreEnabled() { + func test(_ keyEvent1: KeyEventType, _ keyEvent2: KeyEventType) { + let action1 = ShortcutAction(shortcut: .default) { _ in true } + let action2 = ShortcutAction(shortcut: .default) { _ in true } + let monitor = TrackingMonitor() + + monitor.addAction(action1, forKeyEvent: keyEvent1) + monitor.addAction(action2, forKeyEvent: keyEvent2) + var changes: [TrackingMonitor.Change] = [ + .willChangeActions(Set()), + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])), + .didChangeActions(Set(), Set([action1])), + + .willChangeActions(Set([action1])), + .didChangeActions(Set([action1]), Set([action1, action2])) + ] + XCTAssertEqual(monitor.changes, changes) + + action1.isEnabled = false + XCTAssertEqual(monitor.changes, changes) + + action2.isEnabled = false + changes.append(contentsOf: [ + .willChangeShortcuts(Set([.default])), + .willRemoveShortcut(.default), + .didRemoveShortcut(.default), + .didChangeShortcuts(Set([.default]), Set()) + ]) + XCTAssertEqual(monitor.changes, changes) + + action1.isEnabled = true + changes.append(contentsOf: [ + .willChangeShortcuts(Set()), + .willAddShortcut(.default), + .didAddShortcut(.default), + .didChangeShortcuts(Set(), Set([.default])) + ]) + } + + XCTContext.runActivity(named: "down & down key events") { _ in test(.down, .down) } + XCTContext.runActivity(named: "down & up key events") { _ in test(.down, .up) } + XCTContext.runActivity(named: "up & down key events") { _ in test(.up, .down) } + XCTContext.runActivity(named: "up & up key events") { _ in test(.down, .down) } + } } @@ -671,15 +1007,17 @@ class SRGlobalShortcutMonitorTests: XCTestCase { enum Change: Equatable { case add, remove } var changes: [Change] = [] - var didAddExpectation: XCTestExpectation? - var didRemoveExpectation: XCTestExpectation? + var didAddExpectation: XCTestExpectation! + var didRemoveExpectation: XCTestExpectation! override func didAddEventHandler() { + super.didAddEventHandler() changes.append(.add) didAddExpectation?.fulfill() } override func didRemoveEventHandler() { + super.didRemoveEventHandler() changes.append(.remove) didRemoveExpectation?.fulfill() } @@ -781,4 +1119,17 @@ class SRGlobalShortcutMonitorTests: XCTestCase { monitor.resume() XCTAssertEqual(monitor.changes, [.add, .remove, .add]) } + + func testAddingKeylessShortcutDoesNotInstallHandler() { + let monitor = TrackingMonitor() + monitor.didAddExpectation = XCTestExpectation(description: "did add", isInverted: true) + monitor.didRemoveExpectation = XCTestExpectation(description: "did remove", isInverted: true) + let shortcut = Shortcut(code: .none, modifierFlags: .shift, characters: nil, charactersIgnoringModifiers: nil) + let action = ShortcutAction(shortcut: shortcut) { _ in true } + + monitor.addAction(action, forKeyEvent: .down) + monitor.removeAllActions() + + wait(for: [monitor.didAddExpectation, monitor.didRemoveExpectation], timeout: 0, enforceOrder: true) + } } diff --git a/Unit Tests/SRShortcutControllerTests.swift b/Unit Tests/SRShortcutControllerTests.swift index c5a8838e..e7084d0a 100644 --- a/Unit Tests/SRShortcutControllerTests.swift +++ b/Unit Tests/SRShortcutControllerTests.swift @@ -116,8 +116,8 @@ class SRShortcutControllerTests: XCTestCase { ]) } - let s1 = Shortcut(code: 0, modifierFlags: .option, characters: "å", charactersIgnoringModifiers: "a") - let s2 = Shortcut(code: 1, modifierFlags: .command, characters: "b", charactersIgnoringModifiers: "b") + let s1 = Shortcut(code: KeyCode.ansiA, modifierFlags: .option, characters: "å", charactersIgnoringModifiers: "a") + let s2 = Shortcut(code: KeyCode.ansiS, modifierFlags: .command, characters: "b", charactersIgnoringModifiers: "b") let s1Computed = getComputed(s1) let s2Computed = getComputed(s2) diff --git a/Unit Tests/SRShortcutTests.swift b/Unit Tests/SRShortcutTests.swift index 4b7d1bb4..0c17f29a 100644 --- a/Unit Tests/SRShortcutTests.swift +++ b/Unit Tests/SRShortcutTests.swift @@ -20,7 +20,7 @@ class SRShortcutTests: XCTestCase { func testInitialization() { let s = Shortcut.default - XCTAssertEqual(s.keyCode, 0) + XCTAssertEqual(s.keyCode, KeyCode.ansiA) XCTAssertEqual(s.modifierFlags, [.option, .command]) XCTAssertEqual(s.characters, "å") XCTAssertEqual(s.charactersIgnoringModifiers, "a") @@ -38,7 +38,7 @@ class SRShortcutTests: XCTestCase { isARepeat: false, keyCode: 0) let s = ShortcutRecorder.Shortcut(event: e!)! - XCTAssertEqual(s.keyCode, 0) + XCTAssertEqual(s.keyCode, KeyCode.ansiA) XCTAssertEqual(s.modifierFlags, .option) XCTAssertEqual(s.characters, "å") XCTAssertEqual(s.charactersIgnoringModifiers, "a") @@ -62,27 +62,27 @@ class SRShortcutTests: XCTestCase { func testDictionaryInitialization() { let s1 = Shortcut(dictionary: [ShortcutKey.keyCode: 0])! - XCTAssertEqual(s1.keyCode, 0) + XCTAssertEqual(s1.keyCode, KeyCode.ansiA) XCTAssertEqual(s1.modifierFlags, []) XCTAssertEqual(s1.characters, "a") XCTAssertEqual(s1.charactersIgnoringModifiers, "a") let s2 = Shortcut(dictionary: [ShortcutKey.keyCode: 0, ShortcutKey.modifierFlags: NSEvent.ModifierFlags.option.rawValue])! - XCTAssertEqual(s2.keyCode, 0) + XCTAssertEqual(s2.keyCode, KeyCode.ansiA) XCTAssertEqual(s2.modifierFlags, NSEvent.ModifierFlags.option) XCTAssertEqual(s2.characters, "å") XCTAssertEqual(s2.charactersIgnoringModifiers, "a") let s3 = Shortcut(dictionary: [ShortcutKey.keyCode: 0, ShortcutKey.modifierFlags: NSEvent.ModifierFlags.option.rawValue, ShortcutKey.characters: NSNull(), ShortcutKey.charactersIgnoringModifiers: NSNull()])! - XCTAssertEqual(s3.keyCode, 0) + XCTAssertEqual(s3.keyCode, KeyCode.ansiA) XCTAssertEqual(s3.modifierFlags, NSEvent.ModifierFlags.option) XCTAssertEqual(s3.characters, "å") XCTAssertEqual(s3.charactersIgnoringModifiers, "a") let s4 = Shortcut(dictionary: [ShortcutKey.keyCode: 0, ShortcutKey.modifierFlags: NSEvent.ModifierFlags.option.rawValue, ShortcutKey.characters: "å", ShortcutKey.charactersIgnoringModifiers: "a"])! - XCTAssertEqual(s4.keyCode, 0) + XCTAssertEqual(s4.keyCode, KeyCode.ansiA) XCTAssertEqual(s4.modifierFlags, NSEvent.ModifierFlags.option) XCTAssertEqual(s4.characters, "å") XCTAssertEqual(s4.charactersIgnoringModifiers, "a") @@ -115,7 +115,7 @@ class SRShortcutTests: XCTestCase { XCTAssertEqual(s, s) XCTAssertEqual(s, Shortcut.default) - XCTAssertNotEqual(s, Shortcut(code: 0, modifierFlags: .command, characters: nil, charactersIgnoringModifiers: nil)); + XCTAssertNotEqual(s, Shortcut(code: KeyCode.ansiA, modifierFlags: .command, characters: nil, charactersIgnoringModifiers: nil)); XCTAssertTrue(s.isEqual(dictionary: [ShortcutKey.keyCode: 0, ShortcutKey.modifierFlags: modifierFlags.rawValue, ShortcutKey.characters: "å", @@ -446,24 +446,50 @@ class SRShortcutTests: XCTestCase { AssertNotEqual(opt_shift_a, shift_ƒ_ke, us_transformer) AssertEqual(opt_shift_a, shift_ƒ_ke, ru_transformer) - let ctrl_tab = Shortcut(code: UInt16(kVK_Tab), modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) + let ctrl_tab = Shortcut(code: KeyCode.tab, modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) XCTAssertTrue(ctrl_tab.isEqual(keyEquivalent: "\u{0009}", modifierFlags: [.control])) XCTAssertTrue(ctrl_tab.isEqual(keyEquivalent: "\u{0019}", modifierFlags: [.control])) - let ctrl_del = Shortcut(code: UInt16(kVK_Delete), modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) + let ctrl_del = Shortcut(code: KeyCode.delete, modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) XCTAssertTrue(ctrl_del.isEqual(keyEquivalent: "\u{0008}", modifierFlags: [.control])); XCTAssertFalse(ctrl_del.isEqual(keyEquivalent: "\u{007f}", modifierFlags: [.control])); - let ctrl_fdel = Shortcut(code: UInt16(kVK_ForwardDelete), modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) + let ctrl_fdel = Shortcut(code: KeyCode.forwardDelete, modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) XCTAssertFalse(ctrl_fdel.isEqual(keyEquivalent: "\u{0008}", modifierFlags: [.control])); XCTAssertTrue(ctrl_fdel.isEqual(keyEquivalent: "\u{007f}", modifierFlags: [.control])); } func testInitializationWithKeyEquivalent() { - let shift_cmd_a = Shortcut(code: UInt16(kVK_ANSI_A), modifierFlags: [.shift, .command], characters: nil, charactersIgnoringModifiers: nil) + let shift_cmd_a = Shortcut(code: KeyCode.ansiA, modifierFlags: [.shift, .command], characters: nil, charactersIgnoringModifiers: nil) XCTAssertEqual(Shortcut(keyEquivalent: "⇧⌘A"), shift_cmd_a) - let ctrl_esc = Shortcut(code: UInt16(kVK_Escape), modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) + let ctrl_esc = Shortcut(code: KeyCode.escape, modifierFlags: [.control], characters: nil, charactersIgnoringModifiers: nil) XCTAssertEqual(Shortcut(keyEquivalent: "⌃Escape"), ctrl_esc) } + + func testInitializationWithFlagsChangedEvent() { + let shift_cmd_down_event = NSEvent.keyEvent(with: .flagsChanged, + location: NSPoint(), + modifierFlags: [.shift, .command], + timestamp: 0, + windowNumber: 0, + context: nil, + characters: "", + charactersIgnoringModifiers: "", + isARepeat: false, + keyCode: UInt16(kVK_Command))! + let shift_cmd_up_event = NSEvent.keyEvent(with: .flagsChanged, + location: NSPoint(), + modifierFlags: [.shift], + timestamp: 0, + windowNumber: 0, + context: nil, + characters: "", + charactersIgnoringModifiers: "", + isARepeat: false, + keyCode: UInt16(kVK_Command))! + let shift_cmd = Shortcut(keyEquivalent: "⇧⌘") + XCTAssertEqual(shift_cmd, Shortcut(event: shift_cmd_down_event)) + XCTAssertEqual(shift_cmd, Shortcut(event: shift_cmd_up_event)) + } } diff --git a/Unit Tests/Utility.swift b/Unit Tests/Utility.swift index e03ec746..09c402ee 100644 --- a/Unit Tests/Utility.swift +++ b/Unit Tests/Utility.swift @@ -12,7 +12,7 @@ import ShortcutRecorder extension Shortcut { class var `default`: Shortcut { - return self.init(code: 0, + return self.init(code: KeyCode.ansiA, modifierFlags: [.option, .command], characters: "å", charactersIgnoringModifiers: "a")