diff --git a/crates/ui-egui/src/adjust_editors.rs b/crates/ui-egui/src/adjust_editors.rs index 43feb7857d..85732681de 100644 --- a/crates/ui-egui/src/adjust_editors.rs +++ b/crates/ui-egui/src/adjust_editors.rs @@ -573,7 +573,11 @@ fn curves(ui: &mut egui::Ui, v: &mut Value, cx: &EditorCx) -> Edit { } }); let t2 = Tokens::get(ui.ctx()); - ui.label(RichText::new("Click to add a point · drag off or ⌘/Ctrl-click or Delete to remove").color(t2.text_faint).size(11.0)); + ui.label( + RichText::new(format!("Click to add a point · drag off or {}-click or Delete to remove", crate::shortcuts::pretty("Cmd"))) + .color(t2.text_faint) + .size(11.0), + ); ui.data_mut(|d| d.insert_temp(state_id, st)); if changed { v[key] = json!(pts.iter().map(|q| [q[0].round(), q[1].round()]).collect::>()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 324c9d1fb6..15fb38aefd 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -963,6 +963,8 @@ fn start_screen(app: &mut PhotocraftApp, ui: &mut egui::Ui) { let recent: Vec = if cfg!(target_arch = "wasm32") { Vec::new() } else { app.ui.recent_files.iter().take(HOME_RECENT).cloned().collect() }; let recent_h = if recent.is_empty() { 0.0 } else { 34.0 + recent.len() as f32 * HOME_RECENT_ROW }; let card = Rect::from_center_size(area.center(), egui::vec2(460.0, 330.0 + recent_h)); + let new_label = crate::shortcuts::command_label(app, "New document…", "file.new"); + let open_label = crate::shortcuts::command_label(app, "Open…", "file.open"); ui.scope_builder(egui::UiBuilder::new().max_rect(card), |ui| { ui.vertical_centered(|ui| { ui.horizontal(|ui| { @@ -981,10 +983,10 @@ fn start_screen(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.add_space(((card.width() - 2.0 * 190.0 - 12.0) / 2.0).max(0.0)); ui.spacing_mut().item_spacing.x = 12.0; - if crate::widgets::primary_button(ui, "New document… ⌘N", 190.0).clicked() { + if crate::widgets::primary_button(ui, &new_label, 190.0).clicked() { app.ui.open_dialog(crate::state::DialogKind::NewDocument, crate::state::UiState::new_document_fields()); } - if crate::widgets::secondary_button(ui, "Open… ⌘O", 190.0).clicked() { + if crate::widgets::secondary_button(ui, &open_label, 190.0).clicked() { app.open_dialog_file(); } }); diff --git a/crates/ui-egui/src/doc_props_ui.rs b/crates/ui-egui/src/doc_props_ui.rs index 4dfe79fbf5..ac61db4d80 100644 --- a/crates/ui-egui/src/doc_props_ui.rs +++ b/crates/ui-egui/src/doc_props_ui.rs @@ -217,11 +217,11 @@ pub fn properties(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.spacing_mut().item_spacing.x = 6.0; let pixel_grid = crate::view_cmds::checked(app, "view.show.pixelGrid").unwrap_or(false); for (icon, tip, id, on) in [ - ("ruler", "Rulers (⌘R)", "view.rulers", app.ui.extras.rulers), - ("grid-3x3", "Grid (⌘')", "view.show.grid", app.ui.extras.grid), - ("grid-2x2", "Pixel Grid", "view.show.pixelGrid", pixel_grid), + ("ruler", crate::shortcuts::tip_label(app, "Rulers", "view.rulers"), "view.rulers", app.ui.extras.rulers), + ("grid-3x3", crate::shortcuts::tip_label(app, "Grid", "view.show.grid"), "view.show.grid", app.ui.extras.grid), + ("grid-2x2", crate::shortcuts::tip_label(app, "Pixel Grid", "view.show.pixelGrid"), "view.show.pixelGrid", pixel_grid), ] { - if icons::button(ui, icon, 26.0, on, tip).clicked() { + if icons::button(ui, icon, 26.0, on, &tip).clicked() { run.push((id.to_string(), Value::Null)); } } @@ -240,9 +240,12 @@ pub fn properties(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.add_space(8.0); ui.spacing_mut().item_spacing.x = 6.0; - for (icon, tip, id) in [("eye", "Show Guides (⌘;)", "view.show.guides"), ("lock", "Lock Guides", "view.lockGuides")] { + for (icon, tip, id) in [ + ("eye", crate::shortcuts::tip_label(app, "Show Guides", "view.show.guides"), "view.show.guides"), + ("lock", crate::shortcuts::tip_label(app, "Lock Guides", "view.lockGuides"), "view.lockGuides"), + ] { let on = if id == "view.lockGuides" { app.ui.extras.lock_guides } else { app.ui.extras.guides }; - if icons::button(ui, icon, 26.0, on, tip).clicked() { + if icons::button(ui, icon, 26.0, on, &tip).clicked() { run.push((id.to_string(), Value::Null)); } } diff --git a/crates/ui-egui/src/layer_row_ui.rs b/crates/ui-egui/src/layer_row_ui.rs index 0ba530b6d8..cdd9462aee 100644 --- a/crates/ui-egui/src/layer_row_ui.rs +++ b/crates/ui-egui/src/layer_row_ui.rs @@ -160,11 +160,11 @@ pub fn indicators( actions.push(("layer.setEffectsExpanded".into(), json!({"layer": l.id.0, "expanded": !fx_open, "all": all}))); } let (verb, name) = (if fx_open { "Collapse" } else { "Expand" }, l.name.clone()); - let resp = resp.on_hover_text(if fx_open { - "Hide the layer's effects (⌥-click: all layers)" - } else { - "Show the layer's effects (⌥-click: all layers)" - }); + let resp = resp.on_hover_text(format!( + "{} the layer's effects ({}-click: all layers)", + if fx_open { "Hide" } else { "Show" }, + crate::shortcuts::pretty("Alt") + )); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("{verb} effects {name}"))); } } diff --git a/crates/ui-egui/src/layer_tree_ui.rs b/crates/ui-egui/src/layer_tree_ui.rs index 335d258b05..024b4949f3 100644 --- a/crates/ui-egui/src/layer_tree_ui.rs +++ b/crates/ui-egui/src/layer_tree_ui.rs @@ -62,7 +62,7 @@ pub fn disclosure(ui: &mut egui::Ui, row: Rect, x: &mut f32, l: &Layer, actions: actions.push(("layer.setExpanded".into(), json!({"layer": l.id.0, "expanded": !g.expanded, "all": all}))); } let (verb, name) = (if g.expanded { "Collapse" } else { "Expand" }, l.name.clone()); - let resp = resp.on_hover_text(if g.expanded { "Collapse group (⌥-click: all groups)" } else { "Expand group (⌥-click: all groups)" }); + let resp = resp.on_hover_text(format!("{} group ({}-click: all groups)", if g.expanded { "Collapse" } else { "Expand" }, crate::shortcuts::pretty("Alt"))); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("{verb} group {name}"))); clicked } diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index e5642f3354..8b5b0521ed 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -143,7 +143,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { app.ui.palette_open = false; } ui.add_space(4.0); - ui.label(RichText::new("↑↓ navigate ↵ run esc close").small().color(t.text_faint)); + ui.label(RichText::new(format!("↑↓ navigate {} run esc close", crate::shortcuts::pretty("Enter"))).small().color(t.text_faint)); ui.data_mut(|d| { d.insert_temp(qid, q); d.insert_temp(sel_id, sel); diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 02433854fe..3f85c6cbe6 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -311,7 +311,8 @@ pub fn title_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { app.ui.workspace = ws; crate::menus::apply_workspace(app); } - if icons::button(ui, "search", 28.0, app.ui.palette_open, "Search commands (⌘K)").clicked() { + if icons::button(ui, "search", 28.0, app.ui.palette_open, &crate::shortcuts::tip_label(app, "Search commands", "edit.search")).clicked() + { app.ui.palette_open = !app.ui.palette_open; } let theme_icon = if t.dark() { "sun" } else { "moon" }; @@ -494,10 +495,10 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { Tool::RectMarquee | Tool::EllipseMarquee if t.pro => { ui.spacing_mut().item_spacing.x = 2.0; for (i, (icon, tip)) in [ - ("square", "New selection"), - ("plus", "Add to selection (⇧)"), - ("minus", "Subtract from selection (⌥)"), - ("squares-subtract", "Intersect with selection (⇧⌥)"), + ("square", "New selection".to_string()), + ("plus", format!("Add to selection ({})", crate::shortcuts::pretty("Shift"))), + ("minus", format!("Subtract from selection ({})", crate::shortcuts::pretty("Alt"))), + ("squares-subtract", format!("Intersect with selection ({})", crate::shortcuts::pretty("Shift+Alt"))), ] .iter() .enumerate() @@ -546,7 +547,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { let _ = crate::menus::invoke(app, ui.ctx(), "select.selectAndMask", json!({})); } if app.ui.tool == Tool::PolygonLasso && !app.ui.polygon.is_empty() { - hint(ui, "Click the first point or press ↵ to close · Esc cancels"); + hint(ui, &format!("Click the first point or press {} to close · Esc cancels", crate::shortcuts::pretty("Enter"))); } } Tool::MagicWand => { @@ -636,7 +637,9 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { let _ = icons::button(ui, "grid-3x3", 24.0, true, "Overlay: Rule of Thirds"); widgets::checkbox(ui, &mut o.crop_delete, "Delete Cropped Pixels"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if icons::button(ui, "check", 26.0, false, "Commit current crop operation (↵)").clicked() { + if icons::button(ui, "check", 26.0, false, &format!("Commit current crop operation ({})", crate::shortcuts::pretty("Enter"))) + .clicked() + { crate::canvas::commit_crop(app); } if icons::button(ui, "ban", 26.0, false, "Cancel current crop operation (Esc)").clicked() { @@ -681,12 +684,22 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { section(ui, "Distribute", "layer.distribute."); }); } - Tool::RectMarquee | Tool::EllipseMarquee => hint(ui, "Drag to select · ⇧ add · ⌥ subtract · ⇧⌥ intersect · click to deselect"), + Tool::RectMarquee | Tool::EllipseMarquee => hint( + ui, + &format!( + "Drag to select · {} add · {} subtract · {} intersect · click to deselect", + crate::shortcuts::pretty("Shift"), + crate::shortcuts::pretty("Alt"), + crate::shortcuts::pretty("Shift+Alt") + ), + ), Tool::Move => hint(ui, "Drag to move the active layer"), - Tool::Eyedropper => hint(ui, "Click to sample the foreground colour · ⌥-click for background"), + Tool::Eyedropper => { + hint(ui, &format!("Click to sample the foreground colour · {}-click for background", crate::shortcuts::pretty("Alt"))) + } Tool::Zoom => { widgets::checkbox(ui, &mut app.ui.tool_options.zoom_scrubby, "Scrubby Zoom"); - hint(ui, "Click to zoom in · ⌥-click to zoom out · drag right/left to zoom in/out"); + hint(ui, &format!("Click to zoom in · {}-click to zoom out · drag right/left to zoom in/out", crate::shortcuts::pretty("Alt"))); if widgets::secondary_button(ui, "Fit Screen", 0.0).clicked() && let Some(i) = app.session.active_index() { @@ -699,10 +712,22 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { } } Tool::Hand => hint(ui, "Drag to pan · hold Space with any tool"), - Tool::Lasso | Tool::PolygonLasso => hint(ui, "Drag (lasso) or click points (polygonal) · ⇧ add · ⌥ subtract"), + Tool::Lasso | Tool::PolygonLasso => hint( + ui, + &format!( + "Drag (lasso) or click points (polygonal) · {} add · {} subtract", + crate::shortcuts::pretty("Shift"), + crate::shortcuts::pretty("Alt") + ), + ), Tool::Crop => hint( ui, - "Drag a crop box · drag inside to move · edges resize (⇧ ratio, ⌥ centre) · Space moves while drawing · ↵ commits · Esc cancels", + &format!( + "Drag a crop box · drag inside to move · edges resize ({} ratio, {} centre) · Space moves while drawing · {} commits · Esc cancels", + crate::shortcuts::pretty("Shift"), + crate::shortcuts::pretty("Alt"), + crate::shortcuts::pretty("Enter") + ), ), Tool::Gradient => hint(ui, "Drag to draw a gradient"), Tool::PaintBucket => hint(ui, "Click to fill similar colours"), @@ -1294,7 +1319,7 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { if icons::button(ui, "trash", 26.0, false, "Delete layer").clicked() { actions.push(("layer.delete".into(), json!({}))); } - if icons::button(ui, "square-plus", 26.0, false, "Create a new layer (⇧⌘N)").clicked() { + if icons::button(ui, "square-plus", 26.0, false, &crate::shortcuts::tip_label(app, "Create a new layer", "layer.new.layer")).clicked() { actions.push(("layer.new.layer".into(), json!({}))); } if icons::button(ui, "folder", 26.0, false, "Create a new group").clicked() { @@ -1319,7 +1344,8 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.close(); } }); - if icons::button(ui, "square-dot", 26.0, false, "Add a mask (from the selection; ⌥ inverts)").clicked() { + if icons::button(ui, "square-dot", 26.0, false, &format!("Add a mask (from the selection; {} inverts)", crate::shortcuts::pretty("Alt"))).clicked() + { let alt = ui.input(|i| i.modifiers.alt); actions.push((crate::layer_menu_ui::add_mask_command(doc.selection.is_some(), alt).into(), json!({}))); } @@ -2252,10 +2278,10 @@ fn effect_rows(app: &mut PhotocraftApp, ui: &mut egui::Ui, l: &Layer, depth: usi fn selection_mode_buttons(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ui.spacing_mut().item_spacing.x = 2.0; for (i, (icon, tip)) in [ - ("square", "New selection"), - ("plus", "Add to selection (⇧)"), - ("minus", "Subtract from selection (⌥)"), - ("squares-subtract", "Intersect with selection (⇧⌥)"), + ("square", "New selection".to_string()), + ("plus", format!("Add to selection ({})", crate::shortcuts::pretty("Shift"))), + ("minus", format!("Subtract from selection ({})", crate::shortcuts::pretty("Alt"))), + ("squares-subtract", format!("Intersect with selection ({})", crate::shortcuts::pretty("Shift+Alt"))), ] .iter() .enumerate() diff --git a/crates/ui-egui/src/perspective_ui.rs b/crates/ui-egui/src/perspective_ui.rs index c43b689b48..899edfe3a8 100644 --- a/crates/ui-egui/src/perspective_ui.rs +++ b/crates/ui-egui/src/perspective_ui.rs @@ -319,7 +319,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { } } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if crate::widgets::primary_button(ui, "✓", 32.0).on_hover_text("Commit Perspective Warp (↩)").clicked() { + if crate::widgets::primary_button(ui, "✓", 32.0).on_hover_text(format!("Commit Perspective Warp ({})", crate::shortcuts::pretty("Enter"))).clicked() { commit(app); } else if crate::widgets::secondary_button(ui, "⊘", 32.0).on_hover_text("Cancel (Esc)").clicked() { app.distort.perspective = None; diff --git a/crates/ui-egui/src/prefs_ui.rs b/crates/ui-egui/src/prefs_ui.rs index 6650641b7a..dbc9dbf2fe 100644 --- a/crates/ui-egui/src/prefs_ui.rs +++ b/crates/ui-egui/src/prefs_ui.rs @@ -310,10 +310,7 @@ fn history_log(app: &mut PhotocraftApp) { // ------------------------------------------------------------------ shortcuts -/// The shortcut in effect for a menu item: the user's override, else `default`. -pub fn effective_shortcut(app: &PhotocraftApp, id: &str, default: Option<&str>) -> Option { - app.session.prefs().shortcut(id, default).map(str::to_string) -} +pub use crate::shortcuts::{default_shortcut, effective_shortcut}; /// Every shortcut-bearing command (engine registry plus shell commands): (id, label, menu path, /// default shortcut), menu items first in Photoshop order. @@ -335,17 +332,6 @@ pub fn shortcut_items(app: &PhotocraftApp) -> Vec<(String, String, Vec, out } -/// Default (unmodified) shortcut of a command id. -pub fn default_shortcut(id: &str) -> Option { - crate::menus::UI_COMMANDS - .iter() - .find(|c| c.0 == id) - .and_then(|c| c.3) - .or_else(|| photocraft_engine::commands::find(id).and_then(|c| c.shortcut)) - .or_else(|| crate::menu_catalog::CATALOG.iter().find(|c| c.3 == id).and_then(|c| c.2)) - .map(str::to_string) -} - /// Text of a key press for the shortcut editor (`Cmd+Shift+K`), `None` for bare modifiers. pub fn shortcut_text(key: egui::Key, m: egui::Modifiers) -> Option { let mut parts = Vec::new(); diff --git a/crates/ui-egui/src/preset_panels.rs b/crates/ui-egui/src/preset_panels.rs index 744d694f6c..a78690f41a 100644 --- a/crates/ui-egui/src/preset_panels.rs +++ b/crates/ui-egui/src/preset_panels.rs @@ -916,7 +916,7 @@ pub fn clone_source_panel(app: &mut PhotocraftApp, ui: &mut egui::Ui) { let set = cs.slots[i].source.is_some(); let tip = match cs.slots[i].source { Some(s) => format!("Clone source {}: {:.0}, {:.0}", i + 1, s[0], s[1]), - None => format!("Clone source {} (⌥-click with Clone Stamp to set)", i + 1), + None => format!("Clone source {} ({}-click with Clone Stamp to set)", i + 1, crate::shortcuts::pretty("Alt")), }; let r = crate::icons::button(ui, "stamp", 30.0, cs.active == i, &tip); if set { @@ -933,7 +933,7 @@ pub fn clone_source_panel(app: &mut PhotocraftApp, ui: &mut egui::Ui) { match (slot.source, slot.layer.and_then(|l| app.session.active().and_then(|d| d.doc.layer(photocraft_doc::LayerId(l)).map(|x| x.name.clone())))) { (Some(_), Some(l)) => format!("Source: {} : {l}", app.session.active().map(|d| d.doc.name.clone()).unwrap_or_default()), (Some(_), None) => "Source: set".to_string(), - _ => "Source: not set (⌥-click with Clone Stamp)".to_string(), + _ => format!("Source: not set ({}-click with Clone Stamp)", crate::shortcuts::pretty("Alt")), }; ui.label(egui::RichText::new(src_line).color(t.text_dim).size(11.0)); ui.add_space(4.0); diff --git a/crates/ui-egui/src/puppet_ui.rs b/crates/ui-egui/src/puppet_ui.rs index 3bf70044a3..91d73061d6 100644 --- a/crates/ui-egui/src/puppet_ui.rs +++ b/crates/ui-egui/src/puppet_ui.rs @@ -361,7 +361,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { s.solve(true); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if crate::widgets::primary_button(ui, "✓", 32.0).on_hover_text("Commit Puppet Warp (↩)").clicked() { + if crate::widgets::primary_button(ui, "✓", 32.0).on_hover_text(format!("Commit Puppet Warp ({})", crate::shortcuts::pretty("Enter"))).clicked() { commit(app); } else if crate::widgets::secondary_button(ui, "⊘", 32.0).on_hover_text("Cancel (Esc)").clicked() { app.distort.puppet = None; diff --git a/crates/ui-egui/src/retouch_ui.rs b/crates/ui-egui/src/retouch_ui.rs index 93f8cba752..eae4cb13ef 100644 --- a/crates/ui-egui/src/retouch_ui.rs +++ b/crates/ui-egui/src/retouch_ui.rs @@ -152,7 +152,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo crate::widgets::dropdown(ui, "clone-sample", &mut o.clone_sample, &opts, 130.0); if app.ui.clone_source.is_none() { crate::widgets::vline(ui, 22.0); - opt(ui, "⌥-click to set the source"); + opt(ui, &format!("{}-click to set the source", crate::shortcuts::pretty("Alt"))); } } Tool::Dodge | Tool::Burn => { @@ -182,7 +182,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo Tool::QuickSelection => { crate::widgets::checkbox(ui, &mut o.sample_all_layers, "Sample All Layers"); crate::widgets::checkbox(ui, &mut o.enhance_edge, "Enhance Edge"); - opt(ui, "⌥ to subtract"); + opt(ui, &format!("{} to subtract", crate::shortcuts::pretty("Alt"))); crate::widgets::vline(ui, 22.0); if crate::widgets::secondary_button(ui, "Select Subject", 0.0).clicked() { let _ = app.run("select.subject", json!({})); diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 547efb255f..9af1cd7ba4 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -33,7 +33,9 @@ pub fn parse(s: &str) -> Option { Some(KeyboardShortcut::new(mods, key?)) } -/// Human-readable form for menus. +/// Human-readable form for menus, tooltips and inline hints (`Cmd+Shift+N` → `⌘⇧N` on +/// macOS, `Ctrl+Shift+N` elsewhere). Works for lone keys too: `pretty("Alt")` is `⌥`/`Alt`, +/// `pretty("Enter")` is `↩`/`Enter`. pub fn pretty(s: &str) -> String { let mac = cfg!(target_os = "macos"); s.split('+') @@ -42,12 +44,48 @@ pub fn pretty(s: &str) -> String { ("Cmd", false) => "Ctrl".to_string(), ("Shift", true) => "⇧".to_string(), ("Alt", true) => "⌥".to_string(), + ("Enter", true) => "↩".to_string(), + ("Delete", true) => "⌫".to_string(), (other, _) => other.to_string(), }) .collect::>() .join(if mac { "" } else { "+" }) } +/// The command's effective shortcut in the platform's notation (`Ctrl+N`), or `None` when +/// the command has no binding (the user removed it in Edit › Keyboard Shortcuts). +pub fn shortcut_label(app: &PhotocraftApp, command: &str) -> Option { + effective_shortcut(app, command, default_shortcut(command).as_deref()).map(|s| pretty(&s)) +} + +/// `label` followed by the command's effective shortcut (`New document… Ctrl+N`); bare +/// `label` when the binding was removed. +pub fn command_label(app: &PhotocraftApp, label: &str, command: &str) -> String { + shortcut_label(app, command).map_or_else(|| label.to_string(), |sc| format!("{label} {sc}")) +} + +/// `label` with the command's effective shortcut in parentheses (`Rulers (Ctrl+R)`); bare +/// `label` when the binding was removed. +pub fn tip_label(app: &PhotocraftApp, label: &str, command: &str) -> String { + shortcut_label(app, command).map_or_else(|| label.to_string(), |sc| format!("{label} ({sc})")) +} + +/// The shortcut in effect for a command: the user's override, else `default`. +pub fn effective_shortcut(app: &PhotocraftApp, id: &str, default: Option<&str>) -> Option { + app.session.prefs().shortcut(id, default).map(str::to_string) +} + +/// Default (unmodified) shortcut of a command id. +pub fn default_shortcut(id: &str) -> Option { + crate::menus::UI_COMMANDS + .iter() + .find(|c| c.0 == id) + .and_then(|c| c.3) + .or_else(|| photocraft_engine::commands::find(id).and_then(|c| c.shortcut)) + .or_else(|| crate::menu_catalog::CATALOG.iter().find(|c| c.3 == id).and_then(|c| c.2)) + .map(str::to_string) +} + /// With ⇧ held the OS reports the shifted character as the logical key (US layout): ⇧; is `:`. fn shifted(k: Key) -> Option { Some(match k { @@ -325,6 +363,35 @@ mod tests { assert!(!key_matches(&clear, Key::Backspace, cmd)); } + #[test] + fn pretty_uses_the_platform_notation() { + let mac = cfg!(target_os = "macos"); + assert_eq!(pretty("Cmd+Shift+N"), if mac { "⌘⇧N" } else { "Ctrl+Shift+N" }); + // Lone keys and inline-hint combos. + assert_eq!(pretty("Alt"), if mac { "⌥" } else { "Alt" }); + assert_eq!(pretty("Shift+Alt"), if mac { "⇧⌥" } else { "Shift+Alt" }); + assert_eq!(pretty("Enter"), if mac { "↩" } else { "Enter" }); + assert_eq!(pretty("Cmd+Enter"), if mac { "⌘↩" } else { "Ctrl+Enter" }); + } + + #[test] + fn labels_show_the_effective_shortcut_in_platform_notation() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default()); + let new = command_label(&app, "New document…", "file.new"); + if cfg!(target_os = "macos") { + assert_eq!(new, "New document… ⌘N"); + } else { + assert_eq!(new, "New document… Ctrl+N"); + } + assert_eq!(tip_label(&app, "Rulers", "view.rulers"), format!("Rulers ({})", pretty("Cmd+R"))); + // A user override is picked up… + app.session.prefs.edit(|p| p.shortcuts.insert("file.new".into(), "Cmd+Shift+N".into())); + assert_eq!(command_label(&app, "New document…", "file.new"), format!("New document… {}", pretty("Cmd+Shift+N"))); + // …and a removed binding hides the shortcut entirely. + app.session.prefs.edit(|p| p.shortcuts.insert("file.new".into(), String::new())); + assert_eq!(command_label(&app, "New document…", "file.new"), "New document…"); + } + #[test] fn clipboard_events_become_pixel_shortcuts_outside_text() { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/transform_tool.rs b/crates/ui-egui/src/transform_tool.rs index d79322f36d..a0c319243b 100644 --- a/crates/ui-egui/src/transform_tool.rs +++ b/crates/ui-egui/src/transform_tool.rs @@ -874,7 +874,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(8.0); - if crate::icons::button(ui, "check", 24.0, false, "Commit transform (↩)").clicked() { + if crate::icons::button(ui, "check", 24.0, false, &format!("Commit transform ({})", crate::shortcuts::pretty("Enter"))).clicked() { commit(app); } if crate::icons::button(ui, "ban", 24.0, false, "Cancel transform (Esc)").clicked() { @@ -945,7 +945,7 @@ fn warp_options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, w: &Warp) { } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(8.0); - if crate::icons::button(ui, "check", 24.0, false, "Commit warp (↩)").clicked() { + if crate::icons::button(ui, "check", 24.0, false, &format!("Commit warp ({})", crate::shortcuts::pretty("Enter"))).clicked() { commit(app); } if crate::icons::button(ui, "ban", 24.0, false, "Cancel warp (Esc)").clicked() { diff --git a/crates/ui-egui/src/type_panels_ui.rs b/crates/ui-egui/src/type_panels_ui.rs index e639490c96..ca020bd9a2 100644 --- a/crates/ui-egui/src/type_panels_ui.rs +++ b/crates/ui-egui/src/type_panels_ui.rs @@ -266,9 +266,9 @@ pub fn styles_panel(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool) } } resp.on_hover_text(if id == 0 && !paragraph { - "No character style" + "No character style".to_string() } else { - "Click to apply, ⌥-click to clear overrides, double-click for Style Options" + format!("Click to apply, {}-click to clear overrides, double-click for Style Options", crate::shortcuts::pretty("Alt")) }); } }); diff --git a/crates/ui-egui/src/type_tool.rs b/crates/ui-egui/src/type_tool.rs index eb56b24ac3..00536bcc77 100644 --- a/crates/ui-egui/src/type_tool.rs +++ b/crates/ui-egui/src/type_tool.rs @@ -761,7 +761,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { if app.ui.text_edit.is_some() { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(8.0); - if crate::icons::button(ui, "check", 24.0, false, "Commit any current edits (⌘↩)").clicked() { + if crate::icons::button(ui, "check", 24.0, false, &format!("Commit any current edits ({})", crate::shortcuts::pretty("Cmd+Enter"))).clicked() { commit(app); } if crate::icons::button(ui, "ban", 24.0, false, "Cancel any current edits (Esc)").clicked() { diff --git a/crates/ui-egui/src/vector_ui.rs b/crates/ui-egui/src/vector_ui.rs index 6b1c34287b..a6d95efcda 100644 --- a/crates/ui-egui/src/vector_ui.rs +++ b/crates/ui-egui/src/vector_ui.rs @@ -360,7 +360,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo let opts = [("path".to_string(), "Path"), ("shape".to_string(), "Shape")]; crate::widgets::dropdown(ui, "pen-mode", &mut o.vector_mode, &opts, 80.0); crate::widgets::vline(ui, 22.0); - lbl(ui, "Click: corner · Drag: smooth · Click first point: close · ↩ finish · Esc cancel"); + lbl(ui, &format!("Click: corner · Drag: smooth · Click first point: close · {} finish · Esc cancel", crate::shortcuts::pretty("Enter"))); return true; } let mut mode = "shape".to_string(); diff --git a/crates/ui-egui/src/workspace_ui.rs b/crates/ui-egui/src/workspace_ui.rs index 8b1863b2e8..7c6eaa2d6e 100644 --- a/crates/ui-egui/src/workspace_ui.rs +++ b/crates/ui-egui/src/workspace_ui.rs @@ -292,12 +292,13 @@ pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) { if app.ui.shell.modifier_keys { let mut close = false; let mut s = app.ui.shell.clone(); - let cmd = if cfg!(target_os = "macos") { "⌘" } else { "Ctrl" }; - let alt = if cfg!(target_os = "macos") { "⌥" } else { "Alt" }; + let cmd = crate::shortcuts::pretty("Cmd"); + let shift = crate::shortcuts::pretty("Shift"); + let alt = crate::shortcuts::pretty("Alt"); crate::analysis_ui::panel_window(app, ctx, "modifier-keys", "Modifier Keys", vec2(-420.0, 80.0), 150.0, |ui| { close = crate::analysis_ui::title_row(ui, "Modifier Keys"); ui.horizontal(|ui| { - for (label, on) in [("⇧", &mut s.sticky_shift), (cmd, &mut s.sticky_command), (alt, &mut s.sticky_alt)] { + for (label, on) in [(&*shift, &mut s.sticky_shift), (&*cmd, &mut s.sticky_command), (&*alt, &mut s.sticky_alt)] { if ui.add(egui::Button::new(RichText::new(label).size(14.0)).selected(*on).min_size(vec2(40.0, 30.0))).clicked() { *on = !*on; }