From 0045898e8cd7b2d8972bb24c8ac27e514bdd8c70 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 11 Jun 2026 16:46:31 +0800
Subject: [PATCH] Implement Quick Switch Space filtering

---
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |  130 ++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 121 insertions(+), 9 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 11aa41d..5a36765 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -30,7 +30,11 @@
         debugOverlayWidth: debugOverlayWidth
     )
     private var currentSnapshot: QuickSwitchSnapshot?
+    private var sourceViewModel: QuickSwitchViewModel?
     private var currentViewModel: QuickSwitchViewModel?
+    private var lockedSpaceFilterID: UInt64?
+    private var selectionBeforeSpaceFilter: QuickSwitchSelection?
+    private var lastSpaceFilterAction: String?
     private var lastSnapshotError: String?
     private var snapshotTask: Task<Void, Never>?
     private var screenshotTask: Task<Void, Never>?
@@ -159,6 +163,9 @@
         lastCloseError = nil
         pendingCloseVerification = nil
         suppressedCloseTargets = []
+        lockedSpaceFilterID = nil
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = nil
         lastDismissReason = nil
         lastSystemCriticalAction = nil
         lastSystemCriticalMonitorSource = nil
@@ -179,6 +186,9 @@
         }
         view.onCommitSelection = { [weak self] selection, source in
             self?.commitSelection(selection, source: source)
+        }
+        view.onSpaceLaneClick = { [weak self] spaceID in
+            self?.handleSpaceLaneClick(spaceID)
         }
         view.onRequestClose = { [weak self] request in
             self?.requestClose(request)
@@ -264,6 +274,9 @@
             "initialSelectionWindowID": currentViewModel?.initialSelection?.windowID ?? NSNull(),
             "appShelfNames": currentViewModel?.appShelf.map(\.app.name) ?? [],
             "spaceLabels": currentViewModel?.displays.flatMap { $0.spaces.map(\.label) } ?? [],
+            "spaceFilterLockedSpaceID": lockedSpaceFilterID ?? NSNull(),
+            "spaceFilterActive": lockedSpaceFilterID != nil,
+            "lastSpaceFilterAction": lastSpaceFilterAction ?? NSNull(),
             "overlayOpenElapsedMilliseconds": overlayOpenElapsedMilliseconds ?? NSNull(),
             "snapshotStartElapsedMilliseconds": snapshotStartElapsedMilliseconds ?? NSNull(),
             "snapshotDurationMilliseconds": snapshotDurationMilliseconds ?? NSNull(),
@@ -375,6 +388,7 @@
 
     private func clearSnapshotState() {
         currentSnapshot = nil
+        sourceViewModel = nil
         currentViewModel = nil
         lastSnapshotError = nil
     }
@@ -446,7 +460,11 @@
         onSnapshotUpdated?()
     }
 
-    private func commitSelection(_ selection: QuickSwitchSelection, source: QuickSwitchCommitSource) {
+    private func commitSelection(
+        _ selection: QuickSwitchSelection,
+        source: QuickSwitchCommitSource,
+        in viewModel: QuickSwitchViewModel? = nil
+    ) {
         DevelopmentDiagnostics.log("quickSwitch.activation.commit.start", [
             "source": source.rawValue,
             "appGroupIndex": selection.appGroupIndex,
@@ -458,7 +476,7 @@
         lastActivationWindowID = selection.windowID
         lastActivationError = nil
 
-        guard let window = window(for: selection) else {
+        guard let window = window(for: selection, in: viewModel ?? currentViewModel) else {
             lastActivationResult = .windowNotFound
             DevelopmentDiagnostics.log("quickSwitch.activation.commit.windowNotFound", [
                 "source": source.rawValue,
@@ -513,6 +531,77 @@
                 "result": lastActivationResult.map(Self.activationResultString)
             ])
         }
+        onSnapshotUpdated?()
+    }
+
+    private func handleSpaceLaneClick(_ spaceID: UInt64) {
+        guard let sourceViewModel else {
+            lastSpaceFilterAction = "blockedNoSourceViewModel"
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.click.blocked", [
+                "spaceID": spaceID,
+                "reason": "noSourceViewModel"
+            ])
+            onSnapshotUpdated?()
+            return
+        }
+
+        if let fullscreenSelection = QuickSwitchSpaceFilterPolicy.singleFullscreenSelection(
+            inSpaceID: spaceID,
+            viewModel: sourceViewModel
+        ) {
+            lockedSpaceFilterID = nil
+            selectionBeforeSpaceFilter = nil
+            lastSpaceFilterAction = "activateSingleFullscreen"
+            let visibleViewModel = projectedViewModel(from: sourceViewModel)
+            currentViewModel = visibleViewModel
+            view.applyProjected(viewModel: visibleViewModel)
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.singleFullscreenActivate", [
+                "spaceID": spaceID,
+                "windowID": fullscreenSelection.windowID
+            ])
+            commitSelection(fullscreenSelection, source: .mouse, in: sourceViewModel)
+            return
+        }
+
+        let preferredSelection: QuickSwitchSelection?
+        if lockedSpaceFilterID == spaceID {
+            lockedSpaceFilterID = nil
+            preferredSelection = selectionBeforeSpaceFilter
+            selectionBeforeSpaceFilter = nil
+            lastSpaceFilterAction = "unlock"
+        } else {
+            if lockedSpaceFilterID == nil {
+                selectionBeforeSpaceFilter = view.effectiveSelectionForProjection()
+                lastSpaceFilterAction = "lock"
+            } else {
+                lastSpaceFilterAction = "switch"
+            }
+            lockedSpaceFilterID = spaceID
+            preferredSelection = nil
+        }
+
+        let visibleViewModel = projectedViewModel(
+            from: sourceViewModel,
+            preferredSelection: preferredSelection
+        )
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.apply", [
+            "spaceID": spaceID,
+            "lockedSpaceID": lockedSpaceFilterID,
+            "action": lastSpaceFilterAction,
+            "visibleAppCount": visibleViewModel.appShelf.count,
+            "visibleWindowCount": visibleViewModel.windowCount
+        ])
+
+        if disableScreenshotRefresh {
+            view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
+            screenshotTask?.cancel()
+            screenshotTask = nil
+        } else {
+            scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
+        }
+
         onSnapshotUpdated?()
     }
 
@@ -618,10 +707,10 @@
             suppressedCloseTargets.append(target)
         }
 
-        guard let currentViewModel else { return }
-        let visibleViewModel = viewModel(currentViewModel, excluding: suppressedCloseTargets)
+        guard let sourceViewModel else { return }
+        let visibleViewModel = projectedViewModel(from: sourceViewModel, preferredSelection: view.effectiveSelectionForProjection())
         self.currentViewModel = visibleViewModel
-        view.apply(viewModel: visibleViewModel)
+        view.applyProjected(viewModel: visibleViewModel)
         DevelopmentDiagnostics.log("quickSwitch.close.optimisticRemove", [
             "targetKind": request.kindDescription,
             "appGroupIndex": request.appGroupIndex,
@@ -683,7 +772,20 @@
             displays: viewModel.displays,
             appShelf: appShelf,
             waterfallColumns: columns,
-            initialSelection: initialSelection(in: columns, fallback: viewModel.initialSelection)
+            initialSelection: initialSelection(in: columns, fallback: viewModel.initialSelection),
+            lockedSpaceID: viewModel.lockedSpaceID
+        )
+    }
+
+    private func projectedViewModel(
+        from sourceViewModel: QuickSwitchViewModel,
+        preferredSelection: QuickSwitchSelection? = nil
+    ) -> QuickSwitchViewModel {
+        let closeProjectedViewModel = viewModel(sourceViewModel, excluding: suppressedCloseTargets)
+        return QuickSwitchSpaceFilterPolicy.projectedViewModel(
+            from: closeProjectedViewModel,
+            lockedSpaceID: lockedSpaceFilterID,
+            preferredSelection: preferredSelection
         )
     }
 
@@ -743,8 +845,11 @@
         }
     }
 
-    private func window(for selection: QuickSwitchSelection) -> AlignerWindow? {
-        currentViewModel?
+    private func window(
+        for selection: QuickSwitchSelection,
+        in viewModel: QuickSwitchViewModel?
+    ) -> AlignerWindow? {
+        viewModel?
             .waterfallColumns
             .flatMap(\.windows)
             .first { card in
@@ -772,7 +877,11 @@
         switch result {
         case .success(let success):
             currentSnapshot = success.snapshot
-            let visibleViewModel = viewModel(success.viewModel, excluding: suppressedCloseTargets)
+            sourceViewModel = success.viewModel
+            let visibleViewModel = projectedViewModel(
+                from: success.viewModel,
+                preferredSelection: view.effectiveSelectionForProjection()
+            )
             currentViewModel = visibleViewModel
             lastSnapshotError = nil
             snapshotStartElapsedMilliseconds = success.timing.startElapsedMilliseconds
@@ -808,6 +917,7 @@
             }
         case .failure(let failure):
             currentSnapshot = nil
+            sourceViewModel = nil
             currentViewModel = nil
             lastSnapshotError = failure.message
             snapshotStartElapsedMilliseconds = failure.timing.startElapsedMilliseconds
@@ -1169,6 +1279,7 @@
     let fixtureMultiDisplay: Bool
     let fixtureCandidateFiltering: Bool
     let fixtureActivation: Bool
+    let fixtureSpaceFilter: Bool
     let debugHoveredAppGroupIndex: Int?
     let debugOverlayWidth: CGFloat?
     let debugKeySequence: [String]
@@ -1195,6 +1306,7 @@
             fixtureMultiDisplay: arguments.contains("--round01-fixture-multi-display"),
             fixtureCandidateFiltering: arguments.contains("--round01-fixture-candidate-filtering"),
             fixtureActivation: arguments.contains("--round01-fixture-window-activation"),
+            fixtureSpaceFilter: arguments.contains("--round01-fixture-space-filter"),
             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),

--
Gitblit v1.9.3