Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0916534db9 |
No files matched your search
@@ -105,6 +105,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- Four brightness themes, persistent preferences.
|
||||
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels and dialogs step with Up/Down (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
|
||||
- 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. Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
- On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one.
|
||||
- **Tools:**
|
||||
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it).
|
||||
|
||||
@@ -61,7 +61,7 @@ impl eframe::App for App {
|
||||
self.menu = Some(native_menu::NativeMenu::install(&mut self.app));
|
||||
}
|
||||
if let Some(m) = &mut self.menu {
|
||||
m.poll(&mut self.app);
|
||||
m.poll(&mut self.app, ctx);
|
||||
}
|
||||
open_files(&mut self.app, open_documents::take());
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct NativeMenu {
|
||||
}
|
||||
|
||||
/// Accelerator for a shortcut like "Cmd+Shift+]" (modifier-less shortcuts stay in the app so they
|
||||
/// don't steal keys from text fields).
|
||||
/// don't steal keys from text fields; a focused field still gets Cmd+A/X/C/V, see [`NativeMenu::poll`]).
|
||||
fn accel(sc: &str) -> Option<Accelerator> {
|
||||
if !(sc.contains("Cmd") || sc.contains("Ctrl") || sc.contains("Alt") || sc.starts_with('F')) {
|
||||
return None;
|
||||
@@ -72,12 +72,13 @@ impl NativeMenu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch clicked items and refresh state.
|
||||
pub fn poll(&mut self, app: &mut VectorcraftApp) {
|
||||
/// Dispatch chosen items (a focused text field takes Select All and the clipboard) and refresh
|
||||
/// state.
|
||||
pub fn poll(&mut self, app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
while let Ok(ev) = MenuEvent::receiver().try_recv() {
|
||||
if let Some((cmd, params, _, _)) = self.items.get(ev.id.as_ref()) {
|
||||
let p = if params.is_null() { serde_json::json!({}) } else { params.clone() };
|
||||
menus::invoke(app, cmd, p);
|
||||
menus::invoke_from_system_menu(app, ctx, cmd, p);
|
||||
}
|
||||
}
|
||||
let now = vectorcraft_ui_egui::now_ms();
|
||||
|
||||
@@ -9,7 +9,15 @@ use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!("select.all", "All", ["Select"], Some("Cmd+A"), "{}", has_doc, all),
|
||||
cmd!(
|
||||
"select.all",
|
||||
"All",
|
||||
["Select"],
|
||||
Some("Cmd+A"),
|
||||
"{} → {count}; while the Type tool edits text, all of that text instead → {editing, start, end} (byte offsets)",
|
||||
has_doc,
|
||||
all
|
||||
),
|
||||
cmd!("select.allOnArtboard", "All on Active Artboard", ["Select"], Some("Cmd+Alt+A"), "{artboard?: index}", has_doc, all_on_artboard),
|
||||
cmd!("select.none", "Deselect", ["Select"], Some("Cmd+Shift+A"), "{}", has_doc, none),
|
||||
cmd!("select.reselect", "Reselect", ["Select"], Some("Cmd+6"), "{}", has_doc, reselect),
|
||||
@@ -107,6 +115,18 @@ fn selectable(d: &Document, iso: Option<NodeId>) -> Vec<NodeId> {
|
||||
}
|
||||
|
||||
fn all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
// While the Type tool edits text, Select All takes all of that text and leaves the art
|
||||
// selection as it is (Illustrator: text cursor in a text object).
|
||||
if s.tool_wants_text() {
|
||||
s.set_tool_option("selectAll", &json!(true));
|
||||
let editing = s.tool_options()["editing"].clone();
|
||||
let id = editing.as_u64().map(NodeId);
|
||||
let end = id.and_then(|id| match &s.doc().ok()?.doc.node(id)?.kind {
|
||||
NodeKind::Text(t) => Some(t.plain_text().len()),
|
||||
_ => None,
|
||||
});
|
||||
return Ok(json!({ "editing": editing, "start": 0, "end": end.unwrap_or(0) }));
|
||||
}
|
||||
let st = s.doc()?;
|
||||
let ids = selectable(&st.doc, st.isolation);
|
||||
s.select(|_, sel| sel.set(ids.iter().copied()))?;
|
||||
|
||||
@@ -310,15 +310,18 @@ fn character_panel_flow_styles_the_selected_range_while_editing() {
|
||||
fn select_all_and_cut_like_ui() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
s.execute("shape.rectangle", &json!({"x": 300, "y": 300, "width": 10, "height": 10})).unwrap();
|
||||
let id = text(&mut s, "abc def");
|
||||
s.select_tool("type", v).unwrap();
|
||||
let t = obj(&s, id);
|
||||
let p = t.xf * Point::new(2.0, -5.0);
|
||||
click(&mut s, p.x, p.y);
|
||||
s.set_tool_option("selectAll", &json!(true));
|
||||
let o = s.tool_options();
|
||||
let len = obj(&s, id).plain_text().len() as u64;
|
||||
let r = s.execute("text.getRange", &json!({"id": o["editing"], "start": o["start"], "end": o["end"].as_u64().unwrap().min(len)})).unwrap();
|
||||
// Select All while editing takes the text, not the art (the rectangle stays unselected).
|
||||
assert!(s.tool_wants_text());
|
||||
let o = s.execute("select.all", &json!({})).unwrap();
|
||||
assert_eq!(o, json!({"editing": id.0, "start": 0, "end": 7}));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [id]);
|
||||
let r = s.execute("text.getRange", &json!({"id": o["editing"], "start": o["start"], "end": o["end"]})).unwrap();
|
||||
assert_eq!(r["text"], json!("abc def"));
|
||||
s.set_tool_option("copy", &r["runs"]);
|
||||
key(&mut s, ToolKey::Delete, Mods::default());
|
||||
@@ -326,6 +329,10 @@ fn select_all_and_cut_like_ui() {
|
||||
s.tool_text("abc def", v).unwrap();
|
||||
s.tool_text("abc def", v).unwrap();
|
||||
assert_eq!(obj(&s, id).plain_text(), "abc defabc def");
|
||||
// Not editing: Select All selects the art again.
|
||||
key(&mut s, ToolKey::Escape, Mods::default());
|
||||
assert!(!s.tool_wants_text());
|
||||
assert_eq!(s.execute("select.all", &json!({})).unwrap(), json!({"count": 2}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! `press_key` with the Width and Puppet Warp tools: Delete and Backspace reach the selected width
|
||||
//! point or pin, and clear the selected art when the tool has nothing selected.
|
||||
//! point or pin, and clear the selected art when the tool has nothing selected. Cmd+A while the
|
||||
//! Type tool edits selects the text, not the art.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -64,3 +65,15 @@ fn delete_and_backspace_reach_the_puppet_warp_tool() {
|
||||
assert!(!exists(&h, id), "{key} cleared the art");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_a_while_the_type_tool_edits_selects_the_text() {
|
||||
let mut h = session();
|
||||
run(&mut h, "shape.rectangle", json!({"x": 300, "y": 300, "width": 20, "height": 20}));
|
||||
let id = run(&mut h, "text.create", json!({"x": 100, "y": 100, "text": "Hello", "size": 20}))["id"].as_u64().unwrap();
|
||||
click(&mut h, "type", 102.0, 95.0);
|
||||
let r = ok(&mut h, "press_key", json!({"key": "A", "mods": {"cmd": true}}));
|
||||
assert_eq!(r["command"], "select.all");
|
||||
assert_eq!(r["result"], json!({"editing": id, "start": 0, "end": 5}));
|
||||
assert_eq!(h.session.doc().unwrap().selection.objects, [vectorcraft_doc::NodeId(id)], "the art selection stays");
|
||||
}
|
||||
@@ -106,6 +106,8 @@ mod tests_save;
|
||||
#[cfg(test)]
|
||||
mod tests_saveext;
|
||||
#[cfg(test)]
|
||||
mod tests_selectall;
|
||||
#[cfg(test)]
|
||||
mod tests_slices;
|
||||
#[cfg(test)]
|
||||
mod tests_svg;
|
||||
|
||||
@@ -2653,6 +2653,32 @@ fn menu_dialog(id: &str) -> Option<(&'static str, Value)> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The event a focused text field takes for Edit-menu command `id` (Select All, Cut, Copy,
|
||||
/// Paste), as the same keys would send it.
|
||||
fn field_event(app: &mut VectorcraftApp, id: &str) -> Option<egui::Event> {
|
||||
Some(match id {
|
||||
"select.all" => egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::COMMAND },
|
||||
"edit.copy" => egui::Event::Copy,
|
||||
"edit.cut" => egui::Event::Cut,
|
||||
"edit.paste" | "edit.pasteWithoutFormatting" => egui::Event::Paste(app.system_clipboard_text()?),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Invoke an item of the system menu bar (macOS), chosen by a click or by its key equivalent: the
|
||||
/// system takes those keys before the window sees them. While a text field has the keyboard,
|
||||
/// Select All, Cut, Copy and Paste act on the field's text, as their keys do in the window;
|
||||
/// everything else (and those commands with no field focused) goes to [`invoke`].
|
||||
pub fn invoke_from_system_menu(app: &mut VectorcraftApp, ctx: &egui::Context, id: &str, p: Value) {
|
||||
if ctx.text_edit_focused()
|
||||
&& let Some(e) = field_event(app, id)
|
||||
{
|
||||
ctx.input_mut(|i| i.events.push(e));
|
||||
return;
|
||||
}
|
||||
invoke(app, id, p);
|
||||
}
|
||||
|
||||
/// Invoke a menu/command id with UI side effects (dialogs for "…" commands that need input).
|
||||
pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
if waits_for_ime(app, id) {
|
||||
|
||||
@@ -123,8 +123,8 @@ fn text_paste(app: &mut VectorcraftApp, s: Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit-menu commands while the Type tool edits text act on the text (Cut/Copy/Paste/Select
|
||||
/// All/Clear). `None` = not intercepted.
|
||||
/// Edit-menu commands while the Type tool edits text act on the text (Cut/Copy/Paste/Clear;
|
||||
/// Select All is the engine's `select.all`). `None` = not intercepted.
|
||||
pub(crate) fn intercept_text_command(app: &mut VectorcraftApp, id: &str) -> Option<Result<Value, String>> {
|
||||
if !app.session.tool_wants_text() {
|
||||
return None;
|
||||
@@ -139,7 +139,6 @@ pub(crate) fn intercept_text_command(app: &mut VectorcraftApp, id: &str) -> Opti
|
||||
}
|
||||
}
|
||||
"edit.paste" | "edit.pasteWithoutFormatting" => text_paste(app, None),
|
||||
"select.all" => app.session.set_tool_option("selectAll", &json!(true)),
|
||||
"edit.clear" => text_key(app, ToolKey::Delete, Mods::default()),
|
||||
_ => return None,
|
||||
}
|
||||
@@ -152,10 +151,10 @@ enum TextInput {
|
||||
Copy,
|
||||
Cut,
|
||||
Paste(Option<String>),
|
||||
SelectAll,
|
||||
}
|
||||
|
||||
/// Route editing keys (with modifiers), clipboard events and Cmd+A/C/X/V to the Type tool.
|
||||
/// Route editing keys (with modifiers), clipboard events and Cmd+C/X/V to the Type tool (Cmd+A is
|
||||
/// left to the Select All shortcut, which selects the text being edited).
|
||||
pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
CTX.get_or_init(|| ctx.clone());
|
||||
let mut todo = vec![];
|
||||
@@ -193,10 +192,9 @@ pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if m.command && !m.shift && !m.alt && matches!(key, Key::A | Key::C | Key::X | Key::V) {
|
||||
if m.command && !m.shift && !m.alt && matches!(key, Key::C | Key::X | Key::V) {
|
||||
if *pressed {
|
||||
todo.push(match key {
|
||||
Key::A => TextInput::SelectAll,
|
||||
Key::C => TextInput::Copy,
|
||||
Key::X => TextInput::Cut,
|
||||
_ => TextInput::Paste(None),
|
||||
@@ -221,7 +219,6 @@ pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
TextInput::Paste(s) => text_paste(app, s),
|
||||
TextInput::SelectAll => app.session.set_tool_option("selectAll", &json!(true)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Editing keys with modifiers, clipboard and Cmd+A.
|
||||
// Editing keys with modifiers and the clipboard (Cmd+A is Select All's, below: the text).
|
||||
crate::panels::character::route_type_input(app, ctx);
|
||||
// Enter was already delivered above as ToolKey::Enter (newline).
|
||||
let fire = all_shortcuts().into_iter().filter(|(sc, ..)| sc.modifiers.command).find(|(sc, ..)| ctx.input_mut(|i| consume(i, sc, false)));
|
||||
|
||||
@@ -89,6 +89,14 @@ impl VectorcraftApp {
|
||||
})
|
||||
}
|
||||
|
||||
/// The text on the system clipboard, if any.
|
||||
pub(crate) fn system_clipboard_text(&mut self) -> Option<String> {
|
||||
match self.services.system_clipboard.as_mut() {
|
||||
Some(cb) => cb.read(&[TEXT]).map(|f| String::from_utf8_lossy(&f.data).into_owned()),
|
||||
None => self.services.clipboard_read.as_mut().and_then(|read| read()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the system clipboard hold something Paste can take (with nothing copied in the app)?
|
||||
pub(crate) fn system_clipboard_pasteable(&mut self) -> bool {
|
||||
match self.services.system_clipboard.as_mut() {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//! Select All (Cmd+A) follows the keyboard, from the window and from the system menu bar (macOS):
|
||||
//! a focused text field selects its own text, the Type tool editing selects the text being edited,
|
||||
//! and only otherwise are all the objects selected.
|
||||
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Rect, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
use crate::{VectorcraftApp, menus};
|
||||
|
||||
struct App {
|
||||
app: VectorcraftApp,
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// A document with a 37 pt wide rectangle (selected) and a second, unselected one.
|
||||
fn new() -> Self {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 200, "y": 150, "width": 20, "height": 20})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 20, "y": 20, "width": 37, "height": 40})).unwrap();
|
||||
let mut a = Self { app, ctx: egui::Context::default(), time: 0.0 };
|
||||
for _ in 0..3 {
|
||||
a.frame(vec![]);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
/// One whole app frame with `events`; `menu`: a system menu item chosen first, as the macOS
|
||||
/// host dispatches it before the app's logic.
|
||||
fn frame_with(&mut self, events: Vec<Event>, menu: Option<&str>) -> egui::FullOutput {
|
||||
self.time += 0.1;
|
||||
let raw = egui::RawInput {
|
||||
events,
|
||||
time: Some(self.time),
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 850.0))),
|
||||
..Default::default()
|
||||
};
|
||||
let app = &mut self.app;
|
||||
let mut out = self.ctx.run_ui(raw, |ui| {
|
||||
if let Some(id) = menu {
|
||||
menus::invoke_from_system_menu(app, ui.ctx(), id, json!({}));
|
||||
}
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) -> egui::FullOutput {
|
||||
self.frame_with(events, None)
|
||||
}
|
||||
|
||||
fn menu(&mut self, id: &str) -> egui::FullOutput {
|
||||
self.frame_with(vec![], Some(id))
|
||||
}
|
||||
|
||||
fn selected(&self) -> usize {
|
||||
self.app.session.active().unwrap().selection.objects.len()
|
||||
}
|
||||
|
||||
/// The panel number field showing `text` (the field keeps its text under its id): id, rect.
|
||||
fn field(&self, text: &str) -> (egui::Id, Rect) {
|
||||
let widgets: Vec<(egui::Id, Rect)> =
|
||||
self.ctx.viewport(|vp| vp.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).map(|w| (w.id, w.rect)).collect());
|
||||
widgets
|
||||
.into_iter()
|
||||
.find(|&(id, _)| {
|
||||
self.ctx.data(|d| d.get_temp::<String>(id)).is_some_and(|t| t == text) && egui::TextEdit::load_state(&self.ctx, id).is_some()
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no field shows {text:?}"))
|
||||
}
|
||||
|
||||
/// The selected character range of text field `id`.
|
||||
fn field_selection(&self, id: egui::Id) -> Option<(usize, usize)> {
|
||||
let r = egui::TextEdit::load_state(&self.ctx, id)?.cursor.char_range()?;
|
||||
let (a, b) = (r.primary.index, r.secondary.index);
|
||||
Some((a.min(b).into(), a.max(b).into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn press(key: Key, modifiers: Modifiers) -> Event {
|
||||
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }
|
||||
}
|
||||
|
||||
fn click(at: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_a_in_a_panel_field_selects_its_text_and_leaves_the_art() {
|
||||
let mut a = App::new();
|
||||
assert_eq!(a.selected(), 1);
|
||||
let (id, rect) = a.field("37 pt");
|
||||
a.frame(vec![Event::PointerMoved(rect.center()), click(rect.center(), true)]);
|
||||
a.frame(vec![click(rect.center(), false)]);
|
||||
a.frame(vec![]);
|
||||
assert!(a.ctx.text_edit_focused(), "the W field has the keyboard");
|
||||
let all = Some((0, "37 pt".chars().count()));
|
||||
// The window's Cmd+A (Windows, Linux, the web, macOS without the system menu).
|
||||
a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
assert_eq!(a.field_selection(id), Some((5, 5)), "End collapses the selection");
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
assert_eq!(a.field_selection(id), all, "Cmd+A selects the field's text");
|
||||
assert_eq!(a.selected(), 1, "and not the art");
|
||||
// Select All from the macOS menu bar (its key equivalent or a click): the same.
|
||||
a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
a.menu("select.all");
|
||||
a.frame(vec![]);
|
||||
assert_eq!(a.field_selection(id), all, "the menu's Select All selects the field's text");
|
||||
assert_eq!(a.selected(), 1, "and not the art");
|
||||
// The menu's Copy copies the field's text, not the art.
|
||||
let out = a.menu("edit.copy");
|
||||
let copied: Vec<_> = out
|
||||
.platform_output
|
||||
.commands
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
egui::OutputCommand::CopyText(t) => Some(t.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(copied, ["37 pt"]);
|
||||
assert!(a.app.session.clipboard.is_empty(), "no art copied");
|
||||
assert!(a.ctx.text_edit_focused(), "the field keeps the keyboard");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_a_while_editing_type_selects_all_of_its_text() {
|
||||
let mut a = App::new();
|
||||
a.app.select_tool("type");
|
||||
let view = a.app.view_info();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
a.app.session.pointer(&PointerEvent::new(kind, 100.0, 200.0), view).unwrap();
|
||||
}
|
||||
a.frame(vec![Event::Text("abc".into())]);
|
||||
let text = a.app.session.active().unwrap().selection.objects.clone();
|
||||
let range = |a: &App| {
|
||||
let o = a.app.session.tool_options();
|
||||
(o["start"].as_u64().unwrap(), o["end"].as_u64().unwrap().min(3))
|
||||
};
|
||||
assert_eq!(range(&a), (3, 3), "the caret after the text");
|
||||
// The window's Cmd+A.
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
assert_eq!(range(&a), (0, 3), "Cmd+A selects the text being edited");
|
||||
assert_eq!(a.app.session.active().unwrap().selection.objects, text, "and not the art");
|
||||
a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
assert_eq!(range(&a), (3, 3));
|
||||
// The macOS menu bar's Select All.
|
||||
a.menu("select.all");
|
||||
assert_eq!(range(&a), (0, 3), "the menu's Select All selects the text being edited");
|
||||
assert_eq!(a.app.session.active().unwrap().selection.objects, text);
|
||||
// Typing replaces it.
|
||||
a.frame(vec![Event::Text("Z".into())]);
|
||||
let doc = a.app.session.execute("document.inspect", &json!({})).unwrap();
|
||||
let names: Vec<_> = doc["layers"][0]["children"].as_array().unwrap().iter().map(|c| c["name"].clone()).collect();
|
||||
assert!(names.contains(&json!("Z")), "{names:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_a_on_the_canvas_selects_all_objects() {
|
||||
let mut a = App::new();
|
||||
assert!(!a.ctx.egui_wants_keyboard_input());
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
assert_eq!(a.selected(), 2, "Cmd+A selects every object");
|
||||
a.app.run("select.none", json!({})).unwrap();
|
||||
a.menu("select.all");
|
||||
assert_eq!(a.selected(), 2, "so does the menu's Select All");
|
||||
}
|
||||
+12
@@ -1890,6 +1890,18 @@ square to the page. `object.resetBoundingBox` squares the box again without movi
|
||||
{"name":"run_command","arguments":{"command":"object.rotate","params":{"angle":45,"absolute":true}}}
|
||||
```
|
||||
|
||||
## Select All while editing type
|
||||
|
||||
While the Type tool edits text (a click into type, `pointer_gesture` with `"tool":"type"`), `select.all` (Cmd+A,
|
||||
`press_key {key: "A", mods: {cmd: true}}`) selects all of that text instead of the art, as in the reference app, and
|
||||
returns `{editing, start, end}` (the text's id and the selected byte range, which `text.getRange` and
|
||||
`text.setRangeStyle` take); the art selection stays as it is. Without text being edited it selects every object and
|
||||
returns `{count}`. Text in threaded frames is selected one frame at a time.
|
||||
|
||||
```json
|
||||
{"name":"press_key","arguments":{"key":"A","mods":{"cmd":true}}}
|
||||
```
|
||||
|
||||
## Empty point type
|
||||
|
||||
Point type the Type tool places with a click and leaves empty is discarded when editing ends: Escape, another tool,
|
||||
|
||||
Reference in new issue
Block a user