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
This commit is contained in:
aiongg
2026-10-24 20:23:48 -04:00
committed by GitHub
parent b595f1b7d3
commit a59880394d
2 changed files with 140 additions and 1 deletions
+1
View File
@@ -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.
+139 -1
View File
@@ -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<Item = (&'static str, &'static str, Option<&'static str>)> {
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());