@lattices/cli 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2271 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +8 -8
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1264 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
|
@@ -1,1203 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import Combine
|
|
3
|
-
import CoreGraphics
|
|
4
|
-
|
|
5
|
-
private enum MouseGestureAccessory {
|
|
6
|
-
case mic
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
final class MouseGestureController {
|
|
10
|
-
static let shared = MouseGestureController()
|
|
11
|
-
|
|
12
|
-
private struct GestureOutcome {
|
|
13
|
-
let label: String
|
|
14
|
-
let success: Bool
|
|
15
|
-
let accessory: MouseGestureAccessory?
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private final class GestureSession {
|
|
19
|
-
let buttonNumber: Int64
|
|
20
|
-
let startPoint: CGPoint
|
|
21
|
-
let overlay: MouseGestureOverlay
|
|
22
|
-
var currentPoint: CGPoint
|
|
23
|
-
var lockedDirection: MouseGestureDirection?
|
|
24
|
-
|
|
25
|
-
init(buttonNumber: Int64, startPoint: CGPoint, overlay: MouseGestureOverlay) {
|
|
26
|
-
self.buttonNumber = buttonNumber
|
|
27
|
-
self.startPoint = startPoint
|
|
28
|
-
self.overlay = overlay
|
|
29
|
-
self.currentPoint = startPoint
|
|
30
|
-
self.lockedDirection = nil
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private static let syntheticMarker: Int64 = 0x4C474D47
|
|
35
|
-
|
|
36
|
-
private var eventTap: CFMachPort?
|
|
37
|
-
private var runLoopSource: CFRunLoopSource?
|
|
38
|
-
private var session: GestureSession?
|
|
39
|
-
private var retainedOverlays: [ObjectIdentifier: MouseGestureOverlay] = [:]
|
|
40
|
-
private var subscriptions: Set<AnyCancellable> = []
|
|
41
|
-
private var installedObservers = false
|
|
42
|
-
|
|
43
|
-
private init() {}
|
|
44
|
-
|
|
45
|
-
func start() {
|
|
46
|
-
installObserversIfNeeded()
|
|
47
|
-
refresh()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
func stop() {
|
|
51
|
-
clearSession()
|
|
52
|
-
removeEventTap()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static func resolveDirection(
|
|
56
|
-
delta: CGPoint,
|
|
57
|
-
threshold: CGFloat = 68,
|
|
58
|
-
axisBias: CGFloat = 1.2
|
|
59
|
-
) -> MouseGestureDirection? {
|
|
60
|
-
let absX = abs(delta.x)
|
|
61
|
-
let absY = abs(delta.y)
|
|
62
|
-
guard max(absX, absY) >= threshold else { return nil }
|
|
63
|
-
|
|
64
|
-
if absX >= absY * axisBias {
|
|
65
|
-
return delta.x >= 0 ? .right : .left
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if absY >= absX * axisBias {
|
|
69
|
-
return delta.y >= 0 ? .down : .up
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return nil
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private func installObserversIfNeeded() {
|
|
76
|
-
guard !installedObservers else { return }
|
|
77
|
-
installedObservers = true
|
|
78
|
-
|
|
79
|
-
Preferences.shared.$mouseGesturesEnabled
|
|
80
|
-
.receive(on: RunLoop.main)
|
|
81
|
-
.sink { [weak self] _ in self?.refresh() }
|
|
82
|
-
.store(in: &subscriptions)
|
|
83
|
-
|
|
84
|
-
PermissionChecker.shared.$accessibility
|
|
85
|
-
.receive(on: RunLoop.main)
|
|
86
|
-
.sink { [weak self] _ in self?.refresh() }
|
|
87
|
-
.store(in: &subscriptions)
|
|
88
|
-
|
|
89
|
-
MouseInputEventViewer.shared.$isCaptureActive
|
|
90
|
-
.receive(on: RunLoop.main)
|
|
91
|
-
.sink { [weak self] _ in self?.refresh() }
|
|
92
|
-
.store(in: &subscriptions)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private func refresh() {
|
|
96
|
-
let shouldCapture = MouseInputEventViewer.shared.isCaptureActive || Preferences.shared.mouseGesturesEnabled
|
|
97
|
-
guard shouldCapture, PermissionChecker.shared.accessibility else {
|
|
98
|
-
clearSession()
|
|
99
|
-
removeEventTap()
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if eventTap == nil {
|
|
104
|
-
installEventTap()
|
|
105
|
-
} else if let eventTap {
|
|
106
|
-
CGEvent.tapEnable(tap: eventTap, enable: true)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private func installEventTap() {
|
|
111
|
-
var mask = CGEventMask(0)
|
|
112
|
-
mask |= CGEventMask(1) << CGEventType.leftMouseDown.rawValue
|
|
113
|
-
mask |= CGEventMask(1) << CGEventType.leftMouseUp.rawValue
|
|
114
|
-
mask |= CGEventMask(1) << CGEventType.rightMouseDown.rawValue
|
|
115
|
-
mask |= CGEventMask(1) << CGEventType.rightMouseUp.rawValue
|
|
116
|
-
mask |= CGEventMask(1) << CGEventType.otherMouseDown.rawValue
|
|
117
|
-
mask |= CGEventMask(1) << CGEventType.otherMouseDragged.rawValue
|
|
118
|
-
mask |= CGEventMask(1) << CGEventType.otherMouseUp.rawValue
|
|
119
|
-
|
|
120
|
-
let tap = CGEvent.tapCreate(
|
|
121
|
-
tap: .cgSessionEventTap,
|
|
122
|
-
place: .headInsertEventTap,
|
|
123
|
-
options: .defaultTap,
|
|
124
|
-
eventsOfInterest: mask,
|
|
125
|
-
callback: Self.eventTapCallback,
|
|
126
|
-
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
guard let tap else {
|
|
130
|
-
DiagnosticLog.shared.warn("MouseGesture: failed to install mouse shortcut event tap")
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
135
|
-
eventTap = tap
|
|
136
|
-
runLoopSource = source
|
|
137
|
-
|
|
138
|
-
if let source {
|
|
139
|
-
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
|
|
140
|
-
}
|
|
141
|
-
CGEvent.tapEnable(tap: tap, enable: true)
|
|
142
|
-
DiagnosticLog.shared.info("MouseGesture: mouse shortcut event tap installed")
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private func removeEventTap() {
|
|
146
|
-
if let source = runLoopSource {
|
|
147
|
-
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
|
|
148
|
-
}
|
|
149
|
-
runLoopSource = nil
|
|
150
|
-
if let tap = eventTap {
|
|
151
|
-
CFMachPortInvalidate(tap)
|
|
152
|
-
}
|
|
153
|
-
eventTap = nil
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
|
|
157
|
-
guard let userInfo else { return Unmanaged.passUnretained(event) }
|
|
158
|
-
let controller = Unmanaged<MouseGestureController>.fromOpaque(userInfo).takeUnretainedValue()
|
|
159
|
-
return controller.handleEvent(type: type, event: event)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private func handleEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
|
163
|
-
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
|
164
|
-
if let eventTap {
|
|
165
|
-
CGEvent.tapEnable(tap: eventTap, enable: true)
|
|
166
|
-
}
|
|
167
|
-
return Unmanaged.passUnretained(event)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if event.getIntegerValueField(.eventSourceUserData) == Self.syntheticMarker {
|
|
171
|
-
return Unmanaged.passUnretained(event)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
switch type {
|
|
175
|
-
case .leftMouseDown, .leftMouseUp, .rightMouseDown, .rightMouseUp:
|
|
176
|
-
return handlePassiveMouseButtonEvent(type: type, event: event)
|
|
177
|
-
default:
|
|
178
|
-
break
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let buttonNumber = event.getIntegerValueField(.mouseEventButtonNumber)
|
|
182
|
-
if buttonNumber < 2 {
|
|
183
|
-
return Unmanaged.passUnretained(event)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
switch type {
|
|
187
|
-
case .otherMouseDown:
|
|
188
|
-
return handleMouseDown(event, buttonNumber: buttonNumber)
|
|
189
|
-
case .otherMouseDragged:
|
|
190
|
-
return handleMouseDragged(event, buttonNumber: buttonNumber)
|
|
191
|
-
case .otherMouseUp:
|
|
192
|
-
return handleMouseUp(event, buttonNumber: buttonNumber)
|
|
193
|
-
default:
|
|
194
|
-
return Unmanaged.passUnretained(event)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private func handlePassiveMouseButtonEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
|
199
|
-
guard MouseInputEventViewer.shared.isCaptureActive else {
|
|
200
|
-
return Unmanaged.passUnretained(event)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let phase: String
|
|
204
|
-
switch type {
|
|
205
|
-
case .leftMouseDown, .rightMouseDown:
|
|
206
|
-
phase = "down"
|
|
207
|
-
case .leftMouseUp, .rightMouseUp:
|
|
208
|
-
phase = "up"
|
|
209
|
-
default:
|
|
210
|
-
return Unmanaged.passUnretained(event)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
let buttonNumber = Int(event.getIntegerValueField(.mouseEventButtonNumber))
|
|
214
|
-
let appInfo = currentAppInfo()
|
|
215
|
-
recordObservedEvent(
|
|
216
|
-
phase: phase,
|
|
217
|
-
button: MouseShortcutButton(rawButtonNumber: buttonNumber),
|
|
218
|
-
location: event.location,
|
|
219
|
-
delta: .zero,
|
|
220
|
-
modifiers: event.flags,
|
|
221
|
-
candidate: nil,
|
|
222
|
-
match: nil,
|
|
223
|
-
note: "pass-through primary button",
|
|
224
|
-
appInfo: appInfo
|
|
225
|
-
)
|
|
226
|
-
return Unmanaged.passUnretained(event)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private func handleMouseDown(_ event: CGEvent, buttonNumber: Int64) -> Unmanaged<CGEvent>? {
|
|
230
|
-
MouseShortcutStore.shared.reloadIfNeeded()
|
|
231
|
-
let point = event.location
|
|
232
|
-
let button = MouseShortcutButton(rawButtonNumber: Int(buttonNumber))
|
|
233
|
-
let appInfo = currentAppInfo()
|
|
234
|
-
let canRecognize = Preferences.shared.mouseGesturesEnabled && MouseShortcutStore.shared.watchedButtonNumbers.contains(buttonNumber)
|
|
235
|
-
|
|
236
|
-
guard let screen = screen(containing: point) else {
|
|
237
|
-
DiagnosticLog.shared.info("MouseGesture: ignored click at \(format(point)) (off-screen)")
|
|
238
|
-
recordObservedEvent(
|
|
239
|
-
phase: "down",
|
|
240
|
-
button: button,
|
|
241
|
-
location: point,
|
|
242
|
-
delta: .zero,
|
|
243
|
-
modifiers: event.flags,
|
|
244
|
-
candidate: nil,
|
|
245
|
-
match: nil,
|
|
246
|
-
note: "off-screen",
|
|
247
|
-
appInfo: appInfo
|
|
248
|
-
)
|
|
249
|
-
clearSession()
|
|
250
|
-
return Unmanaged.passUnretained(event)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
guard canRecognize else {
|
|
254
|
-
recordObservedEvent(
|
|
255
|
-
phase: "down",
|
|
256
|
-
button: button,
|
|
257
|
-
location: point,
|
|
258
|
-
delta: .zero,
|
|
259
|
-
modifiers: event.flags,
|
|
260
|
-
candidate: nil,
|
|
261
|
-
match: nil,
|
|
262
|
-
note: "button not mapped",
|
|
263
|
-
appInfo: appInfo
|
|
264
|
-
)
|
|
265
|
-
return Unmanaged.passUnretained(event)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
clearSession()
|
|
269
|
-
let overlay = MouseGestureOverlay(screen: screen)
|
|
270
|
-
overlay.onDismiss = { [weak self, weak overlay] in
|
|
271
|
-
guard let self, let overlay else { return }
|
|
272
|
-
self.releaseOverlay(overlay)
|
|
273
|
-
}
|
|
274
|
-
let newSession = GestureSession(buttonNumber: buttonNumber, startPoint: point, overlay: overlay)
|
|
275
|
-
session = newSession
|
|
276
|
-
DiagnosticLog.shared.info("MouseGesture: began at \(format(point)) button=\(buttonNumber)")
|
|
277
|
-
recordObservedEvent(
|
|
278
|
-
phase: "down",
|
|
279
|
-
button: button,
|
|
280
|
-
location: point,
|
|
281
|
-
delta: .zero,
|
|
282
|
-
modifiers: event.flags,
|
|
283
|
-
candidate: nil,
|
|
284
|
-
match: nil,
|
|
285
|
-
note: "tracking",
|
|
286
|
-
appInfo: appInfo
|
|
287
|
-
)
|
|
288
|
-
return nil
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private func handleMouseDragged(_ event: CGEvent, buttonNumber: Int64) -> Unmanaged<CGEvent>? {
|
|
292
|
-
guard let session else {
|
|
293
|
-
return Unmanaged.passUnretained(event)
|
|
294
|
-
}
|
|
295
|
-
guard session.buttonNumber == buttonNumber else {
|
|
296
|
-
return Unmanaged.passUnretained(event)
|
|
297
|
-
}
|
|
298
|
-
MouseShortcutStore.shared.reloadIfNeeded()
|
|
299
|
-
|
|
300
|
-
session.currentPoint = event.location
|
|
301
|
-
let delta = CGPoint(
|
|
302
|
-
x: event.location.x - session.startPoint.x,
|
|
303
|
-
y: event.location.y - session.startPoint.y
|
|
304
|
-
)
|
|
305
|
-
let tuning = MouseShortcutStore.shared.tuning
|
|
306
|
-
let direction = Self.resolveDirection(delta: delta, threshold: tuning.dragThreshold, axisBias: tuning.axisBias)
|
|
307
|
-
|
|
308
|
-
if direction != session.lockedDirection {
|
|
309
|
-
session.lockedDirection = direction
|
|
310
|
-
if let direction {
|
|
311
|
-
let button = MouseShortcutButton(rawButtonNumber: Int(buttonNumber))
|
|
312
|
-
let triggerEvent = MouseShortcutTriggerEvent(button: button, kind: .drag, direction: direction, device: nil)
|
|
313
|
-
let match = MouseShortcutStore.shared.match(for: triggerEvent)
|
|
314
|
-
DiagnosticLog.shared.info("MouseGesture: locked \(label(for: direction)) via \(triggerEvent.triggerName)")
|
|
315
|
-
recordObservedEvent(
|
|
316
|
-
phase: "drag",
|
|
317
|
-
button: button,
|
|
318
|
-
location: event.location,
|
|
319
|
-
delta: delta,
|
|
320
|
-
modifiers: event.flags,
|
|
321
|
-
candidate: triggerEvent.triggerName,
|
|
322
|
-
match: match,
|
|
323
|
-
note: match == nil ? "no rule" : "candidate",
|
|
324
|
-
appInfo: currentAppInfo()
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if let direction {
|
|
330
|
-
let dominantDistance = max(abs(delta.x), abs(delta.y))
|
|
331
|
-
let previewProgress = previewProgress(
|
|
332
|
-
dominantDistance: dominantDistance,
|
|
333
|
-
threshold: tuning.dragThreshold
|
|
334
|
-
)
|
|
335
|
-
session.overlay.track(
|
|
336
|
-
origin: session.startPoint,
|
|
337
|
-
direction: direction,
|
|
338
|
-
label: nil,
|
|
339
|
-
progress: previewProgress
|
|
340
|
-
)
|
|
341
|
-
} else {
|
|
342
|
-
session.overlay.track(origin: session.startPoint, direction: nil, label: nil, progress: 0)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return nil
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private func handleMouseUp(_ event: CGEvent, buttonNumber: Int64) -> Unmanaged<CGEvent>? {
|
|
349
|
-
MouseShortcutStore.shared.reloadIfNeeded()
|
|
350
|
-
let button = MouseShortcutButton(rawButtonNumber: Int(buttonNumber))
|
|
351
|
-
let appInfo = currentAppInfo()
|
|
352
|
-
|
|
353
|
-
guard let session else {
|
|
354
|
-
recordObservedEvent(
|
|
355
|
-
phase: "up",
|
|
356
|
-
button: button,
|
|
357
|
-
location: event.location,
|
|
358
|
-
delta: .zero,
|
|
359
|
-
modifiers: event.flags,
|
|
360
|
-
candidate: nil,
|
|
361
|
-
match: nil,
|
|
362
|
-
note: "no active session",
|
|
363
|
-
appInfo: appInfo
|
|
364
|
-
)
|
|
365
|
-
return Unmanaged.passUnretained(event)
|
|
366
|
-
}
|
|
367
|
-
guard session.buttonNumber == buttonNumber else {
|
|
368
|
-
return Unmanaged.passUnretained(event)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
let delta = CGPoint(
|
|
372
|
-
x: event.location.x - session.startPoint.x,
|
|
373
|
-
y: event.location.y - session.startPoint.y
|
|
374
|
-
)
|
|
375
|
-
let tuning = MouseShortcutStore.shared.tuning
|
|
376
|
-
let direction = Self.resolveDirection(delta: delta, threshold: tuning.dragThreshold, axisBias: tuning.axisBias)
|
|
377
|
-
self.session = nil
|
|
378
|
-
|
|
379
|
-
guard let direction else {
|
|
380
|
-
DiagnosticLog.shared.info("MouseGesture: released without a gesture at \(format(event.location))")
|
|
381
|
-
recordObservedEvent(
|
|
382
|
-
phase: "up",
|
|
383
|
-
button: button,
|
|
384
|
-
location: event.location,
|
|
385
|
-
delta: delta,
|
|
386
|
-
modifiers: event.flags,
|
|
387
|
-
candidate: nil,
|
|
388
|
-
match: nil,
|
|
389
|
-
note: "replay click",
|
|
390
|
-
appInfo: appInfo
|
|
391
|
-
)
|
|
392
|
-
DispatchQueue.main.async { [weak self] in
|
|
393
|
-
session.overlay.dismiss()
|
|
394
|
-
self?.replayMouseClick(
|
|
395
|
-
buttonNumber: buttonNumber,
|
|
396
|
-
at: session.startPoint,
|
|
397
|
-
flags: event.flags
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
return nil
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
let triggerEvent = MouseShortcutTriggerEvent(button: button, kind: .drag, direction: direction, device: nil)
|
|
404
|
-
let match = MouseShortcutStore.shared.match(for: triggerEvent)
|
|
405
|
-
let dismissBeforeAction = shouldDismissOverlayBeforeAction(match: match)
|
|
406
|
-
if dismissBeforeAction {
|
|
407
|
-
session.overlay.dismiss(immediately: true)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
DispatchQueue.main.async { [weak self] in
|
|
411
|
-
guard let self else { return }
|
|
412
|
-
if !dismissBeforeAction {
|
|
413
|
-
self.retainOverlay(session.overlay)
|
|
414
|
-
}
|
|
415
|
-
let outcome = self.performAction(match: match, startPoint: session.startPoint)
|
|
416
|
-
DiagnosticLog.shared.info("MouseGesture: \(outcome.label) -> \(outcome.success ? "ok" : "blocked")")
|
|
417
|
-
self.recordObservedEvent(
|
|
418
|
-
phase: "up",
|
|
419
|
-
button: button,
|
|
420
|
-
location: event.location,
|
|
421
|
-
delta: delta,
|
|
422
|
-
modifiers: event.flags,
|
|
423
|
-
candidate: triggerEvent.triggerName,
|
|
424
|
-
match: match,
|
|
425
|
-
note: outcome.success ? "fired" : "blocked",
|
|
426
|
-
appInfo: appInfo
|
|
427
|
-
)
|
|
428
|
-
if !dismissBeforeAction {
|
|
429
|
-
session.overlay.commit(
|
|
430
|
-
origin: session.startPoint,
|
|
431
|
-
direction: direction,
|
|
432
|
-
label: outcome.label,
|
|
433
|
-
success: outcome.success,
|
|
434
|
-
accessory: outcome.accessory
|
|
435
|
-
)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return nil
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
private func performAction(match: MouseShortcutMatchResult?, startPoint: CGPoint) -> GestureOutcome {
|
|
442
|
-
guard let match else {
|
|
443
|
-
return GestureOutcome(label: "No Shortcut Assigned", success: false, accessory: nil)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
switch match.action.type {
|
|
447
|
-
case .spacePrevious:
|
|
448
|
-
guard WindowTiler.adjacentSpaceTarget(offset: -1, from: startPoint) != nil else {
|
|
449
|
-
return GestureOutcome(label: "No Previous Space", success: false, accessory: nil)
|
|
450
|
-
}
|
|
451
|
-
let switched = WindowTiler.switchToAdjacentSpace(offset: -1, from: startPoint)
|
|
452
|
-
return GestureOutcome(label: switched ? "Previous Space" : "Previous Space Blocked", success: switched, accessory: nil)
|
|
453
|
-
case .spaceNext:
|
|
454
|
-
guard WindowTiler.adjacentSpaceTarget(offset: 1, from: startPoint) != nil else {
|
|
455
|
-
return GestureOutcome(label: "No Next Space", success: false, accessory: nil)
|
|
456
|
-
}
|
|
457
|
-
let switched = WindowTiler.switchToAdjacentSpace(offset: 1, from: startPoint)
|
|
458
|
-
return GestureOutcome(label: switched ? "Next Space" : "Next Space Blocked", success: switched, accessory: nil)
|
|
459
|
-
case .screenMapToggle:
|
|
460
|
-
ScreenMapWindowController.shared.showScreenMapOverview()
|
|
461
|
-
return GestureOutcome(label: "Screen Map Overview", success: true, accessory: nil)
|
|
462
|
-
case .dictationStart:
|
|
463
|
-
let sent = sendDictationShortcut()
|
|
464
|
-
return GestureOutcome(
|
|
465
|
-
label: sent ? "Dictation" : "Dictation Blocked",
|
|
466
|
-
success: sent,
|
|
467
|
-
accessory: sent ? .mic : nil
|
|
468
|
-
)
|
|
469
|
-
case .shortcutSend:
|
|
470
|
-
let sent = sendShortcut(match.action.shortcut)
|
|
471
|
-
return GestureOutcome(
|
|
472
|
-
label: sent ? match.action.label : "Shortcut Blocked",
|
|
473
|
-
success: sent,
|
|
474
|
-
accessory: nil
|
|
475
|
-
)
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private func label(for direction: MouseGestureDirection) -> String {
|
|
480
|
-
switch direction {
|
|
481
|
-
case .left:
|
|
482
|
-
return "Previous Space"
|
|
483
|
-
case .right:
|
|
484
|
-
return "Next Space"
|
|
485
|
-
case .up:
|
|
486
|
-
return "Up"
|
|
487
|
-
case .down:
|
|
488
|
-
return "Screen Map Overview"
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private func clearSession() {
|
|
493
|
-
session?.overlay.dismiss(immediately: true)
|
|
494
|
-
session = nil
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
private func replayMouseClick(buttonNumber: Int64, at point: CGPoint, flags: CGEventFlags) {
|
|
498
|
-
let events: [CGEventType] = [.otherMouseDown, .otherMouseUp]
|
|
499
|
-
for type in events {
|
|
500
|
-
guard let mouseButton = CGMouseButton(rawValue: UInt32(buttonNumber)) else { continue }
|
|
501
|
-
guard let event = CGEvent(
|
|
502
|
-
mouseEventSource: nil,
|
|
503
|
-
mouseType: type,
|
|
504
|
-
mouseCursorPosition: point,
|
|
505
|
-
mouseButton: mouseButton
|
|
506
|
-
) else { continue }
|
|
507
|
-
|
|
508
|
-
event.setIntegerValueField(CGEventField.mouseEventButtonNumber, value: buttonNumber)
|
|
509
|
-
event.setIntegerValueField(CGEventField.eventSourceUserData, value: Self.syntheticMarker)
|
|
510
|
-
event.flags = flags
|
|
511
|
-
event.post(tap: CGEventTapLocation.cghidEventTap)
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
private func screen(containing cgPoint: CGPoint) -> NSScreen? {
|
|
516
|
-
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
517
|
-
let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
|
|
518
|
-
return NSScreen.screens.first(where: { $0.frame.contains(nsPoint) }) ?? NSScreen.main ?? NSScreen.screens.first
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
private func format(_ point: CGPoint) -> String {
|
|
522
|
-
"\(Int(point.x)),\(Int(point.y))"
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private func shouldDismissOverlayBeforeAction(match: MouseShortcutMatchResult?) -> Bool {
|
|
526
|
-
guard let match else { return false }
|
|
527
|
-
switch match.action.type {
|
|
528
|
-
case .spacePrevious, .spaceNext, .screenMapToggle:
|
|
529
|
-
return true
|
|
530
|
-
case .dictationStart, .shortcutSend:
|
|
531
|
-
return false
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
private func previewProgress(dominantDistance: CGFloat, threshold: CGFloat) -> CGFloat {
|
|
536
|
-
guard dominantDistance > threshold else { return 0 }
|
|
537
|
-
let overshoot = dominantDistance - threshold
|
|
538
|
-
let normalized = min(1, max(0, overshoot / 90))
|
|
539
|
-
return 0.32 + normalized * 0.68
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private func retainOverlay(_ overlay: MouseGestureOverlay) {
|
|
543
|
-
retainedOverlays[ObjectIdentifier(overlay)] = overlay
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private func releaseOverlay(_ overlay: MouseGestureOverlay) {
|
|
547
|
-
retainedOverlays.removeValue(forKey: ObjectIdentifier(overlay))
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private func currentAppInfo() -> (name: String?, bundleId: String?) {
|
|
551
|
-
let app = NSWorkspace.shared.frontmostApplication
|
|
552
|
-
return (app?.localizedName, app?.bundleIdentifier)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
private func recordObservedEvent(
|
|
556
|
-
phase: String,
|
|
557
|
-
button: MouseShortcutButton,
|
|
558
|
-
location: CGPoint,
|
|
559
|
-
delta: CGPoint,
|
|
560
|
-
modifiers: CGEventFlags,
|
|
561
|
-
candidate: String?,
|
|
562
|
-
match: MouseShortcutMatchResult?,
|
|
563
|
-
note: String?,
|
|
564
|
-
appInfo: (name: String?, bundleId: String?)
|
|
565
|
-
) {
|
|
566
|
-
guard MouseInputEventViewer.shared.isCaptureActive else { return }
|
|
567
|
-
let sourceState = Int(modifiers.rawValue)
|
|
568
|
-
MouseInputEventViewer.shared.record(
|
|
569
|
-
MouseShortcutObservedEvent(
|
|
570
|
-
timestamp: Date(),
|
|
571
|
-
phase: phase,
|
|
572
|
-
buttonNumber: button.rawButtonNumber,
|
|
573
|
-
location: location,
|
|
574
|
-
delta: delta,
|
|
575
|
-
modifiers: NSEvent.ModifierFlags(rawValue: UInt(modifiers.rawValue)),
|
|
576
|
-
frontmostAppName: appInfo.name,
|
|
577
|
-
frontmostBundleId: appInfo.bundleId,
|
|
578
|
-
candidateTrigger: candidate,
|
|
579
|
-
device: nil,
|
|
580
|
-
matchedRuleSummary: match?.rule.summary,
|
|
581
|
-
willFire: match != nil,
|
|
582
|
-
note: note.map { "\($0) | flags=\(sourceState)" } ?? "flags=\(sourceState)"
|
|
583
|
-
)
|
|
584
|
-
)
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
private func sendShortcut(_ shortcut: MouseShortcutKeyStroke?) -> Bool {
|
|
588
|
-
guard let shortcut else { return false }
|
|
589
|
-
let modifiers = shortcut.modifiers.map(\.appleScriptToken).joined(separator: ", ")
|
|
590
|
-
let command: String
|
|
591
|
-
|
|
592
|
-
if let keyCode = shortcut.keyCode {
|
|
593
|
-
command = modifiers.isEmpty
|
|
594
|
-
? "key code \(keyCode)"
|
|
595
|
-
: "key code \(keyCode) using {\(modifiers)}"
|
|
596
|
-
} else if let key = shortcut.key {
|
|
597
|
-
command = modifiers.isEmpty
|
|
598
|
-
? "keystroke \"\(key)\""
|
|
599
|
-
: "keystroke \"\(key)\" using {\(modifiers)}"
|
|
600
|
-
} else {
|
|
601
|
-
return false
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
let script = """
|
|
605
|
-
tell application "System Events"
|
|
606
|
-
\(command)
|
|
607
|
-
end tell
|
|
608
|
-
return "ok"
|
|
609
|
-
"""
|
|
610
|
-
return ProcessQuery.shell(["/usr/bin/osascript", "-e", script]) == "ok"
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private func sendDictationShortcut() -> Bool {
|
|
614
|
-
sendShortcut(
|
|
615
|
-
MouseShortcutKeyStroke(
|
|
616
|
-
key: "a",
|
|
617
|
-
keyCode: nil,
|
|
618
|
-
modifiers: [.command, .shift]
|
|
619
|
-
)
|
|
620
|
-
)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
private final class MouseGestureOverlay {
|
|
625
|
-
private let committedHoldDuration: TimeInterval = 0.0
|
|
626
|
-
private let fadeDuration: TimeInterval = 0.03
|
|
627
|
-
private let accessoryCommittedHoldDuration: TimeInterval = 0.0
|
|
628
|
-
private let accessoryAnimationDuration: TimeInterval = 0.10
|
|
629
|
-
|
|
630
|
-
private let screen: NSScreen
|
|
631
|
-
private let window: NSWindow
|
|
632
|
-
private let overlayView: MouseGestureOverlayView
|
|
633
|
-
private var fadeTimer: Timer?
|
|
634
|
-
var onDismiss: (() -> Void)?
|
|
635
|
-
|
|
636
|
-
init(screen: NSScreen) {
|
|
637
|
-
self.screen = screen
|
|
638
|
-
self.window = NSWindow(
|
|
639
|
-
contentRect: screen.frame,
|
|
640
|
-
styleMask: .borderless,
|
|
641
|
-
backing: .buffered,
|
|
642
|
-
defer: false
|
|
643
|
-
)
|
|
644
|
-
self.overlayView = MouseGestureOverlayView(frame: NSRect(origin: .zero, size: screen.frame.size))
|
|
645
|
-
|
|
646
|
-
window.isOpaque = false
|
|
647
|
-
window.backgroundColor = .clear
|
|
648
|
-
window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
|
|
649
|
-
window.hasShadow = false
|
|
650
|
-
window.ignoresMouseEvents = true
|
|
651
|
-
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
|
652
|
-
window.contentView = overlayView
|
|
653
|
-
window.orderFrontRegardless()
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
func track(origin: CGPoint, direction: MouseGestureDirection?, label: String?, progress: CGFloat) {
|
|
657
|
-
fadeTimer?.invalidate()
|
|
658
|
-
window.alphaValue = 1
|
|
659
|
-
if let direction {
|
|
660
|
-
overlayView.state = .tracking(
|
|
661
|
-
origin: localPoint(from: origin),
|
|
662
|
-
direction: direction,
|
|
663
|
-
label: label,
|
|
664
|
-
progress: progress
|
|
665
|
-
)
|
|
666
|
-
} else {
|
|
667
|
-
overlayView.state = .idle
|
|
668
|
-
}
|
|
669
|
-
overlayView.needsDisplay = true
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
func commit(
|
|
673
|
-
origin: CGPoint,
|
|
674
|
-
direction: MouseGestureDirection,
|
|
675
|
-
label: String,
|
|
676
|
-
success: Bool,
|
|
677
|
-
accessory: MouseGestureAccessory?
|
|
678
|
-
) {
|
|
679
|
-
fadeTimer?.invalidate()
|
|
680
|
-
window.alphaValue = 1
|
|
681
|
-
overlayView.state = .committed(
|
|
682
|
-
origin: localPoint(from: origin),
|
|
683
|
-
direction: direction,
|
|
684
|
-
label: label,
|
|
685
|
-
success: success,
|
|
686
|
-
accessory: accessory,
|
|
687
|
-
accessoryAnimationDuration: accessoryAnimationDuration
|
|
688
|
-
)
|
|
689
|
-
overlayView.needsDisplay = true
|
|
690
|
-
|
|
691
|
-
let postReplayHoldDuration = accessory == nil ? committedHoldDuration : accessoryCommittedHoldDuration
|
|
692
|
-
let totalVisibleDuration = overlayView.replayLeadInDuration + postReplayHoldDuration
|
|
693
|
-
let timer = Timer(timeInterval: totalVisibleDuration, repeats: false) { [weak self] _ in
|
|
694
|
-
self?.dismiss()
|
|
695
|
-
}
|
|
696
|
-
fadeTimer = timer
|
|
697
|
-
RunLoop.main.add(timer, forMode: .common)
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
func dismiss(immediately: Bool = false) {
|
|
701
|
-
fadeTimer?.invalidate()
|
|
702
|
-
fadeTimer = nil
|
|
703
|
-
|
|
704
|
-
if immediately {
|
|
705
|
-
window.orderOut(nil)
|
|
706
|
-
finishDismissal()
|
|
707
|
-
return
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
NSAnimationContext.runAnimationGroup({ ctx in
|
|
711
|
-
ctx.duration = fadeDuration
|
|
712
|
-
window.animator().alphaValue = 0
|
|
713
|
-
}, completionHandler: { [weak self] in
|
|
714
|
-
self?.window.orderOut(nil)
|
|
715
|
-
self?.finishDismissal()
|
|
716
|
-
})
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
private func localPoint(from cgPoint: CGPoint) -> CGPoint {
|
|
720
|
-
let primaryHeight = NSScreen.screens.first?.frame.height ?? screen.frame.height
|
|
721
|
-
let nsY = primaryHeight - cgPoint.y
|
|
722
|
-
return CGPoint(x: cgPoint.x - screen.frame.minX, y: nsY - screen.frame.minY)
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
private func finishDismissal() {
|
|
726
|
-
let callback = onDismiss
|
|
727
|
-
onDismiss = nil
|
|
728
|
-
callback?()
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private final class MouseGestureOverlayView: NSView {
|
|
733
|
-
enum State {
|
|
734
|
-
case idle
|
|
735
|
-
case tracking(origin: CGPoint, direction: MouseGestureDirection?, label: String?, progress: CGFloat)
|
|
736
|
-
case committed(
|
|
737
|
-
origin: CGPoint,
|
|
738
|
-
direction: MouseGestureDirection,
|
|
739
|
-
label: String,
|
|
740
|
-
success: Bool,
|
|
741
|
-
accessory: MouseGestureAccessory?,
|
|
742
|
-
accessoryAnimationDuration: TimeInterval
|
|
743
|
-
)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
var state: State = .idle {
|
|
747
|
-
didSet {
|
|
748
|
-
updateArrowAnimation(from: oldValue, to: state)
|
|
749
|
-
updateAccessoryAnimation()
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
private var accessoryAnimationTimer: Timer?
|
|
753
|
-
private var accessoryAnimationStartedAt: Date?
|
|
754
|
-
private var accessoryAnimationDuration: TimeInterval = 0
|
|
755
|
-
private var arrowAnimationTimer: Timer?
|
|
756
|
-
private var arrowAnimationStartedAt: Date?
|
|
757
|
-
private var arrowAnimationDuration: TimeInterval = 0
|
|
758
|
-
private var committedStartProgress: CGFloat = 0
|
|
759
|
-
private var accessoryAnimationDelay: TimeInterval = 0
|
|
760
|
-
private let committedArrowAnimationDuration: TimeInterval = 0.06
|
|
761
|
-
private let arrowAnimationDelay: TimeInterval = 0.012
|
|
762
|
-
private let labelRevealThreshold: CGFloat = 0.8
|
|
763
|
-
|
|
764
|
-
var replayLeadInDuration: TimeInterval {
|
|
765
|
-
if committedStartProgress >= labelRevealThreshold {
|
|
766
|
-
return 0
|
|
767
|
-
}
|
|
768
|
-
let remainingProgress = max(0, 1 - committedStartProgress)
|
|
769
|
-
return arrowAnimationDelay + committedArrowAnimationDuration * remainingProgress
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
override var isFlipped: Bool {
|
|
773
|
-
false
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
override func viewDidMoveToWindow() {
|
|
777
|
-
super.viewDidMoveToWindow()
|
|
778
|
-
if window == nil {
|
|
779
|
-
arrowAnimationTimer?.invalidate()
|
|
780
|
-
arrowAnimationTimer = nil
|
|
781
|
-
accessoryAnimationTimer?.invalidate()
|
|
782
|
-
accessoryAnimationTimer = nil
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
override func draw(_ dirtyRect: NSRect) {
|
|
787
|
-
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
788
|
-
ctx.clear(bounds)
|
|
789
|
-
|
|
790
|
-
switch state {
|
|
791
|
-
case .idle:
|
|
792
|
-
break
|
|
793
|
-
case .tracking(let origin, let direction, let label, let progress):
|
|
794
|
-
drawOrigin(at: origin, in: ctx, alpha: 0.88)
|
|
795
|
-
if let direction {
|
|
796
|
-
drawArrow(
|
|
797
|
-
from: origin,
|
|
798
|
-
direction: direction,
|
|
799
|
-
label: label,
|
|
800
|
-
success: true,
|
|
801
|
-
committed: false,
|
|
802
|
-
accessory: nil,
|
|
803
|
-
progressOverride: progress,
|
|
804
|
-
in: ctx
|
|
805
|
-
)
|
|
806
|
-
}
|
|
807
|
-
case .committed(let origin, let direction, let label, let success, let accessory, _):
|
|
808
|
-
drawOrigin(at: origin, in: ctx, alpha: 1.0)
|
|
809
|
-
drawArrow(from: origin, direction: direction, label: label, success: success, committed: true, accessory: accessory, progressOverride: nil, in: ctx)
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
private func drawOrigin(at point: CGPoint, in ctx: CGContext, alpha: CGFloat) {
|
|
814
|
-
ctx.setFillColor(NSColor(calibratedRed: 0.48, green: 0.76, blue: 1.0, alpha: alpha * 0.18).cgColor)
|
|
815
|
-
ctx.fillEllipse(in: CGRect(x: point.x - 18, y: point.y - 18, width: 36, height: 36))
|
|
816
|
-
|
|
817
|
-
ctx.setFillColor(NSColor(calibratedRed: 0.62, green: 0.84, blue: 1.0, alpha: alpha * 0.95).cgColor)
|
|
818
|
-
ctx.fillEllipse(in: CGRect(x: point.x - 5, y: point.y - 5, width: 10, height: 10))
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
private func drawArrow(
|
|
822
|
-
from origin: CGPoint,
|
|
823
|
-
direction: MouseGestureDirection,
|
|
824
|
-
label: String?,
|
|
825
|
-
success: Bool,
|
|
826
|
-
committed: Bool,
|
|
827
|
-
accessory: MouseGestureAccessory?,
|
|
828
|
-
progressOverride: CGFloat?,
|
|
829
|
-
in ctx: CGContext
|
|
830
|
-
) {
|
|
831
|
-
let baseLength: CGFloat = 118
|
|
832
|
-
let arrowProgress = progressOverride ?? currentArrowProgress(committed: committed)
|
|
833
|
-
let clampedProgress = min(1, max(0, arrowProgress))
|
|
834
|
-
let length = baseLength * (committed ? max(0.14, clampedProgress) : (0.34 + 0.66 * clampedProgress))
|
|
835
|
-
let vector = arrowVector(for: direction, length: length)
|
|
836
|
-
let end = CGPoint(x: origin.x + vector.x, y: origin.y + vector.y)
|
|
837
|
-
let accent = success
|
|
838
|
-
? NSColor(calibratedRed: 0.45, green: 0.80, blue: 1.0, alpha: 1.0)
|
|
839
|
-
: NSColor(calibratedRed: 0.98, green: 0.52, blue: 0.42, alpha: 1.0)
|
|
840
|
-
let strokeAlpha = committed ? 0.92 : (0.48 + 0.44 * clampedProgress)
|
|
841
|
-
let glowAlpha = committed ? 0.2 : (0.08 + 0.08 * clampedProgress)
|
|
842
|
-
|
|
843
|
-
ctx.saveGState()
|
|
844
|
-
ctx.setLineCap(.round)
|
|
845
|
-
|
|
846
|
-
let glowPath = CGMutablePath()
|
|
847
|
-
glowPath.move(to: origin)
|
|
848
|
-
glowPath.addLine(to: end)
|
|
849
|
-
ctx.addPath(glowPath)
|
|
850
|
-
ctx.setLineWidth(16)
|
|
851
|
-
ctx.setStrokeColor(accent.withAlphaComponent(glowAlpha).cgColor)
|
|
852
|
-
ctx.strokePath()
|
|
853
|
-
|
|
854
|
-
let linePath = CGMutablePath()
|
|
855
|
-
linePath.move(to: origin)
|
|
856
|
-
linePath.addLine(to: end)
|
|
857
|
-
ctx.addPath(linePath)
|
|
858
|
-
ctx.setLineWidth(5)
|
|
859
|
-
ctx.setStrokeColor(accent.withAlphaComponent(strokeAlpha).cgColor)
|
|
860
|
-
ctx.strokePath()
|
|
861
|
-
|
|
862
|
-
drawArrowHead(at: end, direction: direction, color: accent)
|
|
863
|
-
if let label, (!committed || clampedProgress >= labelRevealThreshold) {
|
|
864
|
-
drawLabel(label, from: origin, to: end, direction: direction, color: accent)
|
|
865
|
-
}
|
|
866
|
-
if committed, let accessory, clampedProgress >= labelRevealThreshold {
|
|
867
|
-
drawAccessory(accessory, from: origin, to: end, direction: direction, color: accent, in: ctx)
|
|
868
|
-
}
|
|
869
|
-
ctx.restoreGState()
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
private func drawArrowHead(at end: CGPoint, direction: MouseGestureDirection, color: NSColor) {
|
|
873
|
-
let size: CGFloat = 15
|
|
874
|
-
let path = NSBezierPath()
|
|
875
|
-
|
|
876
|
-
switch direction {
|
|
877
|
-
case .left:
|
|
878
|
-
path.move(to: CGPoint(x: end.x - size, y: end.y))
|
|
879
|
-
path.line(to: CGPoint(x: end.x + size * 0.2, y: end.y + size * 0.72))
|
|
880
|
-
path.line(to: CGPoint(x: end.x + size * 0.2, y: end.y - size * 0.72))
|
|
881
|
-
case .right:
|
|
882
|
-
path.move(to: CGPoint(x: end.x + size, y: end.y))
|
|
883
|
-
path.line(to: CGPoint(x: end.x - size * 0.2, y: end.y + size * 0.72))
|
|
884
|
-
path.line(to: CGPoint(x: end.x - size * 0.2, y: end.y - size * 0.72))
|
|
885
|
-
case .up:
|
|
886
|
-
path.move(to: CGPoint(x: end.x, y: end.y + size))
|
|
887
|
-
path.line(to: CGPoint(x: end.x - size * 0.72, y: end.y - size * 0.2))
|
|
888
|
-
path.line(to: CGPoint(x: end.x + size * 0.72, y: end.y - size * 0.2))
|
|
889
|
-
case .down:
|
|
890
|
-
path.move(to: CGPoint(x: end.x, y: end.y - size))
|
|
891
|
-
path.line(to: CGPoint(x: end.x - size * 0.72, y: end.y + size * 0.2))
|
|
892
|
-
path.line(to: CGPoint(x: end.x + size * 0.72, y: end.y + size * 0.2))
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
path.close()
|
|
896
|
-
color.withAlphaComponent(0.96).setFill()
|
|
897
|
-
path.fill()
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private func drawLabel(_ label: String, from origin: CGPoint, to end: CGPoint, direction: MouseGestureDirection, color: NSColor) {
|
|
901
|
-
let font = NSFont.monospacedSystemFont(ofSize: 11, weight: .semibold)
|
|
902
|
-
let attributes: [NSAttributedString.Key: Any] = [
|
|
903
|
-
.font: font,
|
|
904
|
-
.foregroundColor: NSColor.white.withAlphaComponent(0.94),
|
|
905
|
-
]
|
|
906
|
-
let attributed = NSAttributedString(string: label, attributes: attributes)
|
|
907
|
-
let textSize = attributed.size()
|
|
908
|
-
let paddingX: CGFloat = 10
|
|
909
|
-
let paddingY: CGFloat = 6
|
|
910
|
-
let bubbleSize = CGSize(
|
|
911
|
-
width: textSize.width + paddingX * 2,
|
|
912
|
-
height: textSize.height + paddingY * 2
|
|
913
|
-
)
|
|
914
|
-
let bubbleOrigin = labelOrigin(from: origin, to: end, direction: direction, bubbleSize: bubbleSize)
|
|
915
|
-
let rect = CGRect(
|
|
916
|
-
x: bubbleOrigin.x,
|
|
917
|
-
y: bubbleOrigin.y,
|
|
918
|
-
width: bubbleSize.width,
|
|
919
|
-
height: bubbleSize.height
|
|
920
|
-
)
|
|
921
|
-
|
|
922
|
-
let bg = NSBezierPath(roundedRect: rect, xRadius: 10, yRadius: 10)
|
|
923
|
-
NSColor.black.withAlphaComponent(0.42).setFill()
|
|
924
|
-
bg.fill()
|
|
925
|
-
|
|
926
|
-
let border = NSBezierPath(roundedRect: rect, xRadius: 10, yRadius: 10)
|
|
927
|
-
color.withAlphaComponent(0.26).setStroke()
|
|
928
|
-
border.lineWidth = 1
|
|
929
|
-
border.stroke()
|
|
930
|
-
|
|
931
|
-
let textRect = CGRect(
|
|
932
|
-
x: rect.minX + paddingX,
|
|
933
|
-
y: rect.minY + paddingY,
|
|
934
|
-
width: textSize.width,
|
|
935
|
-
height: textSize.height
|
|
936
|
-
)
|
|
937
|
-
attributed.draw(in: textRect)
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
private func labelOrigin(from origin: CGPoint, to end: CGPoint, direction: MouseGestureDirection, bubbleSize: CGSize) -> CGPoint {
|
|
941
|
-
let midpoint = CGPoint(x: (origin.x + end.x) / 2, y: (origin.y + end.y) / 2)
|
|
942
|
-
let proposedOrigin: CGPoint
|
|
943
|
-
|
|
944
|
-
switch direction {
|
|
945
|
-
case .left, .right:
|
|
946
|
-
proposedOrigin = CGPoint(x: midpoint.x - bubbleSize.width / 2, y: midpoint.y + 18)
|
|
947
|
-
case .up, .down:
|
|
948
|
-
proposedOrigin = CGPoint(x: midpoint.x + 20, y: midpoint.y - bubbleSize.height / 2)
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
let minX: CGFloat = 12
|
|
952
|
-
let minY: CGFloat = 12
|
|
953
|
-
let maxX = max(minX, bounds.width - bubbleSize.width - 12)
|
|
954
|
-
let maxY = max(minY, bounds.height - bubbleSize.height - 12)
|
|
955
|
-
|
|
956
|
-
return CGPoint(
|
|
957
|
-
x: min(max(proposedOrigin.x, minX), maxX),
|
|
958
|
-
y: min(max(proposedOrigin.y, minY), maxY)
|
|
959
|
-
)
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
private func arrowVector(for direction: MouseGestureDirection, length: CGFloat) -> CGPoint {
|
|
963
|
-
switch direction {
|
|
964
|
-
case .left:
|
|
965
|
-
return CGPoint(x: -length, y: 0)
|
|
966
|
-
case .right:
|
|
967
|
-
return CGPoint(x: length, y: 0)
|
|
968
|
-
case .up:
|
|
969
|
-
return CGPoint(x: 0, y: length)
|
|
970
|
-
case .down:
|
|
971
|
-
return CGPoint(x: 0, y: -length)
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private func updateArrowAnimation(from oldState: State, to newState: State) {
|
|
976
|
-
let oldDirection = stateDirection(from: oldState)
|
|
977
|
-
let newDirection = stateDirection(from: newState)
|
|
978
|
-
let oldCommitted = isCommitted(state: oldState)
|
|
979
|
-
let newCommitted = isCommitted(state: newState)
|
|
980
|
-
|
|
981
|
-
if newCommitted, newDirection != nil {
|
|
982
|
-
let shouldRestart = oldDirection != newDirection || !oldCommitted
|
|
983
|
-
if shouldRestart {
|
|
984
|
-
let previousProgress = trackingProgress(from: oldState)
|
|
985
|
-
committedStartProgress = max(0, min(1, previousProgress ?? 0))
|
|
986
|
-
if committedStartProgress >= 0.94 {
|
|
987
|
-
arrowAnimationTimer?.invalidate()
|
|
988
|
-
arrowAnimationTimer = nil
|
|
989
|
-
arrowAnimationStartedAt = nil
|
|
990
|
-
arrowAnimationDuration = 0
|
|
991
|
-
committedStartProgress = 1
|
|
992
|
-
} else {
|
|
993
|
-
startArrowAnimation(duration: committedArrowAnimationDuration)
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
arrowAnimationTimer?.invalidate()
|
|
1000
|
-
arrowAnimationTimer = nil
|
|
1001
|
-
arrowAnimationStartedAt = nil
|
|
1002
|
-
arrowAnimationDuration = 0
|
|
1003
|
-
committedStartProgress = 0
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
private func startArrowAnimation(duration: TimeInterval) {
|
|
1007
|
-
arrowAnimationTimer?.invalidate()
|
|
1008
|
-
arrowAnimationStartedAt = Date()
|
|
1009
|
-
arrowAnimationDuration = duration
|
|
1010
|
-
|
|
1011
|
-
let timer = Timer(timeInterval: 1.0 / 60.0, repeats: true) { [weak self] timer in
|
|
1012
|
-
guard let self else {
|
|
1013
|
-
timer.invalidate()
|
|
1014
|
-
return
|
|
1015
|
-
}
|
|
1016
|
-
self.needsDisplay = true
|
|
1017
|
-
let elapsed = Date().timeIntervalSince(self.arrowAnimationStartedAt ?? Date())
|
|
1018
|
-
if elapsed >= self.replayLeadInDuration {
|
|
1019
|
-
timer.invalidate()
|
|
1020
|
-
self.arrowAnimationTimer = nil
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
arrowAnimationTimer = timer
|
|
1024
|
-
RunLoop.main.add(timer, forMode: .common)
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private func currentArrowProgress(committed: Bool) -> CGFloat {
|
|
1028
|
-
guard committed,
|
|
1029
|
-
let startedAt = arrowAnimationStartedAt,
|
|
1030
|
-
arrowAnimationDuration > 0 else {
|
|
1031
|
-
return committed ? (committedStartProgress > 0 ? committedStartProgress : 1) : 1
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
let delayedElapsed = Date().timeIntervalSince(startedAt) - arrowAnimationDelay
|
|
1035
|
-
guard delayedElapsed > 0 else { return committedStartProgress }
|
|
1036
|
-
let normalized = min(1, max(0, delayedElapsed / arrowAnimationDuration))
|
|
1037
|
-
let animated = easeOut(normalized)
|
|
1038
|
-
return committedStartProgress + (1 - committedStartProgress) * animated
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
private func updateAccessoryAnimation() {
|
|
1042
|
-
accessoryAnimationTimer?.invalidate()
|
|
1043
|
-
accessoryAnimationTimer = nil
|
|
1044
|
-
accessoryAnimationStartedAt = nil
|
|
1045
|
-
accessoryAnimationDuration = 0
|
|
1046
|
-
accessoryAnimationDelay = 0
|
|
1047
|
-
|
|
1048
|
-
if case .committed(_, _, _, _, let accessory, let duration) = state, accessory != nil {
|
|
1049
|
-
accessoryAnimationStartedAt = Date()
|
|
1050
|
-
accessoryAnimationDuration = duration
|
|
1051
|
-
accessoryAnimationDelay = replayLeadInDuration * 0.86
|
|
1052
|
-
let timer = Timer(timeInterval: 1.0 / 60.0, repeats: true) { [weak self] timer in
|
|
1053
|
-
guard let self else {
|
|
1054
|
-
timer.invalidate()
|
|
1055
|
-
return
|
|
1056
|
-
}
|
|
1057
|
-
self.needsDisplay = true
|
|
1058
|
-
let elapsed = Date().timeIntervalSince(self.accessoryAnimationStartedAt ?? Date())
|
|
1059
|
-
if elapsed >= self.accessoryAnimationDelay + self.accessoryAnimationDuration {
|
|
1060
|
-
timer.invalidate()
|
|
1061
|
-
self.accessoryAnimationTimer = nil
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
accessoryAnimationTimer = timer
|
|
1065
|
-
RunLoop.main.add(timer, forMode: .common)
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
private func drawAccessory(
|
|
1070
|
-
_ accessory: MouseGestureAccessory,
|
|
1071
|
-
from origin: CGPoint,
|
|
1072
|
-
to end: CGPoint,
|
|
1073
|
-
direction: MouseGestureDirection,
|
|
1074
|
-
color: NSColor,
|
|
1075
|
-
in ctx: CGContext
|
|
1076
|
-
) {
|
|
1077
|
-
guard let startedAt = accessoryAnimationStartedAt, accessoryAnimationDuration > 0 else { return }
|
|
1078
|
-
let delayedElapsed = Date().timeIntervalSince(startedAt) - accessoryAnimationDelay
|
|
1079
|
-
guard delayedElapsed > 0 else { return }
|
|
1080
|
-
let progress = min(1, max(0, delayedElapsed / accessoryAnimationDuration))
|
|
1081
|
-
let scale = 0.82 + 0.18 * easeOut(progress)
|
|
1082
|
-
let fadeStart: CGFloat = 0.58
|
|
1083
|
-
let alphaProgress = progress <= fadeStart ? 1 : 1 - ((progress - fadeStart) / (1 - fadeStart))
|
|
1084
|
-
let alpha = max(0, min(1, alphaProgress))
|
|
1085
|
-
guard alpha > 0 else { return }
|
|
1086
|
-
|
|
1087
|
-
let center = accessoryCenter(from: end, direction: direction)
|
|
1088
|
-
let badgeDiameter: CGFloat = 34 * scale
|
|
1089
|
-
let badgeRect = CGRect(
|
|
1090
|
-
x: center.x - badgeDiameter / 2,
|
|
1091
|
-
y: center.y - badgeDiameter / 2,
|
|
1092
|
-
width: badgeDiameter,
|
|
1093
|
-
height: badgeDiameter
|
|
1094
|
-
)
|
|
1095
|
-
|
|
1096
|
-
let badge = NSBezierPath(ovalIn: badgeRect)
|
|
1097
|
-
NSColor.black.withAlphaComponent(0.46 * alpha).setFill()
|
|
1098
|
-
badge.fill()
|
|
1099
|
-
|
|
1100
|
-
color.withAlphaComponent(0.32 * alpha).setStroke()
|
|
1101
|
-
badge.lineWidth = 1
|
|
1102
|
-
badge.stroke()
|
|
1103
|
-
|
|
1104
|
-
switch accessory {
|
|
1105
|
-
case .mic:
|
|
1106
|
-
drawMicGlyph(in: badgeRect.insetBy(dx: badgeDiameter * 0.26, dy: badgeDiameter * 0.2), color: color.withAlphaComponent(0.96 * alpha), in: ctx)
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
private func accessoryCenter(from end: CGPoint, direction: MouseGestureDirection) -> CGPoint {
|
|
1111
|
-
switch direction {
|
|
1112
|
-
case .up:
|
|
1113
|
-
return CGPoint(x: end.x, y: end.y + 34)
|
|
1114
|
-
case .down:
|
|
1115
|
-
return CGPoint(x: end.x, y: end.y - 34)
|
|
1116
|
-
case .left:
|
|
1117
|
-
return CGPoint(x: end.x - 34, y: end.y)
|
|
1118
|
-
case .right:
|
|
1119
|
-
return CGPoint(x: end.x + 34, y: end.y)
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
private func drawMicGlyph(in rect: CGRect, color: NSColor, in ctx: CGContext) {
|
|
1124
|
-
ctx.saveGState()
|
|
1125
|
-
color.setStroke()
|
|
1126
|
-
color.withAlphaComponent(0.22).setFill()
|
|
1127
|
-
|
|
1128
|
-
let bodyWidth = rect.width * 0.42
|
|
1129
|
-
let bodyHeight = rect.height * 0.54
|
|
1130
|
-
let bodyRect = CGRect(
|
|
1131
|
-
x: rect.midX - bodyWidth / 2,
|
|
1132
|
-
y: rect.maxY - bodyHeight,
|
|
1133
|
-
width: bodyWidth,
|
|
1134
|
-
height: bodyHeight
|
|
1135
|
-
)
|
|
1136
|
-
let body = NSBezierPath(roundedRect: bodyRect, xRadius: bodyWidth / 2, yRadius: bodyWidth / 2)
|
|
1137
|
-
body.lineWidth = 1.6
|
|
1138
|
-
body.fill()
|
|
1139
|
-
body.stroke()
|
|
1140
|
-
|
|
1141
|
-
let stem = NSBezierPath()
|
|
1142
|
-
stem.move(to: CGPoint(x: rect.midX, y: bodyRect.minY))
|
|
1143
|
-
stem.line(to: CGPoint(x: rect.midX, y: rect.minY + rect.height * 0.24))
|
|
1144
|
-
stem.lineWidth = 1.8
|
|
1145
|
-
stem.lineCapStyle = .round
|
|
1146
|
-
stem.stroke()
|
|
1147
|
-
|
|
1148
|
-
let arcRect = CGRect(
|
|
1149
|
-
x: rect.midX - rect.width * 0.28,
|
|
1150
|
-
y: rect.minY + rect.height * 0.18,
|
|
1151
|
-
width: rect.width * 0.56,
|
|
1152
|
-
height: rect.height * 0.42
|
|
1153
|
-
)
|
|
1154
|
-
let arc = NSBezierPath()
|
|
1155
|
-
arc.appendArc(
|
|
1156
|
-
withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY + arcRect.height * 0.08),
|
|
1157
|
-
radius: arcRect.width / 2,
|
|
1158
|
-
startAngle: 200,
|
|
1159
|
-
endAngle: -20,
|
|
1160
|
-
clockwise: true
|
|
1161
|
-
)
|
|
1162
|
-
arc.lineWidth = 1.6
|
|
1163
|
-
arc.lineCapStyle = .round
|
|
1164
|
-
arc.stroke()
|
|
1165
|
-
|
|
1166
|
-
let base = NSBezierPath()
|
|
1167
|
-
base.move(to: CGPoint(x: rect.midX - rect.width * 0.22, y: rect.minY + rect.height * 0.14))
|
|
1168
|
-
base.line(to: CGPoint(x: rect.midX + rect.width * 0.22, y: rect.minY + rect.height * 0.14))
|
|
1169
|
-
base.lineWidth = 1.6
|
|
1170
|
-
base.lineCapStyle = .round
|
|
1171
|
-
base.stroke()
|
|
1172
|
-
ctx.restoreGState()
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
private func easeOut(_ t: CGFloat) -> CGFloat {
|
|
1176
|
-
1 - pow(1 - t, 3)
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
private func stateDirection(from state: State) -> MouseGestureDirection? {
|
|
1180
|
-
switch state {
|
|
1181
|
-
case .idle:
|
|
1182
|
-
return nil
|
|
1183
|
-
case .tracking(_, let direction, _, _):
|
|
1184
|
-
return direction
|
|
1185
|
-
case .committed(_, let direction, _, _, _, _):
|
|
1186
|
-
return direction
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
private func isCommitted(state: State) -> Bool {
|
|
1191
|
-
if case .committed = state {
|
|
1192
|
-
return true
|
|
1193
|
-
}
|
|
1194
|
-
return false
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
private func trackingProgress(from state: State) -> CGFloat? {
|
|
1198
|
-
if case .tracking(_, _, _, let progress) = state {
|
|
1199
|
-
return progress
|
|
1200
|
-
}
|
|
1201
|
-
return nil
|
|
1202
|
-
}
|
|
1203
|
-
}
|