From 4f1e4979fc14fb1811606e745904925c4724fe16 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 16 Oct 2026 10:09:21 -0700 Subject: [PATCH] Layers: the panel menu's Save Selection submenu saves, lists and updates saved selections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Layers panel menu had no saved-selection items, and the Select menu was the only menu that listed saved selections. The reference app's help also names a Save Selection menu in the Layers panel that lists the saved selections and holds Save Selection, Update Selection and Edit Selection. The Layers panel menu now has a Save Selection submenu after Locate Object. It holds Save Selection…, Update Selection and Edit Selection…, which are enabled and run as in the Select menu, and below them the active document's saved selections. Choosing a name runs select.recall with that name, as the Select menu's list does. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/panels/layers.rs | 69 +++++++++++++++++++++++++++++ docs/mcp.md | 3 +- docs/target-app-parity.md | 3 +- 12 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index bc82fa3df..64e3eddde 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2537,6 +2537,7 @@ mark-style Japanese Japanisch Save Recovery Data Wiederherstellungsdaten speichern Save Save for Web Preset Vorgabe für „Für Web speichern“ speichern Save selected colors as swatches Ausgewählte Farben als Farbfelder speichern + Save Selection Auswahl speichern Save Selection… Auswahl speichern… Update Selection Auswahl aktualisieren Save Swatch Library Farbfeldbibliothek speichern diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 775a6771e..76cb21ce7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2588,6 +2588,7 @@ mark-style Japanese Giapponese Save Recovery Data Salva dati di recupero Save Save for Web Preset Salva preset di Salva per il web Save selected colors as swatches Salva i colori selezionati come campioni + Save Selection Salva selezione Save Selection… Salva selezione… Update Selection Aggiorna selezione Save Swatch Library Salva libreria campioni diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e2ae322f1..cc3a7641a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -396,6 +396,7 @@ Point Text Objects ポイント文字オブジェクト Area Text Objects エリア内文字オブジェクト Start Global Edit グローバル編集を開始 + Save Selection 選択範囲を保存 Save Selection… 選択範囲を保存… Update Selection 選択範囲を更新 Edit Selection… 選択範囲を編集… diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5535ab8c2..38fe2e6d2 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -392,6 +392,7 @@ Point Text Objects 포인트 문자 개체 Area Text Objects 영역 문자 개체 Start Global Edit 전체 편집 시작 + Save Selection 선택 영역 저장 Save Selection… 선택 영역 저장… Update Selection 선택 영역 업데이트 Edit Selection… 선택 영역 편집… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index fccc7dd0f..2c0aa1adc 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2584,6 +2584,7 @@ mark-style Japanese Японский Save Recovery Data Сохранить данные восстановления Save Save for Web Preset Сохранить набор «Сохранить для web» Save selected colors as swatches Сохранить выбранные цвета как образцы + Save Selection Сохранить выделение Save Selection… Сохранить выделение… Update Selection Обновить выделение Save Swatch Library Сохранить библиотеку образцов diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ef756aed6..315084df3 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2580,6 +2580,7 @@ mark-style Japanese Японський Save Recovery Data Зберегти дані відновлення Save Save for Web Preset Зберегти набір «Зберегти для Інтернету» Save selected colors as swatches Зберегти вибрані кольори як зразки + Save Selection Зберегти виділення Save Selection… Зберегти виділення… Update Selection Оновити виділення Save Swatch Library Зберегти бібліотеку зразків diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index a6f53ab87..8780d3baf 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2873,6 +2873,7 @@ mark-style Japanese Kiểu Nhật Save Recovery Data Lưu dữ liệu khôi phục Save Save for Web Preset Lưu thiết lập sẵn Lưu cho Web Save selected colors as swatches Lưu màu đã chọn thành mẫu màu + Save Selection Lưu vùng chọn Save Selection… Lưu vùng chọn… Update Selection Cập nhật vùng chọn Save Swatch Library Lưu thư viện mẫu màu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 686f5b481..5f0ffca7d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2463,6 +2463,7 @@ mark-style Japanese 日式 Save Recovery Data 保存恢复数据 Save Save for Web Preset 保存 Web 预设 Save selected colors as swatches 将所选颜色存储为色板 + Save Selection 保存选区 Save Selection… 保存选区… Update Selection 更新选区 Save Swatch Library 存储色板库 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ebe001a64..80c9dfa94 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2562,6 +2562,7 @@ mark-style Japanese 日式 Save Recovery Data 儲存救援資料 Save Save for Web Preset 儲存「儲存為網頁用」預設集 Save selected colors as swatches 將所選顏色儲存為色票 + Save Selection 儲存選取範圍 Save Selection… 儲存選取範圍… Update Selection 更新選取範圍 Save Swatch Library 儲存色票資料庫 diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 7fe651ced..8966ef3dd 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -1497,6 +1497,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { items.extend([ None, Some(("Locate Object", "layer.locate", json!({}), has_sel, false)), + // Sentinel: rendered as the Save Selection submenu (the Select menu's saved-selection items). + Some(("Save Selection", "", json!({}), true, false)), None, Some(("Merge Selected", "layer.merge", json!({}), rows.len() >= 2, false)), Some(("Flatten Artwork", "layer.flatten", json!({}), true, false)), @@ -1551,6 +1553,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { app.run("assets.add", p).ok(); }); } + Some(("Save Selection", "", ..)) => save_selection_menu(app, ui), Some((label, cmd, p, enabled, checked)) => { if widgets::menu_item(ui, label, enabled, checked) { app.run(cmd, p).ok(); @@ -1577,6 +1580,29 @@ fn collect_for_export_menu(ui: &mut Ui, enabled: bool, mut run: impl FnMut(bool) }); } +/// Save Selection › Save Selection…, Update Selection and Edit Selection…, enabled and run as in +/// the Select menu, then the active document's saved selections: choosing one recalls it. +fn save_selection_menu(app: &mut VectorcraftApp, ui: &mut Ui) { + ui.menu_button(format!(" {}", tl!("Save Selection")), |ui| { + for (label, id) in [("Save Selection…", "select.save"), ("Update Selection", "select.update"), ("Edit Selection…", "select.editSaved")] { + if widgets::menu_item(ui, label, crate::menus::enabled(app, id), false) { + crate::menus::invoke(app, id, json!({})); + ui.close(); + } + } + let names: Vec = app.session.active().map(|d| d.doc.saved_selections.iter().map(|s| s.name.clone()).collect()).unwrap_or_default(); + if !names.is_empty() { + ui.separator(); + } + for name in names { + if widgets::menu_item_name(ui, &name, true, false) { + app.run("select.recall", json!({ "name": name })).ok(); + ui.close(); + } + } + }); +} + /// `ui.layersExpand {ids?, open?}`: open (or close) rows of the active document's Layers panel; /// without `ids`, every row that holds others. pub fn expand(app: &mut VectorcraftApp, p: &Value) -> Result { @@ -2540,6 +2566,7 @@ mod tests { "Make/Release Clipping Mask", "Enter Isolation Mode", "Locate Object", + "Save Selection", "Merge Selected", "Flatten Artwork", "Collect in New Layer", @@ -2558,6 +2585,48 @@ mod tests { } } + /// ≡ › Save Selection lists the saved selections: choosing one recalls it, Update Selection + /// makes it hold the selected objects, and Save Selection… opens the name dialog. + #[test] + fn the_save_selection_submenu_recalls_updates_and_saves_selections() { + let (mut app, _, [a, b]) = two_rects(); + app.run("select.set", json!({"ids": [a.0]})).unwrap(); + app.run("select.save", json!({"name": "Left"})).unwrap(); + app.run("select.set", json!({"ids": [b.0]})).unwrap(); + app.run("select.save", json!({"name": "Right"})).unwrap(); + app.run("select.none", json!({})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let frame = |app: &mut VectorcraftApp, events: Vec| super::super::tests_appearance::frame_events(&ctx, app, events, menu); + // Opens the submenu (when closed) and clicks the item labeled `label` in it. + let choose = |app: &mut VectorcraftApp, label: &str| { + let at = |texts: &[(String, egui::Rect)], label: &str| texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()); + let mut texts = frame(app, vec![]); + if at(&texts, label).is_none() { + let sub = at(&texts, "Save Selection").expect("the Save Selection submenu"); + for e in [egui::Event::PointerMoved(sub), button(sub, true), button(sub, false)] { + frame(app, vec![e]); + } + texts = frame(app, vec![]); + } + let item = at(&texts, label).unwrap_or_else(|| panic!("{label} missing from {texts:?}")); + for e in [egui::Event::PointerMoved(item), button(item, true), button(item, false)] { + frame(app, vec![e]); + } + }; + let selected = |app: &VectorcraftApp| app.session.active().unwrap().selection.objects.clone(); + choose(&mut app, "Left"); + assert_eq!(selected(&app), vec![a], "choosing a name recalls it"); + app.run("select.set", json!({"ids": [a.0, b.0]})).unwrap(); + choose(&mut app, "Update Selection"); + let saved = app.session.active().unwrap().doc.saved_selections.clone(); + assert_eq!(saved.iter().map(|s| (s.name.as_str(), s.objects.len())).collect::>(), vec![("Left", 2), ("Right", 1)]); + assert!(app.ui.dialog.is_none()); + choose(&mut app, "Save Selection…"); + let d = app.ui.dialog.clone().expect("Save Selection dialog"); + assert_eq!((d.str("__command"), d.str("name")), ("select.save".into(), "Selection 1".into())); + } + /// With the dock collapsed, Layers shows in a floating flyout: its rows work there too. #[test] fn rows_work_in_the_floating_flyout() { diff --git a/docs/mcp.md b/docs/mcp.md index c3d250d2b..82378d4c7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1042,7 +1042,8 @@ recalled. Edit Selection… renames and deletes: `select.editSaved {name, newName?, delete?}`, or several at once as `{edits: [{name, newName?, delete?}…]}` in one undo step, where every `name` is the name before the edit and the names must stay unique. In the desktop app the saved selections are listed at the bottom of the Select menu -(`select.recall1` … `select.recall25`). A file's saved selections are checked when it opens: past 25, blank or +(`select.recall1` … `select.recall25`) and in the Layers panel menu's Save Selection submenu +(`select.recall {name}`). A file's saved selections are checked when it opens: past 25, blank or repeated names, and ids the document doesn't have are dropped. ## Ruler guides diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index bed3eefcf..e669d9566 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -484,7 +484,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Clear deletes individual selected compound-path members without deleting their siblings, and explicit object ids work without a selection; Cut still removes the same objects it copies (#786). A command that leaves out an object selected inside a selected group also leaves it out when `ids` list it together with a group that holds it. Make Repeat and Live Paint no longer fail on such a list, New Symbol no longer stores the object twice, and `object.transform` no longer moves it twice. - **Compound paths:** Make keeps the backmost object's appearance and each contour; selecting a member in Direct Selection reads and changes the compound's fill rule in Attributes. Direction remains a per-member edit. Rendered holes, unchanged contour geometry, no-op history and Undo/Redo are covered by `engine/tests/compound_attributes.rs` (#1197). Behaviour reference: [public compound-path documentation](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-paths.html). Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537). - - **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: the Layers panel's saved-selection menu. + - **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Select ▸ Update Selection replaces the objects of the saved selection last recalled with the selected objects, in one undo step that marks the document changed (`select.update`). After Edit Selection… renames that saved selection, Update Selection replaces it under the new name; after Edit Selection… deletes it, Update Selection is disabled until another saved selection is recalled or the delete is undone. Undo and Redo do not change which saved selection is recalled. - **Drawing modes:** in Draw Inside, a double-click with a selection tool outside the object being drawn into sets the drawing mode to Draw Normal, as Shift+D does. - **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661). @@ -517,6 +517,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Releasing ruler guides:** View › Guides › Release Guides turns the selected ruler guides into paths with no fill or stroke. An artboard guide becomes a line across its artboard, and a canvas guide becomes a line across the artboards (`view.guides.release {index?, ids?}`). - **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies) and a dimmed copy of the row under the pointer (dropped on the trash rows are deleted, on New Layer duplicated, as in Illustrator), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets, each with a chip of its colour, or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting. A document opens with only its top-level layers open; the rows left open are saved in the native file (nothing is written for the default) and reopen that way. Only the rows scrolled into view are laid out, from a row list made once per change: a layer of 5,000 groups of 10 paths, all open, draws the panel in 0.4 ms a frame instead of about 29 s (#638). The Filter button at the right of Search All keeps the rows that are of an object kind ticked under By Object (paths, shapes, text, images, effects, groups, linked files, guides) and in a state ticked under By Layer (locked, hidden), with the rows that hold them; a section with nothing ticked takes every row. The filter combines with the search, the button is lit while a filter is on, and Clear All in its popup turns the filter off (`ui.layersFilter`). The filter lasts the session. + The panel menu's Save Selection submenu has Save Selection…, Update Selection and Edit Selection…, enabled and run as in the Select menu, and lists the document's saved selections: choosing one selects its objects again (`select.recall`). `object.setProps` with `visible: false` or `locked: true` deselects every selected object that is then hidden or locked, the key object included, as the eye and lock columns do. Ctrl/Cmd+Alt-clicking an eye switches the other layers to Outline and the row's layer to Preview, or switches all layers back to Preview when the others are in Outline (`layer.outlineOthers`, `layer.previewAll`). Object › Arrange › Send to Current Layer returns the error "the target layer is locked or hidden" and leaves the selected objects where they are when the current layer is hidden or locked, or is inside a hidden or locked layer.