Merge pull request #5172 from dotCooCoo/feat/action-options-rename

Actions: the panel menu's Action Options… renames the selected action
This commit is contained in:
BFlat
2026-11-02 14:48:37 +07:00
committed by GitHub
33 changed files with 298 additions and 17 deletions
@@ -1,8 +1,11 @@
//! Actions panel › Create New Set (and ≡ › New Set…) and ≡ › Set Options…: the name of a new action
//! set, or of the current one.
//! Actions panel › Create New Set (and ≡ › New Set…), ≡ › Set Options… and ≡ › Action Options…:
//! the name of a new action set, of the current one, or of the selected action.
//!
//! Fields: `set` (the index of the set renamed; none for a new set) and `name`. OK adds the set and
//! makes it the current one, or renames the set.
//! Fields: `set` (the index of the set renamed, or of the action's set; none for a new set),
//! `action` (the index of the action renamed, in `set`; none for a set) and `name`. OK adds the set
//! and makes it the current one, renames the set, or renames the action (`ui.action.rename`). A
//! `set` or `action` field that is not the index of a set or action is an error, OK then changes
//! no set or action, and the dialog stays open.
use serde_json::{Value, json};
@@ -10,14 +13,20 @@ use super::DialogSpec;
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of New Set and Set Options.
/// The dialog kind of New Set, Set Options and Action Options.
pub const KIND: &str = "actionSetOptions";
pub(super) const SPEC: DialogSpec =
DialogSpec { heading, body, confirm, ok_enabled: Some(|_, d| !d.str("name").trim().is_empty()), min_width: 300.0, ..DialogSpec::FORM };
fn heading(d: &Dialog) -> String {
if d.fields.contains_key("set") { tl!("Set Options").into() } else { tl!("New Set").into() }
if d.fields.contains_key("action") {
tl!("Action Options").into()
} else if d.fields.contains_key("set") {
tl!("Set Options").into()
} else {
tl!("New Set").into()
}
}
/// Open New Set, named "Set N" with the first number no set uses.
@@ -34,6 +43,13 @@ pub fn open(app: &mut VectorcraftApp, set: usize) {
}
}
/// Open Action Options for the action at `action` in set `set`, if there is one.
pub fn open_action(app: &mut VectorcraftApp, set: usize, action: usize) {
if let Some(a) = app.ui.action_sets.get(set).and_then(|s| s.actions.get(action)) {
app.ui.dialog = Some(Dialog::new(KIND, json!({"set": set, "action": action, "name": a.name})));
}
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
widgets::label_row(ui, "Name:", 50.0, |ui| {
super::form::text(ui, d, "name", 200.0);
@@ -47,17 +63,30 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
if name.is_empty() {
return Ok(Value::Null);
}
match d.fields.get("set").and_then(Value::as_u64) {
Some(set) => {
if let Some(s) = app.ui.action_sets.get_mut(set as usize) {
s.name = name;
}
}
None => {
app.ui.action_sets.push(crate::panels::actions::ActionSet::new(name, vec![]));
app.ui.action_set = Some(app.ui.action_sets.len() - 1);
}
// The fields the dialog holds choose what OK does, as they choose its heading.
if d.fields.contains_key("action") {
app.run(crate::panels::actions::RENAME_ACTION, json!({"set": d.fields.get("set"), "action": d.fields.get("action"), "name": name}))?;
} else if let Some(set) = d.fields.get("set") {
let s = set.as_u64().and_then(|i| usize::try_from(i).ok()).and_then(|i| app.ui.action_sets.get_mut(i)).ok_or("no such action set")?;
s.name = name;
} else {
app.ui.action_sets.push(crate::panels::actions::ActionSet::new(name, vec![]));
app.ui.action_set = Some(app.ui.action_sets.len() - 1);
}
app.ui.dialog = None;
Ok(Value::Null)
}
#[cfg(test)]
mod tests {
use super::*;
/// The heading names the dialog by what it holds: an action, a set, or neither.
#[test]
fn the_heading_names_action_options_set_options_or_new_set() {
let heading_of = |fields: Value| heading(&Dialog::new(KIND, fields));
assert_eq!(heading_of(json!({"set": 1, "action": 0, "name": "Nudge"})), "Action Options");
assert_eq!(heading_of(json!({"set": 1, "name": "Mine"})), "Set Options");
assert_eq!(heading_of(json!({"name": "Set 1"})), "New Set");
}
}
+3
View File
@@ -5561,6 +5561,9 @@ axis Y Axis Y ос
New Set Нов набор
Set Options… Опции на набора…
Set Options Опции на набора
Action Options… Опции на действието…
Action Options Опции на действието
Rename Action Преименуване на действие
Create New Set Създаване на нов набор
Stop Global Edit Край на глобалното редактиране
Global Edit Options Опции за глобално редактиране
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5584,6 +5584,9 @@ axis Y Axis Y osa
New Set Novi skup
Set Options… Opcije skupa…
Set Options Opcije skupa
Action Options… Opcije akcije…
Action Options Opcije akcije
Rename Action Preimenuj akciju
Create New Set Napravi novi skup
Stop Global Edit Zaustavi globalno uređivanje
Global Edit Options Opcije globalnog uređivanja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3744,6 +3744,9 @@ digits Malayalam Malaiàlam
Set Keyboard Shortcut Defineix la drecera de teclat
Set Mesh Point Color Defineix el color del punt de malla
Set Options Defineix les opcions
Action Options… Opcions de l’acció…
Action Options Opcions de l’acció
Rename Action Canvia el nom de l’acció
Set Options… Defineix les opcions…
Set Preference Defineix la preferència
Set Slice Rectangle Defineix el rectangle del tall
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5673,6 +5673,9 @@ axis Y Axis osa Y
New Set Nová sada
Set Options… Možnosti sady…
Set Options Možnosti sady
Action Options… Možnosti akce…
Action Options Možnosti akce
Rename Action Přejmenovat akci
Create New Set Vytvořit novou sadu
Stop Global Edit Ukončit globální úpravy
Global Edit Options Možnosti globálních úprav
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5577,6 +5577,9 @@ axis Y Axis Y-akse
New Set Nyt sæt
Set Options… Indstillinger for sæt…
Set Options Indstillinger for sæt
Action Options… Indstillinger for handling…
Action Options Indstillinger for handling
Rename Action Omdøb handling
Create New Set Opret nyt sæt
Stop Global Edit Stop global redigering
Global Edit Options Indstillinger for global redigering
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5255,6 +5255,9 @@ axis Y Axis Y-Achse
New Set Neuer Satz
Set Options… Satzoptionen…
Set Options Satzoptionen
Action Options… Aktionsoptionen…
Action Options Aktionsoptionen
Rename Action Aktion umbenennen
Create New Set Neuen Satz erstellen
Stop Global Edit Globale Bearbeitung beenden
Global Edit Options Optionen für globale Bearbeitung
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5624,6 +5624,9 @@ axis Y Axis Άξονας Y
New Set Νέο σύνολο
Set Options… Επιλογές συνόλου…
Set Options Επιλογές συνόλου
Action Options… Επιλογές ενέργειας…
Action Options Επιλογές ενέργειας
Rename Action Μετονομασία ενέργειας
Create New Set Δημιουργία νέου συνόλου
Stop Global Edit Τερματισμός καθολικής επεξεργασίας
Global Edit Options Επιλογές καθολικής επεξεργασίας
Can't render this file because it contains an unexpected character in line 163 and column 5.
+3
View File
@@ -5614,6 +5614,9 @@ axis Y Axis Eje Y
New Set Nuevo conjunto
Set Options… Opciones de conjunto…
Set Options Opciones de conjunto
Action Options… Opciones de acción…
Action Options Opciones de acción
Rename Action Cambiar nombre de acción
Create New Set Crear nuevo conjunto
Stop Global Edit Detener edición global
Global Edit Options Opciones de edición global
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5703,6 +5703,9 @@ axis Y Axis Y-akseli
New Set Uusi sarja
Set Options… Sarjan asetukset…
Set Options Sarjan asetukset
Action Options… Toiminnon asetukset…
Action Options Toiminnon asetukset
Rename Action Nimeä toiminto uudelleen
Create New Set Luo uusi sarja
Stop Global Edit Lopeta yleismuokkaus
Global Edit Options Yleismuokkauksen asetukset
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5670,6 +5670,9 @@ axis Y Axis Axe Y
New Set Nouveau jeu
Set Options… Options du jeu…
Set Options Options du jeu
Action Options… Options de script…
Action Options Options de script
Rename Action Renommer le script
Create New Set Créer un jeu
Stop Global Edit Arrêter la modification globale
Global Edit Options Options de modification globale
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5658,6 +5658,9 @@ axis Y Axis Y tengely
New Set Új készlet
Set Options… Készlet beállításai…
Set Options Készlet beállításai
Action Options… Művelet beállításai…
Action Options Művelet beállításai
Rename Action Művelet átnevezése
Create New Set Új készlet létrehozása
Stop Global Edit Globális szerkesztés befejezése
Global Edit Options Globális szerkesztés beállításai
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5577,6 +5577,9 @@ axis Y Axis Sumbu Y
New Set Set baru
Set Options… Opsi set…
Set Options Opsi set
Action Options… Opsi tindakan…
Action Options Opsi tindakan
Rename Action Ganti nama tindakan
Create New Set Buat set baru
Stop Global Edit Hentikan edit global
Global Edit Options Opsi edit global
Can't render this file because it contains an unexpected character in line 2194 and column 5.
+3
View File
@@ -5289,6 +5289,9 @@ axis Y Axis Asse Y
New Set Nuovo set
Set Options… Opzioni set…
Set Options Opzioni set
Action Options… Opzioni azione…
Action Options Opzioni azione
Rename Action Rinomina azione
Create New Set Crea nuovo set
Stop Global Edit Interrompi modifica globale
Global Edit Options Opzioni modifica globale
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5356,6 +5356,9 @@ axis Y Axis Y 軸
New Set 新規セット
Set Options… セットオプション…
Set Options セットオプション
Action Options… アクションオプション…
Action Options アクションオプション
Rename Action アクション名を変更
Create New Set 新規セットを作成
Stop Global Edit グローバル編集を終了
Global Edit Options グローバル編集オプション
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5587,6 +5587,9 @@ axis Y Axis Y ღერძი
New Set ახალი ნაკრები
Set Options… ნაკრების პარამეტრები…
Set Options ნაკრების პარამეტრები
Action Options… მოქმედების პარამეტრები…
Action Options მოქმედების პარამეტრები
Rename Action მოქმედების სახელის შეცვლა
Create New Set ახალი ნაკრების შექმნა
Stop Global Edit გლობალური რედაქტირების დასრულება
Global Edit Options გლობალური რედაქტირების პარამეტრები
Can't render this file because it is too large.
+3
View File
@@ -5252,6 +5252,9 @@ axis Y Axis Y축
New Set 새 세트
Set Options… 세트 옵션…
Set Options 세트 옵션
Action Options… 액션 옵션…
Action Options 액션 옵션
Rename Action 액션 이름 바꾸기
Create New Set 새 세트 만들기
Stop Global Edit 전체 편집 중지
Global Edit Options 전체 편집 옵션
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5621,6 +5621,9 @@ axis Y Axis Y-as
New Set Nieuwe set
Set Options… Setopties…
Set Options Setopties
Action Options… Actieopties…
Action Options Actieopties
Rename Action Actie hernoemen
Create New Set Nieuwe set maken
Stop Global Edit Globale bewerking stoppen
Global Edit Options Opties voor globale bewerking
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5526,6 +5526,9 @@ axis Y Axis Oś Y
New Set Nowy zestaw
Set Options… Opcje zestawu…
Set Options Opcje zestawu
Action Options… Opcje operacji…
Action Options Opcje operacji
Rename Action Zmień nazwę operacji
Create New Set Utwórz nowy zestaw
Stop Global Edit Zakończ edycję globalną
Global Edit Options Opcje edycji globalnej
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5588,6 +5588,9 @@ axis Y Axis Eixo Y
New Set Novo Conjunto
Set Options… Opções do Conjunto…
Set Options Opções do Conjunto
Action Options… Opções da Ação…
Action Options Opções da Ação
Rename Action Renomear Ação
Create New Set Criar Novo Conjunto
Stop Global Edit Parar Edição Global
Global Edit Options Opções de Edição Global
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -5284,6 +5284,9 @@ axis Y Axis Ось Y
New Set Новый набор
Set Options… Параметры набора…
Set Options Параметры набора
Action Options… Параметры действия…
Action Options Параметры действия
Rename Action Переименовать действие
Create New Set Создать новый набор
Stop Global Edit Завершить глобальное редактирование
Global Edit Options Параметры глобального редактирования
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5696,6 +5696,9 @@ axis Y Axis Y-axel
New Set Ny uppsättning
Set Options… Alternativ för uppsättning…
Set Options Alternativ för uppsättning
Action Options… Alternativ för åtgärd…
Action Options Alternativ för åtgärd
Rename Action Byt namn på åtgärd
Create New Set Skapa ny uppsättning
Stop Global Edit Avsluta global redigering
Global Edit Options Alternativ för global redigering
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5630,6 +5630,9 @@ axis Y Axis Y ekseni
New Set Yeni Set
Set Options… Set Seçenekleri…
Set Options Set Seçenekleri
Action Options… Eylem Seçenekleri…
Action Options Eylem Seçenekleri
Rename Action Eylemi Yeniden Adlandır
Create New Set Yeni Set Oluştur
Stop Global Edit Genel Düzenlemeyi Durdur
Global Edit Options Genel Düzenleme Seçenekleri
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5269,6 +5269,9 @@ axis Y Axis Вісь Y
New Set Новий набір
Set Options… Параметри набору…
Set Options Параметри набору
Action Options… Параметри дії…
Action Options Параметри дії
Rename Action Перейменувати дію
Create New Set Створити новий набір
Stop Global Edit Завершити глобальне редагування
Global Edit Options Параметри глобального редагування
Can't render this file because it contains an unexpected character in line 177 and column 5.
+3
View File
@@ -5272,6 +5272,9 @@ axis Y Axis Trục Y
New Set Bộ mới
Set Options… Tùy chọn bộ…
Set Options Tùy chọn bộ
Action Options… Tùy chọn hành động…
Action Options Tùy chọn hành động
Rename Action Đổi tên hành động
Create New Set Tạo bộ mới
Stop Global Edit Dừng chỉnh sửa toàn cục
Global Edit Options Tùy chọn chỉnh sửa toàn cục
Can't render this file because it contains an unexpected character in line 199 and column 5.
+3
View File
@@ -5513,6 +5513,9 @@ axis Y Axis Y 轴
New Set 新建组
Set Options… 组选项…
Set Options 组选项
Action Options… 动作选项…
Action Options 动作选项
Rename Action 重命名动作
Create New Set 创建新组
Stop Global Edit 停止全局编辑
Global Edit Options 全局编辑选项
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5309,6 +5309,9 @@ axis Y Axis Y 軸
New Set 新增組
Set Options… 組選項…
Set Options 組選項
Action Options… 動作選項…
Action Options 動作選項
Rename Action 重新命名動作
Create New Set 建立新組
Stop Global Edit 停止整體編輯
Global Edit Options 整體編輯選項
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -783,6 +783,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
crate::dialogs::find_links::find_in_folder_command(app, s("folder"), p.get("ids").and_then(Value::as_array).cloned())
}
"ui.packageDialog" => crate::dialogs::package::open(app),
crate::panels::actions::RENAME_ACTION => crate::panels::actions::rename_action(app, p),
"ui.batchDialog" => {
let set = p.get("set").and_then(Value::as_u64).unwrap_or(0) as usize;
let action = p.get("action").and_then(Value::as_u64).unwrap_or(0) as usize;
+6
View File
@@ -1113,6 +1113,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"Cmd+Alt+Shift+P",
"{} open Package for the saved document (dialog `package`: folder, name, copyLinks, linksFolder, relink, copyFonts, report; a document never saved asks to Save As first); OK saves unsaved changes, runs file.package (the web downloads the zip) and offers file.showPackage",
),
(
crate::panels::actions::RENAME_ACTION,
"Rename Action",
"",
"{set: index, action: index, name} rename the Actions panel's action at action in set (0 is the top set) to name, with spaces trimmed from both ends; its steps stay. The panel menu's Action Options… (dialog `actionSetOptions` with set, action and name) runs this for the selected action. An index past the last set or action, or an empty name, is an error and renames nothing → {name}",
),
(
"ui.batchDialog",
"Batch…",
+162 -1
View File
@@ -76,6 +76,24 @@ pub fn play(app: &mut VectorcraftApp, set: usize, idx: usize) -> Result<Value, S
app.run("command.batch", json!({"label": a.name, "commands": commands}))
}
/// The command that renames an action ([`rename_action`]).
pub const RENAME_ACTION: &str = "ui.action.rename";
/// `ui.action.rename {set, action, name}`: renames the action at index `action` of set `set` (0 is
/// the top set) to `name` with spaces trimmed from both ends, and keeps its steps. An index past
/// the last set or action, or a name with nothing but spaces, is an error, and then nothing is
/// renamed. The panel menu's Action Options… runs this for the selected action. → {name}
pub fn rename_action(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let index = |k: &str| p.get(k).and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok());
let name = p.get("name").and_then(Value::as_str).map(str::trim).unwrap_or_default();
let action = index("set").zip(index("action")).and_then(|(s, a)| app.ui.action_sets.get_mut(s)?.actions.get_mut(a)).ok_or("no such action")?;
if name.is_empty() {
return Err("name is empty".into());
}
action.name = name.to_string();
Ok(json!({ "name": name }))
}
/// The command that deletes action sets ([`delete_sets`]).
pub const DELETE_SETS: &str = "ui.actionSet.delete";
@@ -163,6 +181,14 @@ pub fn delete_sets(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
d.fields.insert("set".into(), json!(set - sets.iter().filter(|&&s| s < set).count()));
}
}
// The panel's selected action follows its set down too, and is no longer selected when its set
// is deleted, so a set made later at its old index does not find it selected.
if let Some(ctx) = app.ui_ctx.clone()
&& let Some((set, action)) = pstate::<Option<(usize, usize)>>(&ctx, "actions-selected")
{
let moved = (!sets.contains(&set)).then(|| (set - sets.iter().filter(|&&s| s < set).count(), action));
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", moved);
}
for &s in sets.iter().rev() {
app.ui.action_sets.remove(s);
}
@@ -257,7 +283,9 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
/// The panel's (≡) menu.
pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
let selected: Option<(usize, usize)> = pstate(ui.ctx(), "actions-selected");
// A selected action counts only while its set is the current one, as the panel shows it: a set
// deleted since leaves the stored pair pointing at another set's action.
let selected = pstate::<Option<(usize, usize)>>(ui.ctx(), "actions-selected").filter(|&(s, _)| app.ui.action_set == Some(s));
let has = selected.and_then(|(s, a)| app.ui.action_sets.get(s).and_then(|set| set.actions.get(a))).is_some();
if widgets::menu_item(ui, tl!("New Set…"), true, false) {
crate::dialogs::action_set_options::open_new(app);
@@ -286,6 +314,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
{
app.status(e);
}
if widgets::menu_item(ui, tl!("Action Options…"), has, false)
&& let Some((s, a)) = selected
{
crate::dialogs::action_set_options::open_action(app, s, a);
}
}
pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
@@ -532,6 +565,134 @@ mod tests {
assert_eq!(names, [("Default Actions", 6), ("Web", 0)], "the clicked set is renamed, the other kept");
}
/// #1629: the panel menu's Action Options… is off with no action selected, and with an action
/// selected it asks for a new name: OK renames that action and keeps its steps, Cancel keeps
/// the name.
#[test]
fn action_options_renames_the_selected_action() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = app_with_sets();
let ctx = egui::Context::default();
let action_options = |ctx: &egui::Context, app: &mut VectorcraftApp| {
let texts = frame_events(ctx, app, vec![], menu);
let item = texts.iter().find(|(s, _)| s.trim() == "Action Options…").map(|(_, r)| *r).expect("Action Options… in the menu");
click(ctx, app, item.center(), menu);
};
action_options(&ctx, &mut app);
assert!(app.ui.dialog.is_none(), "no action selected: Action Options… is off");
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "Nudge Again").center(), show);
frame_events(&ctx, &mut app, vec![], show);
action_options(&ctx, &mut app);
let d = app.ui.dialog.as_ref().expect("Action Options opens");
assert_eq!((d.kind.as_str(), d.str("name")), (crate::dialogs::action_set_options::KIND, "Nudge Again".to_string()));
crate::dialogs::cancel(&mut app);
assert_eq!(app.ui.action_sets[1].actions[1].name, "Nudge Again", "Cancel keeps the name");
action_options(&ctx, &mut app);
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!(" Nudge Right "));
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let names = app.ui.action_sets[1].actions.iter().map(|a| a.name.as_str()).collect::<Vec<_>>();
assert_eq!(names, ["Nudge", "Nudge Right"], "the selected action is renamed, trimmed; the other is kept");
assert_eq!(app.ui.action_sets[1].actions[1].steps, [("object.move".to_string(), json!({"dx": 1, "dy": 0}))], "the steps stay");
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"], "no set is renamed");
}
/// `ui.action.rename` renames the action at its set and action indexes and refuses a missing
/// index or a name with nothing but spaces, renaming nothing.
#[test]
fn rename_action_renames_one_action_and_refuses_bad_params() {
let mut app = app_with_sets();
let names = |app: &VectorcraftApp| app.ui.action_sets[1].actions.iter().map(|a| a.name.clone()).collect::<Vec<_>>();
assert_eq!(app.run(RENAME_ACTION, json!({"set": 1, "action": 0, "name": "Push"})).unwrap(), json!({"name": "Push"}));
assert_eq!(names(&app), ["Push", "Nudge Again"]);
for p in [
json!({"set": 1, "action": 2, "name": "X"}),
json!({"set": 3, "action": 0, "name": "X"}),
json!({"set": 1, "name": "X"}),
json!({"action": 0, "name": "X"}),
json!({"set": 4_294_967_297u64, "action": 0, "name": "X"}),
] {
assert_eq!(app.run(RENAME_ACTION, p.clone()).unwrap_err(), "no such action", "{p}");
}
assert_eq!(app.run(RENAME_ACTION, json!({"set": 1, "action": 0, "name": " "})).unwrap_err(), "name is empty");
assert_eq!(app.run(RENAME_ACTION, json!({"set": 1, "action": 0})).unwrap_err(), "name is empty");
assert_eq!(names(&app), ["Push", "Nudge Again"], "a refused rename renames nothing");
}
/// OK in Action Options or Set Options whose `set` or `action` field `ui.dialog.set` changed to
/// something that is not an index is an error: it renames no set or action, adds no set, and
/// keeps the dialog open.
#[test]
fn options_with_a_field_that_is_not_an_index_change_nothing() {
use crate::dialogs::action_set_options::KIND;
let mut app = app_with_sets();
for fields in [
json!({"set": 1, "action": null, "name": "X"}),
json!({"set": null, "action": 0, "name": "X"}),
json!({"set": 1, "action": 9, "name": "X"}),
json!({"set": null, "name": "X"}),
json!({"set": 9, "name": "X"}),
] {
app.ui.dialog = Some(crate::state::Dialog::new(KIND, fields.clone()));
assert!(crate::dialogs::confirm(&mut app).is_err(), "{fields}");
assert!(app.ui.dialog.is_some(), "the dialog stays open: {fields}");
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"], "{fields}");
let names = app.ui.action_sets[1].actions.iter().map(|a| a.name.as_str()).collect::<Vec<_>>();
assert_eq!(names, ["Nudge", "Nudge Again"], "{fields}");
}
}
/// After `ui.actionSet.delete` removes a set above the selected action's set, the panel menu's
/// Action Options… is off: it does not open for the action that moved to the stored position.
#[test]
fn action_options_is_off_after_a_set_above_the_selection_is_deleted() {
use super::super::tests_appearance::{click, frame_events};
let mut app = app_with_sets();
let action = |name: &str| Action { name: name.into(), steps: vec![] };
app.ui.action_sets[2].actions.push(action("Spin"));
app.ui.action_sets.push(ActionSet::new("Last", vec![action("Wrong")]));
let ctx = egui::Context::default();
app.ui.action_set = Some(2);
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((2, 0)));
app.run(DELETE_SETS, json!({"sets": [1]})).unwrap();
assert_eq!(app.ui.action_set, Some(1), "Other is still current");
let texts = frame_events(&ctx, &mut app, vec![], menu);
let item = texts.iter().find(|(s, _)| s.trim() == "Action Options…").map(|(_, r)| *r).expect("Action Options… in the menu");
click(&ctx, &mut app, item.center(), menu);
assert!(app.ui.dialog.is_none(), "Action Options… does not open for Last's action");
}
/// `ui.actionSet.delete` moves the panel's selected action down with its set, and clears it
/// when its set is deleted: a new set made at that index, and the action recorded into it, are
/// not selected, so the panel menu's Action Options… stays off.
#[test]
fn deleting_sets_moves_or_clears_the_selected_action() {
use super::super::tests_appearance::{click, frame_events};
let selected = |ctx: &egui::Context| pstate::<Option<(usize, usize)>>(ctx, "actions-selected");
let mut app = app_with_sets();
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
app.ui.action_set = Some(1);
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((1, 1)));
app.run(DELETE_SETS, json!({"sets": [0]})).unwrap();
assert_eq!(selected(&ctx), Some((0, 1)), "Nudge Again is still selected, at its set's new index");
app.ui.action_sets.push(ActionSet::new("Last", vec![]));
app.ui.action_set = Some(2);
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((2, 0)));
app.ui.action_sets[2].actions.push(Action { name: "Spin".into(), steps: vec![] });
app.run(DELETE_SETS, json!({"sets": [2]})).unwrap();
assert_eq!(selected(&ctx), None, "the deleted set's action is no longer selected");
crate::dialogs::action_set_options::open_new(&mut app);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.ui.action_set, Some(2), "the new set is current, at the deleted set's index");
app.ui.action_sets[2].actions.push(Action { name: "Recorded".into(), steps: vec![] });
let texts = frame_events(&ctx, &mut app, vec![], menu);
let item = texts.iter().find(|(s, _)| s.trim() == "Action Options…").map(|(_, r)| *r).expect("Action Options… in the menu");
click(&ctx, &mut app, item.center(), menu);
assert!(app.ui.dialog.is_none(), "Action Options… does not open for the new set's action");
}
/// An app with the default set and the sets Mine (two actions) and Other.
fn app_with_sets() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
+4
View File
@@ -1686,6 +1686,10 @@ already walks Image Trace. Short agent entry points:
is one of the format's extensions (`.svg` and `SVG` give `.svg`, `.JPEG` gives `.jpeg`), and otherwise in the
format's first extension (`png8` gives `.png`, `template` gives `.vctemplate`). A step without `format` keeps the
source file's extension (`.vectorcraft` over data sets).
- Actions: `ui.action.rename {set, action, name}` renames the Actions panel's action at `action` in set `set` (0 is
the top set) to `name` with spaces trimmed from both ends, keeps its steps, and returns `{name}`. An index past the
last set or action, or an empty name, is an error, and then nothing is renamed. The panel menu's Action Options…
runs this for the selected action.
- Action sets: `ui.actionSet.delete {sets?: [index], ids?: [id]}` deletes the Actions panel's sets at these indexes
(0 is the top set) with their actions, by default the current set, and returns `{deleted}`. An index past the last
set is an error, and then no set is deleted. `ids` holds the panel's own set ids, which its question passes: the
+1
View File
@@ -549,6 +549,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Variables: when no object is bound to a variable that the data set has a value for, for example after Import Data Sets… defines new variables without binding them, choosing the data set in the Variables panel or stepping to it with the panel's arrows shows a status message, and `dataset.select`, `dataset.next` and `dataset.prev` add the message to the reply as `warning`.
- Variables: Lock Variables (`variable.lock`, in the Variables panel's bottom bar and menu) stops variables from being defined, deleted or renamed until they are unlocked, while objects can still be bound and unbound and data sets still change and apply; the lock is saved with the document.
- Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`.
- Actions panel: the panel menu's Action Options… renames the selected action and keeps its steps ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html): "Opens a dialog to rename or assign function keys/colors to the action"; `ui.action.rename`). The dialog has the name only; the function key and color are not there yet.
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
- Actions panel: the current set is kept with the UI preferences, so after a restart a recording goes into the set that was current, not into the first set. With no set current, Begin Recording records into the first set and makes it current.
- 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): "Removes the selected action, command, or action set from the panel"; `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.
+1
View File
@@ -296,6 +296,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Live trace result bounds and selection highlights exclude the retained bitmap; source-image views retain it | done | All TraceView modes and rotated result bounds regression-covered; ordinary hidden group behavior retained |
| Image Trace panel: the Preview checkbox before Trace chooses whether a setting change traces the selected Image Trace object again at once | done | Per the public documentation, Preview shows the result of the current settings and Trace traces with them (<https://web.archive.org/web/20250831090623/https://helpx.adobe.com/illustrator/using/image-trace.html>). Preview is on by default and kept with the UI preferences; Reset to Default leaves it. Off, changes wait in the panel until Trace applies them as one undo step, or until Preview is turned on again. Changes stay with the object they were made for while another object is selected, and come back when it is selected again. `imageTrace.preview {on?}`, `ui.set {imageTracePreview}`; `panels/image_trace.rs` headless tests (`with_preview_off_a_slider_change_waits_for_trace`, `turning_preview_on_traces_the_changes_that_waited`, `preview_turned_on_with_nothing_selected_traces_when_the_object_is_selected_again`, `settings_that_wait_stay_with_their_object`, `settings_that_wait_come_back_when_their_object_is_selected_again`, `settings_that_wait_are_dropped_when_their_object_changed_since`, `preview_is_kept_with_the_preferences_and_reset_to_default_leaves_it`) |
| Toolbar without group labels by default, one or two columns; All Tools drawer by flyout group with shortcuts, its menu (Reset, Basic, Advanced) and Show section | done | #1751; a preference brings the labels back; `toolbar.show` sets the Fill/Stroke, Coloring, Draw Mode and Screen Mode controls |
| Actions panel: ≡ › Action Options… renames the selected action ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html): "Opens a dialog to rename or assign function keys/colors to the action") | partial | `ui.action.rename`; `panels/actions.rs` tests (part of #1629). The dialog has the name only: the function key and color remain |
| Actions panel: a clicked action stays selected for Play Current Selection and Delete Selection | partial | #1853 (part of #1629); editing actions and action shortcuts remain |
| Actions panel: Create New Set (bottom bar, ≡ › New Set…) names a new set and Set Options… renames the current one; a click on a set's name makes it current and a recording goes into the current set ([helpx: Automation with actions](https://helpx.adobe.com/illustrator/using/automation-actions.html)); the current set is kept with the UI preferences, and with no set current a recording goes into the first set, which becomes current | partial | #3712; the New Action dialog (name, set menu, shortcut, colour) and dragging actions between sets remain |
| Actions panel: Delete Selection (bottom bar) deletes the selected 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): "Removes the selected action, command, or action set from the panel") | partial | `ui.actionSet.delete`; `panels/actions.rs` tests. VectorCraft's own rules, which the overview does not cover: deleting a set asks first (a selected action is still deleted at once), Shift-click on a set's name selects a range of sets, ≡ › Delete does the same as the button, and Default Actions and the last set can be deleted. Undo does not restore a deleted set, and recorded commands cannot be selected or deleted |