├── AbnormalMouse ├── .swift-version ├── AbnormalMouse │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── zh-Hans.lproj │ │ └── InfoPlist.strings │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── icon_app.imageset │ │ │ ├── 64.pdf │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Content.png │ │ │ ├── Content_128.png │ │ │ ├── Content_16.png │ │ │ ├── Content_256.png │ │ │ ├── Content_32-1.png │ │ │ ├── Content_32.png │ │ │ ├── Content_512.png │ │ │ ├── Content_64.png │ │ │ ├── Content_256-1.png │ │ │ ├── Content_512-1.png │ │ │ └── Contents.json │ │ ├── icon_accessibilityOff.imageset │ │ │ ├── NeedAccessability.pdf │ │ │ └── Contents.json │ │ ├── icon_advanced.imageset │ │ │ └── Contents.json │ │ ├── icon_general.imageset │ │ │ ├── Contents.json │ │ │ └── General.pdf │ │ ├── icon_menuBar.imageset │ │ │ ├── Contents.json │ │ │ └── MenuBar.pdf │ │ ├── icon_dockSwipe.imageset │ │ │ └── Contents.json │ │ ├── icon_moveToScroll.imageset │ │ │ └── Contents.json │ │ └── icon_zoomAndRotate.imageset │ │ │ ├── Contents.json │ │ │ └── zoomAndRotate.pdf │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AbnormalMouse.entitlements │ ├── Queue.swift │ ├── StringFiles │ │ ├── zh-Hans.lproj │ │ │ ├── DockSwipeSettings.strings │ │ │ ├── NeedAccessibility.strings │ │ │ ├── Advanced.strings │ │ │ ├── Activation.strings │ │ │ ├── StatusBarMenu.strings │ │ │ ├── MainView.strings │ │ │ ├── Shared.strings │ │ │ ├── General.strings │ │ │ ├── ScrollSettings.strings │ │ │ └── ZoomAndRotateSettings.strings │ │ └── en.lproj │ │ │ ├── DockSwipeSettings.strings │ │ │ ├── NeedAccessibility.strings │ │ │ ├── StatusBarMenu.strings │ │ │ ├── Activation.strings │ │ │ ├── Advanced.strings │ │ │ ├── MainView.strings │ │ │ ├── Shared.strings │ │ │ ├── General.strings │ │ │ ├── ScrollSettings.strings │ │ │ └── ZoomAndRotateSettings.strings │ ├── KeyDescription │ │ ├── MouseCode+Name.swift │ │ └── KeyboardCode+Name.swift │ ├── Library │ │ ├── LaunchAtLoginManager.swift │ │ ├── ComposableArchitectureExtension │ │ │ ├── Effect+Extensions.swift │ │ │ └── SCA+Extentions.swift │ │ ├── Persistency │ │ │ ├── Readonly.swift │ │ │ ├── KeychainValue.swift │ │ │ └── UserDefault.swift │ │ ├── MoveMouseDirection.swift │ │ ├── GestureRecognizer │ │ │ ├── GestureRecognizer.swift │ │ │ ├── MouseMovementGestureRecognizer.swift │ │ │ ├── DoubleTapGestureRecognizer.swift │ │ │ └── TapHoldGestureRecognizer.swift │ │ ├── MoveApplication │ │ │ ├── zh-Hans.lproj │ │ │ │ └── MoveApplication.strings │ │ │ └── en.lproj │ │ │ │ └── MoveApplication.strings │ │ ├── EventThrottler.swift │ │ ├── KeyCombinationValidityChecker.swift │ │ ├── ActivatorConflictChecker.swift │ │ ├── TipsViewBuilder.swift │ │ └── License.swift │ ├── Updater.swift │ ├── Features │ │ ├── SharedViews │ │ │ ├── SettingsPicker.swift │ │ │ ├── SettingsCheckbox.swift │ │ │ ├── SettingsSlider.swift │ │ │ ├── SettingsTipsView.swift │ │ │ ├── KeyboardEventChecker.swift │ │ │ └── SettingsPageView.swift │ │ ├── NeedAccessability │ │ │ ├── NeedAccessabilityDomain.swift │ │ │ └── NeedAccessabilityView.swift │ │ ├── DockSwipe │ │ │ ├── DockSwipeSettingsView.swift │ │ │ └── DockSwipeDomain.swift │ │ ├── Advanced │ │ │ ├── AdvancedDomain.swift │ │ │ └── AdvancedView.swift │ │ └── Activation │ │ │ ├── ActivationDomain.swift │ │ │ └── ActivationView.swift │ ├── Info.plist │ ├── OverrideController │ │ ├── OverrideController.swift │ │ └── EventSequeceController.swift │ ├── Generated │ │ └── Assets.swift │ ├── Style.swift │ └── Persisted.swift ├── AbnormalMouse.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── AbnormalMouseLauncher.xcscheme │ │ ├── AbnormalMouse.xcscheme │ │ └── CanvasPreview.xcscheme ├── swiftgen.yml └── AbnormalMouseTests │ ├── Info.plist │ ├── DockSwipeDomainTests.swift │ ├── ActivatorConflictCheckerTests.swift │ └── MoveToScrollDomainTests.swift ├── screenshot.png ├── AppDependencies ├── .gitignore ├── Sources │ └── AppDependencies │ │ └── License.swift ├── README.md ├── Package_NoLicense.swift ├── Package_NeedLicense.swift └── .swiftpm │ └── xcode │ └── xcshareddata │ └── xcschemes │ └── AppDependencies.xcscheme ├── AbnormalMouse.xcworkspace ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ │ └── Package.resolved └── contents.xcworkspacedata ├── podfile ├── Makefile ├── Podfile.lock ├── README_CN.md ├── .github └── FUNDING.yml ├── .swiftformat ├── README.md ├── .gitignore └── CHANGELOG.md /AbnormalMouse/.swift-version: -------------------------------------------------------------------------------- 1 | 5.3 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/screenshot.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Abnormal Mouse"; 2 | -------------------------------------------------------------------------------- /AppDependencies/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Abnormal Mouse"; 2 | -------------------------------------------------------------------------------- /AppDependencies/Sources/AppDependencies/License.swift: -------------------------------------------------------------------------------- 1 | struct License { 2 | var text = "Hello, World!" 3 | } 4 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_app.imageset/64.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_app.imageset/64.pdf -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_128.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_16.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_256.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_32-1.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_32.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_512.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_64.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_256-1.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Content_512-1.png -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_accessibilityOff.imageset/NeedAccessability.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intitni/AbnormalMouseApp/HEAD/AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_accessibilityOff.imageset/NeedAccessability.pdf -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/AbnormalMouse.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Queue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let defaultQueue = DispatchQueue(label: "com.intii.abnormalmouse.default") 4 | 5 | extension DispatchQueue { 6 | static var `default`: DispatchQueue { defaultQueue } 7 | } 8 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AppDependencies/README.md: -------------------------------------------------------------------------------- 1 | # AppDependencies 2 | 3 | This package is used to add swift package dependencies to the app. Please don't use the Xcode built-in method to add dependencies because we may want to swap remote dependencies to use a local package for dev. 4 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_app.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "64.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/DockSwipeSettings.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "四指轻扫"; 2 | "View.Introduction" = "这个功能能够将鼠标移动转变为四指轻扫手势,如此一来你就能通过一个普通鼠标在空间之间移动、呼出调度中心了。"; 3 | "View.ActivationKeyCombinationTitle" = "按住触发键生效"; 4 | "View.Tips.Usage" = "你需要按住触发键,同时移动鼠标以实现轻扫。"; 5 | -------------------------------------------------------------------------------- /AbnormalMouse.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/NeedAccessibility.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "Abnormal Mouse 需要被解放!"; 2 | 3 | "View.Introduction" = "这个 app 需要在辅助功能开启之后才能正常工作。在开启辅助功能之后,它将可以将键盘、鼠标事件进行转变,比如说将鼠标的位移转变为滚轮的滚动。"; 4 | 5 | "View.EnableButtonTitle" = "开启辅助功能"; 6 | 7 | "View.Manual" = "前往 系统偏好设置 > 安全性与隐私 > 隐私 > 辅助功能"; 8 | -------------------------------------------------------------------------------- /podfile: -------------------------------------------------------------------------------- 1 | platform :macos, '10.15' 2 | 3 | inhibit_all_warnings! 4 | use_frameworks! 5 | 6 | workspace 'AbnormalMouse' 7 | 8 | def tool 9 | pod 'SwiftFormat/CLI' 10 | pod 'SwiftGen', '~> 6.0' 11 | pod 'Sparkle', '1.26.0' 12 | end 13 | 14 | target 'AbnormalMouse' do 15 | project 'AbnormalMouse/AbnormalMouse' 16 | tool 17 | end 18 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NEED_LICENSE ?= false 2 | 3 | bootstrap: needlicense 4 | pod install 5 | 6 | needlicense: 7 | @if [ $(NEED_LICENSE) == true ]; then \ 8 | cp AppDependencies/Package_NeedLicense.swift AppDependencies/Package.swift; \ 9 | else \ 10 | cp AppDependencies/Package_NoLicense.swift AppDependencies/Package.swift;\ 11 | fi 12 | 13 | .PHONY: needlicense -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_advanced.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "General.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_general.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "General.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_menuBar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MenuBar.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_dockSwipe.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "DockSwipe.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_moveToScroll.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MoveToScroll.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_zoomAndRotate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "zoomAndRotate.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/DockSwipeSettings.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "4-Finger Swipe"; 2 | "View.Introduction" = "This feature converts mouse movement into a four-finger swipe, enabling you to switch spaces with a normal mouse."; 3 | "View.ActivationKeyCombinationTitle" = "Active when holding"; 4 | "View.Tips.Usage" = "Hold activator then move your mouse to swipe."; 5 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_accessibilityOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NeedAccessability.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AbnormalMouse/swiftgen.yml: -------------------------------------------------------------------------------- 1 | xcassets: 2 | inputs: 3 | - AbnormalMouse/Assets.xcassets 4 | outputs: 5 | - templateName: swift4 6 | output: AbnormalMouse/Generated/Assets.swift 7 | strings: 8 | inputs: AbnormalMouse/StringFiles/en.lproj 9 | filter: .+\.strings$ 10 | outputs: 11 | - templateName: structured-swift4 12 | output: AbnormalMouse/Generated/Strings.swift -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/NeedAccessibility.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "Abnormal Mouse Needs to Be Free!"; 2 | 3 | "View.Introduction" = "The app needs accessibility enabled to read and manipulate keyboard and mouse events."; 4 | 5 | "View.EnableButtonTitle" = "Turn On Accessibility"; 6 | 7 | "View.Manual" = "Go to System Preferences > Security & Privacy > Privacy > Accessibility"; 8 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/Advanced.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "高级"; 2 | 3 | "View.ListenToKeyboardEvent" = "监听键盘事件(重开 app 时生效)"; 4 | "View.ListenToKeyboardEventIntroduction" = "默认情况下,这个 app 不会监听键盘事件(除了修饰键)。毕竟让一个 app 监听键盘事件是一件有风险的事情(尽管我们什么都没做)。如果你希望使用键盘按键作为触发键,你可以将它开启,开启后会稍稍提高 Abnormal Mouse 在静默时的 CPU 占用率。\n\n你可以使用像是 ReiKey 这样的工具来判断这个 app 是否还在监听键盘事件。"; 5 | 6 | "View.ExcludeListTitle" = "在下列 app 中禁用 Abnormal Mouse"; 7 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/KeyDescription/MouseCode+Name.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | 3 | extension MouseCode { 4 | var name: String { 5 | switch self { 6 | case .mouseLeft: return L10n.Shared.MouseCodeName.left 7 | case .mouseRight: return L10n.Shared.MouseCodeName.right 8 | case .mouseMiddle: return L10n.Shared.MouseCodeName.middle 9 | case let .mouse(n): return L10n.Shared.MouseCodeName.other(String(n + 1)) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/Activation.strings: -------------------------------------------------------------------------------- 1 | "Instruction" = "请输入许可证和邮箱。"; 2 | "LicenseKeyTitle" = "许可证"; 3 | "EmailTitle" = "邮箱"; 4 | "Button.Cancel" = "取消"; 5 | "Button.BuyNow" = "前往购买"; 6 | "Button.Activating" = "正在激活.."; 7 | "Button.Activate" = "激活"; 8 | 9 | "FailureReason.NetworkError" = "请检查你的网络并重试。"; 10 | "FailureReason.Invalid" = "无法验证你的许可证,请确认许可证和邮箱填写正确。"; 11 | "FailureReason.Refunded" = "此许可证已经因为退款而失效了。"; 12 | "FailureReason.ReachedLimit" = "此许可证已经达到了激活上限,请在反激活后再尝试激活。"; 13 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/StatusBarMenu.strings: -------------------------------------------------------------------------------- 1 | "IsEnabled" = "Abnormal Mouse 正在运行中.."; 2 | "IsDisabled" = "Abnormal Mouse 已停止运行.."; 3 | "ShowPreferences" = "显示设置"; 4 | "Quit" = "退出"; 5 | 6 | "PurchaseStatus.Fetching" = "正在获取激活状态.."; 7 | "PurchaseStatus.Ended" = "试用期已结束。"; 8 | "PurchaseStatus.Trial" = "试用期剩余 %@ 天。"; 9 | "PurchaseStatus.Activated" = "已激活。"; 10 | "PurchaseStatus.CantVerify" = "无法验证激活状态。"; 11 | "PurchaseStatus.Refunded" = "已退款。"; 12 | "PurchaseStatus.Invalid" = "许可证无效"; 13 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/LaunchAtLoginManager.swift: -------------------------------------------------------------------------------- 1 | protocol LaunchAtLoginManagerType: AnyObject { 2 | var launchAtLogin: Bool { get set } 3 | } 4 | 5 | final class FakeLaunchAtLoginManager: LaunchAtLoginManagerType { 6 | var launchAtLogin: Bool = false 7 | } 8 | 9 | import LaunchAtLogin 10 | 11 | final class LaunchAtLoginManager: LaunchAtLoginManagerType { 12 | var launchAtLogin: Bool { 13 | get { LaunchAtLogin.isEnabled } 14 | set { LaunchAtLogin.isEnabled = newValue } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/ComposableArchitectureExtension/Effect+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import ComposableArchitecture 3 | 4 | private var someCancellables = Set() 5 | 6 | extension Effect { 7 | static func fireAsyncAndForget(_ work: @escaping () -> AnyCancellable) -> Effect { 8 | Deferred { () -> Empty in 9 | work().store(in: &someCancellables) 10 | return Empty(completeImmediately: true) 11 | } 12 | .eraseToEffect() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/Persistency/Readonly.swift: -------------------------------------------------------------------------------- 1 | /// `Readonly` provides a readonly interface of the wrapped object. Properties of wrapped object 2 | /// can be directly accessed through this wrapper, but none of them will be mutatable. 3 | @dynamicMemberLookup 4 | struct Readonly { 5 | private let wrappedValue: T 6 | 7 | init(_ object: T) { 8 | wrappedValue = object 9 | } 10 | 11 | subscript(dynamicMember member: KeyPath) -> V { 12 | wrappedValue[keyPath: member] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Sparkle (1.26.0) 3 | - SwiftFormat/CLI (0.48.11) 4 | - SwiftGen (6.4.0) 5 | 6 | DEPENDENCIES: 7 | - Sparkle (= 1.26.0) 8 | - SwiftFormat/CLI 9 | - SwiftGen (~> 6.0) 10 | 11 | SPEC REPOS: 12 | trunk: 13 | - Sparkle 14 | - SwiftFormat 15 | - SwiftGen 16 | 17 | SPEC CHECKSUMS: 18 | Sparkle: d56d028f4b0c123577825f5d1004f6140d77f90e 19 | SwiftFormat: 938e5865a118c49d63c7a290ddad86335f9e585f 20 | SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 21 | 22 | PODFILE CHECKSUM: dcc8b76adf8cfb0e8fef7fc85a88da225434db15 23 | 24 | COCOAPODS: 1.11.2 25 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/MainView.strings: -------------------------------------------------------------------------------- 1 | "TabTitle.MoveToScroll" = "滚动和轻扫"; 2 | 3 | "TabTitle.TapAndClick" = "点按和轻点"; 4 | 5 | "TabTitle.ZoomAndRotate" = "缩放和旋转"; 6 | 7 | "TabTitle.Advanced" = "高级设置"; 8 | 9 | "TabTitle.General" = "一般设置"; 10 | 11 | "TabTitle.DockSwipe" = "四指轻扫"; 12 | 13 | "Status.NotEnabled" = "Abnormal Mouse 尚未启动"; 14 | 15 | "Status.EnableButtonTitle" = "启动"; 16 | 17 | "ExpireAlert.Title" = "试用期已经结束了"; 18 | "ExpireAlert.Content" = "非常感谢你能够试用 Abnormal Mouse,如果你希望继续使用它,可以输入激活码将其激活。"; 19 | "ExpireAlert.Activate" = "激活"; 20 | "ExpireAlert.BuyNow" = "立即购买"; 21 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/StatusBarMenu.strings: -------------------------------------------------------------------------------- 1 | "IsEnabled" = "Abnormal Mouse is functioning.."; 2 | "IsDisabled" = "Abnormal Mouse is disabled.."; 3 | "ShowPreferences" = "Show preferences"; 4 | "Quit" = "Quit"; 5 | 6 | "PurchaseStatus.Fetching" = "Fetching activation status.."; 7 | "PurchaseStatus.Ended" = "Trial has ended."; 8 | "PurchaseStatus.Trial" = "Trial ends in %@ days."; 9 | "PurchaseStatus.Activated" = "License is valid."; 10 | "PurchaseStatus.CantVerify" = "Failed to verify license."; 11 | "PurchaseStatus.Refunded" = "License is refunded."; 12 | "PurchaseStatus.Invalid" = "License is invalid."; 13 | -------------------------------------------------------------------------------- /AbnormalMouse.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/Shared.strings: -------------------------------------------------------------------------------- 1 | "View.EnterKeyCombination" = "录制中.."; 2 | "View.KeyCombinationNotSetup" = "设置"; 3 | "View.ActivatorConflict" = "触发键已被其它功能使用"; 4 | "View.KeyCombinationLeftRightMouseButtonNeedModifier" = "鼠标左右键需要配合修饰键使用"; 5 | "View.KeyCombinationNeedsKeyboardEventListener" = "请先在高级设置中开启监听键盘事件"; 6 | 7 | "MouseCodeName.Left" = "鼠标(左)"; 8 | "MouseCodeName.Right" = "鼠标(右)"; 9 | "MouseCodeName.Middle" = "鼠标(中)"; 10 | "MouseCodeName.Other" = "鼠标(%@)"; 11 | 12 | "AppName" = "Abnormal Mouse"; 13 | 14 | "TipsTitle.Default" = "小帖士"; 15 | "TipsTitle.Usage" = "用法"; 16 | "TipsTitle.Bug" = "已知问题"; 17 | 18 | "HomepageURLString" = "https://abnormalmouse.intii.com/zh-cn"; 19 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/Activation.strings: -------------------------------------------------------------------------------- 1 | "Instruction" = "Please enter your license key and email to activate."; 2 | "LicenseKeyTitle" = "License key"; 3 | "EmailTitle" = "Email"; 4 | "Button.Cancel" = "Cancel"; 5 | "Button.BuyNow" = "Buy Now"; 6 | "Button.Activating" = "Activating.."; 7 | "Button.Activate" = "Activate"; 8 | 9 | "FailureReason.NetworkError" = "Please check your network and try again."; 10 | "FailureReason.Invalid" = "Can't verify the license. Please check the key and email entered are correct."; 11 | "FailureReason.Refunded" = "The license key has been refunded."; 12 | "FailureReason.ReachedLimit" = "The license key has reached the activation limit. Please deactivate first."; 13 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/MoveMouseDirection.swift: -------------------------------------------------------------------------------- 1 | enum MoveMouseDirection: Int, Equatable, PropertyListStorable, CaseIterable { 2 | case none 3 | case left 4 | case right 5 | case up 6 | case down 7 | 8 | func isSameAxis(to another: MoveMouseDirection) -> Bool { 9 | switch (self, another) { 10 | case (.left, .right), (.right, .left): return true 11 | case (.up, .down), (.down, .up): return true 12 | default: return self == another 13 | } 14 | } 15 | 16 | var propertyListValue: Int { rawValue } 17 | static func makeFromPropertyListValue(value: Int) -> MoveMouseDirection { 18 | MoveMouseDirection(rawValue: value) ?? .none 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/Advanced.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "Advanced"; 2 | 3 | "View.ListenToKeyboardEvent" = "Listen to keyboard event (restart app to take effect)"; 4 | "View.ListenToKeyboardEventIntroduction" = "By default, this app does not listen to your keystrokes so you can't use keyboard keys (except modifiers) as activators. Letting an app read your keystrokes can be risky (though we are not doing anything with Abnormal Mouse). Turn it on if you want to use keyboard keys as activators. It will also slightly increase CPU usage when idel. \n\nYou can use tools like ReiKey to check if the app is still listening to your keystrokes"; 5 | 6 | "View.ExcludeListTitle" = "Disable Abnormal Mouse for apps listing below"; 7 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/MainView.strings: -------------------------------------------------------------------------------- 1 | "TabTitle.MoveToScroll" = "Scroll and Swipe"; 2 | 3 | "TabTitle.TapAndClick" = "Tap and Click"; 4 | 5 | "TabTitle.ZoomAndRotate" = "Zoom and Rotate"; 6 | 7 | "TabTitle.Advanced" = "Advanced"; 8 | 9 | "TabTitle.General" = "General"; 10 | 11 | "TabTitle.DockSwipe" = "4-Finger Swipe"; 12 | 13 | "Status.NotEnabled" = "Abnormal Mouse is not enabled."; 14 | 15 | "Status.EnableButtonTitle" = "Enable"; 16 | 17 | "ExpireAlert.Title" = "Trial Has Ended."; 18 | "ExpireAlert.Content" = "Thanks for trying out Abnormal Mouse. To continue to use the app, you can activate it with an activation key."; 19 | "ExpireAlert.Activate" = "Activate"; 20 | "ExpireAlert.BuyNow" = "Buy Now"; 21 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/General.strings: -------------------------------------------------------------------------------- 1 | "DevelopedBy" = "开发者"; 2 | "ContactMe" = "在使用过程中遇到任何问题,可以通过邮件与我联系"; 3 | "Quit" = "关闭 Abnormal Mouse"; 4 | "AutoStart" = "登录时自动启动"; 5 | 6 | "Title.General" = "一般设置"; 7 | "Title.About" = "关于"; 8 | 9 | "TrialEnd" = "试用已经结束了。"; 10 | "TrialDaysRemain" = "试用期还剩 %@ 天"; 11 | "Activate" = "激活"; 12 | "Purchase" = "立即购买"; 13 | "Deactivate" = "反激活"; 14 | "Deactivating" = "正在反激活.."; 15 | "LicenseValid" = "已激活。"; 16 | "LicenseInvalid" = "许可证无效。"; 17 | "LicenseRefunded" = "已退款,许可证已失效。"; 18 | "LicenseTo" = "激活邮箱 %@。"; 19 | 20 | "Version" = "版本号 %@"; 21 | "CheckForUpdate" = "检查更新"; 22 | "AutomaticallyCheckForUpdate" = "自动检查更新"; 23 | 24 | "ErrorMessage.FailedToDeactivate" = "无法反激活,请稍后再试。"; 25 | "ErrorMessage.NetworkError" = "请检查你的网络并重试。"; 26 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/Shared.strings: -------------------------------------------------------------------------------- 1 | "View.EnterKeyCombination" = "Recording..."; 2 | "View.KeyCombinationNotSetup" = "Setup"; 3 | "View.ActivatorConflict" = "Activator already in use"; 4 | "View.KeyCombinationLeftRightMouseButtonNeedModifier" = "Set modifiers for left/right mouse button"; 5 | "View.KeyCombinationNeedsKeyboardEventListener" = "Turn on listen to keyboard events in advanced settings"; 6 | 7 | "MouseCodeName.Left" = "Mouse(L)"; 8 | "MouseCodeName.Right" = "Mouse(R)"; 9 | "MouseCodeName.Middle" = "Mouse(M)"; 10 | "MouseCodeName.Other" = "Mouse(%@)"; 11 | 12 | "AppName" = "Abnormal Mouse"; 13 | 14 | "TipsTitle.Default" = "Tips"; 15 | "TipsTitle.Usage" = "Usage"; 16 | "TipsTitle.Bug" = "Bug"; 17 | 18 | "HomepageURLString" = "https://abnormalmouse.intii.com"; 19 | 20 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Abnormal Mouse Logo 2 | 3 | ## 使用方法 4 | 5 | 你可以前往[这里下载最新版](https://abnormalmouse.intii.com/zh-cn)并免费使用它。 6 | 7 | ## 关于这个 app 8 | 9 | 2019年末我买了一只十分奇特的鼠标,它有棱角分明的外形、一个方向键、一对 AB 键以及一个几乎不可用的触控滚轮,如果不把它当鼠标看,它应该算是一个不错的装饰品。今年因为疫情我决定不再带 MacBook Pro 上班,然而公司只给了我一台垃圾 Mac Mini,贫穷的我找不到再买一个 Magic Mouse 放公司的理由,就决定启用这只鼠标。 10 | 11 | 在 macOS 中使用一般鼠标本身就是一件很糟糕的事情,用不了手势操作、无法进行四向滚动给我带来了很大的麻烦。比如看 UI 设计的时候没有办法通过鼠标左右平移、我很喜欢的窗口管理工具 Swish 也没法用了,于是我决定写个 app 改善一下,通过移动鼠标去触发这些功能。 12 | 13 | Screenshot 14 | 15 | ### 目前支持的功能有: 16 | - 四向滚动(通过按住触发键移动鼠标进行“拖拽”滚动,用起来可能会很奇怪,但我自己还挺喜欢的)。 17 | - 半页下翻。 18 | - 双指轻扫手势(Safari 的右划返回、Reeder 的下拉刷新等)。 19 | - 缩放、旋转、只能缩放。 20 | - 四指轻扫手势(切换 Space、呼出 Mission Control)。 21 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouseTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Updater.swift: -------------------------------------------------------------------------------- 1 | protocol Updater: AnyObject { 2 | var automaticallyChecksForUpdates: Bool { get set } 3 | func checkForUpdates(_ sender: Any!) 4 | } 5 | 6 | #if canImport(Sparkle) 7 | import Sparkle 8 | final class SparkleUpdater: Updater { 9 | var automaticallyChecksForUpdates: Bool { 10 | get { SUUpdater.shared()?.automaticallyChecksForUpdates ?? false } 11 | set { SUUpdater.shared()?.automaticallyChecksForUpdates = newValue } 12 | } 13 | 14 | func checkForUpdates(_ sender: Any!) { 15 | SUUpdater.shared()?.checkForUpdates(sender) 16 | } 17 | 18 | func initialize() { 19 | _ = SUUpdater.shared() 20 | } 21 | } 22 | #endif 23 | 24 | final class FakeUpdater: Updater { 25 | var automaticallyChecksForUpdates: Bool = false 26 | func checkForUpdates(_: Any!) {} 27 | } 28 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/GestureRecognizer/GestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | 4 | enum GestureRecognizers {} 5 | 6 | private var allGestureRecognizers = [GestureRecognizer]() 7 | 8 | class GestureRecognizer { 9 | init() { 10 | allGestureRecognizers.append(self) 11 | } 12 | 13 | final var shouldDiscardEvent: Bool { 14 | NSWorkspace.shared.frontmostApplication?.bundleIdentifier != "com.intii.AbnormalMouse" 15 | } 16 | 17 | final func cancelOtherGestures(where condition: (GestureRecognizer) -> Bool = { _ in true }) { 18 | allGestureRecognizers.forEach { 19 | guard $0 !== self else { return } 20 | guard condition($0) else { return } 21 | guard let cancellable = $0 as? Cancellable else { return } 22 | cancellable.cancel() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: intitni 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/SettingsPicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsPicker: View { 4 | let title: Title 5 | let selection: Binding 6 | let content: () -> Content 7 | 8 | var body: some View { 9 | HStack(alignment: .center) { 10 | title.asWidgetTitle() 11 | Picker(selection: selection, label: EmptyView(), content: content) 12 | .frame(maxWidth: 200) 13 | } 14 | } 15 | } 16 | 17 | struct SettingsPicker_Previews: PreviewProvider { 18 | @State static var selected = 1 19 | 20 | static var previews: some View { 21 | SettingsPicker( 22 | title: Text("Title"), 23 | selection: $selected, 24 | content: { 25 | ForEach(1..<4) { 26 | Text(String($0)) 27 | } 28 | } 29 | ) 30 | .padding(10) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/MoveApplication/zh-Hans.lproj/MoveApplication.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "Could not move to Applications folder" = "无法移动到“应用程序”文件夹中"; 3 | 4 | /* No comment provided by engineer. */ 5 | "Do Not Move" = "不移动"; 6 | 7 | /* No comment provided by engineer. */ 8 | "I can move myself to the Applications folder if you'd like." = "我要自己把程序移动到“应用程序”文件夹中。"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Move to Applications Folder" = "移动到“应用程序”文件夹中"; 12 | 13 | /* No comment provided by engineer. */ 14 | "Move to Applications folder in your Home folder?" = "是否要把程序移动到你的个人文件夹里的“应用程序”文件夹中?"; 15 | 16 | /* No comment provided by engineer. */ 17 | "Move to Applications folder?" = "是否要把程序移动到“应用程序”文件夹中?"; 18 | 19 | /* No comment provided by engineer. */ 20 | "Note that this will require an administrator password." = "注意,本操作需要你输入管理员密码。"; 21 | 22 | /* No comment provided by engineer. */ 23 | "This will keep your Downloads folder uncluttered." = "这样做能让你的“下载”文件夹保持整洁。"; 24 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/General.strings: -------------------------------------------------------------------------------- 1 | "DevelopedBy" = "Developed by"; 2 | "ContactMe" = "If you have any problem using the app, feel free to contact me at"; 3 | "Quit" = "Quit Abnormal Mouse"; 4 | "AutoStart" = "Automatically start at login"; 5 | 6 | "Title.General" = "General"; 7 | "Title.About" = "About"; 8 | 9 | "TrialEnd" = "Trial has ended."; 10 | "TrialDaysRemain" = "Trial will end in %@ days."; 11 | "Activate" = "Activate"; 12 | "Purchase" = "Buy Now"; 13 | "Deactivate" = "Deactivate"; 14 | "Deactivating" = "Deactivating.."; 15 | "LicenseValid" = "License is valid."; 16 | "LicenseInvalid" = "License is not valid."; 17 | "LicenseRefunded" = "License is refunded."; 18 | "LicenseTo" = "Licensed to %@"; 19 | 20 | "Version" = "Version %@"; 21 | "CheckForUpdate" = "Check Now"; 22 | "AutomaticallyCheckForUpdate" = "Automatically check for update"; 23 | 24 | "ErrorMessage.FailedToDeactivate" = "Failed to deactivate, please try again later."; 25 | "ErrorMessage.NetworkError" = "Please check your network and try again."; 26 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/ComposableArchitectureExtension/SCA+Extentions.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | extension View { 5 | func backgroundEmptyViewWithViewStore( 6 | _ store: Store, 7 | modify: @escaping (EmptyView, ViewStore) -> Content 8 | ) -> some View where State: Equatable, Content: View { 9 | background(WithViewStore(store) { viewStore in 10 | modify(EmptyView(), viewStore) 11 | }) 12 | } 13 | 14 | func lifeCycleWithViewStore( 15 | _ store: Store, 16 | onAppear: @escaping (ViewStore) -> Void = { _ in }, 17 | onDisappear: @escaping (ViewStore) -> Void = { _ in } 18 | ) -> some View where State: Equatable { 19 | backgroundEmptyViewWithViewStore(store) { view, viewStore in 20 | view 21 | .onAppear { onAppear(viewStore) } 22 | .onDisappear { onDisappear(viewStore) } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/MoveApplication/en.lproj/MoveApplication.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "Could not move to Applications folder" = "Could not move to Applications folder"; 3 | 4 | /* No comment provided by engineer. */ 5 | "Do Not Move" = "Do Not Move"; 6 | 7 | /* No comment provided by engineer. */ 8 | "I can move myself to the Applications folder if you'd like." = "I can move myself to the Applications folder if you'd like."; 9 | 10 | /* No comment provided by engineer. */ 11 | "Move to Applications Folder" = "Move to Applications Folder"; 12 | 13 | /* No comment provided by engineer. */ 14 | "Move to Applications folder in your Home folder?" = "Move to Applications folder in your Home folder?"; 15 | 16 | /* No comment provided by engineer. */ 17 | "Move to Applications folder?" = "Move to Applications folder?"; 18 | 19 | /* No comment provided by engineer. */ 20 | "Note that this will require an administrator password." = "Note that this will require an administrator password."; 21 | 22 | /* No comment provided by engineer. */ 23 | "This will keep your Downloads folder uncluttered." = "This will keep your Downloads folder uncluttered."; 24 | -------------------------------------------------------------------------------- /AppDependencies/Package_NoLicense.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppDependencies", 7 | platforms: [.macOS(.v10_15)], 8 | products: [ 9 | .library( 10 | name: "AppDependencies", 11 | targets: ["AppDependencies"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package( 16 | name: "CGEventOverride", 17 | url: "https://github.com/intitni/CGEventOverride.git", 18 | .branch("master") 19 | ), 20 | .package( 21 | url: "https://github.com/pointfreeco/swift-composable-architecture", 22 | .upToNextMajor(from: "0.3.0") 23 | ), 24 | .package( 25 | url: "https://github.com/CombineCommunity/CombineExt", 26 | .upToNextMajor(from: "1.2.0") 27 | ), 28 | ], 29 | targets: [ 30 | .target( 31 | name: "AppDependencies", 32 | dependencies: [ 33 | "CGEventOverride", 34 | "CombineExt", 35 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 36 | ] 37 | ), 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/NeedAccessability/NeedAccessabilityDomain.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | 6 | enum NeedAccessibilityDomain: Domain { 7 | struct State: Equatable {} 8 | 9 | enum Action { 10 | case goToAccessbilityPreferencePane 11 | } 12 | 13 | typealias Environment = SystemEnvironment<_Environment> 14 | struct _Environment {} 15 | 16 | static let reducer = Reducer { _, action, environment in 17 | switch action { 18 | case .goToAccessbilityPreferencePane: 19 | return .fireAndForget { 20 | let urlString = 21 | "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" 22 | environment.openURL(URL(string: urlString)!) 23 | } 24 | } 25 | } 26 | } 27 | 28 | extension Store where Action == NeedAccessibilityDomain.Action, 29 | State == NeedAccessibilityDomain.State 30 | { 31 | static var testStore: Self { 32 | Self( 33 | initialState: .init(), 34 | reducer: NeedAccessibilityDomain.reducer, 35 | environment: .live(environment: .init()) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/ScrollSettings.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "滚动和轻扫"; 2 | "View.Introduction" = "这个功能能够将鼠标移动转变为四向滚动。不仅如此,你还能通过鼠标的左右移动触发轻扫手势,就像你能在 Safari 或者邮件中通过触控版轻扫返回、删除邮件那样。"; 3 | "View.ActivationKeyCombinationTitle" = "按住触发键生效"; 4 | "View.ScrollSpeedSliderTitle" = "滚动速度"; 5 | "View.SwipeSpeedSliderTitle" = "轻扫速度"; 6 | "View.ScrollSpeedSliderIntroduction" = "通过调整滚动速度,你可以改变鼠标位移转变为滚动时的比例。我们建议使用一个较大的比例,这样你只需要轻轻移动鼠标就能实现远距离的滚动。"; 7 | "View.SwipeSliderIntroduction" = "轻扫速度控制的是鼠标位移转变为划动手势时的比例,与滚动速度不同,更小的比例能获得更好的体验。"; 8 | "View.Tips.Usage" = "你需要按住触发键,同时移动鼠标以实现滚动。"; 9 | "View.Tips.ActivatorChoice" = "我们建议你使用多余的鼠标按键作为触发键以实现类似于触控板的那种拖拽滚动的体验。"; 10 | "View.Tips.PageDown" = "双击可以向下滚动半页。"; 11 | "View.Tips.ScrollBar" = "如果你没有连接任何的触控板或妙控鼠标,你可以在系统设置中将滚动条隐藏。"; 12 | "View.InertiaEffectCheckboxTitle" = "模拟滚动惯性"; 13 | "View.InertiaEffectIntroduction" = "我们建议你开启这个功能,关闭之后某些 app 会没有滚动惯性效果。"; 14 | 15 | "HalfPageScrollView.Tips.UsageA" = "连续点击“滚动和轻扫”的触发键触发半页滚动。"; 16 | "HalfPageScrollView.Tips.UsageB" = "点击触发键触发半页滚动。"; 17 | 18 | "HalfPageScrollView.Title" = "半页滚动"; 19 | "HalfPageScrollView.introduction" = "键盘上的 PageDown 本身是个很棒的功能,但是它只能滚动整页。这个功能让滚动半页成为可能。"; 20 | "HalfPageScrollView.ActivationKeyCombinationTitle" = "通过快捷键触发"; 21 | "HalfPageScrollView.DoubleTapToActivate" = "重用“滚动和轻扫”的触发键"; 22 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/SettingsCheckbox.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsCheckbox: View { 4 | let title: Title 5 | @Binding var isOn: Bool 6 | 7 | private struct SettingsToggleStyle: ToggleStyle { 8 | func makeBody(configuration: Configuration) -> some View { 9 | HStack(alignment: .center) { 10 | configuration.label.font(.widgetTitle) 11 | .onTapGesture(perform: { configuration.isOn.toggle() }) 12 | Toggle(isOn: configuration.$isOn, label: { EmptyView() }) 13 | .toggleStyle(CheckboxToggleStyle()) 14 | } 15 | } 16 | } 17 | 18 | init( 19 | isOn: Binding, 20 | @ViewBuilder title: () -> Title 21 | ) { 22 | _isOn = isOn 23 | self.title = title() 24 | } 25 | 26 | var body: some View { 27 | Toggle(isOn: $isOn, label: { title }) 28 | .toggleStyle(SettingsToggleStyle()) 29 | } 30 | } 31 | 32 | struct SettingsCheckbox_Previews: PreviewProvider { 33 | @State static var isOn = false 34 | 35 | static var previews: some View { 36 | SettingsCheckbox( 37 | isOn: $isOn, 38 | title: { Text("Checkbox") } 39 | ) 40 | .padding(10) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --allman false 2 | --beforemarks 3 | --binarygrouping 4,8 4 | --categorymark "MARK: %c" 5 | --classthreshold 0 6 | --closingparen balanced 7 | --commas always 8 | --conflictmarkers reject 9 | --decimalgrouping 3,6 10 | --elseposition same-line 11 | --enumthreshold 0 12 | --exponentcase lowercase 13 | --exponentgrouping disabled 14 | --fractiongrouping disabled 15 | --fragment false 16 | --funcattributes preserve 17 | --guardelse auto 18 | --header ignore 19 | --hexgrouping 4,8 20 | --hexliteralcase uppercase 21 | --ifdef no-indent 22 | --importgrouping testable-bottom 23 | --indent 4 24 | --indentcase false 25 | --lifecycle 26 | --linebreaks lf 27 | --maxwidth 100 28 | --modifierorder 29 | --nospaceoperators ...,..< 30 | --nowrapoperators 31 | --octalgrouping 4,8 32 | --operatorfunc spaced 33 | --patternlet hoist 34 | --ranges spaced 35 | --self remove 36 | --selfrequired 37 | --semicolons inline 38 | --shortoptionals always 39 | --smarttabs enabled 40 | --stripunusedargs unnamed-only 41 | --structthreshold 0 42 | --tabwidth unspecified 43 | --trailingclosures 44 | --trimwhitespace always 45 | --typeattributes preserve 46 | --varattributes preserve 47 | --voidtype void 48 | --wraparguments before-first 49 | --wrapcollections disabled 50 | --wrapparameters before-first 51 | --xcodeindentation disabled 52 | --yodaswap always 53 | 54 | --enable isEmpty 55 | 56 | --exclude Pods,**/Generated -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abnormal Mouse Logo 2 | 3 | ## About the App 4 | 5 | At the end of 2019, I bought a strange mouse with an angular shape, an arrow key, a pair of AB keys, and an almost unusable touch scroll wheel. It would be a nice decoration if I didn't think of it as a mouse. Because of the epidemic of Covid-19, I decided not to bring my MacBook Pro to work. I had to use a Mac Mini at work, and I couldn't find a reason to buy another Magic Mouse to use in the office, I decided to use the strange mouse. 6 | 7 | Using a normal mouse in macOS is a terrible thing, the missing gestures and four-way scrolling is a huge problem. For example, there's no way to pan left or right when looking at UI designs. And Swish, my favorite window management tool, doesn't work anymore. So I decided to write an app to fix it, trying to trigger these features by just moving the mouse (and holding some buttons). 8 | 9 | Screenshot 10 | 11 | ### The currently supported features are 12 | 13 | - Four-way scrolling (drag-to-scroll by holding down the trigger button and moving the mouse, which may seem odd, but I kind of like it). 14 | - Half page down. 15 | - Two-finger swipe gestures (Safari's swipe to back, Reeder's pull to refresh, etc.). 16 | - Zoom and rotate. 17 | - Four-finger swipe gestures (Switch between spaces, Mission Control). 18 | -------------------------------------------------------------------------------- /AppDependencies/Package_NeedLicense.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppDependencies", 7 | platforms: [.macOS(.v10_15)], 8 | products: [ 9 | .library( 10 | name: "AppDependencies", 11 | targets: ["AppDependencies"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package( 16 | name: "License", 17 | url: "https://github.com/intitni/AbnormalMouseLicense", 18 | .branch("master") 19 | ), 20 | .package( 21 | name: "CGEventOverride", 22 | url: "https://github.com/intitni/CGEventOverride.git", 23 | .branch("master") 24 | ), 25 | .package( 26 | url: "https://github.com/pointfreeco/swift-composable-architecture", 27 | .upToNextMajor(from: "0.3.0") 28 | ), 29 | .package( 30 | url: "https://github.com/CombineCommunity/CombineExt", 31 | .upToNextMajor(from: "1.2.0") 32 | ), 33 | ], 34 | targets: [ 35 | .target( 36 | name: "AppDependencies", 37 | dependencies: [ 38 | "License", 39 | "CGEventOverride", 40 | "CombineExt", 41 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 42 | ] 43 | ), 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/EventThrottler.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | final class EventThrottler { 5 | private var cancellables = [AnyCancellable]() 6 | private var accumulation: T 7 | private var previous: T? 8 | private var initial: T 9 | private var perform: (T) -> Void = { _ in } 10 | private var time: TimeInterval = 0 11 | private var windowSize: TimeInterval 12 | 13 | var rate: Int = 70 { 14 | didSet { windowSize = 1 / Double(rate) } 15 | } 16 | 17 | init(_ initial: T, perform: @escaping (T) -> Void) { 18 | self.initial = initial 19 | self.perform = perform 20 | accumulation = initial 21 | windowSize = 1 / Double(rate) 22 | } 23 | 24 | func post(accumulate: @escaping (inout T) -> Void) { 25 | accumulate(&accumulation) 26 | let current = Date().timeIntervalSinceReferenceDate 27 | if current - time > windowSize { 28 | time = current 29 | perform(accumulation) 30 | previous = accumulation 31 | accumulation = initial 32 | } 33 | } 34 | 35 | func end(accumulate: @escaping (inout T) -> Void) { 36 | accumulate(&accumulation) 37 | time = 0 38 | perform(accumulation) 39 | accumulation = initial 40 | previous = nil 41 | } 42 | 43 | func endWithLastValue() { 44 | if let p = previous { 45 | perform(p) 46 | previous = nil 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/NeedAccessability/NeedAccessabilityView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct NeedAccessibilityScreen: View { 5 | let store: NeedAccessibilityDomain.Store 6 | 7 | var body: some View { 8 | NeedAccessibilityView(store: store) 9 | } 10 | } 11 | 12 | private struct NeedAccessibilityView: View { 13 | let store: NeedAccessibilityDomain.Store 14 | 15 | var body: some View { 16 | WithViewStore(store) { viewStore in 17 | VStack { 18 | Image(Asset.iconAccessibilityOff.name) 19 | Text(_L10n.View.title) 20 | .font(.pageTitle) 21 | Spacer().frame(height: 8) 22 | Text(_L10n.View.introduction) 23 | .asFeatureIntroduction() 24 | Spacer().frame(height: 8) 25 | Button(action: { viewStore.send(.goToAccessbilityPreferencePane) }) { 26 | Text(_L10n.View.enableButtonTitle) 27 | } 28 | Text(_L10n.View.manual) 29 | .asFeatureIntroduction() 30 | } 31 | } 32 | .padding(.init(top: 20, leading: 20, bottom: 20, trailing: 20)) 33 | .frame(width: 500) 34 | } 35 | } 36 | 37 | struct NeedAccessibilityView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | NeedAccessibilityView(store: .testStore) 40 | } 41 | } 42 | 43 | private enum _L10n { 44 | typealias View = L10n.NeedAccessibility.View 45 | } 46 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/zh-Hans.lproj/ZoomAndRotateSettings.strings: -------------------------------------------------------------------------------- 1 | "ZoomAndRotateView.Title" = "缩放与旋转"; 2 | "ZoomAndRotateView.Introduction" = "将鼠标位移转变为缩放和旋转手势。"; 3 | "ZoomAndRotateView.ActivationKeyCombinationTitle" = "按住触发键生效"; 4 | "ZoomAndRotateView.zoomSpeedSliderTitle" = "缩放速度"; 5 | "ZoomAndRotateView.rotateSpeedSliderTitle" = "旋转速度"; 6 | "SmartZoomView.Title" = "智能缩放"; 7 | "SmartZoomView.introduction" = "双击触控板触发的智能缩放,现在可以通过设置快捷键实现。"; 8 | "SmartZoomView.ActivationKeyCombinationTitle" = "通过快捷键触发"; 9 | "SmartZoomView.DoubleTapToActivate" = "重用“缩放与旋转”的触发键"; 10 | 11 | "ZoomAndRotateView.ZoomDirectionTitle" = "缩放手势方向"; 12 | "ZoomAndRotateView.ZoomDirectionNone" = "不缩放"; 13 | "ZoomAndRotateView.ZoomDirectionLeft" = "往左移放大"; 14 | "ZoomAndRotateView.ZoomDirectionRight" = "往右移放大"; 15 | "ZoomAndRotateView.ZoomDirectionUp" = "往上移放大"; 16 | "ZoomAndRotateView.ZoomDirectionDown" = "往下移放大"; 17 | 18 | "ZoomAndRotateView.RotateDirectionTitle" = "旋转手势方向"; 19 | "ZoomAndRotateView.RotateDirectionNone" = "不旋转"; 20 | "ZoomAndRotateView.RotateDirectionLeft" = "往左移顺时针旋转"; 21 | "ZoomAndRotateView.RotateDirectionRight" = "往右移顺时针旋转"; 22 | "ZoomAndRotateView.RotateDirectionUp" = "往上移顺时针旋转"; 23 | "ZoomAndRotateView.RotateDirectionDown" = "往下移顺时针旋转"; 24 | 25 | "ZoomAndRotateView.Tips.Usage" = "你需要按住触发键,同时移动鼠标以实现缩放和旋转。"; 26 | "ZoomAndRotateView.Tips.Recover" = "有时缩放和旋转会突然间不工作。这是一个已知的系统问题,而且无法由我们解决。如果你遇到了这个问题,可以尝试通过以下步骤恢复: \n① 进入系统设置中的触控板选项,尝试重新开启相关手势;\n② 如果你在别的应用中使用了缩放和旋转手势作为触发手势,请尝试关闭它们然后重启电脑;\n③ 重启能解决一切问题。"; 27 | 28 | "SmartZoomView.Tips.UsageA" = "连续点击“缩放与旋转”的触发键以触发智能缩放。"; 29 | "SmartZoomView.Tips.UsageB" = "点击触发键以触发智能缩放。"; 30 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/KeyCombinationValidityChecker.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | 4 | enum KeyCombinationInvalidReason { 5 | case leftRightMouseButtonNeedModifier 6 | case needsKeyboardEventListener 7 | } 8 | 9 | final class KeyCombinationValidityChecker { 10 | let persisted: Readonly 11 | 12 | init(persisted: Readonly) { 13 | self.persisted = persisted 14 | } 15 | 16 | func checkValidity(_ keyCombination: KeyCombination?) -> KeyCombinationInvalidReason? { 17 | switch keyCombination?.activator { 18 | case .none: 19 | return nil 20 | case .key: 21 | return persisted.advanced.listenToKeyboardEvent 22 | ? nil 23 | : .needsKeyboardEventListener 24 | case .mouse: 25 | return KeyCombinationLeftRightMouseKeyValidityChecker().checkIsValid(keyCombination) 26 | ? nil 27 | : .leftRightMouseButtonNeedModifier 28 | } 29 | } 30 | } 31 | 32 | final class KeyCombinationLeftRightMouseKeyValidityChecker { 33 | func checkIsValid(_ keyCombination: KeyCombination?) -> Bool { 34 | if let kc = keyCombination, case let .mouse(index) = kc.activator, 35 | index == 1 || index == 0 36 | { 37 | return !kc.modifiers.isEmpty 38 | } 39 | return true 40 | } 41 | } 42 | 43 | extension KeyCombination { 44 | var validated: Self? { 45 | KeyCombinationLeftRightMouseKeyValidityChecker().checkIsValid(self) 46 | ? self 47 | : nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Content_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Content_32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Content_32-1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Content_64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Content_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Content_256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Content_256-1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Content_512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Content_512-1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Content.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | ITSAppUsesNonExemptEncryption 26 | 27 | LSApplicationCategoryType 28 | public.app-category.utilities 29 | LSMinimumSystemVersion 30 | $(MACOSX_DEPLOYMENT_TARGET) 31 | NSHumanReadableCopyright 32 | Copyright © 2020 Intii.com. All rights reserved. 33 | NSMainStoryboardFile 34 | Main 35 | NSPrincipalClass 36 | NSApplication 37 | NSSupportsAutomaticTermination 38 | 39 | NSSupportsSuddenTermination 40 | 41 | SUFeedURL 42 | https://abnormalmouse.intii.com/appcast.xml 43 | SUPublicEDKey 44 | WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY= 45 | 46 | 47 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_menuBar.imageset/MenuBar.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 5.000000 2.000000 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 4.000176 16.000000 m 16 | 1.687536 16.000000 -0.143230 14.044624 0.008833 11.736989 c 17 | 0.535997 3.736988 l 18 | 0.674541 1.634509 2.420301 0.000000 4.527339 0.000000 c 19 | 5.471784 0.000000 l 20 | 7.578823 0.000000 9.324583 1.634508 9.463127 3.736987 c 21 | 9.990292 11.736988 l 22 | 10.142354 14.044623 8.311585 16.000000 5.998946 16.000000 c 23 | 4.000176 16.000000 l 24 | h 25 | 5.000000 14.000000 m 26 | 4.447715 14.000000 4.000000 13.552284 4.000000 13.000000 c 27 | 4.000000 11.000000 l 28 | 4.000000 10.447716 4.447715 10.000000 5.000000 10.000000 c 29 | 5.552285 10.000000 6.000000 10.447716 6.000000 11.000000 c 30 | 6.000000 13.000000 l 31 | 6.000000 13.552284 5.552285 14.000000 5.000000 14.000000 c 32 | h 33 | f* 34 | n 35 | Q 36 | 37 | endstream 38 | endobj 39 | 40 | 3 0 obj 41 | 764 42 | endobj 43 | 44 | 4 0 obj 45 | << /Annots [] 46 | /Type /Page 47 | /MediaBox [ 0.000000 0.000000 20.000000 20.000000 ] 48 | /Resources 1 0 R 49 | /Contents 2 0 R 50 | /Parent 5 0 R 51 | >> 52 | endobj 53 | 54 | 5 0 obj 55 | << /Kids [ 4 0 R ] 56 | /Count 1 57 | /Type /Pages 58 | >> 59 | endobj 60 | 61 | 6 0 obj 62 | << /Type /Catalog 63 | /Pages 5 0 R 64 | >> 65 | endobj 66 | 67 | xref 68 | 0 7 69 | 0000000000 65535 f 70 | 0000000010 00000 n 71 | 0000000034 00000 n 72 | 0000000854 00000 n 73 | 0000000876 00000 n 74 | 0000001049 00000 n 75 | 0000001123 00000 n 76 | trailer 77 | << /ID [ (some) (id) ] 78 | /Root 6 0 R 79 | /Size 7 80 | >> 81 | startxref 82 | 1182 83 | %%EOF -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/ScrollSettings.strings: -------------------------------------------------------------------------------- 1 | "View.Title" = "Scroll and Swipe"; 2 | "View.Introduction" = "This feature converts mouse movement into scrolling. Better than that, it also allows you to play drag gestures by moving the mouse, like navigating back in Safari or marking emails as read in the Mail app."; 3 | "View.ActivationKeyCombinationTitle" = "Active when holding"; 4 | "View.ScrollSpeedSliderTitle" = "Scroll speed"; 5 | "View.SwipeSpeedSliderTitle" = "Swipe speed"; 6 | "View.ScrollSpeedSliderIntroduction" = "Scroll speed controls how mouse movements will be scaled-up into scrolls. A large number is preferred since you will not want to move the mouse a lot to scroll."; 7 | "View.SwipeSpeedSliderIntroduction" = "Swipe speed controls how it's scaled into swipe gesture translations. A slower speed will make it feel more natural."; 8 | "View.Tips.Usage" = "Hold activator then move your mouse to scroll."; 9 | "View.Tips.ActivatorChoice" = "Set a mouse button as activator to have a drag-to-scroll like experience."; 10 | "View.Tips.PageDown" = "Double tap to scroll down half a page."; 11 | "View.Tips.ScrollBar" = "Turn off show scroll bar in system preferences if you don't have a trackpad connected."; 12 | "View.InertiaEffectCheckboxTitle" = "Emulate inertia effect"; 13 | "View.InertiaEffectIntroduction" = "We recommend you to keep it on. Disabling it will disable inertia effect in some apps."; 14 | 15 | "HalfPageScrollView.Tips.UsageA" = "Double tap activator for \"move to scroll\" to trigger half page scroll."; 16 | "HalfPageScrollView.Tips.UsageB" = "Tap activator to trigger half page scroll."; 17 | 18 | "HalfPageScrollView.Title" = "Half Page Scroll"; 19 | "HalfPageScrollView.introduction" = "Page-down is cool, but a full-page scroll can be annoying. This feature allows you to scroll down half a page."; 20 | "HalfPageScrollView.ActivationKeyCombinationTitle" = "Trigger when"; 21 | "HalfPageScrollView.DoubleTapToActivate" = "Reuse activator for \"scroll and swipe\""; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,cocoapods,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,cocoapods,macos 4 | 5 | ### CocoaPods ### 6 | ## CocoaPods GitIgnore Template 7 | 8 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 9 | # - Also handy if you have a large number of dependant pods 10 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 11 | Pods/ 12 | 13 | ### macOS ### 14 | # General 15 | .DS_Store 16 | .AppleDouble 17 | .LSOverride 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | ### Xcode ### 42 | # Xcode 43 | # 44 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 45 | 46 | ## User settings 47 | xcuserdata/ 48 | 49 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 50 | *.xcscmblueprint 51 | *.xccheckout 52 | 53 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 54 | build/ 55 | DerivedData/ 56 | *.moved-aside 57 | *.pbxuser 58 | !default.pbxuser 59 | *.mode1v3 60 | !default.mode1v3 61 | *.mode2v3 62 | !default.mode2v3 63 | *.perspectivev3 64 | !default.perspectivev3 65 | 66 | ## Gcc Patch 67 | /*.gcno 68 | 69 | ### Xcode Patch ### 70 | *.xcodeproj/* 71 | !*.xcodeproj/project.pbxproj 72 | !*.xcodeproj/xcshareddata/ 73 | !*.xcworkspace/contents.xcworkspacedata 74 | **/xcshareddata/WorkspaceSettings.xcsettings 75 | 76 | # End of https://www.toptal.com/developers/gitignore/api/xcode,cocoapods,macos 77 | /AppDependencies/Package.swift 78 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/OverrideController/OverrideController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import CGEventOverride 3 | import Combine 4 | import Foundation 5 | 6 | protocol OverrideController {} 7 | 8 | class BaseOverrideController { 9 | var isDisabled: Bool = false 10 | let sharedPersisted: Readonly 11 | var cancellables: Set = .init() 12 | 13 | init(sharedPersisted: Readonly) { 14 | self.sharedPersisted = sharedPersisted 15 | 16 | let updateSettings = { [weak self] in 17 | guard let self = self, 18 | let id = NSWorkspace.shared.frontmostApplication?.bundleIdentifier 19 | else { return } 20 | if self.sharedPersisted.gloablExcludedApplications.contains(where: { 21 | $0.bundleIdentifier == id 22 | }) { 23 | self.isDisabled = true 24 | } else { 25 | self.isDisabled = false 26 | } 27 | } 28 | 29 | NSWorkspace.shared.notificationCenter 30 | .publisher(for: NSWorkspace.didActivateApplicationNotification) 31 | .sink { _ in updateSettings() } 32 | .store(in: &cancellables) 33 | 34 | updateSettings() 35 | } 36 | } 37 | 38 | final class FakeOverrideController: OverrideController { 39 | var updateSettingsCount = 0 40 | func updateSettings() { 41 | updateSettingsCount += 1 42 | } 43 | } 44 | 45 | final class FakeCGEventHook: CGEventHookType { 46 | var isEnabled: Bool = false 47 | var manipulations = [AnyHashable: CGEventManipulation]() 48 | @discardableResult 49 | func activateIfPossible() -> Bool { 50 | isEnabled = true 51 | return true 52 | } 53 | 54 | func add(_ manipulation: CGEventManipulation, forKey key: AnyHashable) { 55 | manipulations[key] = manipulation 56 | } 57 | 58 | func removeManipulation(forKey key: AnyHashable) { 59 | manipulations[key] = nil 60 | } 61 | 62 | func deactivate() { 63 | isEnabled = false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/StringFiles/en.lproj/ZoomAndRotateSettings.strings: -------------------------------------------------------------------------------- 1 | "ZoomAndRotateView.Title" = "Zoom and Rotate"; 2 | "ZoomAndRotateView.Introduction" = "This feature converts mouse movement into zoom and rotate gestures."; 3 | "ZoomAndRotateView.ActivationKeyCombinationTitle" = "Active when holding"; 4 | "ZoomAndRotateView.zoomSpeedSliderTitle" = "Zoom speed"; 5 | "ZoomAndRotateView.rotateSpeedSliderTitle" = "Rotate speed"; 6 | "SmartZoomView.Title" = "Smart Zoom"; 7 | "SmartZoomView.introduction" = "With trackpads, you can zoom in with a double-tap. Now you can do the same with a key combination."; 8 | "SmartZoomView.ActivationKeyCombinationTitle" = "Trigger when"; 9 | "SmartZoomView.DoubleTapToActivate" = "Reuse activator for \"zoom and rotate\""; 10 | 11 | "ZoomAndRotateView.ZoomDirectionTitle" = "Zoom gesture direction"; 12 | "ZoomAndRotateView.ZoomDirectionNone" = "Never zoom"; 13 | "ZoomAndRotateView.ZoomDirectionLeft" = "Left to zoom-in"; 14 | "ZoomAndRotateView.ZoomDirectionRight" = "Right to zoom-in"; 15 | "ZoomAndRotateView.ZoomDirectionUp" = "Up to zoom-in"; 16 | "ZoomAndRotateView.ZoomDirectionDown" = "Down to zoom-in"; 17 | 18 | "ZoomAndRotateView.RotateDirectionTitle" = "Rotate gesture direction"; 19 | "ZoomAndRotateView.RotateDirectionNone" = "Never Rotate"; 20 | "ZoomAndRotateView.RotateDirectionLeft" = "Left to rotate clockwise"; 21 | "ZoomAndRotateView.RotateDirectionRight" = "Right to rotate clockwise"; 22 | "ZoomAndRotateView.RotateDirectionUp" = "Up to rotate clockwise"; 23 | "ZoomAndRotateView.RotateDirectionDown" = "Down to rotate clockwise"; 24 | 25 | "ZoomAndRotateView.Tips.Usage" = "Hold activator then move your mouse to zoom or rotate."; 26 | "ZoomAndRotateView.Tips.Recover" = "Sometimes, zoom and rotate will stop working. It's a known os issue that can't be fixed on our side. If you encounter this issue, pleas try the following steps to recover: \n① Re-enable these gestures from system preferences trackpad pane.\n② If you are using any other app that is using these gestures as triggers, turn them off, then reboot.\n③ Reboot fixes everything."; 27 | 28 | "SmartZoomView.Tips.UsageA" = "Double tap activator for \"zoom and rotate\" to trigger smart zoom."; 29 | "SmartZoomView.Tips.UsageB" = "Tap activator to trigger smart zoom."; 30 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/SettingsSlider.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsSlider { 4 | @Binding var value: Value 5 | let range: ClosedRange 6 | let step: Value.Stride 7 | let title: Title 8 | let valueDisplay: ValueDisplay 9 | } 10 | 11 | extension SettingsSlider: View where Value.Stride: BinaryFloatingPoint { 12 | init( 13 | value: Binding, 14 | in range: ClosedRange, 15 | step: Value.Stride, 16 | @ViewBuilder valueDisplay: () -> ValueDisplay, 17 | @ViewBuilder title: () -> Title 18 | ) { 19 | _value = value 20 | self.range = range 21 | self.step = step 22 | self.title = title() 23 | self.valueDisplay = valueDisplay() 24 | } 25 | 26 | var body: some View { 27 | HStack(alignment: .center, spacing: 10) { 28 | title 29 | .asWidgetTitle() 30 | slider 31 | valueDisplay 32 | .foregroundColor(Color(NSColor.placeholderTextColor)) 33 | .font(Font.introduction.monospacedDigit()) 34 | } 35 | } 36 | 37 | private var slider: some View { 38 | Slider(value: $value, in: range, step: step) 39 | .frame(height: 20) 40 | .frame(maxWidth: 120) 41 | } 42 | } 43 | 44 | extension SettingsSlider where Value.Stride: BinaryFloatingPoint, ValueDisplay == EmptyView { 45 | init( 46 | value: Binding, 47 | in range: ClosedRange, 48 | step: Value.Stride, 49 | @ViewBuilder title: () -> Title 50 | ) { 51 | _value = value 52 | self.range = range 53 | self.step = step 54 | self.title = title() 55 | valueDisplay = EmptyView() 56 | } 57 | } 58 | 59 | struct SettingsSlider_Previews: PreviewProvider { 60 | @State static var value: Double = 5 61 | 62 | static var previews: some View { 63 | SettingsSlider( 64 | value: $value, 65 | in: 1...10, 66 | step: 1, 67 | valueDisplay: { Text("\(value)") }, 68 | title: { Text("Slider") } 69 | ) 70 | .padding(10) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /AppDependencies/.swiftpm/xcode/xcshareddata/xcschemes/AppDependencies.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/OverrideController/EventSequeceController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | /// The logical refresh rate. 5 | let virtualRefreshRate: Double = 60 6 | /// The actual physical refresh rate of main display. 7 | var refreshRate: Double { 8 | max(CGDisplayCopyDisplayMode(CGMainDisplayID())?.refreshRate ?? virtualRefreshRate, 60) 9 | } 10 | 11 | var refreshSpeedScale: Double { refreshRate / virtualRefreshRate } 12 | 13 | /// Used to send a sequence of event to fire one per each frame. 14 | final class EventSequenceController { 15 | typealias Task = () -> Void 16 | 17 | static var shared: EventSequenceController? { 18 | if let controller = _shared { return controller } 19 | _shared = EventSequenceController() 20 | return _shared 21 | } 22 | 23 | private static var _shared: EventSequenceController? 24 | 25 | private var queuedTasks = [AnyHashable: [Task]]() 26 | private var displayLink: CVDisplayLink! 27 | private var isDisplayLinkStarted: Bool = false 28 | 29 | private init?() { 30 | _ = CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &displayLink) 31 | guard displayLink != nil else { return nil } 32 | CVDisplayLinkSetOutputHandler(displayLink) { [weak self] _, _, _, _, _ in 33 | guard let self = self else { return kCVReturnSuccess } 34 | let tasks: [Task] = DispatchQueue.default.sync { 35 | var runNow = [Task]() 36 | var temp = self.queuedTasks 37 | for (key, _list) in self.queuedTasks { 38 | var list = _list 39 | guard !list.isEmpty else { continue } 40 | runNow.append(list.removeFirst()) 41 | temp[key] = list 42 | } 43 | self.queuedTasks = temp 44 | return runNow 45 | } 46 | 47 | // for better performance, we disable displayLink when there is no more event to send. 48 | if tasks.isEmpty { 49 | self.disableLink() 50 | return kCVReturnDisplayLinkNotRunning 51 | } 52 | tasks.forEach { $0() } 53 | return kCVReturnSuccess 54 | } 55 | } 56 | 57 | /// Schedule a sequence of event to be fired for a specific feature. 58 | func scheduleTasks(_ tasks: [() -> Void], forKey key: AnyHashable) { 59 | DispatchQueue.main.async { 60 | if !tasks.isEmpty { 61 | CVDisplayLinkStart(self.displayLink) 62 | self.isDisplayLinkStarted = true 63 | } 64 | self.queuedTasks[key] = tasks 65 | } 66 | } 67 | 68 | private func disableLink() { 69 | DispatchQueue.main.async { 70 | CVDisplayLinkStop(self.displayLink) 71 | self.isDisplayLinkStarted = false 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/DockSwipe/DockSwipeSettingsView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct DockSwipeSettingsScreen: View { 5 | let store: DockSwipeDomain.Store 6 | 7 | var body: some View { 8 | DockSwipeSettingsView(store: store) 9 | .lifeCycleWithViewStore(store, onAppear: { viewStore in 10 | viewStore.send(.appear) 11 | }) 12 | } 13 | } 14 | 15 | private struct DockSwipeSettingsView: View { 16 | let store: DockSwipeDomain.Store 17 | 18 | var body: some View { 19 | ScrollView { 20 | DockSwipeView(store: store) 21 | Spacer() 22 | } 23 | } 24 | } 25 | 26 | private struct DockSwipeView: View { 27 | let store: DockSwipeDomain.Store 28 | 29 | var body: some View { 30 | SettingsSectionView( 31 | showSeparator: false, 32 | title: { Text(_L10n.View.title) }, 33 | introduction: { Text(_L10n.View.introduction) }, 34 | content: { 35 | WithViewStore( 36 | store.scope( 37 | state: \.dockSwipeActivator, 38 | action: DockSwipeDomain.Action.dockSwipe 39 | ) 40 | ) { 41 | viewStore in 42 | SettingsKeyCombinationInput( 43 | keyCombination: viewStore.binding( 44 | get: { $0.keyCombination }, 45 | send: { .setKeyCombination($0) } 46 | ), 47 | numberOfTapsRequired: viewStore.binding( 48 | get: { $0.numberOfTapsRequired }, 49 | send: { .setNumberOfTapsRequired($0) } 50 | ), 51 | hasConflict: viewStore.hasConflict, 52 | invalidReason: viewStore.invalidReason, 53 | title: { Text(_L10n.View.activationKeyCombinationTitle) } 54 | ) 55 | } 56 | 57 | SettingsTips { 58 | Text(_L10n.View.Tips.usage).tipsTitle(_L10n.TipsTitle.usage) 59 | EmptyView() 60 | } 61 | } 62 | ) 63 | } 64 | } 65 | 66 | private enum _L10n { 67 | typealias View = L10n.DockSwipeSettings.View 68 | typealias TipsTitle = L10n.Shared.TipsTitle 69 | } 70 | 71 | struct DockSwipeSettingsView_Previews: PreviewProvider { 72 | static var previews: some View { 73 | DockSwipeSettingsView(store: .init( 74 | initialState: .init(), 75 | reducer: DockSwipeDomain.reducer, 76 | environment: .live(environment: .init( 77 | persisted: .init(), 78 | featureHasConflict: { _ in true }, 79 | checkKeyCombinationValidity: { _ in nil } 80 | )) 81 | )) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/ActivatorConflictChecker.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | 4 | final class ActivatorConflictChecker { 5 | enum Feature: Equatable, CaseIterable { 6 | case moveToScroll 7 | case halfPageScroll 8 | case zoomAndRotate 9 | case smartZoom 10 | case dockSwipe 11 | } 12 | 13 | private let persisted: Readonly 14 | private var activators = [Feature: Activator]() 15 | private var cancellables = Set() 16 | 17 | init(persisted: Readonly) { 18 | self.persisted = persisted 19 | updateKeyCombinations() 20 | } 21 | 22 | func featureHasConflict(_ feature: Feature) -> Bool { 23 | updateKeyCombinations() 24 | guard let activator = activators[feature] else { return false } 25 | for key in activators.keys { 26 | guard key != feature else { continue } 27 | if activators[key] == activator { return true } 28 | } 29 | return false 30 | } 31 | 32 | private func updateKeyCombinations() { 33 | activators[.moveToScroll] = Activator( 34 | keyCombination: persisted.moveToScroll.keyCombination, 35 | numberOfTapRequired: persisted.moveToScroll.numberOfTapsRequired 36 | ) 37 | activators[.halfPageScroll] = { 38 | if persisted.moveToScroll.halfPageScroll.useMoveToScrollDoubleTap { 39 | return Activator( 40 | keyCombination: persisted.moveToScroll.keyCombination, 41 | numberOfTapRequired: persisted.moveToScroll.numberOfTapsRequired + 1 42 | ) 43 | } 44 | return Activator( 45 | keyCombination: persisted.moveToScroll.halfPageScroll.keyCombination, 46 | numberOfTapRequired: persisted.moveToScroll.halfPageScroll.numberOfTapsRequired 47 | ) 48 | }() 49 | activators[.zoomAndRotate] = Activator( 50 | keyCombination: persisted.zoomAndRotate.keyCombination, 51 | numberOfTapRequired: persisted.zoomAndRotate.numberOfTapsRequired 52 | ) 53 | activators[.smartZoom] = { 54 | if persisted.zoomAndRotate.smartZoom.useZoomAndRotateDoubleTap { 55 | return Activator( 56 | keyCombination: persisted.zoomAndRotate.keyCombination, 57 | numberOfTapRequired: persisted.zoomAndRotate.numberOfTapsRequired + 1 58 | ) 59 | } 60 | return Activator( 61 | keyCombination: persisted.zoomAndRotate.smartZoom.keyCombination, 62 | numberOfTapRequired: persisted.zoomAndRotate.smartZoom.numberOfTapsRequired 63 | ) 64 | }() 65 | activators[.dockSwipe] = Activator( 66 | keyCombination: persisted.dockSwipe.keyCombination, 67 | numberOfTapRequired: persisted.dockSwipe.numberOfTapsRequired 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/TipsViewBuilder.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @resultBuilder 4 | enum TipsViewBuilder { 5 | typealias Decorator = SettingsTipsDecorator 6 | 7 | static func d(_ content: Content) -> Decorator { 8 | Decorator(content: content) 9 | } 10 | 11 | static func buildBlock() -> EmptyView { 12 | EmptyView() 13 | } 14 | 15 | static func buildBlock(_ content: Content) -> Decorator { 16 | ViewBuilder.buildBlock(d(content)) 17 | } 18 | 19 | static func buildBlock( 20 | _ c0: C0, 21 | _ c1: C1 22 | ) -> TupleView<(Decorator, Decorator)> { 23 | ViewBuilder.buildBlock(d(c0), d(c1)) 24 | } 25 | 26 | static func buildBlock( 27 | _ c0: C0, 28 | _ c1: C1, 29 | _ c2: C2 30 | ) -> TupleView<(Decorator, Decorator, Decorator)> { 31 | ViewBuilder.buildBlock(d(c0), d(c1), d(c2)) 32 | } 33 | 34 | static func buildBlock( 35 | _ c0: C0, 36 | _ c1: C1, 37 | _ c2: C2, 38 | _ c3: C3 39 | ) -> TupleView<(Decorator, Decorator, Decorator, Decorator)> { 40 | ViewBuilder.buildBlock(d(c0), d(c1), d(c2), d(c3)) 41 | } 42 | 43 | static func buildBlock( 44 | _ c0: C0, 45 | _ c1: C1, 46 | _ c2: C2, 47 | _ c3: C3, 48 | _ c4: C4 49 | ) -> TupleView<(Decorator, Decorator, Decorator, Decorator, Decorator)> { 50 | ViewBuilder.buildBlock(d(c0), d(c1), d(c2), d(c3), d(c4)) 51 | } 52 | 53 | static func buildBlock( 54 | _ c0: C0, 55 | _ c1: C1, 56 | _ c2: C2, 57 | _ c3: C3, 58 | _ c4: C4, 59 | _ c5: C5 60 | ) -> TupleView<( 61 | Decorator, 62 | Decorator, 63 | Decorator, 64 | Decorator, 65 | Decorator, 66 | Decorator 67 | )> { 68 | ViewBuilder.buildBlock(d(c0), d(c1), d(c2), d(c3), d(c4), d(c5)) 69 | } 70 | 71 | static func buildEither< 72 | TrueContent: View, 73 | FalseContent: View 74 | >( 75 | first: TrueContent 76 | ) -> _ConditionalContent, Decorator> { 77 | ViewBuilder.buildEither(first: d(first)) 78 | } 79 | 80 | static func buildEither< 81 | TrueContent: View, 82 | FalseContent: View 83 | >( 84 | second: FalseContent 85 | ) -> _ConditionalContent, Decorator> { 86 | ViewBuilder.buildEither(second: d(second)) 87 | } 88 | 89 | static func buildIf(_ content: Content?) -> Decorator? { 90 | ViewBuilder.buildIf(content.map(d)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouseTests/DockSwipeDomainTests.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import ComposableArchitecture 3 | import XCTest 4 | 5 | @testable import AbnormalMouse 6 | 7 | class DockSwipeDomainTests: XCTestCase { 8 | func testDockSwipeSettings() { 9 | let persisted = Persisted( 10 | userDefaults: MemoryPropertyListStorage(), 11 | keychainAccess: FakeKeychainAccess() 12 | ).dockSwipe 13 | 14 | var hasConflict: (ActivatorConflictChecker.Feature) -> Bool = { $0 == .dockSwipe } 15 | 16 | let initialState = DockSwipeDomain.State(from: persisted) 17 | let store = TestStore( 18 | initialState: initialState, 19 | reducer: DockSwipeDomain.reducer, 20 | environment: .init( 21 | environment: .init( 22 | persisted: persisted, 23 | featureHasConflict: { hasConflict($0) } 24 | ), 25 | date: { Date() }, 26 | openURL: { _ in }, 27 | quitApp: {}, 28 | mainQueue: { .main } 29 | ) 30 | ) 31 | 32 | let keyCombination = KeyCombination(Set([ 33 | .key(KeyboardCode.command.rawValue), 34 | .key(KeyboardCode.a.rawValue), 35 | ])) 36 | 37 | XCTAssertEqual(initialState.dockSwipeActivator.keyCombination, nil) 38 | XCTAssertEqual(initialState.dockSwipeActivator.numberOfTapsRequired, 1) 39 | XCTAssertEqual(initialState.dockSwipeActivator.hasConflict, false) 40 | 41 | store.assert( 42 | .send(.dockSwipe(.setKeyCombination(keyCombination))) { 43 | $0.dockSwipeActivator.keyCombination = keyCombination 44 | }, 45 | .receive(._internal(.checkConflict)) { 46 | $0.dockSwipeActivator.hasConflict = true 47 | }, 48 | .do { 49 | XCTAssertEqual(persisted.keyCombination, keyCombination) 50 | hasConflict = { _ in false } 51 | }, 52 | .send(.dockSwipe(.setKeyCombination(keyCombination))) { 53 | $0.dockSwipeActivator.keyCombination = keyCombination 54 | }, 55 | .receive(._internal(.checkConflict)) { 56 | $0.dockSwipeActivator.hasConflict = false 57 | }, 58 | .do { 59 | XCTAssertEqual(persisted.keyCombination, keyCombination) 60 | }, 61 | .send(.dockSwipe(.setNumberOfTapsRequired(2))) { 62 | $0.dockSwipeActivator.numberOfTapsRequired = 2 63 | }, 64 | .receive(._internal(.checkConflict)), 65 | .do { 66 | XCTAssertEqual(persisted.numberOfTapsRequired, 2) 67 | }, 68 | .send(.dockSwipe(.clearKeyCombination)) { 69 | $0.dockSwipeActivator.keyCombination = nil 70 | }, 71 | .receive(._internal(.checkConflict)), 72 | .do { 73 | XCTAssertEqual(persisted.keyCombination, nil) 74 | } 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/GestureRecognizer/MouseMovementGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import Combine 3 | import Foundation 4 | 5 | extension GestureRecognizers { 6 | final class MouseMovement: GestureRecognizer { 7 | private struct Key: Hashable {} 8 | 9 | var isActive: Bool = false { 10 | didSet { 11 | guard isActive != oldValue else { return } 12 | if isActive { 13 | cancelOtherGestures { $0 is MouseMovement } 14 | } else { 15 | throttler.endWithLastValue() 16 | } 17 | } 18 | } 19 | 20 | let publisher: AnyPublisher<(CGSize, CGEvent), Never> 21 | 22 | private let hook: CGEventHookType 23 | private let translation: PassthroughSubject<(CGSize, CGEvent), Never> 24 | private let key: AnyHashable 25 | private let throttler: EventThrottler<(CGSize, CGEvent?)> 26 | var rate: Int { 27 | get { throttler.rate } 28 | set { throttler.rate = newValue } 29 | } 30 | 31 | deinit { 32 | hook.removeManipulation(forKey: key) 33 | } 34 | 35 | init(hook: CGEventHookType, key: AnyHashable) { 36 | self.key = key 37 | self.hook = hook 38 | let translation = PassthroughSubject<(CGSize, CGEvent), Never>() 39 | self.translation = translation 40 | publisher = translation 41 | .receive(on: DispatchQueue.default) 42 | .eraseToAnyPublisher() 43 | throttler = .init((.zero, nil)) { p in 44 | guard let e = p.1 else { return } 45 | translation.send((p.0, e)) 46 | } 47 | super.init() 48 | 49 | hook.add( 50 | .init( 51 | eventsOfInterest: [.mouseMoved, .otherMouseDragged, .leftMouseDragged, 52 | .rightMouseDragged], 53 | convert: { [weak self] _, _, event -> CGEventManipulation.Result in 54 | DispatchQueue.default.sync { 55 | guard let self = self else { return .unchange } 56 | return self.handleMouse(event: event) 57 | } 58 | } 59 | ), 60 | forKey: key 61 | ) 62 | } 63 | } 64 | } 65 | 66 | extension GestureRecognizers.MouseMovement: Cancellable { 67 | func cancel() { 68 | isActive = false 69 | } 70 | } 71 | 72 | extension GestureRecognizers.MouseMovement { 73 | private func handleMouse(event: CGEvent) -> CGEventManipulation.Result { 74 | guard isActive else { return .unchange } 75 | 76 | let v = event[double: .mouseEventDeltaY] 77 | let h = event[double: .mouseEventDeltaX] 78 | throttler.post(accumulate: { t in 79 | t.0.width += CGFloat(h) 80 | t.0.height += CGFloat(v) 81 | t.1 = event 82 | }) 83 | 84 | return .discarded 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Generated/Assets.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | #if os(macOS) 5 | import AppKit 6 | #elseif os(iOS) 7 | import UIKit 8 | #elseif os(tvOS) || os(watchOS) 9 | import UIKit 10 | #endif 11 | 12 | // Deprecated typealiases 13 | @available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") 14 | internal typealias AssetImageTypeAlias = ImageAsset.Image 15 | 16 | // swiftlint:disable superfluous_disable_command file_length implicit_return 17 | 18 | // MARK: - Asset Catalogs 19 | 20 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 21 | internal enum Asset { 22 | internal static let iconAccessibilityOff = ImageAsset(name: "icon_accessibilityOff") 23 | internal static let iconAdvanced = ImageAsset(name: "icon_advanced") 24 | internal static let iconApp = ImageAsset(name: "icon_app") 25 | internal static let iconDockSwipe = ImageAsset(name: "icon_dockSwipe") 26 | internal static let iconGeneral = ImageAsset(name: "icon_general") 27 | internal static let iconMenuBar = ImageAsset(name: "icon_menuBar") 28 | internal static let iconMoveToScroll = ImageAsset(name: "icon_moveToScroll") 29 | internal static let iconZoomAndRotate = ImageAsset(name: "icon_zoomAndRotate") 30 | } 31 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 32 | 33 | // MARK: - Implementation Details 34 | 35 | internal struct ImageAsset { 36 | internal fileprivate(set) var name: String 37 | 38 | #if os(macOS) 39 | internal typealias Image = NSImage 40 | #elseif os(iOS) || os(tvOS) || os(watchOS) 41 | internal typealias Image = UIImage 42 | #endif 43 | 44 | internal var image: Image { 45 | let bundle = BundleToken.bundle 46 | #if os(iOS) || os(tvOS) 47 | let image = Image(named: name, in: bundle, compatibleWith: nil) 48 | #elseif os(macOS) 49 | let name = NSImage.Name(self.name) 50 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) 51 | #elseif os(watchOS) 52 | let image = Image(named: name) 53 | #endif 54 | guard let result = image else { 55 | fatalError("Unable to load image named \(name).") 56 | } 57 | return result 58 | } 59 | } 60 | 61 | internal extension ImageAsset.Image { 62 | @available(macOS, deprecated, 63 | message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") 64 | convenience init!(asset: ImageAsset) { 65 | #if os(iOS) || os(tvOS) 66 | let bundle = BundleToken.bundle 67 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 68 | #elseif os(macOS) 69 | self.init(named: NSImage.Name(asset.name)) 70 | #elseif os(watchOS) 71 | self.init(named: asset.name) 72 | #endif 73 | } 74 | } 75 | 76 | // swiftlint:disable convenience_type 77 | private final class BundleToken { 78 | static let bundle: Bundle = { 79 | #if SWIFT_PACKAGE 80 | return Bundle.module 81 | #else 82 | return Bundle(for: BundleToken.self) 83 | #endif 84 | }() 85 | } 86 | // swiftlint:enable convenience_type 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 2022.3 4 | 5 | - Reimplement zoom and rotate. 6 | - Support setting zoom and rotate speed. 7 | - Reopening app shows preference panel. 8 | - Fix half page scroll in full screen app, now it uses window under cursor as reference. 9 | - Allow disabling Abnormal Mouse in selected apps. 10 | 11 | ### 2022.2 12 | 13 | - Allow using left/right mouse button as activator. 14 | - Adjust UI. 15 | - Fix potential crash by data race. 16 | - Fix that mouse button is not handled correctly in gesture recognizers. 17 | - Set listen to keyboard events to defaultly false. 18 | - Stop listening to mouse move events when keyboard events are off for better CPU usage when idle. 19 | 20 | ### 2022.1 21 | 22 | - Fix that the auto check for update prompt can't be display and blocks the app. 23 | - Fix that launch at login not working. 24 | 25 | ### 2021.3 26 | 27 | - Fix that network requests are not sending sometimes (remove waitAtLeast). 28 | 29 | ### 2021.2 30 | 31 | - Fix that some localizations may crash the app at launch. 32 | 33 | ### 2020.10 34 | 35 | - Fix that SUUpdater was loaded too early and messed up windows. 36 | - Again use CocoaPods for Sparkle. 37 | - Remove the name localization. 38 | 39 | ### 2020.9 40 | 41 | - Fix Reeder.app won't recognize scroll sometimes. 42 | - Fix Calendar.app won't recognize scroll. 43 | 44 | ### 2020.8 45 | 46 | - Adjust app icon to have a stronger Big Sur taste. 47 | - Add more keyboard code name. 48 | - Build for Apple Silicon (not tested). 49 | 50 | ### 2020.7 51 | 52 | - Update UI for Big Sur. 53 | - Use a swift package AppDependencies to handle all swift dependencies. 54 | - Compute rotation value with mouse translation. 55 | 56 | ### 2020.6 57 | 58 | - Fix that multiple tap hold gesture with keyboard key as activator is automatically canceled after trigger. 59 | - Fix that modifiers are ignored when using mouse buttons as activators. 60 | 61 | ### 2020.5 62 | 63 | - Code cleanup. 64 | - New activator setter. 65 | - Allow setting half page scroll activator. 66 | - Support activator sharing with different number-of-taps-required. 67 | - Add activator conflict check. 68 | - Tweak zoom and rotate. 69 | 70 | ### 2020.4 71 | 72 | - Support 4 finger swipe gestures. 73 | - Fix that smart zoom settings are not persisted. 74 | 75 | ### Open Source 76 | 77 | - Open source. 78 | - Support to build without license management logics. 79 | - Detach license management and CGEventOverride to other repos. 80 | 81 | ### 2020.3 82 | 83 | - Gesture base override controllers. 84 | - Code clean up. 85 | - Add listen to keyboard event toggle. 86 | - Rename "Scroll" to "Scroll and Swipe". 87 | - Add `MainDomain` extracted from `TheApp`. 88 | - Tweak zooming. 89 | 90 | ### 2020.2 91 | 92 | - Remove sandbox entitlement from launcher. 93 | 94 | ### 2020.1 95 | 96 | - Enable harden runtime. 97 | - Fix crashes. 98 | - Fix main scene accessability sheet can't present. 99 | 100 | ### 2020.0 101 | 102 | - Initial release. 103 | - Move to scroll, gesture, half page scroll. 104 | - Rotate and zoom, smart zoom. 105 | - License management, trial mode. 106 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse.xcodeproj/xcshareddata/xcschemes/AbnormalMouseLauncher.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/SettingsTipsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsTips: View { 4 | let content: Content 5 | 6 | init(@TipsViewBuilder content: () -> Content) { 7 | self.content = content() 8 | } 9 | 10 | var body: some View { 11 | VStack(alignment: .leading, spacing: 8) { content } 12 | .padding(.top, 12) 13 | } 14 | } 15 | 16 | struct SettingsTipsDecorator: View { 17 | let content: Content 18 | @State var title: String = "" 19 | var body: some View { 20 | HStack(alignment: .firstTextBaseline, spacing: 4) { 21 | if !title.isEmpty { 22 | Text(title) 23 | .font(.system(size: 12)) 24 | .foregroundColor(Color.white) 25 | .padding([.leading, .trailing], 4) 26 | .padding(.top, 2) 27 | .padding(.bottom, 3) 28 | .roundedCornerBackground(cornerRadius: 4, fillColor: Color.accentColor) 29 | .shadow(radius: 1) 30 | .overlay( 31 | LinearGradient( 32 | gradient: Gradient(colors: [ 33 | Color.white.opacity(0.2), 34 | Color.clear, 35 | ]), 36 | startPoint: .init(x: 0.5, y: 0), 37 | endPoint: .init(x: 0.5, y: 0.1) 38 | ) 39 | .blur(radius: 4).cornerRadius(4) 40 | ) 41 | .overlay( 42 | LinearGradient( 43 | gradient: Gradient(colors: [ 44 | Color.black.opacity(0.1), 45 | Color.clear, 46 | ]), 47 | startPoint: .init(x: 0.5, y: 1), 48 | endPoint: .init(x: 0.5, y: 0.9) 49 | ) 50 | .blur(radius: 4).cornerRadius(4) 51 | ) 52 | } 53 | 54 | content 55 | .asFeatureIntroduction() 56 | } 57 | // .onPreferenceChange(SettingsTipsTitleKey.self) { title in 58 | // self.title = title 59 | // } // There is a bug preventing the upper block to call! Workaround below. 60 | .overlayPreferenceValue(SettingsTipsTitleKey.self) { title in 61 | EmptyView().onAppear { 62 | self.title = title 63 | } 64 | } 65 | } 66 | } 67 | 68 | struct SettingsTipsTitleKey: PreferenceKey { 69 | static var defaultValue: String = "" 70 | static func reduce(value: inout String, nextValue: () -> String) { 71 | value = nextValue() 72 | } 73 | } 74 | 75 | extension View { 76 | func tipsTitle(_ title: String) -> some View { 77 | preference(key: SettingsTipsTitleKey.self, value: title) 78 | } 79 | } 80 | 81 | struct SettingsTipsView_Previews: PreviewProvider { 82 | static var previews: some View { 83 | SettingsTips(content: { Text("Hello world.").tipsTitle("New") }) 84 | .padding(10) 85 | .frame(width: 300, alignment: .leading) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /AbnormalMouse.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CGEventOverride", 6 | "repositoryURL": "https://github.com/intitni/CGEventOverride.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "a6585d580eedc151ec9918af06a182d26e1248f5", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "combine-schedulers", 15 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 16 | "state": { 17 | "branch": null, 18 | "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", 19 | "version": "0.5.3" 20 | } 21 | }, 22 | { 23 | "package": "CombineExt", 24 | "repositoryURL": "https://github.com/CombineCommunity/CombineExt", 25 | "state": { 26 | "branch": null, 27 | "revision": "0880829102152185190064fd17847a7c681d2127", 28 | "version": "1.5.1" 29 | } 30 | }, 31 | { 32 | "package": "LaunchAtLogin", 33 | "repositoryURL": "https://github.com/sindresorhus/LaunchAtLogin", 34 | "state": { 35 | "branch": null, 36 | "revision": "e8171b3e38a2816f579f58f3dac1522aa39efe41", 37 | "version": "4.2.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-case-paths", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", 43 | "state": { 44 | "branch": null, 45 | "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", 46 | "version": "0.7.0" 47 | } 48 | }, 49 | { 50 | "package": "swift-collections", 51 | "repositoryURL": "https://github.com/apple/swift-collections", 52 | "state": { 53 | "branch": null, 54 | "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", 55 | "version": "1.0.1" 56 | } 57 | }, 58 | { 59 | "package": "swift-composable-architecture", 60 | "repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture", 61 | "state": { 62 | "branch": null, 63 | "revision": "599a2398adaaa7a4e3f5420cde7728c39e33677e", 64 | "version": "0.28.1" 65 | } 66 | }, 67 | { 68 | "package": "swift-custom-dump", 69 | "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", 70 | "state": { 71 | "branch": null, 72 | "revision": "c2dd2c64b753dda592f5619303e02f741cd3e862", 73 | "version": "0.2.0" 74 | } 75 | }, 76 | { 77 | "package": "swift-identified-collections", 78 | "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", 79 | "state": { 80 | "branch": null, 81 | "revision": "c8e6a40209650ab619853cd4ce89a0aa51792754", 82 | "version": "0.3.0" 83 | } 84 | }, 85 | { 86 | "package": "xctest-dynamic-overlay", 87 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 88 | "state": { 89 | "branch": null, 90 | "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", 91 | "version": "0.2.1" 92 | } 93 | } 94 | ] 95 | }, 96 | "version": 1 97 | } 98 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/Advanced/AdvancedDomain.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | import ServiceManagement 6 | 7 | enum AdvancedDomain: Domain { 8 | typealias ExcludedApplication = Persisted.Advanced.ExcludedApplication 9 | 10 | struct State: Equatable { 11 | var listenToKeyboardEvent: Bool = false 12 | var excludedApps: [ExcludedApplication] = [] 13 | var availableApplications: [ExcludedApplication] = [] 14 | var selectedExcludedApp: ExcludedApplication? 15 | } 16 | 17 | enum Action { 18 | case appear 19 | case toggleListenToKeyboardEvents 20 | case addExcludedApp(ExcludedApplication) 21 | case selectExcludedApp(ExcludedApplication) 22 | case removeSelectedExcludedApp 23 | } 24 | 25 | typealias Environment = SystemEnvironment<_Environment> 26 | struct _Environment { 27 | let persisted: Persisted.Advanced 28 | } 29 | 30 | enum CancellableKeys: Hashable { 31 | case observePurchaseState 32 | } 33 | 34 | static let reducer = Reducer { state, action, environment in 35 | switch action { 36 | case .appear: 37 | state = .init(from: environment.persisted) 38 | return .none 39 | case .toggleListenToKeyboardEvents: 40 | state.listenToKeyboardEvent.toggle() 41 | let result = state.listenToKeyboardEvent 42 | return .fireAndForget { 43 | environment.persisted.listenToKeyboardEvent = result 44 | } 45 | case let .addExcludedApp(app): 46 | if state.excludedApps.contains(app) { return .none } 47 | state.excludedApps.append(app) 48 | let list = state.excludedApps 49 | return .fireAndForget { 50 | environment.persisted.gloablExcludedApplications = list 51 | } 52 | case .removeSelectedExcludedApp: 53 | guard let selected = state.selectedExcludedApp else { return .none } 54 | state.excludedApps.removeAll { selected.bundleIdentifier == $0.bundleIdentifier } 55 | let list = state.excludedApps 56 | state.selectedExcludedApp = nil 57 | return .fireAndForget { 58 | environment.persisted.gloablExcludedApplications = list 59 | } 60 | case let .selectExcludedApp(app): 61 | state.selectedExcludedApp = app 62 | return .none 63 | } 64 | } 65 | } 66 | 67 | extension AdvancedDomain.State { 68 | init(from persisted: Persisted.Advanced) { 69 | listenToKeyboardEvent = persisted.listenToKeyboardEvent 70 | excludedApps = persisted.gloablExcludedApplications 71 | availableApplications = NSWorkspace.shared.runningApplications.compactMap { 72 | guard let name = $0.localizedName, 73 | let identifier = $0.bundleIdentifier, 74 | $0.activationPolicy == .regular else { return nil } 75 | return .init(appName: name, bundleIdentifier: identifier) 76 | } 77 | } 78 | } 79 | 80 | extension Store where State == AdvancedDomain.State, Action == AdvancedDomain.Action { 81 | static let testStore: AdvancedDomain.Store = .init( 82 | initialState: .init(), 83 | reducer: AdvancedDomain.reducer, 84 | environment: .live(environment: .init( 85 | persisted: .init() 86 | )) 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/License.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import CombineExt 4 | import ComposableArchitecture 5 | import Foundation 6 | 7 | protocol PurchaseManagerType { 8 | var purchaseState: CurrentValueRelay { get } 9 | func startTrialIfNeeded() 10 | func reverifyIfNeeded() 11 | func activateLicense(key: String, email: String) -> Effect, Never> 12 | func verifyLicense() -> Effect, Never> 13 | func deactivate() -> Effect, Never> 14 | func updatePurchaseState() 15 | } 16 | 17 | enum LicenseState: Int, Codable { 18 | /// There is no license 19 | case none 20 | /// License is considered valid 21 | case valid 22 | /// License is considered fake 23 | case fake 24 | /// License is refunded 25 | case refunded 26 | /// License is invalid 27 | case invalid 28 | } 29 | 30 | #if canImport(License) 31 | 32 | import License 33 | 34 | typealias PurchaseState = License.PurchaseState 35 | typealias PurchaseError = License.PurchaseError 36 | 37 | final class RealPurchaseManager: PurchaseManagerType { 38 | let purchaseState = CurrentValueRelay(.initial) 39 | private let p = FastSpringPurchaseManager() 40 | private var cancellables = Set() 41 | 42 | init() { 43 | p.purchaseState.subscribe(purchaseState).store(in: &cancellables) 44 | } 45 | 46 | func startTrialIfNeeded() { 47 | p.startTrialIfNeeded() 48 | } 49 | 50 | func reverifyIfNeeded() { 51 | p.reverifyIfNeeded() 52 | } 53 | 54 | func activateLicense( 55 | key: String, 56 | email: String 57 | ) -> Effect, Never> { 58 | p.activateLicense(key: key, email: email).catchToEffect() 59 | } 60 | 61 | func verifyLicense() -> Effect, Never> { 62 | p.verifyLicense().catchToEffect() 63 | } 64 | 65 | func deactivate() -> Effect, Never> { 66 | p.deactivate().catchToEffect() 67 | } 68 | 69 | func updatePurchaseState() { 70 | p.updatePurchaseState() 71 | } 72 | } 73 | 74 | #else 75 | 76 | typealias RealPurchaseManager = FakePurchaseManager 77 | 78 | enum PurchaseError: Swift.Error { 79 | case other(Swift.Error) 80 | case failedToVerifyLicenseKeyLocally 81 | case licenseKeyIsInvalid 82 | case licenseKeyIsRefunded 83 | case licenseKeyHasReachedActivationLimit 84 | } 85 | 86 | enum PurchaseState: Equatable { 87 | case initial 88 | case trialDidEnd 89 | case trialMode(daysLeft: Int) 90 | case activated(email: String) 91 | case activatedInvalid(email: String) 92 | case activatedUnverifiedForALongTime(email: String) 93 | case activatedRefunded(email: String) 94 | case activatedMaybePirateUser(email: String) 95 | } 96 | 97 | #endif 98 | 99 | struct FakePurchaseManager: PurchaseManagerType { 100 | func reverifyIfNeeded() {} 101 | 102 | var purchaseState: CurrentValueRelay = .init(.activated(email: "github")) 103 | 104 | func updatePurchaseState() {} 105 | 106 | func startTrialIfNeeded() {} 107 | 108 | func activateLicense( 109 | key _: String, 110 | email _: String 111 | ) -> Effect, Never> { 112 | Effect(value: .success(())) 113 | } 114 | 115 | func verifyLicense() -> Effect, Never> { 116 | Effect(value: .success(())) 117 | } 118 | 119 | func deactivate() -> Effect, Never> { 120 | Effect(value: .success(())) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/Activation/ActivationDomain.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | 6 | enum ActivationDomain: Domain { 7 | struct State: Equatable { 8 | var licenseKey = "" 9 | var email = "" 10 | 11 | var activationState: ActivationState = .entering 12 | enum ActivationState: Equatable { 13 | case entering 14 | case activating 15 | case failed(reason: String) 16 | } 17 | 18 | var isActivateButtonEnabled: Bool { 19 | if case .activating = activationState { return false } 20 | return !licenseKey.isEmpty && !email.isEmpty 21 | } 22 | } 23 | 24 | enum Action { 25 | case updateLicenseKey(String) 26 | case updateEmail(String) 27 | case buyNow 28 | case activate 29 | case failInActivation(reason: String) 30 | case dismiss 31 | } 32 | 33 | typealias Environment = SystemEnvironment<_Environment> 34 | struct _Environment { 35 | let purchaseManager: PurchaseManagerType 36 | } 37 | 38 | static let reducer = Reducer { state, action, environment in 39 | switch action { 40 | case let .updateLicenseKey(key): 41 | state.licenseKey = key 42 | return .none 43 | case let .updateEmail(email): 44 | state.email = email 45 | return .none 46 | case .buyNow: // handled by parent 47 | return .none 48 | case .activate: 49 | switch state.activationState { 50 | case .entering, .failed: 51 | state.activationState = .activating 52 | return environment.purchaseManager.activateLicense( 53 | key: state.licenseKey, 54 | email: state.email 55 | ) 56 | .receive(on: DispatchQueue.main) 57 | .map { result in 58 | switch result { 59 | case .success: return .dismiss 60 | case let .failure(error): 61 | switch error { 62 | case .other: 63 | return .failInActivation(reason: _L10n.FailureReason.networkError) 64 | case .failedToVerifyLicenseKeyLocally: 65 | return .failInActivation(reason: _L10n.FailureReason.invalid) 66 | case .licenseKeyIsInvalid: 67 | return .failInActivation(reason: _L10n.FailureReason.invalid) 68 | case .licenseKeyIsRefunded: 69 | return .failInActivation(reason: _L10n.FailureReason.refunded) 70 | case .licenseKeyHasReachedActivationLimit: 71 | return .failInActivation(reason: _L10n.FailureReason.reachedLimit) 72 | } 73 | } 74 | } 75 | .eraseToEffect() 76 | case .activating: return .none 77 | } 78 | case let .failInActivation(reason): 79 | state.activationState = .failed(reason: reason) 80 | return .none 81 | case .dismiss: // handled by parent 82 | return .none 83 | } 84 | } 85 | } 86 | 87 | extension Store where State == ActivationDomain.State, Action == ActivationDomain.Action { 88 | static func testStore(_ transform: (inout State) -> Void) -> ActivationDomain.Store { 89 | var state = State() 90 | transform(&state) 91 | return .init( 92 | initialState: state, 93 | reducer: ActivationDomain.reducer, 94 | environment: .live(environment: .init( 95 | purchaseManager: FakePurchaseManager() 96 | )) 97 | ) 98 | } 99 | } 100 | 101 | private typealias _L10n = L10n.Activation 102 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/Activation/ActivationView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct ActivationScreen: View { 5 | let store: ActivationDomain.Store 6 | 7 | var body: some View { 8 | ActivationView(store: store) 9 | } 10 | } 11 | 12 | private struct ActivationView: View { 13 | let store: ActivationDomain.Store 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 0) { 17 | Text(_L10n.instruction) 18 | .asWidgetTitle() 19 | 20 | Spacer().frame(height: 8) 21 | 22 | WithViewStore(store.scope(state: \.licenseKey)) { viewStore in 23 | self.inputBox( 24 | text: viewStore.binding( 25 | get: { $0 }, 26 | send: { .updateLicenseKey($0) } 27 | ), 28 | title: { Text(_L10n.licenseKeyTitle) } 29 | ) 30 | } 31 | 32 | Spacer().frame(height: 8) 33 | 34 | WithViewStore(store.scope(state: \.email)) { viewStore in 35 | self.inputBox( 36 | text: viewStore.binding( 37 | get: { $0 }, 38 | send: { .updateEmail($0) } 39 | ), 40 | title: { Text(_L10n.emailTitle) } 41 | ) 42 | } 43 | 44 | warningMessage.frame(height: 40) 45 | buttons 46 | } 47 | .padding(16) 48 | .frame(width: 500) 49 | } 50 | 51 | func inputBox( 52 | text: Binding<String>, 53 | title: () -> Title 54 | ) -> some View where Title: View { 55 | VStack(alignment: .leading, spacing: 4) { 56 | title() 57 | TextField("", text: text) 58 | } 59 | } 60 | 61 | var warningMessage: some View { 62 | Spacer() 63 | .frame(maxWidth: .infinity) 64 | .overlay( 65 | WithViewStore(store.scope(state: \.activationState)) { viewStore in 66 | Text({ 67 | if case let .failed(reason) = viewStore.state { return reason } 68 | return "" 69 | }() as String) 70 | .foregroundColor(Color.red) 71 | .multilineTextAlignment(.trailing) 72 | .padding(.bottom, 4) 73 | }, 74 | alignment: .bottomTrailing 75 | ) 76 | } 77 | 78 | var buttons: some View { 79 | WithViewStore(store) { viewStore in 80 | HStack { 81 | Button(action: { viewStore.send(.dismiss) }) { 82 | Text(_L10n.Button.cancel) 83 | } 84 | Spacer() 85 | HStack { 86 | Button(action: { viewStore.send(.buyNow) }) { 87 | Text(_L10n.Button.buyNow) 88 | .frame(minWidth: 70) 89 | } 90 | 91 | Button(action: { viewStore.send(.activate) }) { 92 | Text({ 93 | if case .activating = viewStore.state.activationState { 94 | return _L10n.Button.activating 95 | } 96 | return _L10n.Button.activate 97 | }() as String) 98 | .frame(minWidth: 70) 99 | } 100 | .disabled(!viewStore.state.isActivateButtonEnabled) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | struct ActivationView_Previews: PreviewProvider { 108 | static var previews: some View { 109 | ActivationView(store: .testStore { state in 110 | state.activationState = .failed(reason: "Failed for some reasons.") 111 | }) 112 | } 113 | } 114 | 115 | private typealias _L10n = L10n.Activation 116 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/KeyboardEventChecker.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import CGEventOverride 3 | import SwiftUI 4 | 5 | struct KeyEventHandling: NSViewRepresentable { 6 | @Binding var isEditing: Bool 7 | let onKeyReceive: (Set<KeyDown>) -> Void 8 | 9 | class KeyView: NSView { 10 | var onKeyReceive: (Set<KeyDown>) -> Void = { _ in } 11 | var isEditing: Binding<Bool> = .init(get: { false }, set: { _ in }) 12 | 13 | override var acceptsFirstResponder: Bool { true } 14 | 15 | override func resignFirstResponder() -> Bool { 16 | isEditing.wrappedValue = false 17 | return super.resignFirstResponder() 18 | } 19 | 20 | override func keyDown(with event: NSEvent) { 21 | super.keyDown(with: event) 22 | handleEvent(event) 23 | } 24 | 25 | override func mouseDown(with event: NSEvent) { 26 | super.mouseDown(with: event) 27 | if !isEditing.wrappedValue { 28 | isEditing.wrappedValue = true 29 | } else { 30 | handleEvent(event) 31 | } 32 | } 33 | 34 | override func rightMouseDown(with event: NSEvent) { 35 | handleEvent(event) 36 | } 37 | 38 | override func otherMouseDown(with event: NSEvent) { 39 | super.otherMouseDown(with: event) 40 | handleEvent(event) 41 | } 42 | 43 | private func handleEvent(_ event: NSEvent) { 44 | guard isEditing.wrappedValue else { return } 45 | 46 | var modifierCodes = [KeyboardCode]() 47 | if event.modifierFlags.contains(.command) { modifierCodes.append(.command) } 48 | if event.modifierFlags.contains(.option) { modifierCodes.append(.option) } 49 | if event.modifierFlags.contains(.control) { modifierCodes.append(.control) } 50 | if event.modifierFlags.contains(.shift) { modifierCodes.append(.shift) } 51 | let modifierKeyDowns = modifierCodes.map { KeyDown.key($0.rawValue) } 52 | 53 | if event.type == .keyDown, let keyCode = KeyboardCode(rawValue: Int(event.keyCode)) { 54 | if keyCode == .delete || keyCode == .forwardDelete { 55 | onKeyReceive([]) 56 | return 57 | } 58 | 59 | if keyCode == .escape { 60 | isEditing.wrappedValue = false 61 | return 62 | } 63 | 64 | let combination = Set([KeyDown.key(keyCode.rawValue)] + modifierKeyDowns) 65 | onKeyReceive(combination) 66 | } else if event.type == .otherMouseDown, 67 | let mouseCode = MouseCode(rawValue: Int(event.buttonNumber)) 68 | { 69 | let combination = Set([KeyDown.mouse(mouseCode.rawValue)] + modifierKeyDowns) 70 | onKeyReceive(combination) 71 | } else if event.type == .leftMouseDown || event.type == .rightMouseDown, 72 | let mouseCode = MouseCode(rawValue: Int(event.buttonNumber)) 73 | { 74 | let combination = Set([KeyDown.mouse(mouseCode.rawValue)] + modifierKeyDowns) 75 | onKeyReceive(combination) 76 | } 77 | } 78 | } 79 | 80 | func makeNSView(context _: Context) -> NSView { 81 | let view = KeyView() 82 | if isEditing { 83 | DispatchQueue.main.async { 84 | view.window?.makeFirstResponder(view) 85 | } 86 | } 87 | return view 88 | } 89 | 90 | func updateNSView(_ nsView: NSView, context _: Context) { 91 | guard let view = nsView as? KeyView else { return } 92 | view.isEditing = _isEditing 93 | view.onKeyReceive = { 94 | self.onKeyReceive($0) 95 | self.isEditing = false 96 | } 97 | if isEditing { 98 | DispatchQueue.main.async { // bring it outside of SwiftUI update context 99 | view.window?.makeFirstResponder(view) 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/KeyDescription/KeyboardCode+Name.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | 3 | extension KeyboardCode { 4 | var name: String { 5 | switch self { 6 | case .returnKey: return "↩︎" 7 | case .enter: return "↩︎" 8 | case .tab: return "⇥" 9 | case .space: return "␣" 10 | case .delete: return "⌫" 11 | case .escape: return "⎋" 12 | case .command: return "⌘" 13 | case .shift: return "⇧" 14 | case .capsLock: return "⇪" 15 | case .option: return "⌥" 16 | case .control: return "⌃" 17 | case .rightShift: return "⇧" 18 | case .rightOption: return "⌥" 19 | case .rightControl: return "⌃" 20 | case .leftArrow: return "←" 21 | case .rightArrow: return "→" 22 | case .downArrow: return "↓" 23 | case .upArrow: return "↑" 24 | case .function: return "Fn" 25 | case .f1: return "F1" 26 | case .f2: return "F2" 27 | case .f3: return "F3" 28 | case .f4: return "F4" 29 | case .f5: return "F5" 30 | case .f6: return "F6" 31 | case .f7: return "F7" 32 | case .f8: return "F8" 33 | case .f9: return "F9" 34 | case .f10: return "F10" 35 | case .f11: return "F11" 36 | case .f12: return "F12" 37 | case .f13: return "F13" 38 | case .f14: return "F14" 39 | case .f15: return "F15" 40 | case .f16: return "F16" 41 | case .f17: return "F17" 42 | case .f18: return "F18" 43 | case .f19: return "F19" 44 | case .f20: return "F20" 45 | case .a: return "A" 46 | case .b: return "B" 47 | case .c: return "C" 48 | case .d: return "D" 49 | case .e: return "E" 50 | case .f: return "F" 51 | case .g: return "G" 52 | case .h: return "H" 53 | case .i: return "I" 54 | case .j: return "J" 55 | case .k: return "K" 56 | case .l: return "L" 57 | case .m: return "M" 58 | case .n: return "N" 59 | case .o: return "O" 60 | case .p: return "P" 61 | case .q: return "Q" 62 | case .r: return "R" 63 | case .s: return "S" 64 | case .t: return "T" 65 | case .u: return "U" 66 | case .v: return "V" 67 | case .w: return "W" 68 | case .x: return "X" 69 | case .y: return "Y" 70 | case .z: return "Z" 71 | case .zero: return "0" 72 | case .one: return "1" 73 | case .two: return "2" 74 | case .three: return "3" 75 | case .four: return "4" 76 | case .five: return "5" 77 | case .six: return "6" 78 | case .seven: return "7" 79 | case .eight: return "8" 80 | case .nine: return "9" 81 | case .equals: return "=" 82 | case .minus: return "-" 83 | case .semicolon: return ";" 84 | case .apostrophe: return "'" 85 | case .comma: return "," 86 | case .period: return "." 87 | case .forwardSlash: return "/" 88 | case .backslash: return "\\" 89 | case .grave: return "`" 90 | case .leftBracket: return "[" 91 | case .rightBracket: return "]" 92 | case .volumeUp: return "Volumn+" 93 | case .volumeDown: return "Volumn-" 94 | case .mute: return "Mute" 95 | case .help: return "Help" 96 | case .home: return "Home" 97 | case .pageUp: return "Page↑" 98 | case .forwardDelete: return "⌫" 99 | case .end: return "End" 100 | case .pageDown: return "Page↓" 101 | case .keypadDecimal: return "." 102 | case .keypadMultiply: return "*" 103 | case .keypadPlus: return "+" 104 | case .keypadClear: return "Clear" 105 | case .keypadDivide: return "÷" 106 | case .keypadMinus: return "-" 107 | case .keypadEquals: return "=" 108 | case .keypad0: return "0" 109 | case .keypad1: return "1" 110 | case .keypad2: return "2" 111 | case .keypad3: return "3" 112 | case .keypad4: return "4" 113 | case .keypad5: return "5" 114 | case .keypad6: return "6" 115 | case .keypad7: return "7" 116 | case .keypad8: return "8" 117 | case .keypad9: return "9" 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_general.imageset/General.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /ExtGState << /E1 << /ca 0.200000 >> >> >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 14.000000 14.000000 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 4.000000 2.000000 m 16 | 4.000000 0.895431 3.104569 0.000000 2.000000 0.000000 c 17 | 0.895431 0.000000 0.000000 0.895431 0.000000 2.000000 c 18 | 0.000000 3.104569 0.895431 4.000000 2.000000 4.000000 c 19 | 3.104569 4.000000 4.000000 3.104569 4.000000 2.000000 c 20 | h 21 | f 22 | n 23 | Q 24 | q 25 | 1.000000 0.000000 -0.000000 1.000000 20.000000 14.000000 cm 26 | 0.000000 0.000000 0.000000 scn 27 | 4.000000 2.000000 m 28 | 4.000000 0.895431 3.104569 0.000000 2.000000 0.000000 c 29 | 0.895431 0.000000 0.000000 0.895431 0.000000 2.000000 c 30 | 0.000000 3.104569 0.895431 4.000000 2.000000 4.000000 c 31 | 3.104569 4.000000 4.000000 3.104569 4.000000 2.000000 c 32 | h 33 | f 34 | n 35 | Q 36 | q 37 | 1.000000 0.000000 -0.000000 1.000000 8.000000 14.000000 cm 38 | 0.000000 0.000000 0.000000 scn 39 | 4.000000 2.000000 m 40 | 4.000000 0.895431 3.104569 0.000000 2.000000 0.000000 c 41 | 0.895431 0.000000 0.000000 0.895431 0.000000 2.000000 c 42 | 0.000000 3.104569 0.895431 4.000000 2.000000 4.000000 c 43 | 3.104569 4.000000 4.000000 3.104569 4.000000 2.000000 c 44 | h 45 | f 46 | n 47 | Q 48 | q 49 | q 50 | /E1 gs 51 | 1.000000 0.000000 -0.000000 1.000000 4.000000 7.000000 cm 52 | 0.000000 0.000000 0.000000 scn 53 | 0.000000 9.000000 m 54 | 0.000000 13.970562 4.029438 18.000000 9.000000 18.000000 c 55 | 15.000000 18.000000 l 56 | 19.970562 18.000000 24.000000 13.970562 24.000000 9.000000 c 57 | 24.000000 9.000000 l 58 | 24.000000 4.029437 19.970562 0.000000 15.000000 0.000000 c 59 | 9.000000 0.000000 l 60 | 4.029438 0.000000 0.000000 4.029437 0.000000 9.000000 c 61 | 0.000000 9.000000 l 62 | h 63 | f 64 | n 65 | Q 66 | 4.000000 16.000000 m 67 | 4.000000 20.970562 8.029438 25.000000 13.000000 25.000000 c 68 | 19.000000 25.000000 l 69 | 23.970562 25.000000 28.000000 20.970562 28.000000 16.000000 c 70 | 28.000000 16.000000 l 71 | 28.000000 11.029438 23.970562 7.000000 19.000000 7.000000 c 72 | 13.000000 7.000000 l 73 | 8.029438 7.000000 4.000000 11.029438 4.000000 16.000000 c 74 | 4.000000 16.000000 l 75 | h 76 | W* 77 | n 78 | q 79 | 1.000000 0.000000 -0.000000 1.000000 4.000000 7.000000 cm 80 | 0.000000 0.000000 0.000000 scn 81 | 9.000000 17.000000 m 82 | 15.000000 17.000000 l 83 | 15.000000 19.000000 l 84 | 9.000000 19.000000 l 85 | 9.000000 17.000000 l 86 | h 87 | 15.000000 1.000000 m 88 | 9.000000 1.000000 l 89 | 9.000000 -1.000000 l 90 | 15.000000 -1.000000 l 91 | 15.000000 1.000000 l 92 | h 93 | 9.000000 1.000000 m 94 | 4.581722 1.000000 1.000000 4.581722 1.000000 9.000000 c 95 | -1.000000 9.000000 l 96 | -1.000000 3.477153 3.477153 -1.000000 9.000000 -1.000000 c 97 | 9.000000 1.000000 l 98 | h 99 | 23.000000 9.000000 m 100 | 23.000000 4.581722 19.418278 1.000000 15.000000 1.000000 c 101 | 15.000000 -1.000000 l 102 | 20.522848 -1.000000 25.000000 3.477153 25.000000 9.000000 c 103 | 23.000000 9.000000 l 104 | h 105 | 15.000000 17.000000 m 106 | 19.418278 17.000000 23.000000 13.418278 23.000000 9.000000 c 107 | 25.000000 9.000000 l 108 | 25.000000 14.522847 20.522848 19.000000 15.000000 19.000000 c 109 | 15.000000 17.000000 l 110 | h 111 | 9.000000 19.000000 m 112 | 3.477153 19.000000 -1.000000 14.522847 -1.000000 9.000000 c 113 | 1.000000 9.000000 l 114 | 1.000000 13.418278 4.581722 17.000000 9.000000 17.000000 c 115 | 9.000000 19.000000 l 116 | h 117 | f 118 | n 119 | Q 120 | Q 121 | 122 | endstream 123 | endobj 124 | 125 | 3 0 obj 126 | 2913 127 | endobj 128 | 129 | 4 0 obj 130 | << /Annots [] 131 | /Type /Page 132 | /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] 133 | /Resources 1 0 R 134 | /Contents 2 0 R 135 | /Parent 5 0 R 136 | >> 137 | endobj 138 | 139 | 5 0 obj 140 | << /Kids [ 4 0 R ] 141 | /Count 1 142 | /Type /Pages 143 | >> 144 | endobj 145 | 146 | 6 0 obj 147 | << /Type /Catalog 148 | /Pages 5 0 R 149 | >> 150 | endobj 151 | 152 | xref 153 | 0 7 154 | 0000000000 65535 f 155 | 0000000010 00000 n 156 | 0000000074 00000 n 157 | 0000003043 00000 n 158 | 0000003066 00000 n 159 | 0000003239 00000 n 160 | 0000003313 00000 n 161 | trailer 162 | << /ID [ (some) (id) ] 163 | /Root 6 0 R 164 | /Size 7 165 | >> 166 | startxref 167 | 3372 168 | %%EOF -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/DockSwipe/DockSwipeDomain.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | 6 | enum DockSwipeDomain: Domain { 7 | struct State: Equatable { 8 | struct DockSwipeActivator: Equatable { 9 | var keyCombination: KeyCombination? 10 | var hasConflict = false 11 | var numberOfTapsRequired = 1 12 | var invalidReason: KeyCombinationInvalidReason? 13 | } 14 | 15 | var dockSwipeActivator = DockSwipeActivator() 16 | } 17 | 18 | enum Action: Equatable { 19 | case appear 20 | 21 | case dockSwipe(DockSwipe) 22 | enum DockSwipe: Equatable { 23 | case setKeyCombination(KeyCombination?) 24 | case clearKeyCombination 25 | case setNumberOfTapsRequired(Int) 26 | } 27 | 28 | case _internal(Internal) 29 | enum Internal: Equatable { 30 | case checkConflict 31 | case checkValidity 32 | } 33 | } 34 | 35 | typealias Environment = SystemEnvironment<_Environment> 36 | struct _Environment { 37 | var persisted: Persisted.DockSwipe 38 | var featureHasConflict: (ActivatorConflictChecker.Feature) -> Bool 39 | var checkKeyCombinationValidity: (KeyCombination?) -> KeyCombinationInvalidReason? 40 | } 41 | 42 | static let reducer = Reducer.combine( 43 | Reducer { state, action, environment in 44 | switch action { 45 | case let .dockSwipe(action): 46 | switch action { 47 | case let .setKeyCombination(combination): 48 | state.dockSwipeActivator.keyCombination = combination 49 | let (keys, mouse) = combination?.raw ?? ([], nil) 50 | return .fireAndForget { 51 | environment.persisted.keyCombination = combination 52 | } 53 | case let .setNumberOfTapsRequired(count): 54 | let clamped = min(max(1, count), 3) 55 | state.dockSwipeActivator.numberOfTapsRequired = clamped 56 | return .fireAndForget { 57 | environment.persisted.numberOfTapsRequired = clamped 58 | } 59 | case .clearKeyCombination: 60 | state.dockSwipeActivator.keyCombination = nil 61 | return .fireAndForget { 62 | environment.persisted.keyCombination = nil 63 | } 64 | } 65 | default: return .none 66 | } 67 | }, 68 | Reducer { state, action, environment in 69 | switch action { 70 | case .appear: 71 | state = State(from: environment.persisted) 72 | return .merge([ 73 | .init(value: ._internal(.checkConflict)), 74 | .init(value: ._internal(.checkValidity)), 75 | ]) 76 | case .dockSwipe: 77 | return .merge([ 78 | .init(value: ._internal(.checkConflict)), 79 | .init(value: ._internal(.checkValidity)), 80 | ]) 81 | case let ._internal(internalAction): 82 | switch internalAction { 83 | case .checkConflict: 84 | state.dockSwipeActivator.hasConflict = environment 85 | .featureHasConflict(.dockSwipe) 86 | return .none 87 | case .checkValidity: 88 | let check = environment.checkKeyCombinationValidity 89 | state.dockSwipeActivator.invalidReason = check( 90 | state.dockSwipeActivator.keyCombination 91 | ) 92 | return .none 93 | } 94 | } 95 | } 96 | ) 97 | } 98 | 99 | extension DockSwipeDomain.State { 100 | init(from persisted: Persisted.DockSwipe) { 101 | self.init( 102 | dockSwipeActivator: .init( 103 | keyCombination: persisted.keyCombination, 104 | numberOfTapsRequired: persisted.numberOfTapsRequired 105 | ) 106 | ) 107 | } 108 | } 109 | 110 | extension Store where Action == DockSwipeDomain.Action, State == DockSwipeDomain.State { 111 | static var testStore: Self { 112 | .init( 113 | initialState: .init(), 114 | reducer: DockSwipeDomain.reducer, 115 | environment: .live(environment: .init( 116 | persisted: .init(), 117 | featureHasConflict: { _ in true }, 118 | checkKeyCombinationValidity: { _ in nil } 119 | )) 120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/Persistency/KeychainValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | class KeychainStored<Value: KeychainStorable> { 5 | let key: String 6 | let defaultValue: Value 7 | var keychain: KeychainAccess 8 | 9 | init(_ key: String, defaultValue: Value, keychain: KeychainAccess = FakeKeychainAccess()) { 10 | self.key = key 11 | self.defaultValue = defaultValue 12 | self.keychain = keychain 13 | } 14 | 15 | var wrappedValue: Value { 16 | get { 17 | if Value.KV.self == String.self || Value.KV.self == String?.self { 18 | guard let rawValue = keychain.string(for: key) as? Value.KV 19 | else { return defaultValue } 20 | return Value.makeFromKeychainValue(value: rawValue) 21 | } 22 | guard let rawValue = keychain.data(for: key) as? Value.KV else { return defaultValue } 23 | return Value.makeFromKeychainValue(value: rawValue) 24 | } 25 | set { 26 | let storableValue = newValue.keychianValue 27 | if storableValue.isKeychainRemoveValue { 28 | keychain.remove(key: key) 29 | } else if let string = storableValue as? String { 30 | keychain.set(string, for: key) 31 | } else if let data = storableValue as? Data { 32 | keychain.set(data, for: key) 33 | } 34 | } 35 | } 36 | } 37 | 38 | protocol KeychainAccess { 39 | func remove(key: String) 40 | func set(_ string: String, for key: String) 41 | func string(for key: String) -> String? 42 | func set(_ data: Data, for key: String) 43 | func data(for key: String) -> Data? 44 | } 45 | 46 | final class FakeKeychainAccess: KeychainAccess { 47 | enum Storable { 48 | case string(String) 49 | case data(Data) 50 | } 51 | 52 | var contents = [String: Storable]() 53 | func remove(key: String) { contents[key] = nil } 54 | func set(_ string: String, for key: String) { contents[key] = .string(string) } 55 | func set(_ data: Data, for key: String) { contents[key] = .data(data) } 56 | 57 | func string(for key: String) -> String? { 58 | if case let .string(s) = contents[key] { return s } 59 | return nil 60 | } 61 | 62 | func data(for key: String) -> Data? { 63 | if case let .data(d) = contents[key] { return d } 64 | return nil 65 | } 66 | } 67 | 68 | // MARK: - Storables 69 | 70 | protocol KeychainValue { 71 | var isKeychainRemoveValue: Bool { get } 72 | } 73 | 74 | protocol KeychainStorable { 75 | associatedtype KV: KeychainValue 76 | /// The actual value to be stored in UserDefaults. 77 | var keychianValue: KV { get } 78 | /// Convert the stored data back into the type. 79 | static func makeFromKeychainValue(value: KV) -> Self 80 | } 81 | 82 | extension KeychainValue { 83 | var isKeychainRemoveValue: Bool { false } 84 | } 85 | 86 | extension KeychainStorable { 87 | var keychianValue: Self { self } 88 | static func makeFromKeychainValue(value: Self) -> Self { value } 89 | } 90 | 91 | typealias TrivialKeychainStorable = KeychainValue & KeychainStorable 92 | 93 | extension String: TrivialKeychainStorable {} 94 | extension Data: TrivialKeychainStorable {} 95 | 96 | extension Int: KeychainStorable { 97 | var keychianValue: String { String(self) } 98 | static func makeFromKeychainValue(value: String) -> Int { Int(value) ?? 0 } 99 | } 100 | 101 | extension TimeInterval: KeychainStorable { 102 | var keychianValue: String { String(self) } 103 | static func makeFromKeychainValue(value: String) -> TimeInterval { TimeInterval(value) ?? 0 } 104 | } 105 | 106 | // MARK: - Optional 107 | 108 | extension Optional: KeychainValue where Wrapped: KeychainValue { 109 | var isKeychainRemoveValue: Bool { self == nil } 110 | } 111 | 112 | extension Optional: KeychainStorable where Wrapped: KeychainStorable { 113 | var isKeychainRemoveValue: Bool { self == nil } 114 | var keychianValue: Wrapped.KV? { map(\.keychianValue) } 115 | static func makeFromKeychainValue(value: Wrapped.KV?) -> Self { 116 | value.map(Wrapped.makeFromKeychainValue) 117 | } 118 | } 119 | 120 | // MARK: - Set 121 | 122 | extension Set: KeychainValue, KeychainStorable where Element: Codable { 123 | var isKeychainRemoveValue: Bool { isEmpty } 124 | var keychianValue: Data { 125 | (try? JSONEncoder().encode(self)) ?? Data() 126 | } 127 | 128 | static func makeFromKeychainValue(value: Data) -> Self { 129 | (try? JSONDecoder().decode(Self.self, from: value)) ?? .init() 130 | } 131 | } 132 | 133 | // MARK: - Date 134 | 135 | extension Date: KeychainStorable { 136 | var keychianValue: String { timeIntervalSince1970.keychianValue } 137 | static func makeFromKeychainValue(value: String) -> Self { 138 | Date(timeIntervalSince1970: TimeInterval.makeFromKeychainValue(value: value)) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/GestureRecognizer/DoubleTapGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import Combine 3 | import Foundation 4 | 5 | final class TapGestureRecognizer { 6 | private struct Key: Hashable {} 7 | 8 | let publisher: AnyPublisher<Void, Never> 9 | var keyCombination: KeyCombination? { didSet { state = State() } } 10 | var numberOfTapsRequired = 1 { didSet { state = State() } } 11 | 12 | struct State { 13 | /// Last timestamp when key combination is triggered with event keyUp or mouseUp. 14 | /// Double tap gesture should use it to determine if it should trigger. 15 | var lastButtonUpTimestamp = 0 as TimeInterval 16 | var tapCount = 0 17 | var isDown = false 18 | var holdingDownKeys = Set<Int64>() 19 | var holdingDownMouseButtons = Set<Int64>() 20 | } 21 | 22 | private let hook: CGEventHookType 23 | private let subject: PassthroughSubject<Void, Never> 24 | private(set) var state = State() 25 | private let key: AnyHashable 26 | 27 | deinit { 28 | hook.removeManipulation(forKey: key) 29 | } 30 | 31 | init(hook: CGEventHookType, key: AnyHashable) { 32 | self.key = key 33 | self.hook = hook 34 | subject = .init() 35 | publisher = subject 36 | .receive(on: DispatchQueue.main) 37 | .share(replay: 1) 38 | .eraseToAnyPublisher() 39 | 40 | hook.add( 41 | .init( 42 | eventsOfInterest: [.keyUp, .keyDown, .otherMouseUp, .otherMouseDown], 43 | convert: { [weak self] _, type, event -> CGEventManipulation.Result in 44 | guard let self = self else { return .unchange } 45 | switch type { 46 | case .keyUp, .keyDown: 47 | return self.handleKeys(type: type, event: event) 48 | case .otherMouseUp, .otherMouseDown: 49 | return self.handleMouseButton(type: type, event: event) 50 | default: 51 | return .unchange 52 | } 53 | } 54 | ), 55 | forKey: key 56 | ) 57 | } 58 | } 59 | 60 | extension TapGestureRecognizer { 61 | private func handleKeys( 62 | type: CGEventType, 63 | event: CGEvent 64 | ) -> CGEventManipulation.Result { 65 | guard let combination = keyCombination else { return .unchange } 66 | let code = event[.keyboardEventKeycode] 67 | let activator = combination.activator 68 | guard case let .key(target) = activator, 69 | code == target, 70 | combination.matchesFlags(event.flags) 71 | else { return .unchange } 72 | 73 | fix() 74 | 75 | switch type { 76 | case .keyDown: 77 | guard !state.isDown else { return .discarded } 78 | down(code: code) 79 | state.isDown = true 80 | return .discarded 81 | case .keyUp: 82 | up(code: code) 83 | state.isDown = false 84 | return .discarded 85 | default: return .unchange 86 | } 87 | } 88 | 89 | private func handleMouseButton( 90 | type: CGEventType, 91 | event: CGEvent 92 | ) -> CGEventManipulation.Result { 93 | guard let combination = keyCombination else { return .unchange } 94 | let code = event[.mouseEventButtonNumber] 95 | let activator = combination.activator 96 | guard case let .mouse(target) = activator, target == code else { return .unchange } 97 | 98 | fix() 99 | 100 | switch type { 101 | case .otherMouseDown: 102 | down(code: code) 103 | return .discarded 104 | case .otherMouseUp: 105 | up(code: code) 106 | return .discarded 107 | default: return .unchange 108 | } 109 | } 110 | 111 | private var duration: TimeInterval { Double(numberOfTapsRequired) * 0.3 } 112 | 113 | private func fix() { 114 | if Date().timeIntervalSince1970 - state.lastButtonUpTimestamp >= duration { 115 | state.tapCount = 0 116 | } 117 | } 118 | 119 | private func down(code: Int64) { 120 | state.holdingDownMouseButtons.insert(code) 121 | if state.tapCount == 0 { 122 | state.lastButtonUpTimestamp = Date().timeIntervalSince1970 123 | } 124 | } 125 | 126 | private func up(code: Int64) { 127 | state.holdingDownMouseButtons.remove(code) 128 | if Date().timeIntervalSince1970 - state.lastButtonUpTimestamp < duration { 129 | if state.tapCount == numberOfTapsRequired { 130 | subject.send(()) 131 | state.lastButtonUpTimestamp = 0 132 | state.tapCount = 0 133 | } else if state.tapCount > numberOfTapsRequired { 134 | state.lastButtonUpTimestamp = 0 135 | state.tapCount = 0 136 | } else { 137 | state.tapCount += 1 138 | } 139 | } else { 140 | state.lastButtonUpTimestamp = 0 141 | state.tapCount = 0 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouseTests/ActivatorConflictCheckerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import AbnormalMouse 4 | 5 | class ActivatorConflictCheckerTests: XCTestCase { 6 | func testNilKeyCombinationNoConflict() throws { 7 | let persisted = Persisted( 8 | userDefaults: MemoryPropertyListStorage(), 9 | keychainAccess: FakeKeychainAccess() 10 | ) 11 | 12 | let checker = ActivatorConflictChecker(persisted: Readonly(persisted)) 13 | 14 | for f in ActivatorConflictChecker.Feature.allCases { 15 | XCTAssertFalse(checker.featureHasConflict(f)) 16 | } 17 | } 18 | 19 | func testDifferenctKeyCombinationNoConflict() throws { 20 | let persisted = Persisted( 21 | userDefaults: MemoryPropertyListStorage(), 22 | keychainAccess: FakeKeychainAccess() 23 | ) 24 | 25 | let checker = ActivatorConflictChecker(persisted: Readonly(persisted)) 26 | 27 | persisted.moveToScroll.keyCombination = .init(modifiers: [.key(2)], activator: .key(2)) 28 | persisted.moveToScroll.numberOfTapsRequired = 1 29 | persisted.zoomAndRotate.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 30 | persisted.zoomAndRotate.numberOfTapsRequired = 1 31 | 32 | XCTAssertFalse(checker.featureHasConflict(.moveToScroll)) 33 | XCTAssertFalse(checker.featureHasConflict(.zoomAndRotate)) 34 | 35 | persisted.moveToScroll.keyCombination = .init(modifiers: [.key(1)], activator: .key(1)) 36 | persisted.moveToScroll.numberOfTapsRequired = 1 37 | persisted.zoomAndRotate.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 38 | persisted.zoomAndRotate.numberOfTapsRequired = 1 39 | 40 | XCTAssertFalse(checker.featureHasConflict(.moveToScroll)) 41 | XCTAssertFalse(checker.featureHasConflict(.zoomAndRotate)) 42 | } 43 | 44 | func testSameCombinationSameNumberOfTapsConflict() throws { 45 | let persisted = Persisted( 46 | userDefaults: MemoryPropertyListStorage(), 47 | keychainAccess: FakeKeychainAccess() 48 | ) 49 | 50 | let checker = ActivatorConflictChecker(persisted: Readonly(persisted)) 51 | 52 | persisted.moveToScroll.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 53 | persisted.moveToScroll.numberOfTapsRequired = 1 54 | persisted.zoomAndRotate.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 55 | persisted.zoomAndRotate.numberOfTapsRequired = 2 56 | persisted.dockSwipe.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 57 | persisted.dockSwipe.numberOfTapsRequired = 3 58 | persisted.moveToScroll.halfPageScroll.useMoveToScrollDoubleTap = false 59 | persisted.zoomAndRotate.smartZoom.useZoomAndRotateDoubleTap = false 60 | 61 | XCTAssertFalse(checker.featureHasConflict(.moveToScroll)) 62 | XCTAssertFalse(checker.featureHasConflict(.zoomAndRotate)) 63 | XCTAssertFalse(checker.featureHasConflict(.dockSwipe)) 64 | 65 | persisted.moveToScroll.numberOfTapsRequired = 2 66 | 67 | XCTAssertTrue(checker.featureHasConflict(.moveToScroll)) 68 | XCTAssertTrue(checker.featureHasConflict(.zoomAndRotate)) 69 | XCTAssertFalse(checker.featureHasConflict(.dockSwipe)) 70 | 71 | persisted.moveToScroll.numberOfTapsRequired = 3 72 | 73 | XCTAssertTrue(checker.featureHasConflict(.moveToScroll)) 74 | XCTAssertFalse(checker.featureHasConflict(.zoomAndRotate)) 75 | XCTAssertTrue(checker.featureHasConflict(.dockSwipe)) 76 | } 77 | 78 | func testDoubleTapGesturesConflictToHoldGestures() throws { 79 | let persisted = Persisted( 80 | userDefaults: MemoryPropertyListStorage(), 81 | keychainAccess: FakeKeychainAccess() 82 | ) 83 | 84 | let checker = ActivatorConflictChecker(persisted: Readonly(persisted)) 85 | 86 | persisted.moveToScroll.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 87 | persisted.moveToScroll.numberOfTapsRequired = 1 88 | persisted.zoomAndRotate.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 89 | persisted.zoomAndRotate.numberOfTapsRequired = 2 90 | persisted.dockSwipe.keyCombination = .init(modifiers: [.key(1)], activator: .key(2)) 91 | persisted.dockSwipe.numberOfTapsRequired = 3 92 | persisted.moveToScroll.halfPageScroll.useMoveToScrollDoubleTap = true 93 | persisted.zoomAndRotate.smartZoom.useZoomAndRotateDoubleTap = true 94 | 95 | XCTAssertTrue(checker.featureHasConflict(.halfPageScroll)) 96 | XCTAssertTrue(checker.featureHasConflict(.zoomAndRotate)) 97 | XCTAssertTrue(checker.featureHasConflict(.smartZoom)) 98 | XCTAssertTrue(checker.featureHasConflict(.dockSwipe)) 99 | 100 | persisted.zoomAndRotate.keyCombination = .init(modifiers: [.key(2)], activator: .key(2)) 101 | 102 | XCTAssertFalse(checker.featureHasConflict(.halfPageScroll)) 103 | XCTAssertFalse(checker.featureHasConflict(.zoomAndRotate)) 104 | XCTAssertFalse(checker.featureHasConflict(.smartZoom)) 105 | XCTAssertFalse(checker.featureHasConflict(.dockSwipe)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Style.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | // MARK: - Text 5 | 6 | public extension Font { 7 | static let pageTitle: Font = .system(size: 22, weight: .bold, design: .default) 8 | static let introduction: Font = .system(size: 12) 9 | static let widgetTitle: Font = .system(size: 14, weight: .semibold, design: .default) 10 | } 11 | 12 | extension View { 13 | func asFeatureIntroduction() -> some View { 14 | modifier(FeatureIntroductionTextModifier()) 15 | } 16 | 17 | func asFeatureTitle() -> some View { 18 | modifier(FeatureTitleTextModifier()) 19 | } 20 | 21 | func asWidgetTitle() -> some View { 22 | modifier(WidgetTitleTextModifier()) 23 | } 24 | } 25 | 26 | struct FeatureIntroductionTextModifier: ViewModifier { 27 | func body(content: Content) -> some View { 28 | content 29 | .fixedSize(horizontal: false, vertical: true) 30 | .font(.introduction) 31 | .foregroundColor(.gray) 32 | } 33 | } 34 | 35 | struct FeatureTitleTextModifier: ViewModifier { 36 | func body(content: Content) -> some View { 37 | content 38 | .font(.pageTitle) 39 | } 40 | } 41 | 42 | struct WidgetTitleTextModifier: ViewModifier { 43 | func body(content: Content) -> some View { 44 | content 45 | .font(.widgetTitle) 46 | } 47 | } 48 | 49 | struct Style_Title_Previews: PreviewProvider { 50 | static var previews: some View { 51 | VStack(alignment: .leading, spacing: 2) { 52 | Text("Feature Title").asFeatureTitle() 53 | Text("Feature Introduction").asFeatureIntroduction() 54 | Text("Widget Title").asWidgetTitle() 55 | } 56 | .frame(width: 200, alignment: .leading) 57 | .padding(4) 58 | } 59 | } 60 | 61 | // MARK: - Background 62 | 63 | struct ShadowModifier: ViewModifier { 64 | var color = Color(NSColor.shadowColor).opacity(0.2) 65 | var radius: CGFloat = 4 66 | var x: CGFloat = 0 67 | var y: CGFloat = 0 68 | 69 | func body(content: Content) -> some View { 70 | content 71 | .shadow(color: color, radius: radius, x: x, y: y) 72 | } 73 | } 74 | 75 | extension View { 76 | func roundedCornerBackground( 77 | cornerRadius: CGFloat, 78 | fillColor: Color = .white, 79 | strokeColor: Color = .clear, 80 | strokeWidth: CGFloat = 0, 81 | shadow: ShadowModifier? = nil 82 | ) -> some View { 83 | let content = GeometryReader { proxy in 84 | ZStack { 85 | RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) 86 | .fill(fillColor) 87 | RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) 88 | .stroke(strokeColor, lineWidth: strokeWidth) 89 | }.frame(width: proxy.size.width, height: proxy.size.height) 90 | } 91 | if let shadowModifier = shadow { 92 | return background(AnyView(content.modifier(shadowModifier))) 93 | } 94 | return background(AnyView(content)) 95 | } 96 | } 97 | 98 | struct Style_RoundedCornerBackground_Previews: PreviewProvider { 99 | static var previews: some View { 100 | /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ 101 | .padding(4) 102 | .roundedCornerBackground( 103 | cornerRadius: 4, 104 | fillColor: .gray, 105 | strokeColor: .black, 106 | strokeWidth: 2, 107 | shadow: ShadowModifier(color: .black, radius: 4, x: 0, y: 1) 108 | ) 109 | .padding(10) 110 | } 111 | } 112 | 113 | extension View { 114 | func circleBackground( 115 | fillColor: Color = .white, 116 | strokeColor: Color = .clear, 117 | strokeWidth: CGFloat = 0, 118 | shadow: ShadowModifier? = nil 119 | ) -> some View { 120 | let content = GeometryReader { proxy in 121 | ZStack { 122 | RoundedRectangle( 123 | cornerRadius: min(proxy.size.width, proxy.size.height) / 2, 124 | style: .continuous 125 | ) 126 | .fill(fillColor) 127 | RoundedRectangle( 128 | cornerRadius: min(proxy.size.width, proxy.size.height) / 2, 129 | style: .continuous 130 | ) 131 | .stroke(strokeColor, lineWidth: strokeWidth) 132 | }.frame(width: proxy.size.width, height: proxy.size.height) 133 | } 134 | if let shadowModifier = shadow { 135 | return background(AnyView(content.modifier(shadowModifier))) 136 | } 137 | return background(AnyView(content)) 138 | } 139 | } 140 | 141 | struct Style_CircleBackground_Previews: PreviewProvider { 142 | static var previews: some View { 143 | /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ 144 | .padding(4) 145 | .circleBackground( 146 | fillColor: .gray, 147 | strokeColor: .black, 148 | strokeWidth: 2 149 | ) 150 | .padding(10) 151 | } 152 | } 153 | 154 | extension View { 155 | func overlayWhen<Overlay>( 156 | _ shouldOverlay: Bool, 157 | view: Overlay, 158 | alignment: Alignment = .center 159 | ) -> some View where Overlay: View { 160 | if shouldOverlay { 161 | return overlay(AnyView(view), alignment: alignment) 162 | } 163 | return overlay(AnyView(EmptyView()), alignment: alignment) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse.xcodeproj/xcshareddata/xcschemes/AbnormalMouse.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1240" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "YES" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "1383615E2487E70500E76F41" 18 | BuildableName = "AbnormalMouse.app" 19 | BlueprintName = "AbnormalMouse" 20 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | <BuildActionEntry 24 | buildForTesting = "YES" 25 | buildForRunning = "YES" 26 | buildForProfiling = "YES" 27 | buildForArchiving = "YES" 28 | buildForAnalyzing = "YES"> 29 | <BuildableReference 30 | BuildableIdentifier = "primary" 31 | BlueprintIdentifier = "907B44DC8AE318640A710B2B71CDAE09" 32 | BuildableName = "Pods_AbnormalMouse.framework" 33 | BlueprintName = "Pods-AbnormalMouse" 34 | ReferencedContainer = "container:../Pods/Pods.xcodeproj"> 35 | </BuildableReference> 36 | </BuildActionEntry> 37 | <BuildActionEntry 38 | buildForTesting = "YES" 39 | buildForRunning = "YES" 40 | buildForProfiling = "YES" 41 | buildForArchiving = "YES" 42 | buildForAnalyzing = "YES"> 43 | <BuildableReference 44 | BuildableIdentifier = "primary" 45 | BlueprintIdentifier = "License::LicenseWrapper" 46 | BuildableName = "LicenseWrapper.framework" 47 | BlueprintName = "LicenseWrapper" 48 | ReferencedContainer = "container:../LicenseWrapper/License.xcodeproj"> 49 | </BuildableReference> 50 | </BuildActionEntry> 51 | </BuildActionEntries> 52 | </BuildAction> 53 | <TestAction 54 | buildConfiguration = "Debug" 55 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 56 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 57 | shouldUseLaunchSchemeArgsEnv = "NO" 58 | codeCoverageEnabled = "YES" 59 | onlyGenerateCoverageForSpecifiedTargets = "YES"> 60 | <EnvironmentVariables> 61 | <EnvironmentVariable 62 | key = "NEVER_WAIT_AT_LEAST" 63 | value = "YES" 64 | isEnabled = "YES"> 65 | </EnvironmentVariable> 66 | <EnvironmentVariable 67 | key = "IS_UNIT_TEST" 68 | value = "YES" 69 | isEnabled = "YES"> 70 | </EnvironmentVariable> 71 | </EnvironmentVariables> 72 | <CodeCoverageTargets> 73 | <BuildableReference 74 | BuildableIdentifier = "primary" 75 | BlueprintIdentifier = "1383615E2487E70500E76F41" 76 | BuildableName = "AbnormalMouse.app" 77 | BlueprintName = "AbnormalMouse" 78 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 79 | </BuildableReference> 80 | </CodeCoverageTargets> 81 | <Testables> 82 | <TestableReference 83 | skipped = "NO"> 84 | <BuildableReference 85 | BuildableIdentifier = "primary" 86 | BlueprintIdentifier = "138361732487E70700E76F41" 87 | BuildableName = "AbnormalMouseTests.xctest" 88 | BlueprintName = "AbnormalMouseTests" 89 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 90 | </BuildableReference> 91 | </TestableReference> 92 | </Testables> 93 | </TestAction> 94 | <LaunchAction 95 | buildConfiguration = "Debug" 96 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 97 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 98 | launchStyle = "0" 99 | useCustomWorkingDirectory = "NO" 100 | ignoresPersistentStateOnLaunch = "NO" 101 | debugDocumentVersioning = "YES" 102 | debugServiceExtension = "internal" 103 | allowLocationSimulation = "YES"> 104 | <BuildableProductRunnable 105 | runnableDebuggingMode = "0"> 106 | <BuildableReference 107 | BuildableIdentifier = "primary" 108 | BlueprintIdentifier = "1383615E2487E70500E76F41" 109 | BuildableName = "AbnormalMouse.app" 110 | BlueprintName = "AbnormalMouse" 111 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 112 | </BuildableReference> 113 | </BuildableProductRunnable> 114 | </LaunchAction> 115 | <ProfileAction 116 | buildConfiguration = "Debug" 117 | shouldUseLaunchSchemeArgsEnv = "YES" 118 | savedToolIdentifier = "" 119 | useCustomWorkingDirectory = "NO" 120 | debugDocumentVersioning = "YES"> 121 | <BuildableProductRunnable 122 | runnableDebuggingMode = "0"> 123 | <BuildableReference 124 | BuildableIdentifier = "primary" 125 | BlueprintIdentifier = "1383615E2487E70500E76F41" 126 | BuildableName = "AbnormalMouse.app" 127 | BlueprintName = "AbnormalMouse" 128 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 129 | </BuildableReference> 130 | </BuildableProductRunnable> 131 | </ProfileAction> 132 | <AnalyzeAction 133 | buildConfiguration = "Debug"> 134 | </AnalyzeAction> 135 | <ArchiveAction 136 | buildConfiguration = "Release" 137 | revealArchiveInOrganizer = "YES"> 138 | </ArchiveAction> 139 | </Scheme> 140 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse.xcodeproj/xcshareddata/xcschemes/CanvasPreview.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1240" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "YES" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "1383615E2487E70500E76F41" 18 | BuildableName = "AbnormalMouse.app" 19 | BlueprintName = "AbnormalMouse" 20 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | <BuildActionEntry 24 | buildForTesting = "YES" 25 | buildForRunning = "YES" 26 | buildForProfiling = "YES" 27 | buildForArchiving = "YES" 28 | buildForAnalyzing = "YES"> 29 | <BuildableReference 30 | BuildableIdentifier = "primary" 31 | BlueprintIdentifier = "907B44DC8AE318640A710B2B71CDAE09" 32 | BuildableName = "Pods_AbnormalMouse.framework" 33 | BlueprintName = "Pods-AbnormalMouse" 34 | ReferencedContainer = "container:../Pods/Pods.xcodeproj"> 35 | </BuildableReference> 36 | </BuildActionEntry> 37 | <BuildActionEntry 38 | buildForTesting = "YES" 39 | buildForRunning = "YES" 40 | buildForProfiling = "YES" 41 | buildForArchiving = "YES" 42 | buildForAnalyzing = "YES"> 43 | <BuildableReference 44 | BuildableIdentifier = "primary" 45 | BlueprintIdentifier = "License::LicenseWrapper" 46 | BuildableName = "LicenseWrapper.framework" 47 | BlueprintName = "LicenseWrapper" 48 | ReferencedContainer = "container:../LicenseWrapper/License.xcodeproj"> 49 | </BuildableReference> 50 | </BuildActionEntry> 51 | </BuildActionEntries> 52 | </BuildAction> 53 | <TestAction 54 | buildConfiguration = "Debug" 55 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 56 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 57 | shouldUseLaunchSchemeArgsEnv = "NO" 58 | codeCoverageEnabled = "YES" 59 | onlyGenerateCoverageForSpecifiedTargets = "YES"> 60 | <EnvironmentVariables> 61 | <EnvironmentVariable 62 | key = "NEVER_WAIT_AT_LEAST" 63 | value = "YES" 64 | isEnabled = "YES"> 65 | </EnvironmentVariable> 66 | <EnvironmentVariable 67 | key = "IS_UNIT_TEST" 68 | value = "YES" 69 | isEnabled = "YES"> 70 | </EnvironmentVariable> 71 | </EnvironmentVariables> 72 | <CodeCoverageTargets> 73 | <BuildableReference 74 | BuildableIdentifier = "primary" 75 | BlueprintIdentifier = "1383615E2487E70500E76F41" 76 | BuildableName = "AbnormalMouse.app" 77 | BlueprintName = "AbnormalMouse" 78 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 79 | </BuildableReference> 80 | </CodeCoverageTargets> 81 | <Testables> 82 | <TestableReference 83 | skipped = "NO"> 84 | <BuildableReference 85 | BuildableIdentifier = "primary" 86 | BlueprintIdentifier = "138361732487E70700E76F41" 87 | BuildableName = "AbnormalMouseTests.xctest" 88 | BlueprintName = "AbnormalMouseTests" 89 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 90 | </BuildableReference> 91 | </TestableReference> 92 | </Testables> 93 | </TestAction> 94 | <LaunchAction 95 | buildConfiguration = "Preview" 96 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 97 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 98 | launchStyle = "0" 99 | useCustomWorkingDirectory = "NO" 100 | ignoresPersistentStateOnLaunch = "NO" 101 | debugDocumentVersioning = "YES" 102 | debugServiceExtension = "internal" 103 | allowLocationSimulation = "YES"> 104 | <BuildableProductRunnable 105 | runnableDebuggingMode = "0"> 106 | <BuildableReference 107 | BuildableIdentifier = "primary" 108 | BlueprintIdentifier = "1383615E2487E70500E76F41" 109 | BuildableName = "AbnormalMouse.app" 110 | BlueprintName = "AbnormalMouse" 111 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 112 | </BuildableReference> 113 | </BuildableProductRunnable> 114 | </LaunchAction> 115 | <ProfileAction 116 | buildConfiguration = "Debug" 117 | shouldUseLaunchSchemeArgsEnv = "YES" 118 | savedToolIdentifier = "" 119 | useCustomWorkingDirectory = "NO" 120 | debugDocumentVersioning = "YES"> 121 | <BuildableProductRunnable 122 | runnableDebuggingMode = "0"> 123 | <BuildableReference 124 | BuildableIdentifier = "primary" 125 | BlueprintIdentifier = "1383615E2487E70500E76F41" 126 | BuildableName = "AbnormalMouse.app" 127 | BlueprintName = "AbnormalMouse" 128 | ReferencedContainer = "container:AbnormalMouse.xcodeproj"> 129 | </BuildableReference> 130 | </BuildableProductRunnable> 131 | </ProfileAction> 132 | <AnalyzeAction 133 | buildConfiguration = "Debug"> 134 | </AnalyzeAction> 135 | <ArchiveAction 136 | buildConfiguration = "Release" 137 | revealArchiveInOrganizer = "YES"> 138 | </ArchiveAction> 139 | </Scheme> 140 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/Advanced/AdvancedView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct AdvancedScreen: View { 5 | let store: AdvancedDomain.Store 6 | 7 | var body: some View { 8 | AdvancedView(store: store) 9 | .lifeCycleWithViewStore(store, onAppear: { viewStore in 10 | viewStore.send(.appear) 11 | }) 12 | } 13 | } 14 | 15 | private struct AdvancedView: View { 16 | let store: AdvancedDomain.Store 17 | 18 | var body: some View { 19 | ScrollView { 20 | settings 21 | if #available(macOS 11.0, *) { 22 | excludedApps 23 | } 24 | Spacer() 25 | } 26 | } 27 | 28 | private var settings: some View { 29 | SettingsSectionView( 30 | showSeparator: false, 31 | title: { Text(_L10n.View.title) }, 32 | content: { 33 | WithViewStore(store) { viewStore in 34 | VStack(alignment: .leading) { 35 | SettingsCheckbox(isOn: viewStore.binding( 36 | get: { $0.listenToKeyboardEvent }, 37 | send: { _ in .toggleListenToKeyboardEvents } 38 | )) { 39 | Text(_L10n.View.listenToKeyboardEvent) 40 | } 41 | 42 | Text(_L10n.View.listenToKeyboardEventIntroduction).asFeatureIntroduction() 43 | } 44 | } 45 | } 46 | ) 47 | } 48 | 49 | @available(macOS 11.0, *) 50 | private var excludedApps: some View { 51 | SettingsSectionView(showSeparator: false, content: { 52 | WithViewStore(store) { viewStore in 53 | Text(_L10n.View.excludeListTitle).asWidgetTitle() 54 | VStack(alignment: .leading, spacing: 0) { 55 | ScrollView { 56 | VStack(spacing: 4) { 57 | ForEach(viewStore.excludedApps) { app in 58 | Button(action: { 59 | viewStore.send(.selectExcludedApp(app)) 60 | }) { 61 | HStack { 62 | Text("\(app.appName) (\(app.bundleIdentifier))") 63 | Spacer() 64 | } 65 | .contentShape(Rectangle()) 66 | } 67 | .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6)) 68 | .roundedCornerBackground( 69 | cornerRadius: 4, 70 | fillColor: viewStore.state.selectedExcludedApp == app 71 | ? Color(NSColor.selectedControlColor) 72 | : .clear, 73 | strokeColor: .clear, 74 | strokeWidth: 0, 75 | shadow: nil 76 | ) 77 | .padding(EdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6)) 78 | .frame(maxWidth: .infinity) 79 | .buttonStyle(PlainButtonStyle()) 80 | } 81 | } 82 | } 83 | .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) 84 | .background(Color.clear) 85 | .frame(maxWidth: 320, minHeight: 120, maxHeight: 120) 86 | 87 | HStack { 88 | Menu("+") { 89 | ForEach(viewStore.availableApplications) { app in 90 | Button(action: { 91 | viewStore.send(.addExcludedApp(app)) 92 | }) { 93 | Text("\(app.appName) (\(app.bundleIdentifier))") 94 | } 95 | } 96 | } 97 | .frame(width: 40) 98 | 99 | Button(action: { 100 | viewStore.send(.removeSelectedExcludedApp) 101 | }) { 102 | Image(nsImage: NSImage(named: NSImage.touchBarDeleteTemplateName)!) 103 | }.buttonStyle(.plain) 104 | } 105 | .padding(EdgeInsets(top: 0, leading: 6, bottom: 6, trailing: 6)) 106 | } 107 | .roundedCornerBackground( 108 | cornerRadius: 2, 109 | fillColor: Color(NSColor.controlBackgroundColor), 110 | strokeColor: Color(NSColor.separatorColor), 111 | strokeWidth: 1, 112 | shadow: nil 113 | ) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | private enum _L10n { 120 | typealias View = L10n.Advanced.View 121 | } 122 | 123 | struct AdvancedView_Previews: PreviewProvider { 124 | static var previews: some View { 125 | AdvancedView(store: .init( 126 | initialState: .init( 127 | listenToKeyboardEvent: true, 128 | excludedApps: [ 129 | .init(appName: "A", bundleIdentifier: "A"), 130 | .init(appName: "B", bundleIdentifier: "B"), 131 | ], 132 | availableApplications: [ 133 | .init(appName: "A", bundleIdentifier: "A"), 134 | .init(appName: "B", bundleIdentifier: "B"), 135 | ] 136 | ), 137 | reducer: AdvancedDomain.reducer, 138 | environment: .live(environment: .init(persisted: .init())) 139 | )) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Persisted.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Persisted: PersistedType { 4 | init(userDefaults: PropertyListStorage) { 5 | replace(userDefaults: userDefaults) 6 | } 7 | 8 | // MARK: App 9 | 10 | @UserDefault("LaunchCount", defaultValue: 0) 11 | var launchCount: Int 12 | 13 | // MARK: General 14 | 15 | let general = General() 16 | struct General: PersistedType { 17 | @UserDefault("StartAtLogin", defaultValue: false) 18 | var startAtLogin: Bool 19 | } 20 | 21 | let advanced = Advanced() 22 | struct Advanced: PersistedType { 23 | static func key(_ name: String) -> String { "Advanced\(name)" } 24 | @UserDefault(key("ListenToKeyboardEvent"), defaultValue: false) 25 | var listenToKeyboardEvent: Bool 26 | @UserDefault(key("TapGestureDelay"), defaultValue: 0) 27 | var tapGestureDelayInMilliseconds: Int 28 | @UserDefault(key("GlobalExcludedApplications"), defaultValue: []) 29 | var gloablExcludedApplications: [ExcludedApplication] 30 | struct ExcludedApplication: PropertyListStorable, Equatable, Identifiable, Hashable { 31 | var appName: String 32 | var bundleIdentifier: String 33 | var id: String { bundleIdentifier } 34 | 35 | static func makeFromPropertyListValue( 36 | value: [String: String] 37 | ) throws -> ExcludedApplication { 38 | .init( 39 | appName: value["appName"] ?? "N/A", 40 | bundleIdentifier: value["bundleIdentifier"] ?? "N/A" 41 | ) 42 | } 43 | 44 | var propertyListValue: [String: String] { 45 | [ 46 | "appName": appName, 47 | "bundleIdentifier": bundleIdentifier, 48 | ] 49 | } 50 | } 51 | } 52 | 53 | // MARK: Magic Scroll 54 | 55 | let moveToScroll = MoveToScroll() 56 | struct MoveToScroll: PersistedType { 57 | static func key(_ name: String) -> String { "MoveToScroll\(name)" } 58 | @UserDefault(key("KeyCombination"), defaultValue: nil) 59 | var keyCombination: KeyCombination? 60 | @UserDefault(key("NumberOfTapsRequired"), defaultValue: 1) 61 | var numberOfTapsRequired: Int 62 | @UserDefault(key("SpeedMultiplier"), defaultValue: 3) 63 | var scrollSpeedMultiplier: Double 64 | @UserDefault(key("SwipeSpeedMultiplier"), defaultValue: 0.5) 65 | var swipeSpeedMultiplier: Double 66 | @UserDefault(key("InertiaEffect"), defaultValue: true) 67 | var isInertiaEffectEnabled: Bool 68 | 69 | let halfPageScroll = HalfPageScroll() 70 | struct HalfPageScroll: PersistedType { 71 | static func key(_ name: String) -> String { "HalfPageScroll\(name)" } 72 | @UserDefault(key("UseMoveToScrollDoubleTap"), defaultValue: true) 73 | var useMoveToScrollDoubleTap: Bool 74 | @UserDefault(key("KeyCombination"), defaultValue: nil) 75 | var keyCombination: KeyCombination? 76 | @UserDefault(key("NumberOfTapsRequired"), defaultValue: 1) 77 | var numberOfTapsRequired: Int 78 | } 79 | } 80 | 81 | // MARK: Zoom and Rotate 82 | 83 | let zoomAndRotate = ZoomAndRotate() 84 | struct ZoomAndRotate: PersistedType { 85 | static func key(_ name: String) -> String { "ZoomAndRotate\(name)" } 86 | @UserDefault(key("KeyCombination"), defaultValue: nil) 87 | var keyCombination: KeyCombination? 88 | @UserDefault(key("NumberOfTapsRequired"), defaultValue: 1) 89 | var numberOfTapsRequired: Int 90 | @UserDefault(key("ZoomSpeedDirection"), defaultValue: .up) 91 | var zoomGestureDirection: MoveMouseDirection 92 | @UserDefault(key("RotateSpeedDirection"), defaultValue: .right) 93 | var rotateGestureDirection: MoveMouseDirection 94 | @UserDefault(key("ZoomSpeedMultiplier"), defaultValue: 1) 95 | var zoomSpeedMultiplier: Double 96 | @UserDefault(key("RotateSpeedMultiplier"), defaultValue: 1) 97 | var rotateSpeedMultiplier: Double 98 | 99 | let smartZoom = SmartZoom() 100 | struct SmartZoom: PersistedType { 101 | static func key(_ name: String) -> String { "SmartZoom\(name)" } 102 | @UserDefault(key("UseZoomAndRotateDoubleTap"), defaultValue: true) 103 | var useZoomAndRotateDoubleTap: Bool 104 | @UserDefault(key("KeyCombination"), defaultValue: nil) 105 | var keyCombination: KeyCombination? 106 | @UserDefault(key("NumberOfTapsRequired"), defaultValue: 1) 107 | var numberOfTapsRequired: Int 108 | } 109 | } 110 | 111 | // MARK: Dock Swipe 112 | 113 | let dockSwipe = DockSwipe() 114 | struct DockSwipe: PersistedType { 115 | static func key(_ name: String) -> String { "DockSwipe\(name)" } 116 | @UserDefault(key("KeyCombination"), defaultValue: nil) 117 | var keyCombination: KeyCombination? 118 | @UserDefault(key("NumberOfTapsRequired"), defaultValue: 1) 119 | var numberOfTapsRequired: Int 120 | } 121 | } 122 | 123 | // MARK: - PersistedType 124 | 125 | private protocol PersistedType {} 126 | 127 | private extension PersistedType { 128 | func replace(userDefaults: PropertyListStorage) { 129 | for child in Mirror(reflecting: self).children { 130 | if let ud = child.value as? UserDefaultStorableWrapper { 131 | ud.userDefaults = userDefaults 132 | } 133 | if let pt = child.value as? PersistedType { 134 | pt.replace(userDefaults: userDefaults) 135 | } 136 | } 137 | } 138 | 139 | func reset() { 140 | for child in Mirror(reflecting: self).children { 141 | if let ud = child.value as? UserDefaultStorableWrapper { 142 | ud.reset() 143 | } 144 | if let pt = child.value as? PersistedType { 145 | pt.reset() 146 | } 147 | } 148 | } 149 | } 150 | 151 | protocol UserDefaultStorableWrapper: AnyObject { 152 | var userDefaults: PropertyListStorage { get set } 153 | func reset() 154 | } 155 | 156 | extension UserDefault: UserDefaultStorableWrapper { 157 | func reset() { 158 | wrappedValue = defaultValue 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/Persistency/UserDefault.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | /// Typed UserDefaults。 4 | @propertyWrapper 5 | class UserDefault<Value: PropertyListStorable> { 6 | let key: String 7 | let defaultValue: Value 8 | var userDefaults: PropertyListStorage 9 | 10 | init( 11 | _ key: String, 12 | defaultValue: Value, 13 | userDefaults: PropertyListStorage = MemoryPropertyListStorage() 14 | ) { 15 | self.key = key 16 | self.defaultValue = defaultValue 17 | self.userDefaults = userDefaults 18 | } 19 | 20 | var wrappedValue: Value { 21 | get { 22 | ( 23 | try? (userDefaults.object(forKey: key) as? Value.V) 24 | .map(Value.makeFromPropertyListValue(value:)) 25 | ) ?? defaultValue 26 | } 27 | set { 28 | if newValue.shouldRemove { 29 | userDefaults.removeObject(forKey: key) 30 | } else { 31 | userDefaults.set(newValue.propertyListValue, forKey: key) 32 | } 33 | } 34 | } 35 | } 36 | 37 | protocol PropertyListStorage { 38 | func object(forKey: String) -> Any? 39 | func removeObject(forKey: String) 40 | func set(_: Any?, forKey: String) 41 | } 42 | 43 | extension UserDefaults: PropertyListStorage {} 44 | 45 | final class MemoryPropertyListStorage: PropertyListStorage { 46 | var content = [String: Any]() { 47 | didSet { 48 | NSWorkspace.shared.notificationCenter.post( 49 | name: UserDefaults.didChangeNotification, 50 | object: nil 51 | ) 52 | } 53 | } 54 | 55 | func object(forKey key: String) -> Any? { 56 | content[key] 57 | } 58 | 59 | func removeObject(forKey key: String) { 60 | content[key] = nil 61 | } 62 | 63 | func set(_ value: Any?, forKey key: String) { 64 | content[key] = value 65 | } 66 | } 67 | 68 | // MARK: - Storables 69 | 70 | /// Anything that can be stored in UserDefaults. 71 | protocol PropertyListStorable { 72 | associatedtype V: PropertyListValue 73 | /// The actual value to be stored in UserDefaults. 74 | var propertyListValue: V { get } 75 | /// Convert the stored data back into the type. 76 | static func makeFromPropertyListValue(value: V) throws -> Self 77 | } 78 | 79 | extension PropertyListStorable { 80 | /// Determine when a value is considered *not exist* in UserDefaults. 81 | /// 82 | /// e.g. When an array is empty, we may want to remove the entry from UserDefaults. 83 | var shouldRemove: Bool { propertyListValue.isRemoveValue } 84 | } 85 | 86 | /// A type than can be stored in `UserDefaults`. 87 | protocol PropertyListValue { 88 | /// If the value is considered *not exist*. 89 | var isRemoveValue: Bool { get } 90 | } 91 | 92 | extension PropertyListValue { 93 | var isRemoveValue: Bool { false } 94 | } 95 | 96 | extension PropertyListStorable { 97 | var propertyListValue: Self { self } 98 | static func makeFromPropertyListValue(value: Self) -> Self { value } 99 | } 100 | 101 | // MARK: - Trivial Values 102 | 103 | typealias TrivialPropertyListStorable = PropertyListValue & PropertyListStorable 104 | 105 | extension Data: TrivialPropertyListStorable {} 106 | extension String: TrivialPropertyListStorable {} 107 | extension Date: TrivialPropertyListStorable {} 108 | extension Bool: TrivialPropertyListStorable {} 109 | extension Int: TrivialPropertyListStorable {} 110 | extension Int8: TrivialPropertyListStorable {} 111 | extension Int16: TrivialPropertyListStorable {} 112 | extension Int32: TrivialPropertyListStorable {} 113 | extension Int64: TrivialPropertyListStorable {} 114 | extension UInt: TrivialPropertyListStorable {} 115 | extension UInt8: TrivialPropertyListStorable {} 116 | extension UInt16: TrivialPropertyListStorable {} 117 | extension UInt32: TrivialPropertyListStorable {} 118 | extension UInt64: TrivialPropertyListStorable {} 119 | extension Double: TrivialPropertyListStorable {} 120 | extension Float: TrivialPropertyListStorable {} 121 | 122 | // MARK: - Array 123 | 124 | extension Array: PropertyListValue where Element: PropertyListValue { 125 | var isRemoveValue: Bool { isEmpty } 126 | } 127 | 128 | extension Array: PropertyListStorable where Element: PropertyListStorable { 129 | var isRemoveValue: Bool { isEmpty } 130 | var propertyListValue: [Element.V] { map(\.propertyListValue) } 131 | static func makeFromPropertyListValue(value: [Element.V]) throws -> Self { 132 | try value.map(Element.makeFromPropertyListValue(value:)) 133 | } 134 | } 135 | 136 | // MARK: - Optional 137 | 138 | extension Optional: PropertyListValue where Wrapped: PropertyListValue { 139 | var isRemoveValue: Bool { self == nil } 140 | } 141 | 142 | extension Optional: PropertyListStorable where Wrapped: PropertyListStorable { 143 | var isRemoveValue: Bool { self == nil } 144 | var propertyListValue: Wrapped.V? { map(\.propertyListValue) } 145 | static func makeFromPropertyListValue(value: Wrapped.V?) throws -> Self { 146 | try value.map(Wrapped.makeFromPropertyListValue(value:)) 147 | } 148 | } 149 | 150 | // MARK: - Dictionary 151 | 152 | /// When a dictionary has values of trivial values, it's already storable in UserDefaults. 153 | extension Dictionary: PropertyListValue 154 | where Key == String, Value: TrivialPropertyListStorable 155 | { 156 | var isRemoveValue: Bool { isEmpty } 157 | } 158 | 159 | /// When a dictionary has values that are codable, it's stored as JSON in UserDefaults. 160 | extension Dictionary: PropertyListStorable where Key == String, Value: Codable { 161 | var isRemoveValue: Bool { isEmpty } 162 | 163 | var propertyListValue: [String: Data] { 164 | let encoder = JSONEncoder() 165 | return compactMapValues { try? encoder.encode($0) } 166 | } 167 | 168 | static func makeFromPropertyListValue(value: [String: Data]) throws -> Self { 169 | let decoder = JSONDecoder() 170 | return try value.compactMapValues { try decoder.decode(Value.self, from: $0) } 171 | } 172 | } 173 | 174 | // MARK: - Raw 175 | 176 | private struct NilError: Swift.Error {} 177 | 178 | extension PropertyListStorable where Self: RawRepresentable, Self.RawValue: PropertyListValue { 179 | var propertyListValue: RawValue { rawValue } 180 | static func makeFromPropertyListValue(value: RawValue) throws -> Self { 181 | guard let it = Self(rawValue: value) else { throw NilError() } 182 | return it 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Library/GestureRecognizer/TapHoldGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import Combine 3 | import Foundation 4 | 5 | extension GestureRecognizers { 6 | final class TapHold: GestureRecognizer { 7 | private struct Key: Hashable {} 8 | 9 | var keyCombination: KeyCombination? { didSet { state = State() } } 10 | var numberOfTapsRequired: Int = 1 { didSet { state = State() } } 11 | private(set) lazy var publisher: AnyPublisher<Bool, Never> = $isHolding 12 | .receive(on: DispatchQueue.default) 13 | .share(replay: 1) 14 | .eraseToAnyPublisher() 15 | 16 | struct State { 17 | var lastButtonDownTimestamp = 0 as TimeInterval 18 | var tapCount = 0 19 | var holdingDownKeys = Set<Int64>() 20 | var holdingDownMouseButtons = Set<Int64>() 21 | } 22 | 23 | private let hook: CGEventHookType 24 | /// Track to prevent keyDown events to repeat 25 | private var isDown = false 26 | @Published private var isHolding: Bool = false 27 | 28 | private(set) var state = State() 29 | private let key: AnyHashable 30 | 31 | deinit { 32 | hook.removeManipulation(forKey: key) 33 | } 34 | 35 | init(hook: CGEventHookType, key: AnyHashable) { 36 | self.key = key 37 | self.hook = hook 38 | super.init() 39 | 40 | hook.add( 41 | .init( 42 | eventsOfInterest: [.keyUp, .keyDown, .otherMouseUp, .otherMouseDown, 43 | .leftMouseUp, .leftMouseDown, .rightMouseUp, 44 | .rightMouseDown], 45 | convert: { [weak self] _, type, event -> CGEventManipulation.Result in 46 | guard let self = self else { return .unchange } 47 | return DispatchQueue.default.sync { 48 | switch type { 49 | case .keyUp, 50 | .keyDown: 51 | return self.handleKeys(type: type, event: event) 52 | case .otherMouseUp, 53 | .otherMouseDown, 54 | .leftMouseUp, 55 | .leftMouseDown, 56 | .rightMouseUp, 57 | .rightMouseDown: 58 | return self.handleMouseButton(type: type, event: event) 59 | default: 60 | return .unchange 61 | } 62 | } 63 | } 64 | ), 65 | forKey: key 66 | ) 67 | } 68 | 69 | func consume() { 70 | state = State() 71 | } 72 | } 73 | } 74 | 75 | extension GestureRecognizers.TapHold: Cancellable { 76 | func cancel() { 77 | guard isHolding else { return } 78 | state = State() 79 | isHolding = false 80 | } 81 | } 82 | 83 | extension GestureRecognizers.TapHold { 84 | private var duration: TimeInterval { Double(numberOfTapsRequired - 1) * 0.3 } 85 | 86 | private func handleKeys( 87 | type: CGEventType, 88 | event: CGEvent 89 | ) -> CGEventManipulation.Result { 90 | guard let combination = keyCombination else { return .unchange } 91 | let code = event[.keyboardEventKeycode] 92 | let activator = combination.activator 93 | guard case let .key(target) = activator, code == target else { return .unchange } 94 | 95 | resetStateIfNeeded() 96 | 97 | switch type { 98 | case .keyDown: 99 | guard combination.matchesFlags(event.flags) else { return .unchange } 100 | guard !isDown else { return shouldDiscardEvent ? .discarded : .unchange } 101 | down(code: code) 102 | isDown = true 103 | return shouldDiscardEvent ? .discarded : .unchange 104 | case .keyUp: 105 | guard isDown else { return .unchange } 106 | up(code: code) 107 | isDown = false 108 | return shouldDiscardEvent ? .discarded : .unchange 109 | default: return .unchange 110 | } 111 | } 112 | 113 | private func handleMouseButton( 114 | type: CGEventType, 115 | event: CGEvent 116 | ) -> CGEventManipulation.Result { 117 | guard let combination = keyCombination else { return .unchange } 118 | let activator = combination.activator 119 | let code = event[.mouseEventButtonNumber] 120 | guard case let .mouse(target) = activator, code == target else { return .unchange } 121 | 122 | resetStateIfNeeded() 123 | 124 | switch type { 125 | case .otherMouseDown, .leftMouseDown, .rightMouseDown: 126 | guard combination.matchesFlags(event.flags) else { return .unchange } 127 | guard !isDown else { return shouldDiscardEvent ? .discarded : .unchange } 128 | down(code: code) 129 | isDown = true 130 | return shouldDiscardEvent ? .discarded : .unchange 131 | case .otherMouseUp, .leftMouseUp, .rightMouseUp: 132 | guard isDown else { return .unchange } 133 | up(code: code) 134 | isDown = false 135 | return shouldDiscardEvent ? .discarded : .unchange 136 | default: 137 | return .unchange 138 | } 139 | } 140 | 141 | private func resetStateIfNeeded() { 142 | if Date().timeIntervalSince1970 - state.lastButtonDownTimestamp >= duration + 0.2 { 143 | state.tapCount = 0 144 | } 145 | } 146 | 147 | private func down(code: Int64) { 148 | state.holdingDownMouseButtons.insert(code) 149 | if state.tapCount == 0 { 150 | state.lastButtonDownTimestamp = Date().timeIntervalSince1970 151 | } 152 | state.tapCount += 1 153 | if state.tapCount == numberOfTapsRequired { 154 | cancelOtherGestures { $0 is GestureRecognizers.TapHold } 155 | isHolding = true 156 | } else { 157 | isHolding = false 158 | } 159 | } 160 | 161 | private func up(code: Int64) { 162 | state.holdingDownMouseButtons.remove(code) 163 | isHolding = false 164 | 165 | let now = Date().timeIntervalSince1970 166 | if now - state.lastButtonDownTimestamp >= max(duration, 0.3) { 167 | state.tapCount = 0 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Assets.xcassets/icon_zoomAndRotate.imageset/zoomAndRotate.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /ExtGState << /E2 << /ca 0.200000 >> 5 | /E1 << /ca 0.200000 >> 6 | >> >> 7 | endobj 8 | 9 | 2 0 obj 10 | << /Length 3 0 R >> 11 | stream 12 | /DeviceRGB CS 13 | /DeviceRGB cs 14 | q 15 | q 16 | /E1 gs 17 | 1.000000 0.000000 -0.000000 1.000000 9.000000 9.000000 cm 18 | 0.000000 0.000000 0.000000 scn 19 | 14.000000 7.000000 m 20 | 14.000000 3.134007 10.865993 0.000000 7.000000 0.000000 c 21 | 3.134007 0.000000 0.000000 3.134007 0.000000 7.000000 c 22 | 0.000000 10.865993 3.134007 14.000000 7.000000 14.000000 c 23 | 10.865993 14.000000 14.000000 10.865993 14.000000 7.000000 c 24 | h 25 | f 26 | n 27 | Q 28 | 23.000000 16.000000 m 29 | 23.000000 12.134007 19.865993 9.000000 16.000000 9.000000 c 30 | 12.134007 9.000000 9.000000 12.134007 9.000000 16.000000 c 31 | 9.000000 19.865993 12.134007 23.000000 16.000000 23.000000 c 32 | 19.865993 23.000000 23.000000 19.865993 23.000000 16.000000 c 33 | h 34 | W* 35 | n 36 | q 37 | 1.000000 0.000000 -0.000000 1.000000 9.000000 9.000000 cm 38 | 0.000000 0.000000 0.000000 scn 39 | 13.000000 7.000000 m 40 | 13.000000 3.686292 10.313708 1.000000 7.000000 1.000000 c 41 | 7.000000 -1.000000 l 42 | 11.418278 -1.000000 15.000000 2.581722 15.000000 7.000000 c 43 | 13.000000 7.000000 l 44 | h 45 | 7.000000 1.000000 m 46 | 3.686291 1.000000 1.000000 3.686292 1.000000 7.000000 c 47 | -1.000000 7.000000 l 48 | -1.000000 2.581722 2.581722 -1.000000 7.000000 -1.000000 c 49 | 7.000000 1.000000 l 50 | h 51 | 1.000000 7.000000 m 52 | 1.000000 10.313708 3.686291 13.000000 7.000000 13.000000 c 53 | 7.000000 15.000000 l 54 | 2.581722 15.000000 -1.000000 11.418278 -1.000000 7.000000 c 55 | 1.000000 7.000000 l 56 | h 57 | 7.000000 13.000000 m 58 | 10.313708 13.000000 13.000000 10.313708 13.000000 7.000000 c 59 | 15.000000 7.000000 l 60 | 15.000000 11.418278 11.418278 15.000000 7.000000 15.000000 c 61 | 7.000000 13.000000 l 62 | h 63 | f 64 | n 65 | Q 66 | Q 67 | q 68 | 0.819152 -0.573576 0.573576 0.819152 12.290586 11.723944 cm 69 | 0.000000 0.000000 0.000000 scn 70 | 0.000000 17.212738 m 71 | -0.276142 17.212738 -0.500000 16.988880 -0.500000 16.712738 c 72 | -0.500000 16.436596 -0.276142 16.212738 0.000000 16.212738 c 73 | 0.000000 17.212738 l 74 | h 75 | 10.500000 5.712738 m 76 | 10.500000 5.436596 10.723858 5.212738 11.000000 5.212738 c 77 | 11.276142 5.212738 11.500000 5.436596 11.500000 5.712738 c 78 | 10.500000 5.712738 l 79 | h 80 | 0.000000 16.212738 m 81 | 5.798990 16.212738 10.500000 11.511728 10.500000 5.712738 c 82 | 11.500000 5.712738 l 83 | 11.500000 12.064013 6.351275 17.212738 0.000000 17.212738 c 84 | 0.000000 16.212738 l 85 | h 86 | f 87 | n 88 | Q 89 | q 90 | 0.573576 0.819152 -0.819152 0.573576 23.288414 19.542591 cm 91 | 0.000000 0.000000 0.000000 scn 92 | 4.000000 8.256332 m 93 | 7.464102 3.756332 l 94 | 0.535898 3.756332 l 95 | 4.000000 8.256332 l 96 | h 97 | f 98 | n 99 | Q 100 | q 101 | -0.819152 0.573576 -0.573576 -0.819152 19.709415 20.276072 cm 102 | 0.000000 0.000000 0.000000 scn 103 | 0.000000 17.212738 m 104 | -0.276142 17.212738 -0.500000 16.988880 -0.500000 16.712738 c 105 | -0.500000 16.436596 -0.276142 16.212738 0.000000 16.212738 c 106 | 0.000000 17.212738 l 107 | h 108 | 10.500000 5.712738 m 109 | 10.500000 5.436596 10.723858 5.212738 11.000000 5.212738 c 110 | 11.276142 5.212738 11.500000 5.436596 11.500000 5.712738 c 111 | 10.500000 5.712738 l 112 | h 113 | 0.000000 16.212738 m 114 | 5.798990 16.212738 10.500000 11.511728 10.500000 5.712738 c 115 | 11.500000 5.712738 l 116 | 11.500000 12.064013 6.351275 17.212738 0.000000 17.212738 c 117 | 0.000000 16.212738 l 118 | h 119 | f 120 | n 121 | Q 122 | q 123 | -0.573576 -0.819152 0.819152 -0.573576 8.711617 12.457439 cm 124 | 0.000000 0.000000 0.000000 scn 125 | 4.000000 8.256332 m 126 | 7.464102 3.756332 l 127 | 0.535898 3.756332 l 128 | 4.000000 8.256332 l 129 | h 130 | f 131 | n 132 | Q 133 | q 134 | 0.898794 0.438371 -0.438371 0.898794 19.780060 4.956146 cm 135 | 0.000000 0.000000 0.000000 scn 136 | 3.051895 3.403651 m 137 | 3.551757 3.415398 l 138 | 3.051895 3.403651 l 139 | h 140 | 0.053551 3.333186 m 141 | -0.446311 3.321439 l 142 | 0.053551 3.333186 l 143 | h 144 | 2.500138 5.600054 m 145 | 2.552033 3.391903 l 146 | 3.551757 3.415398 l 147 | 3.499862 5.623549 l 148 | 2.500138 5.600054 l 149 | h 150 | 0.553413 3.344934 m 151 | 0.499862 5.623549 l 152 | -0.499862 5.600054 l 153 | -0.446311 3.321439 l 154 | 0.553413 3.344934 l 155 | h 156 | 1.552723 2.368833 m 157 | 1.009817 2.368833 0.566168 2.802178 0.553413 3.344934 c 158 | -0.446311 3.321439 l 159 | -0.420795 2.235702 0.466686 1.368833 1.552723 1.368833 c 160 | 1.552723 2.368833 l 161 | h 162 | 2.552033 3.391903 m 163 | 2.565219 2.830808 2.113974 2.368833 1.552723 2.368833 c 164 | 1.552723 1.368833 l 165 | 2.675457 1.368833 3.578135 2.292974 3.551757 3.415398 c 166 | 2.552033 3.391903 l 167 | h 168 | f 169 | n 170 | Q 171 | q 172 | 1.000000 0.000000 -0.000000 1.000000 12.000000 11.000000 cm 173 | 0.000000 0.000000 0.000000 scn 174 | 4.000000 0.500000 m 175 | 4.276143 0.500000 4.500000 0.723857 4.500000 1.000000 c 176 | 4.500000 1.276142 4.276143 1.500000 4.000000 1.500000 c 177 | 4.000000 0.500000 l 178 | h 179 | 0.500000 5.000000 m 180 | 0.500000 5.276143 0.276142 5.500000 0.000000 5.500000 c 181 | -0.276142 5.500000 -0.500000 5.276143 -0.500000 5.000000 c 182 | 0.500000 5.000000 l 183 | h 184 | 4.000000 1.500000 m 185 | 2.067003 1.500000 0.500000 3.067003 0.500000 5.000000 c 186 | -0.500000 5.000000 l 187 | -0.500000 2.514719 1.514719 0.500000 4.000000 0.500000 c 188 | 4.000000 1.500000 l 189 | h 190 | f 191 | n 192 | Q 193 | q 194 | /E2 gs 195 | 1.000000 0.000000 -0.000000 1.000000 17.500000 7.500000 cm 196 | 0.000000 0.000000 0.000000 scn 197 | 1.000000 0.000000 m 198 | 0.000000 2.000000 l 199 | 2.500000 3.500000 l 200 | 3.500000 1.500000 l 201 | 3.000000 0.000000 l 202 | 1.000000 0.000000 l 203 | h 204 | f 205 | n 206 | Q 207 | 208 | endstream 209 | endobj 210 | 211 | 3 0 obj 212 | 4674 213 | endobj 214 | 215 | 4 0 obj 216 | << /Annots [] 217 | /Type /Page 218 | /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] 219 | /Resources 1 0 R 220 | /Contents 2 0 R 221 | /Parent 5 0 R 222 | >> 223 | endobj 224 | 225 | 5 0 obj 226 | << /Kids [ 4 0 R ] 227 | /Count 1 228 | /Type /Pages 229 | >> 230 | endobj 231 | 232 | 6 0 obj 233 | << /Type /Catalog 234 | /Pages 5 0 R 235 | >> 236 | endobj 237 | 238 | xref 239 | 0 7 240 | 0000000000 65535 f 241 | 0000000010 00000 n 242 | 0000000132 00000 n 243 | 0000004862 00000 n 244 | 0000004885 00000 n 245 | 0000005058 00000 n 246 | 0000005132 00000 n 247 | trailer 248 | << /ID [ (some) (id) ] 249 | /Root 6 0 R 250 | /Size 7 251 | >> 252 | startxref 253 | 5191 254 | %%EOF -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouseTests/MoveToScrollDomainTests.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import ComposableArchitecture 3 | import XCTest 4 | 5 | @testable import AbnormalMouse 6 | 7 | class MoveToScrollDomainTests: XCTestCase { 8 | func testMoveToScrollSettings() throws { 9 | let persisted = Persisted( 10 | userDefaults: MemoryPropertyListStorage(), 11 | keychainAccess: FakeKeychainAccess() 12 | ).moveToScroll 13 | 14 | var hasConflict: (ActivatorConflictChecker.Feature) -> Bool = { _ in false } 15 | 16 | let initialState = MoveToScrollDomain.State(from: persisted) 17 | let store = TestStore( 18 | initialState: initialState, 19 | reducer: MoveToScrollDomain.reducer, 20 | environment: .init( 21 | environment: .init( 22 | persisted: persisted, 23 | featureHasConflict: { hasConflict($0) } 24 | ), 25 | date: { Date() }, 26 | openURL: { _ in }, 27 | quitApp: {}, 28 | mainQueue: { .main } 29 | ) 30 | ) 31 | 32 | let keyCombination = KeyCombination(Set([ 33 | .key(KeyboardCode.command.rawValue), 34 | .key(KeyboardCode.a.rawValue), 35 | ])) 36 | 37 | XCTAssertEqual(initialState.scrollSpeedMultiplier, 3) 38 | XCTAssertEqual(initialState.moveToScrollActivator.keyCombination, nil) 39 | XCTAssertEqual(initialState.moveToScrollActivator.numberOfTapsRequired, 1) 40 | XCTAssertEqual(initialState.moveToScrollActivator.hasConflict, false) 41 | 42 | store.assert( 43 | .send(.changeScrollSpeedMultiplierTo(2)) { 44 | $0.scrollSpeedMultiplier = 2 45 | }, 46 | .do { 47 | hasConflict = { $0 == .moveToScroll } 48 | }, 49 | .send(.moveToScroll(.setKeyCombination(keyCombination))) { 50 | $0.moveToScrollActivator.keyCombination = keyCombination 51 | }, 52 | .receive(._internal(.checkConflict)) { 53 | $0.moveToScrollActivator.hasConflict = true 54 | }, 55 | .do { 56 | XCTAssertEqual(persisted.keyCombination, keyCombination) 57 | hasConflict = { _ in false } 58 | }, 59 | .send(.moveToScroll(.setKeyCombination(keyCombination))) { 60 | $0.moveToScrollActivator.keyCombination = keyCombination 61 | }, 62 | .receive(._internal(.checkConflict)) { 63 | $0.moveToScrollActivator.hasConflict = false 64 | }, 65 | .do { 66 | XCTAssertEqual(persisted.keyCombination, keyCombination) 67 | }, 68 | .send(.moveToScroll(.setNumberOfTapsRequired(2))) { 69 | $0.moveToScrollActivator.numberOfTapsRequired = 2 70 | }, 71 | .receive(._internal(.checkConflict)), 72 | .do { 73 | XCTAssertEqual(persisted.numberOfTapsRequired, 2) 74 | }, 75 | .send(.moveToScroll(.clearKeyCombination)) { 76 | $0.moveToScrollActivator.keyCombination = nil 77 | }, 78 | .receive(._internal(.checkConflict)), 79 | .do { 80 | XCTAssertEqual(persisted.keyCombination, nil) 81 | XCTAssertEqual(persisted.scrollSpeedMultiplier, 2, accuracy: 0) 82 | } 83 | ) 84 | } 85 | 86 | func testHalfPageScrollSettings() throws { 87 | let persisted = Persisted( 88 | userDefaults: MemoryPropertyListStorage(), 89 | keychainAccess: FakeKeychainAccess() 90 | ).moveToScroll 91 | 92 | var hasConflict: (ActivatorConflictChecker.Feature) -> Bool = { _ in false } 93 | 94 | let initialState = MoveToScrollDomain.State(from: persisted) 95 | let store = TestStore( 96 | initialState: initialState, 97 | reducer: MoveToScrollDomain.reducer, 98 | environment: .init( 99 | environment: .init( 100 | persisted: persisted, 101 | featureHasConflict: { hasConflict($0) } 102 | ), 103 | date: { Date() }, 104 | openURL: { _ in }, 105 | quitApp: {}, 106 | mainQueue: { .main } 107 | ) 108 | ) 109 | 110 | let keyCombination = KeyCombination(Set([ 111 | .key(KeyboardCode.command.rawValue), 112 | .key(KeyboardCode.a.rawValue), 113 | ])) 114 | 115 | XCTAssertTrue(initialState.halfPageScrollActivator.shouldUseMoveToScrollKeyCombination) 116 | XCTAssertNil(initialState.halfPageScrollActivator.keyCombination) 117 | XCTAssertEqual(initialState.halfPageScrollActivator.numberOfTapsRequired, 1) 118 | XCTAssertFalse(initialState.halfPageScrollActivator.hasConflict) 119 | 120 | store.assert( 121 | .send(.halfPageScroll(.toggleUseMoveToScrollKeyCombinationDoubleTap)) { 122 | $0.halfPageScrollActivator.shouldUseMoveToScrollKeyCombination = false 123 | }, 124 | .receive(._internal(.checkConflict)), 125 | .do { 126 | XCTAssertFalse(persisted.halfPageScroll.useMoveToScrollDoubleTap) 127 | hasConflict = { $0 == .halfPageScroll } 128 | }, 129 | .send(.halfPageScroll(.setKeyCombination(keyCombination))) { 130 | $0.halfPageScrollActivator.keyCombination = keyCombination 131 | }, 132 | .receive(._internal(.checkConflict)) { 133 | $0.halfPageScrollActivator.hasConflict = true 134 | }, 135 | .do { 136 | XCTAssertEqual(persisted.halfPageScroll.keyCombination, keyCombination) 137 | hasConflict = { _ in false } 138 | }, 139 | .send(.halfPageScroll(.setNumberOfTapsRequired(2))) { 140 | $0.halfPageScrollActivator.numberOfTapsRequired = 2 141 | }, 142 | .receive(._internal(.checkConflict)) { 143 | $0.halfPageScrollActivator.hasConflict = false 144 | }, 145 | .do { 146 | XCTAssertEqual(persisted.halfPageScroll.numberOfTapsRequired, 2) 147 | }, 148 | .send(.halfPageScroll(.clearKeyCombination)) { 149 | $0.halfPageScrollActivator.keyCombination = nil 150 | }, 151 | .receive(._internal(.checkConflict)), 152 | .do { 153 | XCTAssertEqual(persisted.halfPageScroll.keyCombination, nil) 154 | } 155 | ) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /AbnormalMouse/AbnormalMouse/Features/SharedViews/SettingsPageView.swift: -------------------------------------------------------------------------------- 1 | import CGEventOverride 2 | import SwiftUI 3 | 4 | // MARK: - Section 5 | 6 | struct SettingsSectionView<Title: View, Introduction: View, Content: View>: View { 7 | let title: Title? 8 | let introduction: Introduction? 9 | let content: Content 10 | let showSeparator: Bool 11 | 12 | init( 13 | showSeparator: Bool = false, 14 | @ViewBuilder title: () -> Title?, 15 | @ViewBuilder introduction: () -> Introduction?, 16 | @ViewBuilder content: () -> Content 17 | ) { 18 | self.showSeparator = showSeparator 19 | self.title = title() 20 | self.introduction = introduction() 21 | self.content = content() 22 | } 23 | 24 | var body: some View { 25 | VStack(alignment: .leading) { 26 | VStack(alignment: .leading, spacing: 4) { 27 | title 28 | .asFeatureTitle() 29 | introduction? 30 | .asFeatureIntroduction() 31 | } 32 | .padding( 33 | .bottom, 34 | introduction == nil 35 | ? title == nil ? 0 : 8 36 | : 20 37 | ) 38 | content 39 | } 40 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 41 | .multilineTextAlignment(.leading) 42 | .padding(.bottom, 20) 43 | .overlay(separator) 44 | .padding(.init(top: 20, leading: 20, bottom: 0, trailing: 20)) 45 | } 46 | 47 | private var separator: some View { 48 | Group { 49 | if showSeparator { 50 | GeometryReader { proxy in 51 | Path { p in 52 | p.move(to: .init(x: 0, y: proxy.size.height)) 53 | p.addLine(to: .init(x: proxy.size.width, y: proxy.size.height)) 54 | } 55 | .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | extension SettingsSectionView where Introduction == EmptyView { 63 | init( 64 | showSeparator: Bool = false, 65 | @ViewBuilder title: () -> Title, 66 | @ViewBuilder content: () -> Content 67 | ) { 68 | self.showSeparator = showSeparator 69 | self.title = title() 70 | introduction = nil 71 | self.content = content() 72 | } 73 | } 74 | 75 | extension SettingsSectionView where Title == EmptyView, Introduction == EmptyView { 76 | init( 77 | showSeparator: Bool = false, 78 | @ViewBuilder content: () -> Content 79 | ) { 80 | self.showSeparator = showSeparator 81 | title = nil 82 | introduction = nil 83 | self.content = content() 84 | } 85 | } 86 | 87 | // MARK: - Preview 88 | 89 | struct SettingsSectionView_Previews: PreviewProvider { 90 | @State static var sliderValue: Double = 3 91 | @State static var toggle: Bool = false 92 | @State static var keyCombination: KeyCombination? = KeyCombination([.key(1)]) 93 | @State static var pickerSelection: Int = 0 94 | static var previews: some View { 95 | ScrollView { 96 | VStack(spacing: 0) { 97 | SettingsSectionView( 98 | showSeparator: true, 99 | title: { Text("Title") }, 100 | introduction: { 101 | Text( 102 | """ 103 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec tortor risus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean metus purus, placerat cursus tempus dictum, bibendum sit amet dolor. Etiam venenatis lacinia sapien sodales rutrum. Cras cursus nisl nec porttitor tempus. Praesent ac lacus blandit, bibendum elit in, sodales velit. Aenean et gravida justo. Nulla eget interdum quam. Proin gravida massa sit amet ultricies sagittis. Quisque bibendum lorem massa, ac laoreet sem dapibus in. Praesent maximus tortor libero, at dignissim eros facilisis semper. Nulla porta, felis sed aliquet vestibulum, lacus tellus auctor enim, at pellentesque metus sapien ut justo. 104 | """ 105 | ) 106 | }, 107 | content: { 108 | SettingsKeyCombinationInput( 109 | keyCombination: $keyCombination, 110 | title: { Text("Key") } 111 | ) 112 | 113 | SettingsCheckbox( 114 | isOn: $toggle, 115 | title: { Text("Checkbox") } 116 | ) 117 | 118 | SettingsSlider( 119 | value: $sliderValue, 120 | in: 1...5, 121 | step: 1, 122 | valueDisplay: { Text("\(sliderValue)") }, 123 | title: { Text("Slider") } 124 | ) 125 | 126 | SettingsTips { 127 | Text("tips 1").tipsTitle("Usage") 128 | Text("tips 2").tipsTitle("123") 129 | EmptyView().tipsTitle("") 130 | } 131 | } 132 | ) 133 | 134 | SettingsSectionView( 135 | showSeparator: false, 136 | title: { Text("Title") }, 137 | introduction: { 138 | Text( 139 | """ 140 | Lorem ipsum dolor sit amet 141 | """ 142 | ) 143 | }, 144 | content: { 145 | SettingsPicker( 146 | title: Text("Picker"), 147 | selection: $pickerSelection 148 | ) { 149 | ForEach(1..<4) { 150 | Text(String($0)) 151 | } 152 | } 153 | 154 | SettingsCheckbox( 155 | isOn: .init( 156 | get: { true }, 157 | set: { _ in } 158 | ), 159 | title: { Text("Checkbox") } 160 | ) 161 | 162 | SettingsSlider( 163 | value: .init(get: { 3 }, set: { _ in }), 164 | in: 1...10, 165 | step: 1, 166 | title: { Text("Slider") } 167 | ) 168 | } 169 | ) 170 | } 171 | } 172 | .frame(height: 600) 173 | } 174 | } 175 | --------------------------------------------------------------------------------