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 {