@lattices/cli 0.4.11 → 0.4.13
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/apps/mac/Lattices.app/Contents/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +17 -6
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +93 -81
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +33 -56
- package/docs/reference/dewey.config.ts +1 -1
- package/package.json +1 -1
|
@@ -26,17 +26,17 @@
|
|
|
26
26
|
</dict>
|
|
27
27
|
</array>
|
|
28
28
|
<key>CFBundleVersion</key>
|
|
29
|
-
<string>0.4.
|
|
29
|
+
<string>0.4.13</string>
|
|
30
30
|
<key>CFBundleShortVersionString</key>
|
|
31
|
-
<string>0.4.
|
|
31
|
+
<string>0.4.13</string>
|
|
32
32
|
<key>LatticesBuildChannel</key>
|
|
33
33
|
<string>dev</string>
|
|
34
34
|
<key>LatticesBuildTrack</key>
|
|
35
35
|
<string>latest</string>
|
|
36
36
|
<key>LatticesBuildRevision</key>
|
|
37
|
-
<string>
|
|
37
|
+
<string>42928c7</string>
|
|
38
38
|
<key>LatticesBuildTimestamp</key>
|
|
39
|
-
<string>2026-05-
|
|
39
|
+
<string>2026-05-04T17:39:23Z</string>
|
|
40
40
|
<key>LSMinimumSystemVersion</key>
|
|
41
41
|
<string>13.0</string>
|
|
42
42
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -21,11 +21,12 @@ final class KeyboardRemapController: ObservableObject {
|
|
|
21
21
|
private var capsLayerLastEventAt: CFAbsoluteTime?
|
|
22
22
|
private var bypassUntil: CFAbsoluteTime = 0
|
|
23
23
|
private var lastCapsLayerStaleLogAt: CFAbsoluteTime = 0
|
|
24
|
-
private var pressedKeyCodes =
|
|
24
|
+
private var pressedKeyCodes: [Int64: CFAbsoluteTime] = [:]
|
|
25
25
|
private let breaker = EventTapBreaker(label: "KeyboardRemap")
|
|
26
26
|
private let budgetMeter = TapBudgetMeter(label: "KeyboardRemap")
|
|
27
27
|
private let maxCapsLayerIdleDuration: TimeInterval = 2.0
|
|
28
28
|
private let maxCapsLayerHeldDuration: TimeInterval = 20.0
|
|
29
|
+
private let maxTrackedKeyDownDuration: TimeInterval = 120.0
|
|
29
30
|
private let emergencyBypassDuration: TimeInterval = 3.0
|
|
30
31
|
|
|
31
32
|
private init() {
|
|
@@ -182,7 +183,8 @@ final class KeyboardRemapController: ObservableObject {
|
|
|
182
183
|
return Unmanaged.passUnretained(event)
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
expireStalePressedKeys(now: started)
|
|
187
|
+
updatePressedKeys(type: type, keyCode: event.getIntegerValueField(.keyboardEventKeycode), now: started)
|
|
186
188
|
if shouldTriggerEmergencyReset(type: type, event: event) {
|
|
187
189
|
emergencyClear(now: started)
|
|
188
190
|
InputCaptureResetCenter.reset(reason: "keyboard emergency chord")
|
|
@@ -295,23 +297,32 @@ final class KeyboardRemapController: ObservableObject {
|
|
|
295
297
|
DiagnosticLog.shared.warn("KeyboardRemap: emergency bypass via Escape")
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
private func updatePressedKeys(type: CGEventType, keyCode: Int64) {
|
|
300
|
+
private func updatePressedKeys(type: CGEventType, keyCode: Int64, now: CFAbsoluteTime) {
|
|
299
301
|
switch type {
|
|
300
302
|
case .keyDown:
|
|
301
|
-
pressedKeyCodes
|
|
303
|
+
pressedKeyCodes[keyCode] = now
|
|
302
304
|
case .keyUp:
|
|
303
|
-
pressedKeyCodes.
|
|
305
|
+
pressedKeyCodes.removeValue(forKey: keyCode)
|
|
304
306
|
default:
|
|
305
307
|
break
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
private func expireStalePressedKeys(now: CFAbsoluteTime) {
|
|
312
|
+
let staleKeys = pressedKeyCodes.filter { now - $0.value > maxTrackedKeyDownDuration }.map(\.key)
|
|
313
|
+
guard !staleKeys.isEmpty else { return }
|
|
314
|
+
for keyCode in staleKeys {
|
|
315
|
+
pressedKeyCodes.removeValue(forKey: keyCode)
|
|
316
|
+
}
|
|
317
|
+
DiagnosticLog.shared.warn("KeyboardRemap: cleared stale key-down state for \(staleKeys.count) key(s)")
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
private func shouldTriggerEmergencyReset(type: CGEventType, event: CGEvent) -> Bool {
|
|
310
321
|
guard type == .keyDown else { return false }
|
|
311
322
|
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
|
|
312
323
|
let flags = event.flags
|
|
313
324
|
return keyCode == 40
|
|
314
|
-
&& pressedKeyCodes
|
|
325
|
+
&& pressedKeyCodes[53] != nil
|
|
315
326
|
&& flags.contains(.maskShift)
|
|
316
327
|
}
|
|
317
328
|
|
|
@@ -93,6 +93,7 @@ final class MouseGestureController: ObservableObject {
|
|
|
93
93
|
|
|
94
94
|
private var eventTap: CFMachPort?
|
|
95
95
|
private var runLoopSource: CFRunLoopSource?
|
|
96
|
+
private var installedEventMask: CGEventMask = 0
|
|
96
97
|
private var session: GestureSession?
|
|
97
98
|
private var retainedOverlays: [ObjectIdentifier: MouseGestureOverlay] = [:]
|
|
98
99
|
private var staleSessionTimer: Timer?
|
|
@@ -106,6 +107,9 @@ final class MouseGestureController: ObservableObject {
|
|
|
106
107
|
let buttonNumber: Int64
|
|
107
108
|
let startPoint: CGPoint
|
|
108
109
|
let nativeClickPassthrough: Bool
|
|
110
|
+
var nativeClickBalanced: Bool
|
|
111
|
+
let dragThreshold: CGFloat
|
|
112
|
+
let axisBias: CGFloat
|
|
109
113
|
let startedAt: CFAbsoluteTime
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -139,14 +143,11 @@ final class MouseGestureController: ObservableObject {
|
|
|
139
143
|
return tapTrackingState
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
private func currentTrackingButton() -> Int64? {
|
|
143
|
-
currentTrackingState()?.buttonNumber
|
|
144
|
-
}
|
|
145
|
-
|
|
146
146
|
private func setTrackingButton(
|
|
147
147
|
_ value: Int64?,
|
|
148
148
|
startPoint: CGPoint = .zero,
|
|
149
|
-
nativeClickPassthrough: Bool = false
|
|
149
|
+
nativeClickPassthrough: Bool = false,
|
|
150
|
+
tuning: MouseShortcutTuning = .defaults
|
|
150
151
|
) {
|
|
151
152
|
trackingLock.lock()
|
|
152
153
|
if let value {
|
|
@@ -154,6 +155,9 @@ final class MouseGestureController: ObservableObject {
|
|
|
154
155
|
buttonNumber: value,
|
|
155
156
|
startPoint: startPoint,
|
|
156
157
|
nativeClickPassthrough: nativeClickPassthrough,
|
|
158
|
+
nativeClickBalanced: !nativeClickPassthrough,
|
|
159
|
+
dragThreshold: tuning.dragThreshold,
|
|
160
|
+
axisBias: tuning.axisBias,
|
|
157
161
|
startedAt: CFAbsoluteTimeGetCurrent()
|
|
158
162
|
)
|
|
159
163
|
} else {
|
|
@@ -162,6 +166,21 @@ final class MouseGestureController: ObservableObject {
|
|
|
162
166
|
trackingLock.unlock()
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
private func markNativeClickBalanced(buttonNumber: Int64) -> Bool {
|
|
170
|
+
trackingLock.lock()
|
|
171
|
+
defer { trackingLock.unlock() }
|
|
172
|
+
guard var state = tapTrackingState,
|
|
173
|
+
state.buttonNumber == buttonNumber,
|
|
174
|
+
state.nativeClickPassthrough,
|
|
175
|
+
!state.nativeClickBalanced else {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
state.nativeClickBalanced = true
|
|
180
|
+
tapTrackingState = state
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
private init() {
|
|
166
185
|
breaker.onStateChanged = { [weak self] newState in
|
|
167
186
|
self?.breakerState = newState
|
|
@@ -238,10 +257,16 @@ final class MouseGestureController: ObservableObject {
|
|
|
238
257
|
return
|
|
239
258
|
}
|
|
240
259
|
|
|
260
|
+
let desiredMask = desiredEventMask()
|
|
241
261
|
if eventTap == nil {
|
|
242
|
-
installEventTap()
|
|
262
|
+
installEventTap(mask: desiredMask)
|
|
243
263
|
} else if let eventTap {
|
|
244
|
-
|
|
264
|
+
if installedEventMask != desiredMask {
|
|
265
|
+
removeEventTap()
|
|
266
|
+
installEventTap(mask: desiredMask)
|
|
267
|
+
} else {
|
|
268
|
+
CGEvent.tapEnable(tap: eventTap, enable: true)
|
|
269
|
+
}
|
|
245
270
|
}
|
|
246
271
|
}
|
|
247
272
|
|
|
@@ -256,20 +281,18 @@ final class MouseGestureController: ObservableObject {
|
|
|
256
281
|
}
|
|
257
282
|
}
|
|
258
283
|
|
|
259
|
-
private func
|
|
260
|
-
// Fresh install is a clean slate — drop any stale trip history so
|
|
261
|
-
// the new tap's first failure is judged on its own merits.
|
|
262
|
-
breaker.reset()
|
|
263
|
-
|
|
284
|
+
private func desiredEventMask() -> CGEventMask {
|
|
264
285
|
var mask = CGEventMask(0)
|
|
265
|
-
mask |= CGEventMask(1) << CGEventType.leftMouseDown.rawValue
|
|
266
|
-
mask |= CGEventMask(1) << CGEventType.leftMouseUp.rawValue
|
|
267
|
-
mask |= CGEventMask(1) << CGEventType.rightMouseDown.rawValue
|
|
268
|
-
mask |= CGEventMask(1) << CGEventType.rightMouseDragged.rawValue
|
|
269
|
-
mask |= CGEventMask(1) << CGEventType.rightMouseUp.rawValue
|
|
270
286
|
mask |= CGEventMask(1) << CGEventType.otherMouseDown.rawValue
|
|
271
287
|
mask |= CGEventMask(1) << CGEventType.otherMouseDragged.rawValue
|
|
272
288
|
mask |= CGEventMask(1) << CGEventType.otherMouseUp.rawValue
|
|
289
|
+
return mask
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private func installEventTap(mask: CGEventMask) {
|
|
293
|
+
// Fresh install is a clean slate — drop any stale trip history so
|
|
294
|
+
// the new tap's first failure is judged on its own merits.
|
|
295
|
+
breaker.reset()
|
|
273
296
|
|
|
274
297
|
let tap = CGEvent.tapCreate(
|
|
275
298
|
tap: .cgSessionEventTap,
|
|
@@ -288,6 +311,7 @@ final class MouseGestureController: ObservableObject {
|
|
|
288
311
|
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
289
312
|
eventTap = tap
|
|
290
313
|
runLoopSource = source
|
|
314
|
+
installedEventMask = mask
|
|
291
315
|
|
|
292
316
|
if let source {
|
|
293
317
|
EventTapThread.shared.add(source: source)
|
|
@@ -309,6 +333,7 @@ final class MouseGestureController: ObservableObject {
|
|
|
309
333
|
CFMachPortInvalidate(tap)
|
|
310
334
|
}
|
|
311
335
|
eventTap = nil
|
|
336
|
+
installedEventMask = 0
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
|
|
@@ -352,19 +377,6 @@ final class MouseGestureController: ObservableObject {
|
|
|
352
377
|
return Unmanaged.passUnretained(event)
|
|
353
378
|
}
|
|
354
379
|
|
|
355
|
-
switch type {
|
|
356
|
-
case .leftMouseDown, .leftMouseUp:
|
|
357
|
-
return handlePassiveMouseButtonEvent(type: type, event: event)
|
|
358
|
-
case .rightMouseDown:
|
|
359
|
-
return handleMouseDown(event, buttonNumber: Int64(CGMouseButton.right.rawValue))
|
|
360
|
-
case .rightMouseDragged:
|
|
361
|
-
return handleMouseDragged(event, buttonNumber: Int64(CGMouseButton.right.rawValue))
|
|
362
|
-
case .rightMouseUp:
|
|
363
|
-
return handleMouseUp(event, buttonNumber: Int64(CGMouseButton.right.rawValue))
|
|
364
|
-
default:
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
|
|
368
380
|
let buttonNumber = event.getIntegerValueField(.mouseEventButtonNumber)
|
|
369
381
|
if buttonNumber < 2 {
|
|
370
382
|
return Unmanaged.passUnretained(event)
|
|
@@ -389,54 +401,15 @@ final class MouseGestureController: ObservableObject {
|
|
|
389
401
|
// MouseEventSnapshot, and hand the heavy work to main async — so a slow
|
|
390
402
|
// main thread never adds latency to mouse events at the head-insert tap.
|
|
391
403
|
|
|
392
|
-
private func handlePassiveMouseButtonEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
|
393
|
-
let snapshot = MouseEventSnapshot(
|
|
394
|
-
location: event.location,
|
|
395
|
-
flags: event.flags,
|
|
396
|
-
buttonNumber: event.getIntegerValueField(.mouseEventButtonNumber)
|
|
397
|
-
)
|
|
398
|
-
DispatchQueue.main.async { [weak self] in
|
|
399
|
-
self?.processPassiveMouseButton(type: type, snapshot: snapshot)
|
|
400
|
-
}
|
|
401
|
-
return Unmanaged.passUnretained(event)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
404
|
private func isEmergencyMouseReset(type: CGEventType, event: CGEvent) -> Bool {
|
|
405
405
|
switch type {
|
|
406
|
-
case .
|
|
406
|
+
case .otherMouseDown:
|
|
407
407
|
return event.flags.intersection(.latticesHyper) == .latticesHyper
|
|
408
408
|
default:
|
|
409
409
|
return false
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
private func processPassiveMouseButton(type: CGEventType, snapshot: MouseEventSnapshot) {
|
|
414
|
-
dispatchPrecondition(condition: .onQueue(.main))
|
|
415
|
-
guard MouseInputEventViewer.shared.isCaptureActive else { return }
|
|
416
|
-
|
|
417
|
-
let phase: String
|
|
418
|
-
switch type {
|
|
419
|
-
case .leftMouseDown, .rightMouseDown:
|
|
420
|
-
phase = "down"
|
|
421
|
-
case .leftMouseUp, .rightMouseUp:
|
|
422
|
-
phase = "up"
|
|
423
|
-
default:
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
recordObservedEvent(
|
|
428
|
-
phase: phase,
|
|
429
|
-
button: MouseShortcutButton(rawButtonNumber: Int(snapshot.buttonNumber)),
|
|
430
|
-
location: snapshot.location,
|
|
431
|
-
delta: .zero,
|
|
432
|
-
modifiers: snapshot.flags,
|
|
433
|
-
candidate: nil,
|
|
434
|
-
match: nil,
|
|
435
|
-
note: "pass-through primary button",
|
|
436
|
-
appInfo: currentAppInfo()
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
413
|
private func handleMouseDown(_ event: CGEvent, buttonNumber: Int64) -> Unmanaged<CGEvent>? {
|
|
441
414
|
let snapshot = MouseEventSnapshot(
|
|
442
415
|
location: event.location,
|
|
@@ -448,6 +421,7 @@ final class MouseGestureController: ObservableObject {
|
|
|
448
421
|
let button = MouseShortcutButton(rawButtonNumber: Int(buttonNumber))
|
|
449
422
|
let needsNativeClickCapture = MouseShortcutStore.shared.hasEnabledRule(button: button, kind: .click)
|
|
450
423
|
|| MouseShortcutStore.shared.hasEnabledRule(button: button, kind: .shape)
|
|
424
|
+
let tuning = MouseShortcutStore.shared.tuning
|
|
451
425
|
let nativeClickPassthrough = buttonNumber >= 2 && !needsNativeClickCapture
|
|
452
426
|
let canRecognize = Preferences.shared.mouseGesturesEnabled
|
|
453
427
|
&& MouseShortcutStore.shared.watchedButtonNumbers.contains(buttonNumber)
|
|
@@ -472,7 +446,8 @@ final class MouseGestureController: ObservableObject {
|
|
|
472
446
|
setTrackingButton(
|
|
473
447
|
buttonNumber,
|
|
474
448
|
startPoint: snapshot.location,
|
|
475
|
-
nativeClickPassthrough: nativeClickPassthrough
|
|
449
|
+
nativeClickPassthrough: nativeClickPassthrough,
|
|
450
|
+
tuning: tuning
|
|
476
451
|
)
|
|
477
452
|
DispatchQueue.main.async { [weak self] in
|
|
478
453
|
self?.processMouseDownConsume(
|
|
@@ -572,7 +547,8 @@ final class MouseGestureController: ObservableObject {
|
|
|
572
547
|
}
|
|
573
548
|
|
|
574
549
|
private func handleMouseDragged(_ event: CGEvent, buttonNumber: Int64) -> Unmanaged<CGEvent>? {
|
|
575
|
-
guard
|
|
550
|
+
guard let trackingState = currentTrackingState(),
|
|
551
|
+
trackingState.buttonNumber == buttonNumber else {
|
|
576
552
|
return Unmanaged.passUnretained(event)
|
|
577
553
|
}
|
|
578
554
|
let snapshot = MouseEventSnapshot(
|
|
@@ -580,6 +556,28 @@ final class MouseGestureController: ObservableObject {
|
|
|
580
556
|
flags: event.flags,
|
|
581
557
|
buttonNumber: buttonNumber
|
|
582
558
|
)
|
|
559
|
+
|
|
560
|
+
let delta = CGPoint(
|
|
561
|
+
x: snapshot.location.x - trackingState.startPoint.x,
|
|
562
|
+
y: snapshot.location.y - trackingState.startPoint.y
|
|
563
|
+
)
|
|
564
|
+
let direction = Self.resolveDirection(
|
|
565
|
+
delta: delta,
|
|
566
|
+
threshold: trackingState.dragThreshold,
|
|
567
|
+
axisBias: trackingState.axisBias
|
|
568
|
+
)
|
|
569
|
+
if direction != nil,
|
|
570
|
+
markNativeClickBalanced(buttonNumber: buttonNumber) {
|
|
571
|
+
postSyntheticMouseUp(
|
|
572
|
+
buttonNumber: buttonNumber,
|
|
573
|
+
at: snapshot.location,
|
|
574
|
+
flags: snapshot.flags
|
|
575
|
+
)
|
|
576
|
+
DispatchQueue.main.async {
|
|
577
|
+
DiagnosticLog.shared.info("MouseGesture: balanced native mouseUp for claimed gesture button=\(buttonNumber)")
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
583
581
|
DispatchQueue.main.async { [weak self] in
|
|
584
582
|
self?.processMouseDragged(snapshot: snapshot)
|
|
585
583
|
}
|
|
@@ -677,8 +675,11 @@ final class MouseGestureController: ObservableObject {
|
|
|
677
675
|
x: snapshot.location.x - trackingState.startPoint.x,
|
|
678
676
|
y: snapshot.location.y - trackingState.startPoint.y
|
|
679
677
|
)
|
|
680
|
-
let
|
|
681
|
-
|
|
678
|
+
let direction = Self.resolveDirection(
|
|
679
|
+
delta: delta,
|
|
680
|
+
threshold: trackingState.dragThreshold,
|
|
681
|
+
axisBias: trackingState.axisBias
|
|
682
|
+
)
|
|
682
683
|
if direction == nil {
|
|
683
684
|
setTrackingButton(nil)
|
|
684
685
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -982,13 +983,7 @@ final class MouseGestureController: ObservableObject {
|
|
|
982
983
|
}
|
|
983
984
|
|
|
984
985
|
private func replayMouseClick(buttonNumber: Int64, at point: CGPoint, flags: CGEventFlags) {
|
|
985
|
-
|
|
986
|
-
if buttonNumber == Int64(CGMouseButton.right.rawValue) {
|
|
987
|
-
events = [.rightMouseDown, .rightMouseUp]
|
|
988
|
-
} else {
|
|
989
|
-
events = [.otherMouseDown, .otherMouseUp]
|
|
990
|
-
}
|
|
991
|
-
for type in events {
|
|
986
|
+
for type in [CGEventType.otherMouseDown, .otherMouseUp] {
|
|
992
987
|
guard let mouseButton = CGMouseButton(rawValue: UInt32(buttonNumber)) else { continue }
|
|
993
988
|
guard let event = CGEvent(
|
|
994
989
|
mouseEventSource: nil,
|
|
@@ -1004,6 +999,23 @@ final class MouseGestureController: ObservableObject {
|
|
|
1004
999
|
}
|
|
1005
1000
|
}
|
|
1006
1001
|
|
|
1002
|
+
private func postSyntheticMouseUp(buttonNumber: Int64, at point: CGPoint, flags: CGEventFlags) {
|
|
1003
|
+
guard let mouseButton = CGMouseButton(rawValue: UInt32(buttonNumber)),
|
|
1004
|
+
let event = CGEvent(
|
|
1005
|
+
mouseEventSource: nil,
|
|
1006
|
+
mouseType: .otherMouseUp,
|
|
1007
|
+
mouseCursorPosition: point,
|
|
1008
|
+
mouseButton: mouseButton
|
|
1009
|
+
) else {
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
event.setIntegerValueField(CGEventField.mouseEventButtonNumber, value: buttonNumber)
|
|
1014
|
+
event.setIntegerValueField(CGEventField.eventSourceUserData, value: Self.syntheticMarker)
|
|
1015
|
+
event.flags = flags
|
|
1016
|
+
event.post(tap: CGEventTapLocation.cghidEventTap)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1007
1019
|
private func screen(containing cgPoint: CGPoint) -> NSScreen? {
|
|
1008
1020
|
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
1009
1021
|
let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
|
|
@@ -125,6 +125,7 @@ final class ScreenOverlayCanvasController {
|
|
|
125
125
|
private var globalDismissMonitor: Any?
|
|
126
126
|
private var localDismissMonitor: Any?
|
|
127
127
|
private var dragState: OverlayActorDragState?
|
|
128
|
+
private var actorDragTimeoutTimer: Timer?
|
|
128
129
|
private var agentActorsHidden = false
|
|
129
130
|
private let maxActorDragDuration: TimeInterval = 8.0
|
|
130
131
|
|
|
@@ -176,6 +177,7 @@ final class ScreenOverlayCanvasController {
|
|
|
176
177
|
motionsByLayerID.removeValue(forKey: id)
|
|
177
178
|
if dragState?.id == id {
|
|
178
179
|
dragState = nil
|
|
180
|
+
cancelActorDragTimeout()
|
|
179
181
|
resetPointerCapture()
|
|
180
182
|
}
|
|
181
183
|
render()
|
|
@@ -188,6 +190,7 @@ final class ScreenOverlayCanvasController {
|
|
|
188
190
|
motionsByLayerID = motionsByLayerID.filter { id, _ in layersByID[id] != nil }
|
|
189
191
|
if let dragState, removedIDs.contains(dragState.id) {
|
|
190
192
|
self.dragState = nil
|
|
193
|
+
cancelActorDragTimeout()
|
|
191
194
|
resetPointerCapture()
|
|
192
195
|
}
|
|
193
196
|
render()
|
|
@@ -198,6 +201,7 @@ final class ScreenOverlayCanvasController {
|
|
|
198
201
|
agentActorsHidden.toggle()
|
|
199
202
|
if agentActorsHidden {
|
|
200
203
|
dragState = nil
|
|
204
|
+
cancelActorDragTimeout()
|
|
201
205
|
resetPointerCapture()
|
|
202
206
|
}
|
|
203
207
|
render()
|
|
@@ -206,6 +210,7 @@ final class ScreenOverlayCanvasController {
|
|
|
206
210
|
|
|
207
211
|
func resetInputCapture(reason: String) {
|
|
208
212
|
dragState = nil
|
|
213
|
+
cancelActorDragTimeout()
|
|
209
214
|
resetPointerCapture()
|
|
210
215
|
DiagnosticLog.shared.warn("ScreenOverlay: input capture reset for \(reason)")
|
|
211
216
|
}
|
|
@@ -366,30 +371,19 @@ final class ScreenOverlayCanvasController {
|
|
|
366
371
|
let hasAgentLayer = layersByID.values.contains { $0.owner == .agentApi }
|
|
367
372
|
if hasAgentLayer, globalDismissMonitor == nil {
|
|
368
373
|
let mask: NSEvent.EventTypeMask = [
|
|
369
|
-
.mouseMoved,
|
|
370
374
|
.leftMouseDown,
|
|
371
|
-
.leftMouseUp,
|
|
372
375
|
.rightMouseDown,
|
|
373
376
|
.otherMouseDown,
|
|
374
|
-
.leftMouseDragged,
|
|
375
|
-
.rightMouseDragged,
|
|
376
|
-
.otherMouseDragged,
|
|
377
377
|
]
|
|
378
378
|
globalDismissMonitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { [weak self] event in
|
|
379
379
|
DispatchQueue.main.async {
|
|
380
380
|
_ = self?.handlePointerEvent(event)
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
-
localDismissMonitor = NSEvent.addLocalMonitorForEvents(matching:
|
|
384
|
-
if event.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return nil
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
if self?.handlePointerEvent(event) == true {
|
|
391
|
-
return nil
|
|
392
|
-
}
|
|
383
|
+
localDismissMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
384
|
+
if event.keyCode == 53 {
|
|
385
|
+
self?.dismissAgentOverlays()
|
|
386
|
+
return nil
|
|
393
387
|
}
|
|
394
388
|
return event
|
|
395
389
|
}
|
|
@@ -403,6 +397,7 @@ final class ScreenOverlayCanvasController {
|
|
|
403
397
|
self.localDismissMonitor = nil
|
|
404
398
|
}
|
|
405
399
|
dragState = nil
|
|
400
|
+
cancelActorDragTimeout()
|
|
406
401
|
resetPointerCapture()
|
|
407
402
|
}
|
|
408
403
|
}
|
|
@@ -410,35 +405,7 @@ final class ScreenOverlayCanvasController {
|
|
|
410
405
|
@discardableResult
|
|
411
406
|
private func handlePointerEvent(_ event: NSEvent) -> Bool {
|
|
412
407
|
switch event.type {
|
|
413
|
-
case .
|
|
414
|
-
updatePointerCapture(at: NSEvent.mouseLocation)
|
|
415
|
-
return false
|
|
416
|
-
case .leftMouseDown:
|
|
417
|
-
updatePointerCapture(at: NSEvent.mouseLocation)
|
|
418
|
-
if beginActorDrag(at: NSEvent.mouseLocation) {
|
|
419
|
-
return true
|
|
420
|
-
}
|
|
421
|
-
dismissAgentOverlays()
|
|
422
|
-
return false
|
|
423
|
-
case .leftMouseDragged:
|
|
424
|
-
if dragActor(to: NSEvent.mouseLocation) {
|
|
425
|
-
return true
|
|
426
|
-
}
|
|
427
|
-
dismissAgentOverlays()
|
|
428
|
-
return false
|
|
429
|
-
case .leftMouseUp:
|
|
430
|
-
let wasDragging = dragState != nil
|
|
431
|
-
endActorDrag()
|
|
432
|
-
updatePointerCapture(at: NSEvent.mouseLocation)
|
|
433
|
-
return wasDragging
|
|
434
|
-
case .rightMouseDown:
|
|
435
|
-
updatePointerCapture(at: NSEvent.mouseLocation)
|
|
436
|
-
if closeActor(at: NSEvent.mouseLocation) {
|
|
437
|
-
return true
|
|
438
|
-
}
|
|
439
|
-
dismissAgentOverlays()
|
|
440
|
-
return false
|
|
441
|
-
case .otherMouseDown, .rightMouseDragged, .otherMouseDragged:
|
|
408
|
+
case .leftMouseDown, .rightMouseDown, .otherMouseDown:
|
|
442
409
|
dismissAgentOverlays()
|
|
443
410
|
return false
|
|
444
411
|
default:
|
|
@@ -447,16 +414,7 @@ final class ScreenOverlayCanvasController {
|
|
|
447
414
|
}
|
|
448
415
|
|
|
449
416
|
private func updatePointerCapture(at globalPoint: CGPoint) {
|
|
450
|
-
|
|
451
|
-
let captureWindow: ScreenOverlayWindow?
|
|
452
|
-
if let dragState {
|
|
453
|
-
captureWindow = windowsByScreenID[dragState.screenID]
|
|
454
|
-
} else {
|
|
455
|
-
captureWindow = hitActor(at: globalPoint)?.window
|
|
456
|
-
}
|
|
457
|
-
for window in windowsByScreenID.values {
|
|
458
|
-
window.ignoresMouseEvents = window !== captureWindow
|
|
459
|
-
}
|
|
417
|
+
resetPointerCapture()
|
|
460
418
|
}
|
|
461
419
|
|
|
462
420
|
private func beginActorDrag(at globalPoint: CGPoint) -> Bool {
|
|
@@ -472,6 +430,7 @@ final class ScreenOverlayCanvasController {
|
|
|
472
430
|
lastPoint: currentPoint,
|
|
473
431
|
startedAt: Date()
|
|
474
432
|
)
|
|
433
|
+
scheduleActorDragTimeout()
|
|
475
434
|
layersByID[hit.id] = layer.replacingPayload(.pet(payload.moved(to: currentPoint, state: "idle", isDragging: true)))
|
|
476
435
|
render()
|
|
477
436
|
updateLifecycleMonitors()
|
|
@@ -505,14 +464,16 @@ final class ScreenOverlayCanvasController {
|
|
|
505
464
|
let layer = layersByID[dragState.id],
|
|
506
465
|
case .pet(let payload) = layer.payload else {
|
|
507
466
|
self.dragState = nil
|
|
467
|
+
cancelActorDragTimeout()
|
|
508
468
|
resetPointerCapture()
|
|
509
469
|
return
|
|
510
470
|
}
|
|
511
471
|
layersByID[dragState.id] = layer.replacingPayload(.pet(payload.moved(to: dragState.lastPoint, state: "idle", isDragging: false)))
|
|
512
472
|
self.dragState = nil
|
|
473
|
+
cancelActorDragTimeout()
|
|
513
474
|
render()
|
|
514
475
|
updateLifecycleMonitors()
|
|
515
|
-
|
|
476
|
+
resetPointerCapture()
|
|
516
477
|
}
|
|
517
478
|
|
|
518
479
|
private func clearStaleActorDragIfNeeded() {
|
|
@@ -522,6 +483,20 @@ final class ScreenOverlayCanvasController {
|
|
|
522
483
|
endActorDrag()
|
|
523
484
|
}
|
|
524
485
|
|
|
486
|
+
private func scheduleActorDragTimeout() {
|
|
487
|
+
cancelActorDragTimeout()
|
|
488
|
+
actorDragTimeoutTimer = Timer.scheduledTimer(withTimeInterval: maxActorDragDuration, repeats: false) { [weak self] _ in
|
|
489
|
+
guard let self, self.dragState != nil else { return }
|
|
490
|
+
DiagnosticLog.shared.warn("ScreenOverlay: actor drag timed out; releasing pointer capture")
|
|
491
|
+
self.endActorDrag()
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private func cancelActorDragTimeout() {
|
|
496
|
+
actorDragTimeoutTimer?.invalidate()
|
|
497
|
+
actorDragTimeoutTimer = nil
|
|
498
|
+
}
|
|
499
|
+
|
|
525
500
|
private func resetPointerCapture() {
|
|
526
501
|
for window in windowsByScreenID.values {
|
|
527
502
|
window.ignoresMouseEvents = true
|
|
@@ -536,10 +511,11 @@ final class ScreenOverlayCanvasController {
|
|
|
536
511
|
motionsByLayerID.removeValue(forKey: hit.id)
|
|
537
512
|
if dragState?.id == hit.id {
|
|
538
513
|
dragState = nil
|
|
514
|
+
cancelActorDragTimeout()
|
|
539
515
|
}
|
|
540
516
|
render()
|
|
541
517
|
updateLifecycleMonitors()
|
|
542
|
-
|
|
518
|
+
resetPointerCapture()
|
|
543
519
|
return true
|
|
544
520
|
}
|
|
545
521
|
|
|
@@ -569,6 +545,7 @@ final class ScreenOverlayCanvasController {
|
|
|
569
545
|
motionsByLayerID = motionsByLayerID.filter { id, _ in layersByID[id] != nil }
|
|
570
546
|
if let dragState, layersByID[dragState.id] == nil {
|
|
571
547
|
self.dragState = nil
|
|
548
|
+
cancelActorDragTimeout()
|
|
572
549
|
resetPointerCapture()
|
|
573
550
|
}
|
|
574
551
|
guard layersByID.count != before else { return }
|