Merge pull request #2235 from Messhias/fix/2094-create-outlines-shortcut

Fix #2094: Cmd+Shift+O Create Outlines while typing
This commit is contained in:
BFlat
2026-10-16 02:34:55 +07:00
committed by GitHub
2 changed files with 62 additions and 3 deletions
+1 -2
View File
@@ -4137,8 +4137,7 @@ mod tests {
Item::Cmd(_, id, _)
if vectorcraft_engine::find_command(id).is_none()
&& !UI_COMMANDS.iter().any(|c| c.0 == *id)
&& !id.starts_with("object.path.")
&& *id != "type.createOutlines" =>
&& !id.starts_with("object.path.") =>
{
bad.push(id.to_string());
}
+61 -1
View File
@@ -173,18 +173,26 @@ pub(crate) fn keep_marked_text(app: &mut VectorcraftApp) {
}
}
/// Cmd/Ctrl chords are shortcuts (Create Outlines, Bold, …): their letter is never typed into the
/// text if a host still emits `Text` for them (#2094). Alt is left to type: Option+key on macOS
/// and AltGr (Ctrl+Alt) on Windows keyboards type characters such as é, ø and @.
fn is_shortcut_chord(m: egui::Modifiers) -> bool {
(m.command || m.ctrl) && !m.alt
}
/// Typed text and IME events, in the order they came, to the Type tool.
fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) {
let view = app.view_info();
// Switching apps mid-composition needs nothing: the macOS IME keeps its marked text and goes
// on with it when the window comes back (measured with Kotoeri), as the tool does.
let shortcut_mods = ctx.input(|i| is_shortcut_chord(i.modifiers));
let events: Vec<egui::Event> =
ctx.input(|i| i.events.iter().filter(|e| matches!(e, egui::Event::Text(_) | egui::Event::Ime(_))).cloned().collect());
for e in events {
// Errors are already reported by the engine's interaction (a failed preview keeps it).
let _ = match e {
// Keys the IME passed through come as text only when nothing is marked.
egui::Event::Text(t) if !app.session.tool_composing() => app.session.tool_text(&t, view),
egui::Event::Text(t) if !app.session.tool_composing() && !shortcut_mods => app.session.tool_text(&t, view),
egui::Event::Ime(egui::ImeEvent::Preedit { text, active_range_chars }) => {
app.ime_marked = Some(text.clone()).filter(|t| !t.is_empty());
app.session.tool_preedit(&text, active_range_chars, view)
@@ -532,6 +540,58 @@ mod tests {
out
}
#[test]
fn option_and_altgr_still_type_but_cmd_and_ctrl_chords_do_not() {
use egui::Modifiers as M;
assert!(is_shortcut_chord(M::COMMAND | M::SHIFT));
assert!(is_shortcut_chord(M::CTRL));
assert!(!is_shortcut_chord(M::NONE));
assert!(!is_shortcut_chord(M::SHIFT));
assert!(!is_shortcut_chord(M::ALT), "Option+key types é, ø on macOS");
assert!(!is_shortcut_chord(M::CTRL | M::ALT), "AltGr types @ on a German keyboard");
}
/// #2094: Cmd+Shift+O is Type › Create Outlines (not File › Open's Cmd+O, not the Artboard
/// tool's Shift+O), with the Selection tool or while the Type tool edits the text.
#[test]
fn cmd_shift_o_creates_outlines() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
assert_eq!(crate::menus::shortcut_of("type.createOutlines"), Some("Cmd+Shift+O"));
let open = parse("Cmd+O").unwrap();
let outlines = parse("Cmd+Shift+O").unwrap();
let pos = |want: egui::KeyboardShortcut, id: &str| super::all_shortcuts().iter().position(|(sc, i, _)| *i == id && *sc == want);
assert!(pos(outlines, "type.createOutlines").unwrap() < pos(open, "file.open").unwrap(), "more modifiers win so Open does not eat the chord");
let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT;
let chord = egui::Event::Key { key: Key::O, physical_key: None, pressed: true, repeat: false, modifiers: cmd_shift };
let is_text = |app: &VectorcraftApp, id: u64| {
matches!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_)))
};
let group_kids = |app: &VectorcraftApp| -> usize {
let id = app.session.active().unwrap().selection.objects.first().copied().unwrap();
match &app.session.doc().unwrap().doc.node(id).unwrap().kind {
vectorcraft_doc::NodeKind::Group { children, .. } => children.len(),
_ => 0,
}
};
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
assert!(is_text(&app, id));
frame(&mut app, vec![chord.clone()]);
assert!(!is_text(&app, id), "Selection tool: the chord outlines the text");
assert_eq!(group_kids(&app), 2, "H and i");
assert_ne!(app.session.tool_id(), "artboard", "not the Artboard tool's Shift+O");
assert!(app.ui.dialog.is_none(), "not File › Open");
// While the Type tool edits, the same chord outlines; a Text event with Cmd held must not type.
let id = app.session.execute("text.create", &json!({"x": 20, "y": 80, "text": "Yo"})).unwrap()["id"].as_u64().unwrap();
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
assert!(app.session.tool_wants_text());
frame(&mut app, vec![egui::Event::ModifiersChanged(cmd_shift), chord, egui::Event::Text("o".into())]);
assert!(!is_text(&app, id), "Type tool: the chord outlines the text being edited");
assert_eq!(group_kids(&app), 2, "Yo only: the stray Text with Cmd was not typed");
assert!(app.ui.dialog.is_none(), "not File › Open while typing");
}
/// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724).
#[test]
fn cmd_shift_b_while_typing_is_bold_not_the_bounding_box() {