diff --git a/ClickIt/Info.plist b/ClickIt/Info.plist
index e6aeca3..55c61ac 100644
--- a/ClickIt/Info.plist
+++ b/ClickIt/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.5.3
+ 1.5.5
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
LSMinimumSystemVersion
diff --git a/Sources/ClickIt/ClickItApp.swift b/Sources/ClickIt/ClickItApp.swift
index 660b62c..2b93f7d 100644
--- a/Sources/ClickIt/ClickItApp.swift
+++ b/Sources/ClickIt/ClickItApp.swift
@@ -44,6 +44,14 @@ struct ClickItApp: App {
}
}
}
+
+ // Separate window for click test - can be moved independently
+ WindowGroup(id: "click-test-window") {
+ ClickTestWindow()
+ }
+ .windowResizability(.contentSize)
+ .defaultSize(width: 900, height: 750)
+ .windowToolbarStyle(.unified)
}
// MARK: - Safe Initialization
@@ -76,6 +84,8 @@ struct ClickItApp: App {
// Cleanup visual feedback overlay when app terminates
VisualFeedbackOverlay.shared.cleanup()
HotkeyManager.shared.cleanup()
+ // Restore cursor to normal
+ CursorManager.shared.forceRestoreNormalCursor()
}
print("ClickItApp: Safe app initialization completed")
diff --git a/Sources/ClickIt/Core/Click/ClickCoordinator.swift b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
index 1957426..51d479d 100644
--- a/Sources/ClickIt/Core/Click/ClickCoordinator.swift
+++ b/Sources/ClickIt/Core/Click/ClickCoordinator.swift
@@ -360,21 +360,37 @@ class ClickCoordinator: ObservableObject {
/// Simple automation step execution from working version
private func executeAutomationStep(configuration: AutomationConfiguration) async -> ClickResult {
print("ClickCoordinator: executeAutomationStep() - Simple working approach")
-
+
+ // Determine click location based on mode
+ let clickLocation: CGPoint // CoreGraphics coordinates for clicking
+ let visualFeedbackLocation: CGPoint // AppKit coordinates for visual feedback
+
+ if configuration.useDynamicMouseTracking {
+ // Active target mode: get current mouse position
+ let appKitLocation = NSEvent.mouseLocation
+ clickLocation = convertAppKitToCoreGraphicsMultiMonitor(appKitLocation)
+ visualFeedbackLocation = appKitLocation
+ print("ClickCoordinator: Active target mode - AppKit: \(appKitLocation) → CoreGraphics: \(clickLocation)")
+ } else {
+ // Fixed location mode: use configured location (already in CoreGraphics)
+ clickLocation = configuration.location
+ visualFeedbackLocation = convertCoreGraphicsToAppKitMultiMonitor(configuration.location)
+ }
+
// Use the working performSingleClick method
let result = await performSingleClick(
configuration: ClickConfiguration(
type: configuration.clickType,
- location: configuration.location,
+ location: clickLocation,
targetPID: nil
)
)
-
- // Update visual feedback if enabled
+
+ // Update visual feedback if enabled (requires AppKit coordinates)
if configuration.showVisualFeedback {
- VisualFeedbackOverlay.shared.updateOverlay(at: configuration.location, isActive: true)
+ VisualFeedbackOverlay.shared.updateOverlay(at: visualFeedbackLocation, isActive: true)
}
-
+
return result
}
diff --git a/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift b/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
index 487e118..68591be 100644
--- a/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
+++ b/Sources/ClickIt/Core/Hotkeys/HotkeyManager.swift
@@ -17,12 +17,22 @@ class HotkeyManager: ObservableObject {
@Published var emergencyStopActivated: Bool = false
// MARK: - Private Properties
-
+
private var globalEventMonitor: Any?
private var localEventMonitor: Any?
+ private var globalMouseMonitor: Any?
+ private var localMouseMonitor: Any?
private var lastHotkeyTime: TimeInterval = 0
private let hotkeyDebounceInterval: TimeInterval = 0.01 // 10ms debounce for ultra-fast emergency response
private var responseTimeTracker: EmergencyStopResponseTracker?
+
+ // MARK: - Mouse Click Handling
+
+ /// Callback invoked when right mouse button is clicked (for active target mode - TOGGLE)
+ var onRightMouseClick: (() -> Void)?
+
+ private var lastMouseClickTime: TimeInterval = 0
+ private let mouseClickDebounceInterval: TimeInterval = 0.1 // 100ms debounce for mouse clicks
// MARK: - Initialization
@@ -36,6 +46,7 @@ class HotkeyManager: ObservableObject {
func cleanup() {
unregisterGlobalHotkey()
+ unregisterMouseMonitor()
}
func registerGlobalHotkey(_ config: HotkeyConfiguration) -> Bool {
@@ -70,15 +81,49 @@ class HotkeyManager: ObservableObject {
NSEvent.removeMonitor(monitor)
globalEventMonitor = nil
}
-
+
if let monitor = localEventMonitor {
NSEvent.removeMonitor(monitor)
localEventMonitor = nil
}
-
+
isRegistered = false
print("HotkeyManager: Successfully unregistered hotkey monitoring")
}
+
+ /// Register global mouse click monitoring for active target mode
+ func registerMouseMonitor() {
+ // Unregister existing monitors first
+ unregisterMouseMonitor()
+
+ // Monitor right mouse clicks globally (when app is in background)
+ globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .rightMouseDown) { [weak self] event in
+ self?.handleMouseClick(event)
+ }
+
+ // Monitor right mouse clicks locally (when app is in foreground)
+ localMouseMonitor = NSEvent.addLocalMonitorForEvents(matching: .rightMouseDown) { [weak self] event in
+ self?.handleMouseClick(event)
+ return event // Pass through the event
+ }
+
+ print("HotkeyManager: Successfully registered right-click monitoring (toggle start/stop)")
+ }
+
+ /// Unregister mouse click monitoring
+ func unregisterMouseMonitor() {
+ if let monitor = globalMouseMonitor {
+ NSEvent.removeMonitor(monitor)
+ globalMouseMonitor = nil
+ }
+
+ if let monitor = localMouseMonitor {
+ NSEvent.removeMonitor(monitor)
+ localMouseMonitor = nil
+ }
+
+ print("HotkeyManager: Successfully unregistered mouse click monitoring")
+ }
// MARK: - Private Methods
@@ -91,12 +136,12 @@ class HotkeyManager: ObservableObject {
private func handleMultiKeyEvent(_ event: NSEvent) {
let currentTime = CFAbsoluteTimeGetCurrent()
-
+
// Debounce emergency stop to prevent rapid fire (50ms for emergency response)
if currentTime - lastHotkeyTime < hotkeyDebounceInterval {
return
}
-
+
// Check if this event matches any emergency stop key
if let matchedConfig = matchEmergencyStopKey(event) {
print("HotkeyManager: Emergency stop key activated - \(matchedConfig.description)")
@@ -104,6 +149,25 @@ class HotkeyManager: ObservableObject {
handleEmergencyStop(triggeredBy: matchedConfig)
}
}
+
+ private func handleMouseClick(_ event: NSEvent) {
+ let currentTime = CFAbsoluteTimeGetCurrent()
+
+ // Debounce mouse clicks to prevent accidental double-clicks
+ if currentTime - lastMouseClickTime < mouseClickDebounceInterval {
+ return
+ }
+
+ lastMouseClickTime = currentTime
+
+ // Handle right-click for toggle (start/stop)
+ if event.type == .rightMouseDown {
+ print("HotkeyManager: Right-click detected for active target mode (TOGGLE)")
+ Task { @MainActor in
+ self.onRightMouseClick?()
+ }
+ }
+ }
private func matchEmergencyStopKey(_ event: NSEvent) -> HotkeyConfiguration? {
// Check all available emergency stop configurations
diff --git a/Sources/ClickIt/Core/Models/ClickSettings.swift b/Sources/ClickIt/Core/Models/ClickSettings.swift
index b6052a4..139ebe3 100644
--- a/Sources/ClickIt/Core/Models/ClickSettings.swift
+++ b/Sources/ClickIt/Core/Models/ClickSettings.swift
@@ -105,7 +105,14 @@ class ClickSettings: ObservableObject {
saveSettings()
}
}
-
+
+ /// Active target mode - clicks follow cursor position
+ @Published var isActiveTargetMode: Bool = false {
+ didSet {
+ saveSettings()
+ }
+ }
+
// MARK: - CPS Randomization Properties
/// Whether to enable CPS timing randomization
@@ -231,6 +238,7 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
showVisualFeedback: showVisualFeedback,
playSoundFeedback: playSoundFeedback,
+ isActiveTargetMode: isActiveTargetMode,
randomizeTiming: randomizeTiming,
timingVariancePercentage: timingVariancePercentage,
distributionPattern: distributionPattern,
@@ -268,6 +276,7 @@ class ClickSettings: ObservableObject {
stopOnError = settings.stopOnError
showVisualFeedback = settings.showVisualFeedback
playSoundFeedback = settings.playSoundFeedback
+ isActiveTargetMode = settings.isActiveTargetMode ?? false // Default for backward compatibility
randomizeTiming = settings.randomizeTiming
timingVariancePercentage = settings.timingVariancePercentage
distributionPattern = settings.distributionPattern
@@ -293,6 +302,7 @@ class ClickSettings: ObservableObject {
stopOnError = false
showVisualFeedback = true
playSoundFeedback = false
+ isActiveTargetMode = false
randomizeTiming = false
timingVariancePercentage = 0.1
distributionPattern = .normal
@@ -318,8 +328,8 @@ class ClickSettings: ObservableObject {
func createAutomationConfiguration() -> AutomationConfiguration {
let maxClicksValue = durationMode == .clickCount ? maxClicks : nil
let maxDurationValue = durationMode == .timeLimit ? durationSeconds : nil
-
- print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback)")
+
+ print("ClickSettings: Creating automation config with location \(clickLocation), showVisualFeedback: \(showVisualFeedback), activeTargetMode: \(isActiveTargetMode)")
return AutomationConfiguration(
location: clickLocation,
@@ -331,6 +341,8 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(locationVariance),
+ useDynamicMouseTracking: isActiveTargetMode,
+ showVisualFeedback: showVisualFeedback,
cpsRandomizerConfig: createCPSRandomizerConfiguration()
)
}
@@ -353,13 +365,14 @@ class ClickSettings: ObservableObject {
stopOnError: stopOnError,
showVisualFeedback: showVisualFeedback,
playSoundFeedback: playSoundFeedback,
+ isActiveTargetMode: isActiveTargetMode,
randomizeTiming: randomizeTiming,
timingVariancePercentage: timingVariancePercentage,
distributionPattern: distributionPattern,
humannessLevel: humannessLevel,
schedulingMode: schedulingMode,
scheduledDateTime: scheduledDateTime,
- exportVersion: "1.1",
+ exportVersion: "1.2",
exportDate: Date(),
appVersion: AppConstants.appVersion
)
@@ -403,6 +416,7 @@ class ClickSettings: ObservableObject {
stopOnError = importData.stopOnError
showVisualFeedback = importData.showVisualFeedback
playSoundFeedback = importData.playSoundFeedback
+ isActiveTargetMode = importData.isActiveTargetMode ?? false // Default for backward compatibility
randomizeTiming = importData.randomizeTiming
timingVariancePercentage = importData.timingVariancePercentage
distributionPattern = importData.distributionPattern
@@ -527,6 +541,7 @@ private struct SettingsData: Codable {
let stopOnError: Bool
let showVisualFeedback: Bool
let playSoundFeedback: Bool
+ let isActiveTargetMode: Bool? // Optional for backward compatibility
let randomizeTiming: Bool
let timingVariancePercentage: Double
let distributionPattern: CPSRandomizer.DistributionPattern
@@ -550,6 +565,7 @@ struct SettingsExportData: Codable {
let stopOnError: Bool
let showVisualFeedback: Bool
let playSoundFeedback: Bool
+ let isActiveTargetMode: Bool? // Optional for backward compatibility
let randomizeTiming: Bool
let timingVariancePercentage: Double
let distributionPattern: CPSRandomizer.DistributionPattern
diff --git a/Sources/ClickIt/Services/CursorManager.swift b/Sources/ClickIt/Services/CursorManager.swift
new file mode 100644
index 0000000..d98b614
--- /dev/null
+++ b/Sources/ClickIt/Services/CursorManager.swift
@@ -0,0 +1,77 @@
+//
+// CursorManager.swift
+// ClickIt
+//
+// Manages custom cursor states for active target mode
+//
+
+import Cocoa
+import os.log
+
+/// Manages cursor appearance for different automation modes
+final class CursorManager {
+ static let shared = CursorManager()
+
+ private let logger = Logger(subsystem: "com.clickit.app", category: "CursorManager")
+ private var isTargetCursorActive = false
+ private var cursorUpdateTimer: Timer?
+
+ private init() {}
+
+ /// Shows the target/crosshair cursor for active target mode
+ func showTargetCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ if !self.isTargetCursorActive {
+ self.isTargetCursorActive = true
+
+ // Set the cursor immediately
+ NSCursor.crosshair.set()
+
+ // Keep re-setting the cursor on a timer to ensure it stays active
+ // This is needed because system can reset cursor on window focus changes
+ self.cursorUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
+ guard let self = self, self.isTargetCursorActive else { return }
+ NSCursor.crosshair.set()
+ }
+
+ self.logger.info("Target cursor activated with timer refresh")
+ }
+ }
+ }
+
+ /// Restores the normal system cursor
+ func restoreNormalCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ if self.isTargetCursorActive {
+ self.isTargetCursorActive = false
+
+ // Stop the cursor update timer
+ self.cursorUpdateTimer?.invalidate()
+ self.cursorUpdateTimer = nil
+
+ // Restore arrow cursor
+ NSCursor.arrow.set()
+
+ self.logger.info("Normal cursor restored")
+ }
+ }
+ }
+
+ /// Force restore cursor (useful for cleanup on app termination)
+ func forceRestoreNormalCursor() {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ self.isTargetCursorActive = false
+ self.cursorUpdateTimer?.invalidate()
+ self.cursorUpdateTimer = nil
+ NSCursor.arrow.set()
+
+ self.logger.info("Cursor forcefully restored")
+ }
+ }
+}
diff --git a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
index 035e845..eec4b4b 100644
--- a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
+++ b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift
@@ -21,7 +21,12 @@ class ClickItViewModel: ObservableObject {
// Settings
@Published var clickSettings = ClickSettings()
-
+
+ // Expose settings for UI binding
+ var settings: ClickSettings {
+ clickSettings
+ }
+
// Configuration Properties
@Published var intervalHours = 0 {
didSet {
@@ -112,6 +117,9 @@ class ClickItViewModel: ObservableObject {
// MARK: - Dependencies
private let clickCoordinator = ClickCoordinator.shared
private let schedulingManager = SchedulingManager.shared
+
+ // MARK: - Active Target Mode State
+ private var isProcessingActiveTargetClick = false
// MARK: - Initialization
init() {
@@ -153,8 +161,15 @@ class ClickItViewModel: ObservableObject {
return
}
- guard let point = targetPoint, canStartAutomation else {
+ // In active target mode, only require targetPoint and interval
+ // In normal mode, require full canStartAutomation checks
+ let hasMinimumRequirements = targetPoint != nil && totalMilliseconds > 0 && !isRunning
+ let canStart = clickSettings.isActiveTargetMode ? hasMinimumRequirements : canStartAutomation
+
+ guard let point = targetPoint, canStart else {
print("ClickItViewModel: Cannot start automation - missing prerequisites")
+ print(" targetPoint: \(targetPoint != nil), totalMs: \(totalMilliseconds), isRunning: \(isRunning)")
+ print(" activeTargetMode: \(clickSettings.isActiveTargetMode), canStartAutomation: \(canStartAutomation)")
return
}
@@ -173,6 +188,13 @@ class ClickItViewModel: ObservableObject {
private func executeAutomation(at point: CGPoint) {
print("ClickItViewModel: Executing automation immediately")
+ // Disable mouse monitoring while automation is running to prevent
+ // automated clicks from triggering the click handler
+ if clickSettings.isActiveTargetMode {
+ HotkeyManager.shared.unregisterMouseMonitor()
+ print("ClickItViewModel: Disabled mouse monitoring during automation")
+ }
+
let config = createAutomationConfiguration(at: point)
clickCoordinator.startAutomation(with: config)
isRunning = true
@@ -213,10 +235,10 @@ class ClickItViewModel: ObservableObject {
targetApplication: nil,
maxClicks: durationMode == .clickCount ? maxClicks : nil,
maxDuration: durationMode == .timeLimit ? durationSeconds : nil,
- stopOnError: stopOnError,
+ stopOnError: clickSettings.isActiveTargetMode ? false : stopOnError, // Disable stopOnError for active target mode
randomizeLocation: randomizeLocation,
locationVariance: CGFloat(randomizeLocation ? locationVariance : 0),
- useDynamicMouseTracking: false, // Normal automation uses fixed position
+ useDynamicMouseTracking: clickSettings.isActiveTargetMode, // Use active target mode setting
showVisualFeedback: showVisualFeedback
)
}
@@ -268,6 +290,12 @@ class ClickItViewModel: ObservableObject {
isRunning = false
appStatus = .ready
+ // Re-enable mouse monitoring if active target mode is still enabled
+ if clickSettings.isActiveTargetMode {
+ HotkeyManager.shared.registerMouseMonitor()
+ print("ClickItViewModel: Re-enabled mouse monitoring after automation stopped")
+ }
+
print("ClickItViewModel: Stopped automation with direct ClickCoordinator")
}
@@ -453,11 +481,11 @@ class ClickItViewModel: ObservableObject {
self?.updateStatistics()
}
.store(in: &cancellables)
-
+
// Monitor automation active state to sync UI state
clickCoordinator.$isActive.sink { [weak self] isActive in
guard let self = self else { return }
-
+
// Sync ViewModel state with ClickCoordinator state
if !isActive && (self.isRunning || self.isPaused) {
print("ClickItViewModel: Automation stopped externally (e.g., DELETE key), updating UI state")
@@ -466,31 +494,130 @@ class ClickItViewModel: ObservableObject {
self.appStatus = .ready
// Also cancel any active timer when automation stops
self.cancelTimer()
+
+ // Re-enable mouse monitoring if active target mode is still enabled
+ if self.clickSettings.isActiveTargetMode {
+ HotkeyManager.shared.registerMouseMonitor()
+ print("ClickItViewModel: Re-enabled mouse monitoring after external stop")
+ }
}
}
.store(in: &cancellables)
+
+ // Monitor active target mode changes
+ clickSettings.$isActiveTargetMode.sink { [weak self] isEnabled in
+ guard let self = self else { return }
+ self.handleActiveTargetModeChange(isEnabled)
+ }
+ .store(in: &cancellables)
}
private func updateStatistics() {
// SIMPLE WORKING APPROACH: Use ClickCoordinator statistics directly
statistics = clickCoordinator.getSessionStatistics()
}
-
+
+ // MARK: - Active Target Mode Management
+
+ private func handleActiveTargetModeChange(_ isEnabled: Bool) {
+ print("ClickItViewModel: Active target mode changed to \(isEnabled)")
+
+ if isEnabled {
+ // Enable active target mode
+ CursorManager.shared.showTargetCursor()
+ setupMouseClickHandler()
+ } else {
+ // Disable active target mode
+ CursorManager.shared.restoreNormalCursor()
+ removeMouseClickHandler()
+ }
+ }
+
+ private func setupMouseClickHandler() {
+ // Set up the RIGHT click handler for active target mode (TOGGLE start/stop)
+ HotkeyManager.shared.onRightMouseClick = { [weak self] in
+ Task { @MainActor in
+ self?.handleActiveTargetRightClick()
+ }
+ }
+
+ // Register mouse monitoring
+ HotkeyManager.shared.registerMouseMonitor()
+ print("ClickItViewModel: Right-click handler registered (TOGGLE start/stop)")
+ }
+
+ private func removeMouseClickHandler() {
+ // Remove the click handler
+ HotkeyManager.shared.onRightMouseClick = nil
+
+ // Unregister mouse monitoring
+ HotkeyManager.shared.unregisterMouseMonitor()
+ print("ClickItViewModel: Right-click handler removed")
+ }
+
+ private func handleActiveTargetRightClick() {
+ // Prevent re-entrancy from rapid clicks or automated clicks
+ guard !isProcessingActiveTargetClick else {
+ print("ClickItViewModel: Ignoring right-click - already processing")
+ return
+ }
+
+ isProcessingActiveTargetClick = true
+ defer {
+ // Reset the flag after a short delay to allow next click
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
+ self?.isProcessingActiveTargetClick = false
+ }
+ }
+
+ print("ClickItViewModel: Active target RIGHT-CLICK detected, isRunning: \(isRunning)")
+
+ // Right-click: TOGGLE automation (start if stopped, stop if running)
+ if isRunning {
+ // Stop automation
+ stopAutomation()
+ print("ClickItViewModel: Stopped automation via right-click")
+ } else {
+ // Start automation - but only if other prerequisites are met
+ if canStartAutomation || clickSettings.isActiveTargetMode {
+ // In active target mode, capture the current mouse position
+ // This is just for validation - the actual clicking will use live position
+ if clickSettings.isActiveTargetMode {
+ let currentMousePosition = NSEvent.mouseLocation
+ targetPoint = currentMousePosition
+ clickSettings.clickLocation = currentMousePosition
+ print("ClickItViewModel: Captured mouse position for active target mode: \(currentMousePosition)")
+ }
+
+ startAutomation()
+ print("ClickItViewModel: Started automation via right-click")
+ } else {
+ print("ClickItViewModel: Cannot start automation - prerequisites not met")
+ }
+ }
+ }
+
// MARK: - Emergency Stop
/// Performs emergency stop using ClickCoordinator directly
func emergencyStopAutomation() {
// SIMPLE WORKING APPROACH: Direct ClickCoordinator call
clickCoordinator.emergencyStopAutomation()
-
+
// Cancel any active timer
cancelTimer()
-
+
// Update UI state immediately
isRunning = false
isPaused = false
appStatus = .ready
-
+
+ // Re-enable mouse monitoring if active target mode is still enabled
+ if clickSettings.isActiveTargetMode {
+ HotkeyManager.shared.registerMouseMonitor()
+ print("ClickItViewModel: Re-enabled mouse monitoring after emergency stop")
+ }
+
print("ClickItViewModel: Emergency stop executed with direct ClickCoordinator")
}
diff --git a/Sources/ClickIt/UI/Views/AdvancedTab.swift b/Sources/ClickIt/UI/Views/AdvancedTab.swift
index 540e24a..8829f6c 100644
--- a/Sources/ClickIt/UI/Views/AdvancedTab.swift
+++ b/Sources/ClickIt/UI/Views/AdvancedTab.swift
@@ -11,7 +11,8 @@ import SwiftUI
/// Advanced tab containing developer information and app details
struct AdvancedTab: View {
@EnvironmentObject private var viewModel: ClickItViewModel
-
+ @Environment(\.openWindow) private var openWindow
+
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
@@ -20,13 +21,13 @@ struct AdvancedTab: View {
Image(systemName: "wrench.and.screwdriver")
.font(.title2)
.foregroundColor(.purple)
-
+
Text("Advanced")
.font(.title2)
.fontWeight(.semibold)
-
+
Spacer()
-
+
// Build info indicator
Text("Debug")
.font(.caption)
@@ -38,15 +39,18 @@ struct AdvancedTab: View {
}
.padding(.horizontal, 16)
.padding(.top, 8)
-
+
VStack(spacing: 12) {
+ // Developer Tools
+ DeveloperTools(openWindow: openWindow)
+
// App information
AppInformation()
-
+
// System status
SystemStatus()
-
- // Debug information
+
+ // Debug information
DebugInformation()
}
.padding(.horizontal, 16)
@@ -58,6 +62,97 @@ struct AdvancedTab: View {
}
}
+// MARK: - Developer Tools Component
+
+private struct DeveloperTools: View {
+ let openWindow: OpenWindowAction
+ @State private var showingWindowDetectionTest = false
+ @State private var isExpanded = true
+
+ var body: some View {
+ DisclosureGroup("Developer Tools", isExpanded: $isExpanded) {
+ VStack(spacing: 12) {
+ // Description
+ Text("Testing utilities for validating auto-clicker functionality")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.top, 8)
+
+ // Click Test Window Button
+ Button {
+ openWindow(id: "click-test-window")
+ } label: {
+ HStack {
+ Image(systemName: "hand.tap.fill")
+ .font(.system(size: 16))
+ .foregroundColor(.blue)
+ .frame(width: 24)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Click Test Window")
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Text("Test auto-clicker with visual targets (opens in separate window)")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Image(systemName: "arrow.up.right.square")
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ }
+ .padding(10)
+ .background(Color(NSColor.controlBackgroundColor).opacity(0.5))
+ .cornerRadius(6)
+ }
+ .buttonStyle(.plain)
+
+ // Window Detection Test Button
+ Button {
+ showingWindowDetectionTest = true
+ } label: {
+ HStack {
+ Image(systemName: "rectangle.3.offgrid")
+ .font(.system(size: 16))
+ .foregroundColor(.green)
+ .frame(width: 24)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Window Detection Test")
+ .font(.subheadline)
+ .fontWeight(.medium)
+
+ Text("Test window targeting functionality")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Image(systemName: "arrow.up.right.square")
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ }
+ .padding(10)
+ .background(Color(NSColor.controlBackgroundColor).opacity(0.5))
+ .cornerRadius(6)
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(12)
+ .background(Color(NSColor.controlBackgroundColor))
+ .cornerRadius(8)
+ .sheet(isPresented: $showingWindowDetectionTest) {
+ WindowDetectionTestView()
+ }
+ }
+}
+
// MARK: - App Information Component
private struct AppInformation: View {
diff --git a/Sources/ClickIt/UI/Views/ClickTestWindow.swift b/Sources/ClickIt/UI/Views/ClickTestWindow.swift
new file mode 100644
index 0000000..391695a
--- /dev/null
+++ b/Sources/ClickIt/UI/Views/ClickTestWindow.swift
@@ -0,0 +1,328 @@
+//
+// ClickTestWindow.swift
+// ClickIt
+//
+// Click testing window for validating auto-clicker functionality
+//
+
+import SwiftUI
+
+/// Test window for validating click automation functionality
+struct ClickTestWindow: View {
+ @Environment(\.dismiss) private var dismiss
+ @State private var clickCounts: [String: Int] = [:]
+ @State private var lastClickTime: Date?
+ @State private var lastClickPosition: CGPoint?
+ @State private var totalClicks: Int = 0
+ @State private var showClickIndicator: Bool = false
+ @State private var indicatorPosition: CGPoint = .zero
+
+ // Target zones
+ private let targets = [
+ ClickTarget(id: "top-left", name: "Top Left", color: .blue, position: .topLeft),
+ ClickTarget(id: "top-right", name: "Top Right", color: .green, position: .topRight),
+ ClickTarget(id: "center", name: "Center", color: .purple, position: .center),
+ ClickTarget(id: "bottom-left", name: "Bottom Left", color: .orange, position: .bottomLeft),
+ ClickTarget(id: "bottom-right", name: "Bottom Right", color: .red, position: .bottomRight)
+ ]
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Custom header with close button
+ HStack {
+ Text("Click Test Window")
+ .font(.headline)
+ .fontWeight(.semibold)
+
+ Spacer()
+
+ Button("Reset") {
+ resetCounters()
+ }
+ .buttonStyle(.bordered)
+
+ Button("Close") {
+ dismiss()
+ }
+ .buttonStyle(.borderedProminent)
+ .keyboardShortcut(.cancelAction)
+ }
+ .padding()
+ .background(Color(NSColor.windowBackgroundColor))
+
+ Divider()
+
+ ScrollView {
+ VStack(spacing: 0) {
+ // Header with instructions
+ headerSection
+
+ Divider()
+
+ // Main click testing area
+ ZStack {
+ // Background
+ Color(NSColor.controlBackgroundColor)
+
+ // Target zones - use VStack/HStack layout instead of absolute positioning
+ VStack(spacing: 30) {
+ // Top row
+ HStack(spacing: 60) {
+ targetView(for: targets[0]) // Top Left
+ Spacer()
+ targetView(for: targets[1]) // Top Right
+ }
+
+ Spacer()
+
+ // Center row
+ HStack {
+ Spacer()
+ targetView(for: targets[2]) // Center
+ Spacer()
+ }
+
+ Spacer()
+
+ // Bottom row
+ HStack(spacing: 60) {
+ targetView(for: targets[3]) // Bottom Left
+ Spacer()
+ targetView(for: targets[4]) // Bottom Right
+ }
+ }
+ .padding(40)
+
+ // Click indicator overlay
+ if showClickIndicator {
+ Circle()
+ .fill(Color.yellow.opacity(0.5))
+ .frame(width: 30, height: 30)
+ .position(indicatorPosition)
+ .transition(.scale.combined(with: .opacity))
+ }
+ }
+ .frame(height: 500)
+
+ Divider()
+
+ // Statistics panel
+ statisticsPanel
+ }
+ }
+ }
+ .frame(width: 900, height: 750)
+ }
+
+ // MARK: - View Components
+
+ private var headerSection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Image(systemName: "hand.tap.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.accentColor)
+
+ Text("Click Test Window")
+ .font(.title2)
+ .fontWeight(.bold)
+ }
+
+ VStack(alignment: .leading, spacing: 6) {
+ instructionRow(icon: "1.circle.fill", text: "Position your auto-clicker over any colored target zone")
+ instructionRow(icon: "2.circle.fill", text: "Start the auto-clicker from the main window")
+ instructionRow(icon: "3.circle.fill", text: "Watch the click counters update in real-time")
+ instructionRow(icon: "scope", text: "For Active Target Mode: Enable it in main window, then click on targets")
+ }
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(Color(NSColor.controlBackgroundColor).opacity(0.5))
+ }
+
+ private func instructionRow(icon: String, text: String) -> some View {
+ HStack(spacing: 8) {
+ Image(systemName: icon)
+ .frame(width: 20)
+ .foregroundColor(.accentColor)
+ Text(text)
+ }
+ }
+
+ private func targetView(for target: ClickTarget) -> some View {
+ let size: CGFloat = 130
+
+ return VStack(spacing: 8) {
+ // Click counter
+ Text("\(clickCounts[target.id] ?? 0)")
+ .font(.system(size: 36, weight: .bold, design: .rounded))
+ .foregroundColor(.white)
+
+ // Target name
+ Text(target.name)
+ .font(.caption)
+ .fontWeight(.semibold)
+ .foregroundColor(.white)
+ }
+ .frame(width: size, height: size)
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(target.color.opacity(0.85))
+ .shadow(color: target.color.opacity(0.4), radius: 10, x: 0, y: 5)
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 16)
+ .stroke(Color.white.opacity(0.4), lineWidth: 3)
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ handleClick(on: target.id)
+ }
+ }
+
+ private var statisticsPanel: some View {
+ HStack(spacing: 20) {
+ // Total clicks
+ statisticView(
+ title: "Total Clicks",
+ value: "\(totalClicks)",
+ icon: "hand.tap",
+ color: .blue
+ )
+
+ Divider()
+
+ // Last click time
+ statisticView(
+ title: "Last Click",
+ value: lastClickTimeString,
+ icon: "clock",
+ color: .green
+ )
+
+ Divider()
+
+ // Most clicked target
+ statisticView(
+ title: "Most Clicked",
+ value: mostClickedTarget,
+ icon: "star",
+ color: .orange
+ )
+
+ Spacer()
+
+ // Visual feedback indicator
+ HStack(spacing: 8) {
+ Circle()
+ .fill(showClickIndicator ? Color.green : Color.gray)
+ .frame(width: 12, height: 12)
+
+ Text("Click Detection")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ .padding()
+ .background(Color(NSColor.controlBackgroundColor))
+ }
+
+ private func statisticView(title: String, value: String, icon: String, color: Color) -> some View {
+ HStack(spacing: 12) {
+ Image(systemName: icon)
+ .font(.title2)
+ .foregroundColor(color)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Text(value)
+ .font(.headline)
+ .fontWeight(.semibold)
+ }
+ }
+ }
+
+ // MARK: - Helper Methods
+
+ private func handleClick(on targetId: String) {
+ // Update click count
+ clickCounts[targetId, default: 0] += 1
+ totalClicks += 1
+ lastClickTime = Date()
+
+ // Show visual feedback
+ withAnimation(.easeOut(duration: 0.3)) {
+ showClickIndicator = true
+ }
+
+ // Hide indicator after delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ withAnimation {
+ showClickIndicator = false
+ }
+ }
+
+ // Provide haptic feedback if available
+ NSHapticFeedbackManager.defaultPerformer.perform(
+ .alignment,
+ performanceTime: .now
+ )
+
+ print("ClickTestWindow: Click detected on \(targetId), total: \(totalClicks)")
+ }
+
+ private func resetCounters() {
+ clickCounts.removeAll()
+ totalClicks = 0
+ lastClickTime = nil
+ lastClickPosition = nil
+ print("ClickTestWindow: Counters reset")
+ }
+
+ private var lastClickTimeString: String {
+ guard let time = lastClickTime else {
+ return "None"
+ }
+
+ let formatter = DateFormatter()
+ formatter.timeStyle = .medium
+ return formatter.string(from: time)
+ }
+
+ private var mostClickedTarget: String {
+ guard let maxTarget = clickCounts.max(by: { $0.value < $1.value }) else {
+ return "None"
+ }
+
+ if let target = targets.first(where: { $0.id == maxTarget.key }) {
+ return "\(target.name) (\(maxTarget.value))"
+ }
+
+ return "None"
+ }
+}
+
+// MARK: - Supporting Types
+
+struct ClickTarget: Identifiable {
+ let id: String
+ let name: String
+ let color: Color
+ let position: TargetPosition
+}
+
+enum TargetPosition {
+ case topLeft, topRight, center, bottomLeft, bottomRight
+}
+
+// MARK: - Preview
+
+struct ClickTestWindow_Previews: PreviewProvider {
+ static var previews: some View {
+ ClickTestWindow()
+ }
+}
diff --git a/Sources/ClickIt/UI/Views/QuickStartTab.swift b/Sources/ClickIt/UI/Views/QuickStartTab.swift
index 6bd1285..109fb93 100644
--- a/Sources/ClickIt/UI/Views/QuickStartTab.swift
+++ b/Sources/ClickIt/UI/Views/QuickStartTab.swift
@@ -31,6 +31,9 @@ struct QuickStartTab: View {
// Target Point Selector (with Timer Support)
TargetPointSelectionCard(viewModel: viewModel)
+ // Active Target Mode Toggle
+ ActiveTargetModeCard()
+
// Inline Timing Controls
InlineTimingControls()
@@ -258,19 +261,19 @@ private struct CompactStatisticView: View {
let title: String
let value: String
let icon: String
-
+
var body: some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 14))
.foregroundColor(.blue)
-
+
Text(value)
.font(.system(.caption, design: .monospaced))
.fontWeight(.semibold)
.lineLimit(1)
.minimumScaleFactor(0.8)
-
+
Text(title)
.font(.system(size: 9))
.foregroundColor(.secondary)
@@ -282,6 +285,47 @@ private struct CompactStatisticView: View {
}
}
+// Active Target Mode Card
+private struct ActiveTargetModeCard: View {
+ @EnvironmentObject private var viewModel: ClickItViewModel
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack {
+ Image(systemName: "scope")
+ .foregroundColor(.purple)
+ .font(.system(size: 14))
+
+ Toggle("Active Target Mode", isOn: $viewModel.clickSettings.isActiveTargetMode)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .toggleStyle(.switch)
+ }
+
+ if viewModel.clickSettings.isActiveTargetMode {
+ HStack(spacing: 6) {
+ Image(systemName: "info.circle")
+ .foregroundColor(.purple)
+ .font(.system(size: 10))
+
+ Text("Cursor becomes crosshair. Right-click to start/stop clicking at cursor position.")
+ .font(.system(size: 10))
+ .foregroundColor(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+
+ Spacer()
+ }
+ .padding(.top, 4)
+ }
+ }
+ .padding(12)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color(NSColor.textBackgroundColor))
+ )
+ }
+}
+
// MARK: - Preview
struct QuickStartTab_Previews: PreviewProvider {