Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions Loop/Core/Observers/KeybindTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ final class KeybindTrigger {
// State-tracking
private var pressedKeys: Set<CGKeyCode> = []
private var previousEventFlags: CGEventFlags = []
private var lastKeyReleaseTime: Date = .now
private var eventMonitor: ActiveEventMonitor?

private var systemKeybindCache: Set<Set<CGKeyCode>> = []
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand All @@ -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.
Expand Down
48 changes: 32 additions & 16 deletions Loop/Private APIs/SkyLightToolBelt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand All @@ -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.
Expand All @@ -62,7 +78,7 @@ enum SkyLightToolBelt {
bytes[0x3A] = 0x10
memcpy(&bytes[0x3C], &wid, MemoryLayout<UInt32>.size)
memset(&bytes[0x20], 0xFF, 0x10)
cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in
let cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in
SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Loop/Settings Window/SettingsWindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ final class SettingsWindowManager: ObservableObject {
if let controller {
controller.close()
self.controller = nil

Log.success("Settings window closed", category: .settingsWindowManager)
}

stopTimer()

if !Defaults[.showDockIcon] {
NSApp.setActivationPolicy(.accessory)
}

Log.success("Settings window closed", category: .settingsWindowManager)
}

private func restartTimer() {
Expand Down
4 changes: 2 additions & 2 deletions Loop/Stashing/StashManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ private extension StashManager {

if shiftFocusWhenStashed {
Task { @MainActor in
window.window.activate()
window.window.focus()
}
}

Expand Down Expand Up @@ -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()
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Loop/Window Management/Window Manipulation/WindowEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, *),
Expand Down
38 changes: 21 additions & 17 deletions Loop/Window Management/Window/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension WindowUtility {
Log.info("Focusing window: \(nextWindowTitle)", category: .windowUtility)

Task { @MainActor in
directionalWindow.activate()
directionalWindow.focus()
}

return directionalWindow
Expand All @@ -49,7 +49,7 @@ extension WindowUtility {
Log.info("Focusing window: \(nextWindowTitle)", category: .windowUtility)

Task { @MainActor in
directionalWindow.activate()
directionalWindow.focus()
}

return directionalWindow
Expand Down