diff --git a/Loop/Core/Observers/KeybindTrigger.swift b/Loop/Core/Observers/KeybindTrigger.swift index 2b57f322..b74b3857 100644 --- a/Loop/Core/Observers/KeybindTrigger.swift +++ b/Loop/Core/Observers/KeybindTrigger.swift @@ -20,7 +20,6 @@ final class KeybindTrigger { // State-tracking private var pressedKeys: Set = [] private var previousEventFlags: CGEventFlags = [] - private var lastKeyReleaseTime: Date = .now private var eventMonitor: ActiveEventMonitor? private var systemKeybindCache: Set> = [] @@ -162,12 +161,6 @@ final class KeybindTrigger { } if type == .keyUp { - // Ignore key-up events occurring within 100ms of each other. - // Prevents direction changes when rapidly (normally) releasing multiple pressed keys. - if abs(lastKeyReleaseTime.timeIntervalSinceNow) > 0.1 { - lastKeyReleaseTime = Date.now - } - return .forward } @@ -177,7 +170,7 @@ final class KeybindTrigger { } } - if type != .keyUp { + if type != .keyUp { // keyDown for flagsChanged if containsTrigger { if let action = windowActionCache.actionsByKeybind[actionKeys] { if !isARepeat || action.canRepeat { @@ -186,7 +179,7 @@ final class KeybindTrigger { /// Only consume the event if the last command actually opened Loop. /// The main reason Loop *wouldn't* open after an `openLoop` call would be because the user has enabled a trigger delay. - return checkIfLoopOpen() ? .consume : .opening + return isLoopOpen ? .consume : .opening } // Only trigger Loop without an action if the only pressed keys perfectly matches the trigger key. diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index 6092d11a..0615edf1 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -10,6 +10,35 @@ import SwiftUI /// A wrapper for functions defined in `SkyLightSymbolLoader` enum SkyLightToolBelt { + /// Brings the window’s owning process to the front using SkyLight APIs. + /// - Parameters: + /// - windowID: The `CGWindowID` of the window to make the frontmost process. + /// - pid: The PID of the target window's owner process. + /// - Returns: Whether this operation was successful. + static func makeFrontProcess(windowID: CGWindowID, pid: pid_t) -> Bool { + guard let SLPSSetFrontProcessWithOptions = SkyLightSymbolLoader.SLPSSetFrontProcessWithOptions else { + Log.error("Failed to load SkyLight symbols in \(#function)", category: .skyLightToolBelt) + return false + } + + var wid = windowID + var psn = ProcessSerialNumber() + let status = GetProcessForPID(pid, &psn) + + var cgStatus = SLPSSetFrontProcessWithOptions( + &psn, + wid, + kCPSUserGenerated + ) + + guard cgStatus == .success else { + Log.error("Failed to set frontmost process with status: \(cgStatus.rawValue)", category: .skyLightToolBelt) + return false + } + + return true + } + /// /// Focuses a window. This will attempt to bring the window to the front and make it the active window. /// Note that this first sets the process as frontmost, *then* sends a left click event to the window itself. @@ -21,10 +50,8 @@ enum SkyLightToolBelt { /// - windowID: The `CGWindowID` of the window to focus. /// - pid: The PID of the target window's owner process. /// - Returns: Whether this operation was successful. - static func focusWindow(windowID: CGWindowID, pid: pid_t) -> Bool { - guard let SLPSSetFrontProcessWithOptions = SkyLightSymbolLoader.SLPSSetFrontProcessWithOptions, - let SLPSPostEventRecordTo = SkyLightSymbolLoader.SLPSPostEventRecordTo - else { + static func makeKeyWindow(windowID: CGWindowID, pid: pid_t) -> Bool { + guard let SLPSPostEventRecordTo = SkyLightSymbolLoader.SLPSPostEventRecordTo else { Log.error("Failed to load SkyLight symbols in \(#function)", category: .skyLightToolBelt) return false } @@ -38,17 +65,6 @@ enum SkyLightToolBelt { return false } - var cgStatus = SLPSSetFrontProcessWithOptions( - &psn, - wid, - kCPSUserGenerated - ) - - guard cgStatus == .success else { - Log.error("Failed to set frontmost process with status: \(cgStatus.rawValue)", category: .skyLightToolBelt) - return false - } - /// `0x01` is left click down, `0x02` is left click up (see `CGEventType`) for byte in [0x01, 0x02] { /// Create raw `SLSEvent` data. @@ -62,7 +78,7 @@ enum SkyLightToolBelt { bytes[0x3A] = 0x10 memcpy(&bytes[0x3C], &wid, MemoryLayout.size) memset(&bytes[0x20], 0xFF, 0x10) - cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in + let cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee) } diff --git a/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift b/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift index 68df647b..98f39c1e 100644 --- a/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift +++ b/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift @@ -110,7 +110,8 @@ struct BehaviorConfigurationView: View { LuminareToggle("Resize window under cursor", isOn: $resizeWindowUnderCursor) - if resizeWindowUnderCursor { + // If the system WM is enabled, the window under the cursor requires focus. + if resizeWindowUnderCursor, !useSystemWindowManagerWhenAvailable { LuminareToggle("Focus window on resize", isOn: $focusWindowOnResize) } } diff --git a/Loop/Settings Window/SettingsWindowManager.swift b/Loop/Settings Window/SettingsWindowManager.swift index 7af2c1b9..9a913634 100644 --- a/Loop/Settings Window/SettingsWindowManager.swift +++ b/Loop/Settings Window/SettingsWindowManager.swift @@ -107,6 +107,8 @@ final class SettingsWindowManager: ObservableObject { if let controller { controller.close() self.controller = nil + + Log.success("Settings window closed", category: .settingsWindowManager) } stopTimer() @@ -114,8 +116,6 @@ final class SettingsWindowManager: ObservableObject { if !Defaults[.showDockIcon] { NSApp.setActivationPolicy(.accessory) } - - Log.success("Settings window closed", category: .settingsWindowManager) } private func restartTimer() { diff --git a/Loop/Stashing/StashManager.swift b/Loop/Stashing/StashManager.swift index 7c990007..7cb853bc 100644 --- a/Loop/Stashing/StashManager.swift +++ b/Loop/Stashing/StashManager.swift @@ -279,7 +279,7 @@ private extension StashManager { if shiftFocusWhenStashed { Task { @MainActor in - window.window.activate() + window.window.focus() } } @@ -352,7 +352,7 @@ private extension StashManager { if let focusWindow { Log.info("Focusing another window on the same screen: \(focusWindow.description).", category: .stashManager) Task { @MainActor in - focusWindow.activate() + focusWindow.focus() } } } diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index 16d2f289..2f1ea16c 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -82,16 +82,16 @@ enum WindowEngine { default: break } - if Defaults[.focusWindowOnResize] { - await window.activate() - } - let useSystemWM: Bool = if #available(macOS 15, *) { Defaults[.useSystemWindowManagerWhenAvailable] } else { false } + if Defaults[.focusWindowOnResize] || useSystemWM { + await window.focus() + } + // Attempt system window manager if possible if !willChangeScreens, useSystemWM, #available(macOS 15, *), diff --git a/Loop/Window Management/Window/Window.swift b/Loop/Window Management/Window/Window.swift index ad56a7ec..e13ceffd 100644 --- a/Loop/Window Management/Window/Window.swift +++ b/Loop/Window Management/Window/Window.swift @@ -172,31 +172,35 @@ final class Window { } } - /// Activate the window. This will bring it to the front and focus it if possible + /// Focus the window. @MainActor - func activate() { + func focus() { // First activate the application to ensure proper window management context if let runningApplication = nsRunningApplication { runningApplication.activate(options: .activateIgnoringOtherApps) } - // Then set the window as main after a brief delay to ensure proper ordering - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - try? self.axWindow.setValue(.main, value: true) - } + try? axWindow.performAction(.raise) - focus() - } + /// See: https://github.com/yresk/alt-tab-macos/blob/5b8a9110dbdb9b4802a8a85ee1469427fbc192e8/alt-tab-macos/api-wrappers/AXUIElement.swift#L60 + if let pid = try? axWindow.getPID() { + _ = SkyLightToolBelt.makeKeyWindow( + windowID: cgWindowID, + pid: pid + ) - /// - Returns: - /// `true` if the window was successfully focused; `false` otherwise. - @discardableResult - private func focus() -> Bool { - guard let pid = try? axWindow.getPID() else { return false } - return SkyLightToolBelt.focusWindow( - windowID: cgWindowID, - pid: pid - ) + _ = SkyLightToolBelt.makeFrontProcess( + windowID: cgWindowID, + pid: pid + ) + + _ = SkyLightToolBelt.makeKeyWindow( + windowID: cgWindowID, + pid: pid + ) + } + + try? axWindow.performAction(.raise) } var isAppExcluded: Bool { diff --git a/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift b/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift index 144370bf..c4d16b28 100644 --- a/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift +++ b/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift @@ -33,7 +33,7 @@ extension WindowUtility { Log.info("Focusing window: \(nextWindowTitle)", category: .windowUtility) Task { @MainActor in - directionalWindow.activate() + directionalWindow.focus() } return directionalWindow @@ -49,7 +49,7 @@ extension WindowUtility { Log.info("Focusing window: \(nextWindowTitle)", category: .windowUtility) Task { @MainActor in - directionalWindow.activate() + directionalWindow.focus() } return directionalWindow