feat: allow up to 9 shortcuts (#3882)
This commit is contained in:
1 parent
3f1617cba0
commit
8cab3e1f8b
11 files changed
+731
-180
No files matched your search
@@ -1,3 +1,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"3-finger Horizontal Swipe" = "3-finger Horizontal Swipe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"3-finger Vertical Swipe" = "3-finger Vertical Swipe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"4-finger Horizontal Swipe" = "4-finger Horizontal Swipe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"4-finger Vertical Swipe" = "4-finger Vertical Swipe";
|
||||
|
||||
/* Menubar option. %@ is AltTab */
|
||||
"About %@" = "About %@";
|
||||
|
||||
@@ -220,12 +232,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Hold" = "Hold";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Horizontal Swipe with Four Fingers" = "Horizontal Swipe with Four Fingers";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Horizontal Swipe with Three Fingers" = "Horizontal Swipe with Three Fingers";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"I think the app could be improved with…" = "I think the app could be improved with…";
|
||||
|
||||
@@ -295,6 +301,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Not allowed" = "Not allowed";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Not set" = "Not set";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Only on different screen" = "Only on different screen";
|
||||
|
||||
@@ -325,7 +334,8 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Preview selected window" = "Preview selected window";
|
||||
|
||||
/* Menubar option. %@ is AltTab */
|
||||
/* %@ is AltTab
|
||||
Menubar option. %@ is AltTab */
|
||||
"Quit %@" = "Quit %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -404,13 +414,7 @@
|
||||
"Share improvement ideas, or report bugs" = "Share improvement ideas, or report bugs";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shortcut 1" = "Shortcut 1";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shortcut 2" = "Shortcut 2";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shortcut 3" = "Shortcut 3";
|
||||
"Shortcut" = "Shortcut";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shortcut already assigned to another action: %@" = "Shortcut already assigned to another action: %@";
|
||||
@@ -536,7 +540,7 @@
|
||||
"Trackpad haptic feedback" = "Trackpad haptic feedback";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Trigger shortcut" = "Trigger shortcut";
|
||||
"Trigger" = "Trigger";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unassign existing shortcut and continue" = "Unassign existing shortcut and continue";
|
||||
@@ -550,12 +554,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Version" = "Version";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Vertical Swipe with Four Fingers" = "Vertical Swipe with Four Fingers";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Vertical Swipe with Three Fingers" = "Vertical Swipe with Three Fingers";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"View existing discussions" = "View existing discussions";
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ class ATShortcut {
|
||||
}
|
||||
|
||||
func matches(_ id: Int?, _ shortcutState: ShortcutState?, _ keyCode: UInt32?, _ modifiers: NSEvent.ModifierFlags?) -> Bool {
|
||||
if let id, let shortcutState {
|
||||
let shortcutIndex = id
|
||||
let shortcutId = KeyboardEventsTestable.globalShortcutsIds.first { $0.value == shortcutIndex }!.key
|
||||
if let id, let shortcutState, let shortcutId = KeyboardEventsTestable.globalShortcutsIds.first(where: { $0.value == id })?.key {
|
||||
if shortcutId == self.id {
|
||||
state = shortcutState
|
||||
if (triggerPhase == .down && state == .down) || (triggerPhase == .up && state == .up) {
|
||||
|
||||
@@ -23,10 +23,10 @@ enum GesturePreference: CaseIterable, MacroPreference {
|
||||
var localizedString: LocalizedString {
|
||||
switch self {
|
||||
case .disabled: return NSLocalizedString("Disabled", comment: "")
|
||||
case .threeFingerHorizontalSwipe: return NSLocalizedString("Horizontal Swipe with Three Fingers", comment: "")
|
||||
case .threeFingerVerticalSwipe: return NSLocalizedString("Vertical Swipe with Three Fingers", comment: "")
|
||||
case .fourFingerHorizontalSwipe: return NSLocalizedString("Horizontal Swipe with Four Fingers", comment: "")
|
||||
case .fourFingerVerticalSwipe: return NSLocalizedString("Vertical Swipe with Four Fingers", comment: "")
|
||||
case .threeFingerHorizontalSwipe: return NSLocalizedString("3-finger Horizontal Swipe", comment: "")
|
||||
case .threeFingerVerticalSwipe: return NSLocalizedString("3-finger Vertical Swipe", comment: "")
|
||||
case .fourFingerHorizontalSwipe: return NSLocalizedString("4-finger Horizontal Swipe", comment: "")
|
||||
case .fourFingerVerticalSwipe: return NSLocalizedString("4-finger Vertical Swipe", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-34
@@ -4,13 +4,13 @@ import ShortcutRecorder
|
||||
|
||||
class Preferences {
|
||||
// default values
|
||||
static var defaultValues: [String: String] = [
|
||||
static var defaultValues: [String: String] = {
|
||||
var values: [String: String] = [
|
||||
"shortcutCount": "2",
|
||||
"holdShortcut": "⌥",
|
||||
"holdShortcut2": "⌥",
|
||||
"holdShortcut3": "⌥",
|
||||
"nextWindowShortcut": "⇥",
|
||||
"nextWindowShortcut2": keyAboveTabDependingOnInputSource(),
|
||||
"nextWindowShortcut3": "",
|
||||
"nextWindowGesture": GesturePreference.disabled.indexAsString,
|
||||
"focusWindowShortcut": returnKeyEquivalent(),
|
||||
"previousWindowShortcut": "⇧",
|
||||
@@ -28,24 +28,14 @@ class Preferences {
|
||||
"cursorFollowFocus": CursorFollowFocus.never.indexAsString,
|
||||
"showMinimizedWindows": ShowHowPreference.show.indexAsString,
|
||||
"showMinimizedWindows2": ShowHowPreference.show.indexAsString,
|
||||
"showMinimizedWindows3": ShowHowPreference.show.indexAsString,
|
||||
"showMinimizedWindows4": ShowHowPreference.show.indexAsString,
|
||||
"showHiddenWindows": ShowHowPreference.show.indexAsString,
|
||||
"showHiddenWindows2": ShowHowPreference.show.indexAsString,
|
||||
"showHiddenWindows3": ShowHowPreference.show.indexAsString,
|
||||
"showHiddenWindows4": ShowHowPreference.show.indexAsString,
|
||||
"showFullscreenWindows": ShowHowPreference.show.indexAsString,
|
||||
"showFullscreenWindows2": ShowHowPreference.show.indexAsString,
|
||||
"showFullscreenWindows3": ShowHowPreference.show.indexAsString,
|
||||
"showFullscreenWindows4": ShowHowPreference.show.indexAsString,
|
||||
"showWindowlessApps": ShowHowPreference.showAtTheEnd.indexAsString,
|
||||
"showWindowlessApps2": ShowHowPreference.showAtTheEnd.indexAsString,
|
||||
"showWindowlessApps3": ShowHowPreference.showAtTheEnd.indexAsString,
|
||||
"showWindowlessApps4": ShowHowPreference.showAtTheEnd.indexAsString,
|
||||
"windowOrder": WindowOrderPreference.recentlyFocused.indexAsString,
|
||||
"windowOrder2": WindowOrderPreference.recentlyFocused.indexAsString,
|
||||
"windowOrder3": WindowOrderPreference.recentlyFocused.indexAsString,
|
||||
"windowOrder4": WindowOrderPreference.recentlyFocused.indexAsString,
|
||||
"showTabsAsWindows": "false",
|
||||
"hideColoredCircles": "false",
|
||||
"windowDisplayDelay": "100",
|
||||
@@ -60,16 +50,10 @@ class Preferences {
|
||||
"showTitles": ShowTitlesPreference.windowTitle.indexAsString,
|
||||
"appsToShow": AppsToShowPreference.all.indexAsString,
|
||||
"appsToShow2": AppsToShowPreference.active.indexAsString,
|
||||
"appsToShow3": AppsToShowPreference.nonActive.indexAsString,
|
||||
"appsToShow4": AppsToShowPreference.all.indexAsString,
|
||||
"spacesToShow": SpacesToShowPreference.all.indexAsString,
|
||||
"spacesToShow2": SpacesToShowPreference.all.indexAsString,
|
||||
"spacesToShow3": SpacesToShowPreference.all.indexAsString,
|
||||
"spacesToShow4": SpacesToShowPreference.all.indexAsString,
|
||||
"screensToShow": ScreensToShowPreference.all.indexAsString,
|
||||
"screensToShow2": ScreensToShowPreference.all.indexAsString,
|
||||
"screensToShow3": ScreensToShowPreference.all.indexAsString,
|
||||
"screensToShow4": ScreensToShowPreference.all.indexAsString,
|
||||
"fadeOutAnimation": "false",
|
||||
"previewFadeInAnimation": "true",
|
||||
"hideSpaceNumberLabels": "false",
|
||||
@@ -83,22 +67,37 @@ class Preferences {
|
||||
"crashPolicy": CrashPolicyPreference.ask.indexAsString,
|
||||
"shortcutStyle": ShortcutStylePreference.focusOnRelease.indexAsString,
|
||||
"shortcutStyle2": ShortcutStylePreference.focusOnRelease.indexAsString,
|
||||
"shortcutStyle3": ShortcutStylePreference.focusOnRelease.indexAsString,
|
||||
"shortcutStyle4": ShortcutStylePreference.focusOnRelease.indexAsString,
|
||||
"hideAppBadges": "false",
|
||||
"hideThumbnails": "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() : "")
|
||||
}
|
||||
(0...maxShortcutCount).forEach { index in
|
||||
values[indexToName("appsToShow", index)] = index == 1 ? AppsToShowPreference.active.indexAsString : (index == 2 ? AppsToShowPreference.nonActive.indexAsString : AppsToShowPreference.all.indexAsString)
|
||||
values[indexToName("spacesToShow", index)] = SpacesToShowPreference.all.indexAsString
|
||||
values[indexToName("screensToShow", index)] = ScreensToShowPreference.all.indexAsString
|
||||
values[indexToName("showMinimizedWindows", index)] = ShowHowPreference.show.indexAsString
|
||||
values[indexToName("showHiddenWindows", index)] = ShowHowPreference.show.indexAsString
|
||||
values[indexToName("showFullscreenWindows", index)] = ShowHowPreference.show.indexAsString
|
||||
values[indexToName("showWindowlessApps", index)] = ShowHowPreference.showAtTheEnd.indexAsString
|
||||
values[indexToName("windowOrder", index)] = WindowOrderPreference.recentlyFocused.indexAsString
|
||||
values[indexToName("shortcutStyle", index)] = ShortcutStylePreference.focusOnRelease.indexAsString
|
||||
}
|
||||
return values
|
||||
}()
|
||||
|
||||
// system preferences
|
||||
static var finderShowsQuitMenuItem: Bool { UserDefaults(suiteName: "com.apple.Finder")?.bool(forKey: "QuitMenuItem") ?? false }
|
||||
|
||||
// persisted values
|
||||
static var holdShortcut: [String] { ["holdShortcut", "holdShortcut2", "holdShortcut3"].map { CachedUserDefaults.string($0) } }
|
||||
static var nextWindowShortcut: [String] { ["nextWindowShortcut", "nextWindowShortcut2", "nextWindowShortcut3"].map { CachedUserDefaults.string($0) } }
|
||||
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 nextWindowGesture: GesturePreference { CachedUserDefaults.macroPref("nextWindowGesture", GesturePreference.allCases) }
|
||||
static var focusWindowShortcut: String { CachedUserDefaults.string("focusWindowShortcut") }
|
||||
static var previousWindowShortcut: String { CachedUserDefaults.string("previousWindowShortcut") }
|
||||
@@ -145,20 +144,26 @@ class Preferences {
|
||||
static var showTitles: ShowTitlesPreference { CachedUserDefaults.macroPref("showTitles", ShowTitlesPreference.allCases) }
|
||||
static var updatePolicy: UpdatePolicyPreference { CachedUserDefaults.macroPref("updatePolicy", UpdatePolicyPreference.allCases) }
|
||||
static var crashPolicy: CrashPolicyPreference { CachedUserDefaults.macroPref("crashPolicy", CrashPolicyPreference.allCases) }
|
||||
static var appsToShow: [AppsToShowPreference] { ["appsToShow", "appsToShow2", "appsToShow3", "appsToShow4"].map { CachedUserDefaults.macroPref($0, AppsToShowPreference.allCases) } }
|
||||
static var spacesToShow: [SpacesToShowPreference] { ["spacesToShow", "spacesToShow2", "spacesToShow3", "spacesToShow4"].map { CachedUserDefaults.macroPref($0, SpacesToShowPreference.allCases) } }
|
||||
static var screensToShow: [ScreensToShowPreference] { ["screensToShow", "screensToShow2", "screensToShow3", "screensToShow4"].map { CachedUserDefaults.macroPref($0, ScreensToShowPreference.allCases) } }
|
||||
static var showMinimizedWindows: [ShowHowPreference] { ["showMinimizedWindows", "showMinimizedWindows2", "showMinimizedWindows3", "showMinimizedWindows4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
|
||||
static var showHiddenWindows: [ShowHowPreference] { ["showHiddenWindows", "showHiddenWindows2", "showHiddenWindows3", "showHiddenWindows4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
|
||||
static var showFullscreenWindows: [ShowHowPreference] { ["showFullscreenWindows", "showFullscreenWindows2", "showFullscreenWindows3", "showFullscreenWindows4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
|
||||
static var showWindowlessApps: [ShowHowPreference] { ["showWindowlessApps", "showWindowlessApps2", "showWindowlessApps3", "showWindowlessApps4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
|
||||
static var windowOrder: [WindowOrderPreference] { ["windowOrder", "windowOrder2", "windowOrder3", "windowOrder4"].map { CachedUserDefaults.macroPref($0, WindowOrderPreference.allCases) } }
|
||||
static var appsToShow: [AppsToShowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("appsToShow", $0), AppsToShowPreference.allCases) } }
|
||||
static var spacesToShow: [SpacesToShowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("spacesToShow", $0), SpacesToShowPreference.allCases) } }
|
||||
static var screensToShow: [ScreensToShowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("screensToShow", $0), ScreensToShowPreference.allCases) } }
|
||||
static var showMinimizedWindows: [ShowHowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("showMinimizedWindows", $0), ShowHowPreference.allCases) } }
|
||||
static var showHiddenWindows: [ShowHowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("showHiddenWindows", $0), ShowHowPreference.allCases) } }
|
||||
static var showFullscreenWindows: [ShowHowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("showFullscreenWindows", $0), ShowHowPreference.allCases) } }
|
||||
static var showWindowlessApps: [ShowHowPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("showWindowlessApps", $0), ShowHowPreference.allCases) } }
|
||||
static var windowOrder: [WindowOrderPreference] { (0...maxShortcutCount).map { CachedUserDefaults.macroPref(indexToName("windowOrder", $0), WindowOrderPreference.allCases) } }
|
||||
static var shortcutStyle: ShortcutStylePreference { CachedUserDefaults.macroPref("shortcutStyle", ShortcutStylePreference.allCases) }
|
||||
static var menubarIcon: MenubarIconPreference { CachedUserDefaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }
|
||||
static var menubarIconShown: Bool { CachedUserDefaults.bool("menubarIconShown") }
|
||||
static var language: LanguagePreference { CachedUserDefaults.macroPref("language", LanguagePreference.allCases) }
|
||||
|
||||
static let gestureIndex = 3
|
||||
static let minShortcutCount = 1
|
||||
static let maxShortcutCount = 9
|
||||
static var shortcutCount: Int {
|
||||
max(minShortcutCount, min(maxShortcutCount, CachedUserDefaults.int("shortcutCount")))
|
||||
}
|
||||
|
||||
static let gestureIndex = maxShortcutCount
|
||||
|
||||
static func initialize() {
|
||||
PreferencesMigrations.removeCorruptedPreferences()
|
||||
@@ -238,7 +243,8 @@ class Preferences {
|
||||
}
|
||||
|
||||
static func nameToIndex(_ name: String) -> Int {
|
||||
guard let number = name.last?.wholeNumberValue else { return 0 }
|
||||
let digits = String(name.reversed().prefix { $0.isNumber }.reversed())
|
||||
guard !digits.isEmpty, let number = Int(digits) else { return 0 }
|
||||
return number - 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,39 +21,42 @@ class PreferencesMigrations {
|
||||
private static func updateToNewPreferences(_ versionInPlist: String) {
|
||||
Logger.debug { "App-version:\(App.version), Plist-version:\(versionInPlist)" }
|
||||
// x.compare(y) is .orderedDescending if x > y
|
||||
if versionInPlist.compare("7.27.0", options: .numeric) != .orderedDescending {
|
||||
migrateCursorFollowFocus()
|
||||
if versionInPlist.compare("7.26.0", options: .numeric) != .orderedDescending {
|
||||
migrateShowWindowlessApps()
|
||||
if versionInPlist.compare("7.25.0", options: .numeric) != .orderedDescending {
|
||||
migrateHideWindowlessApps()
|
||||
if versionInPlist.compare("7.13.1", options: .numeric) != .orderedDescending {
|
||||
migrateGestures()
|
||||
if versionInPlist.compare("7.8.0", options: .numeric) != .orderedDescending {
|
||||
migrateMenubarIconWithNewShownToggle()
|
||||
if versionInPlist.compare("7.0.0", options: .numeric) != .orderedDescending {
|
||||
migratePreferencesIndexes()
|
||||
if versionInPlist.compare("6.43.0", options: .numeric) != .orderedDescending {
|
||||
migrateBlacklists()
|
||||
if versionInPlist.compare("6.28.1", options: .numeric) != .orderedDescending {
|
||||
migrateMinMaxWindowsWidthInRow()
|
||||
if versionInPlist.compare("6.27.1", options: .numeric) != .orderedDescending {
|
||||
// "Start at login" new implem doesn't use Login Items; we remove the entry from previous versions
|
||||
(PreferencesMigrations.self as AvoidDeprecationWarnings.Type).migrateLoginItem()
|
||||
if versionInPlist.compare("6.23.0", options: .numeric) != .orderedDescending {
|
||||
// "Show windows from:" got the "Active Space" option removed
|
||||
migrateShowWindowsFrom()
|
||||
if versionInPlist.compare("6.18.1", options: .numeric) != .orderedDescending {
|
||||
// nextWindowShortcut used to be able to have modifiers already present in holdShortcut; we remove these
|
||||
migrateNextWindowShortcuts()
|
||||
// dropdowns preferences used to store English text; now they store indexes
|
||||
migrateDropdownsFromTextToIndexes()
|
||||
// the "Hide menubar icon" checkbox was replaced with a dropdown of: icon1, icon2, hidden
|
||||
migrateMenubarIconFromCheckboxToDropdown()
|
||||
// "Show minimized/hidden/fullscreen windows" checkboxes were replaced with dropdowns
|
||||
migrateShowWindowsCheckboxToDropdown()
|
||||
// "Max size on screen" was split into max width and max height
|
||||
migrateMaxSizeOnScreenToWidthAndHeight()
|
||||
if versionInPlist.compare("9.0.0", options: .numeric) != .orderedDescending {
|
||||
migrateShortcutIndexes()
|
||||
if versionInPlist.compare("7.27.0", options: .numeric) != .orderedDescending {
|
||||
migrateCursorFollowFocus()
|
||||
if versionInPlist.compare("7.26.0", options: .numeric) != .orderedDescending {
|
||||
migrateShowWindowlessApps()
|
||||
if versionInPlist.compare("7.25.0", options: .numeric) != .orderedDescending {
|
||||
migrateHideWindowlessApps()
|
||||
if versionInPlist.compare("7.13.1", options: .numeric) != .orderedDescending {
|
||||
migrateGestures()
|
||||
if versionInPlist.compare("7.8.0", options: .numeric) != .orderedDescending {
|
||||
migrateMenubarIconWithNewShownToggle()
|
||||
if versionInPlist.compare("7.0.0", options: .numeric) != .orderedDescending {
|
||||
migratePreferencesIndexes()
|
||||
if versionInPlist.compare("6.43.0", options: .numeric) != .orderedDescending {
|
||||
migrateBlacklists()
|
||||
if versionInPlist.compare("6.28.1", options: .numeric) != .orderedDescending {
|
||||
migrateMinMaxWindowsWidthInRow()
|
||||
if versionInPlist.compare("6.27.1", options: .numeric) != .orderedDescending {
|
||||
// "Start at login" new implem doesn't use Login Items; we remove the entry from previous versions
|
||||
(PreferencesMigrations.self as AvoidDeprecationWarnings.Type).migrateLoginItem()
|
||||
if versionInPlist.compare("6.23.0", options: .numeric) != .orderedDescending {
|
||||
// "Show windows from:" got the "Active Space" option removed
|
||||
migrateShowWindowsFrom()
|
||||
if versionInPlist.compare("6.18.1", options: .numeric) != .orderedDescending {
|
||||
// nextWindowShortcut used to be able to have modifiers already present in holdShortcut; we remove these
|
||||
migrateNextWindowShortcuts()
|
||||
// dropdowns preferences used to store English text; now they store indexes
|
||||
migrateDropdownsFromTextToIndexes()
|
||||
// the "Hide menubar icon" checkbox was replaced with a dropdown of: icon1, icon2, hidden
|
||||
migrateMenubarIconFromCheckboxToDropdown()
|
||||
// "Show minimized/hidden/fullscreen windows" checkboxes were replaced with dropdowns
|
||||
migrateShowWindowsCheckboxToDropdown()
|
||||
// "Max size on screen" was split into max width and max height
|
||||
migrateMaxSizeOnScreenToWidthAndHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +70,28 @@ class PreferencesMigrations {
|
||||
}
|
||||
}
|
||||
|
||||
// gesture index moved from 3 (suffix "4") to 9 (suffix "10"); set shortcutCount for users who had a 3rd shortcut
|
||||
private static func migrateShortcutIndexes() {
|
||||
let gesturePrefs = [
|
||||
"nextWindowGesture", "appsToShow", "spacesToShow", "screensToShow",
|
||||
"showMinimizedWindows", "showHiddenWindows", "showFullscreenWindows",
|
||||
"showWindowlessApps", "windowOrder", "shortcutStyle",
|
||||
]
|
||||
for baseName in gesturePrefs {
|
||||
if let old = UserDefaults.standard.string(forKey: baseName + "4") {
|
||||
UserDefaults.standard.set(old, forKey: baseName + "10")
|
||||
UserDefaults.standard.removeObject(forKey: baseName + "4")
|
||||
}
|
||||
}
|
||||
let allPerShortcutPrefs = gesturePrefs + ["holdShortcut", "nextWindowShortcut"]
|
||||
let hasThirdShortcut = allPerShortcutPrefs.contains {
|
||||
UserDefaults.standard.string(forKey: $0 + "3") != nil
|
||||
}
|
||||
if hasThirdShortcut {
|
||||
UserDefaults.standard.set("3", forKey: "shortcutCount")
|
||||
}
|
||||
}
|
||||
|
||||
// cursorFollowFocus was a toggle. It's now a dropdown
|
||||
// before: cursorFollowFocusEnabled: true/false
|
||||
// after: cursorFollowFocus: 0 (never), 1 (always), 2 (differentScreen)
|
||||
|
||||
@@ -84,7 +84,7 @@ class CliServer {
|
||||
return noOutput
|
||||
}
|
||||
if rawValue.hasPrefix("--show="),
|
||||
let shortcutIndex = Int(rawValue.dropFirst("--show=".count)), (0...3).contains(shortcutIndex) {
|
||||
let shortcutIndex = Int(rawValue.dropFirst("--show=".count)), (0..<Preferences.shortcutCount).contains(shortcutIndex) {
|
||||
App.app.showUi(shortcutIndex)
|
||||
return noOutput
|
||||
}
|
||||
|
||||
@@ -58,14 +58,16 @@ class KeyboardEvents {
|
||||
|
||||
private static func unregisterHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
|
||||
if shortcut.keyCode != .none {
|
||||
UnregisterEventHotKey(eventHotKeyRefs[controlId]!)
|
||||
eventHotKeyRefs[controlId] = nil
|
||||
if let ref = eventHotKeyRefs[controlId] {
|
||||
UnregisterEventHotKey(ref)
|
||||
eventHotKeyRefs[controlId] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func registerHotKeyIfNeeded(_ controlId: String, _ shortcut: Shortcut) {
|
||||
if shortcut.keyCode != .none {
|
||||
let id = KeyboardEventsTestable.globalShortcutsIds[controlId]!
|
||||
guard let id = KeyboardEventsTestable.globalShortcutsIds[controlId] else { return }
|
||||
let hotkeyId = EventHotKeyID(signature: signature, id: UInt32(id))
|
||||
let key = shortcut.carbonKeyCode
|
||||
let mods = shortcut.carbonModifierFlags
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import ShortcutRecorder
|
||||
|
||||
class KeyboardEventsTestable {
|
||||
static let globalShortcutsIds = [
|
||||
"nextWindowShortcut": 0,
|
||||
"nextWindowShortcut2": 1,
|
||||
"nextWindowShortcut3": 2,
|
||||
"holdShortcut": 5,
|
||||
"holdShortcut2": 6,
|
||||
"holdShortcut3": 7,
|
||||
]
|
||||
static var globalShortcutsIds: [String: Int] {
|
||||
var ids = [String: Int]()
|
||||
(0..<Preferences.maxShortcutCount).forEach { ids[Preferences.indexToName("nextWindowShortcut", $0)] = $0 }
|
||||
(0..<Preferences.maxShortcutCount).forEach { ids[Preferences.indexToName("holdShortcut", $0)] = Preferences.maxShortcutCount + $0 }
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
||||
@@ -20,9 +20,9 @@ class BlacklistView: NSScrollView {
|
||||
super.scrollWheel(with: event)
|
||||
return
|
||||
}
|
||||
let y = contentView.bounds.origin.y
|
||||
super.scrollWheel(with: event)
|
||||
if abs(contentView.bounds.origin.y - y) < 0.5 {
|
||||
if canScrollInEventDirection(event) {
|
||||
super.scrollWheel(with: event)
|
||||
} else {
|
||||
parentScrollView()?.scrollWheel(with: event)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,30 @@ class BlacklistView: NSScrollView {
|
||||
abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) && abs(event.scrollingDeltaY) > 0.1
|
||||
}
|
||||
|
||||
private func canScrollInEventDirection(_ event: NSEvent) -> Bool {
|
||||
let maxOffset = maxVerticalOffset()
|
||||
guard maxOffset > 0 else { return false }
|
||||
let y = contentView.bounds.origin.y
|
||||
let dy = normalizedVerticalDelta(event)
|
||||
if dy > 0 {
|
||||
return y > 0.5
|
||||
}
|
||||
if dy < 0 {
|
||||
return y < maxOffset - 0.5
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func maxVerticalOffset() -> CGFloat {
|
||||
guard let content = documentView else { return 0 }
|
||||
return max(0, content.fittingSize.height - contentView.bounds.height)
|
||||
}
|
||||
|
||||
private func normalizedVerticalDelta(_ event: NSEvent) -> CGFloat {
|
||||
let delta = event.hasPreciseScrollingDeltas ? event.scrollingDeltaY : event.deltaY
|
||||
return event.isDirectionInvertedFromDevice ? -delta : delta
|
||||
}
|
||||
|
||||
private func parentScrollView() -> NSScrollView? {
|
||||
var parent = superview
|
||||
while let view = parent {
|
||||
|
||||
@@ -501,7 +501,7 @@ class SettingsWindow: NSWindow {
|
||||
private let sectionsStack = NSStackView()
|
||||
private let supportButton = AboutTab.makeSupportProjectButton()
|
||||
private let resetButton = NSButton(title: NSLocalizedString("Reset settings and restart…", comment: ""), target: nil, action: nil)
|
||||
private let quitButton = NSButton(title: NSLocalizedString("Quit %@", comment: ""), target: nil, action: #selector(NSApplication.terminate(_:)))
|
||||
private let quitButton = NSButton(title: String(format: NSLocalizedString("Quit %@", comment: "%@ is AltTab"), App.name), target: nil, action: #selector(NSApplication.terminate(_:)))
|
||||
private var sections = [SettingsSection]()
|
||||
private var visibleSections = [SettingsSection]()
|
||||
private var selectedSectionId: String?
|
||||
|
||||
@@ -2,6 +2,127 @@ import Cocoa
|
||||
import Carbon.HIToolbox.Events
|
||||
import ShortcutRecorder
|
||||
|
||||
private class ShortcutSidebarRow: ClickHoverStackView {
|
||||
private let titleLabel = NSTextField(labelWithString: "")
|
||||
private let summaryLabel = NSTextField(labelWithString: "")
|
||||
private let chevronLabel = NSTextField(labelWithString: "›")
|
||||
private let textColumn = NSStackView()
|
||||
private var isSelectedRow = false
|
||||
private var isHoveredRow = false
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
orientation = .horizontal
|
||||
alignment = .centerY
|
||||
spacing = 8
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
wantsLayer = true
|
||||
layer?.cornerRadius = TableGroupView.cornerRadius
|
||||
textColumn.orientation = .vertical
|
||||
textColumn.alignment = .leading
|
||||
textColumn.spacing = 0
|
||||
textColumn.translatesAutoresizingMaskIntoConstraints = false
|
||||
let spacer = NSView()
|
||||
titleLabel.alignment = .left
|
||||
summaryLabel.alignment = .left
|
||||
summaryLabel.font = NSFont.systemFont(ofSize: 12)
|
||||
summaryLabel.textColor = .secondaryLabelColor
|
||||
chevronLabel.font = NSFont.systemFont(ofSize: 22)
|
||||
chevronLabel.textColor = .secondaryLabelColor
|
||||
textColumn.addArrangedSubview(titleLabel)
|
||||
textColumn.addArrangedSubview(summaryLabel)
|
||||
addArrangedSubview(textColumn)
|
||||
addArrangedSubview(spacer)
|
||||
addArrangedSubview(chevronLabel)
|
||||
textColumn.leadingAnchor.constraint(equalTo: leadingAnchor, constant: TableGroupView.padding).isActive = true
|
||||
textColumn.trailingAnchor.constraint(lessThanOrEqualTo: chevronLabel.leadingAnchor, constant: -8).isActive = true
|
||||
chevronLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -TableGroupView.padding).isActive = true
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
func setContent(_ title: String, _ summary: String) {
|
||||
titleLabel.stringValue = title
|
||||
summaryLabel.stringValue = summary
|
||||
}
|
||||
|
||||
func setSelected(_ selected: Bool) {
|
||||
isSelectedRow = selected
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
func setHovered(_ hovered: Bool) {
|
||||
isHoveredRow = hovered
|
||||
updateStyle()
|
||||
}
|
||||
|
||||
private func updateStyle() {
|
||||
let selectedColor = NSColor.systemAccentColor.withAlphaComponent(0.16)
|
||||
let backgroundColor = isSelectedRow ? selectedColor : (isHoveredRow ? NSColor.tableHoverColor : .clear)
|
||||
let titleFont = NSFont.systemFont(ofSize: 13, weight: isSelectedRow ? .semibold : .regular)
|
||||
titleLabel.attributedStringValue = NSAttributedString(string: titleLabel.stringValue, attributes: [.font: titleFont, .foregroundColor: NSColor.labelColor])
|
||||
layer?.backgroundColor = backgroundColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
private class ControlsSidebarScrollView: NSScrollView {
|
||||
override class var isCompatibleWithResponsiveScrolling: Bool { true }
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
guard shouldHandleVerticalScroll(event) else {
|
||||
super.scrollWheel(with: event)
|
||||
return
|
||||
}
|
||||
if canScrollInEventDirection(event) {
|
||||
super.scrollWheel(with: event)
|
||||
} else {
|
||||
parentScrollView()?.scrollWheel(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldHandleVerticalScroll(_ event: NSEvent) -> Bool {
|
||||
abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) && abs(event.scrollingDeltaY) > 0.1
|
||||
}
|
||||
|
||||
private func canScrollInEventDirection(_ event: NSEvent) -> Bool {
|
||||
let maxOffset = maxVerticalOffset()
|
||||
guard maxOffset > 0 else { return false }
|
||||
let y = contentView.bounds.origin.y
|
||||
let dy = normalizedVerticalDelta(event)
|
||||
if dy > 0 {
|
||||
return y > 0.5
|
||||
}
|
||||
if dy < 0 {
|
||||
return y < maxOffset - 0.5
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func maxVerticalOffset() -> CGFloat {
|
||||
guard let content = documentView?.subviews.first else { return 0 }
|
||||
return max(0, content.fittingSize.height - contentView.bounds.height)
|
||||
}
|
||||
|
||||
private func normalizedVerticalDelta(_ event: NSEvent) -> CGFloat {
|
||||
let delta = event.hasPreciseScrollingDeltas ? event.scrollingDeltaY : event.deltaY
|
||||
return event.isDirectionInvertedFromDevice ? -delta : delta
|
||||
}
|
||||
|
||||
private func parentScrollView() -> NSScrollView? {
|
||||
var parent = superview
|
||||
while let view = parent {
|
||||
if let scrollView = view as? NSScrollView {
|
||||
return scrollView
|
||||
}
|
||||
parent = view.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class ControlsTab {
|
||||
static var shortcuts = [String: ATShortcut]()
|
||||
static var shortcutControls = [String: (CustomRecorderControl, String)]()
|
||||
@@ -34,16 +155,28 @@ class ControlsTab {
|
||||
static var arrowKeysCheckbox: Switch!
|
||||
static var vimKeysCheckbox: Switch!
|
||||
|
||||
static var tableGroupViews: [TableGroupView]!
|
||||
|
||||
static var shortcutsWhenActiveSheet: ShortcutsWhenActiveSheet!
|
||||
static var additionalControlsSheet: AdditionalControlsSheet!
|
||||
private static let managedShortcutPreferences = [
|
||||
"holdShortcut", "holdShortcut2", "holdShortcut3",
|
||||
"nextWindowShortcut", "nextWindowShortcut2", "nextWindowShortcut3",
|
||||
|
||||
private static let shortcutSidebarWidth = CGFloat(200)
|
||||
private static let sidebarRowHeight = CGFloat(52)
|
||||
private static var shortcutEditorWidth: CGFloat { SettingsWindow.contentWidth - shortcutSidebarWidth - 1 }
|
||||
private static let gestureSelectionIndex = -1
|
||||
private static let staticManagedShortcutPreferences = [
|
||||
"focusWindowShortcut", "previousWindowShortcut", "cancelShortcut", "searchShortcut", "lockSearchShortcut",
|
||||
"closeWindowShortcut", "minDeminWindowShortcut", "toggleFullscreenWindowShortcut", "quitAppShortcut", "hideShowAppShortcut",
|
||||
]
|
||||
private static let removableShortcutPreferences = [
|
||||
"holdShortcut", "nextWindowShortcut",
|
||||
"appsToShow", "spacesToShow", "screensToShow",
|
||||
"showMinimizedWindows", "showHiddenWindows", "showFullscreenWindows", "showWindowlessApps",
|
||||
"windowOrder", "shortcutStyle",
|
||||
]
|
||||
private static let shortcutDropdownPreferences = [
|
||||
"appsToShow", "spacesToShow", "screensToShow",
|
||||
"showMinimizedWindows", "showHiddenWindows", "showFullscreenWindows", "showWindowlessApps",
|
||||
"windowOrder",
|
||||
]
|
||||
private static let arrowKeys = ["←", "→", "↑", "↓"]
|
||||
private static let vimKeyActions = [
|
||||
"h": "vimCycleLeft",
|
||||
@@ -52,89 +185,235 @@ class ControlsTab {
|
||||
"j": "vimCycleDown",
|
||||
]
|
||||
|
||||
private static var selectedShortcutIndex = 0
|
||||
private static var shortcutRowsStackView: NSStackView?
|
||||
private static var shortcutRows = [ShortcutSidebarRow]()
|
||||
private static var shortcutEditorViews = [TableGroupView]()
|
||||
private static var gestureSidebarRow: ShortcutSidebarRow?
|
||||
private static var gestureEditorView: TableGroupView?
|
||||
private static var shortcutCountButtons: NSSegmentedControl?
|
||||
|
||||
static func initializePreferencesDependentState() {
|
||||
managedShortcutPreferences.forEach { applyShortcutPreference($0) }
|
||||
applyActiveShortcutPreferences()
|
||||
staticManagedShortcutPreferences.forEach { applyShortcutPreference($0) }
|
||||
applyArrowKeysPreference()
|
||||
applyVimKeysPreferenceWithoutDialogs()
|
||||
}
|
||||
|
||||
static func preferenceChanged(_ key: String) {
|
||||
switch key {
|
||||
case let k where managedShortcutPreferences.contains(k): applyShortcutPreference(k)
|
||||
case "arrowKeysEnabled": applyArrowKeysPreference()
|
||||
case "vimKeysEnabled" where vimKeysCheckbox == nil: applyVimKeysPreferenceWithoutDialogs()
|
||||
default: break
|
||||
case "shortcutCount":
|
||||
applyActiveShortcutPreferences()
|
||||
(0..<Preferences.shortcutCount).forEach { initializeShortcutRecorderState($0) }
|
||||
refreshShortcutUi()
|
||||
case "nextWindowGesture":
|
||||
refreshGestureRow()
|
||||
case let k where isShortcutPreferenceKey(k):
|
||||
if Preferences.nameToIndex(k) < Preferences.shortcutCount {
|
||||
syncShortcutRecorderControlValue(k)
|
||||
applyShortcutPreference(k)
|
||||
} else {
|
||||
removeShortcutIfExists(k)
|
||||
}
|
||||
refreshShortcutRows()
|
||||
case let k where staticManagedShortcutPreferences.contains(k):
|
||||
applyShortcutPreference(k)
|
||||
case "arrowKeysEnabled":
|
||||
applyArrowKeysPreference()
|
||||
case "vimKeysEnabled" where vimKeysCheckbox == nil:
|
||||
applyVimKeysPreferenceWithoutDialogs()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static func initTab() -> NSView {
|
||||
let (holdShortcut, nextWindowShortcut, tab1View) = shortcutTab(0)
|
||||
let (holdShortcut2, nextWindowShortcut2, tab2View) = shortcutTab(1)
|
||||
let (holdShortcut3, nextWindowShortcut3, tab3View) = shortcutTab(2)
|
||||
let tab6View = gestureTab(Preferences.gestureIndex)
|
||||
tableGroupViews = [tab1View, tab2View, tab3View, tab6View]
|
||||
// trigger shortcutChanged for these shortcuts to trigger .restrictModifiers
|
||||
[holdShortcut, holdShortcut2, holdShortcut3].forEach { ControlsTab.shortcutChangedCallback($0[1] as! NSControl) }
|
||||
[nextWindowShortcut, nextWindowShortcut2, nextWindowShortcut3].forEach { ControlsTab.shortcutChangedCallback($0[0] as! NSControl) }
|
||||
let table = TableGroupView(hasHeader: true, width: SettingsWindow.contentWidth)
|
||||
let tab = NSSegmentedControl(labels: [
|
||||
NSLocalizedString("Shortcut 1", comment: ""),
|
||||
NSLocalizedString("Shortcut 2", comment: ""),
|
||||
NSLocalizedString("Shortcut 3", comment: ""),
|
||||
NSLocalizedString("Gesture", comment: ""),
|
||||
], trackingMode: .selectOne, target: self, action: #selector(switchTab(_:)))
|
||||
tab.selectedSegment = 0
|
||||
tab.segmentStyle = .automatic
|
||||
tab.widthAnchor.constraint(equalToConstant: SettingsWindow.contentWidth).isActive = true
|
||||
table.addHeader(views: [tab])
|
||||
let additionalControlsButton = NSButton(title: NSLocalizedString("Additional controls…", comment: ""), target: self, action: #selector(ControlsTab.showAdditionalControlsSettings))
|
||||
let shortcutsButton = NSButton(title: NSLocalizedString("Shortcuts when active…", comment: ""), target: self, action: #selector(ControlsTab.showShortcutsSettings))
|
||||
shortcutEditorViews = (0..<Preferences.maxShortcutCount).map { shortcutTab($0) }
|
||||
gestureEditorView = gestureTab(Preferences.gestureIndex)
|
||||
let shortcutsView = makeShortcutsView()
|
||||
let additionalControlsButton = NSButton(title: NSLocalizedString("Additional controls…", comment: ""), target: self, action: #selector(showAdditionalControlsSettings))
|
||||
let shortcutsButton = NSButton(title: NSLocalizedString("Shortcuts when active…", comment: ""), target: self, action: #selector(showShortcutsSettings))
|
||||
let tools = StackView([additionalControlsButton, shortcutsButton], .horizontal)
|
||||
let view = TableGroupSetView(originalViews: [table, tab1View, tab2View, tab3View, tab6View], toolsViews: [tools], bottomPadding: 0, toolsAlignment: .trailing)
|
||||
let view = TableGroupSetView(originalViews: [shortcutsView], toolsViews: [tools], bottomPadding: 0, othersAlignment: .leading, toolsAlignment: .trailing)
|
||||
shortcutsWhenActiveSheet = ShortcutsWhenActiveSheet()
|
||||
additionalControlsSheet = AdditionalControlsSheet()
|
||||
ControlsTab.switchIndexTab(0)
|
||||
refreshShortcutUi()
|
||||
(0..<Preferences.shortcutCount).forEach { initializeShortcutRecorderState($0) }
|
||||
return view
|
||||
}
|
||||
|
||||
private static func shortcutTab(_ index: Int) -> ([NSView], [NSView], TableGroupView) {
|
||||
var holdShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hold", comment: ""), Preferences.indexToName("holdShortcut", index), Preferences.holdShortcut[index], false, labelPosition: .leftWithoutSeparator)
|
||||
private static func makeShortcutsView() -> NSView {
|
||||
let sidebar = makeShortcutSidebar()
|
||||
let editorPane = makeEditorPane()
|
||||
let separator = NSView()
|
||||
separator.wantsLayer = true
|
||||
separator.layer?.backgroundColor = NSColor.tableSeparatorColor.cgColor
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
let content = NSStackView(views: [sidebar, separator, editorPane])
|
||||
content.orientation = .horizontal
|
||||
content.alignment = .top
|
||||
content.spacing = 0
|
||||
content.translatesAutoresizingMaskIntoConstraints = false
|
||||
content.wantsLayer = true
|
||||
content.layer?.backgroundColor = NSColor.tableBackgroundColor.cgColor
|
||||
content.layer?.cornerRadius = TableGroupView.cornerRadius
|
||||
content.layer?.borderColor = NSColor.tableBorderColor.cgColor
|
||||
content.layer?.borderWidth = TableGroupView.borderWidth
|
||||
content.layer?.masksToBounds = true
|
||||
content.widthAnchor.constraint(equalToConstant: SettingsWindow.contentWidth).isActive = true
|
||||
sidebar.heightAnchor.constraint(equalTo: editorPane.heightAnchor).isActive = true
|
||||
return content
|
||||
}
|
||||
|
||||
private static func makeEditorPane() -> NSView {
|
||||
let pane = NSView()
|
||||
pane.translatesAutoresizingMaskIntoConstraints = false
|
||||
pane.widthAnchor.constraint(equalToConstant: shortcutEditorWidth).isActive = true
|
||||
var views = shortcutEditorViews
|
||||
if let gestureEditorView {
|
||||
views.append(gestureEditorView)
|
||||
}
|
||||
let editorsStack = NSStackView(views: views)
|
||||
editorsStack.orientation = .vertical
|
||||
editorsStack.alignment = .leading
|
||||
editorsStack.spacing = 0
|
||||
editorsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
pane.addSubview(editorsStack)
|
||||
NSLayoutConstraint.activate([
|
||||
editorsStack.topAnchor.constraint(equalTo: pane.topAnchor),
|
||||
editorsStack.leadingAnchor.constraint(equalTo: pane.leadingAnchor),
|
||||
editorsStack.trailingAnchor.constraint(equalTo: pane.trailingAnchor),
|
||||
editorsStack.bottomAnchor.constraint(equalTo: pane.bottomAnchor),
|
||||
])
|
||||
return pane
|
||||
}
|
||||
|
||||
private static func makeShortcutSidebar() -> NSView {
|
||||
let sidebar = NSView()
|
||||
sidebar.translatesAutoresizingMaskIntoConstraints = false
|
||||
sidebar.widthAnchor.constraint(equalToConstant: shortcutSidebarWidth).isActive = true
|
||||
sidebar.wantsLayer = true
|
||||
sidebar.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.25).cgColor
|
||||
let listContainer = NSView()
|
||||
listContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
listContainer.wantsLayer = true
|
||||
listContainer.layer?.backgroundColor = NSColor.tableBackgroundColor.cgColor
|
||||
listContainer.layer?.cornerRadius = TableGroupView.cornerRadius
|
||||
listContainer.layer?.borderColor = NSColor.tableBorderColor.cgColor
|
||||
listContainer.layer?.borderWidth = TableGroupView.borderWidth
|
||||
let rows = NSStackView()
|
||||
rows.orientation = .vertical
|
||||
rows.alignment = .leading
|
||||
rows.spacing = 0
|
||||
rows.translatesAutoresizingMaskIntoConstraints = false
|
||||
shortcutRowsStackView = rows
|
||||
let rowsScrollView = ControlsSidebarScrollView()
|
||||
rowsScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
rowsScrollView.drawsBackground = false
|
||||
rowsScrollView.hasVerticalScroller = true
|
||||
rowsScrollView.hasHorizontalScroller = false
|
||||
rowsScrollView.scrollerStyle = .overlay
|
||||
rowsScrollView.verticalScrollElasticity = .none
|
||||
rowsScrollView.usesPredominantAxisScrolling = true
|
||||
let documentView = FlippedView(frame: .zero)
|
||||
documentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
rowsScrollView.documentView = documentView
|
||||
documentView.addSubview(rows)
|
||||
let gestureSeparator = NSView()
|
||||
gestureSeparator.translatesAutoresizingMaskIntoConstraints = false
|
||||
gestureSeparator.wantsLayer = true
|
||||
gestureSeparator.layer?.backgroundColor = NSColor.tableSeparatorColor.cgColor
|
||||
let gestureRow = ShortcutSidebarRow()
|
||||
gestureRow.onClick = { _, _ in selectGesture() }
|
||||
gestureRow.onMouseEntered = { _, _ in gestureRow.setHovered(true) }
|
||||
gestureRow.onMouseExited = { _, _ in gestureRow.setHovered(false) }
|
||||
gestureSidebarRow = gestureRow
|
||||
listContainer.addSubview(rowsScrollView)
|
||||
listContainer.addSubview(gestureSeparator)
|
||||
listContainer.addSubview(gestureRow)
|
||||
let countButtons = NSSegmentedControl(labels: ["+", "-"], trackingMode: .momentary, target: self, action: #selector(updateShortcutCount(_:)))
|
||||
countButtons.translatesAutoresizingMaskIntoConstraints = false
|
||||
countButtons.segmentStyle = .rounded
|
||||
countButtons.setWidth(28, forSegment: 0)
|
||||
countButtons.setWidth(28, forSegment: 1)
|
||||
shortcutCountButtons = countButtons
|
||||
let buttonsRow = NSStackView(views: [countButtons])
|
||||
buttonsRow.orientation = .horizontal
|
||||
buttonsRow.alignment = .leading
|
||||
buttonsRow.translatesAutoresizingMaskIntoConstraints = false
|
||||
sidebar.addSubview(listContainer)
|
||||
sidebar.addSubview(buttonsRow)
|
||||
NSLayoutConstraint.activate([
|
||||
listContainer.topAnchor.constraint(equalTo: sidebar.topAnchor, constant: 10),
|
||||
listContainer.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 10),
|
||||
listContainer.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -10),
|
||||
listContainer.bottomAnchor.constraint(equalTo: buttonsRow.topAnchor, constant: -10),
|
||||
documentView.widthAnchor.constraint(equalTo: rowsScrollView.contentView.widthAnchor),
|
||||
documentView.heightAnchor.constraint(greaterThanOrEqualTo: rowsScrollView.contentView.heightAnchor),
|
||||
rows.topAnchor.constraint(equalTo: documentView.topAnchor),
|
||||
rows.leadingAnchor.constraint(equalTo: documentView.leadingAnchor),
|
||||
rows.trailingAnchor.constraint(equalTo: documentView.trailingAnchor),
|
||||
rows.bottomAnchor.constraint(lessThanOrEqualTo: documentView.bottomAnchor),
|
||||
rowsScrollView.topAnchor.constraint(equalTo: listContainer.topAnchor),
|
||||
rowsScrollView.leadingAnchor.constraint(equalTo: listContainer.leadingAnchor),
|
||||
rowsScrollView.trailingAnchor.constraint(equalTo: listContainer.trailingAnchor),
|
||||
rowsScrollView.bottomAnchor.constraint(equalTo: gestureSeparator.topAnchor),
|
||||
gestureSeparator.leadingAnchor.constraint(equalTo: listContainer.leadingAnchor),
|
||||
gestureSeparator.trailingAnchor.constraint(equalTo: listContainer.trailingAnchor),
|
||||
gestureSeparator.heightAnchor.constraint(equalToConstant: TableGroupView.borderWidth),
|
||||
gestureSeparator.bottomAnchor.constraint(equalTo: gestureRow.topAnchor),
|
||||
gestureRow.leadingAnchor.constraint(equalTo: listContainer.leadingAnchor),
|
||||
gestureRow.trailingAnchor.constraint(equalTo: listContainer.trailingAnchor),
|
||||
gestureRow.heightAnchor.constraint(equalToConstant: sidebarRowHeight),
|
||||
gestureRow.bottomAnchor.constraint(equalTo: listContainer.bottomAnchor),
|
||||
buttonsRow.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 10),
|
||||
buttonsRow.bottomAnchor.constraint(equalTo: sidebar.bottomAnchor, constant: -10),
|
||||
])
|
||||
refreshGestureRow()
|
||||
return sidebar
|
||||
}
|
||||
|
||||
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)
|
||||
holdShortcut.append(LabelAndControl.makeLabel(NSLocalizedString("and press", comment: "")))
|
||||
let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), Preferences.indexToName("nextWindowShortcut", index), Preferences.nextWindowShortcut[index], labelPosition: .right)
|
||||
let tab = controlTab(index, holdShortcut + [nextWindowShortcut[0]])
|
||||
return (holdShortcut, nextWindowShortcut, tab)
|
||||
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)
|
||||
return controlTab(index, holdShortcut + [nextWindowShortcut[0]], shortcutEditorWidth)
|
||||
}
|
||||
|
||||
private static func gestureTab(_ index: Int) -> TableGroupView {
|
||||
let label = NSLocalizedString("You may need to disable some conflicting system gestures", comment: "")
|
||||
let message = NSLocalizedString("You may need to disable some conflicting system gestures", comment: "")
|
||||
let button = NSButton(title: NSLocalizedString("Open Trackpad Settings…", comment: ""), target: self, action: #selector(openSystemGestures(_:)))
|
||||
let infoBtn = LabelAndControl.makeInfoButton(searchableTooltipTexts: [label], onMouseEntered: { event, view in
|
||||
Popover.shared.show(event: event, positioningView: view, message: label, extraView: button)
|
||||
let infoBtn = LabelAndControl.makeInfoButton(searchableTooltipTexts: [message], onMouseEntered: { event, view in
|
||||
Popover.shared.show(event: event, positioningView: view, message: message, extraView: button)
|
||||
})
|
||||
let gesture = LabelAndControl.makeDropdown("nextWindowGesture", GesturePreference.allCases)
|
||||
let gestureWithTooltip = NSStackView()
|
||||
gestureWithTooltip.orientation = .horizontal
|
||||
gestureWithTooltip.alignment = .centerY
|
||||
// this is a trick to get the height of that row to be consistent when switching between the tabs
|
||||
let dummyRecorderForHeight = CustomRecorderControl("d", true, "dummy")
|
||||
gestureWithTooltip.setViews([gesture, dummyRecorderForHeight], in: .trailing)
|
||||
gestureWithTooltip.setViews([infoBtn], in: .leading)
|
||||
gestureWithTooltip.heightAnchor.constraint(equalTo: dummyRecorderForHeight.heightAnchor).isActive = true
|
||||
dummyRecorderForHeight.isHidden = true
|
||||
return controlTab(index, [gestureWithTooltip])
|
||||
return controlTab(index, [gestureWithTooltip], shortcutEditorWidth)
|
||||
}
|
||||
|
||||
private static func controlTab(_ index: Int, _ trigger: [NSView]) -> TableGroupView {
|
||||
private static func controlTab(_ index: Int, _ trigger: [NSView], _ width: CGFloat) -> TableGroupView {
|
||||
let appsToShow = LabelAndControl.makeDropdown(Preferences.indexToName("appsToShow", index), AppsToShowPreference.allCases)
|
||||
let spacesToShow = LabelAndControl.makeDropdown(Preferences.indexToName("spacesToShow", index), SpacesToShowPreference.allCases)
|
||||
let screensToShow = LabelAndControl.makeDropdown(Preferences.indexToName("screensToShow", index), ScreensToShowPreference.allCases)
|
||||
let showMinimizedWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showMinimizedWindows", index), ShowHowPreference.allCases)
|
||||
let showHiddenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showHiddenWindows", index), ShowHowPreference.allCases)
|
||||
let showFullscreenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showFullscreenWindows", index), ShowHowPreference.allCases.filter { $0 != .showAtTheEnd }) // this filter is ok for serialization because the filtered value is last in the enum
|
||||
let showFullscreenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showFullscreenWindows", index), ShowHowPreference.allCases.filter { $0 != .showAtTheEnd })
|
||||
let showWindowlessApps = LabelAndControl.makeDropdown(Preferences.indexToName("showWindowlessApps", index), ShowHowPreference.allCases)
|
||||
let windowOrder = LabelAndControl.makeDropdown(Preferences.indexToName("windowOrder", index), WindowOrderPreference.allCases)
|
||||
let table = TableGroupView(width: SettingsWindow.contentWidth)
|
||||
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Trigger shortcut", comment: ""), rightViews: trigger))
|
||||
let table = TableGroupView(width: width)
|
||||
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Trigger", comment: ""), rightViews: trigger))
|
||||
table.addNewTable()
|
||||
table.addRow(leftViews: [TableGroupView.makeText(NSLocalizedString("Show windows from applications", comment: ""))], rightViews: [appsToShow])
|
||||
table.addRow(leftViews: [TableGroupView.makeText(NSLocalizedString("Show windows from Spaces", comment: ""))], rightViews: [spacesToShow])
|
||||
@@ -147,17 +426,219 @@ class ControlsTab {
|
||||
return table
|
||||
}
|
||||
|
||||
@objc static func switchTab(_ sender: NSSegmentedControl) {
|
||||
let selectedIndex = sender.selectedSegment
|
||||
switchIndexTab(selectedIndex)
|
||||
private static func refreshShortcutUi() {
|
||||
if selectedShortcutIndex != gestureSelectionIndex {
|
||||
selectedShortcutIndex = max(0, min(selectedShortcutIndex, Preferences.shortcutCount - 1))
|
||||
}
|
||||
refreshShortcutRows()
|
||||
refreshGestureRow()
|
||||
refreshShortcutSelection()
|
||||
refreshShortcutCountButtons()
|
||||
}
|
||||
|
||||
static func switchIndexTab(_ selectedIndex: Int) {
|
||||
ControlsTab.tableGroupViews.enumerated().forEach { (index, view) in
|
||||
if selectedIndex == index {
|
||||
view.isHidden = false
|
||||
private static func refreshShortcutRows() {
|
||||
guard let rows = shortcutRowsStackView else { return }
|
||||
clearArrangedSubviews(rows)
|
||||
shortcutRows.removeAll(keepingCapacity: true)
|
||||
for index in 0..<Preferences.shortcutCount {
|
||||
let row = ShortcutSidebarRow()
|
||||
row.setContent(shortcutTitle(index), shortcutSummary(index))
|
||||
row.setSelected(index == selectedShortcutIndex && selectedShortcutIndex != gestureSelectionIndex)
|
||||
row.onClick = { _, _ in selectShortcut(index) }
|
||||
row.onMouseEntered = { _, _ in row.setHovered(true) }
|
||||
row.onMouseExited = { _, _ in row.setHovered(false) }
|
||||
rows.addArrangedSubview(row)
|
||||
row.widthAnchor.constraint(equalTo: rows.widthAnchor).isActive = true
|
||||
row.heightAnchor.constraint(equalToConstant: sidebarRowHeight).isActive = true
|
||||
shortcutRows.append(row)
|
||||
if index < Preferences.shortcutCount - 1 {
|
||||
let separator = NSView()
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.wantsLayer = true
|
||||
separator.layer?.backgroundColor = NSColor.tableSeparatorColor.cgColor
|
||||
rows.addArrangedSubview(separator)
|
||||
separator.widthAnchor.constraint(equalTo: rows.widthAnchor).isActive = true
|
||||
separator.heightAnchor.constraint(equalToConstant: TableGroupView.borderWidth).isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func refreshShortcutSelection() {
|
||||
shortcutRows.enumerated().forEach { $1.setSelected($0 == selectedShortcutIndex) }
|
||||
shortcutEditorViews.enumerated().forEach { index, view in
|
||||
view.isHidden = index != selectedShortcutIndex || index >= Preferences.shortcutCount
|
||||
}
|
||||
gestureSidebarRow?.setSelected(selectedShortcutIndex == gestureSelectionIndex)
|
||||
gestureEditorView?.isHidden = selectedShortcutIndex != gestureSelectionIndex
|
||||
}
|
||||
|
||||
private static func refreshShortcutCountButtons() {
|
||||
shortcutCountButtons?.setEnabled(Preferences.shortcutCount < Preferences.maxShortcutCount, forSegment: 0)
|
||||
shortcutCountButtons?.setEnabled(Preferences.shortcutCount > Preferences.minShortcutCount && selectedShortcutIndex != gestureSelectionIndex, forSegment: 1)
|
||||
}
|
||||
|
||||
private static func selectShortcut(_ index: Int) {
|
||||
guard (0..<Preferences.shortcutCount).contains(index) else { return }
|
||||
selectedShortcutIndex = index
|
||||
refreshShortcutSelection()
|
||||
refreshShortcutCountButtons()
|
||||
}
|
||||
|
||||
private static func selectGesture() {
|
||||
selectedShortcutIndex = gestureSelectionIndex
|
||||
refreshShortcutSelection()
|
||||
refreshShortcutCountButtons()
|
||||
}
|
||||
|
||||
@objc private static func updateShortcutCount(_ sender: NSSegmentedControl) {
|
||||
let segment = sender.selectedSegment
|
||||
sender.selectedSegment = -1
|
||||
if segment == 0 {
|
||||
addShortcutSlot()
|
||||
} else if segment == 1 && selectedShortcutIndex != gestureSelectionIndex {
|
||||
removeShortcutSlot()
|
||||
}
|
||||
}
|
||||
|
||||
private static func addShortcutSlot() {
|
||||
let currentCount = Preferences.shortcutCount
|
||||
guard currentCount < Preferences.maxShortcutCount else { return }
|
||||
resetShortcutPreferences(currentCount)
|
||||
setAddedShortcutTriggerDefaults(currentCount)
|
||||
selectedShortcutIndex = currentCount
|
||||
Preferences.set("shortcutCount", String(currentCount + 1))
|
||||
initializeShortcutRecorderState(currentCount)
|
||||
}
|
||||
|
||||
private static func removeShortcutSlot() {
|
||||
let currentCount = Preferences.shortcutCount
|
||||
guard currentCount > Preferences.minShortcutCount, selectedShortcutIndex != gestureSelectionIndex else { return }
|
||||
let removedIndex = min(selectedShortcutIndex, currentCount - 1)
|
||||
if removedIndex < currentCount - 1 {
|
||||
for index in removedIndex..<(currentCount - 1) {
|
||||
copyShortcutPreferences(index + 1, index)
|
||||
}
|
||||
}
|
||||
resetShortcutPreferences(currentCount - 1)
|
||||
selectedShortcutIndex = min(removedIndex, currentCount - 2)
|
||||
Preferences.set("shortcutCount", String(currentCount - 1))
|
||||
}
|
||||
|
||||
private static func resetShortcutPreferences(_ index: Int) {
|
||||
removableShortcutPreferences.forEach {
|
||||
Preferences.remove(Preferences.indexToName($0, index), false)
|
||||
}
|
||||
}
|
||||
|
||||
private static func setAddedShortcutTriggerDefaults(_ index: Int) {
|
||||
Preferences.set(Preferences.indexToName("holdShortcut", index), "⌥", false)
|
||||
Preferences.set(Preferences.indexToName("nextWindowShortcut", index), "", false)
|
||||
Preferences.set(Preferences.indexToName("appsToShow", index), AppsToShowPreference.all.indexAsString, false)
|
||||
}
|
||||
|
||||
private static func copyShortcutPreferences(_ fromIndex: Int, _ toIndex: Int) {
|
||||
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)
|
||||
} else {
|
||||
view.isHidden = true
|
||||
Preferences.remove(toKey, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func initializeShortcutRecorderState(_ index: Int) {
|
||||
guard index < Preferences.shortcutCount else { return }
|
||||
let holdControlId = Preferences.indexToName("holdShortcut", index)
|
||||
let nextControlId = Preferences.indexToName("nextWindowShortcut", index)
|
||||
syncShortcutRecorderControlValue(holdControlId)
|
||||
syncShortcutRecorderControlValue(nextControlId)
|
||||
shortcutDropdownPreferences.forEach { syncShortcutDropdownControlValue(Preferences.indexToName($0, index)) }
|
||||
if let holdShortcut = shortcutControls[holdControlId]?.0 {
|
||||
shortcutChangedCallback(holdShortcut)
|
||||
}
|
||||
if let nextWindowShortcut = shortcutControls[nextControlId]?.0 {
|
||||
shortcutChangedCallback(nextWindowShortcut)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private static func syncShortcutDropdownControlValue(_ controlId: String) {
|
||||
let index = Preferences.nameToIndex(controlId)
|
||||
guard index < shortcutEditorViews.count else { return }
|
||||
guard let dropdown = findDropdownControl(shortcutEditorViews[index], controlId) else { return }
|
||||
guard dropdown.numberOfItems > 0 else { return }
|
||||
let selectedIndex = UserDefaults.standard.string(forKey: controlId).flatMap(Int.init) ?? 0
|
||||
dropdown.selectItem(at: min(max(0, selectedIndex), dropdown.numberOfItems - 1))
|
||||
}
|
||||
|
||||
private static func findDropdownControl(_ root: NSView, _ controlId: String) -> NSPopUpButton? {
|
||||
if let dropdown = root as? NSPopUpButton, dropdown.identifier?.rawValue == controlId {
|
||||
return dropdown
|
||||
}
|
||||
for child in root.subviews {
|
||||
if let found = findDropdownControl(child, controlId) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func shortcutTitle(_ index: Int) -> String {
|
||||
return NSLocalizedString("Shortcut", comment: "") + " " + String(index + 1)
|
||||
}
|
||||
|
||||
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)) ?? ""
|
||||
if nextWindowShortcut.isEmpty {
|
||||
return holdShortcut.isEmpty ? NSLocalizedString("Not set", comment: "") : holdShortcut
|
||||
}
|
||||
return holdShortcut + " + " + nextWindowShortcut
|
||||
}
|
||||
|
||||
private static func gestureTitle() -> String {
|
||||
return NSLocalizedString("Gesture", comment: "")
|
||||
}
|
||||
|
||||
private static func gestureSummary() -> String {
|
||||
return Preferences.nextWindowGesture.localizedString
|
||||
}
|
||||
|
||||
private static func refreshGestureRow() {
|
||||
guard let gestureSidebarRow else { return }
|
||||
gestureSidebarRow.setContent(gestureTitle(), gestureSummary())
|
||||
gestureSidebarRow.setSelected(selectedShortcutIndex == gestureSelectionIndex)
|
||||
}
|
||||
|
||||
private static func clearArrangedSubviews(_ stackView: NSStackView) {
|
||||
stackView.arrangedSubviews.forEach {
|
||||
stackView.removeArrangedSubview($0)
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private static func isShortcutPreferenceKey(_ key: String) -> Bool {
|
||||
return (0..<Preferences.maxShortcutCount).contains(where: { index in
|
||||
["holdShortcut", "nextWindowShortcut"].contains { key == Preferences.indexToName($0, index) }
|
||||
})
|
||||
}
|
||||
|
||||
private static func applyActiveShortcutPreferences() {
|
||||
(0..<Preferences.maxShortcutCount).forEach { index in
|
||||
["holdShortcut", "nextWindowShortcut"].forEach { base in
|
||||
let key = Preferences.indexToName(base, index)
|
||||
if index < Preferences.shortcutCount {
|
||||
applyShortcutPreference(key)
|
||||
} else {
|
||||
removeShortcutIfExists(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +653,7 @@ class ControlsTab {
|
||||
|
||||
private static func addShortcut(_ triggerPhase: ShortcutTriggerPhase, _ scope: ShortcutScope, _ shortcut: Shortcut, _ controlId: String, _ index: Int?) {
|
||||
let atShortcut = ATShortcut(shortcut, controlId, scope, triggerPhase, index)
|
||||
removeShortcutIfExists(controlId) // remove the previous shortcut
|
||||
removeShortcutIfExists(controlId)
|
||||
shortcuts[controlId] = atShortcut
|
||||
if scope == .global {
|
||||
KeyboardEvents.addGlobalShortcut(controlId, atShortcut.shortcut)
|
||||
@@ -182,12 +663,11 @@ class ControlsTab {
|
||||
|
||||
static func toggleNativeCommandTabIfNeeded() {
|
||||
let nativeHotkeys: [CGSSymbolicHotKey: (Shortcut) -> Bool] = [
|
||||
.commandTab: { (shortcut) in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandShiftTab: { (shortcut) in CustomRecorderControlTestable.combinedModifiersMatch(shortcut.carbonModifierFlags, UInt32(cmdKey | shiftKey)) && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandKeyAboveTab: { (shortcut) in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_ANSI_Grave },
|
||||
.commandTab: { shortcut in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandShiftTab: { shortcut in CustomRecorderControlTestable.combinedModifiersMatch(shortcut.carbonModifierFlags, UInt32(cmdKey | shiftKey)) && shortcut.carbonKeyCode == kVK_Tab },
|
||||
.commandKeyAboveTab: { shortcut in shortcut.carbonModifierFlags == cmdKey && shortcut.carbonKeyCode == kVK_ANSI_Grave },
|
||||
]
|
||||
var overlappingHotkeys = shortcuts.values.compactMap { (atShortcut) in nativeHotkeys.first { $1(atShortcut.shortcut) }?.key }
|
||||
// if command+tab if bound, disable command+shift+tab always, to avoid confused users
|
||||
var overlappingHotkeys = shortcuts.values.compactMap { atShortcut in nativeHotkeys.first { $1(atShortcut.shortcut) }?.key }
|
||||
if overlappingHotkeys.contains(.commandTab) && !overlappingHotkeys.contains(.commandShiftTab) {
|
||||
overlappingHotkeys.append(.commandShiftTab)
|
||||
}
|
||||
@@ -198,9 +678,17 @@ class ControlsTab {
|
||||
|
||||
@objc static func shortcutChangedCallback(_ sender: NSControl) {
|
||||
let controlId = sender.identifier!.rawValue
|
||||
if isShortcutPreferenceKey(controlId) && Preferences.nameToIndex(controlId) >= Preferences.shortcutCount {
|
||||
return
|
||||
}
|
||||
if controlId.hasPrefix("holdShortcut") {
|
||||
let i = Preferences.nameToIndex(controlId)
|
||||
addShortcut(.up, .global, Shortcut(keyEquivalent: Preferences.holdShortcut[i])!, controlId, i)
|
||||
let holdShortcut = UserDefaults.standard.string(forKey: controlId) ?? ""
|
||||
guard let shortcut = Shortcut(keyEquivalent: holdShortcut) else {
|
||||
removeShortcutIfExists(controlId)
|
||||
return
|
||||
}
|
||||
addShortcut(.up, .global, shortcut, controlId, i)
|
||||
if let nextWindowShortcut = shortcutControls[Preferences.indexToName("nextWindowShortcut", i)]?.0 {
|
||||
nextWindowShortcut.restrictModifiers([(sender as! CustomRecorderControl).objectValue!.modifierFlags])
|
||||
shortcutChangedCallback(nextWindowShortcut)
|
||||
@@ -234,7 +722,7 @@ class ControlsTab {
|
||||
return ""
|
||||
}
|
||||
if controlId.starts(with: "nextWindowShortcut") {
|
||||
let holdShortcut = Preferences.holdShortcut[Preferences.nameToIndex(controlId)]
|
||||
let holdShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) ?? ""
|
||||
return holdShortcut + baseValue
|
||||
}
|
||||
return baseValue
|
||||
@@ -270,8 +758,6 @@ class ControlsTab {
|
||||
}
|
||||
}
|
||||
if !conflicts.isEmpty {
|
||||
// if the app is still launching (App.app.settingsWindow == nil) and we have a conflict
|
||||
// then we don't show the user a dialog, and simply disable vim keys
|
||||
if App.app.settingsWindow == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }) {
|
||||
return false
|
||||
}
|
||||
@@ -327,6 +813,10 @@ class ControlsTab {
|
||||
}
|
||||
|
||||
private static func applyShortcutPreference(_ controlId: String) {
|
||||
if isShortcutPreferenceKey(controlId) && Preferences.nameToIndex(controlId) >= Preferences.shortcutCount {
|
||||
removeShortcutIfExists(controlId)
|
||||
return
|
||||
}
|
||||
if controlId.hasPrefix("holdShortcut") {
|
||||
applyHoldShortcutPreference(controlId)
|
||||
applyShortcutPreference(Preferences.indexToName("nextWindowShortcut", Preferences.nameToIndex(controlId)))
|
||||
@@ -344,7 +834,8 @@ class ControlsTab {
|
||||
|
||||
private static func applyHoldShortcutPreference(_ controlId: String) {
|
||||
let i = Preferences.nameToIndex(controlId)
|
||||
guard let shortcut = Shortcut(keyEquivalent: Preferences.holdShortcut[i]) else {
|
||||
let holdShortcut = UserDefaults.standard.string(forKey: controlId) ?? ""
|
||||
guard let shortcut = Shortcut(keyEquivalent: holdShortcut) else {
|
||||
removeShortcutIfExists(controlId)
|
||||
return
|
||||
}
|
||||
@@ -356,7 +847,7 @@ class ControlsTab {
|
||||
return ""
|
||||
}
|
||||
if controlId.starts(with: "nextWindowShortcut") {
|
||||
let holdShortcut = Preferences.holdShortcut[Preferences.nameToIndex(controlId)]
|
||||
let holdShortcut = UserDefaults.standard.string(forKey: Preferences.indexToName("holdShortcut", Preferences.nameToIndex(controlId))) ?? ""
|
||||
return holdShortcut + baseValue
|
||||
}
|
||||
return baseValue
|
||||
@@ -393,11 +884,20 @@ class ControlsTab {
|
||||
}
|
||||
|
||||
@objc private static func openSystemGestures(_ sender: NSButton) {
|
||||
// Apple doesn't expose the More Gestures tab directly
|
||||
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.Trackpad-Settings.extension")!)
|
||||
}
|
||||
|
||||
static func executeAction(_ action: String) {
|
||||
shortcutsActions[action]!()
|
||||
if let staticAction = shortcutsActions[action] {
|
||||
staticAction()
|
||||
return
|
||||
}
|
||||
if action.hasPrefix("holdShortcut") {
|
||||
App.app.focusTarget()
|
||||
return
|
||||
}
|
||||
if action.hasPrefix("nextWindowShortcut") {
|
||||
App.app.showUiOrCycleSelection(Preferences.nameToIndex(action), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user