| | |
| | | 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 |
| | | ) |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | | } |