mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 10:45:45 +00:00
Actions: Select Object selects objects by their Attributes note during playback
The Actions panel menu gets Select Object…, which opens Set Selection with a Note field and Case Sensitive and Whole Word. The step it inserts runs the new select.byNote command, which replaces the selection with every object that can be selected whose note matches. When none matches, the selection, its Puppet Warp pins and the active Appearance item stay as they were, and playback stops with an error. Action set files that hold the step are saved as version 4.
This commit is contained in:
@@ -475,6 +475,7 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"select.allOnArtboard",
|
||||
"select.anchors",
|
||||
"select.anchorsMany",
|
||||
"select.byNote",
|
||||
"select.color",
|
||||
"select.font",
|
||||
"select.globalEdit",
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::object::can_align;
|
||||
use super::wand::{fill_of, stroke_of};
|
||||
use super::{
|
||||
CommandSpec, Result, Session, bad, bool_or, checked_id, checked_ids_param, cmd, color_value, has_doc, has_selection, index, ok, str_param,
|
||||
switch_or, text_param,
|
||||
};
|
||||
use crate::DocState;
|
||||
|
||||
@@ -103,6 +104,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
select_color
|
||||
),
|
||||
cmd!(
|
||||
"select.byNote",
|
||||
"Select by Note",
|
||||
[],
|
||||
None,
|
||||
"{note, caseSensitive?: bool, wholeWord?: bool} select the objects whose Attributes note (attributes.set) holds `note`, without its leading and trailing spaces (1 to 1,024 bytes): anywhere in the note and in any letter case by default; caseSensitive (default false) asks for the same letter case, and wholeWord (default false) for no letter, digit or _ right before or after it. It looks at the objects Select All takes and the objects inside them, leaving out hidden and locked objects and art on a locked artboard; an object whose note matches is selected without the objects inside it. The objects found replace the selection, in paint order. No object found is an error that leaves the selection as it was. Not an undo step; Select > Reselect repeats it. The Actions panel's Select Object step (ui.action.insertSelectObject) runs this → {count, ids} (resulting selection)",
|
||||
has_doc,
|
||||
by_note
|
||||
),
|
||||
cmd!(
|
||||
"select.same.fillColor",
|
||||
"Fill Color",
|
||||
@@ -730,6 +740,73 @@ fn select_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
select_where(s, C, p, |d, n| paint_reported(d, n) && reads_as(fill_of(n), fill.as_deref()) && reads_as(stroke_of(n).0, stroke.as_deref()))
|
||||
}
|
||||
|
||||
/// The longest note, in bytes, that Select by Note searches for.
|
||||
const MAX_NOTE_QUERY: usize = 1024;
|
||||
|
||||
/// Select by Note: the objects whose Attributes note holds `note` ([`note_matches`]). An object
|
||||
/// whose note matches is selected without the objects inside it. With none found, the selection
|
||||
/// stays as it was.
|
||||
fn by_note(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "select.byNote";
|
||||
let query = text_param(p, "note", C)?.map(str::trim).unwrap_or_default();
|
||||
if query.is_empty() {
|
||||
return Err(bad(C, "the note is empty"));
|
||||
}
|
||||
if query.len() > MAX_NOTE_QUERY {
|
||||
return Err(bad(C, "the note is longer than 1,024 bytes"));
|
||||
}
|
||||
let case_sensitive = switch_or(p, "caseSensitive", false, C)?;
|
||||
let whole_word = switch_or(p, "wholeWord", false, C)?;
|
||||
let not_found = || bad(C, format!("no object that can be selected has a note matching “{query}”"));
|
||||
let st = s.doc()?;
|
||||
let locks = st.doc.locked_artboards();
|
||||
let mut ids = vec![];
|
||||
walk_selectable(&st.doc, st.isolation, |n| {
|
||||
let note = n.extras().attrs.as_deref().map_or("", |a| a.note.as_str());
|
||||
if note_matches(note, query, case_sensitive, whole_word) && is_selectable(&st.doc, st.isolation, &locks, n.id) {
|
||||
ids.push(n.id);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
// The selection leaves out guides under Lock Guides and hidden slices, whatever selects them.
|
||||
let mut found = st.selection.clone();
|
||||
found.set(ids.iter().copied());
|
||||
crate::deselect_hidden(&s.menu, &st.doc, &mut found);
|
||||
if found.objects.is_empty() {
|
||||
return Err(not_found());
|
||||
}
|
||||
let before = st.selection.objects.clone();
|
||||
s.select(|_, sel| sel.set(ids.iter().copied()))?;
|
||||
let st = s.doc_mut()?;
|
||||
st.last_selection_cmd = Some((C.to_string(), p.clone(), before));
|
||||
selection_result(s)
|
||||
}
|
||||
|
||||
/// Whether `note` holds `query`: anywhere, and in any letter case unless `case_sensitive`. With
|
||||
/// `whole_word`, the text right before and right after it is not a letter, a digit or `_`.
|
||||
fn note_matches(note: &str, query: &str, case_sensitive: bool, whole_word: bool) -> bool {
|
||||
if note.is_empty() || query.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let (note, query) = if case_sensitive { (note.to_string(), query.to_string()) } else { (note.to_lowercase(), query.to_lowercase()) };
|
||||
if !whole_word {
|
||||
return note.contains(&query);
|
||||
}
|
||||
let word = |c: char| c.is_alphanumeric() || c == '_';
|
||||
let mut from = 0;
|
||||
while let Some(at) = note.get(from..).and_then(|rest| rest.find(&query)).map(|i| from + i) {
|
||||
let before = note[..at].chars().next_back();
|
||||
let after = note[at + query.len()..].chars().next();
|
||||
if !before.is_some_and(word) && !after.is_some_and(word) {
|
||||
return true;
|
||||
}
|
||||
// Matches may overlap: look again from the next character.
|
||||
from = at + note[at..].chars().next().map_or(1, char::len_utf8);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Does `document.inspect` report a fill and stroke for `n`? A compound path's are its own, though
|
||||
/// it holds its members, and its members report none.
|
||||
fn paint_reported(d: &Document, n: &Node) -> bool {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Window → Attributes (`attributes.set`, `attributes.info`, `path.setFillRule`, `path.reverse`),
|
||||
//! Window → Attributes (`attributes.set`, `attributes.info`, `path.setFillRule`, `path.reverse`,
|
||||
//! and `select.byNote`, which selects objects by their note),
|
||||
//! the links SVG export writes for objects with a URL, and what objects keep for other software
|
||||
//! (their `data-*` attributes and their attributes in other applications' namespaces).
|
||||
|
||||
@@ -216,6 +217,120 @@ fn object_data_is_set_saved_and_written_to_svg() {
|
||||
assert_eq!(back.node(a).unwrap().extras().attrs, s.doc().unwrap().doc.node(a).unwrap().extras().attrs);
|
||||
}
|
||||
|
||||
/// The selected objects after `select.byNote` with `p`, or its error.
|
||||
fn by_note(s: &mut Session, p: Value) -> std::result::Result<Vec<u64>, String> {
|
||||
s.execute("select.byNote", &p).map_err(|e| e.to_string())?;
|
||||
Ok(s.doc().unwrap().selection.objects.iter().map(|id| id.0).collect())
|
||||
}
|
||||
|
||||
fn note(s: &mut Session, id: NodeId, note: &str) {
|
||||
run(s, "attributes.set", json!({"ids": [id.0], "note": note}));
|
||||
}
|
||||
|
||||
/// Select by Note finds a note anywhere and in any letter case by default, replaces the selection
|
||||
/// with every object it finds, in paint order, and is not an undo step. Case Sensitive and Whole
|
||||
/// Word narrow it; Reselect repeats it.
|
||||
#[test]
|
||||
fn select_by_note_matches_part_of_a_note_in_any_case_unless_asked() {
|
||||
let mut s = session();
|
||||
let [a, b, c, d] = [0.0, 20.0, 40.0, 60.0].map(|x| rect(&mut s, x));
|
||||
note(&mut s, a, "Logo front");
|
||||
note(&mut s, b, "logo");
|
||||
note(&mut s, c, "Logotype");
|
||||
note(&mut s, d, "back");
|
||||
run(&mut s, "select.set", json!({"ids": [d.0]}));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
assert_eq!(by_note(&mut s, json!({"note": " LOGO "})), Ok(vec![a.0, b.0, c.0]), "outer spaces are left out");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step");
|
||||
assert_eq!(by_note(&mut s, json!({"note": "logo", "caseSensitive": true})), Ok(vec![b.0]));
|
||||
assert_eq!(by_note(&mut s, json!({"note": "logo", "wholeWord": true})), Ok(vec![a.0, b.0]));
|
||||
assert_eq!(by_note(&mut s, json!({"note": "Logo", "caseSensitive": true, "wholeWord": true})), Ok(vec![a.0]));
|
||||
assert_eq!(by_note(&mut s, json!({"note": "o f"})), Ok(vec![a.0]), "spaces inside the note count");
|
||||
run(&mut s, "select.none", json!({}));
|
||||
run(&mut s, "select.reselect", json!({}));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [a]);
|
||||
}
|
||||
|
||||
/// Whole Word looks at every place the text occurs, also places that overlap.
|
||||
#[test]
|
||||
fn select_by_note_whole_word_matches_overlapping_places_and_other_scripts() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0);
|
||||
let matches = |s: &mut Session, text: &str, p: Value| {
|
||||
note(s, a, text);
|
||||
by_note(s, p).is_ok()
|
||||
};
|
||||
assert!(matches(&mut s, "a aa a a", json!({"note": "a a", "wholeWord": true})));
|
||||
assert!(!matches(&mut s, "aa_a", json!({"note": "a", "wholeWord": true})));
|
||||
assert!(matches(&mut s, "x-ray, x", json!({"note": "x", "wholeWord": true})));
|
||||
assert!(matches(&mut s, "ÉTIQUETTE", json!({"note": "étiquette"})));
|
||||
assert!(!matches(&mut s, "ÉTIQUETTE", json!({"note": "étiquette", "caseSensitive": true})));
|
||||
assert!(matches(&mut s, "标签 一", json!({"note": "一", "wholeWord": true})));
|
||||
assert!(!matches(&mut s, "", json!({"note": "a"})));
|
||||
}
|
||||
|
||||
/// Select by Note leaves out hidden and locked objects, selects a group whose note matches without
|
||||
/// its members and a member whose note matches on its own, keeps to the isolated group, and leaves
|
||||
/// the selection as it was when it finds nothing. Bad params are errors that change nothing.
|
||||
#[test]
|
||||
fn select_by_note_takes_only_objects_that_can_be_selected() {
|
||||
let mut s = session();
|
||||
let [a, b, c, d, e] = [0.0, 10.0, 20.0, 30.0, 40.0].map(|x| rect(&mut s, x));
|
||||
for id in [a, b, c, d, e] {
|
||||
note(&mut s, id, "tag");
|
||||
}
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut s, "object.hide", json!({}));
|
||||
run(&mut s, "select.set", json!({"ids": [b.0]}));
|
||||
run(&mut s, "object.lock", json!({}));
|
||||
run(&mut s, "select.set", json!({"ids": [c.0, d.0]}));
|
||||
let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
|
||||
note(&mut s, g, "tag group");
|
||||
assert_eq!(by_note(&mut s, json!({"note": "tag"})), Ok(vec![g.0, e.0]), "the group, not its members");
|
||||
note(&mut s, g, "box");
|
||||
assert_eq!(by_note(&mut s, json!({"note": "tag"})), Ok(vec![c.0, d.0, e.0]));
|
||||
run(&mut s, "object.isolate", json!({"id": g.0}));
|
||||
assert_eq!(by_note(&mut s, json!({"note": "tag"})), Ok(vec![c.0, d.0]), "inside the isolated group only");
|
||||
run(&mut s, "select.set", json!({"ids": [c.0]}));
|
||||
let missing = by_note(&mut s, json!({"note": "nothing"})).unwrap_err();
|
||||
assert!(missing.contains("no object that can be selected has a note matching “nothing”"), "{missing}");
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [c], "the selection stays");
|
||||
for bad in [
|
||||
json!({}),
|
||||
json!({"note": " "}),
|
||||
json!({"note": 5}),
|
||||
json!({"note": "x".repeat(1025)}),
|
||||
json!({"note": "tag", "caseSensitive": "yes"}),
|
||||
json!({"note": "tag", "wholeWord": 1}),
|
||||
] {
|
||||
assert!(s.execute("select.byNote", &bad).is_err(), "{bad}");
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [c], "{bad}");
|
||||
}
|
||||
assert!(by_note(&mut s, json!({"note": "x".repeat(1024)})).is_err_and(|e| e.contains("no object")), "1,024 bytes is allowed");
|
||||
}
|
||||
|
||||
/// Under Lock Guides a guide whose note matches is not a match, and a search that finds only such
|
||||
/// guides leaves the selection and the Puppet Warp pins on it as they were.
|
||||
#[test]
|
||||
fn select_by_note_leaves_locked_guides_out_before_changing_the_selection() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0);
|
||||
let g = rect(&mut s, 50.0);
|
||||
note(&mut s, g, "edge");
|
||||
run(&mut s, "select.set", json!({"ids": [g.0]}));
|
||||
run(&mut s, "view.guides.make", json!({}));
|
||||
run(&mut s, "view.guides.lock", json!({"locked": true}));
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut s, "object.puppetWarp", json!({"id": a.0, "rest": true, "pins": [[25, 25]], "moved": [[25, 25]]}));
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
assert!(s.doc().unwrap().doc.puppet.is_some(), "a pin on the rectangle");
|
||||
let missing = by_note(&mut s, json!({"note": "edge"})).unwrap_err();
|
||||
assert!(missing.contains("no object that can be selected"), "{missing}");
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.selection.objects, [a], "the selection stays");
|
||||
assert!(st.doc.puppet.is_some(), "the pins stay");
|
||||
}
|
||||
|
||||
/// An object's attributes in other applications' XML namespaces are its own, as its `data-*`
|
||||
/// attributes are: a pasted copy and a duplicate keep them, deleting the object takes them with
|
||||
/// it, and the native file and SVG export keep them.
|
||||
|
||||
@@ -1427,6 +1427,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
"ui.action.insertSelectObject",
|
||||
"Select Object",
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index, note, caseSensitive?: bool, wholeWord?: bool} insert a Select Object step, without running it: playback runs select.byNote {note, caseSensitive, wholeWord}, which selects the objects whose Attributes note matches note, and a playback that finds no such object stops there with an error. note loses its leading and trailing spaces and must then be 1 to 1,024 bytes; caseSensitive and wholeWord default to false. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A missing or empty note, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Select Object… (dialog `actionSelectObject`, Set Selection, with note, caseSensitive, wholeWord, set, action and step) runs this on OK → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
"ui.action.continue",
|
||||
"Continue Action",
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod action_batch;
|
||||
pub mod action_insert_menu_item;
|
||||
pub mod action_new;
|
||||
pub mod action_playback;
|
||||
pub mod action_select_object;
|
||||
pub mod action_set_options;
|
||||
pub mod action_stop;
|
||||
pub mod artboard_options;
|
||||
@@ -396,6 +397,7 @@ registry! {
|
||||
ActionStopRecord: [action_stop::RECORD_KIND] => action_stop::RECORD_SPEC,
|
||||
ActionStopMessage: [action_stop::MESSAGE_KIND] => action_stop::MESSAGE_SPEC,
|
||||
ActionInsertMenuItem: [action_insert_menu_item::KIND] => action_insert_menu_item::SPEC,
|
||||
ActionSelectObject: [action_select_object::KIND] => action_select_object::SPEC,
|
||||
SliceOptions: [slices::OPTIONS] => slices::OPTIONS_SPEC,
|
||||
DivideSlices: [slices::DIVIDE] => slices::DIVIDE_SPEC,
|
||||
EpsOptions: [eps_options::KIND] => eps_options::SPEC,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Actions panel menu › Select Object… (`ui.action.insertSelectObject`): Set Selection, which adds a
|
||||
//! step that selects the objects whose Attributes note matches the note typed.
|
||||
//!
|
||||
//! Its fields: `note`, `caseSensitive`, `wholeWord`, and `set`, `action` and `step`, the place of the
|
||||
//! new step (no `set` and `action`: the recording in progress). OK is off while the note is empty.
|
||||
//! An invalid note or place is an error, and the dialog stays open.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::DialogSpec;
|
||||
use crate::state::Dialog;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
/// The dialog kind of Set Selection.
|
||||
pub const KIND: &str = "actionSelectObject";
|
||||
|
||||
/// The width of the Note label column.
|
||||
const LABEL_W: f32 = 70.0;
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |_| tl!("Set Selection").into(),
|
||||
body,
|
||||
confirm,
|
||||
ok_enabled: Some(|_, d| !d.str("note").trim().is_empty()),
|
||||
min_width: 340.0,
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// Opens Set Selection for action `target` as (set, action), after `step`, else for the recording
|
||||
/// in progress.
|
||||
pub fn open(app: &mut VectorcraftApp, target: Option<(usize, usize)>, step: Option<usize>) {
|
||||
let mut fields = json!({"note": "", "caseSensitive": false, "wholeWord": false});
|
||||
if let Some((set, action)) = target {
|
||||
fields["set"] = json!(set);
|
||||
fields["action"] = json!(action);
|
||||
if let Some(step) = step {
|
||||
fields["step"] = json!(step);
|
||||
}
|
||||
}
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
}
|
||||
|
||||
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
widgets::label_row(ui, "Note:", LABEL_W, |ui| {
|
||||
super::form::text(ui, d, "note", 240.0);
|
||||
});
|
||||
super::form::check(ui, d, "caseSensitive", "Case Sensitive");
|
||||
super::form::check(ui, d, "wholeWord", "Whole Word");
|
||||
false
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut params = json!({"note": d.str("note"), "caseSensitive": d.bool("caseSensitive"), "wholeWord": d.bool("wholeWord")});
|
||||
for key in ["set", "action", "step"] {
|
||||
if let Some(value) = d.fields.get(key) {
|
||||
params[key] = value.clone();
|
||||
}
|
||||
}
|
||||
let r = app.run(crate::panels::actions::INSERT_SELECT_OBJECT, params)?;
|
||||
app.ui.dialog = None;
|
||||
Ok(r)
|
||||
}
|
||||
@@ -5969,6 +5969,16 @@ axis Y Axis Y ос
|
||||
@msg Batch cannot play an action with a stop Пакет не може да изпълни действие със спиране
|
||||
Insert Menu Item… Вмъкване на елемент от меню…
|
||||
Insert Menu Item Вмъкване на елемент от меню
|
||||
Select Object… Избиране на обект…
|
||||
Select Object Избиране на обект
|
||||
Set Selection Задаване на селекция
|
||||
Case Sensitive Различаване на главни и малки букви
|
||||
Whole Word Цяла дума
|
||||
Select Object: {note} Избиране на обект: {note}
|
||||
Select by Note Избиране по забележка
|
||||
@msg the note is empty забележката е празна
|
||||
@msg the note is longer than 1,024 bytes забележката е по-дълга от 1024 байта
|
||||
@msg no object that can be selected has a note matching “{query}” няма обект, който може да бъде избран, със забележка, съответстваща на „{query}“
|
||||
Menu Item: Елемент от меню:
|
||||
No menu item matches. Няма съвпадащ елемент от меню.
|
||||
Several menu items match. Choose one from the list. Съвпадат няколко елемента от меню. Изберете един от списъка.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6011,6 +6011,16 @@ axis Y Axis Y osa
|
||||
@msg Batch cannot play an action with a stop Paketna obrada ne može reprodukovati akciju sa zaustavljanjem
|
||||
Insert Menu Item… Umetni stavku menija…
|
||||
Insert Menu Item Umetni stavku menija
|
||||
Select Object… Odaberi objekt…
|
||||
Select Object Odaberi objekt
|
||||
Set Selection Postavi odabir
|
||||
Case Sensitive Razlikuj velika i mala slova
|
||||
Whole Word Cijela riječ
|
||||
Select Object: {note} Odaberi objekt: {note}
|
||||
Select by Note Odaberi po napomeni
|
||||
@msg the note is empty napomena je prazna
|
||||
@msg the note is longer than 1,024 bytes napomena je duža od 1.024 bajta
|
||||
@msg no object that can be selected has a note matching “{query}” nijedan objekt koji se može odabrati nema napomenu koja odgovara „{query}”
|
||||
Menu Item: Stavka menija:
|
||||
No menu item matches. Nijedna stavka menija ne odgovara.
|
||||
Several menu items match. Choose one from the list. Odgovara više stavki menija. Izaberite jednu sa liste.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4020,6 +4020,16 @@ digits Malayalam Malaiàlam
|
||||
@msg Batch cannot play an action with a stop Lot no pot reproduir una acció amb una aturada
|
||||
Insert Menu Item… Insereix un element de menú…
|
||||
Insert Menu Item Insereix un element de menú
|
||||
Select Object… Selecciona un objecte…
|
||||
Select Object Selecciona un objecte
|
||||
Set Selection Defineix la selecció
|
||||
Case Sensitive Distingeix majúscules i minúscules
|
||||
Whole Word Paraula sencera
|
||||
Select Object: {note} Selecciona un objecte: {note}
|
||||
Select by Note Selecciona per nota
|
||||
@msg the note is empty la nota és buida
|
||||
@msg the note is longer than 1,024 bytes la nota té més de 1.024 bytes
|
||||
@msg no object that can be selected has a note matching “{query}” cap objecte que es pugui seleccionar té una nota que coincideixi amb “{query}”
|
||||
Menu Item: Element de menú:
|
||||
No menu item matches. Cap element de menú no coincideix.
|
||||
Several menu items match. Choose one from the list. Coincideixen diversos elements de menú. Trieu-ne un de la llista.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6226,6 +6226,16 @@ axis Y Axis osa Y
|
||||
@msg Batch cannot play an action with a stop Dávka nemůže přehrát akci se zastavením
|
||||
Insert Menu Item… Vložit položku nabídky…
|
||||
Insert Menu Item Vložit položku nabídky
|
||||
Select Object… Vybrat objekt…
|
||||
Select Object Vybrat objekt
|
||||
Set Selection Nastavit výběr
|
||||
Case Sensitive Rozlišovat velká a malá písmena
|
||||
Whole Word Celé slovo
|
||||
Select Object: {note} Vybrat objekt: {note}
|
||||
Select by Note Vybrat podle poznámky
|
||||
@msg the note is empty poznámka je prázdná
|
||||
@msg the note is longer than 1,024 bytes poznámka je delší než 1 024 bajtů
|
||||
@msg no object that can be selected has a note matching “{query}” žádný objekt, který lze vybrat, nemá poznámku odpovídající „{query}“
|
||||
Menu Item: Položka nabídky:
|
||||
No menu item matches. Žádná položka nabídky neodpovídá.
|
||||
Several menu items match. Choose one from the list. Odpovídá několik položek nabídky. Vyberte jednu ze seznamu.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5986,6 +5986,16 @@ axis Y Axis Y-akse
|
||||
@msg Batch cannot play an action with a stop Batch kan ikke afspille en handling med et stop
|
||||
Insert Menu Item… Indsæt menupunkt…
|
||||
Insert Menu Item Indsæt menupunkt
|
||||
Select Object… Markér objekt…
|
||||
Select Object Markér objekt
|
||||
Set Selection Angiv markering
|
||||
Case Sensitive Forskel på store og små bogstaver
|
||||
Whole Word Helt ord
|
||||
Select Object: {note} Markér objekt: {note}
|
||||
Select by Note Markér efter note
|
||||
@msg the note is empty noten er tom
|
||||
@msg the note is longer than 1,024 bytes noten er længere end 1.024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” intet objekt, der kan markeres, har en note, der matcher »{query}«
|
||||
Menu Item: Menupunkt:
|
||||
No menu item matches. Intet menupunkt passer.
|
||||
Several menu items match. Choose one from the list. Flere menupunkter passer. Vælg et fra listen.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5817,6 +5817,16 @@ axis Y Axis Y-Achse
|
||||
@msg Batch cannot play an action with a stop Die Stapelverarbeitung kann keine Aktion mit einem Stopp abspielen
|
||||
Insert Menu Item… Menübefehl einfügen…
|
||||
Insert Menu Item Menübefehl einfügen
|
||||
Select Object… Objekt auswählen…
|
||||
Select Object Objekt auswählen
|
||||
Set Selection Auswahl festlegen
|
||||
Case Sensitive Groß-/Kleinschreibung beachten
|
||||
Whole Word Ganzes Wort
|
||||
Select Object: {note} Objekt auswählen: {note}
|
||||
Select by Note Nach Notiz auswählen
|
||||
@msg the note is empty die Notiz ist leer
|
||||
@msg the note is longer than 1,024 bytes die Notiz ist länger als 1.024 Byte
|
||||
@msg no object that can be selected has a note matching “{query}” kein auswählbares Objekt hat eine Notiz, die „{query}“ entspricht
|
||||
Menu Item: Menübefehl:
|
||||
No menu item matches. Kein Menübefehl passt.
|
||||
Several menu items match. Choose one from the list. Mehrere Menübefehle passen. Wählen Sie einen aus der Liste.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6177,6 +6177,16 @@ axis Y Axis Άξονας Y
|
||||
@msg Batch cannot play an action with a stop Η μαζική επεξεργασία δεν μπορεί να αναπαράγει ενέργεια με διακοπή
|
||||
Insert Menu Item… Εισαγωγή στοιχείου μενού…
|
||||
Insert Menu Item Εισαγωγή στοιχείου μενού
|
||||
Select Object… Επιλογή αντικειμένου…
|
||||
Select Object Επιλογή αντικειμένου
|
||||
Set Selection Ορισμός επιλογής
|
||||
Case Sensitive Διάκριση πεζών-κεφαλαίων
|
||||
Whole Word Ολόκληρη λέξη
|
||||
Select Object: {note} Επιλογή αντικειμένου: {note}
|
||||
Select by Note Επιλογή βάσει σημείωσης
|
||||
@msg the note is empty η σημείωση είναι κενή
|
||||
@msg the note is longer than 1,024 bytes η σημείωση υπερβαίνει τα 1.024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” κανένα αντικείμενο που μπορεί να επιλεγεί δεν έχει σημείωση που να ταιριάζει με «{query}»
|
||||
Menu Item: Στοιχείο μενού:
|
||||
No menu item matches. Κανένα στοιχείο μενού δεν ταιριάζει.
|
||||
Several menu items match. Choose one from the list. Ταιριάζουν πολλά στοιχεία μενού. Επιλέξτε ένα από τη λίστα.
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6168,6 +6168,16 @@ axis Y Axis Eje Y
|
||||
@msg Batch cannot play an action with a stop Lote no puede reproducir una acción con una parada
|
||||
Insert Menu Item… Insertar elemento de menú…
|
||||
Insert Menu Item Insertar elemento de menú
|
||||
Select Object… Seleccionar objeto…
|
||||
Select Object Seleccionar objeto
|
||||
Set Selection Definir selección
|
||||
Case Sensitive Distinguir mayúsculas y minúsculas
|
||||
Whole Word Palabra completa
|
||||
Select Object: {note} Seleccionar objeto: {note}
|
||||
Select by Note Seleccionar por nota
|
||||
@msg the note is empty la nota está vacía
|
||||
@msg the note is longer than 1,024 bytes la nota ocupa más de 1024 bytes
|
||||
@msg no object that can be selected has a note matching “{query}” ningún objeto que se pueda seleccionar tiene una nota que coincida con “{query}”
|
||||
Menu Item: Elemento de menú:
|
||||
No menu item matches. Ningún elemento de menú coincide.
|
||||
Several menu items match. Choose one from the list. Coinciden varios elementos de menú. Elija uno de la lista.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6256,6 +6256,16 @@ axis Y Axis Y-akseli
|
||||
@msg Batch cannot play an action with a stop Erä ei voi toistaa toimintoa, jossa on pysäytys
|
||||
Insert Menu Item… Lisää valikkokohta…
|
||||
Insert Menu Item Lisää valikkokohta
|
||||
Select Object… Valitse objekti…
|
||||
Select Object Valitse objekti
|
||||
Set Selection Määritä valinta
|
||||
Case Sensitive Kirjainkoko merkitsevä
|
||||
Whole Word Koko sana
|
||||
Select Object: {note} Valitse objekti: {note}
|
||||
Select by Note Valitse huomautuksen mukaan
|
||||
@msg the note is empty huomautus on tyhjä
|
||||
@msg the note is longer than 1,024 bytes huomautus on pidempi kuin 1 024 tavua
|
||||
@msg no object that can be selected has a note matching “{query}” millään valittavissa olevalla objektilla ei ole huomautusta, joka vastaa hakua ”{query}”
|
||||
Menu Item: Valikkokohta:
|
||||
No menu item matches. Mikään valikkokohta ei vastaa hakua.
|
||||
Several menu items match. Choose one from the list. Useampi valikkokohta vastaa hakua. Valitse yksi luettelosta.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6221,6 +6221,16 @@ axis Y Axis Axe Y
|
||||
@msg Batch cannot play an action with a stop Le traitement par lots ne peut pas exécuter un script comportant un arrêt
|
||||
Insert Menu Item… Insérer une commande de menu…
|
||||
Insert Menu Item Insérer une commande de menu
|
||||
Select Object… Sélectionner un objet…
|
||||
Select Object Sélectionner un objet
|
||||
Set Selection Définir la sélection
|
||||
Case Sensitive Respecter la casse
|
||||
Whole Word Mot entier
|
||||
Select Object: {note} Sélectionner un objet : {note}
|
||||
Select by Note Sélectionner par remarque
|
||||
@msg the note is empty la remarque est vide
|
||||
@msg the note is longer than 1,024 bytes la remarque dépasse 1 024 octets
|
||||
@msg no object that can be selected has a note matching “{query}” aucun objet sélectionnable n’a de remarque correspondant à « {query} »
|
||||
Menu Item: Commande de menu :
|
||||
No menu item matches. Aucune commande de menu ne correspond.
|
||||
Several menu items match. Choose one from the list. Plusieurs commandes de menu correspondent. Choisissez-en une dans la liste.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3459,6 +3459,16 @@ antiAlias Sharp शार्प
|
||||
@msg Batch cannot play an action with a stop बैच स्टॉप वाला एक्शन नहीं चला सकता
|
||||
Insert Menu Item… मेनू आइटम जोड़ें…
|
||||
Insert Menu Item मेनू आइटम जोड़ें
|
||||
Select Object… ऑब्जेक्ट सिलेक्ट करें…
|
||||
Select Object ऑब्जेक्ट सिलेक्ट करें
|
||||
Set Selection सिलेक्शन सेट करें
|
||||
Case Sensitive केस सेंसिटिव
|
||||
Whole Word पूरा शब्द
|
||||
Select Object: {note} ऑब्जेक्ट सिलेक्ट करें: {note}
|
||||
Select by Note नोट द्वारा चुनें
|
||||
@msg the note is empty नोट खाली है
|
||||
@msg the note is longer than 1,024 bytes नोट 1,024 बाइट से लंबा है
|
||||
@msg no object that can be selected has a note matching “{query}” सिलेक्ट किए जा सकने वाले किसी भी ऑब्जेक्ट का नोट “{query}” से मेल नहीं खाता
|
||||
Menu Item: मेनू आइटम:
|
||||
No menu item matches. कोई मेनू आइटम मेल नहीं खाता।
|
||||
Several menu items match. Choose one from the list. कई मेनू आइटम मेल खाते हैं। सूची से एक चुनें।
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6209,6 +6209,16 @@ axis Y Axis Y tengely
|
||||
@msg Batch cannot play an action with a stop A kötegelt feldolgozás nem játszhat le megállást tartalmazó műveletet
|
||||
Insert Menu Item… Menüelem beszúrása…
|
||||
Insert Menu Item Menüelem beszúrása
|
||||
Select Object… Objektum kijelölése…
|
||||
Select Object Objektum kijelölése
|
||||
Set Selection Kijelölés beállítása
|
||||
Case Sensitive Kis- és nagybetűk megkülönböztetése
|
||||
Whole Word Teljes szó
|
||||
Select Object: {note} Objektum kijelölése: {note}
|
||||
Select by Note Kijelölés megjegyzés szerint
|
||||
@msg the note is empty a megjegyzés üres
|
||||
@msg the note is longer than 1,024 bytes a megjegyzés hosszabb 1024 bájtnál
|
||||
@msg no object that can be selected has a note matching “{query}” egyetlen kijelölhető objektum megjegyzése sem egyezik ezzel: „{query}”
|
||||
Menu Item: Menüelem:
|
||||
No menu item matches. Egy menüelem sem egyezik.
|
||||
Several menu items match. Choose one from the list. Több menüelem is egyezik. Válasszon egyet a listából.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5986,6 +5986,16 @@ axis Y Axis Sumbu Y
|
||||
@msg Batch cannot play an action with a stop Batch tidak dapat memutar tindakan dengan titik henti
|
||||
Insert Menu Item… Sisipkan item menu…
|
||||
Insert Menu Item Sisipkan item menu
|
||||
Select Object… Pilih objek…
|
||||
Select Object Pilih objek
|
||||
Set Selection Atur seleksi
|
||||
Case Sensitive Peka huruf besar/kecil
|
||||
Whole Word Seluruh kata
|
||||
Select Object: {note} Pilih objek: {note}
|
||||
Select by Note Pilih berdasarkan catatan
|
||||
@msg the note is empty catatan kosong
|
||||
@msg the note is longer than 1,024 bytes catatan lebih panjang dari 1.024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” tidak ada objek yang dapat dipilih dengan catatan yang cocok dengan “{query}”
|
||||
Menu Item: Item menu:
|
||||
No menu item matches. Tidak ada item menu yang cocok.
|
||||
Several menu items match. Choose one from the list. Beberapa item menu cocok. Pilih salah satu dari daftar.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2402 and column 5.
|
@@ -5851,6 +5851,16 @@ axis Y Axis Asse Y
|
||||
@msg Batch cannot play an action with a stop L'elaborazione batch non può eseguire un'azione con un'interruzione
|
||||
Insert Menu Item… Inserisci voce di menu…
|
||||
Insert Menu Item Inserisci voce di menu
|
||||
Select Object… Seleziona oggetto…
|
||||
Select Object Seleziona oggetto
|
||||
Set Selection Imposta selezione
|
||||
Case Sensitive Maiuscole/minuscole
|
||||
Whole Word Parola intera
|
||||
Select Object: {note} Seleziona oggetto: {note}
|
||||
Select by Note Seleziona per nota
|
||||
@msg the note is empty la nota è vuota
|
||||
@msg the note is longer than 1,024 bytes la nota supera 1.024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” nessun oggetto selezionabile ha una nota corrispondente a “{query}”
|
||||
Menu Item: Voce di menu:
|
||||
No menu item matches. Nessuna voce di menu corrisponde.
|
||||
Several menu items match. Choose one from the list. Corrispondono più voci di menu. Sceglierne una dall’elenco.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5916,6 +5916,16 @@ axis Y Axis Y 軸
|
||||
@msg Batch cannot play an action with a stop バッチでは停止を含むアクションを再生できません
|
||||
Insert Menu Item… メニュー項目を挿入…
|
||||
Insert Menu Item メニュー項目を挿入
|
||||
Select Object… オブジェクトを選択…
|
||||
Select Object オブジェクトを選択
|
||||
Set Selection 選択範囲を設定
|
||||
Case Sensitive 大文字と小文字を区別
|
||||
Whole Word 単語単位
|
||||
Select Object: {note} オブジェクトを選択: {note}
|
||||
Select by Note メモで選択
|
||||
@msg the note is empty メモが空です
|
||||
@msg the note is longer than 1,024 bytes メモが 1,024 バイトを超えています
|
||||
@msg no object that can be selected has a note matching “{query}” 「{query}」に一致するメモを持つ選択可能なオブジェクトはありません
|
||||
Menu Item: メニュー項目:
|
||||
No menu item matches. 一致するメニュー項目がありません。
|
||||
Several menu items match. Choose one from the list. 複数のメニュー項目が一致します。リストから 1 つ選択してください。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6177,6 +6177,16 @@ axis Y Axis Y ღერძი
|
||||
@msg Batch cannot play an action with a stop პაკეტურ დამუშავებას არ შეუძლია გაჩერების მქონე მოქმედების შესრულება
|
||||
Insert Menu Item… მენიუს ელემენტის ჩასმა…
|
||||
Insert Menu Item მენიუს ელემენტის ჩასმა
|
||||
Select Object… ობიექტის მონიშვნა…
|
||||
Select Object ობიექტის მონიშვნა
|
||||
Set Selection მონიშვნის დაყენება
|
||||
Case Sensitive რეგისტრის გათვალისწინება
|
||||
Whole Word მთელი სიტყვა
|
||||
Select Object: {note} ობიექტის მონიშვნა: {note}
|
||||
Select by Note მონიშვნა შენიშვნის მიხედვით
|
||||
@msg the note is empty შენიშვნა ცარიელია
|
||||
@msg the note is longer than 1,024 bytes შენიშვნა 1024 ბაიტზე გრძელია
|
||||
@msg no object that can be selected has a note matching “{query}” არცერთ მოსანიშნ ობიექტს არ აქვს შენიშვნა, რომელიც ემთხვევა „{query}“-ს
|
||||
Menu Item: მენიუს ელემენტი:
|
||||
No menu item matches. მენიუს არცერთი ელემენტი არ ემთხვევა.
|
||||
Several menu items match. Choose one from the list. ემთხვევა მენიუს რამდენიმე ელემენტი. აირჩიეთ ერთი სიიდან.
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5812,6 +5812,16 @@ axis Y Axis Y축
|
||||
@msg Batch cannot play an action with a stop 일괄 처리에서는 중지가 있는 액션을 재생할 수 없습니다
|
||||
Insert Menu Item… 메뉴 항목 삽입…
|
||||
Insert Menu Item 메뉴 항목 삽입
|
||||
Select Object… 개체 선택…
|
||||
Select Object 개체 선택
|
||||
Set Selection 선택 영역 설정
|
||||
Case Sensitive 대/소문자 구분
|
||||
Whole Word 전체 단어
|
||||
Select Object: {note} 개체 선택: {note}
|
||||
Select by Note 참고로 선택
|
||||
@msg the note is empty 참고가 비어 있습니다
|
||||
@msg the note is longer than 1,024 bytes 참고가 1,024바이트보다 깁니다
|
||||
@msg no object that can be selected has a note matching “{query}” 선택할 수 있는 개체 중 참고가 “{query}”와(과) 일치하는 개체가 없습니다
|
||||
Menu Item: 메뉴 항목:
|
||||
No menu item matches. 일치하는 메뉴 항목이 없습니다.
|
||||
Several menu items match. Choose one from the list. 여러 메뉴 항목이 일치합니다. 목록에서 하나를 선택하세요.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6037,6 +6037,16 @@ axis Y Axis Y-as
|
||||
@msg Batch cannot play an action with a stop Batch kan geen actie met een stop afspelen
|
||||
Insert Menu Item… Menu-item invoegen…
|
||||
Insert Menu Item Menu-item invoegen
|
||||
Select Object… Object selecteren…
|
||||
Select Object Object selecteren
|
||||
Set Selection Selectie instellen
|
||||
Case Sensitive Hoofdlettergevoelig
|
||||
Whole Word Heel woord
|
||||
Select Object: {note} Object selecteren: {note}
|
||||
Select by Note Selecteren op notitie
|
||||
@msg the note is empty de notitie is leeg
|
||||
@msg the note is longer than 1,024 bytes de notitie is langer dan 1024 bytes
|
||||
@msg no object that can be selected has a note matching “{query}” geen selecteerbaar object heeft een notitie die overeenkomt met „{query}”
|
||||
Menu Item: Menu-item:
|
||||
No menu item matches. Geen menu-item komt overeen.
|
||||
Several menu items match. Choose one from the list. Meerdere menu-items komen overeen. Kies er een uit de lijst.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6080,6 +6080,16 @@ axis Y Axis Oś Y
|
||||
@msg Batch cannot play an action with a stop Przetwarzanie wsadowe nie może odtworzyć operacji z zatrzymaniem
|
||||
Insert Menu Item… Wstaw polecenie menu…
|
||||
Insert Menu Item Wstaw polecenie menu
|
||||
Select Object… Zaznacz obiekt…
|
||||
Select Object Zaznacz obiekt
|
||||
Set Selection Ustaw zaznaczenie
|
||||
Case Sensitive Uwzględniaj wielkość liter
|
||||
Whole Word Całe słowo
|
||||
Select Object: {note} Zaznacz obiekt: {note}
|
||||
Select by Note Zaznacz według notatki
|
||||
@msg the note is empty notatka jest pusta
|
||||
@msg the note is longer than 1,024 bytes notatka jest dłuższa niż 1024 bajty
|
||||
@msg no object that can be selected has a note matching “{query}” żaden obiekt, który można zaznaczyć, nie ma notatki pasującej do „{query}”
|
||||
Menu Item: Polecenie menu:
|
||||
No menu item matches. Żadne polecenie menu nie pasuje.
|
||||
Several menu items match. Choose one from the list. Pasuje kilka poleceń menu. Wybierz jedno z listy.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6141,6 +6141,16 @@ axis Y Axis Eixo Y
|
||||
@msg Batch cannot play an action with a stop Lote não pode reproduzir uma ação com uma parada
|
||||
Insert Menu Item… Inserir Item de Menu…
|
||||
Insert Menu Item Inserir Item de Menu
|
||||
Select Object… Selecionar Objeto…
|
||||
Select Object Selecionar Objeto
|
||||
Set Selection Definir Seleção
|
||||
Case Sensitive Diferenciar Maiúsculas e Minúsculas
|
||||
Whole Word Palavra Inteira
|
||||
Select Object: {note} Selecionar Objeto: {note}
|
||||
Select by Note Selecionar por Nota
|
||||
@msg the note is empty a nota está vazia
|
||||
@msg the note is longer than 1,024 bytes a nota tem mais de 1.024 bytes
|
||||
@msg no object that can be selected has a note matching “{query}” nenhum objeto que pode ser selecionado tem uma nota correspondente a “{query}”
|
||||
Menu Item: Item de menu:
|
||||
No menu item matches. Nenhum item de menu corresponde.
|
||||
Several menu items match. Choose one from the list. Vários itens de menu correspondem. Escolha um da lista.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5837,6 +5837,16 @@ axis Y Axis Ось Y
|
||||
@msg Batch cannot play an action with a stop Пакет не может воспроизвести действие с остановкой
|
||||
Insert Menu Item… Вставить пункт меню…
|
||||
Insert Menu Item Вставить пункт меню
|
||||
Select Object… Выделить объект…
|
||||
Select Object Выделить объект
|
||||
Set Selection Задать выделение
|
||||
Case Sensitive С учетом регистра
|
||||
Whole Word Слово целиком
|
||||
Select Object: {note} Выделить объект: {note}
|
||||
Select by Note Выделить по примечанию
|
||||
@msg the note is empty примечание пустое
|
||||
@msg the note is longer than 1,024 bytes примечание длиннее 1024 байт
|
||||
@msg no object that can be selected has a note matching “{query}” ни у одного объекта, который можно выделить, нет примечания, совпадающего с «{query}»
|
||||
Menu Item: Пункт меню:
|
||||
No menu item matches. Нет подходящих пунктов меню.
|
||||
Several menu items match. Choose one from the list. Подходит несколько пунктов меню. Выберите один из списка.
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6243,6 +6243,16 @@ axis Y Axis Y-axel
|
||||
@msg Batch cannot play an action with a stop Batch kan inte spela upp en åtgärd med ett stopp
|
||||
Insert Menu Item… Infoga menyalternativ…
|
||||
Insert Menu Item Infoga menyalternativ
|
||||
Select Object… Markera objekt…
|
||||
Select Object Markera objekt
|
||||
Set Selection Ange markering
|
||||
Case Sensitive Skiftlägeskänslig
|
||||
Whole Word Helt ord
|
||||
Select Object: {note} Markera objekt: {note}
|
||||
Select by Note Markera efter anteckning
|
||||
@msg the note is empty anteckningen är tom
|
||||
@msg the note is longer than 1,024 bytes anteckningen är längre än 1 024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” inget objekt som kan markeras har en anteckning som matchar ”{query}”
|
||||
Menu Item: Menyalternativ:
|
||||
No menu item matches. Inget menyalternativ matchar.
|
||||
Several menu items match. Choose one from the list. Flera menyalternativ matchar. Välj ett i listan.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6182,6 +6182,16 @@ axis Y Axis Y ekseni
|
||||
@msg Batch cannot play an action with a stop Toplu işlem durak içeren bir eylemi oynatamaz
|
||||
Insert Menu Item… Menü Öğesi Ekle…
|
||||
Insert Menu Item Menü Öğesi Ekle
|
||||
Select Object… Nesne Seç…
|
||||
Select Object Nesne Seç
|
||||
Set Selection Seçimi Ayarla
|
||||
Case Sensitive Büyük/Küçük Harfe Duyarlı
|
||||
Whole Word Tam Sözcük
|
||||
Select Object: {note} Nesne Seç: {note}
|
||||
Select by Note Nota Göre Seç
|
||||
@msg the note is empty not boş
|
||||
@msg the note is longer than 1,024 bytes not 1.024 bayttan uzun
|
||||
@msg no object that can be selected has a note matching “{query}” seçilebilen hiçbir nesnenin notu “{query}” ile eşleşmiyor
|
||||
Menu Item: Menü öğesi:
|
||||
No menu item matches. Eşleşen menü öğesi yok.
|
||||
Several menu items match. Choose one from the list. Birden çok menü öğesi eşleşiyor. Listeden birini seçin.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5831,6 +5831,16 @@ axis Y Axis Вісь Y
|
||||
@msg Batch cannot play an action with a stop Пакет не може відтворити дію із зупинкою
|
||||
Insert Menu Item… Вставити пункт меню…
|
||||
Insert Menu Item Вставити пункт меню
|
||||
Select Object… Виділити об’єкт…
|
||||
Select Object Виділити об’єкт
|
||||
Set Selection Задати виділення
|
||||
Case Sensitive З урахуванням регістру
|
||||
Whole Word Ціле слово
|
||||
Select Object: {note} Виділити об’єкт: {note}
|
||||
Select by Note Виділити за приміткою
|
||||
@msg the note is empty примітка порожня
|
||||
@msg the note is longer than 1,024 bytes примітка довша за 1024 байти
|
||||
@msg no object that can be selected has a note matching “{query}” жоден об’єкт, який можна виділити, не має примітки, що збігається з «{query}»
|
||||
Menu Item: Пункт меню:
|
||||
No menu item matches. Немає відповідних пунктів меню.
|
||||
Several menu items match. Choose one from the list. Відповідає кілька пунктів меню. Виберіть один зі списку.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -5832,6 +5832,16 @@ axis Y Axis Trục Y
|
||||
@msg Batch cannot play an action with a stop Hàng loạt không thể phát hành động có điểm dừng
|
||||
Insert Menu Item… Chèn mục menu…
|
||||
Insert Menu Item Chèn mục menu
|
||||
Select Object… Chọn đối tượng…
|
||||
Select Object Chọn đối tượng
|
||||
Set Selection Đặt vùng chọn
|
||||
Case Sensitive Phân biệt chữ hoa chữ thường
|
||||
Whole Word Toàn bộ từ
|
||||
Select Object: {note} Chọn đối tượng: {note}
|
||||
Select by Note Chọn theo ghi chú
|
||||
@msg the note is empty ghi chú trống
|
||||
@msg the note is longer than 1,024 bytes ghi chú dài hơn 1.024 byte
|
||||
@msg no object that can be selected has a note matching “{query}” không có đối tượng nào có thể chọn có ghi chú khớp với “{query}”
|
||||
Menu Item: Mục menu:
|
||||
No menu item matches. Không có mục menu nào khớp.
|
||||
Several menu items match. Choose one from the list. Có nhiều mục menu khớp. Hãy chọn một mục trong danh sách.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -6064,6 +6064,16 @@ axis Y Axis Y 轴
|
||||
@msg Batch cannot play an action with a stop 批处理无法播放含有停止的动作
|
||||
Insert Menu Item… 插入菜单项…
|
||||
Insert Menu Item 插入菜单项
|
||||
Select Object… 选择对象…
|
||||
Select Object 选择对象
|
||||
Set Selection 设置选区
|
||||
Case Sensitive 区分大小写
|
||||
Whole Word 全字匹配
|
||||
Select Object: {note} 选择对象:{note}
|
||||
Select by Note 按注释选择
|
||||
@msg the note is empty 注释为空
|
||||
@msg the note is longer than 1,024 bytes 注释超过 1,024 字节
|
||||
@msg no object that can be selected has a note matching “{query}” 没有可选对象的注释与“{query}”匹配
|
||||
Menu Item: 菜单项:
|
||||
No menu item matches. 没有匹配的菜单项。
|
||||
Several menu items match. Choose one from the list. 有多个菜单项匹配。请从列表中选择一个。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5869,6 +5869,16 @@ axis Y Axis Y 軸
|
||||
@msg Batch cannot play an action with a stop 批次無法播放含有停止的動作
|
||||
Insert Menu Item… 插入選單項目…
|
||||
Insert Menu Item 插入選單項目
|
||||
Select Object… 選取物件…
|
||||
Select Object 選取物件
|
||||
Set Selection 設定選取範圍
|
||||
Case Sensitive 區分大小寫
|
||||
Whole Word 全字拼寫須相符
|
||||
Select Object: {note} 選取物件:{note}
|
||||
Select by Note 依註解選取
|
||||
@msg the note is empty 註解是空的
|
||||
@msg the note is longer than 1,024 bytes 註解超過 1,024 位元組
|
||||
@msg no object that can be selected has a note matching “{query}” 沒有可選取物件的註解符合「{query}」
|
||||
Menu Item: 選單項目:
|
||||
No menu item matches. 沒有相符的選單項目。
|
||||
Several menu items match. Choose one from the list. 有多個選單項目相符。請從清單中選擇一個。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -978,6 +978,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
|
||||
crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p),
|
||||
crate::panels::actions::INSERT_STOP => crate::panels::actions::insert_stop(app, p),
|
||||
crate::panels::actions::INSERT_MENU_ITEM => crate::panels::actions::insert_menu_item(app, p),
|
||||
crate::panels::actions::INSERT_SELECT_OBJECT => crate::panels::actions::insert_select_object(app, p),
|
||||
crate::panels::actions::CONTINUE_PLAYBACK => crate::panels::actions::continue_playback(app, p),
|
||||
crate::panels::actions::DELETE_STEP => crate::panels::actions::delete_step(app, p),
|
||||
"ui.batchDialog" => {
|
||||
|
||||
@@ -1428,6 +1428,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::INSERT_SELECT_OBJECT,
|
||||
"Select Object",
|
||||
"",
|
||||
"{set?: index, action?: index, step?: index, note, caseSensitive?: bool, wholeWord?: bool} insert a Select Object step, without running it: playback runs select.byNote {note, caseSensitive, wholeWord}, which selects the objects whose Attributes note matches note, and a playback that finds no such object stops there with an error. note loses its leading and trailing spaces and must then be 1 to 1,024 bytes; caseSensitive and wholeWord default to false. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A missing or empty note, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Select Object… (dialog `actionSelectObject`, Set Selection, with note, caseSensitive, wholeWord, set, action and step) runs this on OK → {set, action, step}, or {recording: true}",
|
||||
),
|
||||
(
|
||||
crate::panels::actions::CONTINUE_PLAYBACK,
|
||||
"Continue Action",
|
||||
|
||||
@@ -948,6 +948,55 @@ pub fn insert_menu_item(app: &mut VectorcraftApp, p: &Value) -> Result<Value, St
|
||||
insert(app, p, (command.to_string(), params))
|
||||
}
|
||||
|
||||
/// The command a Select Object step runs, with {note, caseSensitive, wholeWord} as its params: it
|
||||
/// selects the objects whose Attributes note matches.
|
||||
pub const SELECT_OBJECT_STEP: &str = "select.byNote";
|
||||
|
||||
/// The longest note a Select Object step searches for, in bytes, as `select.byNote` takes it.
|
||||
const MAX_SELECT_NOTE: usize = 1024;
|
||||
|
||||
/// The note, Case Sensitive and Whole Word in a Select Object step's params, the note without its
|
||||
/// leading and trailing spaces. Case Sensitive and Whole Word may be left out. No note, an empty
|
||||
/// note or one longer than 1,024 bytes, a value of another type, or params that are not an object
|
||||
/// is an error.
|
||||
pub(crate) fn select_object_params(p: &Value) -> Result<(String, bool, bool), String> {
|
||||
if !p.is_object() {
|
||||
return Err("`params` must be an object".into());
|
||||
}
|
||||
let key = "note";
|
||||
let note = match p.get(key) {
|
||||
None | Some(Value::Null) => "",
|
||||
Some(v) => v.as_str().ok_or_else(|| format!("`{key}` must be a string"))?,
|
||||
}
|
||||
.trim();
|
||||
if note.is_empty() {
|
||||
return Err("the note is empty".into());
|
||||
}
|
||||
if note.len() > MAX_SELECT_NOTE {
|
||||
return Err("the note is longer than 1,024 bytes".into());
|
||||
}
|
||||
let flag = |key: &str| match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(false),
|
||||
Some(v) => v.as_bool().ok_or_else(|| format!("`{key}` must be true or false")),
|
||||
};
|
||||
Ok((note.to_string(), flag("caseSensitive")?, flag("wholeWord")?))
|
||||
}
|
||||
|
||||
/// The command that inserts a Select Object step ([`insert_select_object`]).
|
||||
pub const INSERT_SELECT_OBJECT: &str = "ui.action.insertSelectObject";
|
||||
|
||||
/// `ui.action.insertSelectObject {set?, action?, step?, note, caseSensitive?, wholeWord?}`: inserts
|
||||
/// a Select Object step, without running it. Playback selects the objects whose Attributes note
|
||||
/// matches `note` ([`SELECT_OBJECT_STEP`]); a playback that finds none stops there with an error.
|
||||
/// The note and options are as [`select_object_params`] reads them. The place is as [`insert_stop`]
|
||||
/// takes it, and the step is selected as a stop is. Bad params, or a place [`insert_stop`]
|
||||
/// refuses, is an error, and then nothing changes. Set Selection's OK runs this. → {set, action,
|
||||
/// step}, or {recording: true}
|
||||
pub fn insert_select_object(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let (note, case_sensitive, whole_word) = select_object_params(p)?;
|
||||
insert(app, p, (SELECT_OBJECT_STEP.to_string(), json!({"note": note, "caseSensitive": case_sensitive, "wholeWord": whole_word})))
|
||||
}
|
||||
|
||||
/// The journal entry the recording in progress records from.
|
||||
fn recording_start(app: &VectorcraftApp) -> Option<usize> {
|
||||
let (_, _, start) = app.ui.recording.as_ref()?;
|
||||
@@ -1256,7 +1305,7 @@ pub const DELETE_STEP: &str = "ui.action.deleteStep";
|
||||
|
||||
/// `ui.action.deleteStep {set, action, step}`: deletes step `step`, a stop or a command, of the
|
||||
/// action at index `action` of set `set`. The steps after it keep whether they are excluded, and
|
||||
/// no step of that action stays selected. A Record Stop or Insert Menu Item dialog open on that step
|
||||
/// no step of that action stays selected. A Record Stop, Insert Menu Item or Set Selection dialog open on that step
|
||||
/// closes, and one open on a later step of the action follows it up. A missing or invalid index is an error, and
|
||||
/// then nothing changes. Undo does not bring the step back. Delete Selection runs this for the
|
||||
/// selected step. → {set, action, step}
|
||||
@@ -1567,10 +1616,12 @@ fn holds_set(d: &crate::state::Dialog) -> bool {
|
||||
|| (d.kind == crate::dialogs::action_set_options::KIND && d.fields.contains_key("set"))
|
||||
}
|
||||
|
||||
/// Whether dialog `d` is a Record Stop or Insert Menu Item dialog, which holds the place of a new
|
||||
/// step in its `set`, `action` and `step` fields.
|
||||
/// Whether dialog `d` is a Record Stop, Insert Menu Item or Set Selection dialog, which holds the
|
||||
/// place of a new step in its `set`, `action` and `step` fields.
|
||||
fn holds_step(d: &crate::state::Dialog) -> bool {
|
||||
d.kind == crate::dialogs::action_stop::RECORD_KIND || d.kind == crate::dialogs::action_insert_menu_item::KIND
|
||||
d.kind == crate::dialogs::action_stop::RECORD_KIND
|
||||
|| d.kind == crate::dialogs::action_insert_menu_item::KIND
|
||||
|| d.kind == crate::dialogs::action_select_object::KIND
|
||||
}
|
||||
|
||||
/// The command that duplicates an action or an action set ([`duplicate`]).
|
||||
@@ -1624,8 +1675,8 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
edited.actions.insert(a + 1, copy);
|
||||
edit_keeps_set_loadable(app, Some(set), &edited)?;
|
||||
app.ui.action_sets[set] = edited;
|
||||
// A Batch, Record Stop or Insert Menu Item dialog open on a later action of the set follows it
|
||||
// down.
|
||||
// A Batch, Record Stop, Insert Menu Item or Set Selection dialog open on a later action of the
|
||||
// set follows it down.
|
||||
if let Some(d) = app.ui.dialog.as_mut().filter(|d| d.kind == crate::dialogs::action_batch::KIND || holds_step(d))
|
||||
&& d.fields.get("set").and_then(index) == Some(set)
|
||||
&& let Some(da) = d.fields.get("action").and_then(index).filter(|&da| da > a)
|
||||
@@ -1706,6 +1757,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
let step = selected_step(app, ui.ctx(), selected);
|
||||
crate::dialogs::action_insert_menu_item::open(app, if recording { None } else { selected }, step);
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Select Object…"), recording || has, false) {
|
||||
let step = selected_step(app, ui.ctx(), selected);
|
||||
crate::dialogs::action_select_object::open(app, if recording { None } else { selected }, step);
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Action Options…"), has, false)
|
||||
&& let Some((s, a)) = selected
|
||||
{
|
||||
@@ -1892,10 +1947,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
current = Some(si);
|
||||
sets_selected = vec![si];
|
||||
}
|
||||
// A stop's row shows its message; other steps show their command.
|
||||
// A stop's row shows its message, and a Select Object step's its note; other
|
||||
// steps show their command.
|
||||
let shown = if command == STOP_STEP {
|
||||
let message = params.get("message").and_then(Value::as_str).and_then(|m| m.lines().next()).unwrap_or_default();
|
||||
crate::i18n::fmt(tl!("Stop: {message}"), &[("message", message)])
|
||||
} else if command == SELECT_OBJECT_STEP {
|
||||
let note = params.get("note").and_then(Value::as_str).and_then(|m| m.lines().next()).unwrap_or_default();
|
||||
crate::i18n::fmt(tl!("Select Object: {note}"), &[("note", note)])
|
||||
} else {
|
||||
command.clone()
|
||||
};
|
||||
@@ -3087,7 +3146,7 @@ mod tests {
|
||||
(json!(long.to_string()), "an action in the action set file has more than 10,000 steps"),
|
||||
(json!(" ".repeat(files::MAX_SET_FILE + 1)), "the action set file is larger than 8 MiB"),
|
||||
(
|
||||
json!("{\"format\": \"vcactions\", \"version\": 4, \"name\": \"S\", \"actions\": []}"),
|
||||
json!("{\"format\": \"vcactions\", \"version\": 5, \"name\": \"S\", \"actions\": []}"),
|
||||
"the action set file needs a later version of VectorCraft",
|
||||
),
|
||||
] {
|
||||
@@ -5520,7 +5579,7 @@ mod tests {
|
||||
json!({"format": "vcactions", "version": version, "name": "Old", "actions": [{"name": "A", "steps": [{"command": "object.move"}]}]});
|
||||
assert_eq!(files::read_set(&old.to_string(), "Test").unwrap().actions[0].steps.len(), 1, "version {version}");
|
||||
}
|
||||
let later = json!({"format": "vcactions", "version": 4, "name": "New", "actions": []});
|
||||
let later = json!({"format": "vcactions", "version": 5, "name": "New", "actions": []});
|
||||
assert_eq!(files::read_set(&later.to_string(), "Test").unwrap_err(), "the action set file needs a later version of VectorCraft");
|
||||
for params in [json!({"message": 5}), json!({"allowContinue": "yes"}), json!(42)] {
|
||||
let stop = json!({"command": "action.stop", "params": params});
|
||||
@@ -5664,4 +5723,169 @@ mod tests {
|
||||
app.run(DELETE_SETS, json!({"sets": [1]})).unwrap();
|
||||
assert_eq!(open_kind(&app), None, "deleting its set closes it");
|
||||
}
|
||||
|
||||
/// [`app_with_three_moves`] with a second rectangle whose Attributes note is "Badge front" →
|
||||
/// (the app, the first rectangle's x offset, the second rectangle's x offset).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn app_with_a_noted_rect() -> (VectorcraftApp, Box<dyn Fn(&VectorcraftApp) -> f64>, Box<dyn Fn(&VectorcraftApp) -> f64>) {
|
||||
let (mut app, x) = app_with_three_moves();
|
||||
let first = app.session.active().unwrap().selection.objects.clone();
|
||||
let id = vectorcraft_doc::NodeId(
|
||||
app.run("shape.rectangle", json!({"x": 100, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(),
|
||||
);
|
||||
app.run("attributes.set", json!({"ids": [id.0], "note": "Badge front"})).unwrap();
|
||||
app.run("select.set", json!({"ids": first.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||||
let noted = move |app: &VectorcraftApp| app.session.active().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap().x0 - 100.0;
|
||||
(app, Box::new(x), Box::new(noted))
|
||||
}
|
||||
|
||||
/// A Select Object step goes after the given step, or at the action's end, runs nothing when it
|
||||
/// goes in, and plays as `select.byNote`: the steps after it act on the objects it selects.
|
||||
#[test]
|
||||
fn insert_select_object_goes_after_the_given_step_and_plays_as_a_selection_by_note() {
|
||||
let (mut app, x, noted) = app_with_a_noted_rect();
|
||||
let selection = app.session.active().unwrap().selection.clone();
|
||||
let r = app.run(INSERT_SELECT_OBJECT, json!({"set": 0, "action": 0, "step": 0, "note": " badge "})).unwrap();
|
||||
assert_eq!(r, json!({"set": 0, "action": 0, "step": 1}));
|
||||
assert_eq!(app.session.active().unwrap().selection, selection, "inserting runs nothing");
|
||||
let action = &app.ui.action_sets[0].actions[0];
|
||||
assert_eq!(action.steps[1], (SELECT_OBJECT_STEP.to_string(), json!({"note": "badge", "caseSensitive": false, "wholeWord": false})));
|
||||
app.run(INSERT_SELECT_OBJECT, json!({"set": 0, "action": 0, "note": "Badge", "caseSensitive": true, "wholeWord": true})).unwrap();
|
||||
assert_eq!(app.ui.action_sets[0].actions[0].steps[4].1, json!({"note": "Badge", "caseSensitive": true, "wholeWord": true}));
|
||||
play(&mut app, 0, 0).unwrap();
|
||||
assert_eq!((x(&app), noted(&app)), (1.0, 6.0), "the moves after the step move the object with the note");
|
||||
let ids = &app.session.active().unwrap().selection.objects;
|
||||
assert_eq!(ids.len(), 1);
|
||||
}
|
||||
|
||||
/// A playback whose Select Object step finds no object stops there with the step's error, at
|
||||
/// every speed, and runs none of the steps after it.
|
||||
#[test]
|
||||
fn a_select_object_step_that_finds_nothing_stops_the_playback() {
|
||||
for speed in ["accelerated", "step"] {
|
||||
let (mut app, x, noted) = app_with_a_noted_rect();
|
||||
app.run(PLAYBACK_OPTIONS, json!({"speed": speed})).unwrap();
|
||||
app.ui.action_sets[0].actions[0].insert_step(0, (SELECT_OBJECT_STEP.into(), json!({"note": "missing"})));
|
||||
let selection = app.session.active().unwrap().selection.clone();
|
||||
let e = play(&mut app, 0, 0).unwrap_err();
|
||||
assert!(e.contains("no object that can be selected has a note matching “missing”"), "{speed}: {e}");
|
||||
play_out(&mut app);
|
||||
assert_eq!((x(&app), noted(&app)), (0.0, 0.0), "{speed}: nothing after it runs");
|
||||
assert_eq!(app.session.active().unwrap().selection, selection, "{speed}: the selection stays");
|
||||
assert!(app.ui.action_playing.is_none(), "{speed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_select_object_refuses_bad_params_and_changes_nothing() {
|
||||
let (mut app, _, _) = app_with_a_noted_rect();
|
||||
let place = |extra: Value| {
|
||||
let mut p = json!({"set": 0, "action": 0});
|
||||
if let (Some(p), Some(extra)) = (p.as_object_mut(), extra.as_object()) {
|
||||
p.extend(extra.clone());
|
||||
}
|
||||
p
|
||||
};
|
||||
for (params, want) in [
|
||||
(place(json!({})), "the note is empty"),
|
||||
(place(json!({"note": " \t "})), "the note is empty"),
|
||||
(place(json!({"note": 5})), "`note` must be a string"),
|
||||
(place(json!({"note": "x".repeat(1025)})), "the note is longer than 1,024 bytes"),
|
||||
(place(json!({"note": "x", "caseSensitive": "yes"})), "`caseSensitive` must be true or false"),
|
||||
(place(json!({"note": "x", "wholeWord": 0})), "`wholeWord` must be true or false"),
|
||||
(place(json!({"note": "x", "action": 5})), "no such action"),
|
||||
(place(json!({"note": "x", "step": 9})), "no such action step"),
|
||||
(json!({"note": "x"}), "no such action"),
|
||||
(json!(["note"]), "`params` must be an object"),
|
||||
] {
|
||||
assert_eq!(app.run(INSERT_SELECT_OBJECT, params.clone()).unwrap_err(), want, "{params}");
|
||||
assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "{params}");
|
||||
}
|
||||
assert!(app.run(INSERT_SELECT_OBJECT, place(json!({"note": "x".repeat(1024)}))).is_ok(), "1,024 bytes goes in");
|
||||
}
|
||||
|
||||
/// A saved Select Object step loads back as it was, in a version 4 file that older builds refuse;
|
||||
/// a file whose Select Object step has bad params is refused, also inside a recorded batch.
|
||||
#[test]
|
||||
fn a_saved_select_object_step_survives_and_bad_ones_are_refused() {
|
||||
let (mut app, _, _) = app_with_a_noted_rect();
|
||||
app.run(INSERT_SELECT_OBJECT, json!({"set": 0, "action": 0, "note": "Badge", "wholeWord": true})).unwrap();
|
||||
let saved = files::write_set(&app.ui.action_sets[0]);
|
||||
let file: Value = serde_json::from_str(&saved).unwrap();
|
||||
assert_eq!(file["version"], json!(4), "older builds must refuse a Select Object step");
|
||||
assert_eq!(
|
||||
file["actions"][0]["steps"][3],
|
||||
json!({"command": "select.byNote", "params": {"note": "Badge", "caseSensitive": false, "wholeWord": true}})
|
||||
);
|
||||
let loaded = files::read_set(&saved, "Test").unwrap();
|
||||
assert_eq!(loaded.actions[0].steps, app.ui.action_sets[0].actions[0].steps);
|
||||
app.ui.action_sets[0].actions[0].steps.push(("action.stop".into(), json!({"message": ""})));
|
||||
let file: Value = serde_json::from_str(&files::write_set(&app.ui.action_sets[0])).unwrap();
|
||||
assert_eq!(file["version"], json!(4), "with a stop too");
|
||||
for params in [json!({}), json!({"note": " "}), json!({"note": 5}), json!({"note": "a", "caseSensitive": 1}), json!(null), json!(42)] {
|
||||
let step = json!({"command": "select.byNote", "params": params});
|
||||
for step in [step.clone(), json!({"command": "command.batch", "params": {"commands": [step]}})] {
|
||||
let bad = json!({"format": "vcactions", "version": 4, "name": "Bad", "actions": [{"name": "A", "steps": [step]}]});
|
||||
assert_eq!(files::read_set(&bad.to_string(), "Test").unwrap_err(), "not an action set file", "{step}");
|
||||
}
|
||||
}
|
||||
let nested = json!({"commands": [{"command": "select.byNote", "params": {"note": "Inner"}}]});
|
||||
let set = ActionSet::new("Nested", vec![Action::new("A", vec![("command.batch".into(), nested)])]);
|
||||
assert_eq!(serde_json::from_str::<Value>(&files::write_set(&set)).unwrap()["version"], json!(4), "a step in a recorded batch counts");
|
||||
}
|
||||
|
||||
/// The panel menu's Select Object… opens Set Selection on the selected step's place, with an
|
||||
/// empty note and both options off; Cancel inserts nothing, and OK inserts the step after the
|
||||
/// selected step, whose row shows the note. While recording, the step goes into the recording.
|
||||
/// With nothing selected and nothing recording, the item is off.
|
||||
#[test]
|
||||
fn select_object_in_the_menu_opens_set_selection_and_inserts_on_ok() {
|
||||
use super::super::tests_appearance::{click, frame_events};
|
||||
let (mut app, _, _) = app_with_a_noted_rect();
|
||||
app.ui.action_set = Some(0);
|
||||
let ctx = egui::Context::default();
|
||||
app.ui_ctx = Some(ctx.clone());
|
||||
let open = |ctx: &egui::Context, app: &mut VectorcraftApp| {
|
||||
let texts = frame_events(ctx, app, vec![], menu);
|
||||
let item = texts.iter().find(|(s, _)| s.trim() == "Select Object…").map(|(_, r)| *r).expect("Select Object… in the menu");
|
||||
click(ctx, app, item.center(), menu);
|
||||
};
|
||||
open(&ctx, &mut app);
|
||||
assert_eq!(open_kind(&app), None, "nothing selected and nothing recording: the item is off");
|
||||
|
||||
click_first_step(&ctx, &mut app);
|
||||
open(&ctx, &mut app);
|
||||
let d = app.ui.dialog.as_ref().expect("Select Object… opens Set Selection");
|
||||
assert_eq!(d.kind, crate::dialogs::action_select_object::KIND);
|
||||
assert_eq!((&d.fields["set"], &d.fields["action"], &d.fields["step"]), (&json!(0), &json!(0), &json!(0)));
|
||||
assert_eq!((d.str("note"), d.bool("caseSensitive"), d.bool("wholeWord")), (String::new(), false, false));
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "Cancel inserts nothing");
|
||||
|
||||
open(&ctx, &mut app);
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
d.fields.insert("note".into(), json!("Badge"));
|
||||
d.fields.insert("caseSensitive".into(), json!(true));
|
||||
}
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(open_kind(&app), None, "OK closes Set Selection");
|
||||
let action = &app.ui.action_sets[0].actions[0];
|
||||
assert_eq!(action.steps[1], (SELECT_OBJECT_STEP.to_string(), json!({"note": "Badge", "caseSensitive": true, "wholeWord": false})));
|
||||
let texts = frame_events(&ctx, &mut app, vec![], show);
|
||||
assert!(texts.iter().any(|(s, _)| s == "Select Object: Badge"), "the step's row shows the note: {texts:?}");
|
||||
|
||||
app.run(START_RECORDING, json!({})).unwrap();
|
||||
open(&ctx, &mut app);
|
||||
assert!(!app.ui.dialog.as_ref().is_some_and(|d| d.fields.contains_key("set")), "while recording it goes into the recording");
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
d.fields.insert("note".into(), json!("While recording"));
|
||||
}
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
app.run(STOP_RECORDING, json!({})).unwrap();
|
||||
let recorded = app.ui.action_sets[0].actions.last().unwrap();
|
||||
assert_eq!(
|
||||
recorded.steps,
|
||||
[(SELECT_OBJECT_STEP.to_string(), json!({"note": "While recording", "caseSensitive": false, "wholeWord": false}))]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Action set files: the Actions panel menu's Save Actions… and Load Actions….
|
||||
//!
|
||||
//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1, 2 or 3, "name",
|
||||
//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1 to 4, "name",
|
||||
//! "actions": [{"name", "steps": [{"command", "params", "excluded"?}], "key"?, "color"?}]}`. Each step is a
|
||||
//! command id and its params, as `command.batch` takes them, or a stop: the command `action.stop`
|
||||
//! with `{"message", "allowContinue"}`. `key` is the action's function key
|
||||
//! with `{"message", "allowContinue"}`. A Select Object step is the command `select.byNote` with
|
||||
//! `{"note", "caseSensitive", "wholeWord"}`. `key` is the action's function key
|
||||
//! (`{"key": 1 to 15, "cmd", "shift"}`) and `color` its Button Mode color; an action without them,
|
||||
//! or with a value that is not one, has none. `excluded: true` keeps a step but skips it during
|
||||
//! playback. Sets with exclusions use version 2 so older builds refuse them rather than replay
|
||||
//! excluded steps, and sets with stops use version 3 so older builds refuse them rather than fail
|
||||
//! at a stop. Other sets use version 1, and earlier version 1 and 2 files still work.
|
||||
//! excluded steps, sets with stops use version 3 so older builds refuse them rather than fail at a
|
||||
//! stop, and sets with Select Object steps use version 4 so older builds refuse them rather than
|
||||
//! fail at that step. Other sets use version 1, and earlier version 1 to 3 files still work.
|
||||
//! Load Actions… also reads the action sets another app saves as text (`.aia`, through
|
||||
//! `actionSet.read`).
|
||||
|
||||
@@ -122,14 +124,25 @@ pub(super) fn step_bytes(command: &str, params: &Value) -> usize {
|
||||
/// The params of the stops in a step: a stop's own, or those of the stops a `command.batch` step
|
||||
/// holds, which playback runs as its own steps.
|
||||
fn stops_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> {
|
||||
if command == super::STOP_STEP {
|
||||
steps_in(super::STOP_STEP, command, params)
|
||||
}
|
||||
|
||||
/// The params of the Select Object steps in a step, as [`stops_in`] finds stops.
|
||||
fn select_objects_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> {
|
||||
steps_in(super::SELECT_OBJECT_STEP, command, params)
|
||||
}
|
||||
|
||||
/// The params of the steps that run command `kind` in a step: the step's own, or those of the
|
||||
/// steps a `command.batch` step holds, which playback runs as its own steps.
|
||||
fn steps_in<'a>(kind: &str, command: &str, params: &'a Value) -> Vec<&'a Value> {
|
||||
if command == kind {
|
||||
return vec![params];
|
||||
}
|
||||
if command != "command.batch" {
|
||||
return vec![];
|
||||
}
|
||||
let steps = params.get("commands").and_then(Value::as_array).into_iter().flatten();
|
||||
steps.filter(|step| super::is_stop(step)).map(|step| step.get("params").unwrap_or(&Value::Null)).collect()
|
||||
steps.filter(|step| step.get("command").and_then(Value::as_str) == Some(kind)).map(|step| step.get("params").unwrap_or(&Value::Null)).collect()
|
||||
}
|
||||
|
||||
/// `set` as the text of an action set file.
|
||||
@@ -149,11 +162,14 @@ pub fn write_set(set: &ActionSet) -> String {
|
||||
color: a.color.clone(),
|
||||
})
|
||||
.collect();
|
||||
let has_stop = set.actions.iter().flat_map(|a| &a.steps).any(|(command, params)| !stops_in(command, params).is_empty());
|
||||
let version = match (has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) {
|
||||
(true, _) => 3,
|
||||
(false, true) => 2,
|
||||
(false, false) => 1,
|
||||
let steps = || set.actions.iter().flat_map(|a| &a.steps);
|
||||
let has_select_object = steps().any(|(command, params)| !select_objects_in(command, params).is_empty());
|
||||
let has_stop = steps().any(|(command, params)| !stops_in(command, params).is_empty());
|
||||
let version = match (has_select_object, has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) {
|
||||
(true, ..) => 4,
|
||||
(false, true, _) => 3,
|
||||
(false, false, true) => 2,
|
||||
(false, false, false) => 1,
|
||||
};
|
||||
let file = SetFile { format: ACTIONS_EXT.into(), version, name: set.name.clone(), actions };
|
||||
serde_json::to_string_pretty(&file).unwrap_or_default()
|
||||
@@ -168,7 +184,7 @@ pub fn read_set(text: &str, name: &str) -> Result<ActionSet, String> {
|
||||
if f.format != ACTIONS_EXT {
|
||||
return Err("not an action set file".into());
|
||||
}
|
||||
if !matches!(f.version, 1..=3) {
|
||||
if !matches!(f.version, 1..=4) {
|
||||
return Err("the action set file needs a later version of VectorCraft".into());
|
||||
}
|
||||
if f.actions.len() > MAX_SET_ACTIONS {
|
||||
@@ -181,6 +197,10 @@ pub fn read_set(text: &str, name: &str) -> Result<ActionSet, String> {
|
||||
if stops.any(|p| super::stop_params(p).is_err()) {
|
||||
return Err("not an action set file".into());
|
||||
}
|
||||
let mut select_objects = f.actions.iter().flat_map(|a| &a.steps).flat_map(|s| select_objects_in(&s.command, &s.params));
|
||||
if select_objects.any(|p| super::select_object_params(p).is_err()) {
|
||||
return Err("not an action set file".into());
|
||||
}
|
||||
let actions = f
|
||||
.actions
|
||||
.into_iter()
|
||||
|
||||
+29
-6
@@ -2864,13 +2864,14 @@ already walks Image Trace. Short agent entry points:
|
||||
the file at `path`, else a picked one, or the file's text `data`, adds its set after the other sets and returns
|
||||
`{set, name, actions}`. A set named like one already in the panel gets the next free number (`Print 2`), and the
|
||||
other sets and the current set stay as they are. An action set file (`.vcactions`) is JSON:
|
||||
`{"format": "vcactions", "version": 1, 2 or 3, "name", "actions": [{"name", "steps": [{"command", "params", "excluded"?}]}]}`, each step
|
||||
`{"format": "vcactions", "version": 1 to 4, "name", "actions": [{"name", "steps": [{"command", "params", "excluded"?}]}]}`, each step
|
||||
a command id and its params as `command.batch` takes them, or a stop: the command `action.stop` with
|
||||
`{message, allowContinue}`. `excluded: true` preserves a step but omits it from playback;
|
||||
an absent or false flag includes the step. Files with stops use version 3, and files with excluded steps and no stops
|
||||
use version 2, so older builds refuse them
|
||||
instead of playing the excluded steps or failing at a stop; other files use version 1. A file with another `format` or a `version` other than
|
||||
1, 2 or 3, a stop whose message is not text or whose `allowContinue` is not true or false, a file larger than 8 MiB, one with more than 1,000 actions and one with an action of more than 10,000 steps are
|
||||
`{message, allowContinue}`. A Select Object step is the command `select.byNote` with `{note, caseSensitive,
|
||||
wholeWord}`. `excluded: true` preserves a step but omits it from playback;
|
||||
an absent or false flag includes the step. Files with Select Object steps use version 4, files with stops and no
|
||||
Select Object step use version 3, and files with excluded steps and neither use version 2, so older builds refuse them
|
||||
instead of playing the excluded steps or failing at a stop or a Select Object step; other files use version 1. A file with another `format` or a `version` other than
|
||||
1 to 4, a stop whose message is not text or whose `allowContinue` is not true or false, a Select Object step whose params `ui.action.insertSelectObject` would refuse, a file larger than 8 MiB, one with more than 1,000 actions and one with an action of more than 10,000 steps are
|
||||
refused, and no set is added. So is a file whose set `ui.actionSet.save` would write larger than 8 MiB (a file with
|
||||
less white space, a set name that gets a number, or an `.aia` set), with the error `the action set would be larger
|
||||
than 8 MiB`. The Actions panel menu's Save Actions… and Load Actions… run these; Load Actions… is
|
||||
@@ -2963,6 +2964,28 @@ already walks Image Trace. Short agent entry points:
|
||||
`actionInsertMenuItem` dialog (`find`, `command`, `params`, `set`, `action`, `step`): typing in Find and pressing
|
||||
Find picks the one item whose menu path, shown name or command id holds every word typed, or lists the matches, and
|
||||
OK runs this.
|
||||
- Actions: `ui.action.insertSelectObject {set?, action?, step?, note, caseSensitive?, wholeWord?}` inserts a Select
|
||||
Object step without running it. The step is `select.byNote {note, caseSensitive, wholeWord}`: during playback it
|
||||
selects the objects whose Attributes note matches `note`, and the steps after it act on them. `note` loses its
|
||||
leading and trailing spaces and must then be 1 to 1,024 bytes; `caseSensitive` and `wholeWord` default to false. The
|
||||
place, the selected step and the result are as `ui.action.insertStop` has them. A missing or empty note, a param of
|
||||
another type, params that are not an object, an index past the last set, action or step, no action and no
|
||||
recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB is an
|
||||
error, and then nothing changes. When the step finds no object, the playback stops there with that error: at
|
||||
Accelerated the whole batch is undone as for any failing step, and at Step by Step and Pause For the steps already
|
||||
played stay. The panel's row for the step reads `Select Object: <note>`. The panel menu's Select Object… opens the
|
||||
`actionSelectObject` dialog, Set Selection (`note`, `caseSensitive`, `wholeWord`, `set`, `action`, `step`), whose OK
|
||||
runs this; OK is off while the note is empty. The two options, the 1,024-byte limit and stopping when nothing is
|
||||
found are VectorCraft's own rules.
|
||||
- Select: `select.byNote {note, caseSensitive?, wholeWord?}` selects the objects whose Attributes note
|
||||
(`attributes.set {note}`) holds `note` without its leading and trailing spaces, and returns `{count, ids}`. By default
|
||||
the text may be anywhere in the note and in any letter case. `caseSensitive: true` asks for the same letter case, and
|
||||
`wholeWord: true` for no letter, digit or `_` right before or after the text. It looks at the objects Select All
|
||||
takes and the objects inside them, and leaves out hidden and locked objects and art on a locked artboard; in
|
||||
isolation mode it looks inside the isolated group only. An object whose note matches is selected without the objects
|
||||
inside it, and a member whose note matches is selected on its own. The objects found replace the selection, in paint
|
||||
order. The command makes no undo step, and Select › Reselect repeats it. An empty note, a note longer than 1,024
|
||||
bytes, a param of another type, or no object found is an error, and then the selection stays as it was.
|
||||
- Actions: `ui.action.continue {}` plays on from the stop the playing action waits at, on the document active now, and
|
||||
closes the stop's message box (`actionStopMessage`). The next step runs at once, or at Accelerated the steps up to
|
||||
the next stop as one undo step. It returns `{results, pending, atStop?}`. With no action waiting at a stop it is an
|
||||
|
||||
@@ -731,6 +731,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed.
|
||||
- Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`).
|
||||
- Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern, and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too.
|
||||
- Actions panel: the panel menu's Select Object… opens Set Selection, where the note typed names the objects to select, and adds a step to the recording in progress, after the selected step, or at the end of the selected action; playback selects the objects whose Attributes note matches it, and the steps after it act on them ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertSelectObject`, `select.byNote`). VectorCraft's own rules, which the page does not cover: the note matches anywhere and in any letter case unless Case Sensitive or Whole Word is checked, hidden and locked objects are left out, the objects found replace the selection, a step that finds none stops the playback with an error, and the note is at most 1,024 bytes. Action sets with the step save as version 4.
|
||||
- Actions panel: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back.
|
||||
- Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected.
|
||||
- Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.
|
||||
|
||||
Reference in New Issue
Block a user