diff --git a/crates/keymap/README.md b/crates/keymap/README.md index d8500ea1..cc25b79a 100644 --- a/crates/keymap/README.md +++ b/crates/keymap/README.md @@ -206,7 +206,9 @@ assert_eq!(sets.active_overrides(&builtins), sets.get("Mine", &builtins).unwrap( The app's reader turns a file (an InDesign Show Set, an Illustrator `.kys`) into an [`import::ForeignSet`](src/import.rs): entries with a menu, a path and keys read by -`ForeignKey::read`. `import::plan` matches each entry to one command and changes nothing; +`ForeignKey::read`. The reader builds it with `ForeignSet::new(entries)`, `with_name` and +`with_truncated`; the struct is `#[non_exhaustive]`, so struct literals outside the crate do not +compile. `import::plan` matches each entry to one command and changes nothing; `ImportPlan::apply` applies the plan and returns an `ImportReport` (serde, camelCase). Report rows carry the entry's `item` (its path) and `line`. @@ -220,14 +222,11 @@ let save = Bindable::new("file.save", "Save", Scope::app()) let mut keymap = Keymap::new(vec![save], Scopes::new(), Platform::Windows); let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows); -let set = ForeignSet { - name: Some("My Set".into()), - entries: vec![ - ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12), - ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13), - ], - truncated: 0, -}; +let set = ForeignSet::new(vec![ + ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12), + ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13), +]) +.with_name("My Set"); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() }; let planned = plan(&set, &keymap, &options); // A review UI can show planned.commands and planned.report here. @@ -328,6 +327,9 @@ Adopt it in three steps: 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. +Apps that import other applications' sets read each file into a `ForeignSet` built with +`ForeignSet::new` ([Importing a shortcut set](#importing-a-shortcut-set)). + ```rust,ignore use craft_keymap::ui::{DialogState, I18nText, Options, Outcome, ShortcutsDialog}; diff --git a/crates/keymap/src/import.rs b/crates/keymap/src/import.rs index e941c4b2..2cb03ddc 100644 --- a/crates/keymap/src/import.rs +++ b/crates/keymap/src/import.rs @@ -29,8 +29,9 @@ pub const MAX_PATH_SEGMENTS: usize = 32; /// in [`ImportReport::truncated`]. Entries without keys get no suggestion and do not count. pub const MAX_SUGGESTIONS: usize = 200; -/// A shortcut set as an app's reader found it. +/// A shortcut set as an app's reader found it. Build it with [`ForeignSet::new`]. #[derive(Clone, Debug, Default, PartialEq)] +#[non_exhaustive] pub struct ForeignSet { pub name: Option, pub entries: Vec, @@ -62,6 +63,25 @@ pub struct ForeignEntry { pub panel_menu: bool, } +impl ForeignSet { + /// A set without a name. + pub fn new(entries: Vec) -> ForeignSet { + ForeignSet { entries, ..ForeignSet::default() } + } + + /// The set's name as the source gives it. + pub fn with_name(mut self, name: impl Into) -> ForeignSet { + self.name = Some(name.into()); + self + } + + /// Entries the reader dropped at its own limits ([`ForeignSet::truncated`]). + pub fn with_truncated(mut self, truncated: usize) -> ForeignSet { + self.truncated = truncated; + self + } +} + impl ForeignEntry { /// An entry without keys. pub fn new>(menu: Option<&str>, path: impl IntoIterator) -> ForeignEntry { diff --git a/crates/keymap/src/lib.rs b/crates/keymap/src/lib.rs index fb9a86a9..d5a82abb 100644 --- a/crates/keymap/src/lib.rs +++ b/crates/keymap/src/lib.rs @@ -118,14 +118,11 @@ //! let mut keymap = Keymap::new(vec![save], Scopes::new(), Platform::Windows); //! //! let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows); -//! let set = ForeignSet { -//! name: Some("My Set".into()), -//! entries: vec![ -//! ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12), -//! ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13), -//! ], -//! truncated: 0, -//! }; +//! let set = ForeignSet::new(vec![ +//! ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12), +//! ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13), +//! ]) +//! .with_name("My Set"); //! let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() }; //! let planned = plan(&set, &keymap, &options); //! assert_eq!(planned.commands.len(), 1); diff --git a/crates/keymap/src/ui/state.rs b/crates/keymap/src/ui/state.rs index 7b2a59c3..f83b4392 100644 --- a/crates/keymap/src/ui/state.rs +++ b/crates/keymap/src/ui/state.rs @@ -1469,11 +1469,7 @@ mod tests { assert!(matches!(st.finish(), Some(Outcome::Import { .. }))); assert!(fx.km.overrides().is_empty() && !st.can_undo()); let read = |text: &str| ForeignKey::read(Some("Default"), text, crate::Flavor::Windows); - let set = ForeignSet { - name: Some("Theirs".into()), - entries: vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S"))], - truncated: 0, - }; + let set = ForeignSet::new(vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S"))]).with_name("Theirs"); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() }; st.review_import(&mut fx.env(), set, options); st.import_reviewed(&mut fx.env()); @@ -1548,14 +1544,11 @@ mod tests { let mut fx = Fx::new(Options::default()); let mut st = open(&mut fx); let read = |text: &str| ForeignKey::read(Some("Default"), text, crate::Flavor::Windows); - let set = ForeignSet { - name: Some("Theirs".into()), - entries: vec![ - ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), - ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), + ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), + ]) + .with_name("Theirs"); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() }; st.review_import(&mut fx.env(), set, options); let r = st.review.as_ref(); diff --git a/crates/keymap/tests/import.rs b/crates/keymap/tests/import.rs index 83d96795..60a9f321 100644 --- a/crates/keymap/tests/import.rs +++ b/crates/keymap/tests/import.rs @@ -110,11 +110,11 @@ fn windows_set() -> ForeignSet { entry(None, &["Somewhere", "Zoom In"], &[(d, "Ctrl+F11")], Flavor::Windows), win("Scripts", &["Frobnicate"], &[(d, "Ctrl+Alt+R")]), ]; - ForeignSet { name: Some("My Set".into()), entries, truncated: 0 } + ForeignSet::new(entries).with_name("My Set") } fn numpad_set() -> ForeignSet { - ForeignSet { name: None, entries: vec![win("Window", &["Layers"], &[("Default", "Num 7")]).at_line(2)], truncated: 0 } + ForeignSet::new(vec![win("Window", &["Layers"], &[("Default", "Num 7")]).at_line(2)]) } fn windows_options() -> ImportOptions { @@ -254,16 +254,13 @@ fn the_set_wins_and_the_report_says_how() { fn a_mac_set_reads_its_modifiers_and_scopes_text_keys() { let mut km = keymap(Platform::Mac); let mac = |menu: &str, path: &[&str], keys: &[(&str, &str)]| entry(Some(menu), path, keys, Flavor::Mac); - let set = ForeignSet { - name: Some("Mac".into()), - entries: vec![ - mac("File", &["Save"], &[("Default", "⇧⌘S")]), - mac("Edit", &["Paste"], &[("Default", "Cmd+Ctrl+V")]), - mac("Type", &["Style", "Italic"], &[("Text", "Shift+Cmd+I")]), - mac("Type", &["Style", "Bold"], &[("Default", "Cmd+B"), ("Tables", "Cmd+Opt+B")]), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + mac("File", &["Save"], &[("Default", "⇧⌘S")]), + mac("Edit", &["Paste"], &[("Default", "Cmd+Ctrl+V")]), + mac("Type", &["Style", "Italic"], &[("Text", "Shift+Cmd+I")]), + mac("Type", &["Style", "Bold"], &[("Default", "Cmd+B"), ("Tables", "Cmd+Opt+B")]), + ]) + .with_name("Mac"); let text = Scope::new("Text").unwrap(); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home), ("Text".into(), ContextRule::Scope(text))], @@ -283,8 +280,7 @@ fn a_mac_set_reads_its_modifiers_and_scopes_text_keys() { #[test] fn start_and_none_defined_options() { - let set = - ForeignSet { name: None, entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q")]), win("Edit", &["Duplicate"], &[])], truncated: 0 }; + let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q")]), win("Edit", &["Duplicate"], &[])]); let user = || { let mut o = Overrides::new(); o.insert("file.save", vec!["Cmd+Alt+S".into()]); @@ -316,15 +312,11 @@ fn a_failed_change_keeps_its_shortcut_when_a_later_change_frees_room() { // Save's change finds the overrides full. Close's change then frees an entry, and Zoom In // takes Cmd+S. Removing Cmd+S from Save would fit now, but Save's import failed, so Save // keeps it and Zoom In stays as it was. - let set = ForeignSet { - name: None, - entries: vec![ - win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]), - win("File", &["Close"], &[("Default", "Ctrl+W")]), - win("View", &["Zoom In"], &[("Default", "Ctrl+S")]), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]), + win("File", &["Close"], &[("Default", "Ctrl+W")]), + win("View", &["Zoom In"], &[("Default", "Ctrl+S")]), + ]); let mut o = Overrides::new(); o.insert("file.close", vec!["Cmd+Q".into()]); o.insert("view.zoomIn", vec!["Cmd+Alt+Z".into()]); @@ -346,15 +338,11 @@ fn a_failed_change_keeps_its_shortcut_when_a_later_change_frees_room() { #[test] fn changes_that_find_the_overrides_full_are_reported() { - let set = ForeignSet { - name: None, - entries: vec![ - win("File", &["Close"], &[("Default", "Ctrl+J")]), - win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]), - win("Edit", &["Duplicate"], &[]), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + win("File", &["Close"], &[("Default", "Ctrl+J")]), + win("File", &["Save"], &[("Default", "Ctrl+Alt+S")]), + win("Edit", &["Duplicate"], &[]), + ]); let full = || { let mut o = Overrides::new(); o.insert("file.close", vec!["Cmd+Q".into()]); @@ -406,11 +394,7 @@ fn changes_that_find_the_overrides_full_are_reported() { has(&km, "edit.duplicate", &[]); // A later entry whose change finds the overrides full leaves the chord with the earlier one. - let set = ForeignSet { - name: None, - entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Alt+J")]), win("File", &["Save"], &[("Default", "Ctrl+Alt+J")])], - truncated: 0, - }; + let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Alt+J")]), win("File", &["Save"], &[("Default", "Ctrl+Alt+J")])]); let mut km = keymap(Platform::Windows).with_overrides(full()); let r = plan(&set, &km, &options(Start::Current)).apply(&mut km); has(&km, "file.close", &["Cmd+Alt+J"]); @@ -422,11 +406,7 @@ fn changes_that_find_the_overrides_full_are_reported() { #[test] fn keys_in_unknown_scopes_are_skipped_and_the_rest_apply() { - let set = ForeignSet { - name: None, - entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q"), ("Story", "Ctrl+K"), ("Text", "Ctrl+L")])], - truncated: 0, - }; + let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q"), ("Story", "Ctrl+K"), ("Text", "Ctrl+L")])]); let options = ImportOptions { contexts: vec![ ("Default".into(), ContextRule::Home), @@ -450,7 +430,7 @@ fn keys_in_unknown_scopes_are_skipped_and_the_rest_apply() { #[test] fn suggestions_are_reported_never_applied() { let mut km = keymap(Platform::Windows); - let set = ForeignSet { name: None, entries: vec![win("Edit", &["Chek Spelling Now"], &[("Default", "Ctrl+K")])], truncated: 0 }; + let set = ForeignSet::new(vec![win("Edit", &["Chek Spelling Now"], &[("Default", "Ctrl+K")])]); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() }; let p = plan(&set, &km, &options); assert!(p.commands.is_empty()); @@ -465,7 +445,7 @@ fn suggestions_are_reported_never_applied() { #[test] fn entries_the_reader_dropped_count_as_truncated() { - let set = ForeignSet { name: None, entries: vec![win("File", &["Close"], &[("Default", "Ctrl+Q")])], truncated: 7 }; + let set = ForeignSet::new(vec![win("File", &["Close"], &[("Default", "Ctrl+Q")])]).with_truncated(7); assert_eq!(plan(&set, &keymap(Platform::Windows), &default_context()).report.truncated, 7); } @@ -486,7 +466,7 @@ fn hostile_sets_are_capped_and_counted() { // Entries without keys fill the set to the cap and 500 past it. let keyless = MAX_IMPORT_ENTRIES - entries.len(); entries.extend(std::iter::repeat_n(ForeignEntry::new(None, Vec::::new()), keyless + 500)); - let set = ForeignSet { name: Some(long.clone()), entries, truncated: 0 }; + let set = ForeignSet::new(entries).with_name(long.clone()); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() }; let r = plan(&set, &km, &options).apply(&mut km); assert_eq!(r.suggestions.len(), MAX_SUGGESTIONS); @@ -529,7 +509,7 @@ fn hostile_sets_finish_in_time() { } entries.insert(0, big); entries.insert(1, win("Edit", &["Paste"], &[("Default", &long), ("Default", "Ctrl+\u{0}"), ("Default", "Win+E")])); - let set = ForeignSet { name: Some(long.clone()), entries, truncated: 0 }; + let set = ForeignSet::new(entries).with_name(long.clone()); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() }; let started = std::time::Instant::now(); let r = plan(&set, &km, &options).apply(&mut km); @@ -553,17 +533,13 @@ fn hostile_sets_finish_in_time() { fn rows_carry_source_lines_and_approximated_keys() { let mut km = keymap(Platform::Windows); let d = "Default"; - let set = ForeignSet { - name: None, - entries: vec![ - win("Window", &["Layers"], &[(d, "Ctrl+Num 5"), (d, "Num 7")]).at_line(3), - win("Window", &["Layers"], &[(d, "Shift+F7")]).at_line(4), - win("Edit", &["Paste"], &[(d, "Ctrl+Num 5")]).at_line(5), - win("Edit", &["Chek Spelling Now"], &[(d, "Ctrl+K")]).at_line(8), - win("File", &["Save"], &[(d, "Ctrl+Hyperspace")]).at_line(9), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + win("Window", &["Layers"], &[(d, "Ctrl+Num 5"), (d, "Num 7")]).at_line(3), + win("Window", &["Layers"], &[(d, "Shift+F7")]).at_line(4), + win("Edit", &["Paste"], &[(d, "Ctrl+Num 5")]).at_line(5), + win("Edit", &["Chek Spelling Now"], &[(d, "Ctrl+K")]).at_line(8), + win("File", &["Save"], &[(d, "Ctrl+Hyperspace")]).at_line(9), + ]); let options = ImportOptions { contexts: vec![(d.into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() }; let r = plan(&set, &km, &options).apply(&mut km); // A keypad key binds the main key and is reported. Paste, a later entry, takes Cmd+5. @@ -639,7 +615,7 @@ fn report_json_uses_camel_case_names() { fn import_into(catalog: Vec, entries: Vec, options: &ImportOptions) -> (Keymap, ImportReport) { let mut km = Keymap::new(catalog, Scopes::new(), Platform::Windows); - let report = plan(&ForeignSet { name: None, entries, truncated: 0 }, &km, options).apply(&mut km); + let report = plan(&ForeignSet::new(entries), &km, options).apply(&mut km); (km, report) } diff --git a/crates/keymap/tests/profiles.rs b/crates/keymap/tests/profiles.rs index d9ce152c..e3b789b8 100644 --- a/crates/keymap/tests/profiles.rs +++ b/crates/keymap/tests/profiles.rs @@ -162,11 +162,7 @@ fn duplicate_copies_built_in_and_user_sets() { fn a_foreign_set_imports_as_a_new_set_and_leaves_the_keymap_alone() { let km = keymap(); let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Mac); - let set = ForeignSet { - name: Some("Other App".into()), - entries: vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Cmd+Shift+S"))], - truncated: 0, - }; + let set = ForeignSet::new(vec![ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Cmd+Shift+S"))]).with_name("Other App"); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], start: Start::Defaults, ..ImportOptions::default() }; let planned = plan(&set, &km, &options); let builtins = [("Craft", Overrides::new())]; diff --git a/crates/keymap/tests/ui_dialog.rs b/crates/keymap/tests/ui_dialog.rs index 751d60ed..59d68b8c 100644 --- a/crates/keymap/tests/ui_dialog.rs +++ b/crates/keymap/tests/ui_dialog.rs @@ -724,17 +724,14 @@ fn set_export_import_round_trip() { fn foreign_set() -> ForeignSet { let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows); - ForeignSet { - name: Some("Theirs".into()), - entries: vec![ - ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), - ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), - ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5), - ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6), - ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7), - ], - truncated: 0, - } + ForeignSet::new(vec![ + ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), + ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), + ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5), + ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6), + ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7), + ]) + .with_name("Theirs") } fn review(h: &mut Harness<'_, Fx>) { diff --git a/crates/keymap/tests/ui_snapshot.rs b/crates/keymap/tests/ui_snapshot.rs index ba27ab05..cbb64482 100644 --- a/crates/keymap/tests/ui_snapshot.rs +++ b/crates/keymap/tests/ui_snapshot.rs @@ -117,17 +117,14 @@ fn fonts() -> egui::FontDefinitions { fn foreign_review(h: &mut Harness<'_, Fx>) { let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows); - let set = ForeignSet { - name: Some("Theirs".into()), - entries: vec![ - ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), - ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), - ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5), - ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6), - ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7), - ], - truncated: 0, - }; + let set = ForeignSet::new(vec![ + ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(3), + ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(4), + ForeignEntry::new(Some("File"), ["Export…"]).with_key(read("Ctrl+Blorp")).at_line(5), + ForeignEntry::new(Some("Object"), ["Arrange", "Bring to Front"]).with_key(read("Num 7")).at_line(6), + ForeignEntry::new(Some("File"), ["Sve Az"]).with_key(read("Ctrl+F9")).at_line(7), + ]) + .with_name("Theirs"); let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], suggest: true, ..ImportOptions::default() }; let (d, state) = h.state_mut().dialog(); d.review_import(state, set, options);