W: Keyboard Shortcuts editor (window.setShortcut), macOS modifier order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-02 16:26:56 -04:00
1 parent 8bfbbe7187
commit 0f86308791
5 files changed
+210 -24

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
- Edit › Keyboard Shortcuts: every command, click and press the new keys, conflicts shown, per-command and global reset; `window.setShortcut`.
- Primary Text Frame (New Document) and Smart Text Reflow: pages are added while the primary story oversets and empty ones at the end removed, in the same undo step (Preferences › Type).
- Glyphs panel: every character of any font (rendered by our renderer), search by character or U+code, click to insert, recently used.
- UI scaling (Preferences › Interface, 50–200%; `window.uiScale`); the window title steps aside when the bar is crowded.
+76
View File
@@ -102,6 +102,80 @@ fn text_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, w: f32) {
}
}
/// Edit › Keyboard Shortcuts: every command and its shortcut; click a shortcut, then press the
/// new keys (Esc cancels, Backspace clears).
fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
let mut q = d.s("query");
ui.add(egui::TextEdit::singleline(&mut q).hint_text("Search commands").desired_width(f32::INFINITY));
d.fields.insert("query".into(), json!(q));
let recording = d.s("recording");
// Capture the next key press for the command being recorded.
if !recording.is_empty() {
let pressed = ui.input(|i| {
i.events.iter().find_map(|e| match e {
egui::Event::Key { key, pressed: true, modifiers, .. } => Some((*key, *modifiers)),
_ => None,
})
});
if let Some((key, m)) = pressed {
let sc = match key {
egui::Key::Escape => None,
egui::Key::Backspace if !m.any() => Some(String::new()),
_ => Some(crate::menus::shortcut_string(m, key)),
};
if let Some(sc) = sc {
let msg = match app.run("window.setShortcut", json!({"id": recording, "shortcut": sc})) {
Ok(v) => match v["conflicts"].as_array().filter(|c| !c.is_empty()) {
Some(c) => format!("Also used by: {}", c.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", ")),
None => String::new(),
},
Err(e) => e,
};
d.fields.insert("message".into(), json!(msg));
}
d.fields.insert("recording".into(), json!(""));
}
}
let ql = q.to_lowercase();
let mut rows: Vec<(String, String, String)> = designcraft_engine::command_specs()
.iter()
.filter(|c| !c.menu.is_empty() || c.shortcut.is_some())
.map(|c| (c.id.to_string(), c.label.to_string(), c.menu.join(" › ")))
.chain(crate::menus::UI_COMMANDS.iter().map(|c| (c.0.to_string(), c.1.to_string(), String::new())))
.filter(|(id, l, _)| ql.is_empty() || l.to_lowercase().contains(&ql) || id.to_lowercase().contains(&ql))
.collect();
rows.sort_by(|a, b| a.1.cmp(&b.1));
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
egui::Grid::new("shortcuts").num_columns(3).striped(true).spacing([12.0, 4.0]).show(ui, |ui| {
for (id, label, menu) in &rows {
ui.label(label.trim_end_matches('…'))
.on_hover_text(format!("{id}{}", if menu.is_empty() { String::new() } else { format!(" ({menu})") }));
let cur = crate::menus::shortcut_of(app, id).map(|s| crate::menus::shortcut_text(&s)).unwrap_or_else(|| "—".into());
let text = if recording == *id { "Press keys…".to_string() } else { cur };
if ui.add(egui::Button::new(text).min_size(egui::vec2(110.0, 0.0))).clicked() {
d.fields.insert("recording".into(), json!(id));
d.fields.insert("message".into(), json!(""));
}
if app.ui.shortcuts.contains_key(id) {
if ui.small_button("Default").clicked() {
let _ = app.run("window.setShortcut", json!({"id": id, "shortcut": null}));
}
} else {
ui.label("");
}
ui.end_row();
}
});
});
let msg = d.s("message");
if !msg.is_empty() {
ui.label(egui::RichText::new(msg).color(egui::Color32::from_rgb(240, 180, 60)));
}
if ui.button("Reset All to Defaults").clicked() {
let _ = app.run("window.resetShortcuts", json!({}));
}
}
/// File › Document Setup, filled from the document.
pub fn open_document_setup(app: &mut DesignApp) {
let Ok(cur) = app.session.execute("layout.documentSetup", &json!({})) else { return };
@@ -406,6 +480,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"findFont" => "Find/Replace Font",
"polygonSettings" => "Polygon Settings",
"preferences" => "Preferences",
"keyboardShortcuts" => "Keyboard Shortcuts",
"colorPicker" => "Color Picker",
id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) {
Some(c) => c.label.trim_end_matches('…'),
@@ -543,6 +618,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"findFont" => find_font(app, ui, &mut d),
"colorPicker" => color_picker(ui, &mut d),
"preferences" => preferences(ui, &mut d),
"keyboardShortcuts" => keyboard_shortcuts(app, ui, &mut d),
"polygonSettings" => {
egui::Grid::new("poly").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
ui.label("Number of Sides:");
+3
View File
@@ -87,6 +87,8 @@ pub struct UiState {
pub display_quality: designcraft_render::DisplayQuality,
/// Preferences › Interface › UI scaling (1 = 100%).
pub ui_scale: f32,
/// Edit › Keyboard Shortcuts: command id → shortcut ("" = none), over the defaults.
pub shortcuts: std::collections::BTreeMap<String, String>,
pub control_bar: bool,
pub tools_double_column: bool,
/// Expanded right-dock panel group tab.
@@ -136,6 +138,7 @@ impl Default for UiState {
hidden_characters: false,
display_quality: designcraft_render::DisplayQuality::High,
ui_scale: 1.0,
shortcuts: Default::default(),
control_bar: false,
tools_double_column: false,
dock_tab: "properties".into(),
+127 -21
View File
@@ -21,6 +21,9 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("app.palette", "Quick Apply…", Some("Cmd+Return"), "{} — search styles and commands"),
("app.preferences", "Preferences…", Some("Cmd+K"), "{}"),
("window.uiScale", "UI Scaling", None, "{scale: 0.5–3 (1 = 100%)} — the size of the whole interface"),
("app.keyboardShortcuts", "Keyboard Shortcuts…", None, "{} — view and change the shortcut of any command"),
("window.setShortcut", "Set Shortcut", None, "{id, shortcut: \"Cmd+Alt+J\" | \"\" (none) | null (default)} → {shortcut, conflicts: [ids]}"),
("window.resetShortcuts", "Reset Shortcuts", None, "{} — every command back to its default shortcut"),
("app.findChange", "Find/Change…", Some("Cmd+F"), "{}"),
("app.insertTableDialog", "Create Table…", None, "{} — Insert Table dialog (body/header/footer rows, columns)"),
("app.footnoteOptionsDialog", "Document Footnote Options…", None, "{} — numbering, formatting and layout of footnotes"),
@@ -135,6 +138,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
"<",
"-",
"ui:app.palette",
"ui:app.keyboardShortcuts",
"ui:app.preferences",
],
),
@@ -709,6 +713,39 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
}
Ok(Value::Null)
}
"app.keyboardShortcuts" => {
app.ui.dialog = Some(crate::dialogs::Dialog::new("keyboardShortcuts", json!({"query": "", "recording": ""})));
Ok(Value::Null)
}
"window.setShortcut" => {
let Some(cmd) = p.get("id").and_then(Value::as_str) else { return Some(Err("missing id".into())) };
if designcraft_engine::find_command(cmd).is_none() && ui_label(cmd).is_none() {
return Some(Err(format!("unknown command `{cmd}`")));
}
match p.get("shortcut") {
None | Some(Value::Null) => {
app.ui.shortcuts.remove(cmd);
}
Some(Value::String(sc)) if sc.is_empty() => {
app.ui.shortcuts.insert(cmd.to_string(), String::new());
}
Some(Value::String(sc)) => {
if parse_shortcut(sc).is_none() {
return Some(Err(format!("can't read shortcut `{sc}`")));
}
app.ui.shortcuts.insert(cmd.to_string(), sc.clone());
}
Some(_) => return Some(Err("shortcut: a string or null".into())),
}
let sc = shortcut_of(app, cmd);
let conflicts: Vec<String> =
effective_shortcuts(app).into_iter().filter(|(i, s)| i != cmd && Some(s) == sc.as_ref()).map(|(i, _)| i).collect();
Ok(json!({"shortcut": sc, "conflicts": conflicts}))
}
"window.resetShortcuts" => {
app.ui.shortcuts.clear();
Ok(Value::Null)
}
"window.uiScale" => {
let v = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0) as f32;
app.ui.ui_scale = v.clamp(0.5, 3.0);
@@ -868,18 +905,65 @@ pub fn enabled(app: &DesignApp, id: &str) -> bool {
}
}
fn shortcut_text(sc: &str) -> String {
let mac = cfg!(target_os = "macos");
sc.split('+')
.map(|k| match k {
"Cmd" => if mac { "⌘" } else { "Ctrl+" }.to_string(),
"Shift" => if mac { "⇧" } else { "Shift+" }.to_string(),
"Alt" => if mac { "⌥" } else { "Alt+" }.to_string(),
k => k.to_string(),
})
/// The default shortcut of a command.
pub fn default_shortcut(id: &str) -> Option<&'static str> {
designcraft_engine::find_command(id).and_then(|c| c.shortcut).or_else(|| ui_label(id).and_then(|(_, s)| s))
}
/// The shortcut in effect for a command (the user's, else the default).
pub fn shortcut_of(app: &DesignApp, id: &str) -> Option<String> {
match app.ui.shortcuts.get(id) {
Some(s) if s.is_empty() => None,
Some(s) => Some(s.clone()),
None => default_shortcut(id).map(str::to_string),
}
}
/// Every command with a shortcut in effect: (id, shortcut).
pub fn effective_shortcuts(app: &DesignApp) -> Vec<(String, String)> {
designcraft_engine::command_specs()
.iter()
.map(|c| c.id)
.chain(UI_COMMANDS.iter().map(|c| c.0))
.filter_map(|id| shortcut_of(app, id).map(|s| (id.to_string(), s)))
.collect()
}
/// A shortcut as written in this app ("Cmd+Alt+Shift+K") from a key press.
pub fn shortcut_string(m: egui::Modifiers, key: egui::Key) -> String {
let mut s = String::new();
if m.command {
s += "Cmd+";
}
if m.alt {
s += "Alt+";
}
if m.shift {
s += "Shift+";
}
s += key.name();
s
}
/// A shortcut for display: ⌃⌥⇧⌘ order on macOS, Ctrl+Alt+Shift+ elsewhere.
pub fn shortcut_text(sc: &str) -> String {
let mac = cfg!(target_os = "macos");
// The key is the last part ("Cmd+-" ends in an empty part before "-").
let (mods, key) = match sc.rsplit_once('+') {
Some((m, "")) => (m.trim_end_matches('+'), "+"),
Some((m, k)) => (m, k),
None => ("", sc),
};
let has = |m: &str| mods.split('+').any(|x| x == m);
let mut out = String::new();
for (m, sym, word) in [("Ctrl", "⌃", "Ctrl+"), ("Alt", "⌥", "Alt+"), ("Shift", "⇧", "Shift+"), ("Cmd", "⌘", "Ctrl+")] {
if has(m) && !(m == "Cmd" && !mac && has("Ctrl")) {
out += if mac { sym } else { word };
}
}
out + key
}
/// A menu entry, shared by the in-window menu bar and the native macOS menu.
#[derive(Clone, Debug)]
pub enum Item {
@@ -1036,15 +1120,15 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item]) {
Item::Sub(name, children) => {
ui.menu_button(name, |ui| menu_items(app, ui, children));
}
Item::Cmd { label, id, params, shortcut } => {
Item::Cmd { label, id, params, shortcut: _ } => {
let text = match checked(app, id, params) {
Some(true) => format!("✓ {label}"),
Some(false) => format!(" {label}"),
None => label.clone(),
};
let mut b = egui::Button::new(text);
if let Some(sc) = shortcut {
b = b.shortcut_text(shortcut_text(sc));
if let Some(sc) = shortcut_of(app, id) {
b = b.shortcut_text(shortcut_text(&sc));
}
if ui.add_enabled(menu_enabled(app, id), b).clicked() {
activate(app, id, params);
@@ -1093,12 +1177,10 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
for e in events {
let egui::Event::Key { key, pressed: true, modifiers, repeat: false, .. } = e else { continue };
// Command shortcuts.
let mut fired = None;
let all = designcraft_engine::command_specs()
.iter()
.filter_map(|c| c.shortcut.map(|s| (c.id, s)))
.chain(UI_COMMANDS.iter().filter_map(|c| c.2.map(|s| (c.0, s))));
for (id, sc) in all {
let mut fired: Option<String> = None;
let all = effective_shortcuts(app);
for (id, sc) in &all {
let id = id.as_str();
if let Some((m, k)) = parse_shortcut(sc)
&& k == key
&& m.command == modifiers.command
@@ -1114,15 +1196,15 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
if id == "edit.clear" || (typing && matches!(id, "edit.copy" | "edit.cut" | "edit.paste" | "edit.pasteInPlace")) {
continue; // Handled by the active tool / text clipboard events.
}
fired = Some(id);
fired = Some(id.to_string());
break;
}
}
if let Some(id) = fired {
if app.native_shortcuts.contains(id) {
if app.native_shortcuts.contains(&id) && !app.ui.shortcuts.contains_key(&id) {
continue; // The native menu handles it.
}
let _ = app.run(id, json!({}));
let _ = app.run(&id, json!({}));
continue;
}
// Tool shortcuts (single keys, Shift+key).
@@ -1239,6 +1321,30 @@ mod tests {
}
}
#[test]
fn custom_shortcuts_override_defaults() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
assert_eq!(shortcut_of(&app, "app.preferences").as_deref(), Some("Cmd+K"));
let r = run_ui(&mut app, "window.setShortcut", &json!({"id": "app.preferences", "shortcut": "Cmd+Alt+Shift+9"})).unwrap().unwrap();
assert_eq!(r["shortcut"], "Cmd+Alt+Shift+9");
assert!(effective_shortcuts(&app).contains(&("app.preferences".into(), "Cmd+Alt+Shift+9".into())));
// Taking another command's keys reports the conflict.
let r = run_ui(&mut app, "window.setShortcut", &json!({"id": "app.palette", "shortcut": "Cmd+Alt+Shift+9"})).unwrap().unwrap();
assert_eq!(r["conflicts"], json!(["app.preferences"]));
run_ui(&mut app, "window.setShortcut", &json!({"id": "app.palette", "shortcut": ""})).unwrap().unwrap();
assert_eq!(shortcut_of(&app, "app.palette"), None);
assert!(run_ui(&mut app, "window.setShortcut", &json!({"id": "app.palette", "shortcut": "Cmd+Nope"})).unwrap().is_err());
run_ui(&mut app, "window.resetShortcuts", &json!({})).unwrap().unwrap();
assert_eq!(shortcut_of(&app, "app.palette").as_deref(), Some("Cmd+Return"));
assert_eq!(shortcut_string(egui::Modifiers { command: true, shift: true, ..Default::default() }, egui::Key::K), "Cmd+Shift+K");
if cfg!(target_os = "macos") {
assert_eq!(shortcut_text("Cmd+Alt+3"), "⌥⌘3");
assert_eq!(shortcut_text("Cmd+Shift+."), "⇧⌘.");
} else {
assert_eq!(shortcut_text("Cmd+Alt+3"), "Alt+Ctrl+3");
}
}
#[test]
fn panels_float_and_dock() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+3 -3
View File
@@ -10,12 +10,12 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 99 | 3 | 0 | 99% |
| P1 | 97 | 51 | 23 | 23 | 64% |
| P1 | 97 | 52 | 23 | 22 | 65% |
| P2 | 76 | 8 | 7 | 61 | 15% |
| Area | Parity |
|---|---|
| Application shell & workspace | 74% |
| Application shell & workspace | 80% |
| Documents, pages, spreads | 74% |
| Layers | 95% |
| Frames, shapes & paths | 85% |
@@ -51,7 +51,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
| Application shell & workspace | Contextual task bar | P2 | D |
| Application shell & workspace | Themes | P0 | D |
| Application shell & workspace | UI scaling | P1 | D |
| Application shell & workspace | Keyboard shortcut editor | P1 | M |
| Application shell & workspace | Keyboard shortcut editor | P1 | D |
| Application shell & workspace | Menu customization | P2 | M |
| Application shell & workspace | Show Full Menus | P2 | M |
| Application shell & workspace | Recovery / autosave | P0 | D |