Skip to content

Commit

Permalink
feat: add vim key window navigation (closes #1229)
Browse files Browse the repository at this point in the history
  • Loading branch information
skolj authored and lwouis committed Oct 10, 2023
1 parent 3f64463 commit 5cf7f99
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 6 deletions.
6 changes: 6 additions & 0 deletions resources/l10n/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
7 changes: 7 additions & 0 deletions src/logic/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Preferences {
"quitAppShortcut": "Q",
"hideShowAppShortcut": "H",
"arrowKeysEnabled": "true",
"vimKeysEnabled": "false",
"mouseHoverEnabled": "false",
"cursorFollowFocusEnabled": "false",
"showMinimizedWindows": ShowHowPreference.show.rawValue,
Expand Down Expand Up @@ -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") }
Expand Down Expand Up @@ -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() {
Expand Down
16 changes: 11 additions & 5 deletions src/ui/generic-components/CustomRecorderControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
69 changes: 68 additions & 1 deletion src/ui/preferences-window/tabs/ControlsTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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) }
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 5cf7f99

Please sign in to comment.