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:
Louis Pontoise authored and lwouis committed 2020-08-18 23:35:13 +09:00
1 parent 2a45dec92a
commit d3253ba4e1
9 files changed
+213 -74

No files matched your search

+1 -1
View File
@@ -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>
+3
View File
@@ -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
+16
View File
@@ -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)`
+7 -1
View File
@@ -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
}
}
-2
View File
@@ -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)
}
}
+150 -53
View File
@@ -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
}
+5
View File
@@ -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 -1
View File
@@ -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)
}
}
}
}