From f11bbb212f6f70b0214d6c7b358a64b620c5182a Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 13:36:19 -0600 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=A1=20Optimize=20resize=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Extensions/AXUIElement+Extensions.swift | 9 + Loop/Managers/LoopManager.swift | 3 - Loop/Window Management/Window.swift | 10 ++ Loop/Window Management/WindowAction.swift | 175 +++++++++++-------- Loop/Window Management/WindowEngine.swift | 9 +- 5 files changed, 127 insertions(+), 79 deletions(-) diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index 1239463f..2203ae65 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -37,6 +37,15 @@ extension AXUIElement { } } + func canSetValue(_ attribute: NSAccessibility.Attribute) throws -> Bool { + var isSettable = DarwinBoolean(false) + let error = AXUIElementIsAttributeSettable(self, attribute as CFString, &isSettable) + guard error == .success else { + throw error + } + return isSettable.boolValue + } + func getElementAtPosition(_ position: CGPoint) throws -> AXUIElement? { var element: AXUIElement? let error = AXUIElementCopyElementAtPosition(self, Float(position.x), Float(position.y), &element) diff --git a/Loop/Managers/LoopManager.swift b/Loop/Managers/LoopManager.swift index 4b0e52ad..54a74cfe 100644 --- a/Loop/Managers/LoopManager.swift +++ b/Loop/Managers/LoopManager.swift @@ -14,7 +14,6 @@ class LoopManager: ObservableObject { // Size Adjustment static var sidesToAdjust: Edge.Set? static var lastTargetFrame: CGRect = .zero - static var canAdjustSize: Bool = true private let keybindMonitor = KeybindMonitor.shared @@ -168,7 +167,6 @@ private extension LoopManager { isLoopActive { if let screenToResizeOn, Defaults[.previewVisibility] { - LoopManager.canAdjustSize = false WindowEngine.resize( targetWindow!, to: currentAction, @@ -191,7 +189,6 @@ private extension LoopManager { isLoopActive = false LoopManager.sidesToAdjust = nil LoopManager.lastTargetFrame = .zero - LoopManager.canAdjustSize = true } func openWindows() { diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index 8de5de94..ac76666a 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -255,6 +255,16 @@ class Window { } } + var isResizable: Bool { + do { + let result: Bool = try self.axWindow.canSetValue(.size) + return result + } catch { + print("Failed to get resizable: \(error.localizedDescription)") + return true + } + } + var frame: CGRect { CGRect(origin: self.position, size: self.size) } diff --git a/Loop/Window Management/WindowAction.swift b/Loop/Window Management/WindowAction.swift index 0c304cdc..42b55245 100644 --- a/Loop/Window Management/WindowAction.swift +++ b/Loop/Window Management/WindowAction.swift @@ -148,102 +148,54 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial LoopManager.sidesToAdjust = nil } - if let frameMultiplyValues = direction.frameMultiplyValues { - result.origin.x += bounds.width * frameMultiplyValues.minX - result.origin.y += bounds.height * frameMultiplyValues.minY - result.size.width = bounds.width * frameMultiplyValues.width - result.size.height = bounds.height * frameMultiplyValues.height + if direction.frameMultiplyValues != nil { + result = applyFrameMultiplyValues(bounds) } else if direction.willAdjustSize { let frameToResizeFrom = LoopManager.lastTargetFrame - - result = frameToResizeFrom - if LoopManager.canAdjustSize { - result = calculateSizeAdjustment(frameToResizeFrom, bounds) - } + result = calculateSizeAdjustment(frameToResizeFrom, bounds) } else if direction.willShrink || direction.willGrow { // This allows for control over each side let frameToResizeFrom = LoopManager.lastTargetFrame - result = frameToResizeFrom - if LoopManager.canAdjustSize { - switch direction { - case .shrinkTop, .growTop: - LoopManager.sidesToAdjust = .top - case .shrinkBottom, .growBottom: - LoopManager.sidesToAdjust = .bottom - case .shrinkLeft, .growLeft: - LoopManager.sidesToAdjust = .leading - default: - LoopManager.sidesToAdjust = .trailing - } - - result = calculateSizeAdjustment(frameToResizeFrom, bounds) + // calculateSizeAdjustment() will read LoopManager.sidesToAdjust, but we compute them here + switch direction { + case .shrinkTop, .growTop: + LoopManager.sidesToAdjust = .top + case .shrinkBottom, .growBottom: + LoopManager.sidesToAdjust = .bottom + case .shrinkLeft, .growLeft: + LoopManager.sidesToAdjust = .leading + default: + LoopManager.sidesToAdjust = .trailing } + result = calculateSizeAdjustment(frameToResizeFrom, bounds) + } else if direction.willMove { let frameToResizeFrom = LoopManager.lastTargetFrame - result = calculatePointAdjustment(frameToResizeFrom) + result = calculatePositionAdjustment(frameToResizeFrom) } else if direction == .custom { result = calculateCustomFrame(window, bounds) } else if direction == .center { - let windowSize: CGSize = if let window { - window.size - } else { - .init(width: bounds.width / 2, height: bounds.height / 2) - } - - result = CGRect( - origin: CGPoint( - x: bounds.midX - (windowSize.width / 2), - y: bounds.midY - (windowSize.height / 2) - ), - size: windowSize - ) + result = calculateCenterFrame(window, bounds) } else if direction == .macOSCenter { - let windowSize: CGSize = if let window { - window.size - } else { - .init(width: bounds.width / 2, height: bounds.height / 2) - } + result = calculateMacOSCenterFrame(window, bounds) - let yOffset = WindowEngine.getMacOSCenterYOffset( - windowSize.height, - screenHeight: bounds.height - ) - - result = CGRect( - origin: CGPoint( - x: bounds.midX - (windowSize.width / 2), - y: bounds.midY - (windowSize.height / 2) + yOffset - ), - size: windowSize - ) } else if direction == .undo, let window { - if let previousAction = WindowRecords.getLastAction(for: window) { - print("Last action was \(previousAction.direction) (name: \(previousAction.name ?? "nil"))") - result = previousAction.getFrame(window: window, bounds: bounds) - } else { - print("Didn't find frame to undo; using current frame") - result = window.frame - } + result = getLastActionFrame(window, bounds) } else if direction == .initialFrame, let window { - if let initialFrame = WindowRecords.getInitialFrame(for: window) { - result = initialFrame - } else { - print("Didn't find initial frame; using current frame") - result = window.frame - } + result = getInitialFrame(window) } if !disablePadding { if direction != .undo, direction != .initialFrame { - result = cropThenApplyInnerPadding(result, bounds) + result = applyInnerPadding(result, bounds) } LoopManager.lastTargetFrame = result @@ -251,8 +203,25 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return result } +} + +// MARK: - Window Frame Calculations + +private extension WindowAction { + func applyFrameMultiplyValues(_ bounds: CGRect) -> CGRect { + guard let frameMultiplyValues = direction.frameMultiplyValues else { + return .zero + } + + return CGRect( + x: bounds.origin.x + (bounds.width * frameMultiplyValues.minX), + y: bounds.origin.y + (bounds.height * frameMultiplyValues.minY), + width: bounds.width * frameMultiplyValues.width, + height: bounds.height * frameMultiplyValues.height + ) + } - private func calculateCustomFrame(_ window: Window?, _ bounds: CGRect) -> CGRect { + func calculateCustomFrame(_ window: Window?, _ bounds: CGRect) -> CGRect { var result = CGRect(origin: bounds.origin, size: .zero) // SIZE @@ -342,7 +311,63 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return result } - private func calculateSizeAdjustment(_ frameToResizeFrom: CGRect, _ bounds: CGRect) -> CGRect { + func calculateCenterFrame(_ window: Window?, _ bounds: CGRect) -> CGRect { + let windowSize: CGSize = if let window { + window.size + } else { + .init(width: bounds.width / 2, height: bounds.height / 2) + } + + return CGRect( + origin: CGPoint( + x: bounds.midX - (windowSize.width / 2), + y: bounds.midY - (windowSize.height / 2) + ), + size: windowSize + ) + } + + func calculateMacOSCenterFrame(_ window: Window?, _ bounds: CGRect) -> CGRect { + let windowSize: CGSize = if let window { + window.size + } else { + .init(width: bounds.width / 2, height: bounds.height / 2) + } + + let yOffset = WindowEngine.getMacOSCenterYOffset( + windowSize.height, + screenHeight: bounds.height + ) + + return CGRect( + origin: CGPoint( + x: bounds.midX - (windowSize.width / 2), + y: bounds.midY - (windowSize.height / 2) + yOffset + ), + size: windowSize + ) + } + + func getLastActionFrame(_ window: Window, _ bounds: CGRect) -> CGRect { + if let previousAction = WindowRecords.getLastAction(for: window) { + print("Last action was \(previousAction.direction) (name: \(previousAction.name ?? "nil"))") + return previousAction.getFrame(window: window, bounds: bounds) + } else { + print("Didn't find frame to undo; using current frame") + return window.frame + } + } + + func getInitialFrame(_ window: Window) -> CGRect { + if let initialFrame = WindowRecords.getInitialFrame(for: window) { + return initialFrame + } else { + print("Didn't find initial frame; using current frame") + return window.frame + } + } + + func calculateSizeAdjustment(_ frameToResizeFrom: CGRect, _ bounds: CGRect) -> CGRect { var result = frameToResizeFrom let totalBounds: Edge.Set = [.top, .bottom, .leading, .trailing] let step = Defaults[.sizeIncrement] * ((direction == .larger || direction.willGrow) ? -1 : 1) @@ -390,7 +415,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return result } - private func calculatePointAdjustment(_ frameToResizeFrom: CGRect) -> CGRect { + func calculatePositionAdjustment(_ frameToResizeFrom: CGRect) -> CGRect { var result = frameToResizeFrom if direction == .moveUp { @@ -406,7 +431,8 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return result } - private func getPaddedBounds(_ bounds: CGRect) -> CGRect { + // This will apply padding to the bounds of the frame + func getPaddedBounds(_ bounds: CGRect) -> CGRect { let padding = Defaults[.padding] var bounds = bounds @@ -418,7 +444,8 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return bounds } - private func cropThenApplyInnerPadding(_ windowFrame: CGRect, _ bounds: CGRect, _ screen: NSScreen? = nil) -> CGRect { + // This will apply padding within the frame, in between windows + func applyInnerPadding(_ windowFrame: CGRect, _ bounds: CGRect, _ screen: NSScreen? = nil) -> CGRect { guard !direction.willMove else { return windowFrame } diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 3d6246bf..fd6ac995 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -30,17 +30,21 @@ enum WindowEngine { window.activate() } + // If window hasn't been recorded yet, record it, so that the user can undo the action if !WindowRecords.hasBeenRecorded(window) { WindowRecords.recordFirst(for: window) } + // If the action is fullscreen, toggle fullscreen then return if action.direction == .fullscreen { window.toggleFullscreen() WindowRecords.record(window, action) return } + // Otherwise, we obviously need to disable fullscreen to resize the window window.fullscreen = false + // If the action is to hide or minimize, perform the action then return if action.direction == .hide { window.toggleHidden() return @@ -51,14 +55,15 @@ enum WindowEngine { return } + // Calculate the target frame let targetFrame = action.getFrame(window: window, bounds: screen.safeScreenFrame, screen: screen) + print("Target window frame: \(targetFrame)") + // If the action is undo, remove the last action from the window, as the target frame already contains the last action's size if action.direction == .undo { WindowRecords.removeLastAction(for: window) } - print("Target window frame: \(targetFrame)") - let enhancedUI = window.enhancedUserInterface let animate = Defaults[.animateWindowResizes] && !enhancedUI WindowRecords.record(window, action) From f8697ab48d9b8649e1986b28fae693687a126d91 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 13:44:23 -0600 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=8E=A8=20Move=20target=20frame=20code?= =?UTF-8?q?=20into=20`calculateTargetFrame()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/WindowAction.swift | 41 ++++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Loop/Window Management/WindowAction.swift b/Loop/Window Management/WindowAction.swift index 42b55245..86f84c04 100644 --- a/Loop/Window Management/WindowAction.swift +++ b/Loop/Window Management/WindowAction.swift @@ -137,17 +137,44 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial guard direction != .cycle, direction != .noAction else { return NSRect(origin: bounds.center, size: .zero) } + var bounds = bounds + var result = .zero + + // Get padded bounds only if padding can be applied if !disablePadding && Defaults[.enablePadding], Defaults[.paddingMinimumScreenSize] == .zero || screen?.diagonalSize ?? .zero > Defaults[.paddingMinimumScreenSize] { bounds = getPaddedBounds(bounds) } - var result = CGRect(origin: bounds.origin, size: .zero) if !willManipulateExistingWindowFrame { LoopManager.sidesToAdjust = nil } + result = calculateTargetFrame(direction, window, bounds) + + if !disablePadding { + // Apply padding between windows + if direction != .undo, direction != .initialFrame { + result = applyInnerPadding(result, bounds) + } + + // Store the last target frame. This is used when growing/shrinking windows + // We only store it when disablePadding is false, as otherwise, it is going to be the preview window using this frame. + LoopManager.lastTargetFrame = result + } + + return result + } +} + +// MARK: - Window Frame Calculations + +private extension WindowAction { + + func calculateTargetFrame(_ direction: WindowDirection, _ window: Window?, _ bounds: CGRect) -> CGRect { + var result: CGRect = .zero + if direction.frameMultiplyValues != nil { result = applyFrameMultiplyValues(bounds) @@ -193,21 +220,9 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial result = getInitialFrame(window) } - if !disablePadding { - if direction != .undo, direction != .initialFrame { - result = applyInnerPadding(result, bounds) - } - - LoopManager.lastTargetFrame = result - } - return result } -} - -// MARK: - Window Frame Calculations -private extension WindowAction { func applyFrameMultiplyValues(_ bounds: CGRect) -> CGRect { guard let frameMultiplyValues = direction.frameMultiplyValues else { return .zero From ada4c985b0be15e40f43d416fea9ac94363ef50f Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 14:11:46 -0600 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20Center=20fixed-size=20windows?= =?UTF-8?q?=20within=20their=20target=20frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Extensions/CGGeometry+Extensions.swift | 23 ++++++++++++++++++++- Loop/Managers/WindowDragManager.swift | 2 +- Loop/Window Management/WindowAction.swift | 10 +++++++-- Loop/Window Management/WindowEngine.swift | 14 +------------ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Loop/Extensions/CGGeometry+Extensions.swift b/Loop/Extensions/CGGeometry+Extensions.swift index 62d83854..67a639f9 100644 --- a/Loop/Extensions/CGGeometry+Extensions.swift +++ b/Loop/Extensions/CGGeometry+Extensions.swift @@ -52,6 +52,19 @@ extension CGSize { func approximatelyEqual(to size: CGSize, tolerance: CGFloat = 10) -> Bool { abs(width - size.width) < tolerance && abs(height - size.height) < tolerance } + + func center(inside parentRect: CGRect) -> CGRect { + let parentRectCenter = parentRect.center + let newX = parentRectCenter.x - width / 2 + let newY = parentRectCenter.y - height / 2 + + return CGRect( + x: newX, + y: newY, + width: width, + height: height + ) + } } extension CGRect { @@ -99,9 +112,17 @@ extension CGRect { abs(height - rect.height) < tolerance } - func pushBottomRightPointInside(_ rect2: CGRect) -> CGRect { + func pushInside(_ rect2: CGRect) -> CGRect { var result = self + if result.minX < rect2.minX { + result.origin.x = rect2.minX + } + + if result.minY < rect2.minY { + result.origin.y = rect2.minY + } + if result.maxX > rect2.maxX { result.origin.x = rect2.maxX - result.width } diff --git a/Loop/Managers/WindowDragManager.swift b/Loop/Managers/WindowDragManager.swift index 1280d85d..66baf756 100644 --- a/Loop/Managers/WindowDragManager.swift +++ b/Loop/Managers/WindowDragManager.swift @@ -104,7 +104,7 @@ class WindowDragManager { if let screen = NSScreen.screenWithMouse { var newWindowFrame = window.frame newWindowFrame.size = initialFrame.size - newWindowFrame = newWindowFrame.pushBottomRightPointInside(screen.frame) + newWindowFrame = newWindowFrame.pushInside(screen.frame) window.setFrame(newWindowFrame) } else { window.size = initialFrame.size diff --git a/Loop/Window Management/WindowAction.swift b/Loop/Window Management/WindowAction.swift index 86f84c04..c2e79029 100644 --- a/Loop/Window Management/WindowAction.swift +++ b/Loop/Window Management/WindowAction.swift @@ -139,7 +139,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } var bounds = bounds - var result = .zero + var result: CGRect = .zero // Get padded bounds only if padding can be applied if !disablePadding && Defaults[.enablePadding], @@ -154,6 +154,13 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial result = calculateTargetFrame(direction, window, bounds) if !disablePadding { + // If window can't be resized, center it within the already-resized frame. + if let window, window.isResizable == false { + result = window.frame.size + .center(inside: result) + .pushInside(bounds) + } + // Apply padding between windows if direction != .undo, direction != .initialFrame { result = applyInnerPadding(result, bounds) @@ -171,7 +178,6 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial // MARK: - Window Frame Calculations private extension WindowAction { - func calculateTargetFrame(_ direction: WindowDirection, _ window: Window?, _ bounds: CGRect) -> CGRect { var result: CGRect = .zero diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index fd6ac995..1376993f 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -70,22 +70,10 @@ enum WindowEngine { if window.nsRunningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier, let window = NSApp.keyWindow ?? NSApp.windows.first { - var newFrame = targetFrame - newFrame.size = window.frame.size - - if newFrame.maxX > screen.safeScreenFrame.maxX { - newFrame.origin.x = screen.safeScreenFrame.maxX - newFrame.width - Defaults[.padding].right - } - - if newFrame.maxY > screen.safeScreenFrame.maxY { - newFrame.origin.y = screen.safeScreenFrame.maxY - newFrame.height - Defaults[.padding].bottom - } - NSAnimationContext.runAnimationGroup { context in context.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 1, 0.68, 1) - window.animator().setFrame(newFrame.flipY(screen: .screens[0]), display: false) + window.animator().setFrame(targetFrame.flipY(screen: .screens[0]), display: false) } - return } From 0cae1047ed1d557058b8c5008de3c2e0e0992fa0 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 14:45:28 -0600 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9D=20Improve=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/Window.swift | 25 ++++++++++---- Loop/Window Management/WindowEngine.swift | 42 +++++++++++++++-------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index ac76666a..e632bc91 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -28,7 +28,9 @@ class Window { let nsRunningApplication: NSRunningApplication? var observer: Observer? - + + /// Initialize a window from an AXUIElement + /// - Parameter element: The AXUIElement to initialize the window with. If it is not a window, an error will be thrown init(element: AXUIElement) throws { self.axWindow = element @@ -50,7 +52,9 @@ class Window { throw WindowError.invalidWindow } } - + + /// Initialize a window from a PID. The frontmost app with the given PID will be used. + /// - Parameter pid: The PID of the app to get the window from convenience init(pid: pid_t) throws { let element = AXUIElementCreateApplication(pid) guard let window: AXUIElement = try element.getValue(.focusedWindow) else { @@ -125,6 +129,8 @@ class Window { } } + + /// Activate the window. This will bring it to the front and focus it if possible func activate() { do { try self.axWindow.setValue(.main, value: true) @@ -260,7 +266,7 @@ class Window { let result: Bool = try self.axWindow.canSetValue(.size) return result } catch { - print("Failed to get resizable: \(error.localizedDescription)") + print("Failed to determine if window size can be set: \(error.localizedDescription)") return true } } @@ -268,12 +274,19 @@ class Window { var frame: CGRect { CGRect(origin: self.position, size: self.size) } - + + /// Set the frame of this Window. + /// - Parameters: + /// - rect: The new frame for the window + /// - animate: Whether or not to animate the window resizing + /// - sizeFirst: This will set the size first, which is useful when switching screens. Only does something when window animations are off + /// - bounds: This will prevent the window from going outside the bounds. Only does something when window animations are on + /// - completionHandler: Something to run after the window has been resized. This can include things like moving the cursor to the center of the window func setFrame( _ rect: CGRect, animate: Bool = false, - sizeFirst: Bool = false, // Only does something when window animations are off - bounds: CGRect = .zero, // Only does something when window animations are on + sizeFirst: Bool = false, + bounds: CGRect = .zero, completionHandler: @escaping (() -> ()) = {} ) { let enhancedUI = self.enhancedUserInterface diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 1376993f..baa59615 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -64,10 +64,13 @@ enum WindowEngine { WindowRecords.removeLastAction(for: window) } + // If enhancedUI is enabled, then window animations will likely lag a LOT. So, if it's enabled, force-disable animations let enhancedUI = window.enhancedUserInterface let animate = Defaults[.animateWindowResizes] && !enhancedUI + WindowRecords.record(window, action) + // If the window is one of Loop's windows, resize it using the actual NSWindow, preventing crashes if window.nsRunningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier, let window = NSApp.keyWindow ?? NSApp.windows.first { NSAnimationContext.runAnimationGroup { context in @@ -77,6 +80,8 @@ enum WindowEngine { return } + // If the window is being moved via shortcuts (move right, move left etc.), then the screenFrame will be zero. + // This is because the window *can* be moved off-screen in this case. let screenFrame = action.direction.willMove ? .zero : screen.safeScreenFrame let bounds = if Defaults[.enablePadding], @@ -92,23 +97,25 @@ enum WindowEngine { sizeFirst: willChangeScreens, bounds: bounds ) { - // If animations are disabled, check if the window needs extra resizing - if !animate { - // Fixes an issue where window isn't resized correctly on multi-monitor setups - if !window.frame.approximatelyEqual(to: targetFrame) { - print("Backup resizing...") - window.setFrame(targetFrame) - } + // Fixes an issue where window isn't resized correctly on multi-monitor setups + // If window is being animated, then the size is very likely to already be correct, as what's really happening is window.setFrame at a really high rate. + if !animate, !window.frame.approximatelyEqual(to: targetFrame) { + print("Backup resizing...") + window.setFrame(targetFrame) } + // If window's minimum size exceeds the screen bounds, push it back in WindowEngine.handleSizeConstrainedWindow(window: window, bounds: bounds) } + // Move cursor to center of window if user has enabled it if Defaults[.moveCursorWithWindow] { CGWarpMouseCursorPosition(targetFrame.center) } } + /// Get the target window, depending on the user's preferences. This could be the frontmost window, or the window under the cursor. + /// - Returns: The target window static func getTargetWindow() -> Window? { var result: Window? @@ -142,12 +149,17 @@ enum WindowEngine { return try Window(pid: app.processIdentifier) } + /// Get the Window at a given position. + /// - Parameter position: The position to check for + /// - Returns: The window at the given position, if any static func windowAtPosition(_ position: CGPoint) throws -> Window? { + // If we can find the window at a point using the Accessibility API, return it if let element = try AXUIElement.systemWide.getElementAtPosition(position), let windowElement: AXUIElement = try element.getValue(.window) { return try Window(element: windowElement) } + // If the previous method didn't work, loop through all windows on-screen and return the first one that contains the desired point let windowList = WindowEngine.windowList if let window = (windowList.first { $0.frame.contains(position) }) { return window @@ -156,6 +168,7 @@ enum WindowEngine { return nil } + /// Get a list of all windows currently shown, that are likely to be resizable by Loop. static var windowList: [Window] { guard let list = CGWindowListCopyWindowInfo( [.optionOnScreenOnly, .excludeDesktopElements], @@ -166,19 +179,20 @@ enum WindowEngine { var windowList: [Window] = [] for window in list { - if let pid = window[kCGWindowOwnerPID as String] as? Int32 { - do { - let window = try Window(pid: pid) - windowList.append(window) - } catch { - print("Failed to create window: \(error.localizedDescription)") - } + if let pid = window[kCGWindowOwnerPID as String] as? Int32, let window = try? Window(pid: pid) { + windowList.append(window) } } return windowList } + /// This function is used to calculate the Y offset for a window to be "macOS centered" on the screen + /// It is identical to `NSWindow.center()`. + /// - Parameters: + /// - windowHeight: Height of the window to be resized + /// - screenHeight: Height of the screen the window will be resized on + /// - Returns: The Y offset of the window, to be added onto the screen's midY point. static func getMacOSCenterYOffset(_ windowHeight: CGFloat, screenHeight: CGFloat) -> CGFloat { let halfScreenHeight = screenHeight / 2 let windowHeightPercent = windowHeight / screenHeight From 5c9e89a6516cb2e06c394eb9d9d43474f85d7094 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 14:46:51 -0600 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=8E=A8=20Format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/Window.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index e632bc91..49200d68 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -28,7 +28,7 @@ class Window { let nsRunningApplication: NSRunningApplication? var observer: Observer? - + /// Initialize a window from an AXUIElement /// - Parameter element: The AXUIElement to initialize the window with. If it is not a window, an error will be thrown init(element: AXUIElement) throws { @@ -52,7 +52,7 @@ class Window { throw WindowError.invalidWindow } } - + /// Initialize a window from a PID. The frontmost app with the given PID will be used. /// - Parameter pid: The PID of the app to get the window from convenience init(pid: pid_t) throws { @@ -129,7 +129,6 @@ class Window { } } - /// Activate the window. This will bring it to the front and focus it if possible func activate() { do { @@ -274,7 +273,7 @@ class Window { var frame: CGRect { CGRect(origin: self.position, size: self.size) } - + /// Set the frame of this Window. /// - Parameters: /// - rect: The new frame for the window From 00be871fc0673ccd1232b6590fdad13c14a21464 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Sun, 15 Sep 2024 15:00:48 -0600 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20Improve=20handling=20of=20when?= =?UTF-8?q?=20resizing=20own=20windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/WindowEngine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index baa59615..c32cfbd5 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -72,7 +72,7 @@ enum WindowEngine { // If the window is one of Loop's windows, resize it using the actual NSWindow, preventing crashes if window.nsRunningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier, - let window = NSApp.keyWindow ?? NSApp.windows.first { + let window = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.level.rawValue <= NSWindow.Level.floating.rawValue }) { NSAnimationContext.runAnimationGroup { context in context.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 1, 0.68, 1) window.animator().setFrame(targetFrame.flipY(screen: .screens[0]), display: false)