Skip to content

Commit efde820

Browse files
authored
Merge pull request #122 from Kentzo/develop
Shortcut Recorder 3.2
2 parents c0b3a41 + 631b609 commit efde820

35 files changed

+3134
-2223
lines changed

CHANGES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
3.2 (2020-04-xx)
2+
---
3+
4+
Improvements:
5+
6+
- Added support for modifier-only shortcuts
7+
- The `*ShortcutMonitor` family of classes considers the `isEnabled` property of its actions before installing any handlers
8+
- The `SRAXGlobalShortcutMonitor` uses Quartz Services to install an event tap via the `CGEvent*` family of functions.
9+
Unlike `SRGlobalShortcutMonitor`, it can alter handled events but requires the user to grant the Accessibility permission
10+
11+
Fixes:
12+
13+
- The control now shifts the label off the center to avoid clipping if there is enough space
14+
- Better invalidation for re-draws
15+
- Handle and warn when AppKit throws exception because NSEvent's `characters*` properties are accessed from a non-main thread
16+
117
3.1 (2019-10-19)
218
---
319

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*:
2+
- Important:
3+
Playground uses Live View.
4+
*/
5+
import AppKit
6+
import PlaygroundSupport
7+
import ShortcutRecorder
8+
9+
PlaygroundPage.current.needsIndefiniteExecution = true
10+
let mainView = NSView(frame: NSRect(x: 0, y: 0, width: 150, height: 50))
11+
PlaygroundPage.current.liveView = mainView
12+
13+
let control = RecorderControl()
14+
mainView.addSubview(control)
15+
NSLayoutConstraint.activate([
16+
control.centerXAnchor.constraint(equalTo: mainView.centerXAnchor),
17+
control.centerYAnchor.constraint(equalTo: mainView.centerYAnchor)
18+
])
19+
/*:
20+
### Configuring Modifier Flags Requirements
21+
`RecorderControl` allows you to forbid some modifier flags while require other.
22+
23+
There are 3 properties that govern this behavior:
24+
- `allowedModifierFlags` controls what flags *can* be set
25+
- `requiredModifierFlags` controls what flags *must* be set
26+
- `allowsEmptyModifierFlags` controls whether no modifier flags are allowed
27+
28+
- Important:
29+
The control will validate the settings raising an exception for conflicts like marking the flag both disallowed and required.
30+
*/
31+
//control.set(allowedModifierFlags: [.command, .shift, .control], // ⌥ is not allowed
32+
// requiredModifierFlags: [.command, .shift], // ⌘ and ⇧ are required
33+
// allowsEmptyModifierFlags: false) // at least one modifier flag must be set
34+
/*:
35+
### Modifier flags only shortcut
36+
`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
37+
modifier flags.
38+
*/
39+
//control.allowsModifierFlagsOnlyShortcut = true
40+
/*:
41+
### Cancelling recording with Esc
42+
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.
43+
*/
44+
//control.allowsEscapeToCancelRecording = false
45+
/*:
46+
### Clearning the recorded value with Delete
47+
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.
48+
*/
49+
//control.allowsDeleteToClearShortcutAndEndRecording = false
50+
51+
/*:
52+
### Communicating change to the controller
53+
*/
54+
class Controller: NSObject {
55+
@objc var objectValue: Shortcut?
56+
}
57+
/*:
58+
Change can be communicated via Target-Action
59+
*/
60+
//extension Controller {
61+
// @objc func action(sender: RecorderControl) {
62+
// objectValue = sender.objectValue
63+
// print("action: \(sender.stringValue)")
64+
// }
65+
//}
66+
//let target = Controller()
67+
//control.target = target
68+
//control.action = #selector(target.action(sender:))
69+
/*:
70+
As well as via Cocoa Bindings and NSEditorRegistration
71+
*/
72+
//extension Controller: NSEditorRegistration {
73+
// func objectDidBeginEditing(_ editor: NSEditor) {
74+
// print("editor: did begin editing")
75+
// }
76+
//
77+
// func objectDidEndEditing(_ editor: NSEditor) {
78+
// print("editor: did end editing with \((editor as! RecorderControl).stringValue)")
79+
// }
80+
//}
81+
//let controller = Controller()
82+
//control.bind(.value, to: controller, withKeyPath: "objectValue", options: nil)
83+
/*:
84+
And via a delegate
85+
*/
86+
//extension Controller: RecorderControlDelegate {
87+
// func recorderControlShouldBeginRecording(_ aControl: RecorderControl) -> Bool {
88+
// print("delegate: should begin editing")
89+
// return true
90+
// }
91+
//
92+
// func recorderControlDidBeginRecording(_ aControl: RecorderControl) {
93+
// print("delegate: did begin editing")
94+
// }
95+
//
96+
// func recorderControl(_ aControl: RecorderControl, shouldUnconditionallyAllowModifierFlags aFlags: Bool, forKeyCode aKeyCode: KeyCode) -> Bool {
97+
// print("delegate: should unconditionally allow modifier flags")
98+
// return true
99+
// }
100+
//
101+
// func recorderControl(_ aControl: RecorderControl, canRecord aShortcut: Shortcut) -> Bool {
102+
// print("delegate: can record shortcut")
103+
// return true
104+
// }
105+
//
106+
// func recorderControlDidEndRecording(_ aControl: RecorderControl) {
107+
// objectValue = aControl.objectValue
108+
// print("delegate: did end editing with \(aControl.stringValue)")
109+
// }
110+
//}
111+
//let controller = Controller()
112+
//control.delegate = controller
113+
114+
/*:
115+
### Shortcut
116+
The result of recording is an instance of `Shortcut`, a model class that represents recorded modifier flags and a key code.
117+
*/
118+
//let shortcut = Shortcut(keyEquivalent: "⌥⇧⌘A")!
119+
//assert(shortcut.keyCode == .ansiA)
120+
//assert(shortcut.modifierFlags == [.option, .shift, .command])
121+
/*:
122+
The `characters` and `charactersIgnoringModifiers` are similar to those of `NSEvent`, and return string-representation of the key code and modifier flags, if available.
123+
*/
124+
//print("Shortcut Characters: \(shortcut.characters!)")
125+
//print("Shortcut Characters Ignoring Modifiers: \(shortcut.charactersIgnoringModifiers!)")
126+
/*:
127+
Since some of the underlying API is using Carbon, there are properties to get Carbon-representation of the `keyCode` and `modifierFlags`:
128+
*/
129+
//print("Carbon Key Code: \(shortcut.carbonKeyCode)")
130+
//print("Carbon Modifier Flags: \(shortcut.carbonModifierFlags)")
131+
132+
/*:
133+
### Shortcut Validation
134+
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.
135+
*/
136+
//let validator = ShortcutValidator()
137+
//do {
138+
// try validator.validate(shortcut: Shortcut(keyEquivalent: "⌘Q"))
139+
//}
140+
//catch let error as NSError {
141+
// print(error.localizedDescription)
142+
//}
143+
/*:
144+
For convenience the validator implements the `RecorderControlDelegate/recorderControl(_:,canRecord:)`.
145+
*/
146+
//control.delegate = validator
147+
148+
/*:
149+
### Cocoa Transformers
150+
Sometimes it's useful to display a shortcut outside of the recorder control. E.g. in a tooltip or in a label.
151+
152+
`ShortcutFormatter`, a subclass of `NSFormatter`, can be used in standard Cocoa controls.
153+
*/
154+
//let textField = NSTextField(labelWithString: "")
155+
//textField.formatter = ShortcutFormatter()
156+
//textField.objectValue = Shortcut(keyEquivalent: "⇧⌘A")!
157+
//print(textField.stringValue)
158+
/*:
159+
A number of transformers, subclasses of `NSValueTransformer`, are available for custom alterations.
160+
161+
#### KeyCodeTransformer
162+
`KeyCodeTransformer` is a class-cluster that transforms numeric key codes into `String`.
163+
164+
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 `⇤`.
165+
166+
- Note:
167+
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`.
168+
169+
There are 4 subclasses in the cluster:
170+
171+
- `SymbolicKeyCodeTransformer`: translates a key code into an input character using current input source
172+
- `LiteralKeyCodeTransformer`: translates a key code into a drawable glyph using current input source
173+
- `ASCIISymbolicKeyCodeTransformer`: translates a key code into an input character using ASCII-capable input source
174+
- `ASCIILiteralKeyCodeTransformer`: translates a key code into a drawable glyph using ASCII-capable input source
175+
this is the only class in the cluster that *allows reverse transformation*
176+
*/
177+
//print("Symbolic Key Code: \"\(ASCIISymbolicKeyCodeTransformer.shared.transformedValue(KeyCode.tab) as! String)\"")
178+
//print("Literal Key Code: \"\(ASCIILiteralKeyCodeTransformer.shared.transformedValue(KeyCode.tab) as! String)\"")
179+
/*:
180+
#### ModifierFlagsTransformer
181+
`ModifierFlagsTransformer` is a class-cluster that transforms of modifier flags into a `String`.
182+
183+
There are 2 subclasses in the cluster:
184+
- `SymbolicModifierFlagsTransformer` translates modifier flags into readable words, e.g. Shift-Command
185+
- `LiteralModifierFlagsTransformer` translates modifier flags into drawable glyphs, e.g. ⇧⌘
186+
*/
187+
//let flags: NSEvent.ModifierFlags = [.shift, .command]
188+
//print("Symbolic Modifier Flags: \"\(SymbolicModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"")
189+
//print("Literal Modifier Flags: \"\(LiteralModifierFlagsTransformer.shared.transformedValue(flags.rawValue) as! String)\"")
190+
/*:
191+
#### Transformers
192+
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.
193+
*/
194+
//print("Key Equivalent: \"\(KeyEquivalentTransformer.shared.transformedValue(shortcut) as! String)\"")
195+
//print("Key Equivalent Modifier Mask: \"\(KeyEquivalentModifierMaskTransformer.shared.transformedValue(shortcut) as! UInt)\"")
196+
197+
/*:
198+
### Shortcut Monitoring
199+
`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).
200+
201+
`GlobalShortcutMonitor` tries to register a system-wide hot key that can be triggered from any app.
202+
*/
203+
//let shortcut = Shortcut(keyEquivalent: "⌘A")
204+
//let action = ShortcutAction(shortcut: Shortcut(keyEquivalent: "⌥⇧⌘A")!) { action in
205+
// print("Handle global shortcut")
206+
// return true
207+
//}
208+
//let globalMonitor = GlobalShortcutMonitor()
209+
//globalMonitor.addAction(action, forKeyEvent: .down)
210+
/*:
211+
`LocalShortcutMonitor` requires you to call the `handle(_:, withTarget:)` method with a key event and an optional target (for selector).
212+
213+
`LocalShortcutMonitor` is designed to be used from:
214+
- `NSResponder/keyDown(with:)`
215+
- `NSResponder/keyUp(with:)`
216+
- `NSResponder/performKeyEquivalent(with:)`
217+
- `NSResponder/flagsChanged(with:)`
218+
- `NSEvent/addLocalMonitorForEvents(matching:handler:)`
219+
- `NSEvent/addGlobalMonitorForEvents(matching:handler:)`
220+
*/
221+
//let shortcut = Shortcut(keyEquivalent: "⌥⇧⌘A")!
222+
//let action = ShortcutAction(shortcut: shortcut) { action in
223+
// print("Handle local shortcut")
224+
// return true
225+
//}
226+
//let event = NSEvent.keyEvent(with: .keyDown,
227+
// location: NSPoint(x: 0, y: 0),
228+
// modifierFlags: shortcut.modifierFlags,
229+
// timestamp: 0,
230+
// windowNumber: 0,
231+
// context: nil,
232+
// characters: "A",
233+
// charactersIgnoringModifiers: "a",
234+
// isARepeat: false,
235+
// keyCode: UInt16(shortcut.keyCode.rawValue))!
236+
//let localMonitor = LocalShortcutMonitor()
237+
//localMonitor.addAction(action, forKeyEvent: .down)
238+
//localMonitor.handle(event, withTarget: nil)
239+
/*:
240+
It can be used to recognize and handle `keyCode`-less shortcuts
241+
*/
242+
//let event = NSEvent.keyEvent(with: .flagsChanged,
243+
// location: NSPoint(x: 0, y: 0),
244+
// modifierFlags: [.shift, .command],
245+
// timestamp: 0,
246+
// windowNumber: 0,
247+
// context: nil,
248+
// characters: "A",
249+
// charactersIgnoringModifiers: "a",
250+
// isARepeat: false,
251+
// keyCode: UInt16(kVK_Command))!
252+
//let shortcut = Shortcut(event: event)!
253+
//let action = ShortcutAction(shortcut: shortcut) { action in
254+
// print("Handle local shortcut")
255+
// return true
256+
//}
257+
//let localMonitor = LocalShortcutMonitor()
258+
//localMonitor.addAction(action, forKeyEvent: .down)
259+
//localMonitor.handle(event, withTarget: nil)

0 commit comments

Comments
 (0)