From a59880394d618ff69cd9f3fefdd8b7cf56d1619d Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:23:48 -0400 Subject: [PATCH] Search: Delete Workspace is left out of Quick Apply and command search (#1791) * Search: Delete Workspace is left out of Quick Apply and command search It needs a saved workspace's name, which a search entry can't supply, so choosing it there failed. The application bar's workspace menu lists the saved workspaces to delete. * Search: leave out the interface commands that fail or reset a setting there Quick Apply and the application bar's search ran their entries without parameters. Commands that need a workspace, a command and shortcut, a menu item, a panel, a window edge, an app page, a recent file, or a swatch or color group failed when chosen there. Interface Color Theme and UI Scaling switched to the dark theme and 100%, Workspace switched to Essentials, and Reset Shortcuts cleared every custom shortcut without asking. Search leaves all of them out; each stays in the menu, panel or dialog that gives it the value. * Search: the flattener preset, Screen Mode, Zoom To and the Task Bar pin stay out of Quick Apply --- ROADMAP.md | 1 + crates/ui-egui/src/menus.rs | 140 +++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4337c28d1..541a56e5d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -358,6 +358,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-24** · Quick Apply and the application bar's search leave out the interface commands that failed or reset a setting when chosen there: those that need a workspace, a command and shortcut, a menu item, a panel, a window edge, an app page, a recent file or a swatch they can't give them, a Contextual Task Bar place, and Interface Color Theme, UI Scaling, Reset Shortcuts, the flattener preset, Screen Mode and Zoom To, which without a value switched to the dark theme, set 100%, cleared every custom shortcut or the preset, or went to Normal mode or 100% zoom. Each stays in the menu, panel or dialog that gives it the value. No readiness percentage change. - **2026-10-24** · EPUB Export Options › Split Document, a dialog before File › Export EPUB… saves (`document.epubSplit`, stored with the document): off, a single paragraph style, or based on paragraph style export tags, with Paragraph Style Options › Export Tagging's Split Document flag marking those styles. A reflowable EPUB is one content file unless Split Document is on; the split before each top-level h1 from #791 is gone. A split before a list item or preformatted line no longer leaves an unclosed list or block in a content file. IDML import and export keep the setting and the flag, and Load Styles the flag. No readiness percentage change. - **2026-10-18** · Repeated placed-PDF layer variants are prepared once per asset and hidden-name set, with recovery warnings retained. All-visible named layers keep their original source bytes, including PDFs whose cross-reference tables the reader repairs, in plain, PDF/X-4, booklet and book-merge output. Layer changes that require writing still reject an unusable cross-reference table with a file-named damage error; the first placement error is retained (#906). No readiness percentage change. - **2026-10-18** · Placed PDF optional-content defaults and per-frame overrides survive plain and PDF/X-4 vector export, including repeated placements, booklet output and book merging (#906). Source usage-application arrays are accepted; groups without a catalog configuration stay visible. Missing or unreadable default configurations and unsupported base states use all-ON defaults with a file-named export warning, retaining explicit frame overrides. Readable groups omitted from the catalog list are added with a distinct file-named warning, preserving their default ON/OFF states. Concurrent recovery warnings are combined. Malformed ON data is recoverable when a known ON base and empty OFF list prove it cannot hide artwork; missing display names are generated without changing known visibility when no override needs name matching. Contradictory states, unresolved references and ambiguous name overrides still fail export. Print-usage adjustments and IDML layer overrides remain follow-ups. No readiness percentage change. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 347e046de..073edc238 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3369,13 +3369,53 @@ pub fn quick_apply_items(session: &designcraft_engine::Session, query: &str, cjk out } +/// UI commands search leaves out: they only run with a parameter search can't supply and have no +/// dialog to ask for it, or without one they reset a setting. Each stays in the menu, panel or +/// dialog that gives it the value (the workspace menus, Keyboard Shortcuts, Preferences…). +const NOT_SEARCHED: &[&str] = &[ + // The workspace to switch to or delete. + "window.workspace", + "window.deleteWorkspace", + // A command and its shortcut, a menu item, a shortcut set; Reset Shortcuts clears every + // custom shortcut, and the Keyboard Shortcuts dialog offers it. + "window.setShortcut", + "window.resetShortcuts", + "window.importShortcuts", + "window.hideMenuItem", + // Without a value these reset the interface to 100% and the dark theme. + "window.uiScale", + "window.brightness", + // A panel, and where it goes. + "window.floatPanel", + "window.dockPanel", + "window.panel.layout", + "window.panel.move", + "window.panel.float", + "window.panel.dock", + "window.panel.activate", + "window.panel.close", + "window.panel.closeGroup", + // A window edge, an app page, a recent file, a swatch or color group. + "window.resize", + "help.app", + "app.openRecent", + "app.swatchOptionsDialog", + "app.colorGroupOptionsDialog", + // Without a value these clear the flattener preset, go to Normal screen mode or 100% zoom. + "app.flattener", + "view.screenMode", + "view.zoom", + // Where to pin the Contextual Task Bar. + "window.taskBarPin", +]; + /// The commands Quick Apply and the application bar's search find: (id, label, default shortcut). fn quick_commands() -> impl Iterator)> { designcraft_engine::command_specs() .iter() .filter(|c| !c.menu.is_empty() || c.shortcut.is_some()) .map(|c| (c.id, c.label, c.shortcut)) - .chain(UI_COMMANDS.iter().map(|c| (c.0, c.1, c.2))) + .chain(UI_COMMANDS.iter().filter(|c| !NOT_SEARCHED.contains(&c.0)).map(|c| (c.0, c.1, c.2))) } /// Whether any of `texts` contains the lowercased query (an empty query matches everything). @@ -5243,6 +5283,104 @@ mod tests { assert!(quick_apply_items(&s, "preferences", false).iter().any(|i| i.id == "app.preferences")); } + #[test] + fn searched_ui_commands_run_without_parameters_and_keep_preferences() { + // Without a value these reset a setting or need a place the test host always has. + let searched: Vec<&str> = quick_commands().map(|c| c.0).collect(); + for id in ["app.flattener", "view.screenMode", "view.zoom", "window.taskBarPin"] { + assert!(!searched.contains(&id), "{id} is searched"); + } + // A placed PDF: a graphic with layers. + let mut scratch = designcraft_engine::Session::new(); + scratch.execute("file.new", &json!({})).unwrap(); + let pdf = scratch.execute("file.exportPdf", &json!({})).unwrap()["base64"].clone(); + let contexts = ["nothing selected", "text insertion point", "text frame selected", "table cell", "placed PDF selected"]; + // A two-page document, a saved workspace and non-default preferences, a host with window + // controls, and file dialogs that are dismissed. + let setup = |context: usize| { + let services = crate::Services { + pick_open: Some(Box::new(|_| None)), + pick_save: Some(Box::new(|_| None)), + clipboard_text: Some(Box::new(|| None)), + ..Default::default() + }; + let mut app = DesignApp::new(designcraft_engine::Session::new(), services); + app.custom_titlebar = true; + app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 700.0))); + app.run("file.new", json!({"pages": 2})).unwrap(); + app.session.prefs.recent_files.push("/nowhere/recent.dcraft".into()); + app.ui.shortcuts.insert("view.rulers".into(), "Ctrl+Alt+R".into()); + app.ui.hidden_menu_items.push("view.guides".into()); + app.ui.ui_scale = 1.25; + app.ui.brightness = crate::theme::Brightness::Light; + app.ui.task_bar_at = Some([200.0, 200.0]); + run_ui(&mut app, "window.newWorkspace", &json!({"name": "Mine"})).unwrap().unwrap(); + match context { + 1 => { + app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "text": "One", "caret": true})).unwrap(); + } + 2 => { + let f = app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "text": "One"})).unwrap()["id"].clone(); + app.run("selection.set", json!({"ids": [f]})).unwrap(); + } + 3 => { + app.run("frame.create", json!({"rect": [72, 72, 400, 400], "content": "text", "caret": true})).unwrap(); + app.run("table.insert", json!({"rows": 2, "cols": 2})).unwrap(); + } + 4 => { + let f = app.run("frame.create", json!({"rect": [72, 72, 400, 400]})).unwrap()["id"].clone(); + app.run("file.place", json!({"base64": pdf, "name": "a.pdf", "frame": f})).unwrap(); + app.run("selection.set", json!({"ids": [f]})).unwrap(); + } + _ => {} + } + app.ui.dialog = None; + app + }; + let prefs = |app: &DesignApp| { + let u = &app.ui; + json!({ + "shortcuts": u.shortcuts, "hiddenMenuItems": u.hidden_menu_items, "uiScale": u.ui_scale, + "brightness": u.brightness, "workspace": u.workspace, "customWorkspaces": u.custom_workspaces, + "docking": u.docking, "dockingHidden": u.docking_hidden, "floating": u.floating, + }) + }; + let quick = quick_apply_items(&setup(0).session, "workspace", false); + assert!(quick.iter().any(|i| i.id == "window.newWorkspace")); + // Deleting needs a workspace name: the application bar's workspace menu lists them. + assert!(!quick.iter().any(|i| i.id == "window.deleteWorkspace")); + // Each command runs as Quick Apply runs an entry. One that needs a selection may fail + // without it, but runs in one of the contexts; none changes a preference it wasn't asked to. + // Opening a panel changes where the panels are, as it should. + let opens_a_panel = ["app.tabsPanel", "app.characterStylesPanel", "app.paragraphStylesPanel"]; + let arrangement = ["docking", "dockingHidden", "floating"]; + let mut failed = Vec::new(); + for (id, ..) in quick_commands().filter(|(id, ..)| UI_COMMANDS.iter().any(|c| c.0 == *id)) { + let mut errors = Vec::new(); + for (i, context) in contexts.iter().enumerate() { + let mut app = setup(i); + let before = prefs(&app); + match app.run(id, json!({})) { + Err(e) => errors.push(format!("{context}: {e}")), + Ok(_) => { + let after = prefs(&app); + let changed: Vec<&String> = + before.as_object().unwrap().iter().filter(|(k, v)| after.get(k.as_str()) != Some(*v)).map(|(k, _)| k).collect(); + let changed: Vec<&String> = + changed.into_iter().filter(|k| !(opens_a_panel.contains(&id) && arrangement.contains(&k.as_str()))).collect(); + if !changed.is_empty() { + failed.push(format!("{id} ({context}): changed {changed:?}")); + } + } + } + } + if errors.len() == contexts.len() { + failed.push(format!("{id}: {errors:?}")); + } + } + assert!(failed.is_empty(), "{failed:#?}"); + } + #[test] fn distribute_rows_menu_dispatches_an_undoable_edit() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());