Shortcuts: deterministic dispatch between panels, physical and logical ranked together

- shortcuts::pick chooses the binding for a key press: narrowest scope, then a
  physical-key binding, then the first. When two other panels (or two active
  contexts) claim the chord at the same rank, neither runs and the status bar
  says which panels own it.
- Physical and logical bindings compete by scope rank in one pass, so a
  narrower logical binding beats a physical one of an unrelated panel.
- An additional viewer (PanelKind::Viewer) ranks as the Composition panel.
- Two contexts on the same keys are reported as a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pravdomil
2026-10-31 22:35:33 +01:00
co-authored by Claude Opus 5.5
parent 189f31d6d1
commit aa149d7bbc
4 changed files with 113 additions and 40 deletions
+43
View File
@@ -606,6 +606,49 @@ fn frontend_commands_are_bindable_with_panel_scope() {
// A panel shortcut conflicts with an application shortcut on the same keys.
let c = s.shortcuts().conflicts(&b, "H");
assert!(c.iter().any(|c| c.key == "tool.hand"));
// A registry command that only the Project panel runs is scoped to it.
assert_eq!(s.shortcuts().find("project.deleteWithoutConfirmation").unwrap().scope, "Project");
}
/// The binding that runs a key press: the narrowest scope; a physical binding at the same rank;
/// nothing when two other panels (or two active contexts) claim it at the same rank.
#[test]
fn shortcut_dispatch_picks_the_narrowest_scope_deterministically() {
use crate::shortcuts::pick;
// Two unrelated panels, neither focused: neither runs, in either order.
let both = [("Project", false, "delete"), ("RenderQueue", false, "duplicate")];
assert_eq!(pick(both, "Timeline", &[]), Err(vec!["Project".to_string(), "RenderQueue".into()]));
assert_eq!(pick([both[1], both[0]], "Timeline", &[]), Err(vec!["RenderQueue".to_string(), "Project".into()]));
// The focused one of them wins.
assert_eq!(pick(both, "RenderQueue", &[]), Ok(Some("duplicate")));
// A single other panel's shortcut runs when nothing nearer claims the chord.
assert_eq!(pick([("Project", false, "delete")], "Timeline", &[]), Ok(Some("delete")));
// An application binding beats an unrelated panel's binding, also a physical one.
assert_eq!(pick([("RenderQueue", true, "duplicate"), ("Application", false, "app")], "Timeline", &[]), Ok(Some("app")));
// At the same rank a physical binding wins, in either order.
assert_eq!(pick([("Application", false, "logical"), ("Application", true, "physical")], "Timeline", &[]), Ok(Some("physical")));
assert_eq!(pick([("Application", true, "physical"), ("Application", false, "logical")], "Timeline", &[]), Ok(Some("physical")));
// A Composition binding runs in the Composition panel over the application's.
assert_eq!(pick([("Application", false, "app"), ("Composition", false, "comp")], "Composition", &[]), Ok(Some("comp")));
// Two active contexts tie; an inactive one doesn't run.
assert_eq!(pick([("Mask", false, "mask"), ("Paint", false, "paint")], "Composition", &["Paint", "Mask"]), Err(vec!["Mask".to_string(), "Paint".into()]));
assert_eq!(pick([("Mask", false, "mask"), ("Paint", false, "paint")], "Composition", &["Paint"]), Ok(Some("paint")));
// The same scope twice (a reported conflict): the first one.
assert_eq!(pick([("Timeline", false, "a"), ("Timeline", false, "b")], "Timeline", &[]), Ok(Some("a")));
}
/// Two contexts can be active at once, so their shortcuts on the same keys conflict.
#[test]
fn context_shortcuts_on_the_same_keys_conflict() {
let mut s = Session::default();
s.set_ui_commands(vec![
crate::shortcuts::UiCommand { id: "test.mask".into(), label: "Mask".into(), shortcut: Some("F9".into()), scope: Some("Mask".into()) },
crate::shortcuts::UiCommand { id: "test.paint".into(), label: "Paint".into(), shortcut: Some("F9".into()), scope: Some("Paint".into()) },
]);
let t = s.shortcuts();
assert!(t.all_conflicts().iter().any(|(a, c)| a == "test.mask" && c.key == "test.paint" && !c.overrides), "{:?}", t.all_conflicts());
let b = t.find("test.mask").unwrap();
assert!(t.conflicts(b, "F9").iter().any(|c| c.key == "test.paint" && !c.overrides));
}
// ---------------------------------------------------------------- auto-save / recovery