mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Isolation mode: breadcrumbs are clickable to navigate hierarchy levels (#4468)
This commit is contained in:
@@ -269,22 +269,34 @@ pub(super) fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.or_else(|| s.active().and_then(|d| d.selection.objects.first().copied()))
|
||||
.ok_or_else(|| bad("object.isolate", "missing id"))?;
|
||||
let guides_locked = s.menu.guides_locked;
|
||||
let (isolation, prev) = {
|
||||
let st = s.doc_mut()?;
|
||||
// Groups, layers and single objects (a path, a compound path, an image…); type isn't
|
||||
// isolated: a double-click on it edits its text.
|
||||
if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) {
|
||||
return Err(bad("object.isolate", "type objects can't be isolated"));
|
||||
}
|
||||
let isolation =
|
||||
st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
|
||||
// A hidden or locked object, one in a hidden or locked group or layer, and a guide path while
|
||||
// Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them.
|
||||
// A Shaper Group isolates its source group, so that group is the one checked.
|
||||
if !st.doc.is_editable(isolation) {
|
||||
return Err(bad("object.isolate", "the object is hidden or locked"));
|
||||
}
|
||||
if guides_locked && crate::is_guide_path(&st.doc, isolation) {
|
||||
return Err(EngineError::Other("guides are locked".into()));
|
||||
}
|
||||
(isolation, st.isolation)
|
||||
};
|
||||
if let Some(prev) = prev
|
||||
&& prev != isolation
|
||||
{
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
let st = s.doc_mut()?;
|
||||
crate::cmd::distortcmds::finish_edit_text(st, prev);
|
||||
}
|
||||
let st = s.doc_mut()?;
|
||||
// Groups, layers and single objects (a path, a compound path, an image…); type isn't
|
||||
// isolated: a double-click on it edits its text.
|
||||
if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) {
|
||||
return Err(bad("object.isolate", "type objects can't be isolated"));
|
||||
}
|
||||
let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
|
||||
// A hidden or locked object, one in a hidden or locked group or layer, and a guide path while
|
||||
// Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them.
|
||||
// A Shaper Group isolates its source group, so that group is the one checked.
|
||||
if !st.doc.is_editable(isolation) {
|
||||
return Err(bad("object.isolate", "the object is hidden or locked"));
|
||||
}
|
||||
if guides_locked && crate::is_guide_path(&st.doc, isolation) {
|
||||
return Err(EngineError::Other("guides are locked".into()));
|
||||
}
|
||||
st.isolation = Some(isolation);
|
||||
st.selection.clear();
|
||||
// New art on a locked artboard, now deselected, is protected.
|
||||
@@ -312,8 +324,11 @@ pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return ok();
|
||||
}
|
||||
let menu = s.menu.clone();
|
||||
let st = s.doc_mut()?;
|
||||
if let Some(i) = st.isolation.take() {
|
||||
let prev = s.active().and_then(|d| d.isolation);
|
||||
if let Some(i) = prev {
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
let st = s.doc_mut()?;
|
||||
st.isolation = None;
|
||||
crate::cmd::distortcmds::finish_edit_text(st, i);
|
||||
let target = st.doc.parent_of(i).filter(|p| st.doc.node(*p).is_some_and(|n| n.extras().shaper.is_some())).unwrap_or(i);
|
||||
st.selection.set([target]);
|
||||
@@ -322,6 +337,7 @@ pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The art on a locked artboard can't be selected.
|
||||
st.protect_locked_artboards(&[], &Default::default(), None);
|
||||
}
|
||||
let st = s.doc_mut()?;
|
||||
st.revision += 1;
|
||||
ok()
|
||||
}
|
||||
|
||||
@@ -158,6 +158,50 @@ fn edit_text_is_off_while_an_opacity_mask_is_edited() {
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolating_ancestor_after_undo_clears_editing_from_redo_history() {
|
||||
let (mut s, rect) = persp_session();
|
||||
let id = persp_text(&mut s);
|
||||
s.execute("select.set", &json!({"ids": [id.0, rect.0]})).unwrap();
|
||||
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
|
||||
// Start editing text in perspective and make an edit so undo/redo has entries
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("perspective.editText", &json!({})).unwrap();
|
||||
s.execute("text.editRange", &json!({"id": id.0, "start": 0, "end": 0, "insert": "Hello "})).unwrap();
|
||||
|
||||
// Undo the text edit so redo stack holds an entry while doc is not actively editing
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// Isolating the parent group must clear editing flag in redo entries too
|
||||
s.execute("object.isolate", &json!({"id": g})).unwrap();
|
||||
for e in &s.doc().unwrap().history.redo {
|
||||
if let Some(p) = e.doc.node(id).and_then(|n| n.extras().perspective.as_deref()) {
|
||||
assert!(!p.editing, "redo entry must not retain editing=true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolate_settles_pending_typing_interaction() {
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let (mut s, rect) = persp_session();
|
||||
let v = crate::ViewInfo::default();
|
||||
s.select_tool("type", v).unwrap();
|
||||
for k in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(k, 200.0, 200.0), v).unwrap();
|
||||
}
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
s.tool_text("abc", v).unwrap();
|
||||
assert!(s.in_interaction());
|
||||
|
||||
s.execute("object.isolate", &json!({"id": rect.0})).unwrap();
|
||||
assert!(!s.in_interaction(), "isolating another object settles typing interaction");
|
||||
let n = node(&s, id);
|
||||
let NodeKind::Text(ref t) = n.kind else { panic!("not text") };
|
||||
assert_eq!(t.plain_text(), "abc");
|
||||
}
|
||||
|
||||
/// Edit Text ending while a background save writes the file leaves the document saved once the
|
||||
/// file is written: the flat view is never saved. Saved while editing and edited after: undoing
|
||||
/// the edit once Edit Text has ended is back at what the file holds.
|
||||
|
||||
Reference in New Issue
Block a user