keymap: DesignCraft shortcuts on craft-keymap

The shortcut model, dispatch and storage move onto the shared craft-keymap
crate. The catalogue is built from UI_COMMANDS, the engine command specs,
the panels (window.panel {panel}) and the tools (tool.select {tool}, one
group per Tools-panel group), with menu paths from the menu tree.

- Dispatch goes through craft_keymap::egui::Dispatcher; presentation keys,
  Esc-to-selection, Clear and the native-menu skip stay app-side, and the
  clipboard events stay with the canvas.
- Panels take InDesign's default keys where DesignCraft leaves them free.
- Tool keys are editable; tools sharing a key take turns.
- ui.json keeps its shortcuts as craft-keymap Overrides; the map form older
  versions saved loads unchanged.
- window.setShortcut accepts params (panels), tool ids and steal; the
  Keyboard Shortcuts dialog lists panels and tools and moves recorded keys.
This commit is contained in:
aiongg
2026-10-19 04:14:59 +00:00
parent 68e2044b91
commit 8383d828ad
13 changed files with 499 additions and 186 deletions
Generated
+11
View File
@@ -824,6 +824,16 @@ dependencies = [
"rayon",
]
[[package]]
name = "craft-keymap"
version = "0.1.0"
source = "git+https://github.com/aiongg/craft-libs?rev=d6c7b45350f7a4352b845c9fdd2e63913d1af27d#d6c7b45350f7a4352b845c9fdd2e63913d1af27d"
dependencies = [
"egui",
"serde",
"serde_json",
]
[[package]]
name = "craft-pdf"
version = "0.1.0"
@@ -1212,6 +1222,7 @@ dependencies = [
name = "designcraft-ui-egui"
version = "0.8.0"
dependencies = [
"craft-keymap",
"craft-ui",
"designcraft-color",
"designcraft-compose",
+2
View File
@@ -13,6 +13,8 @@ repository = "https://github.com/storytold/designcraft"
[workspace.dependencies]
# 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.
craft-keymap = { git = "https://github.com/aiongg/craft-libs", rev = "d6c7b45350f7a4352b845c9fdd2e63913d1af27d", version = "=0.1.0", features = ["egui"] }
# internal
designcraft-geom = { path = "crates/geom" }
designcraft-color = { path = "crates/color" }
+1
View File
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-19** · Keyboard shortcuts run on the shared craft-keymap crate. Panels take shortcuts, with InDesign's defaults where the key is free (Swatches F5, Color F6, Layers F7, Info F8, Stroke F10, Paragraph Styles F11, Character Styles Shift+F11, Pages F12, Align Shift+F7); tool keys are editable in Edit › Keyboard Shortcuts and `window.setShortcut`, and tools sharing a key take turns (B). Saved shortcuts load unchanged. No readiness percentage change.
- **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
- **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change.
+1
View File
@@ -152,6 +152,7 @@ fn load_prefs(app: &mut DesignApp) {
&& let Ok(ui) = designcraft_ui_egui::UiState::from_preferences_json(&bytes)
{
app.ui = ui;
app.sync_keymap();
}
// Engine preferences (Preferences dialog, favourites…) live beside the UI state.
if let Some(p) = prefs_path().map(|p| p.with_file_name("prefs.json"))
+1
View File
@@ -11,6 +11,7 @@ workspace = true
[dependencies]
craft-ui = { workspace = true }
craft-keymap = { workspace = true }
designcraft-geom = { workspace = true }
designcraft-color = { workspace = true }
designcraft-doc = { workspace = true }
+36 -15
View File
@@ -264,16 +264,27 @@ fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
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)),
egui::Key::Backspace if !m.any() => Some(Some(String::new())),
_ => Some(crate::keys::chord_text(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,
let msg = match (sc, craft_keymap::CommandRef::parse_key(&recording)) {
(Some(sc), Some(cmd)) => {
// The recorded keys move to this command; the commands that held them lose them.
match app.run("window.setShortcut", json!({"id": cmd.id, "params": cmd.params, "shortcut": sc, "steal": true})) {
Ok(v) => match v["conflicts"].as_array().filter(|c| !c.is_empty()) {
Some(c) => {
let labels: Vec<String> =
c.iter().filter_map(|x| x.as_str()).map(|k| crate::keys::label(&app.keymap, k)).collect();
format!("Taken from: {}", labels.join(", "))
}
None => String::new(),
},
Err(e) => e,
}
}
(None, _) => "That key can't be used in a shortcut".to_string(),
(_, None) => String::new(),
};
d.fields.insert("message".into(), json!(msg));
}
@@ -281,11 +292,20 @@ fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
}
}
let ql = q.to_lowercase();
let mut rows: Vec<(String, String, String)> = designcraft_engine::command_specs()
// (command key, label, menu path): every command with a menu row or a default shortcut, the
// UI commands, the panels and the tools.
let mut rows: Vec<(String, String, String)> = app
.keymap
.catalogue()
.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(|b| {
let ui_command = crate::menus::UI_COMMANDS.iter().any(|c| c.0 == b.command.id);
ui_command || !b.paths.is_empty() || !b.defaults.is_empty() || b.command.id == crate::keys::PANEL_COMMAND
})
.map(|b| {
let menu = b.paths.first().map(|p| p.get(..p.len().saturating_sub(1)).unwrap_or_default().join(" › ")).unwrap_or_default();
(b.command.key(), b.label.clone(), menu)
})
.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));
@@ -294,15 +314,16 @@ fn keyboard_shortcuts(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
for (id, label, menu) in &rows {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, 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 cmd = craft_keymap::CommandRef::parse_key(id);
let cur = cmd.as_ref().and_then(|c| crate::keys::shortcut_display(&app.keymap, c)).unwrap_or_else(|| "—".into());
let text = if recording == *id { crate::i18n::tr(&app.ui.language, "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 let Some(cmd) = cmd.filter(|_| app.keymap.overrides().get(id).is_some()) {
if ui.small_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Default"))).clicked() {
let _ = app.run("window.setShortcut", json!({"id": id, "shortcut": null}));
let _ = app.run("window.setShortcut", json!({"id": cmd.id, "params": cmd.params, "shortcut": null}));
}
} else {
ui.label("");
+214
View File
@@ -0,0 +1,214 @@
//! Keyboard shortcuts on `craft-keymap`: the catalogue (UI commands, engine commands, panels and
//! tools with their default keys and menu paths), shortcut text, and the edits that
//! `window.setShortcut`, the Keyboard Shortcuts dialog and the shortcut-set importer make.
use std::collections::HashMap;
use std::sync::OnceLock;
use craft_keymap::{Bindable, Binding, Chord, CommandRef, Key, Keymap, Kind, Mods, Overrides, Platform, Policy, Scope, Scopes};
use serde_json::{Value, json};
use crate::DesignApp;
use crate::menus::{self, Item};
/// The context bindings such as `Text: Cmd+B` name; active while text is being typed on the canvas.
pub const TEXT_CONTEXT: &str = "Text";
/// The command that opens a panel; a binding on it carries the panel id.
pub const PANEL_COMMAND: &str = "window.panel";
/// The command that selects a tool; a binding on it carries the tool id.
pub const TOOL_COMMAND: &str = "tool.select";
/// InDesign's default keys for the panels DesignCraft has, where DesignCraft's own defaults leave
/// the key free.
const PANEL_KEYS: &[(&str, &str)] = &[
("swatches", "F5"),
("color", "F6"),
("layers", "F7"),
("info", "F8"),
("stroke", "F10"),
("paragraphStyles", "F11"),
("characterStyles", "Shift+F11"),
("pages", "F12"),
("align", "Shift+F7"),
];
pub fn panel(id: &str) -> CommandRef {
CommandRef::with_params(PANEL_COMMAND, json!({ "panel": id }))
}
pub fn tool(id: &str) -> CommandRef {
CommandRef::with_params(TOOL_COMMAND, json!({ "tool": id }))
}
/// The command a menu row or a `window.setShortcut` call names.
pub fn command_ref(id: &str, params: &Value) -> CommandRef {
match params {
Value::Null => CommandRef::new(id),
Value::Object(m) if m.is_empty() => CommandRef::new(id),
p => CommandRef::with_params(id, p.clone()),
}
}
/// The keymap with the user's overrides.
pub fn new_keymap(overrides: Overrides) -> Keymap {
Keymap::new(catalogue().to_vec(), Scopes::new().with_context(TEXT_CONTEXT), Platform::current()).with_overrides(overrides)
}
/// 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 engine
/// commands, panels and tools.
pub fn catalogue() -> &'static [Bindable] {
static CATALOGUE: OnceLock<Vec<Bindable>> = OnceLock::new();
CATALOGUE.get_or_init(build_catalogue)
}
fn build_catalogue() -> Vec<Bindable> {
let paths = menu_paths();
let app = Scope::app();
let mut out = Vec::new();
let mut add = |command: CommandRef, label: &str, default: Option<&str>, fallback_path: Option<Vec<String>>, kind: Kind| {
let mut b = Bindable::new(command, label, app.clone()).with_kind(kind);
match paths.get(&b.command.key()) {
Some(found) => b.paths.extend(found.iter().cloned()),
None => b.paths.extend(fallback_path),
}
if let Some(binding) = default.and_then(|d| Binding::parse(d).ok()) {
b = b.with_default(binding);
}
out.push(b);
};
for (id, label, _, _) in menus::UI_COMMANDS {
add(CommandRef::new(*id), label, menus::default_shortcut(id), None, Kind::Command);
}
for c in designcraft_engine::command_specs() {
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, c.shortcut, path, Kind::Command);
}
let mut seen = Vec::new();
for (id, label, _) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) {
if seen.contains(id) {
continue;
}
seen.push(id);
let default = PANEL_KEYS.iter().find(|(p, _)| p == id).map(|(_, k)| *k);
add(panel(id), label, default, Some(vec!["Window".into(), (*label).into()]), Kind::Command);
}
for group in designcraft_tools::TOOL_GROUPS {
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, Some(vec!["Tools".into(), t.label.into()]), kind);
}
}
out
}
/// Menu paths (menu, submenus, label) of every menu row, by command key.
fn menu_paths() -> HashMap<String, Vec<Vec<String>>> {
fn walk(items: &[Item], path: &mut Vec<String>, out: &mut HashMap<String, Vec<Vec<String>>>) {
for it in items {
match it {
Item::Sub(name, children) => {
path.push(name.clone());
walk(children, path, out);
path.pop();
}
Item::Cmd { label, id, params, .. } => {
let mut full = path.clone();
full.push(label.clone());
out.entry(command_ref(id, params).key()).or_default().push(full);
}
Item::Sep => {}
}
}
}
let mut out = HashMap::new();
for (menu, entries) in menus::menu_tree() {
walk(&entries, &mut vec![menu.to_string()], &mut out);
}
out
}
/// The binding a command shows: the first in its home scope, else its first.
fn shown_binding(keymap: &Keymap, cmd: &CommandRef) -> Option<Binding> {
let bindings = keymap.bindings(cmd);
bindings.iter().find(|b| b.scope.is_none()).or_else(|| bindings.first()).cloned()
}
/// The shortcut in effect for a command, as stored and sent ("Cmd+Alt+K", "Text: F7").
pub fn shortcut(keymap: &Keymap, cmd: &CommandRef) -> Option<String> {
let b = shown_binding(keymap, cmd)?;
let home = keymap.bindable(cmd).map_or_else(Scope::app, |c| c.scope.clone());
Some(b.text(&home))
}
/// The shortcut in effect for a command, for menus and lists (⌥⌘K on macOS, Ctrl+Alt+K elsewhere).
pub fn shortcut_display(keymap: &Keymap, cmd: &CommandRef) -> Option<String> {
shown_binding(keymap, cmd).map(|b| b.trigger.display(keymap.platform()))
}
/// A key press as shortcut text ("Cmd+Shift+K"); `None` for a key the keymap has no name for.
pub fn chord_text(m: egui::Modifiers, key: egui::Key) -> Option<String> {
let key = Key::from_egui(key)?;
Some(Chord::new(Mods::from_egui(&m, Platform::current()), key).to_string())
}
/// Copies the keymap's overrides to the saved UI state.
pub fn store(app: &mut DesignApp) {
if app.ui.shortcuts != *app.keymap.overrides() {
app.ui.shortcuts = app.keymap.overrides().clone();
}
}
/// Gives a command keys (replacing its others), no keys (`""`), or its defaults back (`None`).
/// Other commands on the same keys keep them unless `policy` is [`Policy::Steal`]. Returns the
/// commands it took the keys from.
pub fn set(app: &mut DesignApp, cmd: &CommandRef, shortcut: Option<&str>, policy: Policy) -> Result<Vec<CommandRef>, String> {
app.sync_keymap();
if app.keymap.bindable(cmd).is_none() {
return Err(format!("unknown command `{cmd}`"));
}
let mut taken = Vec::new();
match shortcut.map(str::trim) {
None => {
app.keymap.reset(cmd);
}
Some("") => {
if !app.keymap.clear(cmd) {
return Err("too many changed shortcuts".into());
}
}
Some(sc) => {
let binding = Binding::parse(sc).map_err(|e| format!("can't read shortcut `{sc}`: {e}"))?;
let done = app.keymap.set_bindings(cmd, vec![binding], policy).map_err(|e| format!("can't assign `{sc}`: {e}"))?;
taken = done.taken_from.into_iter().map(|h| h.command).collect();
}
}
store(app);
Ok(taken)
}
/// Other commands on the keys `cmd` holds, by command key.
pub fn conflicts(keymap: &Keymap, cmd: &CommandRef) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for c in keymap.conflicts() {
let other = if c.a.0 == *cmd {
&c.b.0
} else if c.b.0 == *cmd {
&c.a.0
} else {
continue;
};
let key = other.key();
if !out.contains(&key) {
out.push(key);
}
}
out
}
/// A command's label, by its key.
pub fn label(keymap: &Keymap, key: &str) -> String {
CommandRef::parse_key(key).and_then(|c| keymap.bindable(&c).map(|b| b.label.clone())).unwrap_or_else(|| key.to_string())
}
+17 -2
View File
@@ -17,6 +17,7 @@ pub mod dock;
pub mod effects_dialog;
pub mod i18n;
pub mod icons;
pub mod keys;
pub mod menus;
mod modal_drag;
mod new_document;
@@ -250,8 +251,9 @@ pub struct UiState {
pub rich_black: bool,
/// View › Overprint Preview.
pub overprint_preview: bool,
/// Edit › Keyboard Shortcuts: command id → shortcut ("" = none), over the defaults.
pub shortcuts: std::collections::BTreeMap<String, String>,
/// Edit › Keyboard Shortcuts: the user's changes to the default shortcuts. Older files store a
/// map of command id → shortcut ("" = none), which loads unchanged.
pub shortcuts: craft_keymap::Overrides,
pub control_bar: bool,
pub tools_double_column: bool,
/// Expanded right-dock panel group tab.
@@ -605,6 +607,9 @@ pub struct DesignApp {
pub native_menu: bool,
/// Shortcuts the native menu handles (skip them in the egui shortcut handler).
pub native_shortcuts: std::collections::HashSet<String>,
/// Every command's shortcuts, with the user's changes (`ui.shortcuts`) applied.
pub keymap: craft_keymap::Keymap,
pub(crate) key_dispatcher: craft_keymap::egui::Dispatcher,
last_time: f64,
/// When recovery data was last written (seconds, egui time).
pub last_recovery: f64,
@@ -691,6 +696,8 @@ impl DesignApp {
pending_window_commands: vec![],
native_menu: false,
native_shortcuts: Default::default(),
keymap: keys::new_keymap(craft_keymap::Overrides::new()),
key_dispatcher: craft_keymap::egui::Dispatcher::new(),
last_time: 0.0,
last_recovery: 0.0,
egui_ctx: None,
@@ -1118,6 +1125,13 @@ impl DesignApp {
}
}
/// Applies `ui.shortcuts` to the keymap when they differ, e.g. after preferences were loaded.
pub fn sync_keymap(&mut self) {
if *self.keymap.overrides() != self.ui.shortcuts {
self.keymap.set_overrides(self.ui.shortcuts.clone());
}
}
pub fn select_tool(&mut self, id: &str) {
let _ = self.run("tool.select", json!({"tool": id}));
self.ui.flyout = None;
@@ -1128,6 +1142,7 @@ impl DesignApp {
if self.egui_ctx.is_none() {
self.egui_ctx = Some(ctx.clone());
}
self.sync_keymap();
self.window_maximized = ctx.input(|i| i.viewport().maximized.unwrap_or(false));
if !self.color_applied {
self.color_applied = true;
+198 -158
View File
@@ -83,7 +83,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("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.richBlack", "Appearance of Black", None, "{on: bool} — show 100% black as rich black on screen (off: accurately)"),
("window.setShortcut", "Set Shortcut", None, "{id, shortcut: \"Cmd+Alt+J\" | \"\" (none) | null (default)} → {shortcut, conflicts: [ids]}"),
(
"window.setShortcut",
"Set Shortcut",
None,
"{id, params?, shortcut: \"Cmd+Alt+J\" | \"\" (none) | null (default), steal?} — id is a command; a panel is id \"window.panel\" with params {panel: \"layers\"}, a tool is id \"tool.select\" with params {tool: \"pen\"} (or its tool id, \"pen\"); steal: true takes the keys from the commands holding them → {shortcut, conflicts: [command keys: ids, or `window.panel {\"panel\":\"layers\"}`]}",
),
("window.resetShortcuts", "Reset Shortcuts", None, "{} — every command back to its default shortcut"),
(
"window.importShortcuts",
@@ -1683,32 +1688,34 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
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}`")));
let Some(id) = p.get("id").and_then(Value::as_str) else { return Some(Err("missing id".into())) };
let mut cmd = crate::keys::command_ref(id, p.get("params").unwrap_or(&Value::Null));
// A bare tool id ("pen") names its tool.
if app.keymap.bindable(&cmd).is_none() && designcraft_tools::tool_info(id).is_some() {
cmd = crate::keys::tool(id);
}
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());
}
let shortcut = match p.get("shortcut") {
None | Some(Value::Null) => None,
Some(Value::String(sc)) => Some(sc.as_str()),
Some(_) => return Some(Err("shortcut: a string or null".into())),
};
let steal = p.get("steal").and_then(Value::as_bool).unwrap_or(false);
let policy = if steal { craft_keymap::Policy::Steal } else { craft_keymap::Policy::Keep };
let taken = match crate::keys::set(app, &cmd, shortcut, policy) {
Ok(t) => t,
Err(e) => return Some(Err(e)),
};
let mut conflicts: Vec<String> = taken.iter().map(|c| c.key()).collect();
for key in crate::keys::conflicts(&app.keymap, &cmd) {
if !conflicts.contains(&key) {
conflicts.push(key);
}
}
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}))
Ok(json!({"shortcut": crate::keys::shortcut(&app.keymap, &cmd), "conflicts": conflicts}))
}
"window.resetShortcuts" => {
app.ui.shortcuts.clear();
app.keymap.reset_all();
crate::keys::store(app);
Ok(Value::Null)
}
"window.importShortcuts" => {
@@ -2139,59 +2146,41 @@ 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).
/// The shortcut in effect for a command (the user's, else the default), as stored ("Cmd+Alt+K").
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),
}
crate::keys::shortcut(&app.keymap, &craft_keymap::CommandRef::new(id))
}
/// Every command with a shortcut in effect: (id, shortcut). UI commands come first: where one
/// shares keys with an engine command (⌘N, ⌘O…) it's the one with the dialog.
/// The shortcut a menu row shows: its own binding (a panel's), else its command's.
pub fn shortcut_label(app: &DesignApp, id: &str, params: &Value) -> Option<String> {
let cmd = crate::keys::command_ref(id, params);
let cmd = if app.keymap.bindable(&cmd).is_some() { cmd } else { craft_keymap::CommandRef::new(id) };
crate::keys::shortcut_display(&app.keymap, &cmd)
}
/// The shortcut of a tool, for its button's tooltip.
pub fn tool_shortcut_label(app: &DesignApp, tool: &str) -> Option<String> {
crate::keys::shortcut_display(&app.keymap, &crate::keys::tool(tool))
}
/// Every command with a shortcut in effect: (command key, shortcut), in dispatch order. UI
/// commands come first: where one shares keys with an engine command (⌘N, ⌘O…) it's the one
/// with the dialog.
pub fn effective_shortcuts(app: &DesignApp) -> Vec<(String, String)> {
UI_COMMANDS
.iter()
.map(|c| c.0)
.chain(designcraft_engine::command_specs().iter().map(|c| c.id))
.filter_map(|id| shortcut_of(app, id).map(|s| (id.to_string(), s)))
.collect()
app.keymap.catalogue().iter().filter_map(|b| crate::keys::shortcut(&app.keymap, &b.command).map(|s| (b.command.key(), 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
crate::keys::chord_text(m, key).unwrap_or_else(|| key.name().to_string())
}
/// A shortcut for display: ⌃⌥⇧⌘ order on macOS, Ctrl+Alt+Shift+ elsewhere.
/// A shortcut for display: ⌃⌥⇧⌘K on macOS, Ctrl+Alt+Shift+K 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 };
}
match craft_keymap::Binding::parse(sc) {
Ok(b) => b.trigger.display(craft_keymap::Platform::current()),
Err(_) => sc.to_string(),
}
out + key
}
/// A menu entry, shared by the in-window menu bar and the native macOS menu.
@@ -2913,8 +2902,8 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
None => label.to_owned(),
};
let mut b = egui::Button::new(crate::rtl::widget(ui, text));
if let Some(sc) = shortcut_of(app, id) {
b = b.shortcut_text(shortcut_text(&sc));
if let Some(sc) = shortcut_label(app, id, params) {
b = b.shortcut_text(sc);
}
if ui.add_enabled(menu_enabled(app, id), b).clicked() {
activate(app, id, params);
@@ -2993,49 +2982,17 @@ pub fn all_menu_items() -> Vec<(String, String)> {
out
}
/// Reads shortcut text into egui's modifiers and key (`command` for Cmd, `ctrl` for the Mac
/// Control key).
pub(crate) fn parse_shortcut(sc: &str) -> Option<(egui::Modifiers, egui::Key)> {
let mut m = egui::Modifiers::NONE;
let mut key = None;
for part in sc.split('+').filter(|s| !s.is_empty()) {
match part {
"Cmd" => m.command = true,
"Shift" => m.shift = true,
"Alt" => m.alt = true,
"Ctrl" => m.ctrl = true,
"=" => key = Some(egui::Key::Equals),
"-" => key = Some(egui::Key::Minus),
"[" => key = Some(egui::Key::OpenBracket),
"]" => key = Some(egui::Key::CloseBracket),
";" => key = Some(egui::Key::Semicolon),
"'" => key = Some(egui::Key::Quote),
"," => key = Some(egui::Key::Comma),
"." => key = Some(egui::Key::Period),
"\\" => key = Some(egui::Key::Backslash),
"/" => key = Some(egui::Key::Slash),
"Delete" => key = Some(egui::Key::Delete),
k => key = egui::Key::from_name(k),
}
}
if sc.ends_with("+-") || sc == "-" {
key = Some(egui::Key::Minus);
}
key.map(|k| (m, k))
let chord = craft_keymap::Chord::parse(sc).ok()?;
let s = chord.to_egui_shortcut();
Some((s.modifiers, s.logical_key))
}
/// Whether a key press matches a parsed shortcut `m`+`k`: `None` when it doesn't, `Some(true)` for
/// the shortcut's own key and modifiers, `Some(false)` for the Plus key standing in for `=`. On
/// layouts with a dedicated + key (German) or Shift+= (US), Cmd+= arrives as Plus.
fn key_matches(m: egui::Modifiers, k: egui::Key, key: egui::Key, modifiers: egui::Modifiers) -> Option<bool> {
let plus_for_equals = key == egui::Key::Plus;
let matches = (k == key || (plus_for_equals && k == egui::Key::Equals))
&& m.command == modifiers.command
&& (m.shift == modifiers.shift || (plus_for_equals && !m.shift))
&& m.alt == modifiers.alt;
matches.then_some(k == key && m.shift == modifiers.shift)
}
/// Global keyboard shortcuts: menu commands and single-key tool shortcuts.
/// Global keyboard shortcuts: menu commands, panels and tools, through the keymap.
pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
app.sync_keymap();
// Only a focused text field takes the keys: the canvas (or a button) having focus after a click
// must not swallow tool shortcuts until Esc clears it.
// While an IME composes at the text caret, the keyboard is its own. An open canvas context menu
@@ -3050,17 +3007,87 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
return;
}
let typing = app.session.wants_text();
if app.ui.screen_mode == ScreenMode::Presentation {
presentation_keys(app, ctx);
}
let contexts: &[&str] = if typing { &[crate::keys::TEXT_CONTEXT] } else { &[] };
let active = craft_keymap::Active { panel: None, contexts, typing };
let hits = ctx.input_mut(|i| {
let platform = app.keymap.platform();
// Presses the dispatcher must not take: the clipboard events the canvas and text tools
// handle themselves (Paste carries the text), Clear (the active tool deletes), and the
// keys the native menu's accelerators already handled. They go back afterwards.
let mut held = Vec::new();
i.events.retain(|e| {
let keep = match e {
egui::Event::Copy | egui::Event::Cut | egui::Event::Paste(_) => false,
egui::Event::Key { key, pressed: true, modifiers, .. } => {
let exact =
craft_keymap::Key::from_egui(*key).map(|k| craft_keymap::Chord::new(craft_keymap::Mods::from_egui(modifiers, platform), k));
let run = match exact.map(|c| app.keymap.lookup(&c, false, &active)) {
Some(craft_keymap::Lookup::Run(c)) => Some(c),
_ => None,
};
// The native menu's accelerator holds the command's default keys and fires
// only for them exactly: the Plus key standing in for = never reaches it.
!run.is_some_and(|c| {
c.id == "edit.clear"
|| (c.params.is_null() && app.native_shortcuts.contains(&c.id) && app.keymap.overrides().get(&c.id).is_none())
})
}
_ => true,
};
if !keep {
held.push(e.clone());
}
keep
});
let hits = app.key_dispatcher.dispatch(i, &app.keymap, &active);
i.events.extend(held);
hits
});
for hit in hits {
if !hit.cycle.is_empty() {
// Tools sharing a key: the press after the group's current tool selects the next one.
let current = app.session.tool_id();
let tools: Vec<&str> = hit.cycle.iter().filter_map(|c| c.params.get("tool").and_then(Value::as_str)).collect();
let next = tools.iter().position(|t| *t == current).and_then(|i| tools.get((i + 1) % tools.len())).or(tools.first());
if let Some(tool) = next.map(|t| t.to_string()) {
log::debug!("tool shortcut -> {tool}");
app.select_tool(&tool);
}
} else if hit.command.id == crate::keys::TOOL_COMMAND
&& let Some(tool) = hit.command.params.get("tool").and_then(Value::as_str).map(str::to_string)
{
log::debug!("tool shortcut -> {tool}");
app.select_tool(&tool);
} else {
// Like choosing the menu item: "…" commands open their dialog.
activate(app, &hit.command.id, &hit.command.params);
}
}
// Esc with no shortcut of its own goes back to the Selection tool.
let escape = ctx.input(|i| {
i.events.iter().any(
|e| matches!(e, egui::Event::Key { key: egui::Key::Escape, pressed: true, repeat: false, modifiers, .. } if !modifiers.command && !modifiers.alt),
)
});
if escape && !typing && app.session.tool_id() != "selection" && !app.session.tool_busy() && app.ui.flyout.is_none() {
app.select_tool("selection");
}
}
/// Presentation's keys: Esc leaves it, and only that (the canvas must not also see the press and
/// switch tools or deselect); the arrow keys, Page Up/Down, Home and End go through the spreads.
fn presentation_keys(app: &mut DesignApp, ctx: &egui::Context) {
let events = ctx.input(|i| i.events.clone());
for e in events {
let egui::Event::Key { key, pressed: true, modifiers, repeat: false, .. } = e else { continue };
// Presentation hides every control, so Esc always leaves it, and only that: the canvas must
// not also see the press and switch tools or deselect.
if key == egui::Key::Escape && app.ui.screen_mode == ScreenMode::Presentation {
ctx.input_mut(|i| i.consume_key(modifiers, key));
let _ = app.run("view.togglePresentation", json!({}));
continue;
}
// Presentation goes through the spreads with the arrow keys, Page Up/Down, Home and End.
if app.ui.screen_mode == ScreenMode::Presentation && !modifiers.command && !modifiers.alt {
let step = match key {
egui::Key::ArrowRight | egui::Key::ArrowDown | egui::Key::PageDown => Some(1),
@@ -3072,55 +3099,6 @@ pub fn shortcuts(app: &mut DesignApp, ctx: &egui::Context) {
if let Some(step) = step {
ctx.input_mut(|i| i.consume_key(modifiers, key));
crate::canvas::step_spread(app, step);
continue;
}
}
// Command shortcuts.
let mut fired: Option<(String, bool)> = None;
let all = effective_shortcuts(app);
for (id, sc) in &all {
let id = id.as_str();
if let Some((m, k)) = parse_shortcut(sc)
&& let Some(exact) = key_matches(m, k, key, modifiers)
&& (m.command || m.alt || !typing)
&& !(id == "edit.clear" && typing)
{
// Single-key shortcuts (W) only when not typing.
if !m.command && !m.alt && !m.shift && typing {
continue;
}
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.to_string(), exact));
break;
}
}
if let Some((id, exact)) = fired {
// The native menu's accelerator fires only for the shortcut's own key and modifiers; the
// Plus key standing in for = never reaches it.
if exact && app.native_shortcuts.contains(&id) && !app.ui.shortcuts.contains_key(&id) {
continue; // The native menu handles it.
}
// The key press belongs to the shortcut: what it opens (Quick Apply's list, a dialog's
// default button) must not see the same press as Enter or Space this frame.
ctx.input_mut(|i| i.consume_key(modifiers, key));
// Like choosing the menu item: "…" commands open their dialog.
activate(app, &id, &Value::Null);
continue;
}
// Tool shortcuts (single keys, Shift+key).
if !typing && !modifiers.command && !modifiers.alt {
let name = key.name();
let sc = if modifiers.shift { format!("Shift+{name}") } else { name.to_string() };
if let Some(tool) = designcraft_tools::tool_for_shortcut(&sc) {
log::debug!("tool shortcut {sc} -> {tool}");
if std::env::var_os("DESIGNCRAFT_DEBUG_KEYS").is_some() {
eprintln!("tool shortcut {sc} ({key:?}, {modifiers:?}) -> {tool}");
}
app.select_tool(tool);
} else if key == egui::Key::Escape && app.session.tool_id() != "selection" && !app.session.tool_busy() && app.ui.flyout.is_none() {
app.select_tool("selection");
}
}
}
@@ -3512,16 +3490,78 @@ mod tests {
// ⌘N is the New Document dialog, not a bare file.new.
let first_cmd_n = effective_shortcuts(&app).into_iter().find(|(_, s)| s == "Cmd+N").map(|(i, _)| i);
assert_eq!(first_cmd_n.as_deref(), Some("app.newDocumentDialog"));
assert_eq!(shortcut_of(&app, "app.palette").as_deref(), Some("Cmd+Return"));
assert_eq!(shortcut_of(&app, "app.palette").as_deref(), Some("Cmd+Enter"));
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");
assert_eq!(shortcut_text("Cmd+Alt+3"), "Ctrl+Alt+3");
}
}
/// One frame of key presses through the shortcut dispatcher.
fn press_keys(app: &mut crate::DesignApp, ctx: &egui::Context, keys: &[(egui::Key, egui::Modifiers)]) {
let events = keys
.iter()
.flat_map(|&(key, modifiers)| {
[true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers })
})
.collect();
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| shortcuts(app, &ui.ctx().clone())).textures_delta.clear();
}
#[test]
fn shortcuts_saved_as_a_map_by_older_versions_load_unchanged() {
let saved = br#"{"shortcuts": {"app.preferences": "Cmd+Alt+Shift+9", "app.palette": "", "file.close": [""]}}"#;
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.ui = crate::UiState::from_preferences_json(saved).unwrap();
app.sync_keymap();
assert_eq!(shortcut_of(&app, "app.preferences").as_deref(), Some("Cmd+Alt+Shift+9"));
assert_eq!(shortcut_of(&app, "app.palette"), None, "a removed default stays removed");
assert_eq!(shortcut_of(&app, "file.close"), None);
assert_eq!(shortcut_of(&app, "app.save").as_deref(), Some("Cmd+S"));
// Saved again in the current form, it reads back the same.
let resaved = serde_json::to_vec(&app.ui).unwrap();
assert_eq!(crate::UiState::from_preferences_json(&resaved).unwrap().shortcuts, app.ui.shortcuts);
}
#[test]
fn panel_shortcuts_open_their_panels() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
let ctx = egui::Context::default();
let shown = |app: &crate::DesignApp, p: &str| app.ui.dock_tab == p || app.ui.open_panel.as_deref() == Some(p);
assert!(!shown(&app, "layers"));
press_keys(&mut app, &ctx, &[(egui::Key::F7, egui::Modifiers::NONE)]);
assert!(shown(&app, "layers"), "F7 opens Layers");
// A panel without a default key takes one.
let r = run_ui(&mut app, "window.setShortcut", &json!({"id": "window.panel", "params": {"panel": "links"}, "shortcut": "Shift+F12"}))
.unwrap()
.unwrap();
assert_eq!(r["shortcut"], "Shift+F12");
press_keys(&mut app, &ctx, &[(egui::Key::F12, egui::Modifiers::SHIFT)]);
assert!(shown(&app, "links"), "the new key opens Links");
}
#[test]
fn a_reassigned_tool_key_selects_the_tool_and_the_old_key_does_not() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
let ctx = egui::Context::default();
let none = egui::Modifiers::NONE;
run_ui(&mut app, "window.setShortcut", &json!({"id": "pen", "shortcut": "Y"})).unwrap().unwrap();
press_keys(&mut app, &ctx, &[(egui::Key::P, none)]);
assert_eq!(app.session.tool_id(), "selection", "P no longer selects the Pen");
press_keys(&mut app, &ctx, &[(egui::Key::Y, none)]);
assert_eq!(app.session.tool_id(), "pen");
// Tools sharing a key take turns.
press_keys(&mut app, &ctx, &[(egui::Key::B, none)]);
assert_eq!(app.session.tool_id(), "contentCollector");
press_keys(&mut app, &ctx, &[(egui::Key::B, none)]);
assert_eq!(app.session.tool_id(), "contentPlacer");
}
#[test]
fn split_window_panes_have_their_own_views() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+13 -6
View File
@@ -350,6 +350,13 @@ fn command_label(id: &str) -> String {
.unwrap_or_else(|| id.to_string())
}
/// Gives the command with this key the shortcut, none (`""`), or its default (`None`).
fn store(app: &mut DesignApp, key: &str, shortcut: Option<&str>) {
if let Some(cmd) = craft_keymap::CommandRef::parse_key(key) {
let _ = crate::keys::set(app, &cmd, shortcut, craft_keymap::Policy::Keep);
}
}
fn keys_of(sc: &str) -> Option<(bool, bool, bool, bool, egui::Key)> {
menus::parse_shortcut(sc).map(|(m, k)| (m.command, m.alt, m.shift, m.ctrl, k))
}
@@ -444,16 +451,16 @@ pub fn import(app: &mut DesignApp, text: &str) -> Result<Value, String> {
}
taken.push((keys, d.id.clone()));
if menus::default_shortcut(&d.id).and_then(keys_of) == Some(keys) {
app.ui.shortcuts.remove(&d.id);
store(app, &d.id, None);
} else {
app.ui.shortcuts.insert(d.id.clone(), sc.clone());
store(app, &d.id, Some(sc.as_str()));
}
}
None if d.all_none => {
if menus::default_shortcut(&d.id).is_none() {
app.ui.shortcuts.remove(&d.id);
store(app, &d.id, None);
} else {
app.ui.shortcuts.insert(d.id.clone(), String::new());
store(app, &d.id, Some(""));
}
}
None => continue,
@@ -475,7 +482,7 @@ pub fn import(app: &mut DesignApp, text: &str) -> Result<Value, String> {
if let Some(keys) = keys_of(&sc)
&& taken.iter().any(|(k, by)| *k == keys && !twin(by))
{
app.ui.shortcuts.insert(id.clone(), String::new());
store(app, &id, Some(""));
conflicts.push(json!({"id": id, "label": command_label(&id), "shortcut": sc}));
}
}
@@ -590,7 +597,7 @@ mod tests {
assert_eq!(r["imported"], 4);
// Reset Shortcuts is the way back.
menus::run_ui(&mut app, "window.resetShortcuts", &json!({})).unwrap().unwrap();
assert_eq!(menus::shortcut_of(&app, "app.palette").as_deref(), Some("Cmd+Return"));
assert_eq!(menus::shortcut_of(&app, "app.palette").as_deref(), Some("Cmd+Enter"));
}
#[test]
+3 -3
View File
@@ -84,7 +84,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let shown = g.iter().find(|tl| tl.id == current).unwrap_or(&g[0]);
let active = g.iter().any(|tl| tl.id == current);
let name = crate::i18n::tr(&app.ui.language, shown.label);
let tip = match shown.shortcut {
let tip = match crate::menus::tool_shortcut_label(app, shown.id) {
Some(s) => format!("{name} ({s})"),
None => name.to_string(),
};
@@ -157,7 +157,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
egui::FontId::proportional(12.5),
t.text,
);
if let Some(sc) = tl.shortcut {
if let Some(sc) = crate::menus::tool_shortcut_label(app, tl.id) {
ui.painter().text(
r.right_center() - vec2(6.0, 0.0),
egui::Align2::RIGHT_CENTER,
@@ -432,7 +432,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32, cols: u
}
let on = crate::menus::checked(app, id, &Value::Null).unwrap_or(false);
let label = VIEW_OPTIONS[k];
let sc = crate::menus::ui_label(id).and_then(|c| c.1).unwrap_or("");
let sc = crate::menus::shortcut_label(app, id, &Value::Null).unwrap_or_default();
let text = format!("{} {}", if on { "✓" } else { " " }, crate::i18n::tr(&app.ui.language, label));
if ui.add(egui::Button::new(crate::rtl::widget(ui, &text)).shortcut_text(sc)).clicked() {
let _ = app.run(id, json!({}));
+1 -1
View File
@@ -416,7 +416,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
- Spring-loaded Zoom: hold Cmd+Space (Ctrl+Space on Windows/Linux) for the Zoom cursor and click/scrubby zoom; Alt-click zooms out. Release returns to the previous tool without changing the document or undo history. Text-frame and text-field typing suppress the shortcut; Space remains Hand.
- Guides belong to the active layer: hidden with it (or with its Show Guides off) and locked with it.
- Pencil tool (N): freehand strokes, shown as you draw in the layer's colour, simplified to smooth paths (Alt closes); Smooth and Erase tools re-fit or remove the stretch you drag along (`path.smooth`, `path.erase`).
- Edit › Keyboard Shortcuts: every command, click and press the new keys, conflicts shown, per-command and global reset; `window.setShortcut`. Import InDesign Set… reads a set saved by InDesign's Show Set (.txt) and reports what it changed and what it could not match; `window.importShortcuts`.
- Edit › Keyboard Shortcuts: every command, panel and tool, click and press the new keys (they move from the commands that held them, which the dialog lists), per-command and global reset; `window.setShortcut` (panels as `window.panel` with `{panel}`, tools by id). Panels open with InDesign's keys where DesignCraft leaves them free (Swatches F5, Color F6, Layers F7, Info F8, Stroke F10, Paragraph Styles F11, Character Styles Shift+F11, Pages F12, Align Shift+F7); tools sharing a key take turns (B: Content Collector, Content Placer). Shortcuts run on the shared craft-keymap crate. Import InDesign Set… reads a set saved by InDesign's Show Set (.txt) and reports what it changed and what it could not match; `window.importShortcuts`.
- 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.
+1 -1
View File
@@ -43,7 +43,7 @@ basics of editing fail for too many users on Windows and Linux.
| Context menus (canvas, panels, rulers) | ✓ everywhere | 9 context-menu sites, ruler units by right-click | ~ | **not opening for several users** (#238, #187, #160) |
| IME text input on the canvas | ✓ | ✓ | ~ | not checked by hand with a system IME (#322) |
| Font menu: search, preview, favourites, type-ahead, filters (class, recently added), styles per family, sample text and size, recently used, similar fonts | ✓ | ✓ | ~ | search field focus on macOS (#335, #86); Adobe Fonts (Find More, activated) out of scope |
| Keyboard shortcuts: InDesign defaults, editor, sets | ✓ | ✓ editor; defaults from 09-shortcuts.md; imports an InDesign Show Set `.txt` | ✓ | tool keys are fixed, so a set's Tools items are reported, not applied |
| Keyboard shortcuts: InDesign defaults, editor, sets | ✓ | ✓ editor; defaults from 09-shortcuts.md; panels take keys (InDesign's F5–F12 defaults where free) and tool keys are editable; imports an InDesign Show Set `.txt` | ✓ | the importer applies menu items only, so a set's Tools and panel items are reported, not applied; no Object Styles or Transform panel to give Cmd+F7 or F9, and Text Wrap (Cmd+Alt+W) and Links (Cmd+Shift+D) keep DesignCraft's owners of those keys |
| Quick Apply | ✓ | ✓ | ✓ | |
| Numeric fields: units, arithmetic, scrubbing | ✓ | 21 pt spinner fields with arithmetic | ✓ | |
| Colour fields: hex, picker from any swatch field | ✓ | ✓ | ~ | hex entry and picker issues (#323, #190) |