From 3d8d37306e39db5c8eb41fafd95a13112d573f62 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 11 Jun 2026 22:24:39 +0800
Subject: [PATCH] Center filtered Waterfall columns
---
C1.source/Sources/Aligner/QuickSwitchSessionController.swift | 303 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 294 insertions(+), 9 deletions(-)
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 11aa41d..03e9501 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -15,6 +15,7 @@
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
@@ -30,7 +31,13 @@
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 pendingLockedSpaceUnlockSpaceID: UInt64?
+ private var pendingLockedSpaceUnlockTask: Task<Void, Never>?
private var lastSnapshotError: String?
private var snapshotTask: Task<Void, Never>?
private var screenshotTask: Task<Void, Never>?
@@ -48,6 +55,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?
@@ -102,6 +114,7 @@
windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService(
spaceIDsByWindowIDProvider: { _ in [:] }
),
+ spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(),
windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService(
spaceIDsByWindowIDProvider: { _ in [:] }
),
@@ -118,6 +131,7 @@
self.snapshotLoader = snapshotLoader
self.screenshotProvider = screenshotProvider
self.windowActivationService = windowActivationService
+ self.spaceActivationService = spaceActivationService
self.windowCloseService = windowCloseService
self.systemCriticalWindowDetector = systemCriticalWindowDetector
self.disableScreenshotRefresh = disableScreenshotRefresh
@@ -151,6 +165,11 @@
lastActivationWindowID = nil
lastActivationResult = nil
lastActivationError = nil
+ lastSpaceActivationSpaceID = nil
+ lastSpaceActivationDidRequestFocus = nil
+ lastSpaceActivationDisplayIdentifier = nil
+ lastSpaceActivationPreviousCurrentSpaceID = nil
+ lastSpaceActivationError = nil
lastCloseTargetKind = nil
lastCloseAppGroupIndex = nil
lastCloseAppName = nil
@@ -159,6 +178,10 @@
lastCloseError = nil
pendingCloseVerification = nil
suppressedCloseTargets = []
+ cancelPendingLockedSpaceUnlock()
+ lockedSpaceFilterID = nil
+ selectionBeforeSpaceFilter = nil
+ lastSpaceFilterAction = nil
lastDismissReason = nil
lastSystemCriticalAction = nil
lastSystemCriticalMonitorSource = nil
@@ -180,6 +203,13 @@
view.onCommitSelection = { [weak self] selection, source in
self?.commitSelection(selection, source: source)
}
+ view.onSpaceLaneClick = { [weak self] spaceID, clickCount in
+ self?.handleSpaceLaneClick(spaceID, clickCount: clickCount)
+ }
+ view.onBackgroundClick = { [weak self] in
+ self?.handleBackgroundClick() ?? false
+ }
+ view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
view.onRequestClose = { [weak self] request in
self?.requestClose(request)
}
@@ -218,6 +248,7 @@
snapshotTask = nil
screenshotTask?.cancel()
screenshotTask = nil
+ cancelPendingLockedSpaceUnlock()
pendingCloseVerification = nil
view.clearCloseFeedback()
invalidateSystemCriticalMonitoring()
@@ -264,6 +295,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(),
@@ -275,6 +309,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(),
@@ -375,6 +414,7 @@
private func clearSnapshotState() {
currentSnapshot = nil
+ sourceViewModel = nil
currentViewModel = nil
lastSnapshotError = nil
}
@@ -446,7 +486,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 +502,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 +557,224 @@
"result": lastActivationResult.map(Self.activationResultString)
])
}
+ onSnapshotUpdated?()
+ }
+
+ 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", [
+ "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?()
+ }
+
+ @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?()
}
@@ -618,10 +880,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 +945,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 +1018,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 +1050,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 +1090,7 @@
}
case .failure(let failure):
currentSnapshot = nil
+ sourceViewModel = nil
currentViewModel = nil
lastSnapshotError = failure.message
snapshotStartElapsedMilliseconds = failure.timing.startElapsedMilliseconds
@@ -1169,6 +1452,7 @@
let fixtureMultiDisplay: Bool
let fixtureCandidateFiltering: Bool
let fixtureActivation: Bool
+ let fixtureSpaceFilter: Bool
let debugHoveredAppGroupIndex: Int?
let debugOverlayWidth: CGFloat?
let debugKeySequence: [String]
@@ -1195,6 +1479,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