fix: issues when using multiple keyboard languages (closes #2125)

This commit is contained in:
lwouis committed 2026-03-01 16:45:30 +01:00
1 parent 37ba9662f1
commit ea7284b387
9 files changed
+274 -130

No files matched your search

+4
View File
@@ -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 = "<group>"; };
D04BA5D2FD6E26D82721C574 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA5D9B4EBEB9E4333AE39 /* UserDefaultsEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsEvents.swift; sourceTree = "<group>"; };
D04BB102A1B2C3D4E5F60011 /* InputSourceEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSourceEvents.swift; sourceTree = "<group>"; };
D04BA5DB4FB71B583A28EF4F /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA5E208CBB8E28EE3FAFF /* site.webmanifest */ = {isa = PBXFileReference; lastKnownFileType = file.webmanifest; path = site.webmanifest; sourceTree = "<group>"; };
D04BA60A6D08C041F5BF8CC9 /* SF-Pro-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "SF-Pro-Text-Regular.otf"; sourceTree = "<group>"; };
@@ -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 */,
+1 -6
View File
@@ -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<AnyObject>.fromOpaque(localizedNamePointer).takeUnretainedValue()
return localizedName as? String ?? ""
}
return ""
InputSourceEvents.currentInputSource()
}
return r
}
+146 -84
View File
@@ -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..<maxShortcutCount).forEach { index in
values[indexToName("holdShortcut", index)] = "⌥"
values[indexToName("nextWindowShortcut", index)] = index == 0 ? "⇥" : (index == 1 ? keyAboveTabDependingOnInputSource() : "")
values[indexToName("holdShortcut", index)] = defaultShortcut("⌥")
values[indexToName("nextWindowShortcut", index)] = defaultShortcut(index == 0 ? "⇥" : (index == 1 ? keyAboveTabDependingOnInputSource() : ""))
}
(0...maxShortcutCount).forEach { index in
values[indexToName("appsToShow", index)] = index == 1 ? AppsToShowPreference.active.indexAsString : (index == 2 ? AppsToShowPreference.nonActive.indexAsString : AppsToShowPreference.all.indexAsString)
@@ -94,21 +71,31 @@ class Preferences {
// system preferences
static var finderShowsQuitMenuItem: Bool { UserDefaults(suiteName: "com.apple.Finder")?.bool(forKey: "QuitMenuItem") ?? false }
static let staticShortcutKeys = [
"focusWindowShortcut", "previousWindowShortcut", "cancelShortcut", "lockSearchShortcut", "closeWindowShortcut",
"minDeminWindowShortcut", "toggleFullscreenWindowShortcut", "quitAppShortcut", "hideShowAppShortcut", "searchShortcut",
]
static var allShortcutPreferenceKeys: [String] {
staticShortcutKeys + (0..<maxShortcutCount).flatMap { [indexToName("holdShortcut", $0), indexToName("nextWindowShortcut", $0)] }
}
static let emptyShortcut = Shortcut(code: .none, modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil)
private static let shortcutStorageStringField = "string"
private static let shortcutStorageDataField = "secureData"
// persisted values
static var holdShortcut: [String] { (0..<shortcutCount).map { CachedUserDefaults.string(indexToName("holdShortcut", $0)) } }
static var nextWindowShortcut: [String] { (0..<shortcutCount).map { CachedUserDefaults.string(indexToName("nextWindowShortcut", $0)) } }
static var holdShortcut: [Shortcut?] { (0..<shortcutCount).map { CachedUserDefaults.shortcut(indexToName("holdShortcut", $0)) } }
static var nextWindowShortcut: [Shortcut?] { (0..<shortcutCount).map { CachedUserDefaults.shortcut(indexToName("nextWindowShortcut", $0)) } }
static var nextWindowGesture: GesturePreference { CachedUserDefaults.macroPref("nextWindowGesture", GesturePreference.allCases) }
static var focusWindowShortcut: String { CachedUserDefaults.string("focusWindowShortcut") }
static var previousWindowShortcut: String { CachedUserDefaults.string("previousWindowShortcut") }
static var cancelShortcut: String { CachedUserDefaults.string("cancelShortcut") }
static var lockSearchShortcut: String { CachedUserDefaults.string("lockSearchShortcut") }
static var closeWindowShortcut: String { CachedUserDefaults.string("closeWindowShortcut") }
static var minDeminWindowShortcut: String { CachedUserDefaults.string("minDeminWindowShortcut") }
static var toggleFullscreenWindowShortcut: String { CachedUserDefaults.string("toggleFullscreenWindowShortcut") }
static var quitAppShortcut: String { CachedUserDefaults.string("quitAppShortcut") }
static var hideShowAppShortcut: String { CachedUserDefaults.string("hideShowAppShortcut") }
static var searchShortcut: String { CachedUserDefaults.string("searchShortcut") }
static var focusWindowShortcut: Shortcut? { CachedUserDefaults.shortcut("focusWindowShortcut") }
static var previousWindowShortcut: Shortcut? { CachedUserDefaults.shortcut("previousWindowShortcut") }
static var cancelShortcut: Shortcut? { CachedUserDefaults.shortcut("cancelShortcut") }
static var lockSearchShortcut: Shortcut? { CachedUserDefaults.shortcut("lockSearchShortcut") }
static var closeWindowShortcut: Shortcut? { CachedUserDefaults.shortcut("closeWindowShortcut") }
static var minDeminWindowShortcut: Shortcut? { CachedUserDefaults.shortcut("minDeminWindowShortcut") }
static var toggleFullscreenWindowShortcut: Shortcut? { CachedUserDefaults.shortcut("toggleFullscreenWindowShortcut") }
static var quitAppShortcut: Shortcut? { CachedUserDefaults.shortcut("quitAppShortcut") }
static var hideShowAppShortcut: Shortcut? { CachedUserDefaults.shortcut("hideShowAppShortcut") }
static var searchShortcut: Shortcut? { CachedUserDefaults.shortcut("searchShortcut") }
// periphery:ignore
static var arrowKeysEnabled: Bool { CachedUserDefaults.bool("arrowKeysEnabled") }
// periphery:ignore
@@ -183,6 +170,30 @@ class Preferences {
set("settingsWindowShownOnFirstLaunch", "true", false)
}
static func defaultShortcut(_ keyEquivalent: String) -> [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<T>(_ 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) })
}
+38
View File
@@ -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
+26
View File
@@ -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<AnyObject>.fromOpaque(localizedNamePointer).takeUnretainedValue()
return localizedName as? String ?? "unknown"
}
}
+1
View File
@@ -387,6 +387,7 @@ class App: AppCenterApplication {
ScreensEvents.observe()
SystemAppearanceEvents.observe()
SystemScrollerStyleEvents.observe()
InputSourceEvents.observe()
Applications.initialDiscovery()
KeyboardEvents.addEventHandlers()
CursorEvents.observe()
@@ -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 {
}
}
+11 -2
View File
@@ -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
@@ -222,6 +222,11 @@ class ControlsTab {
}
}
static func inputSourceChanged() {
refreshShortcutRows()
refreshShortcutControlsDisplay()
}
static func initTab() -> NSView {
shortcutEditorViews = (0..<Preferences.maxShortcutCount).map { shortcutTab($0) }
gestureEditorView = gestureTab(Preferences.gestureIndex)
@@ -369,12 +374,10 @@ class ControlsTab {
private static func shortcutTab(_ index: Int) -> 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() {