From 7289b3bb2b5cd0c63911e8f7ecb404d51e4570ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Pe=CC=A8draszewski?= Date: Tue, 4 Jul 2023 18:49:09 +0200 Subject: [PATCH 1/3] Allow user to stop sending mouse clicks after the mouse is moved --- auto-clicker.xcodeproj/project.pbxproj | 12 +++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++++ auto-clicker/Build Assets/Info.plist | 2 +- auto-clicker/Enums/MouseMove.swift | 51 +++++++++++++++++++ .../en-GB.lproj/Localizable.strings | 6 +++ auto-clicker/Models/FormState.swift | 2 + .../AutoClickSimulator.swift | 16 ++++++ .../Main/Components/MouseMoveModal.swift | 42 +++++++++++++++ .../Main/Components/MouseMoveSelector.swift | 24 +++++++++ auto-clicker/Views/Main/MainView.swift | 9 ++++ 10 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 auto-clicker/Enums/MouseMove.swift create mode 100644 auto-clicker/Views/Main/Components/MouseMoveModal.swift create mode 100644 auto-clicker/Views/Main/Components/MouseMoveSelector.swift diff --git a/auto-clicker.xcodeproj/project.pbxproj b/auto-clicker.xcodeproj/project.pbxproj index 65fd4f2..6231297 100644 --- a/auto-clicker.xcodeproj/project.pbxproj +++ b/auto-clicker.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 4C5D699D2A4ECDF800E72CEE /* NotificationsSettingsTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D699C2A4ECDF800E72CEE /* NotificationsSettingsTabView.swift */; }; 4C5D699F2A4EED3500E72CEE /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D699E2A4EED3500E72CEE /* NotificationService.swift */; }; + 5DB2BECC2A54826B008AFE05 /* MouseMoveSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB2BECB2A54826B008AFE05 /* MouseMoveSelector.swift */; }; + 5DB2BECE2A5482DC008AFE05 /* MouseMoveModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB2BECD2A5482DC008AFE05 /* MouseMoveModal.swift */; }; + 5DB2BED02A548325008AFE05 /* MouseMove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB2BECF2A548325008AFE05 /* MouseMove.swift */; }; B50022CA2875F5BF00610474 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50022C92875F5BF00610474 /* HelpCommands.swift */; }; B510760927F4A21500BB1CDA /* DateStrings in Frameworks */ = {isa = PBXBuildFile; productRef = B510760827F4A21500BB1CDA /* DateStrings */; }; B510760C27F4A23300BB1CDA /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = B510760B27F4A23300BB1CDA /* KeyboardShortcuts */; }; @@ -77,6 +80,9 @@ /* Begin PBXFileReference section */ 4C5D699C2A4ECDF800E72CEE /* NotificationsSettingsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsTabView.swift; sourceTree = ""; }; 4C5D699E2A4EED3500E72CEE /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 5DB2BECB2A54826B008AFE05 /* MouseMoveSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseMoveSelector.swift; sourceTree = ""; }; + 5DB2BECD2A5482DC008AFE05 /* MouseMoveModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseMoveModal.swift; sourceTree = ""; }; + 5DB2BECF2A548325008AFE05 /* MouseMove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseMove.swift; sourceTree = ""; }; B50022C92875F5BF00610474 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = ""; }; B510760E27F4A25400BB1CDA /* WindowStateService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowStateService.swift; sourceTree = ""; }; B510761027F4A33D00BB1CDA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -333,6 +339,8 @@ B5F6A01A27F3A8A6003CD730 /* StatBox.swift */, B510761E27F4B6F500BB1CDA /* KeyboardShortcutHint.swift */, B5BCE808287C2F4D00B739AD /* SmallText.swift */, + 5DB2BECB2A54826B008AFE05 /* MouseMoveSelector.swift */, + 5DB2BECD2A5482DC008AFE05 /* MouseMoveModal.swift */, ); path = Components; sourceTree = ""; @@ -380,6 +388,7 @@ isa = PBXGroup; children = ( B5E6395527CA76CB008B111A /* Duration.swift */, + 5DB2BECF2A548325008AFE05 /* MouseMove.swift */, B5BCE802287BF59900B739AD /* Colour.swift */, ); path = Enums; @@ -579,6 +588,7 @@ 4C5D699F2A4EED3500E72CEE /* NotificationService.swift in Sources */, B510763227FF7EC800BB1CDA /* InputAwareView.swift in Sources */, B5B6B46328032D3200C779FD /* PermissionsView.swift in Sources */, + 5DB2BED02A548325008AFE05 /* MouseMove.swift in Sources */, B510762327F4BB5F00BB1CDA /* KeyboardShortcutsSettingsTabView.swift in Sources */, B510761A27F4A5BD00BB1CDA /* AppDelegate.swift in Sources */, B5D603F528830A3600655D2C /* SettingsTabItemView.swift in Sources */, @@ -607,6 +617,7 @@ B5E92B1427F1087E00A7FC63 /* UnderlinedTextFieldStyle.swift in Sources */, B510762D27F4BF0C00BB1CDA /* Defaults+Workaround.swift in Sources */, B510761627F4A43B00BB1CDA /* GeneralSettingsTabView.swift in Sources */, + 5DB2BECE2A5482DC008AFE05 /* MouseMoveModal.swift in Sources */, B510761827F4A58500BB1CDA /* KeyboardShortcuts.swift in Sources */, B510763527FF811B00BB1CDA /* PressKeyListenerModal.swift in Sources */, B53027FA264C2748002B8610 /* DelayTimer.swift in Sources */, @@ -624,6 +635,7 @@ B5E92B1C27F1BA5E00A7FC63 /* ThemeService.swift in Sources */, C4345BB52846056000365CF9 /* ProcessInfo+Extensions.swift in Sources */, B5E6395327CA62EB008B111A /* ThemedButtonStyle.swift in Sources */, + 5DB2BECC2A54826B008AFE05 /* MouseMoveSelector.swift in Sources */, B510762127F4BB4900BB1CDA /* AppearanceSettingsTabView.swift in Sources */, B5BF094328832A4E008092D9 /* MenuBarView.swift in Sources */, ); diff --git a/auto-clicker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/auto-clicker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2bf6d6e..19d981b 100644 --- a/auto-clicker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/auto-clicker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -26,6 +26,15 @@ "revision" : "0dcedd56994d871f243f3d9c76590bfd9f8aba69", "version" : "1.5.0" } + }, + { + "identity" : "launchatlogin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/LaunchAtLogin", + "state" : { + "revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41", + "version" : "4.2.0" + } } ], "version" : 2 diff --git a/auto-clicker/Build Assets/Info.plist b/auto-clicker/Build Assets/Info.plist index e98288c..6f4c362 100644 --- a/auto-clicker/Build Assets/Info.plist +++ b/auto-clicker/Build Assets/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 97ad514 + ef40c75 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/auto-clicker/Enums/MouseMove.swift b/auto-clicker/Enums/MouseMove.swift new file mode 100644 index 0000000..4ad599c --- /dev/null +++ b/auto-clicker/Enums/MouseMove.swift @@ -0,0 +1,51 @@ +// +// MouseMove.swift +// auto-clicker +// +// Created by Tomasz Pędraszewski on 04/07/2023. +// + +import Foundation +import SwiftUI + +enum MouseMove: String, CustomStringConvertible, CaseIterable, Identifiable, Codable { + case enabled = "mousemove_enabled" + case disabled = "mousemove_disabled" + + var id: String { + self.rawValue + } + + var description: String { + self.rawValue + } + + var localised: LocalizedStringKey { + LocalizedStringKey(self.description) + } + + var textView: some View { + switch self { + case .enabled, .disabled: + return Text(self.description) + } + } + + func buttonView(action: @escaping () -> Void) -> some View { + switch self { + case .enabled, .disabled: + return Button(action: action) { + Text(self.localised, comment: "Mouse move option buttons") + } + } + } + + func asBoolean() -> Bool { + switch self { + case .enabled: + return true + case .disabled: + return false + } + } +} diff --git a/auto-clicker/Localisation/en-GB.lproj/Localizable.strings b/auto-clicker/Localisation/en-GB.lproj/Localizable.strings index 954dc5e..fb151c5 100644 --- a/auto-clicker/Localisation/en-GB.lproj/Localizable.strings +++ b/auto-clicker/Localisation/en-GB.lproj/Localizable.strings @@ -15,6 +15,8 @@ "main_window_second" = "second"; "main_window_seconds" = "seconds"; "main_window_before_starting" = " before starting"; +"main_window_stop_mousemove" = "And"; +"main_window_stop_mousemove_end" = "when the mouse is moved"; "main_window_start_btn" = "START"; "main_window_stop_btn" = "STOP"; "main_window_stat_box_next_press_at" = "NEXT PRESS AT"; @@ -27,6 +29,10 @@ "duration_hours" = "Hours(s)"; "duration_modal_cancel_button" = "Cancel"; +"mousemove_enabled" = "stop"; +"mousemove_disabled" = "continue"; +"mousemove_modal_cancel_button" = "Cancel"; + "key_listener_modal_press_prompt" = "Press your desired input..."; "key_listener_modal_dismiss_key_prompt" = "Hold the escape key when done."; "key_listener_modal_dismiss_key_override" = "To use the escape key itself, press it twice."; diff --git a/auto-clicker/Models/FormState.swift b/auto-clicker/Models/FormState.swift index 6f9f869..9b0c650 100644 --- a/auto-clicker/Models/FormState.swift +++ b/auto-clicker/Models/FormState.swift @@ -14,6 +14,7 @@ struct FormState: Codable, Defaults.Serializable { var pressAmount: Int var startDelay: Int var repeatAmount: Int + var stopOnMouseMove: MouseMove } extension FormState { @@ -24,5 +25,6 @@ extension FormState { self.pressAmount = DEFAULT_PRESS_AMOUNT self.startDelay = DEFAULT_START_DELAY self.repeatAmount = DEFAULT_REPEAT_AMOUNT + self.stopOnMouseMove = MouseMove.disabled } } diff --git a/auto-clicker/Observable Objects/AutoClickSimulator.swift b/auto-clicker/Observable Objects/AutoClickSimulator.swift index 1c51525..db72376 100644 --- a/auto-clicker/Observable Objects/AutoClickSimulator.swift +++ b/auto-clicker/Observable Objects/AutoClickSimulator.swift @@ -31,6 +31,8 @@ final class AutoClickSimulator: ObservableObject { private var timer: Timer? private var mouseLocation: NSPoint { NSEvent.mouseLocation } private var activity: Cancellable? + + private var monitorObject: Any? = nil func start() { self.isAutoClicking = true @@ -61,6 +63,10 @@ final class AutoClickSimulator: ObservableObject { userInfo: nil, repeats: true) + if (Defaults[.autoClickerState].stopOnMouseMove.asBoolean()) { + startMouseMonitoring() + } + if Defaults[.notifyOnStart] { NotificationService.scheduleNotification(title: "Started", date: self.nextClickAt) } @@ -72,6 +78,10 @@ final class AutoClickSimulator: ObservableObject { func stop() { self.isAutoClicking = false + + if let monitorObject = self.monitorObject { + NSEvent.removeMonitor(monitorObject) + } if let startMenuItem = MenuBarService.startMenuItem { startMenuItem.isEnabled = true @@ -105,6 +115,12 @@ final class AutoClickSimulator: ObservableObject { self.stop() } } + + private func startMouseMonitoring() { + self.monitorObject = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in + self?.stop() + } + } private let mouseDownEventMap: [NSEvent.EventType: CGEventType] = [ .leftMouseDown: .leftMouseDown, diff --git a/auto-clicker/Views/Main/Components/MouseMoveModal.swift b/auto-clicker/Views/Main/Components/MouseMoveModal.swift new file mode 100644 index 0000000..e46892e --- /dev/null +++ b/auto-clicker/Views/Main/Components/MouseMoveModal.swift @@ -0,0 +1,42 @@ +// +// MouseMoveModal.swift +// auto-clicker +// +// Created by Tomasz Pędraszewski on 04/07/2023. +// + +import SwiftUI +import Defaults + +struct MouseMoveModal: View { + @Environment(\.presentationMode) private var presentationMode + + @Default(.appearanceSelectedTheme) private var activeTheme + + @Binding var selected: MouseMove + + var body: some View { + VStack { + ForEach(MouseMove.allCases) { unit in + unit.buttonView(action: { + self.presentationMode.wrappedValue.dismiss() + + self.selected = unit + }) + .buttonStyle(ModalButtonStyle()) + } + + // 'init(_:role:action:)' is only available in macOS 12.0 or newer + // so cannot use a .destructive theme + Button("mousemove_modal_cancel_button") { + self.presentationMode.wrappedValue.dismiss() + } + .buttonStyle(ModalButtonStyle(isDestructive: true)) + } + .frame(width: 200, height: 220) + .padding(.vertical, 14) + .padding(.horizontal, 5) + .background(self.activeTheme.backgroundColour) + .ignoresSafeArea() + } +} diff --git a/auto-clicker/Views/Main/Components/MouseMoveSelector.swift b/auto-clicker/Views/Main/Components/MouseMoveSelector.swift new file mode 100644 index 0000000..8591154 --- /dev/null +++ b/auto-clicker/Views/Main/Components/MouseMoveSelector.swift @@ -0,0 +1,24 @@ +// +// MouseMoveSelector.swift +// auto-clicker +// +// Created by Tomasz Pędraszewski on 04/07/2023. +// + +import SwiftUI + +struct MouseMoveSelector: View { + @State private var showingMouseMoveModal = false + + @Binding var selectedMouseMove: MouseMove + + var body: some View { + Button(self.selectedMouseMove.localised) { + self.showingMouseMoveModal = true + } + .buttonStyle(UnderlinedButtonStyle()) + .sheet(isPresented: self.$showingMouseMoveModal, content: { + MouseMoveModal(selected: self.$selectedMouseMove) + }) + } +} diff --git a/auto-clicker/Views/Main/MainView.swift b/auto-clicker/Views/Main/MainView.swift index b6971d8..85597ac 100644 --- a/auto-clicker/Views/Main/MainView.swift +++ b/auto-clicker/Views/Main/MainView.swift @@ -130,6 +130,15 @@ struct MainView: View { Text(self.formState.startDelay == 1 ? "main_window_second" : "main_window_seconds", comment: "Main window 'second(s)'") + Text("main_window_before_starting", comment: "Main window 'before starting'") + Text("main_window_full_stop", comment: "Main window full stop") } + + ActionStageLine { + Text("main_window_stop_mousemove", comment: "Main window 'Stop mouse move'") + + MouseMoveSelector(selectedMouseMove: self.$formState.stopOnMouseMove) + .disabled(self.hasStarted) + + Text("main_window_stop_mousemove_end", comment: "Main window 'Stop mouse move end'") + } } .padding(.top, 20) .padding(.leading, 20) From 9976006500a30c37208c3d8d584b1f982002bef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Pe=CC=A8draszewski?= Date: Tue, 4 Jul 2023 19:22:23 +0200 Subject: [PATCH 2/3] remove hanging pointer --- auto-clicker/Build Assets/Info.plist | 2 +- auto-clicker/Observable Objects/AutoClickSimulator.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/auto-clicker/Build Assets/Info.plist b/auto-clicker/Build Assets/Info.plist index 6f4c362..bd5edc4 100644 --- a/auto-clicker/Build Assets/Info.plist +++ b/auto-clicker/Build Assets/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - ef40c75 + 7289b3b LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/auto-clicker/Observable Objects/AutoClickSimulator.swift b/auto-clicker/Observable Objects/AutoClickSimulator.swift index db72376..bddf659 100644 --- a/auto-clicker/Observable Objects/AutoClickSimulator.swift +++ b/auto-clicker/Observable Objects/AutoClickSimulator.swift @@ -81,6 +81,7 @@ final class AutoClickSimulator: ObservableObject { if let monitorObject = self.monitorObject { NSEvent.removeMonitor(monitorObject) + self.monitorObject = nil } if let startMenuItem = MenuBarService.startMenuItem { From e2c712633974ac7ea47968d4fb1c8c9bfe55db21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Pe=CC=A8draszewski?= Date: Tue, 4 Jul 2023 23:26:17 +0200 Subject: [PATCH 3/3] tolerance for mouse movement --- auto-clicker/Build Assets/Info.plist | 2 +- auto-clicker/Constants/FieldConstants.swift | 5 +++++ .../en-GB.lproj/Localizable.strings | 3 ++- auto-clicker/Models/FormState.swift | 2 ++ .../AutoClickSimulator.swift | 20 ++++++++++++++++++- auto-clicker/Views/Main/MainView.swift | 8 ++++++++ 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/auto-clicker/Build Assets/Info.plist b/auto-clicker/Build Assets/Info.plist index bd5edc4..96834e5 100644 --- a/auto-clicker/Build Assets/Info.plist +++ b/auto-clicker/Build Assets/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 7289b3b + 51625fa LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/auto-clicker/Constants/FieldConstants.swift b/auto-clicker/Constants/FieldConstants.swift index 98934fa..7ff6eb6 100644 --- a/auto-clicker/Constants/FieldConstants.swift +++ b/auto-clicker/Constants/FieldConstants.swift @@ -30,6 +30,11 @@ let MAX_REPEAT_AMOUNT: Int = 100_000_000 let DEFAULT_REPEAT_AMOUNT: Int = 100 +// MARK: - Mouse movement detection + +let MIN_MOUSE_THRESHOLD: Int = 0 +let MAX_MOUSE_THRESHOLD: Int = 1000 + // MARK: - Start Delay let MIN_START_DELAY: Int = 0 diff --git a/auto-clicker/Localisation/en-GB.lproj/Localizable.strings b/auto-clicker/Localisation/en-GB.lproj/Localizable.strings index fb151c5..33d0c78 100644 --- a/auto-clicker/Localisation/en-GB.lproj/Localizable.strings +++ b/auto-clicker/Localisation/en-GB.lproj/Localizable.strings @@ -16,7 +16,8 @@ "main_window_seconds" = "seconds"; "main_window_before_starting" = " before starting"; "main_window_stop_mousemove" = "And"; -"main_window_stop_mousemove_end" = "when the mouse is moved"; +"main_window_stop_mousemove_end" = "when the mouse moves"; +"main_window_stop_mousemove_pixel" = "pixels"; "main_window_start_btn" = "START"; "main_window_stop_btn" = "STOP"; "main_window_stat_box_next_press_at" = "NEXT PRESS AT"; diff --git a/auto-clicker/Models/FormState.swift b/auto-clicker/Models/FormState.swift index 9b0c650..f649af6 100644 --- a/auto-clicker/Models/FormState.swift +++ b/auto-clicker/Models/FormState.swift @@ -15,6 +15,7 @@ struct FormState: Codable, Defaults.Serializable { var startDelay: Int var repeatAmount: Int var stopOnMouseMove: MouseMove + var mouseDeltaThreshold: Int } extension FormState { @@ -26,5 +27,6 @@ extension FormState { self.startDelay = DEFAULT_START_DELAY self.repeatAmount = DEFAULT_REPEAT_AMOUNT self.stopOnMouseMove = MouseMove.disabled + self.mouseDeltaThreshold = MIN_MOUSE_THRESHOLD } } diff --git a/auto-clicker/Observable Objects/AutoClickSimulator.swift b/auto-clicker/Observable Objects/AutoClickSimulator.swift index bddf659..b0c3716 100644 --- a/auto-clicker/Observable Objects/AutoClickSimulator.swift +++ b/auto-clicker/Observable Objects/AutoClickSimulator.swift @@ -33,6 +33,8 @@ final class AutoClickSimulator: ObservableObject { private var activity: Cancellable? private var monitorObject: Any? = nil + private var initialMousePosition: NSPoint? = nil + private var mouseDeltaThreshold: CGFloat = 0.0 func start() { self.isAutoClicking = true @@ -64,6 +66,8 @@ final class AutoClickSimulator: ObservableObject { repeats: true) if (Defaults[.autoClickerState].stopOnMouseMove.asBoolean()) { + self.initialMousePosition = nil + self.mouseDeltaThreshold = CGFloat(Defaults[.autoClickerState].mouseDeltaThreshold) startMouseMonitoring() } @@ -119,7 +123,21 @@ final class AutoClickSimulator: ObservableObject { private func startMouseMonitoring() { self.monitorObject = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in - self?.stop() + self?.mouseMoved(event) + } + } + + private func mouseMoved(_ event: NSEvent) { + let position = event.locationInWindow + if let initialPosition = self.initialMousePosition { + let deltaX = position.x - initialPosition.x + let deltaY = position.y - initialPosition.y + let distance = sqrt(deltaX * deltaX + deltaY * deltaY) + if (distance > mouseDeltaThreshold) { + self.stop() + } + } else { + self.initialMousePosition = position } } diff --git a/auto-clicker/Views/Main/MainView.swift b/auto-clicker/Views/Main/MainView.swift index 85597ac..829aa9f 100644 --- a/auto-clicker/Views/Main/MainView.swift +++ b/auto-clicker/Views/Main/MainView.swift @@ -138,6 +138,14 @@ struct MainView: View { .disabled(self.hasStarted) Text("main_window_stop_mousemove_end", comment: "Main window 'Stop mouse move end'") + + DynamicWidthNumberField(text: "", + min: MIN_MOUSE_THRESHOLD, + max: MAX_MOUSE_THRESHOLD, + number: self.$formState.mouseDeltaThreshold) + .disabled(self.hasStarted) + + Text("main_window_stop_mousemove_pixel", comment: "Main window 'Stop mouse move pixel'") } } .padding(.top, 20)