From b275eb04d08a1f287e9e8292802f834167b06c19 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 16 Oct 2026 08:00:29 -0700 Subject: [PATCH] Guides: a Cmd/Ctrl+Shift double-click releases the guide under the pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A double-click on a ruler guide with the Selection, Direct Selection or Group Selection tool did nothing, whatever keys were held. A guide could be released only with View › Guides › Release Guides. With the Selection, Direct Selection or Group Selection tool, a double-click with Cmd/Ctrl and Shift held now runs view.guides.release on the ruler guide under the pointer (index) or, when no ruler guide is there, on the guide path under it (ids). Nothing is released while guides are hidden or locked, and a double-click without both keys does what it did before. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/tools/src/direct.rs | 36 ++++++++++++++++++++++++++- crates/tools/src/select.rs | 50 +++++++++++++++++++++++++++++++++++++- docs/mcp.md | 5 +++- docs/target-app-parity.md | 2 +- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index f45434f4c..b830238bf 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -17,6 +17,8 @@ //! paths and groups outside it stay put. //! Both move objects selected as a whole as the Selection tool does, snapping alike. //! Both pick the key objects of a blend, and click or drag ruler guides ([`crate::rulerguide`]). +//! A Cmd/Ctrl+Shift double-click with either one releases the ruler guide or guide path under the +//! pointer ([`release_guide_at`]). //! Direct Selection also edits a blend's spine: drag its points (a key object on a point moves //! with it) and, once a point is clicked, its handles; and the points of selected gradient meshes //! and mesh envelopes and their handles ([`MeshEdit`]; a solid fill colour recolours a gradient @@ -41,7 +43,7 @@ use crate::guides::{HandleSnap, Leave, PointSnap, Targets}; use crate::meshedit::MeshEdit; use crate::pathtype::{self, BracketDrag, over_bracket}; use crate::rulerguide::GuideEdit; -use crate::select::{MoveSnap, is_area_type, matrix_json}; +use crate::select::{MoveSnap, is_area_type, matrix_json, release_guide_at}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; #[derive(Clone, Debug)] @@ -444,11 +446,15 @@ impl Tool for DirectSelectionTool { vec![Action::Exec("select.set".into(), json!({"ids": [target.0]}))] } } + (PointerKind::DoubleClick, _) if self.group => release_guide_at(cx, p, ev.mods).into_iter().collect(), (PointerKind::DoubleClick, _) if !self.group => { self.state = State::Idle; if let Some(a) = corners::double_click(cx, p, true) { return vec![a]; } + if let Some(a) = release_guide_at(cx, p, ev.mods) { + return vec![a]; + } if crate::rulerguide::guide_at(cx, p).is_some() { return vec![]; } @@ -985,6 +991,34 @@ mod tests { assert_eq!(down(&mut t, 100.0), vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))]); } + /// A Cmd/Ctrl+Shift double-click with Direct Selection or Group Selection releases the ruler + /// guide or the guide path under the pointer; a plain double-click on a ruler guide, or one while + /// guides are hidden or locked, releases nothing. + #[test] + fn a_cmd_shift_double_click_releases_the_guide_under_the_pointer() { + let (mut d, id) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(false, 300.0)); + let Some(NodeKind::Path { guide, .. }) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("the rectangle is a path") }; + *guide = true; + let (s, p) = (Selection::default(), paint()); + let both = Mods { cmd: true, shift: true, ..Mods::default() }; + let index = vec![Action::Exec("view.guides.release".into(), json!({"index": 0}))]; + let ids = vec![Action::Exec("view.guides.release".into(), json!({"ids": [id.0]}))]; + let locked = ToolContext { guides: false, ..cx(&d, &s, &p) }; + for group in [false, true] { + let dbl = |c: &ToolContext, x: f64, y: f64, m: Mods| { + DirectSelectionTool::new(group).pointer(c, &PointerEvent::new(PointerKind::DoubleClick, x, y).with_mods(m)) + }; + let c = cx(&d, &s, &p); + assert_eq!(dbl(&c, 350.0, 301.0, both), index, "on the ruler guide (group: {group})"); + assert_eq!(dbl(&c, 200.0, 150.0, both), ids, "on the guide path (group: {group})"); + assert!(dbl(&c, 350.0, 301.0, Mods::default()).is_empty(), "a plain double-click (group: {group})"); + assert!(dbl(&c, 350.0, 301.0, Mods { cmd: true, ..Mods::default() }).is_empty(), "Cmd/Ctrl alone (group: {group})"); + assert!(dbl(&locked, 350.0, 301.0, both).is_empty(), "guides locked (group: {group})"); + assert!(dbl(&locked, 200.0, 150.0, both).is_empty(), "guides locked (group: {group})"); + } + } + /// An anchor is picked from 2 px past its drawn square even where its segment is nearer, and /// further with a larger anchor Size or Tolerance (#593); away from it the segment is. #[test] diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index b087256f8..72c169c49 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -6,7 +6,8 @@ //! double-click opens the Corners dialog), drag a live ellipse's pie widgets to set its pie angles //! (double-click makes it whole again: [`crate::pie`]), double-click a group or an object to enter isolation //! mode (Double Click To Isolate), Cmd/Ctrl-click to select the object behind (Command Click to Select Objects Behind), -//! click or drag a ruler guide ([`crate::rulerguide`]), drag the brackets of type on a path +//! click or drag a ruler guide ([`crate::rulerguide`]), Cmd/Ctrl+Shift-double-click a ruler guide or +//! a guide path to release it ([`release_guide_at`]), drag the brackets of type on a path //! ([`crate::pathtype`]), double-click the type widget to convert point type to area type and back //! ([`crate::typewidget`]), click a Shaper Group's mode widget to switch between Face Selection and //! Construction mode ([`crate::shaperwidget`]). @@ -315,6 +316,9 @@ impl Tool for SelectionTool { if shaperwidget::over_widget(cx, p) { return vec![]; } + if let Some(a) = release_guide_at(cx, p, m) { + return vec![a]; + } if crate::rulerguide::guide_at(cx, p).is_some() { return vec![]; } @@ -643,6 +647,22 @@ pub fn isolates(kind: &vectorcraft_doc::NodeKind) -> bool { matches!(kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_)) } +/// A Cmd/Ctrl+Shift double-click at `p` with a selection tool releases the guide there: the ruler +/// guide under the pointer (`view.guides.release {index}`), else the guide path hit there +/// (`view.guides.release {ids}`). It returns None without both keys, while guides are hidden or +/// locked, or when no guide is at `p`. +pub(crate) fn release_guide_at(cx: &ToolContext, p: Point, m: Mods) -> Option { + if !(m.cmd && m.shift && cx.guides) { + return None; + } + if let Some(i) = crate::rulerguide::guide_at(cx, p) { + return Some(Action::Exec("view.guides.release".into(), json!({ "index": i }))); + } + let leaf = hit_test(cx.doc, p, cx.hit_options())?.leaf; + let guide = cx.doc.node(leaf).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Path { guide: true, .. })); + guide.then(|| Action::Exec("view.guides.release".into(), json!({ "ids": [leaf.0] }))) +} + /// The resize cursor for handle `h` of a box turned by `angle` (counter-clockwise degrees): the /// handle that sits where `h` appears on screen, in 45° steps. fn handle_cursor(h: Handle, angle: f64) -> Cursor { @@ -787,6 +807,34 @@ mod tests { assert!(t.pointer(&cx, &ev(PointerKind::DoubleClick, 160.0, 150.0)).is_empty()); } + /// A Cmd/Ctrl+Shift double-click releases the ruler guide or the guide path under the pointer; + /// without both keys, or while guides are hidden or locked, nothing is released. + #[test] + fn a_cmd_shift_double_click_releases_the_guide_under_the_pointer() { + let (mut d, id) = doc_with_rect(); + d.guides.push(vectorcraft_doc::Guide::new(false, 300.0)); + let Some(vectorcraft_doc::NodeKind::Path { guide, .. }) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("the rectangle is a path") }; + *guide = true; + let s = Selection::default(); + let p = paint(); + let both = Mods { cmd: true, shift: true, ..Mods::default() }; + let dbl = |c: &ToolContext, x: f64, y: f64, m: Mods| SelectionTool::default().pointer(c, &ev(PointerKind::DoubleClick, x, y).with_mods(m)); + let releases = |a: &[Action]| a.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "view.guides.release")); + let c = cx(&d, &s, &p); + assert_eq!(dbl(&c, 350.0, 301.0, both), vec![Action::Exec("view.guides.release".into(), json!({"index": 0}))]); + assert_eq!(dbl(&c, 200.0, 150.0, both), vec![Action::Exec("view.guides.release".into(), json!({"ids": [id.0]}))]); + assert!(dbl(&c, 350.0, 301.0, Mods::default()).is_empty(), "a plain double-click on a ruler guide"); + for m in [Mods { cmd: true, ..Mods::default() }, Mods { shift: true, ..Mods::default() }, Mods::default()] { + assert!(!releases(&dbl(&c, 200.0, 150.0, m)), "{m:?}"); + } + let locked = ToolContext { guides: false, ..cx(&d, &s, &p) }; + assert!(!releases(&dbl(&locked, 350.0, 301.0, both))); + assert!(!releases(&dbl(&locked, 200.0, 150.0, both))); + // A double-click on art that is not a guide does not release it. + let (plain, _) = doc_with_rect(); + assert!(!releases(&dbl(&cx(&plain, &s, &p), 200.0, 150.0, both))); + } + /// Just outside each corner of the box the rotate cursor bows toward that corner, its arc /// opening toward the box: at the top right it opens to the bottom left. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 82378d4c7..83a299e70 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1067,7 +1067,10 @@ Otherwise it releases the selected guide paths and ruler guides, and with no gui path. A released ruler guide becomes a straight path with no fill or stroke, at the top of the guide's layer (or where new art goes, for a guide on no layer). An artboard guide becomes a line across its artboard, and a canvas guide becomes a line across the bounds of all the artboards. `index` and `ids` are refused while guides are locked. The -release is one undo step, and the released paths are selected. +release is one undo step, and the released paths are selected. With the Selection, Direct Selection or Group +Selection tool, a double-click with Cmd/Ctrl and Shift held releases the ruler guide or guide path under the pointer +(`pointer_gesture` with a `doubleclick` event and `mods: {cmd: true, shift: true}`), unless guides are hidden or +locked. The Selection, Direct Selection and Group Selection tools pick a guide within the selection tolerance, over the art (an anchor on it comes first with Direct Selection), and drag the selected guides: `mods.alt` copies them, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e669d9566..bde4539a5 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -514,7 +514,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Keep selected in the Pencil and Paintbrush Tool Options is on by default; while it is off, the path a stroke draws or continues is not selected afterwards and the rest of the selection stays as it was (`path.freehand` `keepSelected`). The Blob Brush Tool Options have Keep selected and Merge only with selection, both off by default: while Keep selected is off, the blob a stroke draws or merges into is not selected afterwards, and while Merge only with selection is on, a stroke merges only with selected blobs of its color that it touches (`path.blob` `keepSelected` and `mergeOnlySelection`). - **Ruler guides as layer objects:** a guide goes on the current layer, shows, hides, locks and is deleted with it, is listed in the Layers panel (its row selects it) and moves to another layer (`guide.setLayer`, or its row dragged onto that layer's rows); guides on a hidden or locked layer can't be selected, moved or deleted, Duplicate copies a layer's guides and Merge Selected and Flatten Artwork keep them on the merged layer; guides from files saved before guides had layers go on the top visible, unlocked layer when the document opens. Not done: guides stacked among the art of their layer (they are listed above it), a guide row's own eye and lock. Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534). The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score. -- **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?}`). +- **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?}`). A Cmd/Ctrl+Shift double-click with the Selection, Direct Selection or Group Selection tool releases the ruler guide or guide path under the pointer. - **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`).