mirror of
https://github.com/lwouis/alt-tab-macos.git
synced 2026-05-24 11:20:36 +00:00
feat: add vim key window navigation (closes #1229)
This commit is contained in:
@@ -376,6 +376,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"View existing discussions" = "View existing discussions";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Vim keys" = "Vim keys";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Vim keys already assigned to other actions:\n%@" = "Vim keys already assigned to other actions:\n%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Visible Spaces" = "Visible Spaces";
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class Preferences {
|
||||
"quitAppShortcut": "Q",
|
||||
"hideShowAppShortcut": "H",
|
||||
"arrowKeysEnabled": "true",
|
||||
"vimKeysEnabled": "false",
|
||||
"mouseHoverEnabled": "false",
|
||||
"cursorFollowFocusEnabled": "false",
|
||||
"showMinimizedWindows": ShowHowPreference.show.rawValue,
|
||||
@@ -125,6 +126,7 @@ class Preferences {
|
||||
static var quitAppShortcut: String { defaults.string("quitAppShortcut") }
|
||||
static var hideShowAppShortcut: String { defaults.string("hideShowAppShortcut") }
|
||||
static var arrowKeysEnabled: Bool { defaults.bool("arrowKeysEnabled") }
|
||||
static var vimKeysEnabled: Bool { defaults.bool("vimKeysEnabled") }
|
||||
static var mouseHoverEnabled: Bool { defaults.bool("mouseHoverEnabled") }
|
||||
static var cursorFollowFocusEnabled: Bool { defaults.bool("cursorFollowFocusEnabled") }
|
||||
static var showTabsAsWindows: Bool { defaults.bool("showTabsAsWindows") }
|
||||
@@ -194,6 +196,11 @@ class Preferences {
|
||||
UserDefaults.cache.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
static func remove(_ key: String) {
|
||||
defaults.removeObject(forKey: key)
|
||||
UserDefaults.cache.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
static var all: [String: Any] { defaults.persistentDomain(forName: App.id)! }
|
||||
|
||||
static func migratePreferences() {
|
||||
|
||||
@@ -50,11 +50,13 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
|
||||
|
||||
func alertIfSameShortcutAlreadyAssigned(_ shortcut: Shortcut, _ shortcutAlreadyAssigned: ATShortcut) {
|
||||
let isArrowKeys = ["←", "→", "↑", "↓"].contains(shortcutAlreadyAssigned.id)
|
||||
let existing = ControlsTab.shortcutControls[shortcutAlreadyAssigned.id]
|
||||
let isVimKeys = shortcutAlreadyAssigned.id.starts(with: "vimCycle")
|
||||
let existingShortcutLabel = ControlsTab.shortcutControls[shortcutAlreadyAssigned.id]
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "")
|
||||
alert.informativeText = String(format: NSLocalizedString("Shortcut already assigned to another action: %@", comment: ""), (isArrowKeys ? "Arrow keys" : existing!.1).replacingOccurrences(of: " ", with: "\u{00A0}"))
|
||||
alert.informativeText = String(format: NSLocalizedString("Shortcut already assigned to another action: %@", comment: ""),
|
||||
(isArrowKeys ? "Arrow keys" : (isVimKeys ? "Vim keys" : existingShortcutLabel!.1)).replacingOccurrences(of: " ", with: "\u{00A0}"))
|
||||
if !id.starts(with: "holdShortcut") {
|
||||
alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")).setAccessibilityFocused(true)
|
||||
}
|
||||
@@ -69,10 +71,14 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
|
||||
ControlsTab.arrowKeysCheckbox.state = .off
|
||||
ControlsTab.arrowKeysEnabledCallback(ControlsTab.arrowKeysCheckbox)
|
||||
LabelAndControl.controlWasChanged(ControlsTab.arrowKeysCheckbox, nil)
|
||||
} else if isVimKeys {
|
||||
ControlsTab.vimKeysCheckbox.state = .off
|
||||
ControlsTab.vimKeysEnabledCallback(ControlsTab.vimKeysCheckbox)
|
||||
LabelAndControl.controlWasChanged(ControlsTab.vimKeysCheckbox, nil)
|
||||
} else {
|
||||
existing!.0.objectValue = nil
|
||||
ControlsTab.shortcutChangedCallback(existing!.0)
|
||||
LabelAndControl.controlWasChanged(existing!.0, shortcutAlreadyAssigned.id)
|
||||
existingShortcutLabel!.0.objectValue = nil
|
||||
ControlsTab.shortcutChangedCallback(existingShortcutLabel!.0)
|
||||
LabelAndControl.controlWasChanged(existingShortcutLabel!.0, shortcutAlreadyAssigned.id)
|
||||
}
|
||||
ControlsTab.shortcutControls[id]!.0.objectValue = shortcut
|
||||
ControlsTab.shortcutChangedCallback(self)
|
||||
|
||||
@@ -21,6 +21,10 @@ class ControlsTab {
|
||||
"←": { App.app.cycleSelection(.left) },
|
||||
"↑": { App.app.cycleSelection(.up) },
|
||||
"↓": { App.app.cycleSelection(.down) },
|
||||
"vimCycleRight": { App.app.cycleSelection(.right) },
|
||||
"vimCycleLeft": { App.app.cycleSelection(.left) },
|
||||
"vimCycleUp": { App.app.cycleSelection(.up) },
|
||||
"vimCycleDown": { App.app.cycleSelection(.down) },
|
||||
"cancelShortcut": { App.app.hideUi() },
|
||||
"closeWindowShortcut": { App.app.closeSelectedWindow() },
|
||||
"minDeminWindowShortcut": { App.app.minDeminSelectedWindow() },
|
||||
@@ -29,6 +33,7 @@ class ControlsTab {
|
||||
"hideShowAppShortcut": { App.app.hideShowSelectedApp() },
|
||||
]
|
||||
static var arrowKeysCheckbox: NSButton!
|
||||
static var vimKeysCheckbox: NSButton!
|
||||
|
||||
static func initTab() -> NSView {
|
||||
let focusWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Focus selected window", comment: ""), "focusWindowShortcut", Preferences.focusWindowShortcut, labelPosition: .right)
|
||||
@@ -40,11 +45,13 @@ class ControlsTab {
|
||||
let quitAppShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Quit app", comment: ""), "quitAppShortcut", Preferences.quitAppShortcut, labelPosition: .right)
|
||||
let hideShowAppShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Hide/Show app", comment: ""), "hideShowAppShortcut", Preferences.hideShowAppShortcut, labelPosition: .right)
|
||||
let enableArrows = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Arrow keys", comment: ""), "arrowKeysEnabled", extraAction: ControlsTab.arrowKeysEnabledCallback, labelPosition: .right)
|
||||
let enableVimKeys = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Vim keys", comment: ""), "vimKeysEnabled", extraAction: ControlsTab.vimKeysEnabledCallback, labelPosition: .right)
|
||||
arrowKeysCheckbox = enableArrows[0] as? NSButton
|
||||
vimKeysCheckbox = enableVimKeys[0] as? NSButton
|
||||
let enableMouse = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Mouse hover", comment: ""), "mouseHoverEnabled", labelPosition: .right)
|
||||
let enableCursorFollowFocus = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Cursor follows focus", comment: ""), "cursorFollowFocusEnabled", labelPosition: .right)
|
||||
let selectWindowcheckboxesExplanations = LabelAndControl.makeLabel(NSLocalizedString("Also select windows using:", comment: ""))
|
||||
let selectWindowCheckboxes = StackView([StackView(enableArrows), StackView(enableMouse)], .vertical)
|
||||
let selectWindowCheckboxes = StackView([StackView(enableArrows), StackView(enableVimKeys), StackView(enableMouse)], .vertical)
|
||||
let miscCheckboxesExplanations = LabelAndControl.makeLabel(NSLocalizedString("Miscellaneous:", comment: ""))
|
||||
let miscCheckboxes = StackView([StackView(enableCursorFollowFocus)], .vertical)
|
||||
let shortcuts = StackView([focusWindowShortcut, previousWindowShortcut, cancelShortcut, closeWindowShortcut, minDeminWindowShortcut, toggleFullscreenWindowShortcut, quitAppShortcut, hideShowAppShortcut].map { (view: [NSView]) in StackView(view) }, .vertical)
|
||||
@@ -63,6 +70,7 @@ class ControlsTab {
|
||||
])
|
||||
|
||||
ControlsTab.arrowKeysEnabledCallback(arrowKeysCheckbox)
|
||||
ControlsTab.vimKeysEnabledCallback(vimKeysCheckbox)
|
||||
// trigger shortcutChanged for these shortcuts to trigger .restrictModifiers
|
||||
[holdShortcut, holdShortcut2, holdShortcut3, holdShortcut4, holdShortcut5].forEach { ControlsTab.shortcutChangedCallback($0[1] as! NSControl) }
|
||||
[nextWindowShortcut, nextWindowShortcut2, nextWindowShortcut3, nextWindowShortcut4, nextWindowShortcut5].forEach { ControlsTab.shortcutChangedCallback($0[0] as! NSControl) }
|
||||
@@ -219,6 +227,65 @@ class ControlsTab {
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func vimKeysEnabledCallback(_ sender: NSControl) {
|
||||
let keyActions = [
|
||||
"h": "vimCycleLeft",
|
||||
"l": "vimCycleRight",
|
||||
"k": "vimCycleUp",
|
||||
"j": "vimCycleDown"
|
||||
]
|
||||
if (sender as! NSButton).state == .on {
|
||||
if App.app.preferencesWindow != nil && isClearVimKeysSuccessful() {
|
||||
keyActions.forEach { addShortcut(.down, .local, Shortcut(keyEquivalent: $0)!, $1, nil) }
|
||||
} else {
|
||||
(sender as! NSButton).state = .off
|
||||
Preferences.remove("vimKeysEnabled")
|
||||
}
|
||||
} else {
|
||||
keyActions.forEach { removeShortcutIfExists($1) }
|
||||
}
|
||||
}
|
||||
|
||||
private static func isClearVimKeysSuccessful() -> Bool {
|
||||
let vimKeys = ["h", "l", "j", "k"]
|
||||
var conflicts = [String: String]()
|
||||
shortcuts.forEach {
|
||||
let keymap = $1.shortcut.characters
|
||||
if keymap != nil && vimKeys.contains(keymap!) {
|
||||
let control_id = $1.id
|
||||
conflicts[control_id] = shortcutControls[control_id]!.1
|
||||
}
|
||||
}
|
||||
if !conflicts.isEmpty {
|
||||
if !shouldClearConflictingShortcuts(conflicts.map { $0.value }) {
|
||||
return false
|
||||
}
|
||||
conflicts.forEach {
|
||||
removeShortcutIfExists($0.key)
|
||||
let existing = shortcutControls[$0.key]
|
||||
if existing != nil {
|
||||
existing!.0.objectValue = nil
|
||||
shortcutChangedCallback(existing!.0)
|
||||
LabelAndControl.controlWasChanged(existing!.0, $0.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private static func shouldClearConflictingShortcuts(_ conflicts: [String]) -> Bool {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "")
|
||||
let informativeText = conflicts.map { "• " + $0 }.joined(separator: "\n")
|
||||
alert.informativeText = String(format: NSLocalizedString("Vim keys already assigned to other actions:\n%@", comment: ""), informativeText.replacingOccurrences(of: " ", with: "\u{00A0}"))
|
||||
alert.addButton(withTitle: NSLocalizedString("Unassign existing shortcut and continue", comment: "")).setAccessibilityFocused(true)
|
||||
let cancelButton = alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
cancelButton.keyEquivalent = "\u{1b}"
|
||||
let userChoice = alert.runModal()
|
||||
return userChoice == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
private static func removeShortcutIfExists(_ controlId: String) {
|
||||
if let atShortcut = shortcuts[controlId] {
|
||||
if atShortcut.scope == .global {
|
||||
|
||||
Reference in New Issue
Block a user