diff --git a/Loop/Core/LoopManager.swift b/Loop/Core/LoopManager.swift index 5a379e98..21dc8a0d 100644 --- a/Loop/Core/LoopManager.swift +++ b/Loop/Core/LoopManager.swift @@ -58,7 +58,7 @@ final class LoopManager: ObservableObject { private var screenToResizeOn: NSScreen? var isShiftKeyPressed: Bool = false - @Published var currentAction: WindowAction = .init(.noAction) + @Published var currentAction: WindowAction = .init(.noSelection) private var parentCycleAction: WindowAction? private(set) var initialMousePosition: CGPoint = .zero @@ -123,11 +123,11 @@ extension LoopManager { await AccentColorController.shared.refresh() } - currentAction = .init(.noAction) + currentAction = .init(.noSelection) targetWindow = window parentCycleAction = nil initialMousePosition = NSEvent.mouseLocation - screenToResizeOn = Defaults[.useScreenWithCursor] ? NSScreen.screenWithMouse : NSScreen.main + screenToResizeOn = nil // Screen to resize on will be determined by the first action. isShiftKeyPressed = false if !Defaults[.disableCursorInteraction] { @@ -168,7 +168,7 @@ extension LoopManager { if let targetWindow, let screenToResizeOn, forceClose == false, - currentAction.direction != .noAction, !currentAction.direction.willFocusWindow { + !currentAction.direction.willFocusWindow { if Defaults[.previewVisibility] { WindowEngine.resize( targetWindow, @@ -231,9 +231,12 @@ extension LoopManager { canAdvanceCycle: Bool = true ) { guard - currentAction.id != newAction.id || newAction.shouldImmediatelyExecuteAction, isLoopActive, - let currentScreen = screenToResizeOn + currentAction.id != newAction.id || newAction.shouldImmediatelyExecuteAction, + let currentScreen = screenToResizeOn ?? resolveAndStoreTargetScreen( + action: newAction, + window: targetWindow + ) else { return } @@ -308,7 +311,7 @@ extension LoopManager { newScreen = bottomScreen } - if currentAction.direction == .noAction { + if currentAction.direction == .noSelection { if let targetWindow { let screenSwitchingCustomActionName = "autogenerated_screen_switching_action" @@ -360,8 +363,7 @@ extension LoopManager { currentAction = newAction changeAction(parentCycleAction, triggeredFromScreenChange: true) } else { - if let screenToResizeOn, - let window = targetWindow, + if let window = targetWindow, !Defaults[.previewVisibility] { if !disableHapticFeedback { performHapticFeedback() @@ -370,7 +372,7 @@ extension LoopManager { WindowEngine.resize( window, to: currentAction, - on: screenToResizeOn, + on: newScreen, shouldRecord: false ) } @@ -453,14 +455,13 @@ extension LoopManager { var currentIndex: Int? = nil if Defaults[.cycleModeRestartEnabled], - currentAction.direction == .noAction || - !currentCycle.contains(currentAction) { + currentAction.direction == .noSelection || !currentCycle.contains(currentAction) { return currentCycle[0] } - // If the current action is noAction, we can preserve the index from the last action. + // If the current action is noSelection, we can preserve the index from the last action. // This would initially be done by reading the window's records, then would continue by finding the next index from the currentAction. - if currentAction.direction == .noAction, + if currentAction.direction == .noSelection, !currentCycle.contains(currentAction), let window = targetWindow, let latestRecord = WindowRecords.getCurrentAction(for: window) { @@ -495,4 +496,25 @@ extension LoopManager { ) } } + + /// Resolves the target screen for `screenToResizeOn`. + /// + /// By default, this uses the user's `useScreenWithCursor` setting. + /// For actions that move windows between screens, the screen containing the window is preferred to ensure deterministic behavior. + /// - Parameters: + /// - action: The window action being performed. + /// - window: The window to be resized, if any. + /// - Returns: The screen the window should be on after the action. + private func resolveAndStoreTargetScreen(action: WindowAction, window: Window?) -> NSScreen? { + var targetScreen = Defaults[.useScreenWithCursor] ? NSScreen.screenWithMouse : NSScreen.main + + if action.direction.willChangeScreen, + let window, + let screen = ScreenUtility.screenContaining(window) { + targetScreen = screen + } + + screenToResizeOn = targetScreen + return targetScreen + } } diff --git a/Loop/Stashing/StashManager.swift b/Loop/Stashing/StashManager.swift index 35d89d41..397ad5e5 100644 --- a/Loop/Stashing/StashManager.swift +++ b/Loop/Stashing/StashManager.swift @@ -106,10 +106,12 @@ final class StashManager { /// - Returns: `true` if the action is handled by the StashManager and the normal flow should be bypassed; otherwise, `false`. @discardableResult func handleIfStashed(_ action: WindowAction, screen: NSScreen) -> Bool { - guard action.direction == .stash else { return false } - guard let stashedWindow = store.stashedWindow(for: action, on: screen) else { return false } - guard !stashedWindow.window.isWindowHidden, !stashedWindow.window.isApplicationHidden else { return false } - guard stashedWindow.screen.isSameScreen(screen) else { return false } + guard action.direction == .stash, + let stashedWindow = store.stashedWindow(for: action, on: screen), + !stashedWindow.window.isWindowHidden, !stashedWindow.window.isApplicationHidden + else { + return false + } Log.info("Intercepting window action for stashed window \(stashedWindow.window.description)", category: .stashManager) diff --git a/Loop/Stashing/StashedWindowStore.swift b/Loop/Stashing/StashedWindowStore.swift index 8e38bbc9..c7634a81 100644 --- a/Loop/Stashing/StashedWindowStore.swift +++ b/Loop/Stashing/StashedWindowStore.swift @@ -49,12 +49,7 @@ final class StashedWindowsStore { /// Return the stashed window that match the given `action` and `screen` func stashedWindow(for action: WindowAction, on screen: NSScreen) -> StashedWindow? { - for stashedWindow in stashed.values { - if stashedWindow.action.id == action.id, stashedWindow.screen.isSameScreen(screen) { - return stashedWindow - } - } - return nil + stashed.values.first { $0.action.id == action.id && $0.screen.isSameScreen(screen) } } // MARK: Private methods