Merge branch 'craft-keymap' into keymap-ui

# Conflicts:
#	crates/keymap/README.md
#	crates/keymap/src/lib.rs
This commit is contained in:
aiongg committed 2026-10-20 03:26:15 +00:00
commit cf9e10a90a
10 files changed
+163 -163

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
language setting.
- [`craft-keymap`](crates/keymap/README.md): keyboard bindings: chords and their text forms,
scopes, conflicts, user overrides stored through craft-settings and import of other
applications' shortcut sets. Command catalogues, file readers and the shortcut editor UI stay
applications' shortcut sets. Command catalogs, file readers and the shortcut editor UI stay
in each app.
- [`craft-print`](crates/print/README.md): PDF submission to CUPS and Windows, printer discovery,
duplex/copies/colour and driver preferences. PDF generation stays in each app.
+14 -14
View File
@@ -3,12 +3,12 @@
`craft-keymap` holds the data and logic for keyboard bindings in the Craft apps: chords and their
text forms, bindings from chords to commands, scopes, conflicts, user overrides, named sets, an
import pipeline for other applications' shortcut sets, and an editor model. Each application
supplies its command catalogue, its file-format readers and its UI.
supplies its command catalog, its file-format readers and its UI.
## Boundaries
- Data and logic, plus a shared Keyboard Shortcuts dialog behind the `ui` feature. The apps keep
the command catalogue (ids, labels, menu paths, defaults), the readers for other applications'
the command catalog (ids, labels, menu paths, defaults), the readers for other applications'
files, file dialogs and running the commands.
- Never crashes: no panics, no `unsafe`. Loading settings never fails; what cannot be used is
kept or reported (`Keymap::problems`, the import report).
@@ -20,7 +20,7 @@ supplies its command catalogue, its file-format readers and its UI.
- Dependencies: `serde` and `serde_json`; `egui` with the `egui` feature; `craft-ui` and
`craft-i18n` with the `ui` feature.
## A catalogue and dispatch
## A catalog and dispatch
A command with parameters has the key `window.panel {"panel":"layers"}` in settings and reports.
Tools of one group share a key and cycle. A binding can name a scope other than the command's
@@ -33,7 +33,7 @@ use serde_json::json;
let key = |text: &str| Binding::parse(text).unwrap();
let layers = CommandRef::with_params("window.panel", json!({"panel": "layers"}));
let select = Kind::Tool { group: Some("select".into()) };
let catalogue = vec![
let catalog = vec![
Bindable::new("file.save", "Save", Scope::app()).with_path(vec!["File".into(), "Save".into()]).with_default(key("Cmd+S")),
Bindable::new(layers.clone(), "Layers", Scope::app()).with_path(vec!["Window".into(), "Layers".into()]).with_default(key("F7")),
Bindable::new("tool.marquee", "Marquee Tool", Scope::app()).with_kind(select.clone()).with_default(key("M")),
@@ -44,7 +44,7 @@ let catalogue = vec![
// An app command whose binding works only in the Text context.
Bindable::new("type.bold", "Bold", Scope::app()).with_default(key("Text: Cmd+B")),
];
let keymap = Keymap::new(catalogue, Scopes::new().with_panel("Timeline").with_context("Text"), Platform::Mac);
let keymap = Keymap::new(catalog, Scopes::new().with_panel("Timeline").with_context("Text"), Platform::Mac);
// Dispatch: the chord pressed, whether it is a double press, and what has the keyboard.
let press = |text: &str, active: &Active| keymap.lookup(&Chord::parse(text).unwrap(), false, active);
@@ -112,9 +112,9 @@ impl Settings for Prefs {
const SCHEMA_VERSION: u32 = 1;
}
// `catalogue` is the app's Vec<Bindable>.
// `catalog` is the app's Vec<Bindable>.
let (mut store, _report) = SettingsStore::<Prefs>::open(Location::Auto);
let mut keymap = Keymap::new(catalogue, Scopes::new(), Platform::current()).with_overrides(store.get().shortcuts.clone());
let mut keymap = Keymap::new(catalog, Scopes::new(), Platform::current()).with_overrides(store.get().shortcuts.clone());
// Entries for unknown commands and texts that do not parse are kept and listed here.
let _problems = keymap.problems();
@@ -216,9 +216,9 @@ assert_eq!((report.unmatched[0].item.as_str(), report.unmatched[0].line), ("File
- Panel menus: sources list panel-menu items with the panel's name in front (InDesign's Panel
Menus area writes `Character: All Caps`; Illustrator and Photoshop sets have similar areas).
The reader splits the name into segments and marks the entry `ForeignEntry::in_panel_menu()`.
Such an entry also matches a catalogue path that ends with its segments (with the command's
Such an entry also matches a catalog path that ends with its segments (with the command's
label or the path's last segment last), and its last segment alone as a label. The last
segment alone counts only when the first segment, the panel, is a segment of some catalogue
segment alone counts only when the first segment, the panel, is a segment of some catalog
path, so `History: New Document` matches nothing in an app without a History panel. An entry
without keys that matched only these ways leaves the command's shortcuts alone and is listed
in `matched_without_change` (reason `PanelMenu`).
@@ -255,7 +255,7 @@ assert_eq!((report.unmatched[0].item.as_str(), report.unmatched[0].line), ("File
nothing; the user's answer is a second call with `Policy::Steal` or `Policy::Keep`.
- `Recorder::press(chord, time)` gives `Single`, `Double` (the same chord again within
`DOUBLE_PRESS`, 0.6 s) or `Cancel` (Escape).
- `editor::search(&keymap, query, platform)` filters the catalogue by label, alias, menu path,
- `editor::search(&keymap, query, platform)` filters the catalog by label, alias, menu path,
command id and key text, canonical or as shown on `platform`.
```rust
@@ -282,13 +282,13 @@ session.cancel(&mut keymap);
## Shortcuts dialog
With the `ui` feature, `ui::ShortcutsDialog` draws the Keyboard Shortcuts dialog over a `Keymap`
and, optionally, `Profiles`: a table of the catalogue grouped by menu, recording, conflicts, undo,
and, optionally, `Profiles`: a table of the catalog grouped by menu, recording, conflicts, undo,
named sets, import review and a summary. It edits the keymap in place, so menus show new keys at
once, and Cancel puts everything back as it was when the dialog opened.
Adopt it in three steps:
1. Build the `Keymap` from the catalogue and keep the built-in sets (`Builtins`), as above.
1. Build the `Keymap` from the catalog and keep the built-in sets (`Builtins`), as above.
2. While the dialog is open, keep a `DialogState` and call `show` (a window) or `ui` (the body,
for the app's own modal or preferences page) every frame.
3. Act on the `Outcome` it returns, and drop the state when `is_open()` turns false.
@@ -345,9 +345,9 @@ Key dispatch while the dialog is open:
Words come from `ui::Text`. Apps on craft-i18n pass `I18nText`; others implement `ui` and
`plural` over their own lookup, and `rtl` and `visual` (the app's bidi reordering) for
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 catalogue.
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 catalogue, the app's import and export formats, and an
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.
## Features
+1 -1
View File
@@ -163,7 +163,7 @@ pub use crate::editor::DOUBLE_PRESS;
pub struct Hit {
/// The command to run; for tools sharing the chord, the first of `cycle`.
pub command: CommandRef,
/// Tools of one group sharing the chord, in catalogue order (the app picks the next one);
/// Tools of one group sharing the chord, in catalog order (the app picks the next one);
/// empty for a plain command.
pub cycle: Vec<CommandRef>,
/// The chord whose binding matched, with `double` set when the double binding ran.
+18 -18
View File
@@ -53,8 +53,8 @@ pub struct ForeignEntry {
/// submenu of [`ForeignEntry::menu`], which is usually `None`. Sources write such items as
/// `Character: All Caps` (InDesign's Panel Menus area, the panel-menu areas of Illustrator and
/// Photoshop sets); the reader splits them into segments. Such an entry also matches a
/// catalogue path that ends with its path, and its last segment alone when its first segment
/// is a segment of some catalogue path. Without keys, an entry matched only these ways
/// catalog path that ends with its path, and its last segment alone when its first segment
/// is a segment of some catalog path. Without keys, an entry matched only these ways
/// changes nothing ([`ImportReport::matched_without_change`]).
pub panel_menu: bool,
}
@@ -94,7 +94,7 @@ impl ForeignEntry {
self.menu.iter().chain(&self.path).map(String::as_str).collect::<Vec<_>>().join(" > ")
}
/// The key of [`ImportOptions::remembered`] for this entry: the normalised menu and path
/// The key of [`ImportOptions::remembered`] for this entry: the normalized menu and path
/// joined by U+001F.
pub fn remember_key(&self) -> String {
self.menu.iter().chain(&self.path).map(|s| normalize_label(s)).collect::<Vec<_>>().join("\u{1f}")
@@ -344,7 +344,7 @@ pub struct MatchedWithoutChange {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum NoChangeReason {
/// The entry matched only through a panel-menu rule (a catalogue path ending with its
/// The entry matched only through a panel-menu rule (a catalog path ending with its
/// segments, or its last segment alone). Such a match is close enough to bind the entry's
/// keys, not to take a command's shortcuts away.
PanelMenu,
@@ -407,7 +407,7 @@ pub fn normalize_label(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
}
/// A normalised name without a leading `show/hide `, `show ` or `hide ` when something remains:
/// A normalized name without a leading `show/hide `, `show ` or `hide ` when something remains:
/// sources name a toggle by what choosing it does now. Only toggle commands
/// ([`Bindable::is_toggle`]) are matched by this form.
fn toggle_name(s: &str) -> String {
@@ -466,7 +466,7 @@ pub fn plan(set: &ForeignSet, keymap: &Keymap, options: &ImportOptions) -> Impor
if by_panel && entry.keys.is_empty() {
let (command, label) =
keymap.catalogue().get(i).map(|b| (b.command.clone(), b.label.clone())).unwrap_or_else(|| (CommandRef::new(""), String::new()));
keymap.catalog().get(i).map(|b| (b.command.clone(), b.label.clone())).unwrap_or_else(|| (CommandRef::new(""), String::new()));
report.matched_without_change.push(MatchedWithoutChange { command, label, item, line: entry.line, reason: NoChangeReason::PanelMenu });
continue;
}
@@ -511,7 +511,7 @@ pub fn plan(set: &ForeignSet, keymap: &Keymap, options: &ImportOptions) -> Impor
}
let at = *planned_at.entry(i).or_insert_with(|| {
let command = keymap.catalogue().get(i).map(|b| b.command.clone()).unwrap_or_else(|| CommandRef::new(""));
let command = keymap.catalog().get(i).map(|b| b.command.clone()).unwrap_or_else(|| CommandRef::new(""));
commands.push(Planned { command, item: item.clone(), line, bindings: Vec::new(), none_defined: true });
commands.len() - 1
});
@@ -549,8 +549,8 @@ impl ImportPlan {
/// both commands keep the bindings they had. With [`Start::Defaults`] that cannot happen.
pub fn apply(&self, keymap: &mut Keymap) -> ImportReport {
let before: Vec<Vec<Binding>> = self.commands.iter().map(|p| keymap.bindings(&p.command)).collect();
// Dropping every entry leaves room for all of them: each catalogue command takes at most one
// entry, and the catalogue holds at most `MAX_COMMANDS`.
// Dropping every entry leaves room for all of them: each catalog command takes at most one
// entry, and the catalog holds at most `MAX_COMMANDS`.
if self.start == Start::Defaults {
keymap.reset_all();
}
@@ -814,10 +814,10 @@ enum Found {
None,
}
/// Catalogue names in one form, for the match steps.
/// Catalog names in one form, for the match steps.
#[derive(Default)]
struct Names {
/// Normalised paths joined by U+001F.
/// Normalized paths joined by U+001F.
paths: HashMap<String, Vec<usize>>,
/// The last two or more segments of paths, also with the label as the last one, joined by
/// U+001F: for panel-menu entries.
@@ -870,9 +870,9 @@ impl Names {
}
}
/// The lists the match steps look up for an entry's normalised menu and path, most specific
/// The lists the match steps look up for an entry's normalized menu and path, most specific
/// first, each with whether it is a panel-menu step. `panel` is `Some(known)` for a panel-menu
/// entry, `known` saying whether the catalogue has its panel; only then does its last segment
/// entry, `known` saying whether the catalog has its panel; only then does its last segment
/// alone match.
fn steps(&self, menu: Option<&String>, segs: &[String], panel: Option<bool>) -> Vec<(Option<&Vec<usize>>, bool)> {
let Some(last) = segs.last() else { return Vec::new() };
@@ -900,7 +900,7 @@ impl Names {
}
}
/// The catalogue by normalised path and label, for the match steps.
/// The catalog by normalized path and label, for the match steps.
struct Index {
tool: Vec<bool>,
/// Binding keys, for remembered answers.
@@ -908,14 +908,14 @@ struct Index {
names: Names,
/// Toggle commands by their names without a leading Show/Hide ([`toggle_name`]).
toggles: Names,
/// Every normalised segment of every catalogue path: the panels and menus the app has.
/// Every normalized segment of every catalog path: the panels and menus the app has.
segments: HashSet<String>,
}
impl Index {
fn new(keymap: &Keymap) -> Index {
let mut ix = Index { tool: Vec::new(), keys: HashMap::new(), names: Names::default(), toggles: Names::default(), segments: HashSet::new() };
for (i, b) in keymap.catalogue().iter().enumerate() {
for (i, b) in keymap.catalog().iter().enumerate() {
ix.tool.push(matches!(b.kind, Kind::Tool { .. }));
ix.keys.insert(b.command.key(), i);
ix.names.add(i, b, str::to_string);
@@ -939,7 +939,7 @@ impl Index {
/// The match steps in order; the first that finds exactly one command decides. The steps run
/// on the names as written, then on toggle commands with the entry's label and theirs
/// without a leading Show/Hide. A panel-menu entry's last segment alone matches only when
/// its first segment, the panel, is a segment of some catalogue path.
/// its first segment, the panel, is a segment of some catalog path.
fn find(&self, entry: &ForeignEntry, tool: bool, remembered: &BTreeMap<String, CommandRef>) -> Found {
if let Some(c) = remembered.get(&entry.remember_key())
&& let Some(&i) = self.keys.get(&c.key())
@@ -978,7 +978,7 @@ struct ToSuggest {
/// Fuzzy candidates for unmatched entries, among commands of the same kind.
fn suggest(keymap: &Keymap, entries: &[ToSuggest]) -> Vec<Suggestion> {
let pool = |tool: bool| -> Vec<&Bindable> { keymap.catalogue().iter().filter(|b| matches!(b.kind, Kind::Tool { .. }) == tool).collect() };
let pool = |tool: bool| -> Vec<&Bindable> { keymap.catalog().iter().filter(|b| matches!(b.kind, Kind::Tool { .. }) == tool).collect() };
let matcher = |pool: &[&Bindable]| {
let candidates: Vec<namematch::Candidate> = pool
.iter()
+48 -48
View File
@@ -1,4 +1,4 @@
//! A command catalogue with default bindings, the user's overrides, dispatch and conflicts.
//! A command catalog with default bindings, the user's overrides, dispatch and conflicts.
pub(crate) mod overrides;
@@ -14,7 +14,7 @@ use crate::scope::{Active, Scope, ScopeKind, Scopes};
pub use overrides::Overrides;
pub use overrides::list as overrides_list;
/// Most commands a catalogue or an [`Overrides`] holds.
/// Most commands a catalog or an [`Overrides`] holds.
pub const MAX_COMMANDS: usize = 4096;
/// Most bindings per command.
pub const MAX_BINDINGS: usize = 16;
@@ -226,7 +226,7 @@ impl TypingRule {
pub enum Lookup {
None,
Run(CommandRef),
/// Tools of one group share the chord: the app picks the next one (catalogue order).
/// Tools of one group share the chord: the app picks the next one (catalog order).
Cycle(Vec<CommandRef>),
}
@@ -272,7 +272,7 @@ pub enum AssignError {
TooMany,
/// Other commands hold the chord ([`Policy::Refuse`]).
Taken(Vec<Holder>),
/// A scope that is neither registered in [`Scopes`] nor used by the catalogue.
/// A scope that is neither registered in [`Scopes`] nor used by the catalog.
UnknownScope(Scope),
/// The change needs a new [`Overrides`] entry and it already holds [`MAX_COMMANDS`].
Full,
@@ -311,13 +311,13 @@ pub enum DropReason {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Problem {
/// No command in the catalogue has this key; the entry is kept.
/// No command in the catalog has this key; the entry is kept.
UnknownCommand { key: String },
/// A text that does not parse; the entry keeps it.
BadBinding { key: String, text: String, error: BindingError },
/// A reserved chord; not bound.
Reserved { key: String, text: String },
/// A scope that is neither registered in [`Scopes`] nor used by the catalogue; not bound.
/// A scope that is neither registered in [`Scopes`] nor used by the catalog; not bound.
UnknownScope { key: String, text: String },
/// Another entry names the same command; this one is ignored.
DuplicateKey { key: String },
@@ -366,10 +366,10 @@ struct Group<'a> {
/// one's command and binding.
type Pair<'a> = (usize, usize, bool, (usize, &'a Bound), (usize, &'a Bound));
/// The catalogue, the scope registry, the platform and the user's overrides.
/// The catalog, the scope registry, the platform and the user's overrides.
#[derive(Clone, Debug)]
pub struct Keymap {
catalogue: Vec<Bindable>,
catalog: Vec<Bindable>,
keys: Vec<String>,
index: HashMap<String, usize>,
defaults: Vec<Vec<Bound>>,
@@ -381,8 +381,8 @@ pub struct Keymap {
/// Whether the overrides hold a hand-written variant of a command's key
/// (`window.panel { "panel": "layers" }`); only loading adds them.
variants: bool,
/// Home and default scopes of the catalogue.
catalogue_scopes: HashSet<Scope>,
/// Home and default scopes of the catalog.
catalog_scopes: HashSet<Scope>,
scopes: Scopes,
platform: Platform,
typing: TypingRule,
@@ -393,9 +393,9 @@ pub struct Keymap {
impl Keymap {
/// A keymap with default bindings. Entries past [`MAX_COMMANDS`], with a key over
/// [`MAX_KEY`] bytes, or repeating an earlier key are left out.
pub fn new(catalogue: Vec<Bindable>, scopes: Scopes, platform: Platform) -> Keymap {
pub fn new(catalog: Vec<Bindable>, scopes: Scopes, platform: Platform) -> Keymap {
let mut km = Keymap {
catalogue: Vec::new(),
catalog: Vec::new(),
keys: Vec::new(),
index: HashMap::new(),
defaults: Vec::new(),
@@ -403,27 +403,27 @@ impl Keymap {
by_trigger: HashMap::new(),
stored: Vec::new(),
variants: false,
catalogue_scopes: HashSet::new(),
catalog_scopes: HashSet::new(),
scopes,
platform,
typing: TypingRule::default(),
reserved: Vec::new(),
overrides: Overrides::new(),
};
for b in catalogue {
for b in catalog {
let key = b.command.key();
if km.catalogue.len() >= MAX_COMMANDS || key.len() > MAX_KEY || km.index.contains_key(&key) {
if km.catalog.len() >= MAX_COMMANDS || key.len() > MAX_KEY || km.index.contains_key(&key) {
continue;
}
let defaults = km.normalize(b.defaults.iter().cloned(), &b.scope);
km.catalogue_scopes.insert(b.scope.clone());
km.catalogue_scopes.extend(defaults.iter().map(|(_, s)| s.clone()));
km.index.insert(key.clone(), km.catalogue.len());
km.catalog_scopes.insert(b.scope.clone());
km.catalog_scopes.extend(defaults.iter().map(|(_, s)| s.clone()));
km.index.insert(key.clone(), km.catalog.len());
km.keys.push(key);
km.current.push(defaults.clone());
km.defaults.push(defaults);
km.stored.push(None);
km.catalogue.push(b);
km.catalog.push(b);
}
km.rebuild_index();
km
@@ -468,12 +468,12 @@ impl Keymap {
self.typing
}
pub fn catalogue(&self) -> &[Bindable] {
&self.catalogue
pub fn catalog(&self) -> &[Bindable] {
&self.catalog
}
pub fn bindable(&self, command: &CommandRef) -> Option<&Bindable> {
self.find(command).map(|i| &self.catalogue[i])
self.find(command).map(|i| &self.catalog[i])
}
/// The effective bindings of a command; empty for an unknown one.
@@ -492,9 +492,9 @@ impl Keymap {
}
/// Whether bindings may use `scope`: the application, a scope registered in [`Scopes`], or a
/// home or default scope of the catalogue.
/// home or default scope of the catalog.
pub fn knows_scope(&self, scope: &Scope) -> bool {
scope.is_app() || self.scopes.knows(scope) || self.catalogue_scopes.contains(scope)
scope.is_app() || self.scopes.knows(scope) || self.catalog_scopes.contains(scope)
}
/// The command `chord` (pressed twice when `double`) runs with `active` focus.
@@ -502,7 +502,7 @@ impl Keymap {
let want = Trigger { chord: chord.effective(self.platform), double };
let held = active.typing && !self.typing.fires(&want.chord);
let Some(holders) = self.by_trigger.get(&want) else { return Lookup::None };
// Each command's nearest rank; `holders` is in catalogue order.
// Each command's nearest rank; `holders` is in catalog order.
let mut ranks: Vec<(usize, u8)> = Vec::new();
for &(i, k) in holders {
let Some(r) = self.at(i, k).and_then(|(_, s)| self.scopes.rank(s, active)) else { continue };
@@ -519,12 +519,12 @@ impl Keymap {
let winners: Vec<usize> = ranks.iter().filter(|(_, r)| *r == best).map(|(i, _)| *i).collect();
match winners.as_slice() {
[] => Lookup::None,
[i] => Lookup::Run(self.catalogue[*i].command.clone()),
[i] => Lookup::Run(self.catalog[*i].command.clone()),
[first, rest @ ..] => {
if rest.iter().all(|j| self.same_group(*first, *j)) {
Lookup::Cycle(winners.iter().map(|i| self.catalogue[*i].command.clone()).collect())
Lookup::Cycle(winners.iter().map(|i| self.catalog[*i].command.clone()).collect())
} else {
Lookup::Run(self.catalogue[*first].command.clone())
Lookup::Run(self.catalog[*first].command.clone())
}
}
}
@@ -548,7 +548,7 @@ impl Keymap {
if bindings.len() > MAX_BINDINGS {
return Err(AssignError::TooMany);
}
let home = self.catalogue[i].scope.clone();
let home = self.catalog[i].scope.clone();
let list = self.normalize(bindings, &home);
let new: Vec<Bound> = list.iter().filter(|b| !self.current[i].iter().any(|c| self.same(c, b))).cloned().collect();
self.apply(i, list, &new, policy)
@@ -588,14 +588,14 @@ impl Keymap {
}
/// Pairs of commands on one chord, at most [`MAX_CONFLICTS`], in chord order, then in
/// catalogue order. Tools of one group and bindings of panels that are never active together
/// catalog order. Tools of one group and bindings of panels that are never active together
/// are left out; with panel fallthrough two unrelated panels on one chord tie in a third panel,
/// which is reported as a [`ConflictKind::Shadow`].
///
/// When the cap cuts a chord's pairs, the pairs kept are the first found comparing the
/// chord's holders grouped by scope and tool group, group by group in the order each group
/// first appears in the catalogue; the kept pairs are still returned in chord order, then
/// catalogue order.
/// first appears in the catalog; the kept pairs are still returned in chord order, then
/// catalog order.
pub fn conflicts(&self) -> Vec<Conflict> {
let mut chords: Vec<(&Trigger, &BTreeSet<(usize, usize)>)> = self.by_trigger.iter().filter(|(_, h)| h.len() > 1).collect();
chords.sort_by_key(|(t, _)| **t);
@@ -609,7 +609,7 @@ impl Keymap {
}
/// The conflicts [`Keymap::conflicts`] reports for one command's bindings, with that command
/// as `a`, in the order of its bindings, then catalogue order. Empty for an unknown command.
/// as `a`, in the order of its bindings, then catalog order. Empty for an unknown command.
pub fn conflicts_with(&self, command: &CommandRef) -> Vec<Conflict> {
let Some(i) = self.find(command) else { return Vec::new() };
let mut out = Vec::new();
@@ -624,8 +624,8 @@ impl Keymap {
let Some(clash) = self.conflict_relation(&a.1, &b.1) else { continue };
out.push(Conflict {
chord,
a: (self.catalogue[i].command.clone(), self.public(i, a)),
b: (self.catalogue[j].command.clone(), self.public(j, b)),
a: (self.catalog[i].command.clone(), self.public(i, a)),
b: (self.catalog[j].command.clone(), self.public(j, b)),
kind: if clash { ConflictKind::Clash } else { ConflictKind::Shadow },
in_defaults: self.defaults[i].contains(a) && self.defaults[j].contains(b),
});
@@ -647,7 +647,7 @@ impl Keymap {
out.push(Problem::DuplicateKey { key: key.to_string() });
continue;
}
let home = &self.catalogue[i].scope;
let home = &self.catalog[i].scope;
for text in texts {
let (key, text) = (key.to_string(), text.clone());
match Binding::parse(&text) {
@@ -666,7 +666,7 @@ impl Keymap {
pub(crate) fn clashing(&self, command: &CommandRef, bindings: &[Binding]) -> Vec<Holder> {
let Some(i) = self.find(command) else { return Vec::new() };
let new: Vec<Bound> = bindings.iter().map(|b| self.bound(i, b)).collect();
self.clashes(i, &new).into_iter().map(|(j, b)| Holder { command: self.catalogue[j].command.clone(), binding: self.public(j, &b) }).collect()
self.clashes(i, &new).into_iter().map(|(j, b)| Holder { command: self.catalog[j].command.clone(), binding: self.public(j, &b) }).collect()
}
/// Whether binding `x` of command `a` and binding `y` of another command `b` clash, by the
@@ -694,11 +694,11 @@ impl Keymap {
}
fn bound(&self, i: usize, binding: &Binding) -> Bound {
(binding.trigger, binding.scope_or(&self.catalogue[i].scope).clone())
(binding.trigger, binding.scope_or(&self.catalog[i].scope).clone())
}
fn public(&self, i: usize, (trigger, scope): &Bound) -> Binding {
let scope = (*scope != self.catalogue[i].scope).then(|| scope.clone());
let scope = (*scope != self.catalog[i].scope).then(|| scope.clone());
Binding { trigger: *trigger, scope }
}
@@ -724,7 +724,7 @@ impl Keymap {
}
fn same_group(&self, i: usize, j: usize) -> bool {
match (&self.catalogue[i].kind, &self.catalogue[j].kind) {
match (&self.catalog[i].kind, &self.catalog[j].kind) {
(Kind::Tool { group: a }, Kind::Tool { group: b }) => a == b,
_ => false,
}
@@ -774,7 +774,7 @@ impl Keymap {
let mut group_of: HashMap<(&Scope, Option<&Option<String>>), usize> = HashMap::new();
for (pos, &(i, k)) in holders.iter().enumerate() {
let Some(b) = self.at(i, k) else { continue };
let tools = match &self.catalogue[i].kind {
let tools = match &self.catalog[i].kind {
Kind::Tool { group } => Some(group),
_ => None,
};
@@ -814,8 +814,8 @@ impl Keymap {
for (_, _, clash, (i, a), (j, b)) in found {
out.push(Conflict {
chord,
a: (self.catalogue[i].command.clone(), self.public(i, a)),
b: (self.catalogue[j].command.clone(), self.public(j, b)),
a: (self.catalog[i].command.clone(), self.public(i, a)),
b: (self.catalog[j].command.clone(), self.public(j, b)),
kind: if clash { ConflictKind::Clash } else { ConflictKind::Shadow },
in_defaults: self.defaults[i].contains(a) && self.defaults[j].contains(b),
});
@@ -830,13 +830,13 @@ impl Keymap {
if let Some((t, _)) = new.iter().find(|(t, _)| self.is_reserved(&t.chord)) {
return Err(AssignError::Reserved(*t));
}
let home = &self.catalogue[i].scope;
let home = &self.catalog[i].scope;
if let Some((_, s)) = new.iter().find(|(_, s)| !self.usable_scope(s, home)) {
return Err(AssignError::UnknownScope(s.clone()));
}
let clashes = self.clashes(i, new);
let holders: Vec<Holder> =
clashes.iter().map(|(j, b)| Holder { command: self.catalogue[*j].command.clone(), binding: self.public(*j, b) }).collect();
clashes.iter().map(|(j, b)| Holder { command: self.catalog[*j].command.clone(), binding: self.public(*j, b) }).collect();
let mut done = Assigned::default();
let mut writes: Vec<(usize, Vec<Bound>)> = Vec::new();
match policy {
@@ -890,7 +890,7 @@ impl Keymap {
kept.truncate(MAX_BINDINGS.saturating_sub(list.len()));
self.remove_entries(i);
if !kept.is_empty() || !self.is_default(i, &list) {
let home = &self.catalogue[i].scope;
let home = &self.catalog[i].scope;
let mut texts: Vec<String> = list.iter().map(|b| self.public(i, b).text(home)).collect();
texts.extend(kept);
if self.overrides.insert(self.keys[i].clone(), texts) {
@@ -930,7 +930,7 @@ impl Keymap {
/// version, a scope this app lacks, a chord this platform reserves.
fn unusable_texts(&self, i: usize) -> Vec<String> {
let Some(texts) = self.stored[i].as_deref().and_then(|key| self.overrides.get(key)) else { return Vec::new() };
let home = &self.catalogue[i].scope;
let home = &self.catalog[i].scope;
texts.iter().filter(|t| !Binding::parse(t).is_ok_and(|b| self.usable(&b, home))).cloned().collect()
}
@@ -985,7 +985,7 @@ impl Keymap {
if !taken.insert(i) {
continue;
}
let home = &self.catalogue[i].scope;
let home = &self.catalog[i].scope;
let usable = texts.iter().filter_map(|t| Binding::parse(t).ok()).filter(|b| self.usable(b, home));
let list = self.normalize(usable, home);
// An entry none of whose texts is usable here (a key name from a newer version, a scope
+6 -6
View File
@@ -5,7 +5,7 @@
//! [`chord`] holds the key and modifier types, the canonical text form that settings and control
//! protocols store, display text per platform, and a reader for other applications' key text.
//! [`command`] names what a binding runs, [`scope`] where it works, and [`keymap`] holds an app's
//! catalogue with default bindings, the user's [`Overrides`], dispatch ([`Keymap::lookup`]),
//! catalog with default bindings, the user's [`Overrides`], dispatch ([`Keymap::lookup`]),
//! editing and conflicts. [`profiles`] keeps named sets of overrides; [`reserved`] lists the
//! chords each operating system takes, and a keymap reserves no chord unless the app opts in with
//! [`Keymap::with_reserved`], e.g. `with_reserved(reserved::system(platform))`. [`import`] brings
@@ -23,7 +23,7 @@
//! assert_eq!(chord.display(Platform::Windows), "Ctrl+Shift+Z");
//! ```
//!
//! # A catalogue and dispatch
//! # A catalog and dispatch
//!
//! The app lists every command it can bind. A command with parameters has the key
//! `window.panel {"panel":"layers"}` in settings and reports. Tools of one group share a key and
@@ -36,7 +36,7 @@
//! let key = |text: &str| Binding::parse(text).unwrap();
//! let layers = CommandRef::with_params("window.panel", json!({"panel": "layers"}));
//! let select = Kind::Tool { group: Some("select".into()) };
//! let catalogue = vec![
//! let catalog = vec![
//! Bindable::new("file.save", "Save", Scope::app()).with_path(vec!["File".into(), "Save".into()]).with_default(key("Cmd+S")),
//! Bindable::new(layers.clone(), "Layers", Scope::app()).with_path(vec!["Window".into(), "Layers".into()]).with_default(key("F7")),
//! Bindable::new("tool.marquee", "Marquee Tool", Scope::app()).with_kind(select.clone()).with_default(key("M")),
@@ -47,7 +47,7 @@
//! // An app command whose binding works only in the Text context.
//! Bindable::new("type.bold", "Bold", Scope::app()).with_default(key("Text: Cmd+B")),
//! ];
//! let keymap = Keymap::new(catalogue, Scopes::new().with_panel("Timeline").with_context("Text"), Platform::Mac);
//! let keymap = Keymap::new(catalog, Scopes::new().with_panel("Timeline").with_context("Text"), Platform::Mac);
//! assert_eq!(layers.key(), r#"window.panel {"panel":"layers"}"#);
//!
//! // Dispatch: the chord pressed, whether it is a double press, and what has the keyboard.
@@ -89,9 +89,9 @@
//! const SCHEMA_VERSION: u32 = 1;
//! }
//!
//! # let catalogue = Vec::new();
//! # let catalog = Vec::new();
//! let (mut store, _report) = SettingsStore::<Prefs>::open(Location::Auto);
//! let mut keymap = Keymap::new(catalogue, Scopes::new(), Platform::current()).with_overrides(store.get().shortcuts.clone());
//! let mut keymap = Keymap::new(catalog, Scopes::new(), Platform::current()).with_overrides(store.get().shortcuts.clone());
//! // Entries for unknown commands and texts that do not parse are kept and listed here.
//! let _problems = keymap.problems();
//!
+2 -2
View File
@@ -78,7 +78,7 @@ const CTRL: Modifiers = Modifiers { ctrl: true, command: true, ..Modifiers::NONE
fn dispatch_keymap() -> Keymap {
let app = |id: &str, keys: &[&str]| keys.iter().fold(Bindable::new(id, id, Scope::app()), |e, k| e.with_default(Binding::parse(k).unwrap()));
let tool = |id: &str| app(id, &["M"]).with_kind(Kind::Tool { group: Some("select".into()) });
let catalogue = vec![
let catalog = vec![
app("edit.copy", &["Cmd+C", "Cmd+Insert"]),
app("edit.paste", &["Cmd+V", "Shift+Insert"]),
app("app.quit", &["Cmd+Alt+Q"]),
@@ -94,7 +94,7 @@ fn dispatch_keymap() -> Keymap {
app("reveal.one", &["U"]),
app("reveal.all", &["U U"]),
];
Keymap::new(catalogue, Scopes::new().with_panel("Timeline"), Platform::Windows)
Keymap::new(catalog, Scopes::new().with_panel("Timeline"), Platform::Windows)
}
fn press(key: EguiKey, modifiers: Modifiers) -> Event {
+25 -25
View File
@@ -34,7 +34,7 @@ fn panel(name: &str) -> CommandRef {
CommandRef::with_params("window.panel", json!({ "panel": name }))
}
fn catalogue() -> Vec<Bindable> {
fn catalog() -> Vec<Bindable> {
vec![
command("file.new", "Document…", &["File", "New", "Document…"], &["Cmd+N"]),
command("dialog.newDocument", "New Document Dialog", &[], &["Cmd+N"]),
@@ -65,7 +65,7 @@ fn catalogue() -> Vec<Bindable> {
}
fn keymap(platform: Platform) -> Keymap {
Keymap::new(catalogue(), Scopes::new().with_context("Text"), platform).with_reserved(reserved::system(platform))
Keymap::new(catalog(), Scopes::new().with_context("Text"), platform).with_reserved(reserved::system(platform))
}
fn key(flavor: Flavor, context: &str, text: &str) -> ForeignKey {
@@ -139,7 +139,7 @@ fn has(km: &Keymap, id: impl Into<CommandRef>, texts: &[&str]) {
}
#[test]
fn labels_normalise_for_matching() {
fn labels_normalize_for_matching() {
assert_eq!(normalize_label("Check Spelling..."), "check spelling");
assert_eq!(normalize_label("Check Spelling…"), "check spelling");
assert_eq!(normalize_label("Grids && Guides"), "grids guides");
@@ -152,7 +152,7 @@ fn labels_normalise_for_matching() {
fn every_match_step_finds_its_command() {
let (km, r) = import_windows();
assert_eq!(r.set.as_deref(), Some("My Set"));
// 1: full path, with `&`, case and spacing normalised, and Show/Hide dropped for a toggle.
// 1: full path, with `&`, case and spacing normalized, and Show/Hide dropped for a toggle.
has(&km, "view.guides", &["Cmd+Alt+;"]);
// 2: joined segments `Text Frame Options` in Object.
has(&km, "object.textFrameOptions", &["Cmd+Alt+B"]);
@@ -463,12 +463,12 @@ fn hostile_sets_are_capped_and_counted() {
#[test]
#[ignore = "worst-case timing; run with --ignored"]
fn hostile_sets_finish_in_time() {
// A full catalogue whose labels share words with every unmatched entry below, so each
// A full catalog whose labels share words with every unmatched entry below, so each
// suggestion scores every command.
let mut catalogue = catalogue();
let filler = MAX_COMMANDS - catalogue.len();
catalogue.extend((0..filler).map(|i| command(format!("filler.c{i}").as_str(), &format!("Filler Command {i}"), &[], &[])));
let mut km = Keymap::new(catalogue, Scopes::new().with_context("Text"), Platform::Windows).with_reserved(reserved::system(Platform::Windows));
let mut catalog = catalog();
let filler = MAX_COMMANDS - catalog.len();
catalog.extend((0..filler).map(|i| command(format!("filler.c{i}").as_str(), &format!("Filler Command {i}"), &[], &[])));
let mut km = Keymap::new(catalog, Scopes::new().with_context("Text"), Platform::Windows).with_reserved(reserved::system(Platform::Windows));
let long: String = "ä€𝄞 x".repeat(MAX_IMPORT_TEXT);
// Odd entries have keys and long labels that match nothing, so they reach the suggestions:
// words in the whole label, or two words and symbols, which score high enough to suggest.
@@ -593,8 +593,8 @@ fn report_json_uses_camel_case_names() {
assert_eq!(v["changed"][0]["from"], json!(["Cmd+W"]));
}
fn import_into(catalogue: Vec<Bindable>, entries: Vec<ForeignEntry>, options: &ImportOptions) -> (Keymap, ImportReport) {
let mut km = Keymap::new(catalogue, Scopes::new(), Platform::Windows);
fn import_into(catalog: Vec<Bindable>, entries: Vec<ForeignEntry>, options: &ImportOptions) -> (Keymap, ImportReport) {
let mut km = Keymap::new(catalog, Scopes::new(), Platform::Windows);
let report = plan(&ForeignSet { name: None, entries }, &km, options).apply(&mut km);
(km, report)
}
@@ -609,7 +609,7 @@ fn unmatched(r: &ImportReport) -> Vec<(&str, UnmatchedReason)> {
#[test]
fn panel_menu_entries_match_by_path_end_or_label() {
let catalogue = vec![
let catalog = vec![
command("type.allCaps", "All Caps", &["Panels", "Character", "All Caps"], &[]),
command("pages.overrideAll", "Override All Parent Items", &["Pages", "Override All Parent Page Items"], &[]),
command("paragraph.justification", "Justification…", &[], &[]),
@@ -637,10 +637,10 @@ fn panel_menu_entries_match_by_path_end_or_label() {
panel_entry(&["Character", "Small Caps"], "Ctrl+F5"),
// No path end fits and the label alone fits two commands.
panel_entry(&["Color", "New Swatch"], "Ctrl+F6"),
// Without the mark, a path with no menu matches only a whole catalogue path.
// Without the mark, a path with no menu matches only a whole catalog path.
entry(None, &["Character", "All Caps"], &[(d, "Ctrl+F7")], Flavor::Windows),
];
let (km, r) = import_into(catalogue, entries, &default_context());
let (km, r) = import_into(catalog, entries, &default_context());
has(&km, "type.allCaps", &["Cmd+F1"]);
has(&km, "pages.overrideAll", &["Cmd+F2"]);
has(&km, "paragraph.justification", &["Cmd+F3"]);
@@ -659,7 +659,7 @@ fn panel_menu_entries_match_by_path_end_or_label() {
#[test]
fn aliases_match_like_labels() {
let catalogue = vec![
let catalog = vec![
command("app.quit", "Quit DesignCraft", &["File", "Quit DesignCraft"], &[]).with_alias("Exit"),
command("app.preferences", "Preferences…", &["Edit", "Preferences…"], &[]).with_alias("Preferences: General…"),
command("type.bold", "Bold", &["Type", "Style", "Bold"], &[]).with_alias("Apply bold"),
@@ -672,7 +672,7 @@ fn aliases_match_like_labels() {
entry(None, &["Apply Bold"], &[(d, "Ctrl+Shift+B")], Flavor::Windows),
win("Bearbeiten", &["Einfügen"], &[(d, "Ctrl+Shift+V")]),
];
let (km, r) = import_into(catalogue, entries, &default_context());
let (km, r) = import_into(catalog, entries, &default_context());
has(&km, "app.quit", &["Cmd+Q"]);
has(&km, "app.preferences", &["Cmd+K"]);
has(&km, "type.bold", &["Cmd+Shift+B"]);
@@ -682,7 +682,7 @@ fn aliases_match_like_labels() {
#[test]
fn show_and_hide_are_read_off_only_for_toggles() {
let catalogue = vec![
let catalog = vec![
command(panel("conveyor"), "Conveyor", &["Window", "Conveyor"], &[]),
command("view.rulers", "Rulers", &["View", "Rulers"], &[]).toggle(),
command("view.frameEdges", "Show Frame Edges", &["View", "Extras", "Show Frame Edges"], &[]).toggle(),
@@ -700,7 +700,7 @@ fn show_and_hide_are_read_off_only_for_toggles() {
win("Type", &["Show Hidden Characters"], &[(d, "Ctrl+Alt+I")]),
win("Type", &["Hide Hidden Characters"], &[(d, "Ctrl+Alt+H")]),
];
let (km, r) = import_into(catalogue, entries, &default_context());
let (km, r) = import_into(catalog, entries, &default_context());
has(&km, panel("conveyor"), &[]);
has(&km, "view.rulers", &["Cmd+R", "Cmd+Shift+R"]);
has(&km, "view.frameEdges", &["Cmd+H"]);
@@ -713,7 +713,7 @@ fn show_and_hide_are_read_off_only_for_toggles() {
#[test]
fn only_entries_with_keys_to_bind_count_toward_suggestions() {
let catalogue = vec![command("edit.checkSpelling", "Check Spelling…", &["Edit", "Check Spelling…"], &[])];
let catalog = vec![command("edit.checkSpelling", "Check Spelling…", &["Edit", "Check Spelling…"], &[])];
let d = "Default";
// Twice the cap of unmatched entries without keys, or with keys only in a skipped context,
// before one with a key.
@@ -726,7 +726,7 @@ fn only_entries_with_keys_to_bind_count_toward_suggestions() {
suggest: true,
..ImportOptions::default()
};
let (_, r) = import_into(catalogue, entries, &options);
let (_, r) = import_into(catalog, entries, &options);
assert_eq!(r.unmatched_without_shortcut, 2 * MAX_SUGGESTIONS);
assert_eq!(r.unmatched.len(), 1);
assert_eq!(r.unmatched[0].keys, "Default: Ctrl+K", "keys in a skipped context are left out");
@@ -735,7 +735,7 @@ fn only_entries_with_keys_to_bind_count_toward_suggestions() {
assert_eq!(r.truncated, 0);
}
fn pages_catalogue() -> Vec<Bindable> {
fn pages_catalog() -> Vec<Bindable> {
vec![
command("app.newDocumentDialog", "New Document…", &["File", "New Document…"], &["Cmd+N"]),
command("layout.pages.insert", "Insert Pages…", &["Layout", "Pages", "Insert Pages…"], &["Cmd+Shift+P"]),
@@ -754,7 +754,7 @@ fn panel_menu_entries_without_keys_change_nothing() {
// The same for the last segment alone, with a panel the app has.
none(&["Pages", "New Document"]),
];
let (km, r) = import_into(pages_catalogue(), entries, &default_context());
let (km, r) = import_into(pages_catalog(), entries, &default_context());
has(&km, "app.newDocumentDialog", &["Cmd+N"]);
has(&km, "layout.pages.insert", &["Cmd+Shift+P"]);
assert!(r.changed.is_empty(), "{:?}", r.changed);
@@ -781,7 +781,7 @@ fn an_entry_without_keys_never_clears_keys_another_entry_gives() {
let keyed = || win("Layout", &["Pages", "Insert Pages..."], &[(d, "Ctrl+Alt+P")]);
let keyless = || win("Layout", &["Insert Pages..."], &[]);
for entries in [vec![keyed(), keyless()], vec![keyless(), keyed()]] {
let (km, _) = import_into(pages_catalogue(), entries, &default_context());
let (km, _) = import_into(pages_catalog(), entries, &default_context());
has(&km, "layout.pages.insert", &["Cmd+Alt+P"]);
}
@@ -790,14 +790,14 @@ fn an_entry_without_keys_never_clears_keys_another_entry_gives() {
ForeignEntry::new(None, ["Pages", "Insert Pages..."]).in_panel_menu(),
win("Layout", &["Pages", "Insert Pages..."], &[(d, "Ctrl+Shift+P")]),
];
let (km, r) = import_into(pages_catalogue(), entries, &default_context());
let (km, r) = import_into(pages_catalog(), entries, &default_context());
has(&km, "layout.pages.insert", &["Cmd+Shift+P"]);
assert_eq!(r.matched_without_change.len(), 1);
// The later entry still takes the chord from another command; the entry without keys does
// not clear the command that lost it.
let entries = vec![keyless(), keyed(), win("File", &["Close"], &[(d, "Ctrl+Alt+P")])];
let (km, r) = import_into(pages_catalogue(), entries, &default_context());
let (km, r) = import_into(pages_catalog(), entries, &default_context());
has(&km, "file.close", &["Cmd+Alt+P"]);
has(&km, "layout.pages.insert", &["Cmd+Shift+P"]);
assert_eq!(r.skipped.iter().map(|s| (s.command.id.as_str(), s.reason)).collect::<Vec<_>>(), [("layout.pages.insert", SkipReason::Taken)]);
+46 -46
View File
@@ -54,14 +54,14 @@ fn command_keys_are_canonical_whatever_the_map_order() {
#[test]
fn lookup_picks_the_nearest_scope() {
let catalogue = vec![
let catalog = vec![
app("edit.copy", &["Cmd+C"]),
in_scope("timeline.copy", "Timeline", &["Cmd+C"]),
app("file.bake", &["Cmd+B"]),
in_scope("text.bold", "Text", &["Cmd+B", "Cmd+I"]),
in_scope("timeline.loop", "Timeline", &["L"]),
];
let km = Keymap::new(catalogue.clone(), scopes(), Platform::Mac);
let km = Keymap::new(catalog.clone(), scopes(), Platform::Mac);
let text = ["Text"];
assert_eq!(press(&km, "Cmd+C", &Active::NONE), run("edit.copy"));
assert_eq!(press(&km, "Cmd+C", &Active::panel("Timeline")), run("timeline.copy"));
@@ -73,14 +73,14 @@ fn lookup_picks_the_nearest_scope() {
assert_eq!(press(&km, "L", &Active::panel("Effects")), run("timeline.loop"), "fallthrough");
assert_eq!(km.lookup(&chord("L"), true, &Active::panel("Timeline")), Lookup::None, "double press is its own trigger");
let strict = Keymap::new(catalogue, scopes().with_panel_fallthrough(false), Platform::Mac);
let strict = Keymap::new(catalog, scopes().with_panel_fallthrough(false), Platform::Mac);
assert_eq!(press(&strict, "L", &Active::panel("Effects")), Lookup::None);
assert_eq!(press(&strict, "L", &Active::panel("Timeline")), run("timeline.loop"));
}
#[test]
fn typing_rules_decide_what_fires_in_a_text_field() {
let catalogue = vec![app("tool.brush", &["B"]), app("edit.copy", &["Cmd+C"]), app("help", &["F1"]), app("view.alt", &["Alt+V"])];
let catalog = vec![app("tool.brush", &["B"]), app("edit.copy", &["Cmd+C"]), app("help", &["F1"]), app("view.alt", &["Alt+V"])];
let typing = Active { panel: None, contexts: &[], typing: true };
let cases = [
(TypingRule::PlainKeysWait, [false, true, true, true]),
@@ -88,7 +88,7 @@ fn typing_rules_decide_what_fires_in_a_text_field() {
(TypingRule::AllWait, [false, false, false, false]),
];
for (rule, fires) in cases {
let km = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Windows).with_typing_rule(rule);
let km = Keymap::new(catalog.clone(), Scopes::new(), Platform::Windows).with_typing_rule(rule);
for ((key, id), fire) in [("B", "tool.brush"), ("Cmd+C", "edit.copy"), ("F1", "help"), ("Alt+V", "view.alt")].into_iter().zip(fires) {
let want = if fire { run(id) } else { Lookup::None };
assert_eq!(press(&km, key, &typing), want, "{rule:?} {key}");
@@ -99,7 +99,7 @@ fn typing_rules_decide_what_fires_in_a_text_field() {
#[test]
fn bindings_of_an_active_context_fire_while_typing_under_every_rule() {
let catalogue = vec![
let catalog = vec![
app("text.bold", &["Text: Cmd+B", "Text: B"]),
app("file.bake", &["Cmd+B"]),
in_scope("timeline.cut", "Timeline", &["Cmd+K"]),
@@ -107,7 +107,7 @@ fn bindings_of_an_active_context_fire_while_typing_under_every_rule() {
];
let editing = Active { panel: Some("Timeline"), contexts: &["Text"], typing: true };
let elsewhere = Active { contexts: &[], ..editing };
let km = Keymap::new(catalogue, scopes(), Platform::Mac).with_typing_rule(TypingRule::AllWait);
let km = Keymap::new(catalog, scopes(), Platform::Mac).with_typing_rule(TypingRule::AllWait);
assert_eq!(press(&km, "Cmd+B", &editing), run("text.bold"));
assert_eq!(press(&km, "B", &editing), run("text.bold"));
assert_eq!(press(&km, "Cmd+K", &editing), Lookup::None, "panel bindings wait");
@@ -134,8 +134,8 @@ fn tools_of_one_group_cycle_and_keep_their_shared_key() {
#[test]
fn steal_keep_and_refuse() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), app("edit.duplicate", &["Cmd+D"]), in_scope("timeline.copy", "Timeline", &[])];
let mut km = Keymap::new(catalogue, scopes(), Platform::Mac);
let catalog = vec![app("edit.copy", &["Cmd+C"]), app("edit.duplicate", &["Cmd+D"]), in_scope("timeline.copy", "Timeline", &[])];
let mut km = Keymap::new(catalog, scopes(), Platform::Mac);
let dup = cmd("edit.duplicate");
let err = km.assign(&dup, b("Cmd+C"), Policy::Refuse).unwrap_err();
@@ -174,8 +174,8 @@ fn twins_keep_their_shared_default_key() {
#[test]
fn clearing_a_command_frees_its_panel_copies() {
// filmcraft#1333: New Bin holds Cmd+B in the application and in the Project panel.
let catalogue = vec![app("project.newBin", &["Cmd+B", "Project: Cmd+B"]), app("clip.bold", &[])];
let mut km = Keymap::new(catalogue, Scopes::new().with_panel("Project"), Platform::Mac);
let catalog = vec![app("project.newBin", &["Cmd+B", "Project: Cmd+B"]), app("clip.bold", &[])];
let mut km = Keymap::new(catalog, Scopes::new().with_panel("Project"), Platform::Mac);
assert_eq!(press(&km, "Cmd+B", &Active::panel("Project")), run("project.newBin"));
assert!(km.clear(&cmd("project.newBin")));
assert!(km.bindings(&cmd("project.newBin")).is_empty());
@@ -185,25 +185,25 @@ fn clearing_a_command_frees_its_panel_copies() {
#[test]
fn reserved_chords_are_refused() {
let catalogue = vec![app("view.next", &[])];
let mut mac = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Mac).with_reserved(reserved::system(Platform::Mac));
let catalog = vec![app("view.next", &[])];
let mut mac = Keymap::new(catalog.clone(), Scopes::new(), Platform::Mac).with_reserved(reserved::system(Platform::Mac));
let err = mac.assign(&cmd("view.next"), b("Cmd+Tab"), Policy::Steal).unwrap_err();
assert_eq!(err, AssignError::Reserved(Trigger::parse("Cmd+Tab").unwrap()));
let mut win = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Windows).with_reserved(reserved::system(Platform::Windows));
let mut win = Keymap::new(catalog.clone(), Scopes::new(), Platform::Windows).with_reserved(reserved::system(Platform::Windows));
assert!(matches!(win.assign(&cmd("view.next"), b("Ctrl+Alt+Delete"), Policy::Steal), Err(AssignError::Reserved(_))));
assert!(reserved::system(Platform::Linux).contains(&chord("Cmd+Alt+F12")));
let mut o = Overrides::new();
o.insert("view.next", vec!["Alt+F4".into()]);
let linux = Keymap::new(catalogue, Scopes::new(), Platform::Linux).with_reserved(reserved::system(Platform::Linux)).with_overrides(o);
let linux = Keymap::new(catalog, Scopes::new(), Platform::Linux).with_reserved(reserved::system(Platform::Linux)).with_overrides(o);
assert!(linux.bindings(&cmd("view.next")).is_empty());
assert_eq!(linux.problems(), vec![Problem::Reserved { key: "view.next".into(), text: "Alt+F4".into() }]);
}
#[test]
fn conflicts_tell_clashes_from_shadows() {
let catalogue = vec![
let catalog = vec![
app("edit.copy", &["Cmd+C"]),
app("color.copy", &["Cmd+C"]),
in_scope("timeline.copy", "Timeline", &["Cmd+C"]),
@@ -214,7 +214,7 @@ fn conflicts_tell_clashes_from_shadows() {
in_scope("render.find", "Render", &["Cmd+R"]),
in_scope("effects.render", "Effects", &["Cmd+R"]),
];
let mut km = Keymap::new(catalogue, scopes(), Platform::Mac);
let mut km = Keymap::new(catalog, scopes(), Platform::Mac);
let summary = |km: &Keymap| -> Vec<(String, String, String, ConflictKind, bool)> {
km.conflicts().into_iter().map(|c| (c.chord.to_string(), c.a.0.id, c.b.0.id, c.kind, c.in_defaults)).collect()
};
@@ -254,7 +254,7 @@ fn conflicts_tell_clashes_from_shadows() {
assert_eq!(with("edit.copy"), [w("Cmd+C", "color.copy", ConflictKind::Clash), w("Cmd+C", "timeline.copy", ConflictKind::Shadow)]);
assert_eq!(with("edit.cut"), [w("Cmd+X", "text.cut", ConflictKind::Shadow), w("Cmd+D", "edit.find", ConflictKind::Clash)]);
assert!(with("nope").is_empty());
for entry in km.catalogue() {
for entry in km.catalog() {
let id = entry.command.id.as_str();
let mut direct: Vec<_> =
km.conflicts_with(&entry.command).into_iter().map(|c| (c.chord, c.a.0.id, c.b.0.id, c.kind, c.in_defaults)).collect();
@@ -275,18 +275,18 @@ fn conflicts_tell_clashes_from_shadows() {
assert_eq!(direct, filtered, "{id}");
}
let strict = Keymap::new(km.catalogue().to_vec(), scopes().with_panel_fallthrough(false), Platform::Mac);
let strict = Keymap::new(km.catalog().to_vec(), scopes().with_panel_fallthrough(false), Platform::Mac);
assert!(!strict.conflicts().iter().any(|c| c.chord.to_string() == "Cmd+R"));
}
/// `commands` commands, each with 16 bindings on one chord in 16 panels.
fn crowded(commands: usize, scopes: Scopes, kind: Kind) -> (Keymap, Overrides) {
let catalogue: Vec<Bindable> = (0..commands).map(|n| app(&format!("c.{n}"), &[]).with_kind(kind.clone())).collect();
let catalog: Vec<Bindable> = (0..commands).map(|n| app(&format!("c.{n}"), &[]).with_kind(kind.clone())).collect();
let mut o = Overrides::new();
for n in 0..commands {
assert!(o.insert(format!("c.{n}"), (1..=MAX_BINDINGS).map(|p| format!("P{p}: K")).collect()));
}
(Keymap::new(catalogue, scopes, Platform::Mac), o)
(Keymap::new(catalog, scopes, Platform::Mac), o)
}
fn panels(n: usize) -> Scopes {
@@ -344,8 +344,8 @@ fn crowded_chords_stay_fast() {
#[test]
fn assigning_many_commands_stays_fast() {
let catalogue: Vec<Bindable> = (0..MAX_COMMANDS).map(|n| app(&format!("c.{n}"), &[])).collect();
let mut km = Keymap::new(catalogue, panels(MAX_BINDINGS), Platform::Mac);
let catalog: Vec<Bindable> = (0..MAX_COMMANDS).map(|n| app(&format!("c.{n}"), &[])).collect();
let mut km = Keymap::new(catalog, panels(MAX_BINDINGS), Platform::Mac);
let list: Vec<Binding> = (1..=MAX_BINDINGS).map(|p| b(&format!("P{p}: K"))).collect();
let t = Instant::now();
for n in 0..MAX_COMMANDS {
@@ -377,14 +377,14 @@ fn binding_text_round_trips_for_every_key() {
#[test]
fn edits_are_refused_when_the_overrides_are_full() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), app("edit.duplicate", &["Cmd+D"]), app("edit.cut", &[])];
let catalog = vec![app("edit.copy", &["Cmd+C"]), app("edit.duplicate", &["Cmd+D"]), app("edit.cut", &[])];
let mut o = Overrides::new();
o.insert("edit.cut", vec!["Cmd+X".into()]);
for n in 1..MAX_COMMANDS {
o.insert(format!("junk.{n}"), vec![]);
}
assert_eq!(o.len(), MAX_COMMANDS);
let mut km = Keymap::new(catalogue, Scopes::new(), Platform::Mac).with_overrides(o.clone());
let mut km = Keymap::new(catalog, Scopes::new(), Platform::Mac).with_overrides(o.clone());
let (copy, dup, cut) = (cmd("edit.copy"), cmd("edit.duplicate"), cmd("edit.cut"));
assert_eq!(km.assign(&dup, b("Cmd+K"), Policy::Steal), Err(AssignError::Full));
@@ -405,11 +405,11 @@ fn edits_are_refused_when_the_overrides_are_full() {
#[test]
fn bindings_in_unknown_scopes_are_refused() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), in_scope("timeline.mark", "Timeline", &["M"])];
let mut km = Keymap::new(catalogue, Scopes::new().with_context("Text"), Platform::Mac);
let catalog = vec![app("edit.copy", &["Cmd+C"]), in_scope("timeline.mark", "Timeline", &["M"])];
let mut km = Keymap::new(catalog, Scopes::new().with_context("Text"), Platform::Mac);
let copy = cmd("edit.copy");
assert_eq!(km.assign(&copy, b("Nowhere: Cmd+K"), Policy::Steal), Err(AssignError::UnknownScope(scope("Nowhere"))));
assert!(km.assign(&copy, b("Timeline: Cmd+K"), Policy::Steal).is_ok(), "a catalogue scope");
assert!(km.assign(&copy, b("Timeline: Cmd+K"), Policy::Steal).is_ok(), "a catalog scope");
assert!(km.assign(&copy, b("Text: Cmd+K"), Policy::Steal).is_ok(), "a registered scope");
let mut o = Overrides::new();
@@ -421,11 +421,11 @@ fn bindings_in_unknown_scopes_are_refused() {
#[test]
fn unreadable_entries_keep_the_defaults() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), app("edit.paste", &["Cmd+V"])];
let catalog = vec![app("edit.copy", &["Cmd+C"]), app("edit.paste", &["Cmd+V"])];
let mut o = Overrides::new();
o.insert("edit.copy", vec!["Cmd+Nope".into()]);
o.insert("edit.paste", vec![]);
let mut km = Keymap::new(catalogue, Scopes::new(), Platform::Mac).with_overrides(o);
let mut km = Keymap::new(catalog, Scopes::new(), Platform::Mac).with_overrides(o);
assert_eq!(km.bindings(&cmd("edit.copy")), vec![b("Cmd+C")]);
assert!(km.bindings(&cmd("edit.paste")).is_empty());
assert_eq!(km.overrides().get("edit.copy"), Some(&["Cmd+Nope".to_string()][..]), "verbatim");
@@ -435,13 +435,13 @@ fn unreadable_entries_keep_the_defaults() {
#[test]
fn entries_with_nothing_usable_keep_the_defaults() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), app("edit.cut", &["Cmd+X"]), app("edit.paste", &["Cmd+V"]), app("edit.undo", &["Cmd+Z"])];
let catalog = vec![app("edit.copy", &["Cmd+C"]), app("edit.cut", &["Cmd+X"]), app("edit.paste", &["Cmd+V"]), app("edit.undo", &["Cmd+Z"])];
let mut o = Overrides::new();
o.insert("edit.copy", vec!["Nowhere: Cmd+K".into()]);
o.insert("edit.cut", vec!["Alt+F4".into()]);
o.insert("edit.paste", vec!["Nowhere: Cmd+K".into(), "Alt+F4".into(), "Cmd+J".into()]);
o.insert("edit.undo", vec![]);
let km = Keymap::new(catalogue, Scopes::new(), Platform::Linux).with_reserved(reserved::system(Platform::Linux)).with_overrides(o);
let km = Keymap::new(catalog, Scopes::new(), Platform::Linux).with_reserved(reserved::system(Platform::Linux)).with_overrides(o);
assert_eq!(km.bindings(&cmd("edit.copy")), vec![b("Cmd+C")], "only an unknown scope");
assert_eq!(km.bindings(&cmd("edit.cut")), vec![b("Cmd+X")], "only a reserved chord");
assert_eq!(km.bindings(&cmd("edit.paste")), vec![b("Cmd+J")]);
@@ -450,8 +450,8 @@ fn entries_with_nothing_usable_keep_the_defaults() {
#[test]
fn overrides_hold_only_what_differs_from_the_defaults() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), in_scope("timeline.mark", "Timeline", &["M"])];
let mut km = Keymap::new(catalogue, scopes(), Platform::Mac);
let catalog = vec![app("edit.copy", &["Cmd+C"]), in_scope("timeline.mark", "Timeline", &["M"])];
let mut km = Keymap::new(catalog, scopes(), Platform::Mac);
let copy = cmd("edit.copy");
km.assign(&copy, b("Timeline: Cmd+K"), Policy::Steal).unwrap();
assert_eq!(km.overrides().get("edit.copy"), Some(&["Cmd+C".to_string(), "Timeline: Cmd+K".to_string()][..]));
@@ -468,10 +468,10 @@ fn overrides_hold_only_what_differs_from_the_defaults() {
#[test]
fn edits_keep_the_texts_this_build_cannot_use() {
let catalogue = vec![app("file.quit", &["Cmd+Q"]), app("file.close", &["Cmd+W"]), app("file.save", &["Cmd+S"])];
let catalog = vec![app("file.quit", &["Cmd+Q"]), app("file.close", &["Cmd+W"]), app("file.save", &["Cmd+S"])];
let mut o = Overrides::new();
o.insert("file.quit", vec!["Cmd+Q".into(), "Cmd+FutureKey".into(), "Nowhere: Cmd+J".into()]);
let mut km = Keymap::new(catalogue.clone(), scopes(), Platform::Mac).with_overrides(o);
let mut km = Keymap::new(catalog.clone(), scopes(), Platform::Mac).with_overrides(o);
let quit = cmd("file.quit");
let texts = |km: &Keymap| km.overrides().get("file.quit").map(<[String]>::to_vec);
let strings = |t: &[&str]| Some(t.iter().map(|s| s.to_string()).collect::<Vec<_>>());
@@ -480,7 +480,7 @@ fn edits_keep_the_texts_this_build_cannot_use() {
assert_eq!(texts(&km), strings(&["Cmd+Q", "Cmd+W", "Cmd+FutureKey", "Nowhere: Cmd+J"]));
km.assign(&cmd("file.save"), b("Cmd+Q"), Policy::Steal).unwrap();
assert_eq!(texts(&km), strings(&["Cmd+W", "Cmd+FutureKey", "Nowhere: Cmd+J"]), "taken by a Steal");
let reloaded = Keymap::new(catalogue, scopes(), Platform::Mac).with_overrides(km.overrides().clone());
let reloaded = Keymap::new(catalog, scopes(), Platform::Mac).with_overrides(km.overrides().clone());
assert_eq!(reloaded.bindings(&quit), vec![b("Cmd+W")]);
// An entry of only such texts would load as the defaults, so a cleared command drops them.
@@ -500,17 +500,17 @@ fn an_override_equal_to_the_defaults_on_this_platform_is_not_stored() {
#[test]
fn an_empty_text_in_the_map_form_removes_the_shortcut() {
let catalogue = vec![app("edit.copy", &["Cmd+C"]), app("edit.paste", &["Cmd+V"]), app("edit.cut", &["Cmd+X"])];
let catalog = vec![app("edit.copy", &["Cmd+C"]), app("edit.paste", &["Cmd+V"]), app("edit.cut", &["Cmd+X"])];
let o: Overrides = serde_json::from_str(r#"{"edit.copy": "", "edit.paste": [""], "edit.cut": "Cmd+K"}"#).unwrap();
assert!(o.load_notes().is_empty(), "{:?}", o.load_notes());
let km = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Mac).with_overrides(o);
let km = Keymap::new(catalog.clone(), Scopes::new(), Platform::Mac).with_overrides(o);
assert!(km.bindings(&cmd("edit.copy")).is_empty(), "a removed default stays removed");
assert!(km.bindings(&cmd("edit.paste")).is_empty());
assert_eq!(km.bindings(&cmd("edit.cut")), vec![b("Cmd+K")]);
assert!(km.problems().is_empty());
let saved = serde_json::to_string(km.overrides()).unwrap();
let again = Keymap::new(catalogue, Scopes::new(), Platform::Mac).with_overrides(serde_json::from_str(&saved).unwrap());
let again = Keymap::new(catalog, Scopes::new(), Platform::Mac).with_overrides(serde_json::from_str(&saved).unwrap());
assert!(again.bindings(&cmd("edit.copy")).is_empty(), "{saved}");
}
@@ -548,9 +548,9 @@ fn overrides_write_the_map_form_unless_a_command_has_several_keys() {
#[test]
fn ctrl_folds_into_cmd_off_macos() {
let catalogue = vec![app("edit.cut", &["Cmd+X"]), app("mac.cut", &["Ctrl+X"])];
let win = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Windows);
let mac = Keymap::new(catalogue, Scopes::new(), Platform::Mac);
let catalog = vec![app("edit.cut", &["Cmd+X"]), app("mac.cut", &["Ctrl+X"])];
let win = Keymap::new(catalog.clone(), Scopes::new(), Platform::Windows);
let mac = Keymap::new(catalog, Scopes::new(), Platform::Mac);
let conflicts = win.conflicts();
assert_eq!(conflicts.len(), 1);
assert_eq!((conflicts[0].kind, conflicts[0].chord.to_string()), (ConflictKind::Clash, "Cmd+X".to_string()));
@@ -606,12 +606,12 @@ fn hostile_overrides_load_without_panic_and_report_problems() {
#[test]
fn reset_removes_hand_written_variants_of_a_key() {
let layers = CommandRef::with_params("window.panel", json!({"panel": "layers"}));
let catalogue = vec![Bindable::new(layers.clone(), "Layers", Scope::app()).with_default(b("F7"))];
let catalog = vec![Bindable::new(layers.clone(), "Layers", Scope::app()).with_default(b("F7"))];
let load = || {
let mut o = Overrides::new();
o.insert(r#"window.panel {"panel":"layers"}"#, vec!["Cmd+L".into()]);
o.insert(r#"window.panel { "panel": "layers" }"#, vec!["Cmd+K".into()]);
Keymap::new(catalogue.clone(), Scopes::new(), Platform::Mac).with_overrides(o)
Keymap::new(catalog.clone(), Scopes::new(), Platform::Mac).with_overrides(o)
};
let mut km = load();
assert_eq!(km.bindings(&layers), vec![b("Cmd+L")], "the canonical entry wins");
@@ -622,6 +622,6 @@ fn reset_removes_hand_written_variants_of_a_key() {
let mut km = load();
km.set_bindings(&layers, vec![b("F7")], Policy::Steal).unwrap();
assert!(km.overrides().is_empty(), "{:?}", km.overrides());
let again = Keymap::new(catalogue.clone(), Scopes::new(), Platform::Mac).with_overrides(km.overrides().clone());
let again = Keymap::new(catalog.clone(), Scopes::new(), Platform::Mac).with_overrides(km.overrides().clone());
assert_eq!(again.bindings(&layers), vec![b("F7")]);
}
+2 -2
View File
@@ -34,13 +34,13 @@ fn open(dir: &std::path::Path) -> SettingsStore<Prefs> {
}
fn keymap(overrides: Overrides) -> Keymap {
let catalogue = vec![
let catalog = vec![
Bindable::new(CommandRef::with_params("window.panel", json!({"panel": "layers"})), "Layers", Scope::app()),
Bindable::new("text.bold", "Bold", Scope::app()),
Bindable::new("edit.copy", "Copy", Scope::app()).with_default(Binding::parse("Cmd+C").unwrap()),
Bindable::new("edit.paste", "Paste", Scope::app()).with_default(Binding::parse("Cmd+V").unwrap()),
];
Keymap::new(catalogue, Scopes::new().with_context("Text"), Platform::Mac).with_overrides(overrides)
Keymap::new(catalog, Scopes::new().with_context("Text"), Platform::Mac).with_overrides(overrides)
}
#[test]