keymap ui: Options::visible hides catalog entries from the table

Hidden entries keep their bindings and still count in conflicts; they are
left out of the rows, search and the Show entries.
This commit is contained in:
aiongg committed 2026-10-20 20:53:13 +00:00
1 parent 0bff6f4c96
commit 02ba09ee9a
3 files changed
+40 -11

No files matched your search

+3 -2
View File
@@ -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
+5
View File
@@ -125,6 +125,10 @@ pub struct Options {
pub area_tabs: bool,
/// The app's own Show entries.
pub areas: Vec<Area>,
/// `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<fn(&Bindable) -> bool>,
/// false: the on-screen keyboard pane.
pub keyboard: bool,
pub fixed: Vec<FixedRow>,
@@ -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(),
+32 -9
View File
@@ -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<Item = &'k Bindable> + 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<AreaEntry> {
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<String>, Vec<Row>)> = 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<String> = 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()]);