mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Guides: a Cmd/Ctrl+Shift double-click releases the guide under the pointer
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.
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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<Action> {
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user