feat: complete rewrite of the keyboard support (closes #157)
BREAKING CHANGE: the previous keyboard support implementation was not working if any app on the system activated Secure Input and didn't turn it off. This is a major hurdle for most global shortcut apps. This update introduces a new implementation which is unaffected by Secure Input. AltTab shortcuts should now work reliably
This commit is contained in:
1 parent
2a45dec92a
commit
d3253ba4e1
9 files changed
+213
-74
No files matched your search
+1
-1
@@ -33,7 +33,7 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>AppCenterApplication</string>
|
||||
<key>NSSupportsSuddenTermination</key>
|
||||
<string>true</string>
|
||||
<string>false</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<string>true</string>
|
||||
<key>SUScheduledCheckInterval</key>
|
||||
|
||||
@@ -130,10 +130,13 @@ In an attempt to not have too many regressions, this documents will list OS inte
|
||||
### Shortcuts
|
||||
|
||||
* The hold "key" can be multiple modifiers (e.g. `⌥⇧`)
|
||||
* Shortcuts should have priority over system shortcuts such as `cmd+tab`, so the user can replace these
|
||||
* The "select next window" shortcut can be modifiers, modifiers+key, or just key; it can also contain the same modifiers as the hold "key"
|
||||
* All shortcuts, except the hold key, can be disabled by the user
|
||||
* Shortcuts can include the `escape` and `delete` key; these should not stop recording shortcuts
|
||||
* [Secure Input](https://github.com/lwouis/alt-tab-macos/issues/157#issuecomment-659170293) can prevent AltTab from listening to the keyboard
|
||||
* Some shortcuts should only work when AltTab is open
|
||||
* Shortcuts should work with capslock active or inactive
|
||||
* The shortcut sets 1 and 2 should not interact with each other (e.g. opening AltTab with one, then using the other to navigate)
|
||||
* Shortcuts can focus the window on release, or be pressing a key or using the mouse
|
||||
* Keyboards from other countries have different layout which impact shortcuts
|
||||
|
||||
@@ -124,12 +124,28 @@ func SLSRequestScreenCaptureAccess() -> UInt8
|
||||
// for some reason, this attribute is missing from AXAttributeConstants
|
||||
let kAXFullscreenAttribute = "AXFullScreen"
|
||||
|
||||
// enables/disables a symbolic hotkeys. These are system shortcuts such as command+tab or Spotlight
|
||||
// it is possible to find all the existing hotkey IDs by using CGSGetSymbolicHotKeyValue on the first few hundred numbers
|
||||
// note: the effect of enabling/disabling persists after the app is quit
|
||||
@_silgen_name("CGSSetSymbolicHotKeyEnabled")
|
||||
func CGSSetSymbolicHotKeyEnabled(_ hotKey: Int, _ isEnabled: Bool) -> CGError
|
||||
|
||||
func setNativeCommandTabEnabled(_ isEnabled: Bool) {
|
||||
CGSSetSymbolicHotKeyEnabled(1, isEnabled) // command+tab
|
||||
CGSSetSymbolicHotKeyEnabled(2, isEnabled) // command+shift+tab
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// below are some notes on some private APIs I experimented with
|
||||
// ------------------------------------------------------------
|
||||
|
||||
//@_silgen_name("CGSGetSymbolicHotKeyValue")
|
||||
//func CGSGetSymbolicHotKeyValue(_ hotKey: Int, _ options: inout UInt32, _ keyCode: inout UInt32, _ modifiers: inout UInt32) -> CGError
|
||||
//
|
||||
//@_silgen_name("CGSIsSymbolicHotKeyEnabled")
|
||||
//func CGSIsSymbolicHotKeyEnabled(_ hotKey: Int) -> Bool
|
||||
//
|
||||
//// listen to some window server events
|
||||
//// most interesting events for Mission Control seem to be [1204, 1401, 1508]. It seems that these are all the valid events, as the response is .success with these: //// [100, 101, 102, 103, 104, 106, 107, 108, 111, 115, 116, 117, 118, 119, 120, 121, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321, 1322, 1323, 1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1344, 1345, 1346Line truncated
|
||||
//// example calls: `SLSRegisterNotifyProc(callbackFn2, $0, nil)` `SLSRegisterConnectionNotifyProc(cgsMainConnectionId, callbackFn1, $0, nil)`
|
||||
|
||||
@@ -74,7 +74,7 @@ class Applications {
|
||||
}
|
||||
|
||||
private static func isActualApplication(_ app: NSRunningApplication) -> Bool {
|
||||
return app.activationPolicy != .prohibited || isNotXpc(app)
|
||||
return (app.activationPolicy != .prohibited || isNotXpc(app)) && notAltTab(app)
|
||||
}
|
||||
|
||||
private static func isNotXpc(_ app: NSRunningApplication) -> Bool {
|
||||
@@ -83,4 +83,10 @@ class Applications {
|
||||
.flatMap { $0.infoDictionary }
|
||||
.flatMap { $0["CFBundlePackageType"] as? String } != "XPC!"
|
||||
}
|
||||
|
||||
// managing AltTab windows within AltTab create all sorts of side effects
|
||||
// e.g. hiding the thumbnails panel gives focus to the preferences panel if open, thus changing its order in the list
|
||||
private static func notAltTab(_ app: NSRunningApplication) -> Bool {
|
||||
return app.processIdentifier != ProcessInfo.processInfo.processIdentifier
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ class BackgroundWork {
|
||||
static var axCallsQueue: DispatchQueue!
|
||||
static var crashReportsQueue: DispatchQueue!
|
||||
static var accessibilityEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var keyboardEventsThread: BackgroundThreadWithRunLoop!
|
||||
static var mouseEventsThread: BackgroundThreadWithRunLoop!
|
||||
|
||||
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
|
||||
@@ -20,7 +19,6 @@ class BackgroundWork {
|
||||
axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
|
||||
crashReportsQueue = DispatchQueue.globalConcurrent("crashReportsQueue", .utility)
|
||||
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
|
||||
keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread", .userInteractive)
|
||||
mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread", .userInteractive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,74 +5,171 @@ import ShortcutRecorder
|
||||
fileprivate var eventTap: CFMachPort?
|
||||
|
||||
class KeyboardEvents {
|
||||
static let signature = "altt".utf16.reduce(0) { ($0 << 8) + OSType($1) }
|
||||
// GetEventMonitorTarget/GetApplicationEventTarget also work, but require Accessibility Permission
|
||||
static let shortcutEventTarget = GetEventDispatcherTarget()
|
||||
static let globalShortcuts = [
|
||||
"nextWindowShortcut": 0,
|
||||
"nextWindowShortcut2": 1,
|
||||
"holdShortcut": 2,
|
||||
"holdShortcut2": 3,
|
||||
]
|
||||
static var holdShortcutWasDown = [2: false, 3: false]
|
||||
static var eventHotKeyRefs = [EventHotKeyRef?](repeating: nil, count: globalShortcuts.count)
|
||||
static var hotModifierEventHandler: EventHandlerRef?
|
||||
static var hotKeyEventHandler: EventHandlerRef?
|
||||
|
||||
static func observe() {
|
||||
observe_()
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { event in
|
||||
// global .flagsChanged events are not send to the app when it's active, thus modifiers-only shortcuts should be matched in global and local handlers
|
||||
if handleHotModifier(cocoaToCarbonFlags(event.modifierFlags)) || handleLocalEvents(event) {
|
||||
return nil // don't propagate event
|
||||
}
|
||||
return event // propagate event
|
||||
}
|
||||
}
|
||||
|
||||
static func addGlobalShortcut(_ shortcut: Shortcut, _ id: Int) {
|
||||
addHandlerIfNeeded(shortcut)
|
||||
registerHotKeyIfNeeded(id, shortcut)
|
||||
toggleNativeCommandTabIfNeeded(shortcut, false)
|
||||
}
|
||||
|
||||
static func removeGlobalShortcut(_ id: Int, _ shortcut: Shortcut) {
|
||||
UnregisterEventHotKey(eventHotKeyRefs[id])
|
||||
eventHotKeyRefs[id] = nil
|
||||
removeHandlerIfNeeded()
|
||||
toggleNativeCommandTabIfNeeded(shortcut, true)
|
||||
}
|
||||
|
||||
private static func registerHotKeyIfNeeded(_ id: Int, _ shortcut: Shortcut) {
|
||||
if shortcut.keyCode != .none {
|
||||
let hotkeyId = EventHotKeyID(signature: signature, id: UInt32(id))
|
||||
let key = UInt32(shortcut.carbonKeyCode)
|
||||
let mods = UInt32(shortcut.carbonModifierFlags)
|
||||
let options = UInt32(kEventHotKeyNoOptions)
|
||||
var shortcutsReference: EventHotKeyRef?
|
||||
RegisterEventHotKey(key, mods, hotkeyId, shortcutEventTarget, options, &shortcutsReference)
|
||||
eventHotKeyRefs[id] = shortcutsReference
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleNativeCommandTabIfNeeded(_ shortcut: Shortcut, _ enabled: Bool) {
|
||||
if (shortcut.carbonModifierFlags == cmdKey || shortcut.carbonModifierFlags == (cmdKey | shiftKey)) && shortcut.carbonKeyCode == kVK_Tab {
|
||||
setNativeCommandTabEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private static func addHandlerIfNeeded(_ shortcut: Shortcut) {
|
||||
if shortcut.keyCode == .none && hotModifierEventHandler == nil {
|
||||
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventRawKeyModifiersChanged))]
|
||||
InstallEventHandler(GetEventMonitorTarget(), { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
|
||||
if !App.app.shortcutsShouldBeDisabled {
|
||||
var modifiers = UInt32(0)
|
||||
GetEventParameter(event, EventParamName(kEventParamKeyModifiers), EventParamType(typeUInt32), nil, MemoryLayout<UInt32>.size, nil, &modifiers)
|
||||
_ = handleHotModifier(modifiers)
|
||||
}
|
||||
return noErr
|
||||
}, eventTypes.count, &eventTypes, nil, &hotModifierEventHandler)
|
||||
}
|
||||
if shortcut.keyCode != .none && hotKeyEventHandler == nil {
|
||||
var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))]
|
||||
InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
|
||||
if !App.app.shortcutsShouldBeDisabled {
|
||||
var id = EventHotKeyID()
|
||||
GetEventParameter(event, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout<EventHotKeyID>.size, nil, &id)
|
||||
handleHotKey(id)
|
||||
}
|
||||
return noErr
|
||||
}, eventTypes.count, &eventTypes, nil, &hotKeyEventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeHandlerIfNeeded() {
|
||||
if let hotKeyEventHandler_ = hotKeyEventHandler, (ControlsTab.globalShortcuts.values.allSatisfy { $0.keyCode == .none }) {
|
||||
RemoveEventHandler(hotKeyEventHandler_)
|
||||
hotKeyEventHandler = nil
|
||||
} else if let hotModifierEventHandler_ = hotModifierEventHandler, (ControlsTab.globalShortcuts.values.allSatisfy { $0.keyCode != .none }) {
|
||||
RemoveEventHandler(hotModifierEventHandler_)
|
||||
hotModifierEventHandler = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func observe_() {
|
||||
let eventMask = [CGEventType.keyDown, CGEventType.keyUp, CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
|
||||
// CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass
|
||||
eventTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .defaultTap,
|
||||
eventsOfInterest: eventMask,
|
||||
callback: keyboardHandler,
|
||||
userInfo: nil)
|
||||
// permission can have been removed before SystemPermissions timer triggers, thus we check and restart if needed
|
||||
if eventTap == nil { App.app.restart() }
|
||||
let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
|
||||
CFRunLoopAddSource(BackgroundWork.keyboardEventsThread.runLoop, runLoopSource, .commonModes)
|
||||
}
|
||||
|
||||
private func keyboardHandler(_: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, _: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
|
||||
if (type == .keyDown || type == .keyUp || type == .flagsChanged) && !App.app.shortcutsShouldBeDisabled {
|
||||
if let event = NSEvent(cgEvent: cgEvent) {
|
||||
let appWasBeingUsed = App.app.appIsBeingUsed
|
||||
if let shortcut = shortcutThatMatches(event, type) {
|
||||
if shortcut.hasPrefix("nextWindowShortcut") {
|
||||
App.app.appIsBeingUsed = true
|
||||
} else if shortcut.hasPrefix("holdShortcut") || shortcut == "cancelShortcut" || shortcut == "focusWindowShortcut" {
|
||||
App.app.appIsBeingUsed = false
|
||||
App.app.isFirstSummon = true
|
||||
fileprivate func handleHotModifier(_ modifiers: UInt32) -> Bool {
|
||||
for (key, value) in ControlsTab.globalShortcuts {
|
||||
if value.keyCode != .none { continue }
|
||||
let shortcutIndex = KeyboardEvents.globalShortcuts[key]!
|
||||
// modifiers down
|
||||
if value.carbonModifierFlags == modifiers {
|
||||
if key.hasPrefix("holdShortcut") {
|
||||
KeyboardEvents.holdShortcutWasDown[shortcutIndex] = true
|
||||
} else {
|
||||
if handleHotAny(key, shortcutIndex) {
|
||||
return true
|
||||
}
|
||||
DispatchQueue.main.async { () -> () in ControlsTab.shortcutsActions[shortcut]!() }
|
||||
}
|
||||
if appWasBeingUsed || App.app.appIsBeingUsed {
|
||||
return nil // focused app won't receive the event
|
||||
}
|
||||
}
|
||||
} else if type == .tapDisabledByUserInput || type == .tapDisabledByTimeout {
|
||||
CGEvent.tapEnable(tap: eventTap!, enable: true)
|
||||
// modifiers up
|
||||
else if key.hasPrefix("holdShortcut") && (modifiers & value.carbonModifierFlags == 0) && KeyboardEvents.holdShortcutWasDown[shortcutIndex]! {
|
||||
KeyboardEvents.holdShortcutWasDown[shortcutIndex] = false
|
||||
if Preferences.shortcutStyle == .focusOnRelease && handleHotAny(key, shortcutIndex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return Unmanaged.passRetained(cgEvent) // focused app will receive the event
|
||||
return false
|
||||
}
|
||||
|
||||
// shortcutMonitor.handle only does exact matching; we match manually to allow flexible matching
|
||||
// see https://github.com/lwouis/alt-tab-macos/issues/230
|
||||
private func shortcutThatMatches(_ event: NSEvent, _ type: CGEventType) -> String? {
|
||||
for shortcutId in ControlsTab.shortcuts.keys {
|
||||
let postfix = App.app.shortcutIndex == 0 ? "" : "2"
|
||||
let shortcut = ControlsTab.shortcuts[shortcutId]!
|
||||
fileprivate func handleHotKey(_ id: EventHotKeyID) {
|
||||
let shortcutIndex = Int(id.id)
|
||||
let key = Array(KeyboardEvents.globalShortcuts).first { $0.value == shortcutIndex }!.key
|
||||
_ = handleHotAny(key, shortcutIndex)
|
||||
}
|
||||
|
||||
fileprivate func handleHotAny_(_ key: String, _ shortcutIndex: Int) -> Bool {
|
||||
if key.hasPrefix("nextWindowShortcut") {
|
||||
if (!App.app.appIsBeingUsed || App.app.shortcutIndex == (shortcutIndex % 2)) {
|
||||
App.app.appIsBeingUsed = true
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if App.app.appIsBeingUsed && App.app.shortcutIndex == (shortcutIndex % 2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func localShortcutThatMatches(_ event: NSEvent) -> String? {
|
||||
for (shortcutId, shortcut) in ControlsTab.localShortcuts {
|
||||
if shortcutId.hasPrefix("holdShortcut") {
|
||||
if event.sr_keyEventType == .up && type == .flagsChanged && shortcut.keyCode == .none && event.modifierFlags.isDisjoint(with: shortcut.modifierFlags) &&
|
||||
let postfix = App.app.shortcutIndex == 0 ? "" : "2"
|
||||
if event.sr_keyEventType == .up && event.type == .flagsChanged && shortcut.keyCode == .none && event.modifierFlags.isDisjoint(with: shortcut.modifierFlags) &&
|
||||
shortcutId == "holdShortcut" + postfix && App.app.appIsBeingUsed && Preferences.shortcutStyle == .focusOnRelease {
|
||||
return shortcutId
|
||||
}
|
||||
} else if event.sr_keyEventType == .down && (shortcut.keyCode == .none || event.keyCode == shortcut.carbonKeyCode) {
|
||||
if shortcutId.hasPrefix("nextWindowShortcut") {
|
||||
if event.modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) == shortcut.modifierFlags &&
|
||||
(!App.app.appIsBeingUsed || shortcutId == "nextWindowShortcut" + postfix) {
|
||||
return shortcutId
|
||||
}
|
||||
} else {
|
||||
if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(shortcut.modifierFlags) &&
|
||||
App.app.appIsBeingUsed {
|
||||
return shortcutId
|
||||
}
|
||||
}
|
||||
} else if event.sr_keyEventType == .down && (shortcut.keyCode == .none || event.keyCode == shortcut.carbonKeyCode) &&
|
||||
event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(shortcut.modifierFlags) &&
|
||||
App.app.appIsBeingUsed {
|
||||
return shortcutId
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func handleHotAny(_ shortcutId: String, _ shortcutIndex: Int) -> Bool {
|
||||
return executeActionIfShould(handleHotAny_(shortcutId, shortcutIndex) ? shortcutId : nil)
|
||||
}
|
||||
|
||||
fileprivate func handleLocalEvents(_ event: NSEvent) -> Bool {
|
||||
return executeActionIfShould(localShortcutThatMatches(event))
|
||||
}
|
||||
|
||||
fileprivate func executeActionIfShould(_ shortcutId: String?) -> Bool {
|
||||
if let shortcutId = shortcutId {
|
||||
DispatchQueue.main.async { () -> () in ControlsTab.shortcutsActions[shortcutId]!() }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -70,6 +70,11 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
// symbolic hotkeys state persist after the app is quit; we restore this shortcut before quitting
|
||||
setNativeCommandTabEnabled(true)
|
||||
}
|
||||
|
||||
// pre-load some windows so they are faster on first display
|
||||
private func preloadWindows() {
|
||||
thumbnailsPanel.orderFront(nil)
|
||||
|
||||
@@ -2,6 +2,7 @@ import Cocoa
|
||||
|
||||
class ThumbnailsPanel: NSPanel {
|
||||
var thumbnailsView = ThumbnailsView()
|
||||
override var canBecomeKey: Bool { true }
|
||||
|
||||
convenience init() {
|
||||
self.init(contentRect: .zero, styleMask: .utilityWindow, backing: .buffered, defer: true)
|
||||
@@ -26,7 +27,7 @@ class ThumbnailsPanel: NSPanel {
|
||||
}
|
||||
|
||||
func show() {
|
||||
orderFrontRegardless()
|
||||
makeKeyAndOrderFront(nil)
|
||||
MouseEvents.toggle(true)
|
||||
thumbnailsView.scrollView.flashScrollers()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ class ControlsTab: NSViewController, PreferencePane {
|
||||
let toolbarItemIcon = NSImage.initTemplateCopy("controls")
|
||||
|
||||
static var nextWindowShortcut: [NSControl]!
|
||||
static var shortcuts = [String: Shortcut]()
|
||||
static var localShortcuts = [String: Shortcut]()
|
||||
static var globalShortcuts = [String: Shortcut]()
|
||||
static var shortcutsActions = [
|
||||
"holdShortcut": { App.app.focusTarget() },
|
||||
"holdShortcut2": { App.app.focusTarget() },
|
||||
@@ -63,7 +64,6 @@ class ControlsTab: NSViewController, PreferencePane {
|
||||
// currently this looks bad if the right column inside the tabView is larger than the right column of the top gridView
|
||||
let leftColumnWidthTabView = tab1View.column(at: 0).width()
|
||||
let leftColumnWidthTopView = grid.column(at: 0).width(0)
|
||||
debugPrint("hey", leftColumnWidthTabView, leftColumnWidthTopView)
|
||||
if leftColumnWidthTabView > leftColumnWidthTopView {
|
||||
orPress.fit(tab1View.column(at: 0).width() + GridView.interPadding + TabView.padding, orPress.fittingSize.height)
|
||||
} else {
|
||||
@@ -98,32 +98,38 @@ class ControlsTab: NSViewController, PreferencePane {
|
||||
return (nextWindowShortcut, tab)
|
||||
}
|
||||
|
||||
private static func addShortcut(_ type: KeyEventType, _ shortcut: Shortcut, _ controlId: String) {
|
||||
removeShortcutIfExists(controlId, type) // remove the previous shortcut
|
||||
shortcuts[controlId] = shortcut
|
||||
private static func addShortcut(_ type: KeyEventType, _ shortcut: Shortcut, _ controlId: String, _ globalId: Int?) {
|
||||
removeShortcutIfExists(controlId, type, globalId) // remove the previous shortcut
|
||||
if let globalId = globalId {
|
||||
globalShortcuts[controlId] = shortcut
|
||||
KeyboardEvents.addGlobalShortcut(shortcut, globalId)
|
||||
} else {
|
||||
localShortcuts[controlId] = shortcut
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func shortcutChangedCallback(_ sender: NSControl) {
|
||||
let controlId = sender.identifier!.rawValue
|
||||
let globalId = KeyboardEvents.globalShortcuts[controlId]
|
||||
if controlId.hasPrefix("holdShortcut") {
|
||||
let i = controlId == "holdShortcut" ? 0 : 1
|
||||
addShortcut(.up, Shortcut(keyEquivalent: Preferences.holdShortcut[i])!, controlId)
|
||||
addShortcut(.up, Shortcut(keyEquivalent: Preferences.holdShortcut[i])!, controlId, globalId)
|
||||
if let s = nextWindowShortcut?[i] {
|
||||
shortcutChangedCallback(s)
|
||||
}
|
||||
} else {
|
||||
let newValue = shortcutStringValue(controlId, sender)
|
||||
let newValue = shortcutStringValue(controlId, sender, globalId)
|
||||
if newValue.isEmpty {
|
||||
removeShortcutIfExists(controlId, .down)
|
||||
removeShortcutIfExists(controlId, .down, globalId)
|
||||
} else {
|
||||
addShortcut(.down, Shortcut(keyEquivalent: newValue)!, controlId)
|
||||
addShortcut(.down, Shortcut(keyEquivalent: newValue)!, controlId, globalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func shortcutStringValue(_ controlId: String, _ sender: NSControl) -> String {
|
||||
static func shortcutStringValue(_ controlId: String, _ sender: NSControl, _ globalId: Int?) -> String {
|
||||
let baseValue = (sender as! RecorderControl).stringValue
|
||||
if controlId == "nextWindowShortcut" || controlId == "nextWindowShortcut2" {
|
||||
if globalId != nil {
|
||||
let holdShortcut = controlId == "nextWindowShortcut" ? Preferences.holdShortcut[0] : Preferences.holdShortcut[1]
|
||||
// remove the holdShortcut character in case they also use it in the other shortcuts
|
||||
let cleanedShortcut = holdShortcut + holdShortcut.reduce(baseValue, { $0.replacingOccurrences(of: String($1), with: "") })
|
||||
@@ -138,15 +144,22 @@ class ControlsTab: NSViewController, PreferencePane {
|
||||
@objc static func arrowKeysEnabledCallback(_ sender: NSControl) {
|
||||
let keys = ["←", "→", "↑", "↓"]
|
||||
if (sender as! NSButton).state == .on {
|
||||
keys.forEach { addShortcut(.down, Shortcut(keyEquivalent: $0)!, $0) }
|
||||
keys.forEach { addShortcut(.down, Shortcut(keyEquivalent: $0)!, $0, nil) }
|
||||
} else {
|
||||
keys.forEach { removeShortcutIfExists($0, .down) }
|
||||
keys.forEach { removeShortcutIfExists($0, .down, nil) }
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeShortcutIfExists(_ controlId: String, _ type: KeyEventType) {
|
||||
if let a = shortcuts[controlId] {
|
||||
shortcuts.removeValue(forKey: controlId)
|
||||
private static func removeShortcutIfExists(_ controlId: String, _ type: KeyEventType, _ globalId: Int?) {
|
||||
if let globalId = globalId {
|
||||
if globalShortcuts[controlId] != nil {
|
||||
KeyboardEvents.removeGlobalShortcut(globalId, globalShortcuts[controlId]!)
|
||||
globalShortcuts.removeValue(forKey: controlId)
|
||||
}
|
||||
} else {
|
||||
if localShortcuts[controlId] != nil {
|
||||
localShortcuts.removeValue(forKey: controlId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user