Skip to content

Commit 51f68a9

Browse files
jsonifyclaude
andauthored
Claude/fix active target mode 011 c uy l7 ny9 pfo gabvg1q lj w (#12)
* adding active target mode * Fix active target mode and add click test window Changes: - Added ClickTestWindow.swift with visual click targets for testing - Integrated test window into Advanced tab Developer Tools section - Fixed mouse monitoring lifecycle to prevent conflicts during automation - Disable mouse monitor when automation starts in active target mode - Re-enable mouse monitor when automation stops or on emergency stop - Added cursor cleanup to app termination handler - Improved active target mode stability and user experience The test window provides 5 colored targets with click counters, making it easy to validate auto-clicker functionality in both regular and active target modes. * Fix click test window layout and increase size Changes: - Fixed click registration issue by replacing absolute positioning with VStack/HStack layout - All targets now properly register clicks (not just bottom-right) - Increased window size to 1000x800 (ideal 1200x900) for better visibility - Increased target size from 120px to 150px - Enhanced font sizes and visual styling - Removed unused calculatePosition method The layout now uses proper SwiftUI stack-based layout instead of absolute positioning, preventing targets from overlapping in the z-order and ensuring all tap gestures work correctly. * Fix window overflow and add always-visible close button Critical fixes: - Fixed window size to 900x750 (fits on all screens) - Removed NavigationView/toolbar approach that caused overflow - Added custom header with always-visible Close and Reset buttons - Added Escape key shortcut to close window (Cmd+.) - Made content scrollable to prevent overflow issues - Adjusted target sizes (130px) and spacing to fit properly This fixes the issue where users couldn't close the window and had to force quit the application. * Make click test window separate and movable Changed the click test window from a modal sheet to an independent WindowGroup that can be moved around the screen independently from the main ClickIt window. Changes: - Added new WindowGroup with id "click-test-window" in ClickItApp.swift - Updated AdvancedTab to use openWindow environment action instead of sheet - Modified DeveloperTools component to open separate window - Updated button description to clarify it opens in a separate window This allows users to position the click test window anywhere on screen to properly test the auto-clicker functionality without being constrained to the main application window. * Fix active target mode to enable continuous clicking Fixed an issue where active target mode would only perform a single click after being enabled. The automation configuration was hardcoded to use fixed position clicking (useDynamicMouseTracking: false) instead of respecting the active target mode setting. Changes: - Updated createAutomationConfiguration() to set useDynamicMouseTracking based on clickSettings.isActiveTargetMode - When active target mode is enabled, automation now follows cursor position - When active target mode is disabled, automation uses fixed position This fixes the reported issue where autoclicking would only do 1 click after enabling active target mode and making the first click. Fixes: ClickItViewModel.swift:234 * Fix active target mode: disable stopOnError to allow continuous clicking Root cause analysis revealed that automation was stopping after a single click due to strict timing constraints combined with stopOnError=true: 1. Click timing constraints are very strict (15ms total: 10ms delay + 5ms deviation) 2. First clicks often exceed this on busy systems 3. stopOnError defaulted to true, stopping automation on first timing failure 4. Visual feedback showed regardless, masking the actual failure Solution: - Disable stopOnError specifically for active target mode - Active target mode is designed for continuous clicking - Users can manually stop by clicking again - Occasional timing failures shouldn't halt the entire automation - Normal automation modes still respect the stopOnError setting This complements the previous fix (a821c39) which enabled dynamic mouse tracking. Both changes are needed for active target mode to work correctly: - a821c39: Made clicks follow cursor position (useDynamicMouseTracking) - This commit: Prevents stopping on timing errors (stopOnError) Fixes: ClickItViewModel.swift:231 * Fix coordinate system conversion for active target mode Fixed an issue where clicks in active target mode would alternate between two different screen positions due to coordinate system mismatch. Problem: - NSEvent.mouseLocation returns AppKit coordinates (origin bottom-left, Y↑) - ClickEngine expects CoreGraphics coordinates (origin top-left, Y↓) - Mouse position was being used directly without conversion - This caused clicks to alternate between intended position and mirrored position Solution: - Convert AppKit coordinates to CoreGraphics before clicking - Track separate coordinates for clicking vs visual feedback - clickLocation: CoreGraphics coordinates for ClickEngine - visualFeedbackLocation: AppKit coordinates for overlay - Use existing convertAppKitToCoreGraphicsMultiMonitor() method Changes: - Line 368-373: Convert mouse position from AppKit to CoreGraphics - Line 375-377: Also convert fixed positions for visual feedback - Line 390-392: Use AppKit coordinates for visual feedback overlay This ensures clicks happen exactly where the cursor is positioned, not at a vertically mirrored location. Fixes: ClickCoordinator.swift:368 * Implement hybrid start/stop controls for active target mode Added intuitive mouse-based controls for active target mode with redundant stop options for maximum flexibility and ease of use. Controls: - LEFT-CLICK: Start autoclicking at cursor position - RIGHT-CLICK: Stop autoclicking immediately - ESC KEY: Stop autoclicking (emergency stop - already existed) Changes in HotkeyManager.swift: - Added onRightMouseClick callback for stop action - Updated registerMouseMonitor to monitor both .leftMouseDown and .rightMouseDown - Enhanced handleMouseClick to differentiate between left/right clicks - Updated logging to show LEFT=START, RIGHT=STOP Changes in ClickItViewModel.swift: - Split handleActiveTargetClick into: - handleActiveTargetLeftClick: Only starts automation - handleActiveTargetRightClick: Only stops automation - Updated setupMouseClickHandler to register both left and right handlers - Updated removeMouseClickHandler to clean up both handlers - Improved logging to clearly indicate which action is being performed Benefits: - No more confusing toggle behavior - Clear separation: left=start, right=stop - Two ways to stop (right-click or ESC) for maximum flexibility - Mouse-only users can use right-click - Keyboard users can use ESC - More intuitive and less prone to accidental toggles This complements previous fixes: - a821c39: Dynamic mouse tracking - c548735: Disabled stopOnError - 9ce9775: Coordinate system conversion * Simplify to right-click toggle with emergency key stops Changed active target mode controls to use a simpler, more intuitive scheme: - Right-click to toggle (start/stop) automation - All emergency keys (ESC, DELETE, F1, etc.) still work to stop This simplifies the control scheme from the previous hybrid approach and eliminates confusion between left and right mouse buttons. Controls: - RIGHT-CLICK: Toggle automation on/off at cursor position - ESC/DELETE/F1/etc: Stop automation (emergency stops) Changes in HotkeyManager.swift: - Removed onLeftMouseClick callback (no longer needed) - Changed onRightMouseClick to handle toggle behavior - Updated registerMouseMonitor to only monitor .rightMouseDown - Simplified handleMouseClick to only process right-clicks - Updated logging to reflect toggle behavior Changes in ClickItViewModel.swift: - Removed handleActiveTargetLeftClick (no longer needed) - Updated handleActiveTargetRightClick to toggle (start/stop) - Simplified setupMouseClickHandler to only register right-click - Simplified removeMouseClickHandler to only remove right-click - Updated logging to reflect toggle behavior Benefits: - Single mouse button for all mouse control (right-click) - Clear toggle behavior: click once to start, click again to stop - Emergency keys still available for instant stop - Simpler mental model: one button, one action (toggle) - Fewer lines of code, easier to maintain Supersedes: fe68ce8 (hybrid left/right approach) * Fix active target mode: correct UI text and remove double validation Fixed two critical issues preventing active target mode from working: Issue 1: Incorrect UI Instruction - QuickStartTab.swift line 311 said "Left-click" but implementation uses "Right-click" - Users were clicking the wrong button! - Fixed: Changed text to "Right-click to start/stop clicking at cursor position" Issue 2: Double Validation Barrier - handleActiveTargetRightClick() allowed active target mode through first check - But startAutomation() had second guard that didn't account for active target mode - This blocked automation from starting even with valid right-click - Fixed: Added active target mode bypass in startAutomation() Changes: QuickStartTab.swift: - Line 311: Changed "Left-click" → "Right-click" in help text ClickItViewModel.swift: - Lines 164-173: Updated startAutomation() guard logic - In active target mode: only requires targetPoint + interval + !isRunning - In normal mode: uses full canStartAutomation validation - Added detailed debug logging to help troubleshoot prerequisites User Workflow Now: 1. Enable "Active Target Mode" toggle (cursor becomes crosshair) 2. Right-click anywhere to start (no need for "Start Automation" button!) 3. Automation clicks continuously at cursor position with configured interval 4. Right-click again to stop, OR use ESC/DELETE/F1 emergency keys Prerequisites for Active Target Mode: - Interval must be > 0 (✅ default is 10 seconds) - Automation must not already be running - That's it! No need to pre-set target or validate other settings Fixes: QuickStartTab.swift:311, ClickItViewModel.swift:164 * Fix active target mode: re-enable mouse monitoring after external stops Fixed critical bug where right-click would not restart automation after stopping via ESC key or natural completion (maxClicks/maxDuration reached). Root Cause: When automation stops externally (not via ViewModel.stopAutomation()), the $isActive observer (line 486) updates UI state but forgets to re-register mouse monitoring. This left right-click detection disabled after the first stop, making restart impossible. The Bug Flow: 1. Right-click to start → executeAutomation() unregisters mouse monitoring 2. Press ESC key → ClickCoordinator.emergencyStopAutomation() called 3. isActive becomes false → observer triggers 4. Observer updates isRunning/isPaused/appStatus ✅ 5. Observer calls cancelTimer() ✅ 6. Observer FORGETS to re-register mouse monitoring ❌ 7. Next right-click does nothing (no monitors listening!) Comparison: - stopAutomation() (line 294): ✅ Re-registers monitoring - emergencyStopAutomation() (line 604): ✅ Re-registers monitoring - $isActive observer (line 490): ❌ Forgot to re-register (NOW FIXED) The Fix: Added mouse monitoring re-registration to the $isActive observer at lines 498-502, matching the logic in stopAutomation() and emergencyStopAutomation(). Now works for ALL stop scenarios: ✅ Manual right-click stop → re-registers monitoring ✅ ESC/DELETE/F1 emergency stop → re-registers monitoring ✅ Natural completion (maxClicks) → re-registers monitoring ✅ Timer expiration → re-registers monitoring User can now: 1. Enable active target mode 2. Right-click to start 3. Stop via ANY method (ESC, right-click, completion) 4. Right-click again to restart ← NOW WORKS! Fixes: ClickItViewModel.swift:498-502 --------- Co-authored-by: Claude <[email protected]>
1 parent d4521aa commit 51f68a9

File tree

10 files changed

+814
-37
lines changed

10 files changed

+814
-37
lines changed

ClickIt/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundlePackageType</key>
2020
<string>APPL</string>
2121
<key>CFBundleShortVersionString</key>
22-
<string>1.5.3</string>
22+
<string>1.5.5</string>
2323
<key>CFBundleVersion</key>
2424
<string>$(CURRENT_PROJECT_VERSION)</string>
2525
<key>LSMinimumSystemVersion</key>

Sources/ClickIt/ClickItApp.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ struct ClickItApp: App {
4444
}
4545
}
4646
}
47+
48+
// Separate window for click test - can be moved independently
49+
WindowGroup(id: "click-test-window") {
50+
ClickTestWindow()
51+
}
52+
.windowResizability(.contentSize)
53+
.defaultSize(width: 900, height: 750)
54+
.windowToolbarStyle(.unified)
4755
}
4856

4957
// MARK: - Safe Initialization
@@ -76,6 +84,8 @@ struct ClickItApp: App {
7684
// Cleanup visual feedback overlay when app terminates
7785
VisualFeedbackOverlay.shared.cleanup()
7886
HotkeyManager.shared.cleanup()
87+
// Restore cursor to normal
88+
CursorManager.shared.forceRestoreNormalCursor()
7989
}
8090

8191
print("ClickItApp: Safe app initialization completed")

Sources/ClickIt/Core/Click/ClickCoordinator.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,21 +360,37 @@ class ClickCoordinator: ObservableObject {
360360
/// Simple automation step execution from working version
361361
private func executeAutomationStep(configuration: AutomationConfiguration) async -> ClickResult {
362362
print("ClickCoordinator: executeAutomationStep() - Simple working approach")
363-
363+
364+
// Determine click location based on mode
365+
let clickLocation: CGPoint // CoreGraphics coordinates for clicking
366+
let visualFeedbackLocation: CGPoint // AppKit coordinates for visual feedback
367+
368+
if configuration.useDynamicMouseTracking {
369+
// Active target mode: get current mouse position
370+
let appKitLocation = NSEvent.mouseLocation
371+
clickLocation = convertAppKitToCoreGraphicsMultiMonitor(appKitLocation)
372+
visualFeedbackLocation = appKitLocation
373+
print("ClickCoordinator: Active target mode - AppKit: \(appKitLocation) → CoreGraphics: \(clickLocation)")
374+
} else {
375+
// Fixed location mode: use configured location (already in CoreGraphics)
376+
clickLocation = configuration.location
377+
visualFeedbackLocation = convertCoreGraphicsToAppKitMultiMonitor(configuration.location)
378+
}
379+
364380
// Use the working performSingleClick method
365381
let result = await performSingleClick(
366382
configuration: ClickConfiguration(
367383
type: configuration.clickType,
368-
location: configuration.location,
384+
location: clickLocation,
369385
targetPID: nil
370386
)
371387
)
372-
373-
// Update visual feedback if enabled
388+
389+
// Update visual feedback if enabled (requires AppKit coordinates)
374390
if configuration.showVisualFeedback {
375-
VisualFeedbackOverlay.shared.updateOverlay(at: configuration.location, isActive: true)
391+
VisualFeedbackOverlay.shared.updateOverlay(at: visualFeedbackLocation, isActive: true)
376392
}
377-
393+
378394
return result
379395
}
380396

Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,22 @@ class HotkeyManager: ObservableObject {
1717
@Published var emergencyStopActivated: Bool = false
1818

1919
// MARK: - Private Properties
20-
20+
2121
private var globalEventMonitor: Any?
2222
private var localEventMonitor: Any?
23+
private var globalMouseMonitor: Any?
24+
private var localMouseMonitor: Any?
2325
private var lastHotkeyTime: TimeInterval = 0
2426
private let hotkeyDebounceInterval: TimeInterval = 0.01 // 10ms debounce for ultra-fast emergency response
2527
private var responseTimeTracker: EmergencyStopResponseTracker?
28+
29+
// MARK: - Mouse Click Handling
30+
31+
/// Callback invoked when right mouse button is clicked (for active target mode - TOGGLE)
32+
var onRightMouseClick: (() -> Void)?
33+
34+
private var lastMouseClickTime: TimeInterval = 0
35+
private let mouseClickDebounceInterval: TimeInterval = 0.1 // 100ms debounce for mouse clicks
2636

2737
// MARK: - Initialization
2838

@@ -36,6 +46,7 @@ class HotkeyManager: ObservableObject {
3646

3747
func cleanup() {
3848
unregisterGlobalHotkey()
49+
unregisterMouseMonitor()
3950
}
4051

4152
func registerGlobalHotkey(_ config: HotkeyConfiguration) -> Bool {
@@ -70,15 +81,49 @@ class HotkeyManager: ObservableObject {
7081
NSEvent.removeMonitor(monitor)
7182
globalEventMonitor = nil
7283
}
73-
84+
7485
if let monitor = localEventMonitor {
7586
NSEvent.removeMonitor(monitor)
7687
localEventMonitor = nil
7788
}
78-
89+
7990
isRegistered = false
8091
print("HotkeyManager: Successfully unregistered hotkey monitoring")
8192
}
93+
94+
/// Register global mouse click monitoring for active target mode
95+
func registerMouseMonitor() {
96+
// Unregister existing monitors first
97+
unregisterMouseMonitor()
98+
99+
// Monitor right mouse clicks globally (when app is in background)
100+
globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .rightMouseDown) { [weak self] event in
101+
self?.handleMouseClick(event)
102+
}
103+
104+
// Monitor right mouse clicks locally (when app is in foreground)
105+
localMouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .rightMouseDown) { [weak self] event in
106+
self?.handleMouseClick(event)
107+
return event // Pass through the event
108+
}
109+
110+
print("HotkeyManager: Successfully registered right-click monitoring (toggle start/stop)")
111+
}
112+
113+
/// Unregister mouse click monitoring
114+
func unregisterMouseMonitor() {
115+
if let monitor = globalMouseMonitor {
116+
NSEvent.removeMonitor(monitor)
117+
globalMouseMonitor = nil
118+
}
119+
120+
if let monitor = localMouseMonitor {
121+
NSEvent.removeMonitor(monitor)
122+
localMouseMonitor = nil
123+
}
124+
125+
print("HotkeyManager: Successfully unregistered mouse click monitoring")
126+
}
82127

83128
// MARK: - Private Methods
84129

@@ -91,19 +136,38 @@ class HotkeyManager: ObservableObject {
91136

92137
private func handleMultiKeyEvent(_ event: NSEvent) {
93138
let currentTime = CFAbsoluteTimeGetCurrent()
94-
139+
95140
// Debounce emergency stop to prevent rapid fire (50ms for emergency response)
96141
if currentTime - lastHotkeyTime < hotkeyDebounceInterval {
97142
return
98143
}
99-
144+
100145
// Check if this event matches any emergency stop key
101146
if let matchedConfig = matchEmergencyStopKey(event) {
102147
print("HotkeyManager: Emergency stop key activated - \(matchedConfig.description)")
103148
lastHotkeyTime = currentTime
104149
handleEmergencyStop(triggeredBy: matchedConfig)
105150
}
106151
}
152+
153+
private func handleMouseClick(_ event: NSEvent) {
154+
let currentTime = CFAbsoluteTimeGetCurrent()
155+
156+
// Debounce mouse clicks to prevent accidental double-clicks
157+
if currentTime - lastMouseClickTime < mouseClickDebounceInterval {
158+
return
159+
}
160+
161+
lastMouseClickTime = currentTime
162+
163+
// Handle right-click for toggle (start/stop)
164+
if event.type == .rightMouseDown {
165+
print("HotkeyManager: Right-click detected for active target mode (TOGGLE)")
166+
Task { @MainActor in
167+
self.onRightMouseClick?()
168+
}
169+
}
170+
}
107171

108172
private func matchEmergencyStopKey(_ event: NSEvent) -> HotkeyConfiguration? {
109173
// Check all available emergency stop configurations

Sources/ClickIt/Core/Models/ClickSettings.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ class ClickSettings: ObservableObject {
105105
saveSettings()
106106
}
107107
}
108-
108+
109+
/// Active target mode - clicks follow cursor position
110+
@Published var isActiveTargetMode: Bool = false {
111+
didSet {
112+
saveSettings()
113+
}
114+
}
115+
109116
// MARK: - CPS Randomization Properties
110117

111118
/// Whether to enable CPS timing randomization
@@ -231,6 +238,7 @@ class ClickSettings: ObservableObject {
231238
stopOnError: stopOnError,
232239
showVisualFeedback: showVisualFeedback,
233240
playSoundFeedback: playSoundFeedback,
241+
isActiveTargetMode: isActiveTargetMode,
234242
randomizeTiming: randomizeTiming,
235243
timingVariancePercentage: timingVariancePercentage,
236244
distributionPattern: distributionPattern,
@@ -268,6 +276,7 @@ class ClickSettings: ObservableObject {
268276
stopOnError = settings.stopOnError
269277
showVisualFeedback = settings.showVisualFeedback
270278
playSoundFeedback = settings.playSoundFeedback
279+
isActiveTargetMode = settings.isActiveTargetMode ?? false // Default for backward compatibility
271280
randomizeTiming = settings.randomizeTiming
272281
timingVariancePercentage = settings.timingVariancePercentage
273282
distributionPattern = settings.distributionPattern
@@ -293,6 +302,7 @@ class ClickSettings: ObservableObject {
293302
stopOnError = false
294303
showVisualFeedback = true
295304
playSoundFeedback = false
305+
isActiveTargetMode = false
296306
randomizeTiming = false
297307
timingVariancePercentage = 0.1
298308
distributionPattern = .normal
@@ -318,8 +328,8 @@ class ClickSettings: ObservableObject {
318328
func createAutomationConfiguration() -> AutomationConfiguration {
319329
let maxClicksValue = durationMode == .clickCount ? maxClicks : nil
320330
let maxDurationValue = durationMode == .timeLimit ? durationSeconds : nil
321-
322-
print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback)")
331+
332+
print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback), activeTargetMode: \(isActiveTargetMode)")
323333

324334
return AutomationConfiguration(
325335
location: clickLocation,
@@ -331,6 +341,8 @@ class ClickSettings: ObservableObject {
331341
stopOnError: stopOnError,
332342
randomizeLocation: randomizeLocation,
333343
locationVariance: CGFloat(locationVariance),
344+
useDynamicMouseTracking: isActiveTargetMode,
345+
showVisualFeedback: showVisualFeedback,
334346
cpsRandomizerConfig: createCPSRandomizerConfiguration()
335347
)
336348
}
@@ -353,13 +365,14 @@ class ClickSettings: ObservableObject {
353365
stopOnError: stopOnError,
354366
showVisualFeedback: showVisualFeedback,
355367
playSoundFeedback: playSoundFeedback,
368+
isActiveTargetMode: isActiveTargetMode,
356369
randomizeTiming: randomizeTiming,
357370
timingVariancePercentage: timingVariancePercentage,
358371
distributionPattern: distributionPattern,
359372
humannessLevel: humannessLevel,
360373
schedulingMode: schedulingMode,
361374
scheduledDateTime: scheduledDateTime,
362-
exportVersion: "1.1",
375+
exportVersion: "1.2",
363376
exportDate: Date(),
364377
appVersion: AppConstants.appVersion
365378
)
@@ -403,6 +416,7 @@ class ClickSettings: ObservableObject {
403416
stopOnError = importData.stopOnError
404417
showVisualFeedback = importData.showVisualFeedback
405418
playSoundFeedback = importData.playSoundFeedback
419+
isActiveTargetMode = importData.isActiveTargetMode ?? false // Default for backward compatibility
406420
randomizeTiming = importData.randomizeTiming
407421
timingVariancePercentage = importData.timingVariancePercentage
408422
distributionPattern = importData.distributionPattern
@@ -527,6 +541,7 @@ private struct SettingsData: Codable {
527541
let stopOnError: Bool
528542
let showVisualFeedback: Bool
529543
let playSoundFeedback: Bool
544+
let isActiveTargetMode: Bool? // Optional for backward compatibility
530545
let randomizeTiming: Bool
531546
let timingVariancePercentage: Double
532547
let distributionPattern: CPSRandomizer.DistributionPattern
@@ -550,6 +565,7 @@ struct SettingsExportData: Codable {
550565
let stopOnError: Bool
551566
let showVisualFeedback: Bool
552567
let playSoundFeedback: Bool
568+
let isActiveTargetMode: Bool? // Optional for backward compatibility
553569
let randomizeTiming: Bool
554570
let timingVariancePercentage: Double
555571
let distributionPattern: CPSRandomizer.DistributionPattern
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// CursorManager.swift
3+
// ClickIt
4+
//
5+
// Manages custom cursor states for active target mode
6+
//
7+
8+
import Cocoa
9+
import os.log
10+
11+
/// Manages cursor appearance for different automation modes
12+
final class CursorManager {
13+
static let shared = CursorManager()
14+
15+
private let logger = Logger(subsystem: "com.clickit.app", category: "CursorManager")
16+
private var isTargetCursorActive = false
17+
private var cursorUpdateTimer: Timer?
18+
19+
private init() {}
20+
21+
/// Shows the target/crosshair cursor for active target mode
22+
func showTargetCursor() {
23+
DispatchQueue.main.async { [weak self] in
24+
guard let self = self else { return }
25+
26+
if !self.isTargetCursorActive {
27+
self.isTargetCursorActive = true
28+
29+
// Set the cursor immediately
30+
NSCursor.crosshair.set()
31+
32+
// Keep re-setting the cursor on a timer to ensure it stays active
33+
// This is needed because system can reset cursor on window focus changes
34+
self.cursorUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
35+
guard let self = self, self.isTargetCursorActive else { return }
36+
NSCursor.crosshair.set()
37+
}
38+
39+
self.logger.info("Target cursor activated with timer refresh")
40+
}
41+
}
42+
}
43+
44+
/// Restores the normal system cursor
45+
func restoreNormalCursor() {
46+
DispatchQueue.main.async { [weak self] in
47+
guard let self = self else { return }
48+
49+
if self.isTargetCursorActive {
50+
self.isTargetCursorActive = false
51+
52+
// Stop the cursor update timer
53+
self.cursorUpdateTimer?.invalidate()
54+
self.cursorUpdateTimer = nil
55+
56+
// Restore arrow cursor
57+
NSCursor.arrow.set()
58+
59+
self.logger.info("Normal cursor restored")
60+
}
61+
}
62+
}
63+
64+
/// Force restore cursor (useful for cleanup on app termination)
65+
func forceRestoreNormalCursor() {
66+
DispatchQueue.main.async { [weak self] in
67+
guard let self = self else { return }
68+
69+
self.isTargetCursorActive = false
70+
self.cursorUpdateTimer?.invalidate()
71+
self.cursorUpdateTimer = nil
72+
NSCursor.arrow.set()
73+
74+
self.logger.info("Cursor forcefully restored")
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)