Files
vectorcraft/crates/ui-egui/src/tests_pastechords.rs
T
aiongg 8bf090b253 Shortcuts: use the shared craft-keymap crate for the catalogue, overrides, dispatch and .kys import (#3119)
* Shortcuts: the keymap, overrides and dispatch come from craft-keymap

The editor's catalogue (commands, UI commands, panels, tools; one tool
group per tool; show/hide commands marked as toggles) builds a
craft-keymap Keymap. UiState::shortcut_overrides is a craft_keymap
Overrides stored through keymap::stored, which writes the same map of
entry key -> one string as before, with panel: keys and VectorCraft's
spellings, so earlier versions read the file; a value that is not text
drops that entry instead of every preference.

Key presses run through craft_keymap's Dispatcher with accept filters:
keys that type text go to the one-key text path, and while the Type
tool edits only command-key chords run. Held keys repeat. Conflicts
and assignment use the crate's conflicts and set_bindings. Extra Shift
no longer falls back to the chord without it.

* Shortcuts: .kys import matches and applies through craft-keymap

The review proposes targets with craft_keymap's namematch, and Apply
hands the ready rows to craft_keymap::plan as one set, each row's
target passed as the remembered answer for its entry. The later row
takes a chord, and commands outside the set lose the chords the set
gives. Rows with one name in one block keep their own targets.
The engine's own namematch module is removed.

* Shortcuts: Cmd+, beside other hits, web Mac platform, cached dialog view

- Pin craft-keymap at 362eb59f.
- Cmd+, opens Settings on a Mac after the frame's other shortcuts run, with the
  Mac read from egui's OS; drop settings_chord, which dispatch no longer used.
- The web build follows the browser's OS, so Ctrl stays Ctrl on a Mac.
- The Keyboard Shortcuts dialog keeps its keymap, row texts and conflict count
  between frames while the working set is unchanged.
- Every default shortcut text becomes its entry's binding (test).
- craft-keymap counts as a UI crate for the layer check.
- Translate "too many changed shortcuts"; the parse error reuses the existing
  "can't parse shortcut `{c}`" message.

* Shortcuts: the chords the operating system takes can't be assigned

The keymap reserves craft_keymap::reserved::system for its platform:
window switching and closing, and the virtual consoles off the Mac.
The Keyboard Shortcuts dialog and shortcuts.set refuse them with
"<chord> is reserved by the system", and a saved override on one keeps
the entry's default. No default shortcut uses a reserved chord.

* Shortcuts: craft-keymap at craft-libs 2eca9b3a

craft-keymap comes from storytold/craft-libs (craft-libs #164 and #186), at the rev craft-pdf
already uses. The web build keeps the browser's full platform, so Windows browsers get Windows'
reserved chords, and each platform caches its own default keymap.
2026-10-24 15:01:26 -04:00

109 lines
5.5 KiB
Rust

//! The paste chords (egui reports every Cmd+V chord as a paste, not as its key) and the default
//! shortcuts (no chord does two things).
use egui::{Event, Key, Modifiers};
use serde_json::json;
use vectorcraft_engine::Session;
use crate::{VectorcraftApp, shortcut_editor, shortcuts};
/// One headless frame of the shortcut handler (and the app's logic, which publishes copies) with
/// `modifiers` held and `events`.
fn frame(app: &mut VectorcraftApp, modifiers: Modifiers, events: Vec<Event>) -> egui::FullOutput {
let events = std::iter::once(Event::ModifiersChanged(modifiers)).chain(events).collect();
let mut out = egui::Context::default().run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
shortcuts::handle(app, ui.ctx());
app.logic(ui.ctx());
});
out.textures_delta.clear();
out
}
fn last_object_bounds(app: &VectorcraftApp) -> (f64, f64) {
let st = app.session.active().unwrap();
let id = st.doc.layers[0].children().unwrap().last().unwrap().id;
let b = st.doc.node(id).unwrap().geometric_bounds().unwrap();
(b.x0, b.y0)
}
#[test]
fn cmd_shift_v_pastes_in_place() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 30})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
let out = frame(&mut app, Modifiers::COMMAND, vec![Event::Copy]);
let svg = out
.platform_output
.commands
.iter()
.find_map(|c| match c {
egui::OutputCommand::CopyText(t) => Some(t.clone()),
_ => None,
})
.unwrap();
// The text egui read from the clipboard comes with the chord still held.
frame(&mut app, Modifiers::COMMAND | Modifiers::SHIFT, vec![Event::Paste(svg.clone())]);
assert_eq!(app.session.active().unwrap().doc.layers[0].children().unwrap().len(), 2);
assert_eq!(last_object_bounds(&app), (10.0, 20.0), "Cmd+Shift+V pastes in place");
frame(&mut app, Modifiers::COMMAND, vec![Event::Paste(svg)]);
assert_eq!(app.session.active().unwrap().doc.layers[0].children().unwrap().len(), 3);
assert_ne!(last_object_bounds(&app), (10.0, 20.0), "Cmd+V pastes offset or centred");
}
#[test]
fn a_paste_takes_the_command_of_the_v_chord_held() {
let cmd = Modifiers::COMMAND;
assert_eq!(shortcuts::paste_command(cmd), "edit.paste");
assert_eq!(shortcuts::paste_command(cmd | Modifiers::SHIFT), "edit.pasteInPlace");
assert_eq!(shortcuts::paste_command(cmd | Modifiers::ALT), "edit.pasteWithoutFormatting");
assert_eq!(shortcuts::paste_command(cmd | Modifiers::ALT | Modifiers::SHIFT), "edit.pasteOnAllArtboards");
// The Paste key, and Shift+Insert, paste.
assert_eq!(shortcuts::paste_command(Modifiers::NONE), "edit.paste");
assert_eq!(shortcuts::paste_command(Modifiers::SHIFT), "edit.paste");
// What isn't text: only the key's release is reported, with the chord's modifiers.
let mut chord = shortcuts::PasteChord::default();
let release = Event::Key { key: Key::V, physical_key: None, pressed: false, repeat: false, modifiers: cmd | Modifiers::SHIFT };
assert_eq!(chord.textless_paste(&[release]), Some(cmd | Modifiers::SHIFT));
}
#[test]
fn no_two_commands_tools_or_panels_share_a_default_shortcut() {
assert_eq!(shortcut_editor::all_conflicts(&craft_keymap::Overrides::new()), vec![]);
}
/// egui-winit reports every Cmd+C chord as a Copy, Shift and Alt held too, never as its key: the
/// Cmd+Shift+C alignment fired on a Mac (the menu's key equivalent) but copied elsewhere (#1940).
#[test]
fn a_copy_with_more_modifiers_is_the_c_chord_held() {
use vectorcraft_doc::{Justify, NodeId, NodeKind};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "One\nTwo"})).unwrap()["id"].as_u64().unwrap();
let justify = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).justify).collect::<Vec<_>>(),
_ => panic!("text"),
};
let copied = |out: &egui::FullOutput| out.platform_output.commands.iter().any(|c| matches!(c, egui::OutputCommand::CopyText(_)));
app.run("select.set", json!({"ids": [id]})).unwrap();
let out = frame(&mut app, Modifiers::COMMAND | Modifiers::SHIFT, vec![Event::Copy]);
assert_eq!(justify(&app), [Justify::Center, Justify::Center]);
assert!(!copied(&out), "Cmd+Shift+C doesn't copy");
// Cmd+C still copies.
let out = frame(&mut app, Modifiers::COMMAND, vec![Event::Copy]);
assert!(copied(&out));
// While the Type tool edits text, it aligns the paragraph the caret is in.
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 5, "end": 5}));
assert!(app.session.tool_wants_text());
frame(
&mut app,
Modifiers::COMMAND | Modifiers::SHIFT,
vec![Event::Key { key: Key::R, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND | Modifiers::SHIFT }],
);
assert_eq!(justify(&app), [Justify::Center, Justify::Right]);
frame(&mut app, Modifiers::COMMAND | Modifiers::SHIFT, vec![Event::Copy]);
assert_eq!(justify(&app), [Justify::Center, Justify::Center]);
}