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
3 changes: 2 additions & 1 deletion Loop/Core/LoopManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
19 changes: 8 additions & 11 deletions Loop/Utilities/DirectionalNavigationUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,21 @@ final class DirectionalNavigationUtility<T> {
/// 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
)
Expand All @@ -94,7 +94,7 @@ final class DirectionalNavigationUtility<T> {

// 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
)
}
Expand All @@ -103,18 +103,18 @@ final class DirectionalNavigationUtility<T> {
/// 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
)

Expand All @@ -135,7 +135,6 @@ final class DirectionalNavigationUtility<T> {
items
.filter { other in
let otherFrame = frameProvider(other)
guard otherFrame != currentFrame else { return false }

let sharedAxisPointSpan = switch axis {
case .horizontal:
Expand Down Expand Up @@ -181,7 +180,6 @@ final class DirectionalNavigationUtility<T> {
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
Expand Down Expand Up @@ -213,7 +211,6 @@ final class DirectionalNavigationUtility<T> {
items
.filter { other in
let otherFrame = frameProvider(other)
guard otherFrame != currentFrame else { return false }

// Directional check: consider center as well
let currentCenter = currentFrame.center
Expand Down
5 changes: 3 additions & 2 deletions Loop/Utilities/ScreenUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 29 additions & 21 deletions Loop/Window Management/Window/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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? {
Expand Down Expand Up @@ -428,8 +437,7 @@ final class Window {

extension Window: CustomStringConvertible {
var description: String {
let name = nsRunningApplication?.localizedName ?? title ?? "<unknown>"
return "Window(id: \(cgWindowID), title: \(name))"
"Window(id: \(cgWindowID), app: '\(nsRunningApplication?.localizedName ?? "<unknown>")', title: '\(title ?? "<unknown>"))"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SwiftUI
extension WindowUtility {
private static var navigationUtility = DirectionalNavigationUtility<Window>(
minDirectionalSpan: .percentage(10),
minStackedArea: .percentage(60),
minStackedArea: .percentage(50),
frameProvider: \.frame
)

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Loop/Window Management/Window/WindowUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down