mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 12:46:53 -06:00
Shortcuts: craft-keymap f9a65e8 (craft-libs#186): hidden internal commands, import paths, dialog errors
This commit is contained in:
Generated
+4
-4
@@ -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",
|
||||
]
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
@@ -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…", "コピーを保存…", "保存副本…"]),
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("New Document", "مستند جديد"),
|
||||
("Open…", "فتح…"),
|
||||
("Close", "إغلاق"),
|
||||
("Dismiss", "تجاهل"),
|
||||
("Save", "حفظ"),
|
||||
("Save As…", "حفظ باسم…"),
|
||||
("Save a Copy…", "حفظ نسخة…"),
|
||||
|
||||
@@ -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…"),
|
||||
|
||||
@@ -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…"),
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("New Document…", "ახალი დოკუმენტი…"),
|
||||
("Open…", "გახსნა…"),
|
||||
("Close", "დახურვა"),
|
||||
("Dismiss", "დამალვა"),
|
||||
("Save", "შენახვა"),
|
||||
("Save As…", "შენახვა როგორც…"),
|
||||
("Save a Copy…", "ასლის შენახვა…"),
|
||||
|
||||
@@ -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ę…"),
|
||||
|
||||
@@ -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…"),
|
||||
|
||||
@@ -18,6 +18,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("New Document…", "Новый документ…"),
|
||||
("Open…", "Открыть…"),
|
||||
("Close", "Закрыть"),
|
||||
("Dismiss", "Скрыть"),
|
||||
("Quit DesignCraft", "Выйти из DesignCraft"),
|
||||
("Save", "Сохранить"),
|
||||
("Save As…", "Сохранить как…"),
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("New Document…", "Новий документ…"),
|
||||
("Open…", "Відкрити…"),
|
||||
("Close", "Закрити"),
|
||||
("Dismiss", "Приховати"),
|
||||
("Save", "Зберегти"),
|
||||
("Save As…", "Зберегти як…"),
|
||||
("Save a Copy…", "Зберегти копію…"),
|
||||
|
||||
+40
-13
@@ -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<Vec<Bindable>> = 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<Bindable>, HashSet<&'static str>) {
|
||||
static CATALOG: OnceLock<(Vec<Bindable>, HashSet<&'static str>)> = OnceLock::new();
|
||||
CATALOG.get_or_init(build_catalog)
|
||||
}
|
||||
|
||||
fn build_catalog() -> Vec<Bindable> {
|
||||
/// The catalog and the engine commands [`listed`] leaves out.
|
||||
fn build_catalog() -> (Vec<Bindable>, 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<Bindable> {
|
||||
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<Trigger> = 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<Bindable> {
|
||||
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<Bindable> {
|
||||
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() {
|
||||
|
||||
@@ -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<String, CommandRef>) -> 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<ForeignSet, String> {
|
||||
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<Value, String> {
|
||||
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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user