From f091c7fbf0b06b8975255aac94bed8bafed28e45 Mon Sep 17 00:00:00 2001 From: JGF <44821497+bommerts@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:46:41 -0400 Subject: [PATCH] Windows: Alt-click clone hint and Ctrl+N channel errors (#251 follow-up) (#336) (#336) --- crates/engine/src/channel_cmds.rs | 7 +++++-- crates/engine/src/channel_cmds/tests.rs | 3 +++ crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/retouch_ui.rs | 21 ++++++++++++++++++++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/channel_cmds.rs b/crates/engine/src/channel_cmds.rs index eca3f18d7..dcda30211 100644 --- a/crates/engine/src/channel_cmds.rs +++ b/crates/engine/src/channel_cmds.rs @@ -1067,6 +1067,9 @@ fn target_cmd(s: &mut Session, p: &Value) -> Result { Ok(list(s)) } +/// The channel shortcuts' modifier as the platform writes it, for error messages (`⌘4`, `Ctrl+4`). +const SLOT_KEY: &str = if cfg!(target_os = "macos") { "⌘" } else { "Ctrl+" }; + /// ⌘2 composite, ⌘3… colour channels then alpha channels (Photoshop CC defaults). fn target_slot(s: &mut Session, slot: usize) -> Result { let doc = &s.active().ok_or(EngineError::NoDocument)?.doc; @@ -1076,7 +1079,7 @@ fn target_slot(s: &mut Session, slot: usize) -> Result { 2 => ChannelTarget::Composite, n if n - 3 < shown_colors => ChannelTarget::Color(n - 3), n if n - 3 - shown_colors < doc.channels.len() => ChannelTarget::Alpha(n - 3 - shown_colors), - _ => return Err(EngineError::Other(format!("no channel for ⌘{slot}"))), + _ => return Err(EngineError::Other(format!("no channel for {SLOT_KEY}{slot}"))), }; target(s, t); Ok(list(s)) @@ -1595,7 +1598,7 @@ fn slot_ok(s: &Session) -> std::result::Result<(), String> { let d = s.active().ok_or("no document open")?; let colors = color_count(&d.doc); let shown = if colors > 1 { colors } else { 0 }; - if N - 3 < shown + d.doc.channels.len() { Ok(()) } else { Err(format!("no channel for ⌘{N}")) } + if N - 3 < shown + d.doc.channels.len() { Ok(()) } else { Err(format!("no channel for {SLOT_KEY}{N}")) } } fn has_apply_target(s: &Session) -> std::result::Result<(), String> { diff --git a/crates/engine/src/channel_cmds/tests.rs b/crates/engine/src/channel_cmds/tests.rs index b338d94ed..7b0c28e11 100644 --- a/crates/engine/src/channel_cmds/tests.rs +++ b/crates/engine/src/channel_cmds/tests.rs @@ -177,6 +177,9 @@ fn shortcut_slots_target_channels() { s.execute("channel.target.slot6", json!({})).unwrap(); assert_eq!(s.active().unwrap().channel_view.target, ChannelTarget::Alpha(0)); assert!(!s.is_enabled("channel.target.slot7")); + // The reason names the shortcut in the platform's notation (⌘7 on the Mac, Ctrl+7 elsewhere). + let err = s.execute("channel.target.slot7", json!({})).unwrap_err().to_string(); + assert!(err.contains(if cfg!(target_os = "macos") { "no channel for ⌘7" } else { "no channel for Ctrl+7" }), "{err}"); s.execute("channel.target.composite", json!({})).unwrap(); assert_eq!(s.active().unwrap().channel_view.target, ChannelTarget::Composite); // Grayscale: ⌘3 is the first alpha channel. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 50fd61bf7..9c293ba1c 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -727,6 +727,7 @@ All User Slices すべてのユーザースライス All shortcuts reset to Photoshop defaults. すべてのショートカットを初期状態に戻しました。 Alt Alt + Alt-click to define a source point to clone from Alt+クリックでコピー元を指定 Always use pressure for opacity 常に筆圧を不透明度に使う Always use pressure for size 常に筆圧をサイズに使う Amount 量 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d2bc75e44..74e5b341c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -725,6 +725,7 @@ All User Slices 所有用户切片 All shortcuts reset to Photoshop defaults. 所有快捷键已重置为 Photoshop 默认值。 Alt Alt + Alt-click to define a source point to clone from 按 Alt 单击以定义仿制源点 Always use pressure for opacity 始终用压感控制不透明度 Always use pressure for size 始终用压感控制大小 Amount 数量 diff --git a/crates/ui-egui/src/retouch_ui.rs b/crates/ui-egui/src/retouch_ui.rs index de7a6dc55..c9ac8f7bd 100644 --- a/crates/ui-egui/src/retouch_ui.rs +++ b/crates/ui-egui/src/retouch_ui.rs @@ -26,7 +26,13 @@ pub fn finish_stroke(app: &mut PhotocraftApp, tool: Tool, points: &[[f64; 3]], m (Some(off), _) => p["offset"] = json!(off), (None, Some(src)) => p["source"] = json!(src), (None, None) => { - app.ui.status = tl!("Option-click to define a source point to clone from").into(); + // Photoshop says Option-click on the Mac and Alt-click on Windows. + app.ui.status = if cfg!(target_os = "macos") { + tl!("Option-click to define a source point to clone from") + } else { + tl!("Alt-click to define a source point to clone from") + } + .into(); app.ui.status_error = true; return true; } @@ -237,6 +243,19 @@ mod tests { } } + #[test] + fn clone_without_a_source_names_the_platform_modifier() { + // Option-click on the Mac, Alt-click elsewhere (#251). + for tool in [Tool::CloneStamp, Tool::Healing] { + let mut app = app(); + app.ui.tool = tool; + assert!(finish_stroke(&mut app, tool, &[[10.0, 30.0, 1.0], [50.0, 30.0, 1.0]], egui::Modifiers::NONE)); + assert!(app.ui.status_error, "{tool:?}"); + let want = if cfg!(target_os = "macos") { "Option-click" } else { "Alt-click" }; + assert!(app.ui.status.starts_with(want), "{tool:?}: {}", app.ui.status); + } + } + #[test] fn retouch_strokes_paint_the_targeted_mask() { // #207: with the mask targeted, retouching changes the mask, never the pixels.