From 67a39321314c8f422124561f7185b91be0213afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E5=98=89=E4=B9=90?= <2909634071@qq.com> Date: Sat, 25 Apr 2026 19:43:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95=E7=BC=96?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mos/AppDelegate.swift | 5 + Mos/Localizable.xcstrings | 931 +++++++++++++----- Mos/Managers/MouseSensitivityManager.swift | 103 ++ Mos/Managers/SettingsBackupManager.swift | 250 +++++ Mos/Options/Options.swift | 19 + Mos/Utils/Constants.swift | 10 + Mos/Utils/EnhanceArray.swift | 7 + .../PreferencesGeneralViewController.swift | 215 ++++ 8 files changed, 1298 insertions(+), 242 deletions(-) create mode 100644 Mos/Managers/MouseSensitivityManager.swift create mode 100644 Mos/Managers/SettingsBackupManager.swift diff --git a/Mos/AppDelegate.swift b/Mos/AppDelegate.swift index 58bd64eb..4317babf 100644 --- a/Mos/AppDelegate.swift +++ b/Mos/AppDelegate.swift @@ -97,6 +97,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { LogitechHIDManager.shared.stop() ScrollCore.shared.disable() ButtonCore.shared.disable() + MouseSensitivityManager.shared.disable() } // 检查是否有访问 accessibility 权限, 如果有则启动滚动处理, 并结束计时器 @@ -111,6 +112,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { ScrollCore.shared.enable() ButtonCore.shared.enable() LogitechHIDManager.shared.start() + MouseSensitivityManager.shared.refresh() } } else { if Utils.isHadAccessibilityPermissions() { @@ -118,6 +120,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { ScrollCore.shared.enable() ButtonCore.shared.enable() LogitechHIDManager.shared.start() + MouseSensitivityManager.shared.refresh() } else { // 如果应用不在辅助权限列表内, 则弹出欢迎窗口 WindowManager.shared.showWindow(withIdentifier: WINDOW_IDENTIFIER.introductionWindowController, withTitle: "") @@ -143,6 +146,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { LogitechHIDManager.shared.stop() ScrollCore.shared.disable() ButtonCore.shared.disable() + MouseSensitivityManager.shared.disable() } // 辅助功能权限在运行时被撤销 (可能由多个 Interceptor 同时触发, 此方法必须幂等) @objc func handleAccessibilityPermissionLost() { @@ -152,6 +156,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { LogitechHIDManager.shared.stop() ScrollCore.shared.disable() ButtonCore.shared.disable() + MouseSensitivityManager.shared.disable() Toast.show( NSLocalizedString("Accessibility permission lost, Mos has been paused", comment: ""), style: .warning, diff --git a/Mos/Localizable.xcstrings b/Mos/Localizable.xcstrings index de98fd6f..a178e620 100755 --- a/Mos/Localizable.xcstrings +++ b/Mos/Localizable.xcstrings @@ -389,6 +389,100 @@ } } }, + "button_conflict_detail" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This button appears to be managed by another app (e.g. Logitech Options+). When both apps capture the same button, behavior may be unpredictable. Release it in the other app, or quit that app, to let Mos handle it." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このボタンは他のアプリ (Logitech Options+ など) に管理されているようです。両方のアプリが同じボタンを捕捉すると、動作が不安定になる場合があります。他のアプリでこのボタンを解放するか、そのアプリを終了すると、Mos が処理できるようになります。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 버튼은 다른 앱(예: Logitech Options+)이 관리하는 것으로 보입니다. 두 앱이 같은 버튼을 캡처하면 예기치 않은 동작이 발생할 수 있습니다. 다른 앱에서 이 버튼을 해제하거나 해당 앱을 종료하면 Mos가 처리할 수 있습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按键似乎正被其他应用 (例如 Logitech Options+) 接管。两个应用同时捕获同一按键可能导致行为异常。请在对应应用中释放该按键,或退出该应用,以便由 Mos 处理。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" + } + } + } + }, + "button_conflict_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Button may be managed by another app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このボタンは他のアプリが管理している可能性があります" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 버튼은 다른 앱이 관리할 수 있습니다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按键可能被其他应用接管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵可能被其他應用程式接管" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵可能被其他應用程式接管" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此按鍵可能被其他應用程式接管" + } + } + } + }, "categoryAccessibility" : { "extractionState" : "manual", "localizations" : { @@ -2085,23 +2179,6 @@ "Custom Icon (app icon)" : { "comment" : "Toast debug custom icon checkbox" }, - "custom-shortcut" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Custom…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "自定义…" - } - } - } - }, "custom-recording-prompt" : { "comment" : "Prompt shown on the action button while custom shortcut recording is active", "extractionState" : "manual", @@ -2192,6 +2269,23 @@ } } }, + "custom-shortcut" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Custom…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自定义…" + } + } + } + }, "cut" : { "extractionState" : "manual", "localizations" : { @@ -2644,25 +2738,6 @@ } } }, - "escapeKey" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Escape" } } - } - }, "Duration" : { "comment" : "Toast debug duration label" }, @@ -2755,12 +2830,104 @@ } } }, + "Enable Mouse Sensitivity Adjustment" : { + "comment" : "Enable mouse sensitivity adjustment" + }, "Enter toast message..." : { "comment" : "Toast debug message placeholder" }, "Error style" : { "comment" : "Toast debug quick test message" }, + "escapeKey" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escape" + } + } + } + }, "Event Monitor" : { "extractionState" : "stale", "localizations" : { @@ -2852,6 +3019,15 @@ }, "Exceed max, test eviction" : { "comment" : "Toast debug quick test subtitle" + }, + "Export Settings" : { + "comment" : "Export settings" + }, + "Failed to export settings" : { + + }, + "Failed to import settings" : { + }, "featureNotAvailable" : { "localizations" : { @@ -3412,6 +3588,9 @@ "ℹ️ Info" : { "comment" : "Toast debug style button" }, + "Import Settings" : { + "comment" : "Import settings" + }, "Info style" : { "comment" : "Toast debug quick test message" }, @@ -5554,277 +5733,630 @@ } } }, - "mouseBackClick" : { + "modifierCommand" : { "extractionState" : "manual", "localizations" : { "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zpět" + "value" : "Command" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Zurück" + "value" : "Command" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πίσω" + "value" : "Command" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Back" + "value" : "Command" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Précédent" + "value" : "Command" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "戻る" + "value" : "Command" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "뒤로" + "value" : "Command" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Назад" + "value" : "Command" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geri" + "value" : "Command" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Назад" + "value" : "Command" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "后退" + "value" : "Command" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "上一頁" + "value" : "Command" } }, "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", - "value" : "上一頁" + "value" : "Command" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "上一頁" + "value" : "Command" } } } }, - "mouseForwardClick" : { + "modifierControl" : { "extractionState" : "manual", "localizations" : { "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vpřed" + "value" : "Control" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vorwärts" + "value" : "Control" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μπροστά" + "value" : "Control" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Forward" + "value" : "Control" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Suivant" + "value" : "Control" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "進む" + "value" : "Control" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "앞으로" + "value" : "Control" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вперёд" + "value" : "Control" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İleri" + "value" : "Control" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Вперед" + "value" : "Control" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "前进" + "value" : "Control" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "下一頁" + "value" : "Control" } }, "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", - "value" : "下一頁" + "value" : "Control" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "下一頁" + "value" : "Control" } } } }, - "modifierCommand" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Command" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Command" } } - } - }, - "modifierControl" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Control" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Control" } } - } - }, "modifierFn" : { "extractionState" : "manual", "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Function" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Function" } } - } + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Function" + } + } + } }, "modifierOption" : { "extractionState" : "manual", "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Option" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Option" } } + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Option" + } + } } }, "modifierShift" : { "extractionState" : "manual", "localizations" : { - "cs" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "de" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "el" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "en" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "fr" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "ja" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "ko" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "ru" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "tr" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "uk" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "zh-Hant-HK" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } }, - "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", "value" : "Shift" } } + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + } + } + }, + "Mouse Sensitivity" : { + "comment" : "Mouse sensitivity" + }, + "mouseBackClick" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpět" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zurück" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Πίσω" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Précédent" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "戻る" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "뒤로" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назад" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назад" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "后退" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一頁" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一頁" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一頁" + } + } + } + }, + "mouseForwardClick" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vpřed" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorwärts" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μπροστά" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forward" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suivant" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "進む" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "앞으로" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вперёд" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleri" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вперед" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "前进" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一頁" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一頁" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一頁" + } + } } }, "mouseLeftClick" : { @@ -8253,6 +8785,15 @@ }, "SEND TOAST" : { "comment" : "Toast debug section header" + }, + "Settings Backup" : { + "comment" : "Settings backup" + }, + "Settings exported successfully" : { + + }, + "Settings imported successfully" : { + }, "Show each style" : { "comment" : "Toast debug quick test subtitle" @@ -9904,101 +10445,7 @@ } } } - }, - "button_conflict_title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Button may be managed by another app" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "このボタンは他のアプリが管理している可能性があります" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "이 버튼은 다른 앱이 관리할 수 있습니다" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按键可能被其他应用接管" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵可能被其他應用程式接管" - } - }, - "zh-Hant-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵可能被其他應用程式接管" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵可能被其他應用程式接管" - } - } - } - }, - "button_conflict_detail" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This button appears to be managed by another app (e.g. Logitech Options+). When both apps capture the same button, behavior may be unpredictable. Release it in the other app, or quit that app, to let Mos handle it." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "このボタンは他のアプリ (Logitech Options+ など) に管理されているようです。両方のアプリが同じボタンを捕捉すると、動作が不安定になる場合があります。他のアプリでこのボタンを解放するか、そのアプリを終了すると、Mos が処理できるようになります。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "이 버튼은 다른 앱(예: Logitech Options+)이 관리하는 것으로 보입니다. 두 앱이 같은 버튼을 캡처하면 예기치 않은 동작이 발생할 수 있습니다. 다른 앱에서 이 버튼을 해제하거나 해당 앱을 종료하면 Mos가 처리할 수 있습니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按键似乎正被其他应用 (例如 Logitech Options+) 接管。两个应用同时捕获同一按键可能导致行为异常。请在对应应用中释放该按键,或退出该应用,以便由 Mos 处理。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" - } - }, - "zh-Hant-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "此按鍵似乎正被其他應用程式 (例如 Logitech Options+) 接管。兩個應用程式同時捕捉同一按鍵可能導致行為異常。請在對應應用程式中釋放該按鍵,或退出該應用程式,以便由 Mos 處理。" - } - } - } } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Mos/Managers/MouseSensitivityManager.swift b/Mos/Managers/MouseSensitivityManager.swift new file mode 100644 index 00000000..76a83b7f --- /dev/null +++ b/Mos/Managers/MouseSensitivityManager.swift @@ -0,0 +1,103 @@ +// +// MouseSensitivityManager.swift +// Mos +// 鼠标灵敏度管理器 +// Created by Mos on 2026/4/25. +// + +import Cocoa + +class MouseSensitivityManager { + static let shared = MouseSensitivityManager() + + private var isActive = false + private var mouseMoveInterceptor: Interceptor? + + private static let mouseMoveEventMask: CGEventMask = + (CGEventMask(1 << CGEventType.mouseMoved.rawValue)) | + (CGEventMask(1 << CGEventType.leftMouseDragged.rawValue)) | + (CGEventMask(1 << CGEventType.rightMouseDragged.rawValue)) | + (CGEventMask(1 << CGEventType.otherMouseDragged.rawValue)) + + private static let mouseMoveEventCallback: CGEventTapCallBack = { _, type, event, _ in + MouseSensitivityManager.shared.handleMouseMoveEvent(type: type, event: event) + } + + init() { + NSLog("Module initialized: MouseSensitivityManager") + } + + func enable() { + if isActive { return } + isActive = true + + do { + mouseMoveInterceptor = try Interceptor( + event: Self.mouseMoveEventMask, + handleBy: Self.mouseMoveEventCallback, + listenOn: .cgAnnotatedSessionEventTap, + placeAt: .tailAppendEventTap, + for: .defaultTap + ) + mouseMoveInterceptor?.onRestart = { [weak self] in + self?.restartIfNeeded() + } + mouseMoveInterceptor?.shouldRestart = { [weak self] in + self?.isActive ?? false + } + } catch { + NSLog("MouseSensitivityManager: Failed to create interceptor: \(error)") + isActive = false + } + } + + func disable() { + if !isActive { return } + isActive = false + + mouseMoveInterceptor?.stop() + mouseMoveInterceptor = nil + } + + func refresh() { + if Options.shared.mouse.enableSensitivity { + enable() + } else { + disable() + } + } + + private func restartIfNeeded() { + if isActive { + mouseMoveInterceptor?.restart() + } + } + + private func handleMouseMoveEvent(type: CGEventType, event: CGEvent) -> Unmanaged? { + if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { + restartIfNeeded() + return Unmanaged.passUnretained(event) + } + + guard Options.shared.mouse.enableSensitivity else { + return Unmanaged.passUnretained(event) + } + + let sensitivity = Options.shared.mouse.sensitivity + + guard sensitivity != 1.0 else { + return Unmanaged.passUnretained(event) + } + + let deltaX = event.getIntegerValueField(.mouseEventDeltaX) + let deltaY = event.getIntegerValueField(.mouseEventDeltaY) + + let adjustedDeltaX = Int64(Double(deltaX) * sensitivity) + let adjustedDeltaY = Int64(Double(deltaY) * sensitivity) + + event.setIntegerValueField(.mouseEventDeltaX, value: adjustedDeltaX) + event.setIntegerValueField(.mouseEventDeltaY, value: adjustedDeltaY) + + return Unmanaged.passUnretained(event) + } +} diff --git a/Mos/Managers/SettingsBackupManager.swift b/Mos/Managers/SettingsBackupManager.swift new file mode 100644 index 00000000..0763d7ec --- /dev/null +++ b/Mos/Managers/SettingsBackupManager.swift @@ -0,0 +1,250 @@ +// +// SettingsBackupManager.swift +// Mos +// 设置备份导入导出管理器 +// Created by Mos on 2026/4/25. +// + +import Cocoa + +struct SettingsBackup: Codable { + let version: Int + let timestamp: Date + + let general: GeneralBackup + let update: UpdateBackup + let scroll: ScrollBackup + let buttons: ButtonsBackup + let application: ApplicationBackup + let mouse: MouseBackup + + struct GeneralBackup: Codable { + let hideStatusItem: Bool + } + + struct UpdateBackup: Codable { + let checkOnAppStart: Bool + let includingBetaVersion: Bool + } + + struct ScrollBackup: Codable { + let smooth: Bool + let reverse: Bool + let reverseVertical: Bool + let reverseHorizontal: Bool + let dash: ScrollHotkey? + let toggle: ScrollHotkey? + let block: ScrollHotkey? + let step: Double + let speed: Double + let duration: Double + let deadZone: Double + let smoothSimTrackpad: Bool + let smoothVertical: Bool + let smoothHorizontal: Bool + let durationBeforeSimTrackpadLock: Double? + } + + struct ButtonsBackup: Codable { + let binding: [ButtonBinding] + } + + struct ApplicationBackup: Codable { + let allowlist: Bool + let applications: [Application] + } + + struct MouseBackup: Codable { + let enableSensitivity: Bool + let sensitivity: Double + } +} + +class SettingsBackupManager { + static let shared = SettingsBackupManager() + + private let currentVersion = 1 + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + init() { + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + encoder.dateEncodingStrategy = .iso8601 + decoder.dateDecodingStrategy = .iso8601 + } + + func exportSettings() -> Bool { + let backup = createBackup() + + let savePanel = NSSavePanel() + savePanel.canCreateDirectories = true + savePanel.nameFieldStringValue = generateDefaultFilename() + // 兼容 macOS 10.13: allowedContentTypes 是 macOS 11.0+ 的 API + // 使用 allowedFileTypes 替代 + savePanel.allowedFileTypes = ["json"] + + let response = savePanel.runModal() + + guard response == .OK, let url = savePanel.url else { + return false + } + + do { + let data = try encoder.encode(backup) + try data.write(to: url) + + Toast.show( + NSLocalizedString("Settings exported successfully", comment: ""), + style: .success, + duration: 3.0 + ) + + return true + } catch { + NSLog("Failed to export settings: \(error)") + + Toast.show( + NSLocalizedString("Failed to export settings", comment: ""), + style: .error, + duration: 3.0 + ) + + return false + } + } + + func importSettings() -> Bool { + let openPanel = NSOpenPanel() + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + // 兼容 macOS 10.13: allowedContentTypes 是 macOS 11.0+ 的 API + // 使用 allowedFileTypes 替代 + openPanel.allowedFileTypes = ["json"] + + let response = openPanel.runModal() + + guard response == .OK, let url = openPanel.url else { + return false + } + + do { + let data = try Data(contentsOf: url) + let backup = try decoder.decode(SettingsBackup.self, from: data) + + applyBackup(backup) + + Toast.show( + NSLocalizedString("Settings imported successfully", comment: ""), + style: .success, + duration: 3.0 + ) + + return true + } catch { + NSLog("Failed to import settings: \(error)") + + Toast.show( + NSLocalizedString("Failed to import settings", comment: ""), + style: .error, + duration: 3.0 + ) + + return false + } + } + + private func createBackup() -> SettingsBackup { + return SettingsBackup( + version: currentVersion, + timestamp: Date(), + general: SettingsBackup.GeneralBackup( + hideStatusItem: Options.shared.general.hideStatusItem + ), + update: SettingsBackup.UpdateBackup( + checkOnAppStart: Options.shared.update.checkOnAppStart, + includingBetaVersion: Options.shared.update.includingBetaVersion + ), + scroll: SettingsBackup.ScrollBackup( + smooth: Options.shared.scroll.smooth, + reverse: Options.shared.scroll.reverse, + reverseVertical: Options.shared.scroll.reverseVertical, + reverseHorizontal: Options.shared.scroll.reverseHorizontal, + dash: Options.shared.scroll.dash, + toggle: Options.shared.scroll.toggle, + block: Options.shared.scroll.block, + step: Options.shared.scroll.step, + speed: Options.shared.scroll.speed, + duration: Options.shared.scroll.duration, + deadZone: Options.shared.scroll.deadZone, + smoothSimTrackpad: Options.shared.scroll.smoothSimTrackpad, + smoothVertical: Options.shared.scroll.smoothVertical, + smoothHorizontal: Options.shared.scroll.smoothHorizontal, + durationBeforeSimTrackpadLock: Options.shared.scroll.durationBeforeSimTrackpadLock + ), + buttons: SettingsBackup.ButtonsBackup( + binding: Options.shared.buttons.binding + ), + application: SettingsBackup.ApplicationBackup( + allowlist: Options.shared.application.allowlist, + applications: Options.shared.application.applications.allElements + ), + mouse: SettingsBackup.MouseBackup( + enableSensitivity: Options.shared.mouse.enableSensitivity, + sensitivity: Options.shared.mouse.sensitivity + ) + ) + } + + private func applyBackup(_ backup: SettingsBackup) { + Options.shared.general.hideStatusItem = backup.general.hideStatusItem + Options.shared.update.checkOnAppStart = backup.update.checkOnAppStart + Options.shared.update.includingBetaVersion = backup.update.includingBetaVersion + + Options.shared.scroll.smooth = backup.scroll.smooth + Options.shared.scroll.reverse = backup.scroll.reverse + Options.shared.scroll.reverseVertical = backup.scroll.reverseVertical + Options.shared.scroll.reverseHorizontal = backup.scroll.reverseHorizontal + Options.shared.scroll.dash = backup.scroll.dash + Options.shared.scroll.toggle = backup.scroll.toggle + Options.shared.scroll.block = backup.scroll.block + Options.shared.scroll.step = backup.scroll.step + Options.shared.scroll.speed = backup.scroll.speed + Options.shared.scroll.duration = backup.scroll.duration + Options.shared.scroll.deadZone = backup.scroll.deadZone + Options.shared.scroll.smoothSimTrackpad = backup.scroll.smoothSimTrackpad + Options.shared.scroll.smoothVertical = backup.scroll.smoothVertical + Options.shared.scroll.smoothHorizontal = backup.scroll.smoothHorizontal + Options.shared.scroll.durationBeforeSimTrackpadLock = backup.scroll.durationBeforeSimTrackpadLock + + Options.shared.buttons.binding = backup.buttons.binding + + Options.shared.application.allowlist = backup.application.allowlist + Options.shared.application.applications = EnhanceArray( + withArray: backup.application.applications, + matchKey: "path", + forObserver: { Options.shared.saveOptions() } + ) + + Options.shared.mouse.enableSensitivity = backup.mouse.enableSensitivity + Options.shared.mouse.sensitivity = backup.mouse.sensitivity + + Options.shared.saveOptions() + + MouseSensitivityManager.shared.refresh() + LogitechHIDManager.shared.syncDivertWithBindings() + + NotificationCenter.default.post(name: .mosSettingsImported, object: nil) + } + + private func generateDefaultFilename() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd-HHmmss" + let dateString = dateFormatter.string(from: Date()) + return "mos-settings-backup-\(dateString)" + } +} + +extension Notification.Name { + static let mosSettingsImported = Notification.Name("mosSettingsImported") +} diff --git a/Mos/Options/Options.swift b/Mos/Options/Options.swift index 9aef3a0c..08d78977 100644 --- a/Mos/Options/Options.swift +++ b/Mos/Options/Options.swift @@ -46,6 +46,11 @@ struct OptionItem { static let Allowlist = "allowlist" static let Applications = "applications" } + + struct Mouse { + static let Sensitivity = "mouseSensitivity" + static let EnableSensitivity = "enableMouseSensitivity" + } } class Options { @@ -76,6 +81,10 @@ class Options { var application = OPTIONS_APPLICATION_DEFAULT() { didSet { Options.shared.saveOptions() } } + // 鼠标 + var mouse = OPTIONS_MOUSE_DEFAULT() { + didSet { Options.shared.saveOptions() } + } } /** @@ -136,6 +145,13 @@ extension Options { // 应用 application.allowlist = UserDefaults.standard.bool(forKey: OptionItem.Application.Allowlist) application.applications = loadApplicationsData() + // 鼠标 + mouse.enableSensitivity = UserDefaults.standard.bool(forKey: OptionItem.Mouse.EnableSensitivity) + if let storedSensitivity = UserDefaults.standard.object(forKey: OptionItem.Mouse.Sensitivity) as? Double { + mouse.sensitivity = storedSensitivity + } else { + mouse.sensitivity = OPTIONS_MOUSE_DEFAULT().sensitivity + } // 解锁 readingOptionsLock = false } @@ -174,6 +190,9 @@ extension Options { } // 按钮绑定 saveButtonBindingsData() + // 鼠标 + UserDefaults.standard.set(mouse.enableSensitivity, forKey: OptionItem.Mouse.EnableSensitivity) + UserDefaults.standard.set(mouse.sensitivity, forKey: OptionItem.Mouse.Sensitivity) } } diff --git a/Mos/Utils/Constants.swift b/Mos/Utils/Constants.swift index d44cbe42..db348161 100644 --- a/Mos/Utils/Constants.swift +++ b/Mos/Utils/Constants.swift @@ -217,6 +217,16 @@ class OPTIONS_APPLICATION_DEFAULT { ) } +// 鼠标设置 +class OPTIONS_MOUSE_DEFAULT: Codable { + var enableSensitivity = false { + didSet { Options.shared.saveOptions() } + } + var sensitivity = 1.0 { + didSet { Options.shared.saveOptions() } + } +} + // MARK: - Notification Names extension Notification.Name { /// 辅助功能权限在运行时被撤销 diff --git a/Mos/Utils/EnhanceArray.swift b/Mos/Utils/EnhanceArray.swift index 85392c4d..3fd54465 100644 --- a/Mos/Utils/EnhanceArray.swift +++ b/Mos/Utils/EnhanceArray.swift @@ -42,6 +42,13 @@ extension EnhanceArray { return array.count } } + + // 获取所有元素的数组副本 + var allElements: [T] { + get { + return array + } + } // 获取值 public func get(by key: String?) -> T? { guard let validKey = key, let index = dictionary[validKey] else { return nil } diff --git a/Mos/Windows/PreferencesWindow/GeneralView/PreferencesGeneralViewController.swift b/Mos/Windows/PreferencesWindow/GeneralView/PreferencesGeneralViewController.swift index 38810edb..d8dae252 100644 --- a/Mos/Windows/PreferencesWindow/GeneralView/PreferencesGeneralViewController.swift +++ b/Mos/Windows/PreferencesWindow/GeneralView/PreferencesGeneralViewController.swift @@ -14,11 +14,166 @@ class PreferencesGeneralViewController: NSViewController { @IBOutlet weak var launchOnLoginCheckBox: NSButton! @IBOutlet weak var hideStatusBarIconCheckBox: NSButton! + // Mouse Sensitivity UI Elements (created programmatically) + private var mouseSensitivityCheckBox: NSButton! + private var mouseSensitivityLabel: NSTextField! + private var mouseSensitivitySlider: NSSlider! + private var mouseSensitivityInput: NSTextField! + private var mouseSensitivityStepper: NSStepper! + private var mouseSensitivityValueLabel: NSTextField! + + // Backup/Restore UI Elements + private var exportSettingsButton: NSButton! + private var importSettingsButton: NSButton! + + // Layout constraints + private var dynamicConstraints: [NSLayoutConstraint] = [] + + // Separators (kept as properties to prevent deallocation) + private var separator1: NSBox! + private var separator2: NSBox! + private var backupLabel: NSTextField! + override func viewDidLoad() { + super.viewDidLoad() + // 创建动态 UI 元素 + createMouseSensitivityUI() + createBackupUI() // 读取设置 syncViewWithOptions() } + // 创建鼠标灵敏度设置 UI + private func createMouseSensitivityUI() { + // 分隔线 + separator1 = NSBox() + separator1.boxType = .separator + separator1.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(separator1) + + // 鼠标灵敏度复选框 + mouseSensitivityCheckBox = NSButton(checkboxWithTitle: NSLocalizedString("Enable Mouse Sensitivity Adjustment", comment: "Enable mouse sensitivity adjustment"), target: self, action: #selector(mouseSensitivityCheckBoxClick(_:))) + mouseSensitivityCheckBox.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(mouseSensitivityCheckBox) + + // 鼠标灵敏度标签 + mouseSensitivityLabel = NSTextField(labelWithString: NSLocalizedString("Mouse Sensitivity", comment: "Mouse sensitivity")) + mouseSensitivityLabel.translatesAutoresizingMaskIntoConstraints = false + mouseSensitivityLabel.isBezeled = false + mouseSensitivityLabel.isEditable = false + mouseSensitivityLabel.drawsBackground = false + view.addSubview(mouseSensitivityLabel) + + // 鼠标灵敏度滑块 + mouseSensitivitySlider = NSSlider(value: 1.0, minValue: 0.1, maxValue: 5.0, target: self, action: #selector(mouseSensitivitySliderChange(_:))) + mouseSensitivitySlider.translatesAutoresizingMaskIntoConstraints = false + // 注意: allowsTickMarks 是 macOS 10.15+ 的 API,为了兼容 macOS 10.13 + // 在旧版本中,设置 numberOfTickMarks 就会自动显示刻度标记 + mouseSensitivitySlider.numberOfTickMarks = 10 + view.addSubview(mouseSensitivitySlider) + + // 鼠标灵敏度输入框 + mouseSensitivityInput = NSTextField(string: "1.00") + mouseSensitivityInput.translatesAutoresizingMaskIntoConstraints = false + mouseSensitivityInput.delegate = self + mouseSensitivityInput.refusesFirstResponder = true + mouseSensitivityInput.alignment = .center + mouseSensitivityInput.formatter = NumberFormatter() + if let formatter = mouseSensitivityInput.formatter as? NumberFormatter { + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + formatter.minimum = 0.1 + formatter.maximum = 5.0 + } + view.addSubview(mouseSensitivityInput) + + // 鼠标灵敏度值标签 (显示当前值) + mouseSensitivityValueLabel = NSTextField(labelWithString: "1.00x") + mouseSensitivityValueLabel.translatesAutoresizingMaskIntoConstraints = false + mouseSensitivityValueLabel.isBezeled = false + mouseSensitivityValueLabel.isEditable = false + mouseSensitivityValueLabel.drawsBackground = false + mouseSensitivityValueLabel.alignment = .center + mouseSensitivityValueLabel.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize, weight: .regular) + view.addSubview(mouseSensitivityValueLabel) + + // 布局约束 + NSLayoutConstraint.activate([ + separator1.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + separator1.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + separator1.topAnchor.constraint(equalTo: hideStatusBarIconCheckBox.bottomAnchor, constant: 20), + + mouseSensitivityCheckBox.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + mouseSensitivityCheckBox.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: 20), + + mouseSensitivityLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + mouseSensitivityLabel.topAnchor.constraint(equalTo: mouseSensitivityCheckBox.bottomAnchor, constant: 16), + mouseSensitivityLabel.widthAnchor.constraint(equalToConstant: 120), + + mouseSensitivitySlider.leadingAnchor.constraint(equalTo: mouseSensitivityLabel.trailingAnchor, constant: 8), + mouseSensitivitySlider.centerYAnchor.constraint(equalTo: mouseSensitivityLabel.centerYAnchor), + mouseSensitivitySlider.widthAnchor.constraint(equalToConstant: 200), + + mouseSensitivityValueLabel.leadingAnchor.constraint(equalTo: mouseSensitivitySlider.trailingAnchor, constant: 12), + mouseSensitivityValueLabel.centerYAnchor.constraint(equalTo: mouseSensitivityLabel.centerYAnchor), + mouseSensitivityValueLabel.widthAnchor.constraint(equalToConstant: 60), + ]) + } + + // 创建备份/恢复 UI + private func createBackupUI() { + // 分隔线 + separator2 = NSBox() + separator2.boxType = .separator + separator2.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(separator2) + + // 备份/恢复标签 + backupLabel = NSTextField(labelWithString: NSLocalizedString("Settings Backup", comment: "Settings backup")) + backupLabel.translatesAutoresizingMaskIntoConstraints = false + backupLabel.isBezeled = false + backupLabel.isEditable = false + backupLabel.drawsBackground = false + backupLabel.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) + view.addSubview(backupLabel) + + // 导出设置按钮 + exportSettingsButton = NSButton(title: NSLocalizedString("Export Settings", comment: "Export settings"), target: self, action: #selector(exportSettingsClick(_:))) + exportSettingsButton.translatesAutoresizingMaskIntoConstraints = false + exportSettingsButton.bezelStyle = .rounded + view.addSubview(exportSettingsButton) + + // 导入设置按钮 + importSettingsButton = NSButton(title: NSLocalizedString("Import Settings", comment: "Import settings"), target: self, action: #selector(importSettingsClick(_:))) + importSettingsButton.translatesAutoresizingMaskIntoConstraints = false + importSettingsButton.bezelStyle = .rounded + view.addSubview(importSettingsButton) + + // 布局约束 + NSLayoutConstraint.activate([ + separator2.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + separator2.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + separator2.topAnchor.constraint(equalTo: mouseSensitivitySlider.bottomAnchor, constant: 24), + + backupLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + backupLabel.topAnchor.constraint(equalTo: separator2.bottomAnchor, constant: 20), + + exportSettingsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + exportSettingsButton.topAnchor.constraint(equalTo: backupLabel.bottomAnchor, constant: 12), + exportSettingsButton.widthAnchor.constraint(equalToConstant: 120), + + importSettingsButton.leadingAnchor.constraint(equalTo: exportSettingsButton.trailingAnchor, constant: 12), + importSettingsButton.topAnchor.constraint(equalTo: exportSettingsButton.topAnchor), + importSettingsButton.widthAnchor.constraint(equalToConstant: 120), + ]) + + // 设置视图的底部约束,确保窗口可以正确调整大小 + let bottomConstraint = view.bottomAnchor.constraint(greaterThanOrEqualTo: exportSettingsButton.bottomAnchor, constant: 20) + bottomConstraint.priority = .defaultHigh + bottomConstraint.isActive = true + } + // 自启 @IBAction func launchOnLoginClick(_ sender: NSButton) { Options.shared.general.autoLaunch = sender.state.rawValue==0 ? false : true @@ -30,6 +185,37 @@ class PreferencesGeneralViewController: NSViewController { Options.shared.general.hideStatusItem = sender.state.rawValue==0 ? false : true syncViewWithOptions() } + + // 鼠标灵敏度启用/禁用 + @objc func mouseSensitivityCheckBoxClick(_ sender: NSButton) { + Options.shared.mouse.enableSensitivity = sender.state.rawValue != 0 + MouseSensitivityManager.shared.refresh() + syncViewWithOptions() + } + + // 鼠标灵敏度滑块变化 + @objc func mouseSensitivitySliderChange(_ sender: NSSlider) { + setMouseSensitivity(value: sender.doubleValue) + } + + // 设置鼠标灵敏度 + func setMouseSensitivity(value: Double) { + let clampedValue = max(0.1, min(5.0, value)) + Options.shared.mouse.sensitivity = clampedValue + syncViewWithOptions() + } + + // 导出设置 + @objc func exportSettingsClick(_ sender: NSButton) { + SettingsBackupManager.shared.exportSettings() + } + + // 导入设置 + @objc func importSettingsClick(_ sender: NSButton) { + if SettingsBackupManager.shared.importSettings() { + syncViewWithOptions() + } + } } /** @@ -42,5 +228,34 @@ extension PreferencesGeneralViewController { launchOnLoginCheckBox.state = NSControl.StateValue(rawValue: Options.shared.general.autoLaunch ? 1 : 0) // 隐藏 hideStatusBarIconCheckBox.state = NSControl.StateValue(rawValue: Options.shared.general.hideStatusItem ? 1 : 0) + + // 鼠标灵敏度 + let enableSensitivity = Options.shared.mouse.enableSensitivity + mouseSensitivityCheckBox.state = NSControl.StateValue(rawValue: enableSensitivity ? 1 : 0) + + let sensitivity = Options.shared.mouse.sensitivity + mouseSensitivitySlider.doubleValue = sensitivity + mouseSensitivitySlider.isEnabled = enableSensitivity + mouseSensitivityInput.stringValue = String(format: "%.2f", sensitivity) + mouseSensitivityValueLabel.stringValue = String(format: "%.2fx", sensitivity) + + mouseSensitivityLabel.textColor = enableSensitivity ? NSColor.controlTextColor : NSColor.disabledControlTextColor + } +} + +/** + * NSTextFieldDelegate + **/ +extension PreferencesGeneralViewController: NSTextFieldDelegate { + func controlTextDidEndEditing(_ obj: Notification) { + guard let textField = obj.object as? NSTextField else { return } + + if textField === mouseSensitivityInput { + if let value = Double(textField.stringValue) { + setMouseSensitivity(value: value) + } else { + syncViewWithOptions() + } + } } }