diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj index 08fefbc8..d299d99c 100644 --- a/alt-tab-macos.xcodeproj/project.pbxproj +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -215,6 +215,7 @@ D04BA03B560B681AF96E4610 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA88ED495C54CFE0B7B87 /* InfoPlist.strings */; }; D04BA084CD1236EC78D90A01 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BACCBE5F97BE9B6CA645B /* Localizable.strings */; }; D04BA0AE9865276FF8EF5DEB /* UserDefaultsEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */; }; + D04BB101A1B2C3D4E5F60011 /* InputSourceEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BB102A1B2C3D4E5F60011 /* InputSourceEvents.swift */; }; D04BA0DD6819010987B578D1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA07BBF2237CC6C72D264 /* Localizable.strings */; }; D04BA100BD0F47828EB649FF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAAEC2847830A3991F8D1 /* InfoPlist.strings */; }; D04BA11E56383D082D7BE5A5 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA998119CAA8B70A2B67 /* TilesView.swift */; }; @@ -642,6 +643,7 @@ D04BA5C8373D1480280B71A6 /* tr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = tr; path = InfoPlist.strings; sourceTree = ""; }; D04BA5D2FD6E26D82721C574 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = InfoPlist.strings; sourceTree = ""; }; D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsEvents.swift; sourceTree = ""; }; + D04BB102A1B2C3D4E5F60011 /* InputSourceEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSourceEvents.swift; sourceTree = ""; }; D04BA5DB4FB71B583A28EF4F /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = InfoPlist.strings; sourceTree = ""; }; D04BA5E208CBB8E28EE3FAFF /* site.webmanifest */ = {isa = PBXFileReference; lastKnownFileType = file.webmanifest; path = site.webmanifest; sourceTree = ""; }; D04BA60A6D08C041F5BF8CC9 /* SF-Pro-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "SF-Pro-Text-Regular.otf"; sourceTree = ""; }; @@ -2002,6 +2004,7 @@ D04BA4A621A8A15660F973C1 /* KeyboardEvents.swift */, D04BA76AEE37D6656EB80126 /* RunningApplicationsEvents.swift */, D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */, + D04BB102A1B2C3D4E5F60011 /* InputSourceEvents.swift */, 5FA1E0B12F50000100A1A1A1 /* PreferencesEvents.swift */, 5883F0A729A5182C0071DB65 /* TrackpadEvents.swift */, BF0C870C14C20C936BA4AF40 /* DockEvents.swift */, @@ -2569,6 +2572,7 @@ D04BA1766FBCB2E941D081A5 /* RunningApplicationsEvents.swift in Sources */, 5F906A342F16A18500B99924 /* Screens.swift in Sources */, D04BA0AE9865276FF8EF5DEB /* UserDefaultsEvents.swift in Sources */, + D04BB101A1B2C3D4E5F60011 /* InputSourceEvents.swift in Sources */, 5FA1E0B22F50000100A1A1A1 /* PreferencesEvents.swift in Sources */, D04BA4A11F821548EE3C5E95 /* Bash.swift in Sources */, 5FBB24D32F389A0400BF7E14 /* Benchmark.swift in Sources */, diff --git a/src/logic/DebugProfile.swift b/src/logic/DebugProfile.swift index eae6fc49..96b3a115 100644 --- a/src/logic/DebugProfile.swift +++ b/src/logic/DebugProfile.swift @@ -107,12 +107,7 @@ class DebugProfile { static func inputSource() -> String { let r = DispatchQueue.main.sync { - if let inputSource = TISCopyCurrentKeyboardInputSource()?.takeUnretainedValue(), - let localizedNamePointer = TISGetInputSourceProperty(inputSource, kTISPropertyLocalizedName) { - let localizedName = Unmanaged.fromOpaque(localizedNamePointer).takeUnretainedValue() - return localizedName as? String ?? "" - } - return "" + InputSourceEvents.currentInputSource() } return r } diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index 86e4ebe9..4a6f371d 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -3,80 +3,57 @@ import Carbon.HIToolbox.Events import ShortcutRecorder class Preferences { - // default values - static var defaultValues: [String: String] = { - var values: [String: String] = [ - "shortcutCount": "2", - "holdShortcut": "⌥", - "holdShortcut2": "⌥", - "nextWindowShortcut": "⇥", - "nextWindowShortcut2": keyAboveTabDependingOnInputSource(), - "nextWindowGesture": GesturePreference.disabled.indexAsString, - "focusWindowShortcut": returnKeyEquivalent(), - "previousWindowShortcut": "⇧", - "cancelShortcut": "⎋", - "lockSearchShortcut": "Space", - "closeWindowShortcut": "W", - "minDeminWindowShortcut": "M", - "toggleFullscreenWindowShortcut": "F", - "quitAppShortcut": "Q", - "hideShowAppShortcut": "H", - "searchShortcut": "S", - "arrowKeysEnabled": "true", - "vimKeysEnabled": "false", - "mouseHoverEnabled": "false", - "cursorFollowFocus": CursorFollowFocus.never.indexAsString, - "showMinimizedWindows": ShowHowPreference.show.indexAsString, - "showMinimizedWindows2": ShowHowPreference.show.indexAsString, - "showHiddenWindows": ShowHowPreference.show.indexAsString, - "showHiddenWindows2": ShowHowPreference.show.indexAsString, - "showFullscreenWindows": ShowHowPreference.show.indexAsString, - "showFullscreenWindows2": ShowHowPreference.show.indexAsString, - "showWindowlessApps": ShowHowPreference.showAtTheEnd.indexAsString, - "showWindowlessApps2": ShowHowPreference.showAtTheEnd.indexAsString, - "windowOrder": WindowOrderPreference.recentlyFocused.indexAsString, - "windowOrder2": WindowOrderPreference.recentlyFocused.indexAsString, - "showTabsAsWindows": "false", - "hideColoredCircles": "false", - "windowDisplayDelay": "100", - "appearanceStyle": AppearanceStylePreference.thumbnails.indexAsString, - "appearanceSize": AppearanceSizePreference.auto.indexAsString, - "appearanceTheme": AppearanceThemePreference.system.indexAsString, - "theme": ThemePreference.macOs.indexAsString, - "showOnScreen": ShowOnScreenPreference.active.indexAsString, - "titleTruncation": TitleTruncationPreference.end.indexAsString, - "alignThumbnails": AlignThumbnailsPreference.center.indexAsString, - "showAppsOrWindows": ShowAppsOrWindowsPreference.windows.indexAsString, - "showTitles": ShowTitlesPreference.windowTitle.indexAsString, - "appsToShow": AppsToShowPreference.all.indexAsString, - "appsToShow2": AppsToShowPreference.active.indexAsString, - "spacesToShow": SpacesToShowPreference.all.indexAsString, - "spacesToShow2": SpacesToShowPreference.all.indexAsString, - "screensToShow": ScreensToShowPreference.all.indexAsString, - "screensToShow2": ScreensToShowPreference.all.indexAsString, - "fadeOutAnimation": "false", - "previewFadeInAnimation": "true", - "hideSpaceNumberLabels": "false", - "hideStatusIcons": "false", - "startAtLogin": "true", - "menubarIcon": MenubarIconPreference.outlined.indexAsString, - "menubarIconShown": "true", - "language": LanguagePreference.systemDefault.indexAsString, - "exceptions": defaultExceptions(), - "updatePolicy": UpdatePolicyPreference.autoCheck.indexAsString, - "crashPolicy": CrashPolicyPreference.ask.indexAsString, - "shortcutStyle": ShortcutStylePreference.focusOnRelease.indexAsString, - "shortcutStyle2": ShortcutStylePreference.focusOnRelease.indexAsString, - "hideAppBadges": "false", - "hideThumbnails": "false", - "previewFocusedWindow": "false", - "screenRecordingPermissionSkipped": "false", - "trackpadHapticFeedbackEnabled": "true", - "settingsWindowShownOnFirstLaunch": "false", + static var defaultValues: [String: Any] = { + var values: [String: Any] = [ + "shortcutCount": "2", + "nextWindowGesture": GesturePreference.disabled.indexAsString, + "focusWindowShortcut": defaultShortcut(returnKeyEquivalent()), + "previousWindowShortcut": defaultShortcut("⇧"), + "cancelShortcut": defaultShortcut("⎋"), + "lockSearchShortcut": defaultShortcut("Space"), + "closeWindowShortcut": defaultShortcut("W"), + "minDeminWindowShortcut": defaultShortcut("M"), + "toggleFullscreenWindowShortcut": defaultShortcut("F"), + "quitAppShortcut": defaultShortcut("Q"), + "hideShowAppShortcut": defaultShortcut("H"), + "searchShortcut": defaultShortcut("S"), + "arrowKeysEnabled": "true", + "vimKeysEnabled": "false", + "mouseHoverEnabled": "false", + "cursorFollowFocus": CursorFollowFocus.never.indexAsString, + "showTabsAsWindows": "false", + "hideColoredCircles": "false", + "windowDisplayDelay": "100", + "appearanceStyle": AppearanceStylePreference.thumbnails.indexAsString, + "appearanceSize": AppearanceSizePreference.auto.indexAsString, + "appearanceTheme": AppearanceThemePreference.system.indexAsString, + "theme": ThemePreference.macOs.indexAsString, + "showOnScreen": ShowOnScreenPreference.active.indexAsString, + "titleTruncation": TitleTruncationPreference.end.indexAsString, + "alignThumbnails": AlignThumbnailsPreference.center.indexAsString, + "showAppsOrWindows": ShowAppsOrWindowsPreference.windows.indexAsString, + "showTitles": ShowTitlesPreference.windowTitle.indexAsString, + "fadeOutAnimation": "false", + "previewFadeInAnimation": "true", + "startAtLogin": "true", + "menubarIcon": MenubarIconPreference.outlined.indexAsString, + "menubarIconShown": "true", + "language": LanguagePreference.systemDefault.indexAsString, + "exceptions": defaultExceptions(), + "updatePolicy": UpdatePolicyPreference.autoCheck.indexAsString, + "crashPolicy": CrashPolicyPreference.ask.indexAsString, + "hideAppBadges": "false", + "hideThumbnails": "false", + "hideSpaceNumberLabels": "false", + "hideStatusIcons": "false", + "previewFocusedWindow": "false", + "screenRecordingPermissionSkipped": "false", + "trackpadHapticFeedbackEnabled": "true", + "settingsWindowShownOnFirstLaunch": "false", ] (0.. [String: Any] { + shortcutStorage(shortcutFromKeyEquivalent(keyEquivalent), keyEquivalent) + } + + static func setShortcut(_ key: String, _ shortcut: Shortcut?, _ notify: Bool = true) { + setShortcut(key, shortcut, stringRepresentation: nil, notify) + } + + static func setShortcut(_ key: String, _ shortcut: Shortcut?, stringRepresentation: String?, _ notify: Bool = true) { + UserDefaults.standard.set(shortcutStorage(shortcut, stringRepresentation), forKey: key) + CachedUserDefaults.removeFromCache(key) + if notify { + PreferencesEvents.preferenceChanged(key) + } + } + + static func setShortcut(_ key: String, keyEquivalent: String, _ notify: Bool = true) { + setShortcut(key, shortcutFromKeyEquivalent(keyEquivalent), stringRepresentation: keyEquivalent, notify) + } + + static func shortcut(_ key: String) -> Shortcut? { + CachedUserDefaults.shortcut(key) + } + static func set(_ key: String, _ value: T, _ notify: Bool = true) where T: Encodable { UserDefaults.standard.set(key == "exceptions" ? jsonEncode(value) : value, forKey: key) CachedUserDefaults.removeFromCache(key) @@ -238,6 +249,40 @@ class Preferences { return String(data: try! JSONEncoder().encode(value), encoding: .utf8)! } + static func archiveShortcut(_ shortcut: Shortcut?) -> Data { + if #available(macOS 10.13, *) { + return try! NSKeyedArchiver.archivedData(withRootObject: shortcut ?? emptyShortcut, requiringSecureCoding: true) + } + return NSKeyedArchiver.archivedData(withRootObject: shortcut ?? emptyShortcut) + } + + static func shortcutStorage(_ shortcut: Shortcut?, _ stringRepresentation: String?) -> [String: Any] { + [ + shortcutStorageStringField: stringRepresentation ?? shortcut?.readableStringRepresentation(isASCII: true) ?? "", + shortcutStorageDataField: archiveShortcut(shortcut), + ] + } + + static func decodeShortcutStorage(_ value: Any) -> (Bool, Shortcut?) { + guard let storage = value as? [String: Any], let data = storage[shortcutStorageDataField] as? Data else { return (false, nil) } + return unarchiveShortcut(data) + } + + static func unarchiveShortcut(_ data: Data) -> (Bool, Shortcut?) { + let shortcut: Shortcut? + if #available(macOS 10.13, *) { + shortcut = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Shortcut.self, from: data) + } else { + shortcut = NSKeyedUnarchiver.unarchiveObject(with: data) as? Shortcut + } + guard let shortcut else { return (false, nil) } + return (true, shortcut.keyCode == .none && shortcut.modifierFlags == [] ? nil : shortcut) + } + + static func shortcutFromKeyEquivalent(_ keyEquivalent: String) -> Shortcut? { + keyEquivalent.isEmpty ? nil : Shortcut(keyEquivalent: keyEquivalent) + } + static func indexToName(_ baseName: String, _ index: Int) -> String { return baseName + (index == 0 ? "" : String(index + 1)) } @@ -277,6 +322,23 @@ class CachedUserDefaults { return finalValue } + static func shortcut(_ key: String) -> Shortcut? { + if let cachedFinalValue = cache.withLock({ $0[key] }) { + return cachedFinalValue as? Shortcut + } + guard let objectValue = UserDefaults.standard.object(forKey: key) else { + cache.withLock { $0[key] = NSNull() } + return nil + } + let (isValid, finalValue) = Preferences.decodeShortcutStorage(objectValue) + if isValid { + cache.withLock { $0[key] = finalValue ?? NSNull() } + return finalValue + } + UserDefaults.standard.removeObject(forKey: key) + return shortcut(key) + } + static func int(_ key: String) -> Int { return getThenConvertOrReset(key, { s in Int(s) }) } diff --git a/src/logic/PreferencesMigrations.swift b/src/logic/PreferencesMigrations.swift index 752e9e7c..90b2f862 100644 --- a/src/logic/PreferencesMigrations.swift +++ b/src/logic/PreferencesMigrations.swift @@ -295,6 +295,44 @@ class PreferencesMigrations { UserDefaults.standard.set(new, forKey: preference) } } + + private static func migrateShortcutPreferencesToSecureCoding() { + Preferences.allShortcutPreferenceKeys.forEach { + let key = $0 + guard let oldValue = UserDefaults.standard.object(forKey: key) else { return } + if let oldStorage = oldValue as? [String: Any] { + let (isValid, shortcut) = Preferences.decodeShortcutStorage(oldStorage) + guard isValid else { + UserDefaults.standard.removeObject(forKey: key) + return + } + UserDefaults.standard.set(Preferences.shortcutStorage(shortcut, oldStorage["string"] as? String), forKey: key) + return + } + if let oldDataValue = oldValue as? Data { + let (isValid, shortcut) = Preferences.unarchiveShortcut(oldDataValue) + guard isValid else { + UserDefaults.standard.removeObject(forKey: key) + return + } + UserDefaults.standard.set(Preferences.shortcutStorage(shortcut, nil), forKey: key) + return + } + guard let oldStringValue = oldValue as? String else { + UserDefaults.standard.removeObject(forKey: key) + return + } + if oldStringValue.isEmpty { + UserDefaults.standard.set(Preferences.shortcutStorage(nil, ""), forKey: key) + return + } + guard let migratedShortcut = Preferences.shortcutFromKeyEquivalent(oldStringValue) else { + UserDefaults.standard.removeObject(forKey: key) + return + } + UserDefaults.standard.set(Preferences.shortcutStorage(migratedShortcut, oldStringValue), forKey: key) + } + } } /// workaround to silence compiler warning diff --git a/src/logic/events/InputSourceEvents.swift b/src/logic/events/InputSourceEvents.swift new file mode 100644 index 00000000..e3fbbd50 --- /dev/null +++ b/src/logic/events/InputSourceEvents.swift @@ -0,0 +1,26 @@ +import Cocoa +import Carbon.HIToolbox.TextInputSources + +class InputSourceEvents: NSObject { + private static let observer = InputSourceEvents() + private static var isObserving = false + + static func observe() { + guard !isObserving else { return } + isObserving = true + let name = NSNotification.Name(rawValue: kTISNotifySelectedKeyboardInputSourceChanged as String) + DistributedNotificationCenter.default().addObserver(observer, selector: #selector(handleEvent), name: name, object: nil, suspensionBehavior: .deliverImmediately) + } + + @objc private func handleEvent(_ notification: Notification) { + Logger.debug { InputSourceEvents.currentInputSource() } + ControlsTab.inputSourceChanged() + } + + static func currentInputSource() -> String { + guard let inputSource = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), + let localizedNamePointer = TISGetInputSourceProperty(inputSource, kTISPropertyLocalizedName) else { return "unknown" } + let localizedName = Unmanaged.fromOpaque(localizedNamePointer).takeUnretainedValue() + return localizedName as? String ?? "unknown" + } +} diff --git a/src/ui/App.swift b/src/ui/App.swift index 2b4f56a2..dc66c252 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -387,6 +387,7 @@ class App: AppCenterApplication { ScreensEvents.observe() SystemAppearanceEvents.observe() SystemScrollerStyleEvents.observe() + InputSourceEvents.observe() Applications.initialDiscovery() KeyboardEvents.addEventHandlers() CursorEvents.observe() diff --git a/src/ui/generic-components/CustomRecorderControl.swift b/src/ui/generic-components/CustomRecorderControl.swift index 3e0fd7b0..886e0393 100644 --- a/src/ui/generic-components/CustomRecorderControl.swift +++ b/src/ui/generic-components/CustomRecorderControl.swift @@ -8,6 +8,10 @@ class CustomRecorderControl: RecorderControl { var id: String! convenience init(_ shortcutString: String, _ clearable: Bool, _ id: String) { + self.init(Shortcut(keyEquivalent: shortcutString), clearable, id) + } + + convenience init(_ shortcut: Shortcut?, _ clearable: Bool, _ id: String) { self.init(frame: .zero) translatesAutoresizingMaskIntoConstraints = false self.clearable = clearable @@ -17,7 +21,7 @@ class CustomRecorderControl: RecorderControl { allowsDeleteToClearShortcutAndEndRecording = false allowsModifierFlagsOnlyShortcut = true restrictModifiers([]) - objectValue = Shortcut(keyEquivalent: shortcutString) + objectValue = shortcut addOrUpdateConstraint(widthAnchor, 100) } @@ -134,4 +138,3 @@ extension CustomRecorderControl: RecorderControlDelegate { } } - diff --git a/src/ui/settings-window/LabelAndControl.swift b/src/ui/settings-window/LabelAndControl.swift index da0869ad..567deefc 100644 --- a/src/ui/settings-window/LabelAndControl.swift +++ b/src/ui/settings-window/LabelAndControl.swift @@ -106,8 +106,8 @@ class LabelAndControl: NSObject { return view } - static func makeLabelWithRecorder(_ labelText: String, _ rawName: String, _ shortcutString: String, _ clearable: Bool = true, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] { - let input = CustomRecorderControl(shortcutString, clearable, rawName) + static func makeLabelWithRecorder(_ labelText: String, _ rawName: String, _ shortcut: Shortcut?, _ clearable: Bool = true, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] { + let input = CustomRecorderControl(shortcut, clearable, rawName) let views = makeLabelWithProvidedControl(labelText, rawName, input, labelPosition: labelPosition, extraAction: { _ in ControlsTab.shortcutChangedCallback(input) }) ControlsTab.shortcutChangedCallback(input) ControlsTab.shortcutControls[rawName] = (input, labelText) @@ -325,6 +325,15 @@ class LabelAndControl: NSObject { } static func controlWasChanged(_ senderControl: NSControl, _ controlId: String?) { + if let recorderControl = senderControl as? RecorderControl { + let key = senderControl.identifier!.rawValue + let oldValue = Preferences.shortcut(key) + let newValue = recorderControl.objectValue + if oldValue == nil && newValue == nil { return } + if let oldValue, let newValue, oldValue.isEqual(newValue) { return } + Preferences.setShortcut(key, newValue, stringRepresentation: recorderControl.stringValue) + return + } if let newValue = LabelAndControl.getControlValue(senderControl, controlId) { if let oldValue = UserDefaults.standard.string(forKey: senderControl.identifier!.rawValue), newValue == oldValue { return diff --git a/src/ui/settings-window/tabs/controls/ControlsTab.swift b/src/ui/settings-window/tabs/controls/ControlsTab.swift index d4cf37a4..f12c9db4 100644 --- a/src/ui/settings-window/tabs/controls/ControlsTab.swift +++ b/src/ui/settings-window/tabs/controls/ControlsTab.swift @@ -222,6 +222,11 @@ class ControlsTab { } } + static func inputSourceChanged() { + refreshShortcutRows() + refreshShortcutControlsDisplay() + } + static func initTab() -> NSView { shortcutEditorViews = (0.. TableGroupView { let holdName = Preferences.indexToName("holdShortcut", index) - let holdValue = UserDefaults.standard.string(forKey: holdName) ?? "" - var holdShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hold", comment: ""), holdName, holdValue, false, labelPosition: .leftWithoutSeparator) + var holdShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hold", comment: ""), holdName, Preferences.shortcut(holdName), false, labelPosition: .leftWithoutSeparator) holdShortcut.append(LabelAndControl.makeLabel(NSLocalizedString("and press", comment: ""))) let nextName = Preferences.indexToName("nextWindowShortcut", index) - let nextValue = UserDefaults.standard.string(forKey: nextName) ?? "" - let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), nextName, nextValue, labelPosition: .right) + let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), nextName, Preferences.shortcut(nextName), labelPosition: .right) return controlTab(index, holdShortcut + [nextWindowShortcut[0]], shortcutEditorContentWidth) } @@ -419,6 +422,13 @@ class ControlsTab { return table } + private static func refreshShortcutControlsDisplay() { + shortcutControls.values.forEach { + $0.0.needsDisplay = true + $0.0.invalidateIntrinsicContentSize() + } + } + private static func refreshShortcutUi() { if selectedShortcutIndex != gestureSelectionIndex { selectedShortcutIndex = max(0, min(selectedShortcutIndex, Preferences.shortcutCount - 1)) @@ -524,8 +534,8 @@ class ControlsTab { } private static func setAddedShortcutTriggerDefaults(_ index: Int) { - Preferences.set(Preferences.indexToName("holdShortcut", index), "⌥", false) - Preferences.set(Preferences.indexToName("nextWindowShortcut", index), "", false) + Preferences.setShortcut(Preferences.indexToName("holdShortcut", index), keyEquivalent: "⌥", false) + Preferences.setShortcut(Preferences.indexToName("nextWindowShortcut", index), keyEquivalent: "", false) Preferences.set(Preferences.indexToName("appsToShow", index), AppsToShowPreference.all.indexAsString, false) } @@ -533,8 +543,9 @@ class ControlsTab { removableShortcutPreferences.forEach { baseName in let fromKey = Preferences.indexToName(baseName, fromIndex) let toKey = Preferences.indexToName(baseName, toIndex) - if let value = UserDefaults.standard.string(forKey: fromKey) { - Preferences.set(toKey, value, false) + if let value = UserDefaults.standard.object(forKey: fromKey) { + UserDefaults.standard.set(value, forKey: toKey) + CachedUserDefaults.removeFromCache(toKey) } else { Preferences.remove(toKey, false) } @@ -558,8 +569,7 @@ class ControlsTab { private static func syncShortcutRecorderControlValue(_ controlId: String) { guard let control = shortcutControls[controlId]?.0 else { return } - let value = UserDefaults.standard.string(forKey: controlId) ?? "" - control.objectValue = value.isEmpty ? nil : Shortcut(keyEquivalent: value) + control.objectValue = Preferences.shortcut(controlId) } private static func syncShortcutDropdownControlValue(_ controlId: String) { @@ -588,8 +598,8 @@ class ControlsTab { } private static func shortcutSummary(_ index: Int) -> String { - let holdShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("holdShortcut", index)) ?? "" - let nextWindowShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("nextWindowShortcut", index)) ?? "" + let holdShortcut = Preferences.shortcut(Preferences.indexToName("holdShortcut", index))?.keyEquivalent ?? "" + let nextWindowShortcut = Preferences.shortcut(Preferences.indexToName("nextWindowShortcut", index))?.keyEquivalent ?? "" if nextWindowShortcut.isEmpty { return holdShortcut } @@ -717,8 +727,7 @@ class ControlsTab { } if controlId.hasPrefix("holdShortcut") { let i = Preferences.nameToIndex(controlId) - let holdShortcut = UserDefaults.standard.string(forKey: controlId) ?? "" - guard let shortcut = Shortcut(keyEquivalent: holdShortcut) else { + guard let shortcut = Preferences.shortcut(controlId) else { removeShortcutIfExists(controlId) return } @@ -728,9 +737,8 @@ class ControlsTab { shortcutChangedCallback(nextWindowShortcut) } } else { - let newValue = combineHoldAndNextWindow(controlId, sender) - let newShortcut = Shortcut(keyEquivalent: newValue) - if newValue.isEmpty || newShortcut == nil { + let newShortcut = combineHoldAndNextWindow(controlId, sender) + if newShortcut == nil { removeShortcutIfExists(controlId) restrictModifiersOfHoldShortcut(controlId, []) (sender as! CustomRecorderControl).objectValue = nil @@ -750,16 +758,13 @@ class ControlsTab { } } - static func combineHoldAndNextWindow(_ controlId: String, _ sender: NSControl) -> String { - let baseValue = (sender as! RecorderControl).stringValue - if baseValue == "" { - return "" - } + static func combineHoldAndNextWindow(_ controlId: String, _ sender: NSControl) -> Shortcut? { + guard let baseShortcut = (sender as! RecorderControl).objectValue else { return nil } if controlId.starts(with: "nextWindowShortcut") { - let holdShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) ?? "" - return holdShortcut + baseValue + let holdShortcut = Preferences.shortcut(Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) + return combineShortcuts(holdShortcut, baseShortcut) } - return baseValue + return baseShortcut } @objc static func arrowKeysEnabledCallback(_ sender: NSControl) { @@ -856,8 +861,7 @@ class ControlsTab { applyShortcutPreference(Preferences.indexToName("nextWindowShortcut", Preferences.nameToIndex(controlId))) return } - let shortcutString = combinedShortcutString(controlId) - guard !shortcutString.isEmpty, let shortcut = Shortcut(keyEquivalent: shortcutString) else { + guard let shortcut = combinedShortcut(controlId) else { removeShortcutIfExists(controlId) restrictModifiersOfHoldShortcut(controlId, []) return @@ -868,23 +872,25 @@ class ControlsTab { private static func applyHoldShortcutPreference(_ controlId: String) { let i = Preferences.nameToIndex(controlId) - let holdShortcut = UserDefaults.standard.string(forKey: controlId) ?? "" - guard let shortcut = Shortcut(keyEquivalent: holdShortcut) else { + guard let shortcut = Preferences.shortcut(controlId) else { removeShortcutIfExists(controlId) return } addShortcut(.up, .global, shortcut, controlId, i) } - private static func combinedShortcutString(_ controlId: String) -> String { - guard let baseValue = UserDefaults.standard.string(forKey: controlId), !baseValue.isEmpty else { - return "" - } + private static func combinedShortcut(_ controlId: String) -> Shortcut? { + guard let baseShortcut = Preferences.shortcut(controlId) else { return nil } if controlId.starts(with: "nextWindowShortcut") { - let holdShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) ?? "" - return holdShortcut + baseValue + let holdShortcut = Preferences.shortcut(Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) + return combineShortcuts(holdShortcut, baseShortcut) } - return baseValue + return baseShortcut + } + + private static func combineShortcuts(_ holdShortcut: Shortcut?, _ baseShortcut: Shortcut) -> Shortcut { + guard let holdShortcut else { return baseShortcut } + return Shortcut(code: baseShortcut.keyCode, modifierFlags: [holdShortcut.modifierFlags, baseShortcut.modifierFlags], characters: baseShortcut.characters, charactersIgnoringModifiers: baseShortcut.charactersIgnoringModifiers) } private static func applyArrowKeysPreference() {