mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Merge pull request #2258 from dotCooCoo/feat/control-bar-isolation
Control bar: buttons isolate the selected object and exit isolation mode
This commit is contained in:
@@ -279,14 +279,8 @@ impl Tool for SelectionTool {
|
||||
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) {
|
||||
return vec![Action::SwitchTool("type".into())];
|
||||
}
|
||||
// A group, a compound path or a single path, image, mesh or placed file, not
|
||||
// the one already isolated. Blends, envelopes, repeats and symbol instances
|
||||
// aren't: they have editing modes of their own.
|
||||
use vectorcraft_doc::NodeKind as K;
|
||||
let isolates = cx.doc.node(top).is_some_and(|n| {
|
||||
matches!(n.kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_))
|
||||
});
|
||||
if cx.double_click_isolate && isolates && cx.isolation != Some(top) {
|
||||
// An object that [`isolates`], not the one already isolated.
|
||||
if cx.double_click_isolate && cx.doc.node(top).is_some_and(|n| isolates(&n.kind)) && cx.isolation != Some(top) {
|
||||
return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))];
|
||||
}
|
||||
} else if cx.isolation.is_some() {
|
||||
@@ -568,6 +562,14 @@ pub(crate) fn is_area_type(n: &vectorcraft_doc::Node) -> bool {
|
||||
&& matches!(&n.kind, vectorcraft_doc::NodeKind::Text(t) if matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }))
|
||||
}
|
||||
|
||||
/// Does a double-click or the Control bar's Isolate Selected Object button isolate an object of
|
||||
/// this kind? A group, a compound path or a single path, image, mesh or placed file does. Blends,
|
||||
/// envelopes, repeats and symbol instances do not; they have editing modes of their own.
|
||||
pub fn isolates(kind: &vectorcraft_doc::NodeKind) -> bool {
|
||||
use vectorcraft_doc::NodeKind as K;
|
||||
matches!(kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -208,6 +208,36 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon
|
||||
}
|
||||
}
|
||||
|
||||
/// The id of the Control bar's isolation button.
|
||||
const ISOLATION_BUTTON: &str = "cb-isolation";
|
||||
|
||||
/// The Control bar's isolation button. In isolation mode it is Exit Isolation Mode, except while a
|
||||
/// pattern is edited (the isolation bar's Done and Cancel end that). Otherwise, with one object
|
||||
/// selected that [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which
|
||||
/// also isolates a path inside a group that Direct Selection selected. An envelope's contents
|
||||
/// get no button: the envelope's Edit Envelope and Edit Contents buttons show instead.
|
||||
fn isolation_button(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let (icon, tip, id) = if st.isolation.is_some() {
|
||||
if st.doc.pattern_edit.is_some() {
|
||||
return;
|
||||
}
|
||||
("chevron-left", tl!("Exit Isolation Mode"), "object.exitIsolation")
|
||||
} else if let [one] = st.selection.objects[..]
|
||||
&& st.doc.node(one).is_some_and(|n| vectorcraft_tools::select::isolates(&n.kind))
|
||||
&& vectorcraft_doc::live::envelope_of(&st.doc, one).is_none()
|
||||
{
|
||||
("square-dashed", tl!("Isolate Selected Object"), "object.isolate")
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(24.0, 24.0), Sense::hover());
|
||||
let resp = ui.interact(rect, egui::Id::new(ISOLATION_BUTTON), Sense::click());
|
||||
if widgets::paint_icon_button(ui, resp, icon, tip, false).clicked() {
|
||||
crate::menus::invoke(app, id, json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The room the Control bar's inline X/Y/W/H fields need; with less, only the Transform link shows.
|
||||
const INLINE_TRANSFORM_WIDTH: f32 = 440.0;
|
||||
|
||||
@@ -252,6 +282,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if anchors != AnchorControls::None {
|
||||
anchor_buttons(app, ui, anchors);
|
||||
}
|
||||
isolation_button(app, ui);
|
||||
ui.add_space(6.0);
|
||||
// An image or an Image Trace object shows its own controls in place of Fill and Stroke.
|
||||
let image = crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui);
|
||||
@@ -1158,6 +1189,76 @@ mod tests {
|
||||
assert!(title(&s).ends_with("(RGB/Preview)"));
|
||||
}
|
||||
|
||||
/// One headless frame of the Control bar; returns the isolation button's rectangle, if it shows.
|
||||
fn isolation_button(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Option<egui::Rect> {
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 300.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| super::control_bar(app, ui));
|
||||
out.textures_delta.clear();
|
||||
ctx.viewport(|vp| vp.prev_pass.widgets.get(egui::Id::new(super::ISOLATION_BUTTON)).map(|w| w.rect))
|
||||
}
|
||||
|
||||
/// The Control bar's Isolate Selected Object isolates a path inside a group that Direct
|
||||
/// Selection selected, and Exit Isolation Mode leaves with the path selected. Type, symbol
|
||||
/// instances, blends, two objects and an envelope's contents get no isolate button, and pattern
|
||||
/// editing gets no exit button.
|
||||
#[test]
|
||||
fn the_control_bar_isolates_the_selected_object_and_exits_isolation_mode() {
|
||||
use vectorcraft_doc::NodeId;
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
|
||||
let rect = |app: &mut crate::VectorcraftApp, x: f64| {
|
||||
app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 20})).unwrap()["id"].as_u64().unwrap()
|
||||
};
|
||||
let (a, b) = (rect(&mut app, 10.0), rect(&mut app, 50.0));
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
app.run("object.group", json!({})).unwrap();
|
||||
app.select_tool("directSelection");
|
||||
app.run("select.anchors", json!({"id": a, "anchors": [[0, 0]]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let click = |app: &mut crate::VectorcraftApp| {
|
||||
let r = isolation_button(app, &ctx, vec![]).expect("the bar shows the isolation button");
|
||||
let press = |pressed| egui::Event::PointerButton {
|
||||
pos: r.center(),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
for e in [egui::Event::PointerMoved(r.center()), press(true), press(false)] {
|
||||
isolation_button(app, &ctx, vec![e]);
|
||||
}
|
||||
};
|
||||
click(&mut app);
|
||||
assert_eq!(app.session.active().unwrap().isolation, Some(NodeId(a)), "Isolate Selected Object isolates the path");
|
||||
click(&mut app);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!((st.isolation, st.selection.objects.clone()), (None, vec![NodeId(a)]), "Exit Isolation Mode selects the path");
|
||||
app.select_tool("selection");
|
||||
// Type, a symbol instance, a blend and two objects.
|
||||
let text = app.run("text.create", json!({"x": 10, "y": 100, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = rect(&mut app, 100.0);
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
let symbol = app.run("symbol.new", json!({"name": "Box"})).unwrap()["id"].as_u64().unwrap();
|
||||
let (d, e) = (rect(&mut app, 150.0), rect(&mut app, 250.0));
|
||||
let blend = app.run("object.blend.make", json!({"ids": [d, e], "steps": 1})).unwrap()["id"].as_u64().unwrap();
|
||||
for ids in [json!([text]), json!([symbol]), json!([blend]), json!([text, symbol])] {
|
||||
app.run("select.set", json!({ "ids": ids })).unwrap();
|
||||
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "{ids}");
|
||||
}
|
||||
// An envelope's contents, while Edit Contents is on.
|
||||
let g = rect(&mut app, 350.0);
|
||||
app.run("select.set", json!({"ids": [g]})).unwrap();
|
||||
app.run("object.envelope.makeWithWarp", json!({})).unwrap();
|
||||
app.run("object.envelope.editContents", json!({"editing": true})).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().selection.objects, vec![NodeId(g)]);
|
||||
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the envelope's controls show instead");
|
||||
let f = rect(&mut app, 300.0);
|
||||
app.run("select.set", json!({"ids": [f]})).unwrap();
|
||||
app.run("object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
assert!(app.session.active().unwrap().isolation.is_some());
|
||||
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the isolation bar ends pattern editing");
|
||||
}
|
||||
|
||||
/// The Zoom tool's hint follows Performance › Animated Zoom (#394): a drag zooms as it goes,
|
||||
/// or (off) zooms into the area dragged across.
|
||||
#[test]
|
||||
|
||||
@@ -665,6 +665,7 @@
|
||||
Auto Color Automatická barva
|
||||
Automatic Automaticky
|
||||
Isolate Selected Group Izolovat vybranou skupinu
|
||||
Isolate Selected Object Izolovat vybraný objekt
|
||||
Exit Isolation Mode Ukončit izolovaný režim
|
||||
Make Clipping Mask Vytvořit ořezovou masku
|
||||
Release Clipping Mask Uvolnit ořezovou masku
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3320,6 +3320,7 @@ imageTrace Outline Kontur
|
||||
to set spacing and orientation legt Abstand und Ausrichtung fest
|
||||
: :
|
||||
Isolate Selected Group Ausgewählte Gruppe isolieren
|
||||
Isolate Selected Object Ausgewähltes Objekt isolieren
|
||||
Release Clipping Mask Schnittmaske zurückwandeln
|
||||
Make Compound Path Zusammengesetzten Pfad erstellen
|
||||
Release Compound Path Zusammengesetzten Pfad zurückwandeln
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3419,6 +3419,7 @@ imageTrace Outline Contorno
|
||||
to set spacing and orientation para definir espaciado y orientación
|
||||
: :
|
||||
Isolate Selected Group Aislar grupo seleccionado
|
||||
Isolate Selected Object Aislar objeto seleccionado
|
||||
Release Clipping Mask Soltar máscara de recorte
|
||||
Make Compound Path Crear trazado compuesto
|
||||
Release Compound Path Soltar trazado compuesto
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3416,6 +3416,7 @@ imageTrace Outline Contour
|
||||
to set spacing and orientation pour définir l’espacement et l’orientation
|
||||
: :
|
||||
Isolate Selected Group Isoler le groupe sélectionné
|
||||
Isolate Selected Object Isoler l’objet sélectionné
|
||||
Release Clipping Mask Annuler le masque d’écrêtage
|
||||
Make Compound Path Créer un tracé transparent
|
||||
Release Compound Path Annuler le tracé transparent
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1746,6 +1746,7 @@
|
||||
Isolate Blending Keverés elkülönítése
|
||||
Isolate Group Csoport elkülönítése
|
||||
Isolate Selected Group Kijelölt csoport elkülönítése
|
||||
Isolate Selected Object Kijelölt objektum elkülönítése
|
||||
Italian Olasz
|
||||
Italic Dőlt
|
||||
Its graphics, colors and text styles are deleted from this machine. A grafikái, színei és szövegstílusai törlődnek erről a gépről.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3421,6 +3421,7 @@ imageTrace Outline Contorno
|
||||
to set spacing and orientation per impostare spaziatura e orientamento
|
||||
: :
|
||||
Isolate Selected Group Isola gruppo selezionato
|
||||
Isolate Selected Object Isola oggetto selezionato
|
||||
Release Clipping Mask Rilascia maschera di ritaglio
|
||||
Make Compound Path Crea tracciato composto
|
||||
Release Compound Path Rilascia tracciato composto
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -670,6 +670,7 @@
|
||||
Auto Color 自動カラー
|
||||
Automatic 自動
|
||||
Isolate Selected Group 選択したグループを分離
|
||||
Isolate Selected Object 選択したオブジェクトを分離
|
||||
Exit Isolation Mode 分離モードを終了
|
||||
Make Clipping Mask クリッピングマスクを作成
|
||||
Release Clipping Mask クリッピングマスクを解除
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -668,6 +668,7 @@
|
||||
Auto Color 자동 색상
|
||||
Automatic 자동
|
||||
Isolate Selected Group 선택한 그룹 격리
|
||||
Isolate Selected Object 선택한 개체 격리
|
||||
Exit Isolation Mode 격리 모드 종료
|
||||
Make Clipping Mask 클리핑 마스크 만들기
|
||||
Release Clipping Mask 클리핑 마스크 해제
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -612,6 +612,7 @@
|
||||
Auto Color Kolor automatyczny
|
||||
Automatic Automatycznie
|
||||
Isolate Selected Group Izoluj zaznaczoną grupę
|
||||
Isolate Selected Object Izoluj zaznaczony obiekt
|
||||
Exit Isolation Mode Wyjdź z trybu izolacji
|
||||
Make Clipping Mask Utwórz maskę przycinającą
|
||||
Release Clipping Mask Uwolnij maskę przycinającą
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1179,6 +1179,7 @@
|
||||
Isolate Blending Isolar Mesclagem
|
||||
Isolate Group Isolar Grupo
|
||||
Isolate Selected Group Isolar Grupo Selecionado
|
||||
Isolate Selected Object Isolar Objeto Selecionado
|
||||
Italian Italiano
|
||||
Join Unir
|
||||
Join Our Discord Junte-se ao nosso Discord
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3417,6 +3417,7 @@ imageTrace Outline Контур
|
||||
to set spacing and orientation для задания интервала и ориентации
|
||||
: :
|
||||
Isolate Selected Group Изолировать выбранную группу
|
||||
Isolate Selected Object Изолировать выбранный объект
|
||||
Release Clipping Mask Освободить маску обрезки
|
||||
Make Compound Path Создать составной контур
|
||||
Release Compound Path Освободить составной контур
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3414,6 +3414,7 @@ imageTrace Outline Контур
|
||||
to set spacing and orientation для завдання інтервалу та орієнтації
|
||||
: :
|
||||
Isolate Selected Group Ізолювати обрану групу
|
||||
Isolate Selected Object Ізолювати обраний об’єкт
|
||||
Release Clipping Mask Звільнити відсічну маску
|
||||
Make Compound Path Створити складений контур
|
||||
Release Compound Path Звільнити складений контур
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 713 and column 40.
|
@@ -1608,6 +1608,7 @@
|
||||
Isolate Blending Tách biệt hòa trộn
|
||||
Isolate Group Tách biệt nhóm
|
||||
Isolate Selected Group Tách biệt nhóm đã chọn
|
||||
Isolate Selected Object Tách biệt đối tượng đã chọn
|
||||
Isometric Bottom Đẳng góc dưới
|
||||
Isometric Left Đẳng góc trái
|
||||
Isometric Right Đẳng góc phải
|
||||
|
||||
|
@@ -3276,6 +3276,7 @@ imageTrace Outline 轮廓
|
||||
to set spacing and orientation 可设置间距与方向
|
||||
: :
|
||||
Isolate Selected Group 隔离选择的编组
|
||||
Isolate Selected Object 隔离选择的对象
|
||||
Release Clipping Mask 释放剪切蒙版
|
||||
Make Compound Path 制作复合路径
|
||||
Release Compound Path 释放复合路径
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3407,6 +3407,7 @@ imageTrace Outline 輪廓
|
||||
to set spacing and orientation 可設定間距與方向
|
||||
: :
|
||||
Isolate Selected Group 隔離選取的群組
|
||||
Isolate Selected Object 隔離選取的物件
|
||||
Release Clipping Mask 釋放剪裁遮色片
|
||||
Make Compound Path 製作複合路徑
|
||||
Release Compound Path 釋放複合路徑
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -429,6 +429,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- In isolation mode the Control bar has an Exit Isolation Mode button (`object.exitIsolation`), except while a pattern is edited. Outside isolation mode, with one group, compound path, path, image, mesh or placed file selected, it has an Isolate Selected Object button (`object.isolate`), which also isolates a path inside a group that Direct Selection selected.
|
||||
- Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields.
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
- Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`).
|
||||
|
||||
Reference in New Issue
Block a user