From 6f2856fa70722dad7f7c69a3addaa05ea9fd6e94 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 22:36:43 +0800
Subject: [PATCH] Add two-key window shortcut codes

---
 C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift |  243 +++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 229 insertions(+), 14 deletions(-)

diff --git a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
index 49adc7b..18bc70a 100644
--- a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
+++ b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
@@ -1,3 +1,4 @@
+import CoreGraphics
 import Foundation
 
 public struct QuickSwitchViewModel: Equatable, Sendable {
@@ -5,17 +6,20 @@
     public let appShelf: [QuickSwitchAppShelfItemViewModel]
     public let waterfallColumns: [QuickSwitchWaterfallColumnViewModel]
     public let initialSelection: QuickSwitchSelection?
+    public let lockedSpaceID: UInt64?
 
     public init(
         displays: [QuickSwitchDisplayViewModel],
         appShelf: [QuickSwitchAppShelfItemViewModel],
         waterfallColumns: [QuickSwitchWaterfallColumnViewModel],
-        initialSelection: QuickSwitchSelection?
+        initialSelection: QuickSwitchSelection?,
+        lockedSpaceID: UInt64? = nil
     ) {
         self.displays = displays
         self.appShelf = appShelf
         self.waterfallColumns = waterfallColumns
         self.initialSelection = initialSelection
+        self.lockedSpaceID = lockedSpaceID
     }
 
     public var spaceCount: Int {
@@ -59,6 +63,7 @@
     public let windowCount: Int
     public let appCount: Int
     public let appNames: [String]
+    public let splitViewAppNames: [String]
 
     public init(
         id: UInt64,
@@ -69,7 +74,8 @@
         isCurrent: Bool = false,
         windowCount: Int,
         appCount: Int,
-        appNames: [String] = []
+        appNames: [String] = [],
+        splitViewAppNames: [String] = []
     ) {
         self.id = id
         self.displayUUID = displayUUID
@@ -80,6 +86,7 @@
         self.windowCount = windowCount
         self.appCount = appCount
         self.appNames = appNames
+        self.splitViewAppNames = splitViewAppNames
     }
 }
 
@@ -163,12 +170,14 @@
         let windowCountsBySpace = countWindowsBySpace(windowItems)
         let appCountsBySpace = countAppsBySpace(snapshot.appGroups)
         let appNamesBySpace = appNamesBySpace(snapshot.appGroups)
+        let splitViewAppNamesBySpace = splitViewAppNamesBySpace(snapshot.appGroups)
         let displays = displayViewModels(
             from: snapshot.displays,
             currentSpaceIDs: currentSpaceIDs,
             windowCountsBySpace: windowCountsBySpace,
             appCountsBySpace: appCountsBySpace,
-            appNamesBySpace: appNamesBySpace
+            appNamesBySpace: appNamesBySpace,
+            splitViewAppNamesBySpace: splitViewAppNamesBySpace
         )
         let appShelf = appShelfViewModels(from: snapshot.appGroups)
 
@@ -197,7 +206,8 @@
             displays: displays,
             appShelf: appShelf,
             waterfallColumns: columns,
-            initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot)
+            initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot),
+            lockedSpaceID: nil
         )
     }
 
@@ -206,10 +216,14 @@
         currentSpaceIDs: Set<UInt64>,
         windowCountsBySpace: [UInt64: Int],
         appCountsBySpace: [UInt64: Int],
-        appNamesBySpace: [UInt64: [String]]
+        appNamesBySpace: [UInt64: [String]],
+        splitViewAppNamesBySpace: [UInt64: [String]]
     ) -> [QuickSwitchDisplayViewModel] {
-        displays
+        let sortedDisplays = displays
             .sorted { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending }
+        let displayCount = sortedDisplays.count
+
+        return sortedDisplays
             .enumerated()
             .map { displayIndex, display in
                 let displayLabel = displayLabel(for: displayIndex)
@@ -220,12 +234,13 @@
                             id: space.id,
                             displayUUID: space.displayUUID,
                             index: space.index,
-                            label: "\(displayLabel)\(space.index)",
+                            label: spaceLabel(for: space, displayIndex: displayIndex, displayCount: displayCount),
                             type: space.type,
                             isCurrent: currentSpaceIDs.contains(space.id),
                             windowCount: windowCountsBySpace[space.id] ?? 0,
                             appCount: appCountsBySpace[space.id] ?? 0,
-                            appNames: appNamesBySpace[space.id] ?? []
+                            appNames: appNamesBySpace[space.id] ?? [],
+                            splitViewAppNames: splitViewAppNamesBySpace[space.id] ?? []
                         )
                     }
 
@@ -256,14 +271,13 @@
 
     private static func labelsBySpaceID(from displays: [AlignerDisplay]) -> [UInt64: String] {
         var labels: [UInt64: String] = [:]
-
-        for (displayIndex, display) in displays
+        let sortedDisplays = displays
             .sorted(by: { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending })
-            .enumerated()
-        {
-            let displayLabel = displayLabel(for: displayIndex)
+        let displayCount = sortedDisplays.count
+
+        for (displayIndex, display) in sortedDisplays.enumerated() {
             for space in display.spaces {
-                labels[space.id] = "\(displayLabel)\(space.index)"
+                labels[space.id] = spaceLabel(for: space, displayIndex: displayIndex, displayCount: displayCount)
             }
         }
 
@@ -303,6 +317,54 @@
         return namesBySpace
     }
 
+    private static func splitViewAppNamesBySpace(_ appGroups: [QuickSwitchAppGroup]) -> [UInt64: [String]] {
+        struct Candidate {
+            let appName: String
+            let windowID: UInt32
+            let frame: CGRect
+        }
+
+        var candidatesBySpace: [UInt64: [Candidate]] = [:]
+
+        for group in appGroups {
+            for item in group.windows {
+                guard item.window.isFullscreen,
+                      let primarySpaceID = item.primarySpaceID,
+                      let frame = item.window.frame,
+                      frame.width > 0,
+                      frame.height > 0
+                else {
+                    continue
+                }
+
+                candidatesBySpace[primarySpaceID, default: []].append(
+                    Candidate(
+                        appName: group.app.name,
+                        windowID: item.window.id,
+                        frame: frame
+                    )
+                )
+            }
+        }
+
+        return candidatesBySpace.compactMapValues { candidates in
+            guard candidates.count >= 2 else { return nil }
+
+            return candidates
+                .sorted { lhs, rhs in
+                    if lhs.frame.midX != rhs.frame.midX {
+                        return lhs.frame.midX < rhs.frame.midX
+                    }
+                    if lhs.frame.midY != rhs.frame.midY {
+                        return lhs.frame.midY < rhs.frame.midY
+                    }
+                    return lhs.windowID < rhs.windowID
+                }
+                .prefix(2)
+                .map(\.appName)
+        }
+    }
+
     private static func orderedUnique<T: Hashable>(_ values: [T]) -> [T] {
         var seen = Set<T>()
         var result: [T] = []
@@ -328,4 +390,157 @@
 
         return String(String.UnicodeScalarView(scalars))
     }
+
+    private static func spaceLabel(for space: AlignerSpace, displayIndex: Int, displayCount: Int) -> String {
+        if displayCount <= 1 {
+            return String(format: "%02d", space.index)
+        }
+
+        return "\(displayLabel(for: displayIndex))\(space.index)"
+    }
+}
+
+public enum QuickSwitchSpaceFilterPolicy {
+    public static func projectedViewModel(
+        from viewModel: QuickSwitchViewModel,
+        lockedSpaceID: UInt64?,
+        preferredSelection: QuickSwitchSelection? = nil
+    ) -> QuickSwitchViewModel {
+        guard let lockedSpaceID else {
+            return replacingInitialSelection(
+                in: viewModel,
+                lockedSpaceID: nil,
+                preferredSelection: preferredSelection
+            )
+        }
+
+        var nextGlobalIndex = 0
+        let columns = viewModel.waterfallColumns.compactMap { column -> QuickSwitchWaterfallColumnViewModel? in
+            let windows = column.windows.filter { $0.primarySpaceID == lockedSpaceID }
+            guard !windows.isEmpty else { return nil }
+
+            let reindexedWindows = windows.enumerated().map { windowIndex, card in
+                defer { nextGlobalIndex += 1 }
+                return QuickSwitchWindowCardViewModel(
+                    appGroupIndex: column.appGroupIndex,
+                    windowIndex: windowIndex,
+                    globalIndex: nextGlobalIndex,
+                    window: card.window,
+                    primarySpaceID: card.primarySpaceID,
+                    primarySpaceLabel: card.primarySpaceLabel
+                )
+            }
+
+            return QuickSwitchWaterfallColumnViewModel(
+                appGroupIndex: column.appGroupIndex,
+                app: column.app,
+                windows: reindexedWindows
+            )
+        }
+
+        let columnsByAppGroupIndex = Dictionary(uniqueKeysWithValues: columns.map { ($0.appGroupIndex, $0) })
+        let appShelf = viewModel.appShelf.compactMap { item -> QuickSwitchAppShelfItemViewModel? in
+            guard let column = columnsByAppGroupIndex[item.appGroupIndex] else { return nil }
+            return QuickSwitchAppShelfItemViewModel(
+                appGroupIndex: item.appGroupIndex,
+                app: item.app,
+                windowCount: column.windows.count,
+                primarySpaceIDs: orderedPrimarySpaceIDs(in: column.windows),
+                hasMinimizedWindows: column.windows.contains { $0.window.isMinimized },
+                hasFullscreenWindows: column.windows.contains { $0.window.isFullscreen }
+            )
+        }
+
+        return QuickSwitchViewModel(
+            displays: viewModel.displays,
+            appShelf: appShelf,
+            waterfallColumns: columns,
+            initialSelection: initialSelection(in: columns, preferredSelection: preferredSelection),
+            lockedSpaceID: lockedSpaceID
+        )
+    }
+
+    public static func singleFullscreenSelection(
+        inSpaceID spaceID: UInt64,
+        viewModel: QuickSwitchViewModel
+    ) -> QuickSwitchSelection? {
+        guard viewModel.displays
+            .flatMap(\.spaces)
+            .contains(where: { $0.id == spaceID && $0.type == .fullscreen })
+        else {
+            return nil
+        }
+
+        let cards = viewModel.waterfallColumns
+            .flatMap(\.windows)
+            .filter { $0.primarySpaceID == spaceID }
+
+        guard cards.count == 1,
+              let card = cards.first,
+              card.window.isFullscreen
+        else {
+            return nil
+        }
+
+        return QuickSwitchSelection(
+            appGroupIndex: card.appGroupIndex,
+            windowIndex: card.windowIndex,
+            windowID: card.window.id
+        )
+    }
+
+    private static func replacingInitialSelection(
+        in viewModel: QuickSwitchViewModel,
+        lockedSpaceID: UInt64?,
+        preferredSelection: QuickSwitchSelection?
+    ) -> QuickSwitchViewModel {
+        QuickSwitchViewModel(
+            displays: viewModel.displays,
+            appShelf: viewModel.appShelf,
+            waterfallColumns: viewModel.waterfallColumns,
+            initialSelection: initialSelection(in: viewModel.waterfallColumns, preferredSelection: preferredSelection)
+                ?? viewModel.initialSelection,
+            lockedSpaceID: lockedSpaceID
+        )
+    }
+
+    private static func initialSelection(
+        in columns: [QuickSwitchWaterfallColumnViewModel],
+        preferredSelection: QuickSwitchSelection?
+    ) -> QuickSwitchSelection? {
+        if let preferredSelection,
+           let matchingCard = columns
+            .flatMap(\.windows)
+            .first(where: { $0.window.id == preferredSelection.windowID }) {
+            return QuickSwitchSelection(
+                appGroupIndex: matchingCard.appGroupIndex,
+                windowIndex: matchingCard.windowIndex,
+                windowID: matchingCard.window.id
+            )
+        }
+
+        guard let firstCard = columns.first?.windows.first else { return nil }
+        return QuickSwitchSelection(
+            appGroupIndex: firstCard.appGroupIndex,
+            windowIndex: firstCard.windowIndex,
+            windowID: firstCard.window.id
+        )
+    }
+
+    private static func orderedPrimarySpaceIDs(in cards: [QuickSwitchWindowCardViewModel]) -> [UInt64] {
+        var seen = Set<UInt64>()
+        var result: [UInt64] = []
+
+        for card in cards {
+            guard let spaceID = card.primarySpaceID,
+                  !seen.contains(spaceID)
+            else {
+                continue
+            }
+            seen.insert(spaceID)
+            result.append(spaceID)
+        }
+
+        return result
+    }
 }

--
Gitblit v1.9.3