feat: add vim key window navigation (closes #1229)

This commit is contained in:
Sai Chand
2023-10-10 21:53:16 +02:00
committed by lwouis
parent 3f64463580
commit 5cf7f99517
4 changed files with 92 additions and 6 deletions
+6
View File
@@ -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";
+7
View File
@@ -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 {