diff --git a/crates/keymap/README.md b/crates/keymap/README.md
index 0984465b..472c2883 100644
--- a/crates/keymap/README.md
+++ b/crates/keymap/README.md
@@ -378,8 +378,9 @@ Words come from `ui::Text`. Apps on craft-i18n pass `I18nText`; others implement
right-to-left languages, which mirror the layout. Chord text is never reordered. `ui::STRINGS`
and `ui::PLURALS` list every string the dialog looks up, for the translation catalog.
`Options` sets the conflict mode, double-press recording, area tabs, the on-screen keyboard,
-read-only rows for keys outside the catalog, the app's import and export formats, and an
-`accept` rule for which keys a command may get.
+read-only rows for keys outside the catalog, the app's import and export formats, an `accept`
+rule for which keys a command may get, and a `visible` rule for which catalog entries the table
+lists (hidden entries keep their keys and still count in conflicts).
## Features
diff --git a/crates/keymap/src/ui/mod.rs b/crates/keymap/src/ui/mod.rs
index d0da3424..1eb67ab0 100644
--- a/crates/keymap/src/ui/mod.rs
+++ b/crates/keymap/src/ui/mod.rs
@@ -125,6 +125,10 @@ pub struct Options {
pub area_tabs: bool,
/// The app's own Show entries.
pub areas: Vec,
+ /// `None`: every catalog entry is listed. Otherwise the table, its Show entries and search
+ /// list only the entries it returns true for; the others keep their bindings and still count
+ /// in conflicts.
+ pub visible: Option bool>,
/// false: the on-screen keyboard pane.
pub keyboard: bool,
pub fixed: Vec,
@@ -152,6 +156,7 @@ impl Default for Options {
sort_by_label: false,
area_tabs: false,
areas: Vec::new(),
+ visible: None,
keyboard: false,
fixed: Vec::new(),
import_formats: Vec::new(),
diff --git a/crates/keymap/src/ui/rows.rs b/crates/keymap/src/ui/rows.rs
index 7f8658d6..11825af0 100644
--- a/crates/keymap/src/ui/rows.rs
+++ b/crates/keymap/src/ui/rows.rs
@@ -186,12 +186,18 @@ fn is_tool(b: &Bindable) -> bool {
matches!(b.kind, Kind::Tool { .. })
}
+/// The catalog entries the table lists ([`Options::visible`]).
+fn listed<'k>(keymap: &'k Keymap, options: &Options) -> impl Iterator- + Clone {
+ let visible = options.visible;
+ keymap.catalog().iter().filter(move |b| visible.is_none_or(|f| f(b)))
+}
+
/// The Show entries, in catalog order, leaving out areas with no rows.
pub(crate) fn areas(keymap: &Keymap, options: &Options, text: &dyn Text) -> Vec {
- let catalog = keymap.catalog();
+ let catalog = listed(keymap, options);
let mut out = vec![AreaEntry { id: AreaId::All, label: text.ui("All Commands").into_owned() }];
let mut menus: Vec<&str> = Vec::new();
- for m in catalog.iter().filter_map(top_menu) {
+ for m in catalog.clone().filter_map(top_menu) {
if !menus.contains(&m) {
menus.push(m);
}
@@ -200,22 +206,22 @@ pub(crate) fn areas(keymap: &Keymap, options: &Options, text: &dyn Text) -> Vec<
let label = craft_i18n::fmt(&text.ui("{menu} Menu"), &[("menu", &text.segment(m))]);
out.push(AreaEntry { id: AreaId::Menu(m.to_string()), label });
}
- if catalog.iter().any(is_tool) {
+ if catalog.clone().any(is_tool) {
out.push(AreaEntry { id: AreaId::Tools, label: text.ui("Tools").into_owned() });
}
let mut panels: Vec<&str> = keymap.scopes().panels().iter().map(String::as_str).collect();
- for b in catalog.iter().filter(|b| !b.scope.is_app()) {
+ for b in catalog.clone().filter(|b| !b.scope.is_app()) {
if !panels.contains(&b.scope.as_str()) {
panels.push(b.scope.as_str());
}
}
- for p in panels.into_iter().filter(|p| catalog.iter().any(|b| b.scope.as_str() == *p)) {
+ for p in panels.into_iter().filter(|p| catalog.clone().any(|b| b.scope.as_str() == *p)) {
out.push(AreaEntry { id: AreaId::Panel(p.to_string()), label: panel_label(text, p) });
}
- if catalog.iter().any(|b| top_menu(b).is_none() && !is_tool(b)) {
+ if catalog.clone().any(|b| top_menu(b).is_none() && !is_tool(b)) {
out.push(AreaEntry { id: AreaId::Other, label: text.ui("Other").into_owned() });
}
- for a in &options.areas {
+ for a in options.areas.iter().filter(|a| catalog.clone().any(a.rows)) {
out.push(AreaEntry { id: AreaId::App(a.id), label: text.segment(a.label).into_owned() });
}
if !options.fixed.is_empty() {
@@ -344,14 +350,15 @@ fn build(keymap: &Keymap, options: &Options, text: &dyn Text, view: &View) -> Bu
}
}
let submenus = match view.area {
- AreaId::Menu(m) => catalog.iter().any(|b| top_menu(b) == Some(m.as_str()) && b.paths.first().is_some_and(|p| p.len() >= 3)),
+ AreaId::Menu(m) => listed(keymap, options).any(|b| top_menu(b) == Some(m.as_str()) && b.paths.first().is_some_and(|p| p.len() >= 3)),
_ => false,
};
// Groups in first-seen order within their rank.
let mut groups: Vec<(u8, Option, Vec
)> = Vec::new();
for (index, (b, list)) in catalog.iter().zip(&bindings).enumerate() {
- if !in_area(view.area, b, options) || !query.matches(b, list, text, platform) {
+ let hidden = options.visible.is_some_and(|f| !f(b));
+ if hidden || !in_area(view.area, b, options) || !query.matches(b, list, text, platform) {
continue;
}
let key = b.command.key();
@@ -600,6 +607,22 @@ pub(crate) mod tests {
Ok(())
}
+ #[test]
+ fn hidden_entries_are_not_listed_but_still_conflict() -> Result<(), crate::AssignError> {
+ let mut km = fixture(Platform::Mac);
+ let o = Options { visible: Some(|b| b.command.id != "file.export" && !b.command.id.starts_with("timeline.")), ..Options::default() };
+ let names: Vec = areas(&km, &o, &English).into_iter().map(|a| a.label).collect();
+ assert_eq!(names, ["All Commands", "File Menu", "Object Menu", "Tools", "Other"]);
+ assert_eq!(lines(&km, &o, &English, "", AreaId::Menu("File".into()), Filter::All), ["Save: ⌘S", "Save As…: ⇧⌘S"]);
+ assert!(lines(&km, &o, &English, "export", AreaId::All, Filter::All).is_empty());
+
+ km.assign(&"file.save".into(), key("Cmd+E"), crate::Policy::Keep)?;
+ let none = BTreeSet::new();
+ assert_eq!(build(&km, &o, &English, &view("", &AreaId::All, Filter::All, &none)).clashes, 1);
+ assert_eq!(lines(&km, &o, &English, "", AreaId::All, Filter::Conflicts), ["# File", "Save: ⌘S, ⌘E"]);
+ Ok(())
+ }
+
#[test]
fn commands_sharing_a_default_key_are_no_conflict() -> Result<(), crate::AssignError> {
let menu = Bindable::new("app.characterPanel", "Character", Scope::app()).with_path(vec!["Window".into(), "Character".into()]);