diff --git a/Loop/Core/LoopManager.swift b/Loop/Core/LoopManager.swift index da003c27..c73a485e 100644 --- a/Loop/Core/LoopManager.swift +++ b/Loop/Core/LoopManager.swift @@ -184,7 +184,8 @@ extension LoopManager { if !forceClose { // If the preview was disabled, the window will already be in the specified action's frame. // So only resize the window if the preview is enabled. - if Defaults[.previewVisibility] { + if Defaults[.previewVisibility], + !resizeContext.action.direction.willFocusWindow { Task { _ = try? await WindowActionEngine.shared.apply(context: resizeContext) } diff --git a/Loop/Utilities/DirectionalNavigationUtility.swift b/Loop/Utilities/DirectionalNavigationUtility.swift index a19c7905..74d9c6ec 100644 --- a/Loop/Utilities/DirectionalNavigationUtility.swift +++ b/Loop/Utilities/DirectionalNavigationUtility.swift @@ -61,21 +61,21 @@ final class DirectionalNavigationUtility { /// Generic directional navigation for any items with a frame (e.g., Windows or Screens) /// - Parameters: /// - current: The current item - /// - items: All available items to search through + /// - others: All available items to search through /// - direction: The direction to search /// - canRestartCycle: Whether to wrap around when no items found in direction /// - frameProvider: Closure that extracts the CGRect frame from an item /// - Returns: The next item in the specified direction, or nil func directionalItem( from current: T, - in items: [T], + others: [T], direction: NavigationDirection, canWrap: Bool = true ) -> T? { let currentFrame = frameProvider(current) let itemsInSpan = filterItemsBySharedSpan( - in: items, + in: others, axis: direction.axis, currentFrame: currentFrame ) @@ -94,7 +94,7 @@ final class DirectionalNavigationUtility { // Wrap around to the furthest item in the opposite direction return furthestItemInDirection( - in: itemsInSpan.isEmpty ? items : itemsInSpan, + in: itemsInSpan.isEmpty ? others : itemsInSpan, direction: direction.flipped ) } @@ -103,18 +103,18 @@ final class DirectionalNavigationUtility { /// Only considers items that meet the minStackedArea threshold with the current item. /// - Parameters: /// - current: The current item - /// - items: All available items in stack order + /// - others: All available items in stack order /// - Returns: The next item in the stack cycle, or nil if not found or wrapping is disabled func cycleInStack( from current: T, - in items: [T] + others: [T] ) -> T? { - guard !items.isEmpty else { return nil } + guard !others.isEmpty else { return nil } let currentFrame = frameProvider(current) let overlappingItems = filterItemsBySharedArea( - in: items, + in: others, currentFrame: currentFrame ) @@ -135,7 +135,6 @@ final class DirectionalNavigationUtility { items .filter { other in let otherFrame = frameProvider(other) - guard otherFrame != currentFrame else { return false } let sharedAxisPointSpan = switch axis { case .horizontal: @@ -181,7 +180,6 @@ final class DirectionalNavigationUtility { return items .filter { other in let otherFrame = frameProvider(other) - guard otherFrame != currentFrame else { return false } let intersect = otherFrame.intersection(currentFrame) let sharedArea = intersect.size.area @@ -213,7 +211,6 @@ final class DirectionalNavigationUtility { items .filter { other in let otherFrame = frameProvider(other) - guard otherFrame != currentFrame else { return false } // Directional check: consider center as well let currentCenter = currentFrame.center diff --git a/Loop/Utilities/ScreenUtility.swift b/Loop/Utilities/ScreenUtility.swift index caa357e4..93df80e6 100644 --- a/Loop/Utilities/ScreenUtility.swift +++ b/Loop/Utilities/ScreenUtility.swift @@ -120,11 +120,12 @@ enum ScreenUtility { direction: NavigationDirection, canWrap: Bool = true ) -> NSScreen? { - let screens = NSScreen.screens + let currentDisplayID = currentScreen.displayID + let otherScreens = NSScreen.screens.filter { $0.displayID != currentDisplayID } return navigationUtility.directionalItem( from: currentScreen, - in: screens, + others: otherScreens, direction: direction, canWrap: canWrap ) diff --git a/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift b/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift index 72b1f966..a1583d77 100644 --- a/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift +++ b/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift @@ -60,7 +60,7 @@ struct PaddingConfiguration: Codable, Defaults.Serializable, Hashable { action: WindowAction, window: Window? ) -> CGRect { - guard bounds.width > 0, bounds.height > 0 else { return frame } + guard bounds.size.area > 0, frame.size.area > 0 else { return frame } var result = frame diff --git a/Loop/Window Management/Window/Window.swift b/Loop/Window Management/Window/Window.swift index 9f5a32ce..11079688 100644 --- a/Loop/Window Management/Window/Window.swift +++ b/Loop/Window Management/Window/Window.swift @@ -68,9 +68,9 @@ final class Window { try self.init(element: window) } - /// Initialize a window from an entry in a dictionary returned by `CGWindowListCopyWindowInfo`. + /// Retrieve a window from an entry in a dictionary returned by `CGWindowListCopyWindowInfo`. /// - Parameter windowInfo: The dictionary containing information about the window. - convenience init(windowInfo: [String: AnyObject]) throws { + static func fromWindowInfo(_ windowInfo: [String: AnyObject]) throws -> Window { // First, check if we can initialize a window simply based on its PID. guard let alpha = windowInfo[kCGWindowAlpha as String] as? Double, alpha > 0.01, // Ignore invisible windows @@ -85,32 +85,41 @@ final class Window { } let element = AXUIElementCreateApplication(pid) - guard let windows: [AXUIElement] = try element.getValue(.windows), - !windows.isEmpty + guard let windowElements: [AXUIElement] = try element.getValue(.windows), + !windowElements.isEmpty else { throw WindowError.cannotGetWindow } // If there’s only one window, use that as there's no need to grab its frame - if windows.count == 1 { - try self.init(element: windows[0]) - return + if windowElements.count == 1 { + return try Window(element: windowElements[0]) } - // Try to match against the frame when there are multiple windows - if let boundsDict = windowInfo[kCGWindowBounds as String] as? [String: CGFloat], - let frame = CGRect(dictionaryRepresentation: boundsDict as CFDictionary), - let match = try windows.first(where: { window in - let position: CGPoint? = try window.getValue(.position) - let size: CGSize? = try window.getValue(.size) - return position == frame.origin && size == frame.size - }) { - try self.init(element: match) - return + // If we can retrieve bounds, then filter candidates out by their respective frames. + let candidates: [AXUIElement] = if let boundsDict = windowInfo[kCGWindowBounds as String] as? [String: CGFloat], + let frame = CGRect(dictionaryRepresentation: boundsDict as CFDictionary) { + windowElements.filter { + if let position: CGPoint = try? $0.getValue(.position), + let size: CGSize = try? $0.getValue(.size) { + return position == frame.origin && size == frame.size + } + return false + } + } else { + windowElements + } + + let windows = candidates.compactMap { try? Window(element: $0) } + + if let windowID = windowInfo[kCGWindowNumber as String] as? CGWindowID, + let match = windows.first(where: { $0.cgWindowID == windowID }) { + return match + } else if let first = windows.first { + return first } - // Fallback! initialize from the first available window - try self.init(element: windows[0]) + return try Window(element: windowElements[0]) } var role: NSAccessibility.Role? { @@ -428,8 +437,7 @@ final class Window { extension Window: CustomStringConvertible { var description: String { - let name = nsRunningApplication?.localizedName ?? title ?? "" - return "Window(id: \(cgWindowID), title: \(name))" + "Window(id: \(cgWindowID), app: '\(nsRunningApplication?.localizedName ?? "")', title: '\(title ?? ""))" } } diff --git a/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift b/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift index ee701f77..e38df281 100644 --- a/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift +++ b/Loop/Window Management/Window/WindowUtility+FocusNavigation.swift @@ -12,7 +12,7 @@ import SwiftUI extension WindowUtility { private static var navigationUtility = DirectionalNavigationUtility( minDirectionalSpan: .percentage(10), - minStackedArea: .percentage(60), + minStackedArea: .percentage(50), frameProvider: \.frame ) @@ -41,7 +41,6 @@ extension WindowUtility { static func focusNextWindowInStack(from currentWindow: Window?) -> Window? { guard let directionalWindow = WindowUtility.nextStackedWindow(from: currentWindow) else { - log.info("No window found to focus in stack") return nil } @@ -91,7 +90,7 @@ extension WindowUtility { // Use the generic directional navigation from DirectionalNavigationUtility if let nextWindow = navigationUtility.directionalItem( from: currentWindow, - in: otherWindows, + others: otherWindows, direction: direction, canWrap: true ) { @@ -167,7 +166,7 @@ extension WindowUtility { // Use the generic stack cycling from DirectionalNavigationUtility if let nextWindow = navigationUtility.cycleInStack( from: currentWindow, - in: otherWindows + others: otherWindows ) { log.info("Found window to focus in stack: \(nextWindow.description)") return nextWindow diff --git a/Loop/Window Management/Window/WindowUtility.swift b/Loop/Window Management/Window/WindowUtility.swift index 623d37d3..4a38a57d 100644 --- a/Loop/Window Management/Window/WindowUtility.swift +++ b/Loop/Window Management/Window/WindowUtility.swift @@ -81,7 +81,7 @@ enum WindowUtility { var windowList: [Window] = [] for windowInfo in list { - if let window = try? Window(windowInfo: windowInfo) { + if let window = try? Window.fromWindowInfo(windowInfo) { windowList.append(window) } }