keymap_ui: Keyboard navigation for keybind edit modal (#34482)
Adds keyboard navigation to the keybind edit modal. Using up/down arrows to select the previous/next input editor, and `cmd-enter` to save + `escape` to exit Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
parent
3ecdfc9b5a
commit
ebbf02e25b
3 changed files with 228 additions and 97 deletions
|
@ -1129,5 +1129,21 @@
|
||||||
"escape escape escape": "keystroke_input::StopRecording",
|
"escape escape escape": "keystroke_input::StopRecording",
|
||||||
"delete": "keystroke_input::ClearKeystrokes"
|
"delete": "keystroke_input::ClearKeystrokes"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "KeybindEditorModal",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-enter": "menu::Confirm",
|
||||||
|
"escape": "menu::Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "KeybindEditorModal > Editor",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"up": "menu::SelectPrevious",
|
||||||
|
"down": "menu::SelectNext"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1226,5 +1226,21 @@
|
||||||
"escape escape escape": "keystroke_input::StopRecording",
|
"escape escape escape": "keystroke_input::StopRecording",
|
||||||
"delete": "keystroke_input::ClearKeystrokes"
|
"delete": "keystroke_input::ClearKeystrokes"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "KeybindEditorModal",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-enter": "menu::Confirm",
|
||||||
|
"escape": "menu::Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "KeybindEditorModal > Editor",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"up": "menu::SelectPrevious",
|
||||||
|
"down": "menu::SelectNext"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1451,6 +1451,7 @@ struct KeybindingEditorModal {
|
||||||
error: Option<InputError>,
|
error: Option<InputError>,
|
||||||
keymap_editor: Entity<KeymapEditor>,
|
keymap_editor: Entity<KeymapEditor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
focus_state: KeybindingEditorModalFocusState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for KeybindingEditorModal {}
|
impl ModalView for KeybindingEditorModal {}
|
||||||
|
@ -1539,6 +1540,14 @@ impl KeybindingEditorModal {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let focus_state = KeybindingEditorModalFocusState::new(
|
||||||
|
keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)),
|
||||||
|
input_editor.as_ref().map(|input_editor| {
|
||||||
|
input_editor.read_with(cx, |input_editor, cx| input_editor.focus_handle(cx))
|
||||||
|
}),
|
||||||
|
context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
creating: create,
|
creating: create,
|
||||||
editing_keybind,
|
editing_keybind,
|
||||||
|
@ -1550,6 +1559,7 @@ impl KeybindingEditorModal {
|
||||||
error: None,
|
error: None,
|
||||||
keymap_editor,
|
keymap_editor,
|
||||||
workspace,
|
workspace,
|
||||||
|
focus_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1731,6 +1741,33 @@ impl KeybindingEditorModal {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn key_context(&self) -> KeyContext {
|
||||||
|
let mut key_context = KeyContext::new_with_defaults();
|
||||||
|
key_context.add("KeybindEditorModal");
|
||||||
|
key_context
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.focus_state.focus_next(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_prev(
|
||||||
|
&mut self,
|
||||||
|
_: &menu::SelectPrevious,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.focus_state.focus_previous(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.save(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(DismissEvent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for KeybindingEditorModal {
|
impl Render for KeybindingEditorModal {
|
||||||
|
@ -1739,93 +1776,156 @@ impl Render for KeybindingEditorModal {
|
||||||
let action_name =
|
let action_name =
|
||||||
command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
|
command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
|
||||||
|
|
||||||
v_flex().w(rems(34.)).elevation_3(cx).child(
|
v_flex()
|
||||||
Modal::new("keybinding_editor_modal", None)
|
.w(rems(34.))
|
||||||
.header(
|
.elevation_3(cx)
|
||||||
ModalHeader::new().child(
|
.key_context(self.key_context())
|
||||||
v_flex()
|
.on_action(cx.listener(Self::focus_next))
|
||||||
.pb_1p5()
|
.on_action(cx.listener(Self::focus_prev))
|
||||||
.mb_1()
|
.on_action(cx.listener(Self::confirm))
|
||||||
.gap_0p5()
|
.on_action(cx.listener(Self::cancel))
|
||||||
.border_b_1()
|
.child(
|
||||||
.border_color(theme.border_variant)
|
Modal::new("keybinding_editor_modal", None)
|
||||||
.child(Label::new(action_name))
|
.header(
|
||||||
.when_some(self.editing_keybind.action_docs, |this, docs| {
|
ModalHeader::new().child(
|
||||||
this.child(
|
v_flex()
|
||||||
Label::new(docs).size(LabelSize::Small).color(Color::Muted),
|
.pb_1p5()
|
||||||
)
|
.mb_1()
|
||||||
}),
|
.gap_0p5()
|
||||||
),
|
.border_b_1()
|
||||||
)
|
.border_color(theme.border_variant)
|
||||||
.section(
|
.child(Label::new(action_name))
|
||||||
Section::new().child(
|
.when_some(self.editing_keybind.action_docs, |this, docs| {
|
||||||
v_flex()
|
this.child(
|
||||||
.gap_2()
|
Label::new(docs).size(LabelSize::Small).color(Color::Muted),
|
||||||
.child(
|
)
|
||||||
v_flex()
|
}),
|
||||||
.child(Label::new("Edit Keystroke"))
|
),
|
||||||
.gap_1()
|
)
|
||||||
.child(self.keybind_editor.clone()),
|
.section(
|
||||||
)
|
Section::new().child(
|
||||||
.when_some(self.input_editor.clone(), |this, editor| {
|
v_flex()
|
||||||
this.child(
|
.gap_2()
|
||||||
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_1p5()
|
.child(Label::new("Edit Keystroke"))
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Label::new("Edit Arguments"))
|
.child(self.keybind_editor.clone()),
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.py_1()
|
|
||||||
.px_1p5()
|
|
||||||
.rounded_lg()
|
|
||||||
.bg(theme.editor_background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.border_variant)
|
|
||||||
.child(editor),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
.when_some(self.input_editor.clone(), |this, editor| {
|
||||||
.child(self.context_editor.clone())
|
this.child(
|
||||||
.when_some(self.error.as_ref(), |this, error| {
|
v_flex()
|
||||||
this.child(
|
.mt_1p5()
|
||||||
Banner::new()
|
.gap_1()
|
||||||
.map(|banner| match error {
|
.child(Label::new("Edit Arguments"))
|
||||||
InputError::Error(_) => {
|
.child(
|
||||||
banner.severity(ui::Severity::Error)
|
div()
|
||||||
}
|
.w_full()
|
||||||
InputError::Warning(_) => {
|
.py_1()
|
||||||
banner.severity(ui::Severity::Warning)
|
.px_1p5()
|
||||||
}
|
.rounded_lg()
|
||||||
})
|
.bg(theme.editor_background)
|
||||||
// For some reason, the div overflows its container to the
|
.border_1()
|
||||||
//right. The padding accounts for that.
|
.border_color(theme.border_variant)
|
||||||
.child(
|
.child(editor),
|
||||||
div()
|
),
|
||||||
.size_full()
|
)
|
||||||
.pr_2()
|
})
|
||||||
.child(Label::new(error.content())),
|
.child(self.context_editor.clone())
|
||||||
),
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
Banner::new()
|
||||||
|
.map(|banner| match error {
|
||||||
|
InputError::Error(_) => {
|
||||||
|
banner.severity(ui::Severity::Error)
|
||||||
|
}
|
||||||
|
InputError::Warning(_) => {
|
||||||
|
banner.severity(ui::Severity::Warning)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// For some reason, the div overflows its container to the
|
||||||
|
//right. The padding accounts for that.
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.pr_2()
|
||||||
|
.child(Label::new(error.content())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.footer(
|
||||||
|
ModalFooter::new().end_slot(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Button::new("cancel", "Cancel")
|
||||||
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||||
)
|
)
|
||||||
}),
|
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|
||||||
|
|this, _event, _window, cx| {
|
||||||
|
this.save(cx);
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.footer(
|
}
|
||||||
ModalFooter::new().end_slot(
|
}
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
struct KeybindingEditorModalFocusState {
|
||||||
.child(
|
handles: Vec<FocusHandle>,
|
||||||
Button::new("cancel", "Cancel")
|
}
|
||||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
|
||||||
)
|
impl KeybindingEditorModalFocusState {
|
||||||
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|
fn new(
|
||||||
|this, _event, _window, cx| {
|
keystrokes: FocusHandle,
|
||||||
this.save(cx);
|
action_input: Option<FocusHandle>,
|
||||||
},
|
context: FocusHandle,
|
||||||
))),
|
) -> Self {
|
||||||
),
|
Self {
|
||||||
),
|
handles: Vec::from_iter(
|
||||||
)
|
[Some(keystrokes), action_input, Some(context)]
|
||||||
|
.into_iter()
|
||||||
|
.flatten(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
|
||||||
|
self.handles
|
||||||
|
.iter()
|
||||||
|
.position(|handle| handle.contains_focused(window, cx))
|
||||||
|
.map(|i| i as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_index(&self, mut index: i32, window: &mut Window) {
|
||||||
|
if index < 0 {
|
||||||
|
index = self.handles.len() as i32 - 1;
|
||||||
|
}
|
||||||
|
if index >= self.handles.len() as i32 {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
window.focus(&self.handles[index as usize]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_next(&self, window: &mut Window, cx: &App) {
|
||||||
|
let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
|
||||||
|
index + 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.focus_index(index_to_focus, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_previous(&self, window: &mut Window, cx: &App) {
|
||||||
|
let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
|
||||||
|
index - 1
|
||||||
|
} else {
|
||||||
|
self.handles.len() as i32 - 1
|
||||||
|
};
|
||||||
|
self.focus_index(index_to_focus, window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2207,24 +2307,23 @@ impl KeystrokeInput {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
|
let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
|
||||||
if close_keystroke_result == CloseKeystrokeResult::Close {
|
if close_keystroke_result != CloseKeystrokeResult::Close {
|
||||||
return;
|
if let Some(last) = self.keystrokes.last()
|
||||||
}
|
&& last.key.is_empty()
|
||||||
if let Some(last) = self.keystrokes.last()
|
&& self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX
|
||||||
&& last.key.is_empty()
|
|
||||||
&& self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX
|
|
||||||
{
|
|
||||||
self.keystrokes.pop();
|
|
||||||
}
|
|
||||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
|
||||||
if close_keystroke_result == CloseKeystrokeResult::Partial
|
|
||||||
&& self.close_keystrokes_start.is_none()
|
|
||||||
{
|
{
|
||||||
self.close_keystrokes_start = Some(self.keystrokes.len());
|
self.keystrokes.pop();
|
||||||
}
|
}
|
||||||
self.keystrokes.push(keystroke.clone());
|
|
||||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
if close_keystroke_result == CloseKeystrokeResult::Partial
|
||||||
|
&& self.close_keystrokes_start.is_none()
|
||||||
|
{
|
||||||
|
self.close_keystrokes_start = Some(self.keystrokes.len());
|
||||||
|
}
|
||||||
|
self.keystrokes.push(keystroke.clone());
|
||||||
|
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||||
|
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue