diff --git a/Cargo.lock b/Cargo.lock index 71ae28db9..59aee3de2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -832,7 +832,7 @@ source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09 [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=d9b1a89950c65685488da5807a2023dda31e1135#d9b1a89950c65685488da5807a2023dda31e1135" +source = "git+https://github.com/aiongg/craft-libs?rev=f9a65e813f5cb7986fc80b11bc0abfd410f1e224#f9a65e813f5cb7986fc80b11bc0abfd410f1e224" dependencies = [ "intl_pluralrules", "sys-locale", @@ -842,10 +842,10 @@ dependencies = [ [[package]] name = "craft-keymap" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=d9b1a89950c65685488da5807a2023dda31e1135#d9b1a89950c65685488da5807a2023dda31e1135" +source = "git+https://github.com/aiongg/craft-libs?rev=f9a65e813f5cb7986fc80b11bc0abfd410f1e224#f9a65e813f5cb7986fc80b11bc0abfd410f1e224" dependencies = [ "craft-i18n", - "craft-ui 0.1.0 (git+https://github.com/aiongg/craft-libs?rev=d9b1a89950c65685488da5807a2023dda31e1135)", + "craft-ui 0.1.0 (git+https://github.com/aiongg/craft-libs?rev=f9a65e813f5cb7986fc80b11bc0abfd410f1e224)", "egui", "serde", "serde_json", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=d9b1a89950c65685488da5807a2023dda31e1135#d9b1a89950c65685488da5807a2023dda31e1135" +source = "git+https://github.com/aiongg/craft-libs?rev=f9a65e813f5cb7986fc80b11bc0abfd410f1e224#f9a65e813f5cb7986fc80b11bc0abfd410f1e224" dependencies = [ "egui", ] diff --git a/Cargo.toml b/Cargo.toml index e7b90dd4b..c8f1c1223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/storytold/designcraft" # Shared writer; the immutable revision must be published before this consumer. craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false } # Shared keyboard bindings: chords, overrides, conflicts, egui dispatch, the Keyboard Shortcuts dialog. -craft-keymap = { git = "https://github.com/aiongg/craft-libs", rev = "d9b1a89950c65685488da5807a2023dda31e1135", version = "=0.1.0", features = ["egui", "ui"] } +craft-keymap = { git = "https://github.com/aiongg/craft-libs", rev = "f9a65e813f5cb7986fc80b11bc0abfd410f1e224", version = "=0.1.0", features = ["egui", "ui"] } # internal designcraft-geom = { path = "crates/geom" } designcraft-color = { path = "crates/color" } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 5e3604a81..1244225d3 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -74,6 +74,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ], ), ("Close", ["Schließen", "Fermer", "Cerrar", "閉じる", "关闭"]), + ("Dismiss", ["Ausblenden", "Ignorer", "Descartar", "閉じる", "关闭"]), ("Save", ["Speichern", "Enregistrer", "Guardar", "保存", "保存"]), ("Save As…", ["Speichern unter …", "Enregistrer sous…", "Guardar como…", "別名で保存…", "另存为…"]), ("Save a Copy…", ["Kopie speichern …", "Enregistrer une copie…", "Guardar una copia…", "コピーを保存…", "保存副本…"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..66db77215 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -14,6 +14,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document", "مستند جديد"), ("Open…", "فتح…"), ("Close", "إغلاق"), + ("Dismiss", "تجاهل"), ("Save", "حفظ"), ("Save As…", "حفظ باسم…"), ("Save a Copy…", "حفظ نسخة…"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..a5b52f11f 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -16,6 +16,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "Nový dokument…"), ("Open…", "Otevřít…"), ("Close", "Zavřít"), + ("Dismiss", "Skrýt"), ("Quit DesignCraft", "Ukončit DesignCraft"), ("Save", "Uložit"), ("Save As…", "Uložit jako…"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..9cd12bbc3 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -14,6 +14,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "Nuovo documento…"), ("Open…", "Apri…"), ("Close", "Chiudi"), + ("Dismiss", "Ignora"), ("Save", "Salva"), ("Save As…", "Salva con nome…"), ("Save a Copy…", "Salva una copia…"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..165fe5a15 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -17,6 +17,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "ახალი დოკუმენტი…"), ("Open…", "გახსნა…"), ("Close", "დახურვა"), + ("Dismiss", "დამალვა"), ("Save", "შენახვა"), ("Save As…", "შენახვა როგორც…"), ("Save a Copy…", "ასლის შენახვა…"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..9cdaef030 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -15,6 +15,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "Nowy dokument…"), ("Open…", "Otwórz…"), ("Close", "Zamknij"), + ("Dismiss", "Ukryj"), ("Save", "Zapisz"), ("Save As…", "Zapisz jako…"), ("Save a Copy…", "Zapisz kopię…"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..ce094d3cb 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -15,6 +15,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document", "Novo documento"), ("Open…", "Abrir…"), ("Close", "Fechar"), + ("Dismiss", "Dispensar"), ("Save", "Salvar"), ("Save As…", "Salvar como…"), ("Save a Copy…", "Salvar uma cópia…"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..22d0b56b1 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -18,6 +18,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "Новый документ…"), ("Open…", "Открыть…"), ("Close", "Закрыть"), + ("Dismiss", "Скрыть"), ("Quit DesignCraft", "Выйти из DesignCraft"), ("Save", "Сохранить"), ("Save As…", "Сохранить как…"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..34f8bf451 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -15,6 +15,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("New Document…", "Новий документ…"), ("Open…", "Відкрити…"), ("Close", "Закрити"), + ("Dismiss", "Приховати"), ("Save", "Зберегти"), ("Save As…", "Зберегти як…"), ("Save a Copy…", "Зберегти копію…"), diff --git a/crates/ui-egui/src/keys.rs b/crates/ui-egui/src/keys.rs index 9874603bd..72d133f44 100644 --- a/crates/ui-egui/src/keys.rs +++ b/crates/ui-egui/src/keys.rs @@ -56,7 +56,7 @@ const IMPORT_ALIASES: &[(&str, &str)] = &[ /// only to toggles. const TOGGLES: &[&str] = &["view.hiddenCharacters", "view.tagMarkers"]; -/// Extra menu paths for import, where an alias would leave two commands on one name: InDesign's +/// Paths only the importer reads, where an alias would leave two commands on one name: InDesign's /// Tools area has Apply Color, which is also `object.color`'s label. const IMPORT_PATHS: &[(&str, &[&str])] = &[("color.applyColor", &["Tools", "Apply Color"])]; @@ -142,14 +142,26 @@ pub fn keymap_for(platform: Platform, overrides: Overrides) -> Keymap { } /// Every bindable command, in dispatch order: where two share keys the earlier one runs. UI -/// commands come first (⌘N is the New Document dialog, not a bare `file.new`), then the engine -/// commands in menus or with default keys, panels and tools. +/// commands come first (⌘N is the New Document dialog, not a bare `file.new`), then engine +/// commands, panels and tools. pub fn catalog() -> &'static [Bindable] { - static CATALOG: OnceLock> = OnceLock::new(); + &built().0 +} + +/// Whether the Keyboard Shortcuts dialog lists `b`. Engine commands with neither a menu row nor a +/// default key are for scripts and the control channel (`file.save`, `selection.set`): the dialog +/// leaves them out, and they still take keys and count in conflicts. +pub fn listed(b: &Bindable) -> bool { + !(b.command.params.is_null() && built().1.contains(b.command.id.as_str())) +} + +fn built() -> &'static (Vec, HashSet<&'static str>) { + static CATALOG: OnceLock<(Vec, HashSet<&'static str>)> = OnceLock::new(); CATALOG.get_or_init(build_catalog) } -fn build_catalog() -> Vec { +/// The catalog and the engine commands [`listed`] leaves out. +fn build_catalog() -> (Vec, HashSet<&'static str>) { let paths = menu_paths(); // An engine command's own menu placement counts only where no menu row stands: a row there // runs a UI command in its place (File › Save is the Save dialog when needed), and two @@ -173,20 +185,18 @@ fn build_catalog() -> Vec { add(CommandRef::new(*id), label, menus::default_shortcut(id).as_slice(), None, Kind::Command); } // A UI command's default key runs it, never the engine command it stands in for (⌘S is the - // Save dialog when needed, not a bare `file.save`): the engine command does not get that key - // too, so the two are not listed as a conflict. Engine commands left with neither a menu row - // nor a default key are for scripts and the control channel (`file.save`, `selection.set`): - // they take no shortcut and stay out of the Keyboard Shortcuts dialog. + // Save dialog when needed, not a bare `file.save`): the engine command does not get that key. let ui_keys: Vec = menus::UI_COMMANDS .iter() .filter_map(|(id, ..)| menus::default_shortcut(id)) .filter_map(|d| Binding::parse(d).ok()) .map(|b| b.trigger) .collect(); + let mut internal = HashSet::new(); for c in designcraft_engine::command_specs() { let shortcut = c.shortcut.filter(|d| Binding::parse(d).is_ok_and(|b| !ui_keys.contains(&b.trigger))); if c.menu.is_empty() && shortcut.is_none() && !paths.contains_key(c.id) { - continue; + internal.insert(c.id); } let path = (!c.menu.is_empty()).then(|| c.menu.iter().map(|s| s.to_string()).chain([c.label.to_string()]).collect()); add(CommandRef::new(c.id), c.label, shortcut.as_slice(), path, Kind::Command); @@ -206,10 +216,14 @@ fn build_catalog() -> Vec { let Some(first) = group.first() else { continue }; for t in group.iter() { let kind = Kind::Tool { group: Some(first.id.to_string()) }; - add(tool(t.id), t.label, t.shortcut.as_slice(), Some(vec!["Tools".into(), t.label.into()]), kind); + add(tool(t.id), t.label, t.shortcut.as_slice(), None, kind); } } for b in &mut out { + // Shortcut sets file tools under Tools, which is no menu here. + if matches!(b.kind, Kind::Tool { .. }) { + b.import_paths.push(vec!["Tools".into(), b.label.clone()]); + } if !b.command.params.is_null() { continue; } @@ -222,9 +236,9 @@ fn build_catalog() -> Vec { b.defaults.extend(copies); } b.aliases.extend(IMPORT_ALIASES.iter().filter(|(c, _)| *c == id).map(|(_, a)| a.to_string())); - b.paths.extend(IMPORT_PATHS.iter().filter(|(c, _)| *c == id).map(|(_, p)| p.iter().map(|s| s.to_string()).collect())); + b.import_paths.extend(IMPORT_PATHS.iter().filter(|(c, _)| *c == id).map(|(_, p)| p.iter().map(|s| s.to_string()).collect())); } - out + (out, internal) } /// Menu paths (menu, submenus, label) of every menu row, by command key. @@ -540,6 +554,19 @@ mod tests { } } + /// An engine command only scripts run has no row in the dialog, yet takes keys and conflicts. + #[test] + fn an_unlisted_engine_command_is_still_bindable_and_conflicts() { + let find = |id: &str| catalog().iter().find(|b| b.command == CommandRef::new(id)); + let internal = find("selection.set").unwrap(); + assert!(!listed(internal)); + assert!(listed(find("edit.undo").unwrap())); + assert!(catalog().iter().filter(|b| matches!(b.kind, Kind::Tool { .. })).all(listed)); + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + let r = menus::run_ui(&mut app, "window.setShortcut", &json!({"id": "selection.set", "shortcut": "Cmd+Z"})).unwrap().unwrap(); + assert_eq!(r["conflicts"], json!(["edit.undo"])); + } + /// A panel and the command that opens it share their key without a reported conflict. #[test] fn a_panels_twin_is_not_a_conflict() { diff --git a/crates/ui-egui/src/shortcut_import.rs b/crates/ui-egui/src/shortcut_import.rs index 66a98c34e..f977b11bf 100644 --- a/crates/ui-egui/src/shortcut_import.rs +++ b/crates/ui-egui/src/shortcut_import.rs @@ -189,7 +189,7 @@ fn foreign(set: &ParsedSet) -> ForeignSet { entry }) .collect(); - ForeignSet { name: Some(set.name.clone()), entries } + ForeignSet { name: Some(set.name.clone()), entries, truncated: set.dropped } } /// What DesignCraft's contexts take from a set: Default keys bind in the command's home scope; an @@ -203,14 +203,14 @@ pub fn import_options(remembered: BTreeMap) -> ImportOptions } } -/// Reads a set's text for craft-keymap's importer. Returns the set and the number of items past -/// [`MAX_IMPORT_ENTRIES`], which were not read. -pub fn read_set(text: &str) -> Result<(ForeignSet, usize), String> { +/// Reads a set's text for craft-keymap's importer. Items past [`MAX_IMPORT_ENTRIES`] are not read; +/// the set counts them as truncated. +pub fn read_set(text: &str) -> Result { if text.len() > MAX_BYTES { return Err(format!("the set is larger than {} MB", MAX_BYTES >> 20)); } let parsed = parse(text)?; - Ok((foreign(&parsed), parsed.dropped)) + Ok(foreign(&parsed)) } /// Imports a set as a new shortcut set, which becomes active: every matched command takes the @@ -218,13 +218,12 @@ pub fn read_set(text: &str) -> Result<(ForeignSet, usize), String> { /// takes (unless they share them as defaults), and the rest keep theirs. Returns the report, with /// the new set's name as `shortcutSet`. pub fn import(app: &mut DesignApp, text: &str) -> Result { - let (set, dropped) = read_set(text)?; + let set = read_set(text)?; app.sync_keymap(); let options = import_options(app.shortcut_answers.clone()); let plan = craft_keymap::plan(&set, &app.keymap, &options); - let (name, mut report) = + let (name, report) = app.ui.shortcut_sets.import_plan(&plan, &app.keymap, None, &crate::shortcuts_dialog::BUILTINS).map_err(|e| e.to_string())?; - report.truncated = report.truncated.saturating_add(dropped); crate::shortcuts_dialog::load_active_set(app); let mut out = report_json(&report); out["shortcutSet"] = json!(name); diff --git a/crates/ui-egui/src/shortcuts_dialog.rs b/crates/ui-egui/src/shortcuts_dialog.rs index 30636b593..62ea4518e 100644 --- a/crates/ui-egui/src/shortcuts_dialog.rs +++ b/crates/ui-egui/src/shortcuts_dialog.rs @@ -51,11 +51,13 @@ impl Host { } /// The owner-approved behavior: live editing with Cancel restoring, conflicts asked about, no -/// double presses, the US keyboard pane, InDesign sets to import. +/// double presses, the US keyboard pane, InDesign sets to import, and no rows for the engine +/// commands only scripts run. fn options() -> Options { Options::default().with(|o| { o.keyboard = true; o.import_formats = vec![INDESIGN_FORMAT]; + o.visible = Some(crate::keys::listed); }) } @@ -185,7 +187,7 @@ fn request_import(app: &mut DesignApp, host: &mut Host) { let Some(path) = pick(PURPOSE) else { return }; match crate::shortcut_import::read_file(&path) { Ok(text) => review(app, host, &text), - Err(e) => app.status(e), + Err(e) => host.state.show_error(e), } return; } @@ -200,7 +202,7 @@ pub(crate) fn deliver(app: &mut DesignApp, bytes: &[u8]) -> bool { let Some(mut host) = app.shortcut_dialog.take() else { return false }; match crate::shortcut_import::decode(bytes) { Ok(text) => review(app, &mut host, &text), - Err(e) => app.status(e), + Err(e) => host.state.show_error(e), } app.shortcut_dialog = Some(host); true @@ -218,12 +220,12 @@ fn review(app: &mut DesignApp, host: &mut Host, text: &str) { return; } match crate::shortcut_import::read_set(text) { - Ok((set, _)) => { + Ok(set) => { let options = crate::shortcut_import::import_options(app.shortcut_answers.clone()); let dialog = ShortcutsDialog::new(&mut app.keymap, &words).options(&host.options).sets(&mut app.ui.shortcut_sets, &BUILTINS); dialog.review_import(&mut host.state, set, options); } - Err(e) => app.status(e), + Err(e) => host.state.show_error(e), } } @@ -460,7 +462,7 @@ mod tests { for s in strings() { assert_eq!(text.ui(s), s); } - assert_eq!(craft_keymap::ui::STRINGS.len(), 174); + assert_eq!(craft_keymap::ui::STRINGS.len(), 175); assert_eq!(craft_keymap::ui::PLURALS.len(), 11); assert_eq!(text.plural(2, "{n} command will change", "{n} commands will change"), "2 commands will change"); assert_eq!(DcText { lang: "fr" }.ui("Cancel"), tr("fr", "Cancel"));