From 8912c70bcd99462dee942e0fe49056fc19d85511 Mon Sep 17 00:00:00 2001 From: Louis Pontoise Date: Sat, 10 Oct 2020 03:03:50 +0900 Subject: [PATCH] fix: ui would sometimes stay open (closes #588) --- src/logic/ATShortcut.swift | 5 --- src/logic/Spaces.swift | 2 - src/logic/events/KeyboardEvents.swift | 49 ++++++++++++++++-------- src/ui/App.swift | 2 +- src/ui/main-window/ThumbnailsPanel.swift | 2 + 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/logic/ATShortcut.swift b/src/logic/ATShortcut.swift index d02f2cac..b24cfd77 100644 --- a/src/logic/ATShortcut.swift +++ b/src/logic/ATShortcut.swift @@ -25,11 +25,6 @@ class ATShortcut { state = state == .down ? .up : .down if state == .up { KeyRepeatTimer.timer?.invalidate() - } else { - // macOS bug: the app doesn't receive either local or global modifierChanged events during Space transition - // we force holdShortcut to be .down if nextWindowShortcut trigger during a transition, since we know it has to be down at that point - let suffix = shortcutId == "nextWindowShortcut" ? "" : "2" - ControlsTab.shortcuts["holdShortcut" + suffix]!.state = .down } if (triggerPhase == .down && shortcutState == .down) || (triggerPhase == .up && shortcutState == .up) { return true diff --git a/src/logic/Spaces.swift b/src/logic/Spaces.swift index 6110bd44..1f0ab83b 100644 --- a/src/logic/Spaces.swift +++ b/src/logic/Spaces.swift @@ -11,8 +11,6 @@ class Spaces { debugPrint("OS event", "activeSpaceDidChangeNotification") idsAndIndexes = allIdsAndIndexes() updateCurrentSpace() - guard App.app.appIsBeingUsed else { return } - App.app.reopenUi() }) } diff --git a/src/logic/events/KeyboardEvents.swift b/src/logic/events/KeyboardEvents.swift index b888d771..cbdbe30c 100644 --- a/src/logic/events/KeyboardEvents.swift +++ b/src/logic/events/KeyboardEvents.swift @@ -14,7 +14,6 @@ class KeyboardEvents { "holdShortcut2": 3, ] static var eventHotKeyRefs = [String: EventHotKeyRef?]() - static var hotModifierEventHandler: EventHandlerRef? static var hotKeyPressedEventHandler: EventHandlerRef? static var hotKeyReleasedEventHandler: EventHandlerRef? static var localMonitor: Any! @@ -70,22 +69,33 @@ class KeyboardEvents { } } - static func addLocalEventHandler() { - localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp, .flagsChanged]) { event in + static func addEventHandlers() { + addLocalMonitorForKeyDownAndKeyUp() + addCgEventTapForModifierFlags() + } + + private static func addLocalMonitorForKeyDownAndKeyUp() { + localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { event in return handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false) ? nil : event } } + private static func addCgEventTapForModifierFlags() { + let eventMask = [CGEventType.flagsChanged].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) }) + // CGEvent.tapCreate returns null if ensureAccessibilityCheckboxIsChecked() didn't pass + // CGEvent.tapCreate is unaffected by SecureInput for .flagsChanged + eventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: eventMask, + callback: cgEventFlagsChangedHandler, + userInfo: nil) + let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0) + CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) + } + private static func addGlobalHandlerIfNeeded(_ 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 - var modifiers = UInt32(0) - GetEventParameter(event, EventParamName(kEventParamKeyModifiers), EventParamType(typeUInt32), nil, MemoryLayout.size, nil, &modifiers) - handleEvent(nil, nil, nil, modifiers, false) - return noErr - }, eventTypes.count, &eventTypes, nil, &hotModifierEventHandler) - } if shortcut.keyCode != .none && hotKeyPressedEventHandler == nil { var eventTypes = [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed))] InstallEventHandler(shortcutEventTarget, { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in @@ -114,9 +124,6 @@ class KeyboardEvents { hotKeyPressedEventHandler = nil RemoveEventHandler(hotKeyReleasedEventHandler_) hotKeyReleasedEventHandler = nil - } else if let hotModifierEventHandler_ = hotModifierEventHandler, (globalShortcuts.allSatisfy { $0.shortcut.keyCode != .none }) { - RemoveEventHandler(hotModifierEventHandler_) - hotModifierEventHandler = nil } } } @@ -132,3 +139,15 @@ fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutStat } return someShortcutTriggered } + +fileprivate func cgEventFlagsChangedHandler(proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged? { + if type == .flagsChanged { + let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))) + if handleEvent(nil, nil, nil, modifiers, false) { + return nil // focused app won't receive the event + } + } else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) { + CGEvent.tapEnable(tap: eventTap!, enable: true) + } + return Unmanaged.passUnretained(cgEvent) // focused app will receive the event +} diff --git a/src/ui/App.swift b/src/ui/App.swift index 9b56801c..60784234 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -60,7 +60,7 @@ class App: AppCenterApplication, NSApplicationDelegate { Applications.initialDiscovery() self.preferencesWindow = PreferencesWindow() self.feedbackWindow = FeedbackWindow() - KeyboardEvents.addLocalEventHandler() + KeyboardEvents.addEventHandlers() MouseEvents.observe() // TODO: undeterministic; events in the queue may still be processing; good enough for now DispatchQueue.main.async { () -> () in Windows.sortByLevel() } diff --git a/src/ui/main-window/ThumbnailsPanel.swift b/src/ui/main-window/ThumbnailsPanel.swift index 9ce91782..97a151b0 100644 --- a/src/ui/main-window/ThumbnailsPanel.swift +++ b/src/ui/main-window/ThumbnailsPanel.swift @@ -15,6 +15,8 @@ class ThumbnailsPanel: NSPanel { contentView!.addSubview(thumbnailsView) preservesContentDuringLiveResize = false disableSnapshotRestoration() + // triggering AltTab before or during Space transition animation brings the window on the Space post-transition + collectionBehavior = .canJoinAllSpaces // 2nd highest level possible; this allows the app to go on top of context menus // highest level is .screenSaver but makes drag and drop on top the main window impossible level = .popUpMenu