mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Merge pull request #2785 from dotCooCoo/fix/panel-fields-shift-enter
Fields: Shift+Enter applies a pattern, layer, artboard or asset name and keeps the field active
This commit is contained in:
@@ -306,6 +306,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if editing == Some(i) {
|
||||
let id = ui.id().with(("ab-name", i));
|
||||
let mut buf: String = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| name.clone());
|
||||
let kept = widgets::own_field_shift_enter(ui, id, &mut buf, name);
|
||||
let te = ui.put(name_rect, egui::TextEdit::singleline(&mut buf).id(id).font(egui::FontId::proportional(12.0)));
|
||||
if !te.has_focus() && !te.lost_focus() {
|
||||
te.request_focus();
|
||||
@@ -317,6 +318,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if !ui.input(|i| i.key_pressed(egui::Key::Escape)) && !buf.trim().is_empty() && buf != *name {
|
||||
app.run("artboard.setProps", json!({"index": i, "name": buf.trim()})).ok();
|
||||
}
|
||||
} else if kept && !buf.trim().is_empty() && buf != *name {
|
||||
// Shift+Enter renames it and keeps the field open.
|
||||
app.run("artboard.setProps", json!({"index": i, "name": buf.trim()})).ok();
|
||||
}
|
||||
} else {
|
||||
ui.painter().text(name_rect.left_center(), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.5), t.text);
|
||||
@@ -660,6 +664,39 @@ mod tests {
|
||||
assert_eq!((app.session.tool_options()["active"].clone(), selected(&app, 2)), (json!(0), 0));
|
||||
}
|
||||
|
||||
/// Shift+Enter in a row's name field renames the artboard and keeps the field open, the new
|
||||
/// name selected; Enter then renames it again and closes the field.
|
||||
#[test]
|
||||
fn shift_enter_renames_an_artboard_and_keeps_the_field_open() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 150})).unwrap();
|
||||
app.session.execute("artboard.new", &json!({"x": 400, "y": 300, "width": 100, "height": 50})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let name = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards[1].name.clone();
|
||||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
// What a double-click on the second row's name does: its name field opens.
|
||||
set_pstate::<Option<usize>>(&ctx, "ab-edit", Some(1));
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("Cover".into())]);
|
||||
frame(&mut app, vec![key(egui::Key::Enter, egui::Modifiers::SHIFT)]);
|
||||
assert_eq!(name(&app), "Cover", "Shift+Enter applies the name");
|
||||
let field = ctx.memory(|m| m.focused()).expect("the field keeps the focus");
|
||||
assert_eq!(pstate::<Option<usize>>(&ctx, "ab-edit"), Some(1), "and stays open");
|
||||
frame(&mut app, vec![]);
|
||||
let selected = egui::TextEdit::load_state(&ctx, field).and_then(|st| st.cursor.char_range()).map(|r| r.as_sorted_char_range());
|
||||
assert_eq!(selected.map(|r| (r.start.0, r.end.0)), Some((0, 5)), "the name shown is selected");
|
||||
frame(&mut app, vec![egui::Event::Text("Back".into())]);
|
||||
frame(&mut app, vec![key(egui::Key::Enter, egui::Modifiers::NONE)]);
|
||||
frame(&mut app, vec![]);
|
||||
assert_eq!(name(&app), "Back", "typing replaced it, and Enter applies that");
|
||||
assert_eq!(pstate::<Option<usize>>(&ctx, "ab-edit"), None, "and closes the field");
|
||||
}
|
||||
|
||||
/// A row's options icon opens Artboard Options for that row's artboard and leaves the tool as
|
||||
/// it is (#1876): a click on the active artboard's icon, or a double-click on another row's
|
||||
/// icon, whose first click only makes that artboard active. With the Artboard tool in use, the
|
||||
|
||||
@@ -126,15 +126,22 @@ fn tiles(ui: &mut Ui, st: &DocState, sel: &[u64], ev: &mut Events) {
|
||||
match &renaming {
|
||||
Some((id, buf)) if *id == a.id => {
|
||||
let mut buf = buf.clone();
|
||||
let field = rename_id.with("field");
|
||||
let kept = widgets::own_field_shift_enter(ui, field, &mut buf, &a.name);
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
|
||||
let te = child.add(egui::TextEdit::singleline(&mut buf).desired_width(name_rect.width()).font(egui::FontId::proportional(11.5)));
|
||||
// Enter or a click elsewhere commits, Escape cancels.
|
||||
let te = child
|
||||
.add(egui::TextEdit::singleline(&mut buf).id(field).desired_width(name_rect.width()).font(egui::FontId::proportional(11.5)));
|
||||
// Enter or a click elsewhere commits, Escape cancels; Shift+Enter commits and
|
||||
// keeps the field open.
|
||||
if te.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<(u64, String)>(rename_id));
|
||||
if child.input(|i| !i.key_pressed(egui::Key::Escape)) && buf.trim() != a.name && !buf.trim().is_empty() {
|
||||
ev.rename = Some((a.id, buf.trim().to_string()));
|
||||
}
|
||||
} else {
|
||||
if kept && buf.trim() != a.name && !buf.trim().is_empty() {
|
||||
ev.rename = Some((a.id, buf.trim().to_string()));
|
||||
}
|
||||
te.request_focus();
|
||||
ui.data_mut(|d| d.insert_temp(rename_id, (a.id, buf)));
|
||||
}
|
||||
|
||||
@@ -953,8 +953,10 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
let name_end;
|
||||
match renaming {
|
||||
Some((rid, mut buf)) if rid == n.id.0 => {
|
||||
let field = rename_id().with("field");
|
||||
let kept = crate::widgets::own_field_shift_enter(ui, field, &mut buf, &name);
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
|
||||
let te = child.add(egui::TextEdit::singleline(&mut buf).desired_width(name_rect.width()).font(font.clone()));
|
||||
let te = child.add(egui::TextEdit::singleline(&mut buf).id(field).desired_width(name_rect.width()).font(font.clone()));
|
||||
// Focus the field as it opens. Asking every frame took the focus back from Enter,
|
||||
// Escape or a click elsewhere, so the rename never ended.
|
||||
if !te.has_focus() && !te.lost_focus() {
|
||||
@@ -966,6 +968,10 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf})));
|
||||
}
|
||||
} else {
|
||||
// Shift+Enter renames it and keeps the field open.
|
||||
if kept && buf != name {
|
||||
out.actions.push(("layer.setProps".into(), json!({"ids": [n.id.0], "name": buf.clone()})));
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, buf)));
|
||||
}
|
||||
name_end = name_rect.right();
|
||||
@@ -2018,6 +2024,35 @@ mod tests {
|
||||
assert_eq!(name(&app), "Sky", "Escape cancels");
|
||||
}
|
||||
|
||||
/// Shift+Enter in the name field renames the layer and keeps the field open, the new name
|
||||
/// selected; Enter then renames it again and closes the field.
|
||||
#[test]
|
||||
fn shift_enter_renames_a_layer_and_keeps_the_field_open() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
let layer = app.session.doc().unwrap().doc.layers[0].id;
|
||||
let name = |app: &VectorcraftApp| app.session.doc().unwrap().doc.layers[0].display_name();
|
||||
let ctx = egui::Context::default();
|
||||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
let field = rename_id().with("field");
|
||||
ctx.data_mut(|d| d.insert_temp(rename_id(), (layer.0, name(&app))));
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("Sky".into())], false);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::SHIFT)], false);
|
||||
assert_eq!(name(&app), "Sky", "Shift+Enter applies the name");
|
||||
assert!(ctx.memory(|m| m.has_focus(field)), "the field keeps the focus");
|
||||
assert!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id())).is_some(), "and stays open");
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
let selected = egui::TextEdit::load_state(&ctx, field).and_then(|st| st.cursor.char_range()).map(|r| r.as_sorted_char_range());
|
||||
assert_eq!(selected.map(|r| (r.start.0, r.end.0)), Some((0, 3)), "the name shown is selected");
|
||||
frame(&mut app, &ctx, vec![egui::Event::Text("Sea".into())], false);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)], false);
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
assert_eq!(name(&app), "Sea", "typing replaced it, and Enter applies that");
|
||||
assert!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id())).is_none(), "and closes the field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_open_rename_does_not_follow_into_another_document() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
@@ -43,16 +43,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
};
|
||||
let name = def.name.clone();
|
||||
// Name (commit on Enter / focus loss).
|
||||
// Name (commit on Enter / focus loss; Shift+Enter commits and keeps the focus).
|
||||
row(ui, tl!("Name:"), |ui| {
|
||||
let id = ui.id().with("po-name");
|
||||
let mut buf: String = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| name.clone());
|
||||
if !ui.memory(|m| m.has_focus(id)) {
|
||||
buf = name.clone();
|
||||
}
|
||||
let kept = widgets::own_field_shift_enter(ui, id, &mut buf, &name);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut buf).id(id).desired_width(150.0));
|
||||
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
|
||||
if r.lost_focus() && !buf.trim().is_empty() && buf != name {
|
||||
if (r.lost_focus() || kept) && !buf.trim().is_empty() && buf != name {
|
||||
set(app, &name, json!({"newName": buf.trim()}));
|
||||
}
|
||||
});
|
||||
@@ -231,6 +232,36 @@ mod tests {
|
||||
v
|
||||
}
|
||||
|
||||
/// Shift+Enter in the Name field renames the pattern and keeps the field active, the new name
|
||||
/// selected; Enter then renames it again and ends the editing.
|
||||
#[test]
|
||||
fn shift_enter_in_the_name_field_renames_the_pattern_and_keeps_the_focus() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
app.run("object.pattern.make", json!({"name": "Dots"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let named = |app: &VectorcraftApp, name: &str| app.session.active().unwrap().doc.pattern(name).is_some();
|
||||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
let at = texts.iter().find(|(t, _)| t == "Dots").expect("the Name field").1.center();
|
||||
let click = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at), click(true)]);
|
||||
frame(&mut app, &ctx, vec![click(false)]);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("Stripes".into())]);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::SHIFT)]);
|
||||
assert!(named(&app, "Stripes"), "Shift+Enter applies the name");
|
||||
let field = ctx.memory(|m| m.focused()).expect("the field keeps the focus");
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let selected = egui::TextEdit::load_state(&ctx, field).and_then(|st| st.cursor.char_range()).map(|r| r.as_sorted_char_range());
|
||||
assert_eq!(selected.map(|r| (r.start.0, r.end.0)), Some((0, 7)), "the name shown is selected");
|
||||
frame(&mut app, &ctx, vec![egui::Event::Text("Waves".into())]);
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)]);
|
||||
assert!(named(&app, "Waves"), "typing replaced it, and Enter applies that");
|
||||
assert!(ctx.memory(|m| m.focused()).is_none(), "Enter ends the editing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_swatch_bounds_toggles_the_pattern_option() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
@@ -161,6 +161,35 @@ fn renaming_an_asset_runs_assets_rename() {
|
||||
assert_eq!(assets(&mut app)[0]["name"], "Logo");
|
||||
}
|
||||
|
||||
/// Shift+Enter in an asset's name field renames it and keeps the field open, the new name
|
||||
/// selected; Enter then renames it again and closes the field.
|
||||
#[test]
|
||||
fn shift_enter_renames_an_asset_and_keeps_the_field_open() {
|
||||
let (mut app, _, [a, _]) = app(false);
|
||||
let id = app.run("assets.add", json!({"ids": [a]})).unwrap()["assets"][0].as_u64().unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let rename_id = egui::Id::new("asset-export-rename");
|
||||
let field = rename_id.with("field");
|
||||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
// What a double-click on the tile does: its name field opens with the name.
|
||||
ctx.data_mut(|d| d.insert_temp(rename_id, (id, "Asset 1".to_string())));
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("Logo".into())], false);
|
||||
frame(&ctx, &mut app, vec![key(egui::Key::Enter, egui::Modifiers::SHIFT)], false);
|
||||
assert_eq!(assets(&mut app)[0]["name"], "Logo", "Shift+Enter applies the name");
|
||||
assert!(ctx.memory(|m| m.has_focus(field)), "the field keeps the focus");
|
||||
assert!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id)).is_some(), "and stays open");
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let selected = egui::TextEdit::load_state(&ctx, field).and_then(|st| st.cursor.char_range()).map(|r| r.as_sorted_char_range());
|
||||
assert_eq!(selected.map(|r| (r.start.0, r.end.0)), Some((0, 4)), "the name shown is selected");
|
||||
frame(&ctx, &mut app, vec![egui::Event::Text("Mark".into())], false);
|
||||
frame(&ctx, &mut app, vec![key(egui::Key::Enter, egui::Modifiers::NONE)], false);
|
||||
assert_eq!(assets(&mut app)[0]["name"], "Mark", "typing replaced it, and Enter applies that");
|
||||
assert!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id)).is_none(), "and closes the field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rows_are_the_shared_export_settings() {
|
||||
let (mut app, log, [a, b]) = app(true);
|
||||
|
||||
@@ -423,6 +423,20 @@ fn after_shift_enter(ui: &Ui, id: egui::Id) -> bool {
|
||||
ui.data_mut(|d| d.remove_temp::<bool>(id.with("shift-enter"))).unwrap_or(false) && ui.memory(|m| m.has_focus(id))
|
||||
}
|
||||
|
||||
/// Handles Shift+Enter in a single-line panel field that draws its own text box `id` (Pattern
|
||||
/// Options' Name, the inline renames in the Layers, Artboards and Asset Export panels). Call it
|
||||
/// before the box is drawn, with `buf` the text the box will show and `shown` the value as it is
|
||||
/// now. When the field took Shift+Enter in the last frame, `buf` becomes `shown`, all of it
|
||||
/// selected. Returns whether Shift+Enter was pressed in this frame ([`shift_enter_keeps_focus`]):
|
||||
/// the field then applies `buf` as Enter does and keeps the focus.
|
||||
pub(crate) fn own_field_shift_enter(ui: &Ui, id: egui::Id, buf: &mut String, shown: &str) -> bool {
|
||||
if after_shift_enter(ui, id) {
|
||||
*buf = shown.to_string();
|
||||
select_all(ui, id, buf);
|
||||
}
|
||||
shift_enter_keeps_focus(ui, id)
|
||||
}
|
||||
|
||||
/// Numeric fields select the whole text on keyboard focus, a completed focus click or a
|
||||
/// double-click. A pointer press keeps its caret so dragging selects from the pressed position,
|
||||
/// and a short drag the toolkit still counts as a click keeps it too (#1326): only a tap
|
||||
|
||||
@@ -481,6 +481,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423).
|
||||
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). A dialog opened from a menu takes the keyboard focus every time it opens, not only the first (#1367), and one whose first field starts greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the focus to that field once it is enabled (#1395). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
|
||||
- In the number, text and color fields of the panels and the Control bar, Shift+Enter (Shift+Return) applies the value and keeps the field active with its value selected; a text box of several lines takes it as a line break. In a dialog, Shift+Enter applies the value and confirms the dialog, as Enter does.
|
||||
- The Pattern Options panel's Name field and the name fields that a double-click opens in the Layers, Artboards and Asset Export panels take Shift+Enter (Shift+Return) the same way: it applies the name and keeps the field active with the name selected.
|
||||
- Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off.
|
||||
- In the Transform panel, the Properties panel's Transform section and the Control bar, Alt+Enter (Option+Return) applies a value to a copy of the selection and selects the copy, in one undo step. With anchor points selected with Direct Selection it applies the value to them, as Enter does. Ctrl/Cmd+Enter in W or H applies the value and scales the selection proportionally with the link off. With the Artboard tool, it scales the active artboard proportionally. A percentage typed alone in W or H (`50%`) is that share of the size the field shows, and the link and Ctrl/Cmd+Enter keep the proportions as with a length (#2401).
|
||||
- The Control bar's underlined Transform link opens the Transform panel in a popover under it (reference point, X/Y/W/H, Rotate and Shear with their icons and angle presets, flips, shape properties, Scale Corners and Scale Strokes & Effects), as its Stroke link does the Stroke panel; X/Y/W/H also show inline while the bar has room. The reference point locator is nine squares, the outer eight joined around the box.
|
||||
|
||||
Reference in New Issue
Block a user