diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 6b8ce21..ba0a5bd 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -67,6 +67,9 @@ keyboard-shortcuts = Keyboard shortcuts customize-shortcuts = Customize shortcuts shortcut-capture-hint = Press new shortcut, or Esc to 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 add-shortcut = + Add shortcut-group-clipboard = Clipboard diff --git a/src/main.rs b/src/main.rs index c37f08a..a627f82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -377,6 +377,8 @@ pub enum Message { Modifiers(Modifiers), ShortcutCaptureCancel, ShortcutCaptureStart(shortcuts::KeyBindAction), + ShortcutConflictCancel, + ShortcutConflictReplace, ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource), MouseEnter(pane_grid::Pane), Opacity(u8), @@ -444,6 +446,13 @@ pub enum ContextPage { PasswordManager, } +#[derive(Clone, Debug)] +struct ShortcutConflict { + binding: shortcuts::Binding, + existing_action: shortcuts::KeyBindAction, + new_action: shortcuts::KeyBindAction, +} + /// The [`App`] stores application-specific state. pub struct App { core: Core, @@ -488,6 +497,8 @@ pub struct App { show_advanced_font_settings: bool, show_keyboard_shortcuts: bool, shortcut_capture: Option, + shortcut_conflict: Option, + shortcut_conflict_overlay_restore: Option, modifiers: Modifiers, #[cfg(feature = "password_manager")] password_mgr: password_manager::PasswordManager, @@ -577,6 +588,37 @@ impl App { 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 { let theme = self.config.app_theme.theme(); @@ -1744,6 +1786,8 @@ impl Application for App { show_advanced_font_settings: false, show_keyboard_shortcuts: false, shortcut_capture: None, + shortcut_conflict: None, + shortcut_conflict_overlay_restore: None, modifiers: Modifiers::empty(), #[cfg(feature = "password_manager")] password_mgr: Default::default(), @@ -2284,6 +2328,12 @@ impl Application for App { config_set!(focus_follow_mouse, focus_follow_mouse); } 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 key == Key::Named(Named::Escape) { self.shortcut_capture = None; @@ -2291,8 +2341,20 @@ impl Application for App { } if let Some(binding) = shortcuts::binding_from_key(modifiers, key) { self.shortcut_capture = None; - self.shortcuts_config.custom.0.insert(binding, action); - self.save_shortcuts_custom(); + if let Some(existing_action) = + 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(); } @@ -2337,6 +2399,15 @@ impl Application for App { Message::ShortcutCaptureStart(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) => { match source { shortcuts::BindingSource::Default => { @@ -2988,6 +3059,34 @@ impl Application for App { }) } + fn dialog(&self) -> Option> { + 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> { vec![menu_bar(&self.core, &self.config, &self.key_binds)] } diff --git a/src/shortcuts.rs b/src/shortcuts.rs index 74c9ac7..e5bbd5f 100644 --- a/src/shortcuts.rs +++ b/src/shortcuts.rs @@ -218,6 +218,18 @@ impl ShortcutsConfig { bindings } + pub fn action_for_binding(&self, binding: &Binding) -> Option { + 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 { if self.defaults.0.is_empty() { fallback_shortcuts()