graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: Ctrl+Shift+S = Save As; Shift+letter chords actually match
Save As gets its own Action and the save_document_as chord (Ctrl+Shift+s,
rebindable via input.kdl like the rest).
Wiring it exposed that NO Shift+letter chord could ever have fired: the
matcher compared the event key exactly against the stored one, and with
Shift held xkb delivers the SHIFTED character — "S" against a stored "s".
Ctrl+Shift+Tab never noticed because Named keys aren't shift-transformed.
Character keys now compare case-insensitively. The chord test feeds the
real event shapes (upper with shift, lower without) — its first version fed
lowercase for both and passed against a matcher that could never fire live.
Live-verified in a shadow session with injected keys: Ctrl+Shift+S opens
the chooser; Ctrl+S with a loaded project saves in place and does not.
src/app.rs | 4 ++++
src/main.rs | 21 +++++++++++++++++++++
src/shortcut.rs | 21 ++++++++++++++++-----
3 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 5ffa1db..8bb2fe0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2714,6 +2714,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
register("toggle_spreadsheet", "`", Action::ToggleSpreadsheet);
register("toggle_circular_pane", "Ctrl+d", Action::ToggleCircularPane);
register("save_document", "Ctrl+s", Action::Save);
+ register("save_document_as", "Ctrl+Shift+s", Action::SaveAs);
register("next_context", "Ctrl+Tab", Action::NextContext);
register("previous_context", "Ctrl+Shift+Tab", Action::PrevContext);
}
@@ -3712,6 +3713,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
self.apply_layout();
self.sync_grid_settings();
}
+ Action::SaveAs => {
+ self.save_file_chooser();
+ }
Action::Save => {
let path_opt = self.loaded_project_path.clone();
if let Some(path) = path_opt {
diff --git a/src/main.rs b/src/main.rs
index a74a666..155f6c4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -470,6 +470,27 @@ mod tests {
assert_eq!(v, Some("survived"), "migration recreated Guides instead of moving it");
}
+ /// Ctrl+S saves in place, Ctrl+Shift+S is Save As — and the Shift must
+ /// actually discriminate: a matcher that ignores modifiers would fire
+ /// plain Save for both.
+ #[test]
+ fn test_save_as_chord() {
+ let mut m = ShortcutManager::new();
+ m.register("Ctrl+s", Action::Save).unwrap();
+ m.register("Ctrl+Shift+s", Action::SaveAs).unwrap();
+ let ctrl = crate::app::ModifiersState { ctrl: true, ..Default::default() };
+ let ctrl_shift = crate::app::ModifiersState { ctrl: true, shift: true, ..Default::default() };
+ // The REAL event shapes: xkb delivers the shifted character when
+ // Shift is held — "S", not "s". The first version of this test fed
+ // lowercase for both and passed against a matcher that could never
+ // fire in practice.
+ let lower = cce_ui::widget::Key::Character("s".into());
+ let upper = cce_ui::widget::Key::Character("S".into());
+ assert_eq!(m.match_action(&ctrl, &lower), Some(Action::Save));
+ assert_eq!(m.match_action(&ctrl_shift, &upper), Some(Action::SaveAs));
+ assert_eq!(m.match_action(&ctrl_shift, &lower), Some(Action::SaveAs));
+ }
+
#[test]
fn test_load_default_project() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("default_project.json");
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 4262855..55bc248 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -13,6 +13,7 @@ pub enum Action {
ToggleCircularPane,
DetachCircularWindow,
Save,
+ SaveAs,
NextContext,
PrevContext,
}
@@ -76,11 +77,21 @@ impl Shortcut {
}
pub fn matches(&self, mods: &ModifiersState, key: &Key) -> bool {
- mods.control_key() == self.ctrl
- && mods.shift_key() == self.shift
- && mods.alt_key() == self.alt
- && mods.super_key() == self.logo
- && key == &self.key
+ if mods.control_key() != self.ctrl
+ || mods.shift_key() != self.shift
+ || mods.alt_key() != self.alt
+ || mods.super_key() != self.logo
+ {
+ return false;
+ }
+ // Character keys compare case-insensitively: with Shift held, xkb
+ // delivers the SHIFTED character ("S"), so an exact match against the
+ // chord's stored "s" made every Shift+letter chord unmatchable —
+ // Ctrl+Shift+Tab never noticed because Named keys aren't shifted.
+ match (key, &self.key) {
+ (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
+ (a, b) => a == b,
+ }
}
}