| | |
| | | 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 { |
| | |
| | | displays: displays, |
| | | appShelf: appShelf, |
| | | waterfallColumns: columns, |
| | | initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot) |
| | | initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot), |
| | | lockedSpaceID: nil |
| | | ) |
| | | } |
| | | |
| | |
| | | appCountsBySpace: [UInt64: Int], |
| | | appNamesBySpace: [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) |
| | |
| | | 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, |
| | |
| | | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | 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 |
| | | } |
| | | } |