| | |
| | | @MainActor |
| | | final class QuickSwitchRootView: NSView { |
| | | private static let appShelfIndexSymbols = Array("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ").map(String.init) |
| | | private static let windowIndexKeyPendingInterval: CFTimeInterval = 0.8 |
| | | private static let appShelfIndexKeyCodes: [String: UInt16] = [ |
| | | "1": 18, "2": 19, "3": 20, "4": 21, "5": 23, |
| | | "6": 22, "7": 26, "8": 28, "9": 25, "0": 29, |
| | | "A": 0, "B": 11, "C": 8, "D": 2, "E": 14, "F": 3, |
| | | "G": 5, "H": 4, "I": 34, "J": 38, "K": 40, "L": 37, |
| | | "M": 46, "N": 45, "O": 31, "P": 35, "Q": 12, "R": 15, |
| | | "S": 1, "T": 17, "U": 32, "V": 9, "W": 13, "X": 7, |
| | | "Y": 16, "Z": 6 |
| | | ] |
| | | |
| | | private let backdropBlurView = NSVisualEffectView() |
| | | private let layerHostView = NSView() |
| | |
| | | private var keyboardCommandsApplied: [String] = [] |
| | | private var lastKeyboardCommand: String? |
| | | private var lastKeyboardAppIndexCommand: String? |
| | | private var appShelfIndexKeyDownCount = 0 |
| | | private var lastAppShelfIndexKeySymbol: String? |
| | | private var windowIndexKeyPending: WindowIndexKeyPending? |
| | | private var windowIndexKeyPendingClearTask: Task<Void, Never>? |
| | | private var lastWindowIndexKeyCommand: String? |
| | | private var lastWindowIndexKeyCommitCode: String? |
| | | private var lastBoundaryBounceAxis: String? |
| | | private var lastBoundaryBounceDirection: String? |
| | | private var boundaryBounceCount = 0 |
| | |
| | | let closeButton: CloseButtonLayers |
| | | } |
| | | |
| | | private struct WindowIndexKeyPending { |
| | | let appSymbol: String |
| | | let appGroupIndex: Int |
| | | let startedAt: CFTimeInterval |
| | | } |
| | | |
| | | private struct ProjectionLayerSnapshot { |
| | | let layer: CALayer |
| | | let frame: CGRect |
| | |
| | | } |
| | | |
| | | override func scrollWheel(with event: NSEvent) { |
| | | clearWindowIndexKeyPending() |
| | | refreshHoverTarget(at: event.locationInWindow) |
| | | |
| | | let dominantDelta = abs(event.scrollingDeltaX) >= abs(event.scrollingDeltaY) |
| | |
| | | |
| | | override func mouseMoved(with event: NSEvent) { |
| | | guard debugHoveredAppGroupIndex == nil else { return } |
| | | clearWindowIndexKeyPending() |
| | | |
| | | let toggleHovered = viewModeToggleButtonContains(windowLocation: event.locationInWindow) |
| | | if !toggleHovered { |
| | |
| | | |
| | | override func mouseExited(with event: NSEvent) { |
| | | guard debugHoveredAppGroupIndex == nil else { return } |
| | | clearWindowIndexKeyPending() |
| | | isViewModeToggleButtonHovered = false |
| | | suppressViewModeToggleHoverPreview = false |
| | | hoveredCloseTarget = nil |
| | |
| | | ]) |
| | | return |
| | | } |
| | | clearWindowIndexKeyPending() |
| | | |
| | | if handleCloseConfirmationMouseDown(at: event.locationInWindow) { |
| | | return |
| | |
| | | layoutSubtreeIfNeeded() |
| | | for command in commands { |
| | | let measurementStart = CACurrentMediaTime() |
| | | performKeyboardCommand(command) |
| | | if !performDebugPhysicalIndexKeyCommand(command) { |
| | | performKeyboardCommand(command) |
| | | } |
| | | layoutSubtreeIfNeeded() |
| | | CATransaction.flush() |
| | | keyboardResponseLatencyMilliseconds.append(milliseconds(since: measurementStart)) |
| | | } |
| | | } |
| | | |
| | | private func performDebugPhysicalIndexKeyCommand(_ rawCommand: String) -> Bool { |
| | | let command = rawCommand.trimmingCharacters(in: .whitespacesAndNewlines) |
| | | let uppercased = command.uppercased() |
| | | let prefixes = ["PHYSICAL-INDEX:", "PHYSICAL-INDEX-", "KEYDOWN:", "KEYDOWN-"] |
| | | guard let prefix = prefixes.first(where: { uppercased.hasPrefix($0) }) else { |
| | | return false |
| | | } |
| | | |
| | | let symbolStart = command.index(command.startIndex, offsetBy: prefix.count) |
| | | guard let symbol = appShelfIndexSymbol(fromKeyboardCommand: String(command[symbolStart...])), |
| | | let keyCode = Self.appShelfIndexKeyCodes[symbol], |
| | | let event = NSEvent.keyEvent( |
| | | with: .keyDown, |
| | | location: .zero, |
| | | modifierFlags: [.option], |
| | | timestamp: ProcessInfo.processInfo.systemUptime, |
| | | windowNumber: window?.windowNumber ?? 0, |
| | | context: nil, |
| | | characters: symbol.lowercased(), |
| | | charactersIgnoringModifiers: symbol.lowercased(), |
| | | isARepeat: false, |
| | | keyCode: keyCode |
| | | ) |
| | | else { |
| | | recordKeyboardCommand("invalidPhysicalIndex:\(command)") |
| | | selectionChangedByLastCommand = false |
| | | return true |
| | | } |
| | | |
| | | let previousSuppressEventInput = suppressEventInput |
| | | suppressEventInput = false |
| | | keyDown(with: event) |
| | | suppressEventInput = previousSuppressEventInput |
| | | return true |
| | | } |
| | | |
| | | func performDebugMouseCommands(_ commands: [String]) { |
| | |
| | | keyboardCommandsApplied = [] |
| | | lastKeyboardCommand = nil |
| | | lastKeyboardAppIndexCommand = nil |
| | | appShelfIndexKeyDownCount = 0 |
| | | lastAppShelfIndexKeySymbol = nil |
| | | clearWindowIndexKeyPending() |
| | | lastWindowIndexKeyCommand = nil |
| | | lastWindowIndexKeyCommitCode = nil |
| | | lastBoundaryBounceAxis = nil |
| | | lastBoundaryBounceDirection = nil |
| | | boundaryBounceCount = 0 |
| | |
| | | "keyboardCommandsApplied": keyboardCommandsApplied, |
| | | "lastKeyboardCommand": lastKeyboardCommand ?? NSNull(), |
| | | "lastKeyboardAppIndexCommand": lastKeyboardAppIndexCommand ?? NSNull(), |
| | | "appShelfIndexKeyDownCount": appShelfIndexKeyDownCount, |
| | | "lastAppShelfIndexKeySymbol": lastAppShelfIndexKeySymbol ?? NSNull(), |
| | | "windowIndexKeyPendingAppGroupIndex": windowIndexKeyPending?.appGroupIndex ?? NSNull(), |
| | | "windowIndexKeyPendingAppSymbol": windowIndexKeyPending?.appSymbol ?? NSNull(), |
| | | "lastWindowIndexKeyCommand": lastWindowIndexKeyCommand ?? NSNull(), |
| | | "lastWindowIndexKeyCommitCode": lastWindowIndexKeyCommitCode ?? NSNull(), |
| | | "lastBoundaryBounceAxis": lastBoundaryBounceAxis ?? NSNull(), |
| | | "lastBoundaryBounceDirection": lastBoundaryBounceDirection ?? NSNull(), |
| | | "boundaryBounceCount": boundaryBounceCount, |
| | |
| | | if let indexSymbol = appShelfIndexSymbol(fromKeyboardCommand: command) { |
| | | let recordedCommand = "app:\(indexSymbol)" |
| | | recordKeyboardCommand(recordedCommand) |
| | | clearWindowIndexKeyPending() |
| | | if !focusAppShelfItemForKeyboard(indexSymbol: indexSymbol, commandLabel: recordedCommand) { |
| | | selectionChangedByLastCommand = false |
| | | } |
| | |
| | | |
| | | switch command { |
| | | case "left": |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("left") |
| | | if isHorizontalMasonryMode { |
| | | moveSelection(.left) |
| | |
| | | selectionChangedByLastCommand = false |
| | | } |
| | | case "right": |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("right") |
| | | if isHorizontalMasonryMode { |
| | | moveSelection(.right) |
| | |
| | | selectionChangedByLastCommand = false |
| | | } |
| | | case "up": |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("up") |
| | | if isHorizontalMasonryMode { |
| | | moveSelection(.up) |
| | |
| | | moveVerticalSelectionWithinFocusedColumn(.up) |
| | | } |
| | | case "down": |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("down") |
| | | if isHorizontalMasonryMode { |
| | | moveSelection(.down) |
| | |
| | | moveVerticalSelectionWithinFocusedColumn(.down) |
| | | } |
| | | case "enter", "return", "commit": |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("enter") |
| | | selectionChangedByLastCommand = false |
| | | commitCurrentSelection() |
| | | case "tab": |
| | | clearWindowIndexKeyPending() |
| | | if isHorizontalMasonryMode { |
| | | recordKeyboardCommand("tab") |
| | | moveHorizontalMasonryTabSelection() |
| | |
| | | selectionChangedByLastCommand = false |
| | | } |
| | | default: |
| | | clearWindowIndexKeyPending() |
| | | recordKeyboardCommand("unknown:\(command)") |
| | | selectionChangedByLastCommand = false |
| | | } |
| | |
| | | } |
| | | |
| | | private func handleAppShelfIndexKey(_ event: NSEvent) -> Bool { |
| | | guard !isHorizontalMasonryMode else { return false } |
| | | let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) |
| | | guard flags.isDisjoint(with: [.command, .control]) else { return false } |
| | | guard let characters = event.charactersIgnoringModifiers, |
| | |
| | | return false |
| | | } |
| | | |
| | | appShelfIndexKeyDownCount += 1 |
| | | lastAppShelfIndexKeySymbol = symbol |
| | | if flags.contains(.option), handleWindowIndexKey(symbol: symbol) { |
| | | return true |
| | | } |
| | | clearWindowIndexKeyPending() |
| | | performKeyboardCommand(symbol) |
| | | return true |
| | | } |
| | | |
| | | private func handleWindowIndexKey(symbol: String) -> Bool { |
| | | let now = CACurrentMediaTime() |
| | | if let pending = validWindowIndexKeyPending(now: now) { |
| | | let code = "\(pending.appSymbol)\(symbol)" |
| | | if let card = waterfallCard(appGroupIndex: pending.appGroupIndex, windowIndexSymbol: symbol) { |
| | | recordKeyboardCommand("window:\(code)") |
| | | lastWindowIndexKeyCommand = "window:\(code)" |
| | | lastWindowIndexKeyCommitCode = code |
| | | clearWindowIndexKeyPending() |
| | | commitKeyboardWindowCard(card, trigger: "windowIndexKey") |
| | | return true |
| | | } |
| | | |
| | | recordKeyboardCommand("windowInvalid:\(code)") |
| | | lastWindowIndexKeyCommand = "windowInvalid:\(code)" |
| | | lastWindowIndexKeyCommitCode = nil |
| | | selectionChangedByLastCommand = false |
| | | clearWindowIndexKeyPending() |
| | | return true |
| | | } |
| | | |
| | | let recordedCommand = "app:\(symbol)" |
| | | recordKeyboardCommand(recordedCommand) |
| | | lastWindowIndexKeyCommand = nil |
| | | lastWindowIndexKeyCommitCode = nil |
| | | if focusAppShelfItemForKeyboard(indexSymbol: symbol, commandLabel: recordedCommand), |
| | | let ordinal = Self.appShelfIndexSymbols.firstIndex(of: symbol), |
| | | ordinal < appShelfItems.count { |
| | | setWindowIndexKeyPending( |
| | | appSymbol: symbol, |
| | | appGroupIndex: appShelfItems[ordinal].item.appGroupIndex, |
| | | startedAt: now |
| | | ) |
| | | } else { |
| | | selectionChangedByLastCommand = false |
| | | clearWindowIndexKeyPending() |
| | | } |
| | | return true |
| | | } |
| | | |
| | |
| | | appGroupIndex: appShelfItems[ordinal].item.appGroupIndex, |
| | | appIndexCommand: commandLabel |
| | | ) |
| | | } |
| | | |
| | | private func validWindowIndexKeyPending(now: CFTimeInterval) -> WindowIndexKeyPending? { |
| | | guard let pending = windowIndexKeyPending else { return nil } |
| | | guard now - pending.startedAt <= Self.windowIndexKeyPendingInterval else { |
| | | clearWindowIndexKeyPending() |
| | | return nil |
| | | } |
| | | return pending |
| | | } |
| | | |
| | | private func setWindowIndexKeyPending(appSymbol: String, appGroupIndex: Int, startedAt: CFTimeInterval) { |
| | | windowIndexKeyPendingClearTask?.cancel() |
| | | windowIndexKeyPending = WindowIndexKeyPending( |
| | | appSymbol: appSymbol, |
| | | appGroupIndex: appGroupIndex, |
| | | startedAt: startedAt |
| | | ) |
| | | windowIndexKeyPendingClearTask = Task { @MainActor [weak self] in |
| | | do { |
| | | try await Task.sleep(nanoseconds: UInt64(Self.windowIndexKeyPendingInterval * 1_000_000_000)) |
| | | } catch { |
| | | return |
| | | } |
| | | guard let self, |
| | | let pending = self.windowIndexKeyPending, |
| | | pending.appSymbol == appSymbol, |
| | | pending.appGroupIndex == appGroupIndex, |
| | | pending.startedAt == startedAt |
| | | else { |
| | | return |
| | | } |
| | | self.windowIndexKeyPending = nil |
| | | self.windowIndexKeyPendingClearTask = nil |
| | | } |
| | | } |
| | | |
| | | private func clearWindowIndexKeyPending() { |
| | | windowIndexKeyPendingClearTask?.cancel() |
| | | windowIndexKeyPendingClearTask = nil |
| | | windowIndexKeyPending = nil |
| | | } |
| | | |
| | | @discardableResult |
| | |
| | | .trimmingCharacters(in: .whitespacesAndNewlines) |
| | | .lowercased() |
| | | guard !command.isEmpty else { return } |
| | | clearWindowIndexKeyPending() |
| | | |
| | | let parts = command.split(separator: ":").map(String.init) |
| | | switch parts.first { |
| | |
| | | private func clickWindowCard(_ card: WaterfallCardLayers) { |
| | | hoverWindowCard(card) |
| | | commitWindowCard(card, trigger: "windowCard") |
| | | } |
| | | |
| | | private func commitKeyboardWindowCard(_ card: WaterfallCardLayers, trigger: String) { |
| | | let selection = QuickSwitchSelection( |
| | | appGroupIndex: card.item.appGroupIndex, |
| | | windowIndex: card.item.windowIndex, |
| | | windowID: card.item.window.id |
| | | ) |
| | | DevelopmentDiagnostics.log("quickSwitch.view.commitKeyboardWindow", [ |
| | | "trigger": trigger, |
| | | "appGroupIndex": selection.appGroupIndex, |
| | | "windowIndex": selection.windowIndex, |
| | | "windowID": selection.windowID, |
| | | "appName": card.item.window.app.name, |
| | | "appPID": card.item.window.app.processIdentifier, |
| | | "titleHash": DevelopmentDiagnostics.stableFingerprint(card.item.window.title), |
| | | "titleLength": card.item.window.title.count, |
| | | "titleIsEmpty": card.item.window.title.isEmpty, |
| | | "isMinimized": card.item.window.isMinimized, |
| | | "isFullscreen": card.item.window.isFullscreen, |
| | | "identifierSource": String(describing: card.item.window.identifierSource) |
| | | ]) |
| | | currentSelection = selection |
| | | columnSelectionHistory[selection.appGroupIndex] = selection.windowIndex |
| | | lastCommittedSelection = selection |
| | | lastCommitSource = .keyboard |
| | | ensureCurrentSelectionVisible() |
| | | onCommitSelection?(selection, .keyboard) |
| | | } |
| | | |
| | | private func commitWindowCard(_ card: WaterfallCardLayers, trigger: String) { |
| | |
| | | string: waterfallMetaText(for: window), |
| | | fontSize: 10, |
| | | weight: .regular, |
| | | color: .secondaryLabelColor |
| | | color: .secondaryLabelColor, |
| | | alignment: .right |
| | | ) |
| | | let appIcon = CALayer() |
| | | appIcon.contentsGravity = .resizeAspect |
| | |
| | | .first(where: { $0.column.appGroupIndex == appGroupIndex })? |
| | | .cards |
| | | .first(where: { $0.item.windowIndex == windowIndex }) |
| | | } |
| | | |
| | | private func waterfallCard(appGroupIndex: Int, windowIndexSymbol: String) -> WaterfallCardLayers? { |
| | | guard let ordinal = Self.appShelfIndexSymbols.firstIndex(of: windowIndexSymbol), |
| | | ordinal < Self.appShelfIndexSymbols.count |
| | | else { |
| | | return nil |
| | | } |
| | | return waterfallCard(appGroupIndex: appGroupIndex, windowIndex: ordinal) |
| | | } |
| | | |
| | | private func waterfallCard(windowID: UInt32) -> WaterfallCardLayers? { |
| | |
| | | } |
| | | |
| | | private func waterfallMetaText(for card: QuickSwitchWindowCardViewModel) -> String { |
| | | card.primarySpaceLabel ?? "No Space" |
| | | windowShortcutCode(for: card) ?? "" |
| | | } |
| | | |
| | | private func windowShortcutCode(for card: QuickSwitchWindowCardViewModel) -> String? { |
| | | guard let appOrdinal = appShelfItems.firstIndex(where: { $0.item.appGroupIndex == card.appGroupIndex }), |
| | | let appSymbol = singleIndexSymbol(forOrdinal: appOrdinal), |
| | | let windowSymbol = singleIndexSymbol(forOrdinal: card.windowIndex) |
| | | else { |
| | | return nil |
| | | } |
| | | return "\(appSymbol)\(windowSymbol)" |
| | | } |
| | | |
| | | private func singleIndexSymbol(forOrdinal ordinal: Int) -> String? { |
| | | guard ordinal >= 0, |
| | | ordinal < Self.appShelfIndexSymbols.count |
| | | else { |
| | | return nil |
| | | } |
| | | return Self.appShelfIndexSymbols[ordinal] |
| | | } |
| | | |
| | | private func waterfallStateText(for card: QuickSwitchWindowCardViewModel) -> String { |
| | |
| | | "titleIsEmpty": card.item.window.title.isEmpty, |
| | | "primarySpaceID": card.item.primarySpaceID ?? NSNull(), |
| | | "primarySpaceLabel": card.item.primarySpaceLabel ?? NSNull(), |
| | | "windowShortcutCode": waterfallMetaText(for: card.item), |
| | | "isMinimized": card.item.window.isMinimized, |
| | | "isFullscreen": card.item.window.isFullscreen, |
| | | "identifierSource": "\(card.item.window.identifierSource)", |