From 81b5a5f847e74fe1106829b13db2cccda8dd5798 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 19:48:59 +0800
Subject: [PATCH] Fix horizontal app index key handling

---
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |  252 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 246 insertions(+), 6 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 5a36765..18d84f0 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -15,9 +15,11 @@
     private let snapshotLoader: any QuickSwitchSnapshotLoading
     private let screenshotProvider: any ScreenshotProviderProtocol
     private let windowActivationService: any WindowActivationServiceProtocol
+    private let spaceActivationService: any SpaceActivationServiceProtocol
     private let windowCloseService: any WindowCloseServiceProtocol
     private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting
     private let disableScreenshotRefresh: Bool
+    private var waterfallViewMode: QuickSwitchWaterfallViewMode
     private var closeConfirmationRequired: Bool
     private let onCloseConfirmationDisabled: (() -> Void)?
     private let debugOverlayWidth: CGFloat?
@@ -35,6 +37,8 @@
     private var lockedSpaceFilterID: UInt64?
     private var selectionBeforeSpaceFilter: QuickSwitchSelection?
     private var lastSpaceFilterAction: String?
+    private var pendingLockedSpaceUnlockSpaceID: UInt64?
+    private var pendingLockedSpaceUnlockTask: Task<Void, Never>?
     private var lastSnapshotError: String?
     private var snapshotTask: Task<Void, Never>?
     private var screenshotTask: Task<Void, Never>?
@@ -52,6 +56,11 @@
     private var lastActivationWindowID: UInt32?
     private var lastActivationResult: WindowActivationResult?
     private var lastActivationError: String?
+    private var lastSpaceActivationSpaceID: UInt64?
+    private var lastSpaceActivationDidRequestFocus: Bool?
+    private var lastSpaceActivationDisplayIdentifier: String?
+    private var lastSpaceActivationPreviousCurrentSpaceID: UInt64?
+    private var lastSpaceActivationError: String?
     private var lastCloseTargetKind: String?
     private var lastCloseAppGroupIndex: Int?
     private var lastCloseAppName: String?
@@ -77,6 +86,7 @@
     private var lifecycleResidualOverlayWindows = 0
     private var lifecycleResidualVisibleOverlayWindows = 0
     var onSnapshotUpdated: (() -> Void)?
+    var onToggleWaterfallViewMode: (() -> Void)?
 
     private struct PendingCloseVerification {
         let request: QuickSwitchCloseRequest
@@ -106,11 +116,13 @@
         windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService(
             spaceIDsByWindowIDProvider: { _ in [:] }
         ),
+        spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(),
         windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService(
             spaceIDsByWindowIDProvider: { _ in [:] }
         ),
         systemCriticalWindowDetector: any SystemCriticalWindowDetecting = CGWindowSystemCriticalWindowDetector(),
         disableScreenshotRefresh: Bool = false,
+        waterfallViewMode: QuickSwitchWaterfallViewMode = .verticalColumns,
         closeConfirmationRequired: Bool = true,
         onCloseConfirmationDisabled: (() -> Void)? = nil,
         debugHoveredAppGroupIndex: Int? = nil,
@@ -122,9 +134,11 @@
         self.snapshotLoader = snapshotLoader
         self.screenshotProvider = screenshotProvider
         self.windowActivationService = windowActivationService
+        self.spaceActivationService = spaceActivationService
         self.windowCloseService = windowCloseService
         self.systemCriticalWindowDetector = systemCriticalWindowDetector
         self.disableScreenshotRefresh = disableScreenshotRefresh
+        self.waterfallViewMode = waterfallViewMode
         self.closeConfirmationRequired = closeConfirmationRequired
         self.onCloseConfirmationDisabled = onCloseConfirmationDisabled
         self.debugOverlayWidth = debugOverlayWidth
@@ -155,6 +169,11 @@
         lastActivationWindowID = nil
         lastActivationResult = nil
         lastActivationError = nil
+        lastSpaceActivationSpaceID = nil
+        lastSpaceActivationDidRequestFocus = nil
+        lastSpaceActivationDisplayIdentifier = nil
+        lastSpaceActivationPreviousCurrentSpaceID = nil
+        lastSpaceActivationError = nil
         lastCloseTargetKind = nil
         lastCloseAppGroupIndex = nil
         lastCloseAppName = nil
@@ -163,6 +182,7 @@
         lastCloseError = nil
         pendingCloseVerification = nil
         suppressedCloseTargets = []
+        cancelPendingLockedSpaceUnlock()
         lockedSpaceFilterID = nil
         selectionBeforeSpaceFilter = nil
         lastSpaceFilterAction = nil
@@ -187,9 +207,16 @@
         view.onCommitSelection = { [weak self] selection, source in
             self?.commitSelection(selection, source: source)
         }
-        view.onSpaceLaneClick = { [weak self] spaceID in
-            self?.handleSpaceLaneClick(spaceID)
+        view.onSpaceLaneClick = { [weak self] spaceID, clickCount in
+            self?.handleSpaceLaneClick(spaceID, clickCount: clickCount)
         }
+        view.onBackgroundClick = { [weak self] in
+            self?.handleBackgroundClick() ?? false
+        }
+        view.onToggleWaterfallViewMode = { [weak self] in
+            self?.onToggleWaterfallViewMode?()
+        }
+        view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
         view.onRequestClose = { [weak self] request in
             self?.requestClose(request)
         }
@@ -199,6 +226,7 @@
             self.onCloseConfirmationDisabled?()
         }
         view.setCloseConfirmationRequired(closeConfirmationRequired)
+        view.setWaterfallViewMode(waterfallViewMode)
         view.apply(viewModel: nil)
         view.beginPerformanceFirstFrameMeasurement()
         coordinator.openQuickSwitch()
@@ -228,6 +256,7 @@
         snapshotTask = nil
         screenshotTask?.cancel()
         screenshotTask = nil
+        cancelPendingLockedSpaceUnlock()
         pendingCloseVerification = nil
         view.clearCloseFeedback()
         invalidateSystemCriticalMonitoring()
@@ -251,7 +280,44 @@
 
     func retriggerFromShortcut() {
         DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut")
-        show()
+        guard isVisible else {
+            show()
+            return
+        }
+
+        guard currentViewModel != nil else {
+            DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.noSnapshot", [
+                "action": "fallbackShow"
+            ])
+            show()
+            return
+        }
+
+        let didCycle = view.hoverNextAppGroupIndex()
+        DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.hoverApp", [
+            "didCycle": didCycle
+        ])
+
+        if !didCycle {
+            show()
+        }
+    }
+
+    func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
+        guard waterfallViewMode != mode else { return }
+
+        waterfallViewMode = mode
+        DevelopmentDiagnostics.log("quickSwitch.session.waterfallViewModeChanged", [
+            "mode": mode.rawValue,
+            "visible": isVisible,
+            "hasViewModel": currentViewModel != nil
+        ])
+
+        view.setWaterfallViewMode(mode)
+        if isVisible, let currentViewModel {
+            view.applyProjected(viewModel: currentViewModel)
+        }
+        onSnapshotUpdated?()
     }
 
     func reportDictionary() -> [String: Any] {
@@ -288,6 +354,11 @@
             "lastActivationWindowID": lastActivationWindowID ?? NSNull(),
             "lastActivationResult": lastActivationResult.map(Self.activationResultString) ?? NSNull(),
             "lastActivationError": lastActivationError ?? NSNull(),
+            "lastSpaceActivationSpaceID": lastSpaceActivationSpaceID ?? NSNull(),
+            "lastSpaceActivationDidRequestFocus": lastSpaceActivationDidRequestFocus ?? NSNull(),
+            "lastSpaceActivationDisplayIdentifier": lastSpaceActivationDisplayIdentifier ?? NSNull(),
+            "lastSpaceActivationPreviousCurrentSpaceID": lastSpaceActivationPreviousCurrentSpaceID ?? NSNull(),
+            "lastSpaceActivationError": lastSpaceActivationError ?? NSNull(),
             "lastCloseTargetKind": lastCloseTargetKind ?? NSNull(),
             "lastCloseAppGroupIndex": lastCloseAppGroupIndex ?? NSNull(),
             "lastCloseAppName": lastCloseAppName ?? NSNull(),
@@ -534,7 +605,31 @@
         onSnapshotUpdated?()
     }
 
-    private func handleSpaceLaneClick(_ spaceID: UInt64) {
+    private func handleSpaceLaneClick(_ spaceID: UInt64, clickCount: Int) {
+        if clickCount >= 2 {
+            if pendingLockedSpaceUnlockSpaceID == spaceID {
+                cancelPendingLockedSpaceUnlock()
+                activateLockedSpace(spaceID)
+            } else {
+                DevelopmentDiagnostics.log("quickSwitch.spaceFilter.doubleClick.ignored", [
+                    "spaceID": spaceID,
+                    "lockedSpaceID": lockedSpaceFilterID ?? NSNull(),
+                    "pendingUnlockSpaceID": pendingLockedSpaceUnlockSpaceID ?? NSNull()
+                ])
+            }
+            return
+        }
+
+        cancelPendingLockedSpaceUnlock()
+        if lockedSpaceFilterID == spaceID {
+            scheduleLockedSpaceUnlock(spaceID)
+            return
+        }
+
+        applySpaceLaneSingleClick(spaceID)
+    }
+
+    private func applySpaceLaneSingleClick(_ spaceID: UInt64) {
         guard let sourceViewModel else {
             lastSpaceFilterAction = "blockedNoSourceViewModel"
             DevelopmentDiagnostics.log("quickSwitch.spaceFilter.click.blocked", [
@@ -605,6 +700,129 @@
         onSnapshotUpdated?()
     }
 
+    @discardableResult
+    private func handleBackgroundClick() -> Bool {
+        guard lockedSpaceFilterID != nil else { return false }
+
+        cancelPendingLockedSpaceUnlock()
+        let unlocked = unlockSpaceFilter(action: "unlockBackground")
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.backgroundUnlock", [
+            "unlocked": unlocked
+        ])
+        return unlocked
+    }
+
+    private func scheduleLockedSpaceUnlock(_ spaceID: UInt64) {
+        pendingLockedSpaceUnlockSpaceID = spaceID
+        let delay = max(0.12, NSEvent.doubleClickInterval + 0.03)
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.unlock.pending", [
+            "spaceID": spaceID,
+            "delay": delay
+        ])
+
+        pendingLockedSpaceUnlockTask = Task { @MainActor [weak self] in
+            try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+            guard !Task.isCancelled,
+                  let self,
+                  self.pendingLockedSpaceUnlockSpaceID == spaceID
+            else {
+                return
+            }
+
+            self.pendingLockedSpaceUnlockSpaceID = nil
+            self.pendingLockedSpaceUnlockTask = nil
+            self.applySpaceLaneSingleClick(spaceID)
+        }
+    }
+
+    private func cancelPendingLockedSpaceUnlock() {
+        pendingLockedSpaceUnlockTask?.cancel()
+        pendingLockedSpaceUnlockTask = nil
+        pendingLockedSpaceUnlockSpaceID = nil
+    }
+
+    @discardableResult
+    private func unlockSpaceFilter(action: String) -> Bool {
+        guard let sourceViewModel, lockedSpaceFilterID != nil else {
+            return false
+        }
+
+        lockedSpaceFilterID = nil
+        let preferredSelection = selectionBeforeSpaceFilter
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = action
+
+        let visibleViewModel = projectedViewModel(
+            from: sourceViewModel,
+            preferredSelection: preferredSelection
+        )
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+
+        if disableScreenshotRefresh {
+            view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
+            screenshotTask?.cancel()
+            screenshotTask = nil
+        } else {
+            scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
+        }
+
+        onSnapshotUpdated?()
+        return true
+    }
+
+    private func activateLockedSpace(_ spaceID: UInt64) {
+        guard lockedSpaceFilterID == spaceID else {
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.ignored", [
+                "spaceID": spaceID,
+                "lockedSpaceID": lockedSpaceFilterID ?? NSNull()
+            ])
+            return
+        }
+
+        guard let sourceViewModel else {
+            lastSpaceFilterAction = "blockedNoSourceViewModel"
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.blocked", [
+                "spaceID": spaceID,
+                "reason": "noSourceViewModel"
+            ])
+            onSnapshotUpdated?()
+            return
+        }
+
+        lockedSpaceFilterID = nil
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = "activateSpace"
+        lastSpaceActivationSpaceID = spaceID
+        lastSpaceActivationDidRequestFocus = nil
+        lastSpaceActivationDisplayIdentifier = nil
+        lastSpaceActivationPreviousCurrentSpaceID = nil
+        lastSpaceActivationError = nil
+
+        let visibleViewModel = projectedViewModel(from: sourceViewModel)
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.start", [
+            "spaceID": spaceID
+        ])
+        hide(reason: .userClosed)
+        let outcome = spaceActivationService.activate(spaceID: spaceID)
+        lastSpaceActivationSpaceID = outcome.targetSpaceID
+        lastSpaceActivationDidRequestFocus = outcome.didRequestFocus
+        lastSpaceActivationDisplayIdentifier = outcome.displayIdentifier
+        lastSpaceActivationPreviousCurrentSpaceID = outcome.previousCurrentSpaceID
+        lastSpaceActivationError = outcome.error
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.result", [
+            "spaceID": outcome.targetSpaceID,
+            "displayIdentifier": outcome.displayIdentifier ?? NSNull(),
+            "previousCurrentSpaceID": outcome.previousCurrentSpaceID ?? NSNull(),
+            "didRequestFocus": outcome.didRequestFocus,
+            "error": outcome.error ?? NSNull()
+        ])
+        onSnapshotUpdated?()
+    }
+
     private func requestClose(_ request: QuickSwitchCloseRequest) {
         DevelopmentDiagnostics.log("quickSwitch.close.request.start", [
             "targetKind": request.kindDescription,
@@ -646,12 +864,12 @@
                 "result": Self.closeResultString(result)
             ])
             if result == .requested {
-                suppressCloseTarget(request)
                 DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [
                     "targetKind": request.kindDescription,
                     "appGroupIndex": request.appGroupIndex,
                     "appName": request.appName,
-                    "windowID": request.windowID
+                    "windowID": request.windowID,
+                    "optimisticRemove": false
                 ])
                 pendingCloseVerification = PendingCloseVerification(
                     request: request,
@@ -1280,6 +1498,8 @@
     let fixtureCandidateFiltering: Bool
     let fixtureActivation: Bool
     let fixtureSpaceFilter: Bool
+    let fixtureSplitView: Bool
+    let fixtureMultiPageIdentity: Bool
     let debugHoveredAppGroupIndex: Int?
     let debugOverlayWidth: CGFloat?
     let debugKeySequence: [String]
@@ -1288,6 +1508,7 @@
     let debugWindowActivation: Bool
     let debugWindowClose: Bool
     let disableScreenshotRefresh: Bool
+    let waterfallViewMode: QuickSwitchWaterfallViewMode?
     let lifecycleCycles: Int?
     let lifecycleInterval: TimeInterval
     let lifecycleVisibleDuration: TimeInterval
@@ -1307,6 +1528,8 @@
             fixtureCandidateFiltering: arguments.contains("--round01-fixture-candidate-filtering"),
             fixtureActivation: arguments.contains("--round01-fixture-window-activation"),
             fixtureSpaceFilter: arguments.contains("--round01-fixture-space-filter"),
+            fixtureSplitView: arguments.contains("--round01-fixture-split-view"),
+            fixtureMultiPageIdentity: arguments.contains("--round01-fixture-multi-page-identity"),
             debugHoveredAppGroupIndex: intValue(for: "--round01-debug-hover-app-index", in: arguments),
             debugOverlayWidth: cgFloatValue(for: "--round01-debug-overlay-width", in: arguments),
             debugKeySequence: stringListValue(for: "--round01-debug-key-sequence", in: arguments),
@@ -1315,6 +1538,7 @@
             debugWindowActivation: arguments.contains("--round01-debug-window-activation"),
             debugWindowClose: arguments.contains("--round01-debug-window-close"),
             disableScreenshotRefresh: arguments.contains("--round01-disable-screenshot-refresh"),
+            waterfallViewMode: waterfallViewModeValue(for: "--round01-waterfall-view-mode", in: arguments),
             lifecycleCycles: intValue(for: "--round01-quick-switch-lifecycle-cycles", in: arguments),
             lifecycleInterval: timeInterval(for: "--round01-quick-switch-lifecycle-interval", in: arguments) ?? 0.02,
             lifecycleVisibleDuration: timeInterval(for: "--round01-quick-switch-lifecycle-visible-duration", in: arguments) ?? 0.16
@@ -1339,6 +1563,22 @@
         return String(argument.dropFirst(prefix.count))
     }
 
+    private static func waterfallViewModeValue(
+        for key: String,
+        in arguments: [String]
+    ) -> QuickSwitchWaterfallViewMode? {
+        guard let value = stringValue(for: key, in: arguments) else { return nil }
+
+        switch value {
+        case "vertical", "vertical-columns", "verticalColumns":
+            return .verticalColumns
+        case "horizontal", "horizontal-masonry", "horizontalMasonry":
+            return .horizontalMasonry
+        default:
+            return nil
+        }
+    }
+
     private static func intValue(for key: String, in arguments: [String]) -> Int? {
         stringValue(for: key, in: arguments).flatMap(Int.init)
     }

--
Gitblit v1.9.3