add dialog box when replacing existing keybind

This commit is contained in:
nludwig 2026-02-03 22:08:34 -08:00
parent cc27b6ab30
commit 70e0f5a5f2
3 changed files with 116 additions and 2 deletions

View file

@ -67,6 +67,9 @@ keyboard-shortcuts = Keyboard shortcuts
customize-shortcuts = Customize shortcuts customize-shortcuts = Customize shortcuts
shortcut-capture-hint = Press new shortcut, or Esc to cancel shortcut-capture-hint = Press new shortcut, or Esc to cancel
cancel = Cancel cancel = Cancel
replace = Replace
shortcut-replace-title = Replace shortcut?
shortcut-replace-body = { $binding } is already assigned to { $existing }. Replace it with { $new_action }?
no-shortcuts = No shortcuts no-shortcuts = No shortcuts
add-shortcut = + Add add-shortcut = + Add
shortcut-group-clipboard = Clipboard shortcut-group-clipboard = Clipboard

View file

@ -377,6 +377,8 @@ pub enum Message {
Modifiers(Modifiers), Modifiers(Modifiers),
ShortcutCaptureCancel, ShortcutCaptureCancel,
ShortcutCaptureStart(shortcuts::KeyBindAction), ShortcutCaptureStart(shortcuts::KeyBindAction),
ShortcutConflictCancel,
ShortcutConflictReplace,
ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource), ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource),
MouseEnter(pane_grid::Pane), MouseEnter(pane_grid::Pane),
Opacity(u8), Opacity(u8),
@ -444,6 +446,13 @@ pub enum ContextPage {
PasswordManager, PasswordManager,
} }
#[derive(Clone, Debug)]
struct ShortcutConflict {
binding: shortcuts::Binding,
existing_action: shortcuts::KeyBindAction,
new_action: shortcuts::KeyBindAction,
}
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
pub struct App { pub struct App {
core: Core, core: Core,
@ -488,6 +497,8 @@ pub struct App {
show_advanced_font_settings: bool, show_advanced_font_settings: bool,
show_keyboard_shortcuts: bool, show_keyboard_shortcuts: bool,
shortcut_capture: Option<shortcuts::KeyBindAction>, shortcut_capture: Option<shortcuts::KeyBindAction>,
shortcut_conflict: Option<ShortcutConflict>,
shortcut_conflict_overlay_restore: Option<bool>,
modifiers: Modifiers, modifiers: Modifiers,
#[cfg(feature = "password_manager")] #[cfg(feature = "password_manager")]
password_mgr: password_manager::PasswordManager, password_mgr: password_manager::PasswordManager,
@ -577,6 +588,37 @@ impl App {
self.key_binds = key_binds(&self.shortcuts_config); self.key_binds = key_binds(&self.shortcuts_config);
} }
fn apply_shortcut_binding(
&mut self,
binding: shortcuts::Binding,
action: shortcuts::KeyBindAction,
) {
self.shortcuts_config.custom.0.insert(binding, action);
self.save_shortcuts_custom();
}
fn set_context_overlay(&mut self, overlay: bool) {
if self.core.window.context_is_overlay != overlay {
self.core.window.context_is_overlay = overlay;
self.core.set_show_context(self.core.window.show_context);
}
}
fn begin_shortcut_conflict(&mut self, conflict: ShortcutConflict) {
if self.shortcut_conflict.is_none() {
self.shortcut_conflict_overlay_restore = Some(self.core.window.context_is_overlay);
self.set_context_overlay(false);
}
self.shortcut_conflict = Some(conflict);
}
fn clear_shortcut_conflict(&mut self) {
self.shortcut_conflict = None;
if let Some(overlay) = self.shortcut_conflict_overlay_restore.take() {
self.set_context_overlay(overlay);
}
}
fn update_config(&mut self) -> Task<Message> { fn update_config(&mut self) -> Task<Message> {
let theme = self.config.app_theme.theme(); let theme = self.config.app_theme.theme();
@ -1744,6 +1786,8 @@ impl Application for App {
show_advanced_font_settings: false, show_advanced_font_settings: false,
show_keyboard_shortcuts: false, show_keyboard_shortcuts: false,
shortcut_capture: None, shortcut_capture: None,
shortcut_conflict: None,
shortcut_conflict_overlay_restore: None,
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
#[cfg(feature = "password_manager")] #[cfg(feature = "password_manager")]
password_mgr: Default::default(), password_mgr: Default::default(),
@ -2284,6 +2328,12 @@ impl Application for App {
config_set!(focus_follow_mouse, focus_follow_mouse); config_set!(focus_follow_mouse, focus_follow_mouse);
} }
Message::Key(modifiers, key) => { Message::Key(modifiers, key) => {
if self.shortcut_conflict.is_some() {
if key == Key::Named(Named::Escape) {
self.clear_shortcut_conflict();
}
return Task::none();
}
if let Some(action) = self.shortcut_capture { if let Some(action) = self.shortcut_capture {
if key == Key::Named(Named::Escape) { if key == Key::Named(Named::Escape) {
self.shortcut_capture = None; self.shortcut_capture = None;
@ -2291,8 +2341,20 @@ impl Application for App {
} }
if let Some(binding) = shortcuts::binding_from_key(modifiers, key) { if let Some(binding) = shortcuts::binding_from_key(modifiers, key) {
self.shortcut_capture = None; self.shortcut_capture = None;
self.shortcuts_config.custom.0.insert(binding, action); if let Some(existing_action) =
self.save_shortcuts_custom(); self.shortcuts_config.action_for_binding(&binding)
{
if existing_action != action {
self.begin_shortcut_conflict(ShortcutConflict {
binding,
existing_action,
new_action: action,
});
return Task::none();
}
return Task::none();
}
self.apply_shortcut_binding(binding, action);
} }
return Task::none(); return Task::none();
} }
@ -2337,6 +2399,15 @@ impl Application for App {
Message::ShortcutCaptureStart(action) => { Message::ShortcutCaptureStart(action) => {
self.shortcut_capture = Some(action); self.shortcut_capture = Some(action);
} }
Message::ShortcutConflictCancel => {
self.clear_shortcut_conflict();
}
Message::ShortcutConflictReplace => {
if let Some(conflict) = self.shortcut_conflict.clone() {
self.apply_shortcut_binding(conflict.binding, conflict.new_action);
}
self.clear_shortcut_conflict();
}
Message::ShortcutRemove(binding, source) => { Message::ShortcutRemove(binding, source) => {
match source { match source {
shortcuts::BindingSource::Default => { shortcuts::BindingSource::Default => {
@ -2988,6 +3059,34 @@ impl Application for App {
}) })
} }
fn dialog(&self) -> Option<Element<'_, Message>> {
let conflict = self.shortcut_conflict.as_ref()?;
let binding = shortcuts::binding_display(&conflict.binding);
let existing = shortcuts::action_label(conflict.existing_action);
let new_action = shortcuts::action_label(conflict.new_action);
let body = fl!(
"shortcut-replace-body",
binding = binding.as_str(),
existing = existing.as_str(),
new_action = new_action.as_str()
);
Some(
widget::dialog()
.title(fl!("shortcut-replace-title"))
.body(body)
.primary_action(
widget::button::suggested(fl!("replace"))
.on_press(Message::ShortcutConflictReplace),
)
.secondary_action(
widget::button::standard(fl!("cancel"))
.on_press(Message::ShortcutConflictCancel),
)
.into(),
)
}
fn header_start(&self) -> Vec<Element<'_, Self::Message>> { fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
vec![menu_bar(&self.core, &self.config, &self.key_binds)] vec![menu_bar(&self.core, &self.config, &self.key_binds)]
} }

View file

@ -218,6 +218,18 @@ impl ShortcutsConfig {
bindings bindings
} }
pub fn action_for_binding(&self, binding: &Binding) -> Option<KeyBindAction> {
if let Some(action) = self.custom.0.get(binding) {
if *action == KeyBindAction::Unbind {
return None;
}
return Some(*action);
}
let defaults = self.defaults_or_fallback();
defaults.0.get(binding).copied()
}
fn defaults_or_fallback(&self) -> Shortcuts { fn defaults_or_fallback(&self) -> Shortcuts {
if self.defaults.0.is_empty() { if self.defaults.0.is_empty() {
fallback_shortcuts() fallback_shortcuts()