Merge pull request #4976 from dotCooCoo/feat/actions-delete-set

Actions: Delete Selection deletes the selected action set
This commit is contained in:
BFlat
2026-11-01 05:29:56 +07:00
committed by GitHub
35 changed files with 543 additions and 18 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::state::Dialog;
use crate::theme::Tokens;
use crate::{VectorcraftApp, widgets};
pub(super) const KIND: &str = "actionBatch";
pub const KIND: &str = "actionBatch";
const LABEL: f32 = 130.0;
pub(super) const SPEC: DialogSpec =
@@ -54,7 +54,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
}
}
None => {
app.ui.action_sets.push(crate::panels::actions::ActionSet { name, actions: vec![] });
app.ui.action_sets.push(crate::panels::actions::ActionSet::new(name, vec![]));
app.ui.action_set = Some(app.ui.action_sets.len() - 1);
}
}
+3
View File
@@ -743,6 +743,9 @@ font-technology Variable Променливи
No Не
Delete Tab Изтриване на табулация
Delete the graphic style “{name}”? Изтриване на графичния стил „{name}“?
Delete Action Sets Изтриване на набори от действия
Delete the action set “{name}” and its actions? Изтриване на набора от действия „{name}“ и неговите действия?
Delete the selected action sets and their actions? Изтриване на избраните набори от действия и техните действия?
Delete Theme Изтриване на тема
Delete theme Изтриване на тема
Delete these {n} graphic styles? Изтриване на тези {n} графични стила?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1544,6 +1544,9 @@ digits Malayalam Malajalam
Save to Swatches Sačuvaj u uzorke boja
Delete Graphic Style (Alt-click: without asking) Izbriši grafički stil (Alt-klik: bez pitanja)
Delete the graphic style “{name}”? Izbrisati grafički stil „{name}”?
Delete Action Sets Izbriši skupove akcija
Delete the action set “{name}” and its actions? Izbrisati skup akcija „{name}” i njegove akcije?
Delete the selected action sets and their actions? Izbrisati odabrane skupove akcija i njihove akcije?
Delete these {n} graphic styles? Izbrisati ove grafičke stilove ({n})?
Delete this {n} graphic style? Izbrisati ovaj grafički stil ({n})?
Graphic Style Libraries Menu Meni biblioteka grafičkih stilova
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1060,6 +1060,9 @@
No No
Delete Tab Suprimeix la tabulació
Delete the graphic style “{name}”? Voleu suprimir l’estil gràfic “{name}”?
Delete Action Sets Suprimeix els conjunts d’accions
Delete the action set “{name}” and its actions? Voleu suprimir el conjunt d’accions “{name}” i les seves accions?
Delete the selected action sets and their actions? Voleu suprimir els conjunts d’accions seleccionats i les seves accions?
Delete the library “{name}”? Voleu suprimir la biblioteca “{name}”?
Delete Theme Suprimeix el tema
Delete theme Suprimeix el tema
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1568,6 +1568,9 @@ digits Malayalam Malajálamské
Save to Swatches Uložit do vzorníku
Delete Graphic Style (Alt-click: without asking) Odstranit grafický styl (Alt+kliknutí: bez dotazu)
Delete the graphic style “{name}”? Odstranit grafický styl „{name}“?
Delete Action Sets Odstranit sady akcí
Delete the action set “{name}” and its actions? Odstranit sadu akcí „{name}“ a její akce?
Delete the selected action sets and their actions? Odstranit vybrané sady akcí a jejich akce?
Delete these {n} graphic styles? Odstranit tyto grafické styly ({n})?
Delete this {n} graphic style? Odstranit tento grafický styl ({n})?
Graphic Style Libraries Menu Nabídka knihoven grafických stylů
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1481,6 +1481,9 @@ digits Malayalam Malayalam
Save to Swatches Gem i farveprøver
Delete Graphic Style (Alt-click: without asking) Slet grafisk typografi (Alt-klik: uden at spørge)
Delete the graphic style “{name}”? Slet den grafiske typografi »{name}«?
Delete Action Sets Slet handlingssæt
Delete the action set “{name}” and its actions? Slet handlingssættet »{name}« og dets handlinger?
Delete the selected action sets and their actions? Slet de markerede handlingssæt og deres handlinger?
Delete these {n} graphic styles? Slet disse {n} grafiske typografier?
Delete this {n} graphic style? Slet denne {n} grafiske typografi?
Graphic Style Libraries Menu Menu med biblioteker af grafiske typografier
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -737,6 +737,9 @@ font-technology Variable Variabel
No Nein
Delete Tab Tabulator löschen
Delete the graphic style “{name}”? Grafikstil „{name}“ löschen?
Delete Action Sets Aktionssätze löschen
Delete the action set “{name}” and its actions? Aktionssatz „{name}“ und seine Aktionen löschen?
Delete the selected action sets and their actions? Ausgewählte Aktionssätze und ihre Aktionen löschen?
Delete Theme Thema löschen
Delete theme Thema löschen
Delete these {n} graphic styles? Diese {n} Grafikstile löschen?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -717,6 +717,9 @@ font-technology Variable Μεταβλητές
No Όχι
Delete Tab Διαγραφή στηλοθέτη
Delete the graphic style “{name}”? Να διαγραφεί το στυλ γραφικών «{name}»;
Delete Action Sets Διαγραφή συνόλων ενεργειών
Delete the action set “{name}” and its actions? Να διαγραφεί το σύνολο ενεργειών «{name}» και οι ενέργειές του;
Delete the selected action sets and their actions? Να διαγραφούν τα επιλεγμένα σύνολα ενεργειών και οι ενέργειές τους;
Delete Theme Διαγραφή θέματος
Delete theme Διαγραφή θέματος
Delete these {n} graphic styles? Να διαγραφούν αυτά τα {n} στυλ γραφικών;
Can't render this file because it contains an unexpected character in line 164 and column 5.
+3
View File
@@ -758,6 +758,9 @@ font-technology Variable Variables
No No
Delete Tab Eliminar tabulación
Delete the graphic style “{name}”? ¿Eliminar el estilo gráfico “{name}”?
Delete Action Sets Eliminar conjuntos de acciones
Delete the action set “{name}” and its actions? ¿Eliminar el conjunto de acciones “{name}” y sus acciones?
Delete the selected action sets and their actions? ¿Eliminar los conjuntos de acciones seleccionados y sus acciones?
Delete Theme Eliminar tema
Delete theme Eliminar tema
Delete these {n} graphic styles? ¿Eliminar estos {n} estilos gráficos?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1541,6 +1541,9 @@ digits Malayalam Malajalam
Save to Swatches Tallenna värimalleihin
Delete Graphic Style (Alt-click: without asking) Poista grafiikkatyyli (Alt-napsautus: kysymättä)
Delete the graphic style “{name}”? Poistetaanko grafiikkatyyli ”{name}”?
Delete Action Sets Poista toimintosarjat
Delete the action set “{name}” and its actions? Poistetaanko toimintosarja ”{name}” ja sen toiminnot?
Delete the selected action sets and their actions? Poistetaanko valitut toimintosarjat ja niiden toiminnot?
Delete these {n} graphic styles? Poistetaanko nämä {n} grafiikkatyyliä?
Delete this {n} graphic style? Poistetaanko tämä {n} grafiikkatyyli?
Graphic Style Libraries Menu Grafiikkatyylikirjastojen valikko
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -747,6 +747,9 @@ font-technology Variable Variables
Delete Save for Web Preset Supprimer le paramètre prédéfini Enregistrer pour le Web
Delete Selected Item Supprimer l’élément sélectionné
Delete Selection Supprimer la sélection
Delete Action Sets Supprimer les jeux de scripts
Delete the action set “{name}” and its actions? Supprimer le jeu de scripts « {name} » et ses scripts ?
Delete the selected action sets and their actions? Supprimer les jeux de scripts sélectionnés et leurs scripts ?
Delete Slices Supprimer les tranches
Delete Spine Point Supprimer le point de la dorsale
Delete Stop Supprimer l’étape de dégradé
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -982,6 +982,9 @@ font-technology Variable Változó
No Nem
Delete Tab Tabulátor törlése
Delete the graphic style “{name}”? Törlöd ezt a grafikai stílust: „{name}”?
Delete Action Sets Műveletkészletek törlése
Delete the action set “{name}” and its actions? Törlöd ezt a műveletkészletet és a műveleteit: „{name}”?
Delete the selected action sets and their actions? Törlöd a kijelölt műveletkészleteket és a műveleteiket?
Delete the library “{name}”? Törlöd ezt a könyvtárat: „{name}”?
Delete Theme Téma törlése
Delete theme Téma törlése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1481,6 +1481,9 @@ digits Malayalam Malayalam
Save to Swatches Simpan ke swatch
Delete Graphic Style (Alt-click: without asking) Hapus gaya grafis (Alt+klik: tanpa konfirmasi)
Delete the graphic style “{name}”? Hapus gaya grafis “{name}”?
Delete Action Sets Hapus set tindakan
Delete the action set “{name}” and its actions? Hapus set tindakan “{name}” beserta tindakannya?
Delete the selected action sets and their actions? Hapus set tindakan yang dipilih beserta tindakannya?
Delete these {n} graphic styles? Hapus {n} gaya grafis ini?
Delete this {n} graphic style? Hapus {n} gaya grafis ini?
Graphic Style Libraries Menu Menu pustaka gaya grafis
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1481
1482
1483
1484
1485
1486
1487
1488
1489
+3
View File
@@ -758,6 +758,9 @@ font-technology Variable Variabili
No No
Delete Tab Elimina tabulazione
Delete the graphic style “{name}”? Eliminare lo stile grafico “{name}”?
Delete Action Sets Elimina set di azioni
Delete the action set “{name}” and its actions? Eliminare il set di azioni “{name}” e le sue azioni?
Delete the selected action sets and their actions? Eliminare i set di azioni selezionati e le loro azioni?
Delete Theme Elimina tema
Delete theme Elimina tema
Delete these {n} graphic styles? Eliminare questi {n} stili grafici?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1319,6 +1319,9 @@ digits Malayalam マラヤーラム数字
Delete Width Point 幅ポイントを削除
Delete Workspace ワークスペースを削除
Delete the graphic style “{name}”? グラフィックスタイル「{name}」を削除しますか?
Delete Action Sets アクションセットを削除
Delete the action set “{name}” and its actions? アクションセット「{name}」とそのアクションを削除しますか?
Delete the selected action sets and their actions? 選択したアクションセットとそのアクションを削除しますか?
Delete theme テーマを削除
Delete this saved preset この保存済みプリセットを削除
Delete {kind} Style {kind}スタイルを削除
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1398,6 +1398,9 @@ digits Malayalam მალაიალამური
Save to Swatches ფერის ნიმუშებში შენახვა
Delete Graphic Style (Alt-click: without asking) გრაფიკული სტილის წაშლა (Alt-დაწკაპუნებით: დაუკითხავად)
Delete the graphic style “{name}”? წაიშალოს გრაფიკული სტილი „{name}“?
Delete Action Sets მოქმედებების ნაკრებების წაშლა
Delete the action set “{name}” and its actions? წაიშალოს მოქმედებების ნაკრები „{name}“ და მისი მოქმედებები?
Delete the selected action sets and their actions? წაიშალოს მონიშნული მოქმედებების ნაკრებები და მათი მოქმედებები?
Delete these {n} graphic styles? წაიშალოს ეს გრაფიკული სტილები ({n})?
Delete this {n} graphic style? წაიშალოს ეს გრაფიკული სტილი ({n})?
Graphic Style Libraries Menu გრაფიკული სტილების ბიბლიოთეკების მენიუ
Can't render this file because it is too large.
+3
View File
@@ -1309,6 +1309,9 @@ digits Malayalam 말라얄람어
Delete Width Point 너비 포인트 삭제
Delete Workspace 작업 영역 삭제
Delete the graphic style “{name}”? 그래픽 스타일 “{name}”을(를) 삭제하시겠습니까?
Delete Action Sets 액션 세트 삭제
Delete the action set “{name}” and its actions? 액션 세트 “{name}”과(와) 해당 액션을 삭제하시겠습니까?
Delete the selected action sets and their actions? 선택한 액션 세트와 해당 액션을 삭제하시겠습니까?
Delete theme 테마 삭제
Delete this saved preset 저장된 이 프리셋 삭제
Delete {kind} Style {kind} 스타일 삭제
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1495,6 +1495,9 @@ digits Malayalam Malayalam
Save to Swatches Opslaan in stalen
Delete Graphic Style (Alt-click: without asking) Grafische stijl verwijderen (Alt-klik: zonder vragen)
Delete the graphic style “{name}”? De grafische stijl „{name}” verwijderen?
Delete Action Sets Actiesets verwijderen
Delete the action set “{name}” and its actions? De actieset „{name}” en de acties erin verwijderen?
Delete the selected action sets and their actions? De geselecteerde actiesets en de acties erin verwijderen?
Delete these {n} graphic styles? Deze {n} grafische stijlen verwijderen?
Delete this {n} graphic style? Deze {n} grafische stijl verwijderen?
Graphic Style Libraries Menu Menu grafische stijlbibliotheken
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1481,6 +1481,9 @@ digits Malayalam Malajalam
Save to Swatches Zapisz w próbkach
Delete Graphic Style (Alt-click: without asking) Usuń styl graficzny (kliknięcie z Alt: bez pytania)
Delete the graphic style “{name}”? Usunąć styl graficzny „{name}”?
Delete Action Sets Usuń zestawy operacji
Delete the action set “{name}” and its actions? Usunąć zestaw operacji „{name}” i jego operacje?
Delete the selected action sets and their actions? Usunąć zaznaczone zestawy operacji i ich operacje?
Delete these {n} graphic styles? Usunąć te style graficzne ({n})?
Delete this {n} graphic style? Usunąć ten styl graficzny ({n})?
Graphic Style Libraries Menu Menu bibliotek stylów graficznych
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -675,6 +675,9 @@
Delete Width Point Excluir Ponto de Largura
Delete Workspace Excluir Espaço de Trabalho
Delete the graphic style “{name}”? Excluir o estilo gráfico “{name}”?
Delete Action Sets Excluir Conjuntos de Ações
Delete the action set “{name}” and its actions? Excluir o conjunto de ações “{name}” e suas ações?
Delete the selected action sets and their actions? Excluir os conjuntos de ações selecionados e suas ações?
Delete theme Excluir tema
Delete this saved preset Excluir este predefinido salvo
Delete {kind} Style Excluir Estilo de {kind}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -756,6 +756,9 @@ font-technology Variable Вариативные
No Нет
Delete Tab Удалить табуляцию
Delete the graphic style “{name}”? Удалить графический стиль «{name}»?
Delete Action Sets Удалить наборы действий
Delete the action set “{name}” and its actions? Удалить набор действий «{name}» и его действия?
Delete the selected action sets and their actions? Удалить выбранные наборы действий и их действия?
Delete Theme Удалить тему
Delete theme Удалить тему
Delete these {n} graphic styles? Удалить эти графические стили ({n})?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -745,6 +745,9 @@ font-technology Variable Variabla
No Nej
Delete Tab Radera tabb
Delete the graphic style “{name}”? Radera grafikstilen ”{name}”?
Delete Action Sets Radera åtgärdsuppsättningar
Delete the action set “{name}” and its actions? Radera åtgärdsuppsättningen ”{name}” och dess åtgärder?
Delete the selected action sets and their actions? Radera de markerade åtgärdsuppsättningarna och deras åtgärder?
Delete Theme Radera tema
Delete theme Radera tema
Delete these {n} graphic styles? Radera dessa {n} grafikstilar?
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -737,6 +737,9 @@ font-technology Variable Değişken
No Hayır
Delete Tab Sekmeyi Sil
Delete the graphic style “{name}”? “{name}” grafik stili silinsin mi?
Delete Action Sets Eylem Setlerini Sil
Delete the action set “{name}” and its actions? “{name}” eylem seti ve eylemleri silinsin mi?
Delete the selected action sets and their actions? Seçili eylem setleri ve eylemleri silinsin mi?
Delete Theme Temayı Sil
Delete theme Temayı sil
Delete these {n} graphic styles? Bu {n} grafik stilleri silinsin mi?
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -752,6 +752,9 @@ font-technology Variable Варіативні
No Ні
Delete Tab Видалити табуляцію
Delete the graphic style “{name}”? Видалити графічний стиль «{name}»?
Delete Action Sets Видалити набори дій
Delete the action set “{name}” and its actions? Видалити набір дій «{name}» і його дії?
Delete the selected action sets and their actions? Видалити вибрані набори дій і їхні дії?
Delete Theme Видалити тему
Delete theme Видалити тему
Delete these {n} graphic styles? Видалити ці графічні стилі ({n})?
Can't render this file because it contains an unexpected character in line 178 and column 5.
+3
View File
@@ -878,6 +878,9 @@ font-technology Variable Biến thiên
No Không
Delete Tab Xóa tab
Delete the graphic style “{name}”? Xóa kiểu đồ họa “{name}”?
Delete Action Sets Xóa các bộ hành động
Delete the action set “{name}” and its actions? Xóa bộ hành động “{name}” và các hành động của nó?
Delete the selected action sets and their actions? Xóa các bộ hành động đã chọn và các hành động của chúng?
Delete the library “{name}”? Xóa thư viện “{name}”?
Delete Theme Xóa bộ màu
Delete theme Xóa bộ màu
Can't render this file because it contains an unexpected character in line 200 and column 5.
+3
View File
@@ -701,6 +701,9 @@
No 否
Delete Tab 删除制表位
Delete the graphic style “{name}”? 要删除图形样式“{name}”吗?
Delete Action Sets 删除动作组
Delete the action set “{name}” and its actions? 要删除动作组“{name}”及其中的动作吗?
Delete the selected action sets and their actions? 要删除所选动作组及其中的动作吗?
Delete Theme 删除主题
Delete theme 删除主题
Delete these {n} graphic styles? 要删除这 {n} 个图形样式吗?
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -720,6 +720,9 @@ font-technology Variable 可變
No 否
Delete Tab 刪除定位點
Delete the graphic style “{name}”? 要刪除繪圖樣式「{name}」嗎?
Delete Action Sets 刪除動作組
Delete the action set “{name}” and its actions? 要刪除動作組「{name}」及其中的動作嗎?
Delete the selected action sets and their actions? 要刪除所選動作組及其中的動作嗎?
Delete Theme 刪除主題
Delete theme 刪除主題
Delete these {n} graphic styles? 要刪除這 {n} 個繪圖樣式嗎?
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
let action = p.get("action").and_then(Value::as_u64).unwrap_or(0) as usize;
crate::dialogs::action_batch::open(app, set, action)
}
crate::panels::actions::DELETE_SETS => crate::panels::actions::delete_sets(app, p),
"ui.shortcutImportDialog" => crate::dialogs::shortcut_import::command(app, p),
_ => return None,
};
+6
View File
@@ -1119,6 +1119,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{set?: index, action?: index} open Batch (dialog `actionBatch`) for the action at set/action (default: the first action of the first set); OK runs action.batch with the dialog's source, destination and overrides",
),
(
crate::panels::actions::DELETE_SETS,
"Delete Action Sets",
"",
"{sets?: [index], ids?: [id]} delete the Actions panel's sets at these indexes (0 is the top set), with their actions (default: the current set). ids holds the panel's own set ids, which its question passes: the sets with these ids are deleted wherever they are now, sets is not read, and when one of them no longer exists nothing is deleted. The panel's Delete Selection on selected sets (Shift-click adds a range) asks first, and OK runs this. A recording into a deleted set stops and keeps nothing. An index past the last set is an error → {deleted: n}",
),
("file.showPackage", "Show Package", "", "{folder} show a package folder (file.package's folder) in the file manager → {folder}"),
(
"docInfo.save",
+440 -15
View File
@@ -18,16 +18,33 @@ pub struct Action {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ActionSet {
/// Tells the set apart from every other set in this run, even one with the same name. It is
/// not saved: each set gets a new id when it is made or loaded.
#[serde(skip, default = "next_set_id")]
pub id: u64,
pub name: String,
pub actions: Vec<Action>,
}
impl ActionSet {
/// A set with a new [`ActionSet::id`].
pub fn new(name: impl Into<String>, actions: Vec<Action>) -> Self {
Self { id: next_set_id(), name: name.into(), actions }
}
}
/// A set id no other set in this run has.
fn next_set_id() -> u64 {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
/// Built-in actions (our own, handy defaults).
pub fn default_sets() -> Vec<ActionSet> {
let step = |c: &str, p: Value| (c.to_string(), p);
vec![ActionSet {
name: "Default Actions".into(),
actions: vec![
vec![ActionSet::new(
"Default Actions",
vec![
Action { name: "Duplicate and Offset 10 pt".into(), steps: vec![step("object.move", json!({"dx": 10, "dy": 10, "copy": true}))] },
Action {
name: "Unite and Simplify".into(),
@@ -41,7 +58,7 @@ pub fn default_sets() -> Vec<ActionSet> {
Action { name: "Rotate 90° CW".into(), steps: vec![step("object.rotate", json!({"angle": -90}))] },
Action { name: "Reflect Horizontal".into(), steps: vec![step("object.reflect", json!({"axis": "vertical"}))] },
],
}]
)]
}
/// Run an action as a single undoable batch.
@@ -51,6 +68,106 @@ 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 deletes action sets ([`delete_sets`]).
pub const DELETE_SETS: &str = "ui.actionSet.delete";
/// The sets Delete Selection deletes: the selected sets ([`crate::state::UiState::action_sets_selected`])
/// when the current set is one of them, else the current set alone.
fn selected_sets(app: &VectorcraftApp) -> Vec<usize> {
let len = app.ui.action_sets.len();
let current = app.ui.action_set.filter(|&s| s < len);
let sets: Vec<usize> = app.ui.action_sets_selected.iter().copied().filter(|&s| s < len).collect();
if current.is_some_and(|c| sets.contains(&c)) { sets } else { current.into_iter().collect() }
}
/// Delete Selection, from the bottom bar and the panel menu: deletes the selected action, else asks
/// whether to delete the selected sets ([`DELETE_SETS`]).
fn delete_selection(app: &mut VectorcraftApp, ctx: &egui::Context) {
const SELECTED: &str = "actions-selected";
// A selected action counts only while its set is the current one, as the panel shows it.
if let Some((si, ai)) = pstate::<Option<(usize, usize)>>(ctx, SELECTED).filter(|&(s, _)| app.ui.action_set == Some(s)) {
if let Some(s) = app.ui.action_sets.get_mut(si)
&& ai < s.actions.len()
{
s.actions.remove(ai);
set_pstate::<Option<(usize, usize)>>(ctx, SELECTED, None);
}
return;
}
let sets = selected_sets(app);
let message = match sets[..] {
[] => return,
[s] => crate::i18n::fmt(
tl!("Delete the action set “{name}” and its actions?"),
&[("name", app.ui.action_sets.get(s).map_or("", |s| s.name.as_str()))],
),
_ => tl!("Delete the selected action sets and their actions?").to_string(),
};
let ids: Vec<u64> = sets.iter().filter_map(|&s| app.ui.action_sets.get(s)).map(|s| s.id).collect();
let params = json!({ "sets": sets, "ids": ids });
crate::dialogs::confirm::ask(app, &message, "", DELETE_SETS, params);
}
/// `ui.actionSet.delete {sets?: [index], ids?: [id]}`: deletes the action sets at these
/// indexes, with their actions (default: the current set). `ids` holds the panel's own set ids
/// ([`ActionSet::id`]), which Delete Selection's question passes: the sets with these ids are
/// deleted wherever they are now, `sets` is not read, and when one of them no longer exists
/// nothing is deleted. A recording into a deleted set stops, and its steps are dropped; a
/// recording into another set goes on. An open Set Options or Batch dialog for a deleted set
/// closes. When the current set is deleted, no set is current afterwards.
pub fn delete_sets(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let len = app.ui.action_sets.len();
let mut sets: Vec<usize> = match (p.get("ids"), p.get("sets")) {
(Some(v), _) if !v.is_null() => v
.as_array()
.ok_or("the action set ids are not a list")?
.iter()
.map(|id| {
id.as_u64().and_then(|id| app.ui.action_sets.iter().position(|s| s.id == id)).ok_or("the action sets changed; nothing was deleted")
})
.collect::<Result<_, _>>()?,
(_, None | Some(Value::Null)) => app.ui.action_set.filter(|&s| s < len).into_iter().collect(),
(_, Some(v)) => v
.as_array()
.ok_or("no such action set")?
.iter()
.map(|i| i.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < len).ok_or("no such action set"))
.collect::<Result<_, _>>()?,
};
sets.sort_unstable();
sets.dedup();
if let Some((rec, ..)) = app.ui.recording.as_mut() {
if sets.contains(rec) {
app.ui.recording = None;
app.session.journal.hold(None);
} else {
*rec -= sets.iter().filter(|&&s| s < *rec).count();
}
}
// An open Set Options or Batch dialog holds a set index: it closes when its set is deleted,
// and otherwise follows its set down.
let holds_set = |d: &crate::state::Dialog| {
d.kind == crate::dialogs::action_batch::KIND || (d.kind == crate::dialogs::action_set_options::KIND && d.fields.contains_key("set"))
};
if let Some(d) = app.ui.dialog.as_mut().filter(|d| holds_set(d))
&& let Some(set) = d.fields.get("set").and_then(Value::as_u64).and_then(|s| usize::try_from(s).ok())
{
if sets.contains(&set) {
app.ui.dialog = None;
} else {
d.fields.insert("set".into(), json!(set - sets.iter().filter(|&&s| s < set).count()));
}
}
for &s in sets.iter().rev() {
app.ui.action_sets.remove(s);
}
if !sets.is_empty() {
app.ui.action_set = app.ui.action_set.filter(|c| !sets.contains(c)).map(|c| c - sets.iter().filter(|&&s| s < c).count());
app.ui.action_sets_selected.clear();
}
Ok(json!({ "deleted": sets.len() }))
}
/// The panel's (≡) menu.
pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
let selected: Option<(usize, usize)> = pstate(ui.ctx(), "actions-selected");
@@ -58,6 +175,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
if widgets::menu_item(ui, tl!("New Set…"), true, false) {
crate::dialogs::action_set_options::open_new(app);
}
if widgets::menu_item(ui, tl!("Delete"), has || !selected_sets(app).is_empty(), false) {
delete_selection(app, ui.ctx());
}
let current = app.ui.action_set.filter(|&s| s < app.ui.action_sets.len());
if widgets::menu_item(ui, tl!("Set Options…"), current.is_some(), false)
&& let Some(s) = current
@@ -90,18 +210,28 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
selected = None;
}
let mut current = app.ui.action_set;
let mut sets_selected = selected_sets(app);
let shift = ui.input(|i| i.modifiers.shift);
let mut play_req = None;
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
for (si, set) in app.ui.action_sets.iter().enumerate() {
let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 20.0), egui::Sense::click());
if current == Some(si) && selected.is_none() {
if sets_selected.contains(&si) && selected.is_none() {
ui.painter().rect_filled(r, 0.0, t.row_selected);
} else if resp.hovered() {
ui.painter().rect_filled(r, 0.0, t.hover);
}
ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, format!("▾ {}", set.name), egui::FontId::proportional(12.5), t.text);
if resp.clicked() {
current = Some(si);
// Shift-click selects the sets from the current one to the one clicked; the current
// set stays as it was.
match current {
Some(c) if shift && sets_selected.contains(&c) => sets_selected = (c.min(si)..=c.max(si)).collect(),
_ => {
current = Some(si);
sets_selected = vec![si];
}
}
selected = None;
}
for (ai, a) in set.actions.iter().enumerate() {
@@ -129,6 +259,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
if resp.clicked() {
selected = Some((si, ai));
current = Some(si);
sets_selected = vec![si];
}
if resp.double_clicked() {
play_req = Some((si, ai));
@@ -138,6 +269,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
});
set_pstate(ui.ctx(), SELECTED, selected);
app.ui.action_set = current;
app.ui.action_sets_selected = sets_selected;
widgets::divider(ui);
// Bottom bar: stop, record, play, new set, delete.
ui.horizontal(|ui| {
@@ -157,7 +289,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
ui.painter().circle_filled(r.center(), 6.0, if recording { egui::Color32::from_rgb(0xe0, 0x30, 0x30) } else { t.icon });
if resp.on_hover_text(tl!("Begin Recording")).clicked() && !recording {
if app.ui.action_sets.is_empty() {
app.ui.action_sets.push(ActionSet { name: "Set 1".into(), actions: vec![] });
app.ui.action_sets.push(ActionSet::new("Set 1", vec![]));
}
let set = current.filter(|&s| s < app.ui.action_sets.len()).unwrap_or(0);
let n = app.ui.action_sets.get(set).map_or(0, |s| s.actions.len()) + 1;
@@ -174,13 +306,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
if widgets::icon_button(ui, "dc-folder", tl!("Create New Set"), false, 24.0).clicked() {
crate::dialogs::action_set_options::open_new(app);
}
if widgets::icon_button(ui, "trash-2", tl!("Delete Selection"), false, 24.0).clicked()
&& let Some((si, ai)) = selected
&& let Some(s) = app.ui.action_sets.get_mut(si)
&& ai < s.actions.len()
{
s.actions.remove(ai);
set_pstate::<Option<(usize, usize)>>(ui.ctx(), SELECTED, None);
if widgets::icon_button(ui, "trash-2", tl!("Delete Selection"), false, 24.0).clicked() {
delete_selection(app, ui.ctx());
}
if recording {
ui.label(egui::RichText::new(tl!("● Recording")).color(egui::Color32::from_rgb(0xe0, 0x30, 0x30)));
@@ -294,7 +421,7 @@ mod tests {
fn set_options_renames_the_current_set() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.ui.action_sets.push(ActionSet { name: "Mine".into(), actions: vec![] });
app.ui.action_sets.push(ActionSet::new("Mine", vec![]));
let ctx = egui::Context::default();
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "▾ Mine").center(), show);
@@ -309,6 +436,304 @@ mod tests {
assert_eq!(names, [("Default Actions", 6), ("Web", 0)], "the clicked set is renamed, the other kept");
}
/// 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());
let action = |name: &str| Action { name: name.into(), steps: vec![("object.move".into(), json!({"dx": 1, "dy": 0}))] };
app.ui.action_sets.push(ActionSet::new("Mine", vec![action("Nudge"), action("Nudge Again")]));
app.ui.action_sets.push(ActionSet::new("Other", vec![]));
app
}
fn set_names(app: &VectorcraftApp) -> Vec<String> {
app.ui.action_sets.iter().map(|s| s.name.clone()).collect()
}
/// The bottom bar's Delete Selection button (stop, record, play, new set, delete).
fn trash(ctx: &egui::Context, app: &mut VectorcraftApp) -> egui::Pos2 {
super::super::tests_appearance::frame_events(ctx, app, vec![], show);
let mut bar = crate::toolbar::tests::widget_rects(ctx, egui::vec2(24.0, 24.0));
bar.sort_by(|a, b| a.left().total_cmp(&b.left()));
let [_, _, _, _, delete] = bar[..] else { panic!("the bottom bar: {bar:?}") };
delete.center()
}
/// A click on the name of set `name` with `modifiers` held.
fn click_set(ctx: &egui::Context, app: &mut VectorcraftApp, name: &str, modifiers: egui::Modifiers) {
use super::super::tests_appearance::{frame_events, text_rect};
let pos = text_rect(&frame_events(ctx, app, vec![], show), &format!("▾ {name}")).center();
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers };
for e in [egui::Event::PointerMoved(pos), button(true), button(false)] {
frame_events(ctx, app, vec![egui::Event::ModifiersChanged(modifiers), e], show);
}
}
/// The question Delete Selection asks about sets, with the sets OK deletes.
fn asked(app: &VectorcraftApp) -> Option<(String, Value)> {
let d = app.ui.dialog.as_ref()?;
(d.kind == crate::dialogs::confirm::KIND && d.str("__command") == DELETE_SETS)
.then(|| (d.str("message"), d.fields["__params"]["sets"].clone()))
}
/// Delete Selection with a set selected asks first: Cancel keeps every set, OK deletes the
/// selected set with its actions and keeps the others.
#[test]
fn delete_selection_asks_and_then_deletes_the_selected_set() {
use super::super::tests_appearance::click;
let mut app = app_with_sets();
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "Mine", egui::Modifiers::NONE);
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
let (message, sets) = asked(&app).expect("Delete Selection asks before deleting a set");
assert_eq!((message.as_str(), sets), ("Delete the action set “Mine” and its actions?", json!([1])));
crate::dialogs::cancel(&mut app);
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"], "Cancel deletes nothing");
click(&ctx, &mut app, delete, show);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(set_names(&app), ["Default Actions", "Other"], "OK deletes the selected set and its actions");
assert_eq!(app.ui.action_set, None, "no set is current after the current one is deleted");
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert!(app.ui.dialog.is_none(), "nothing selected: nothing to ask about");
}
/// The panel menu's Delete asks the same question about the selected set, and deletes a
/// selected action at once, as the bottom bar's button does.
#[test]
fn the_panel_menus_delete_deletes_the_selected_set_or_action() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = app_with_sets();
let ctx = egui::Context::default();
let delete = |ctx: &egui::Context, app: &mut VectorcraftApp| {
let texts = frame_events(ctx, app, vec![], menu);
let item = texts.iter().find(|(s, _)| s.trim() == "Delete").map(|(_, r)| *r).expect("Delete in the menu");
click(ctx, app, item.center(), menu);
};
click_set(&ctx, &mut app, "Other", egui::Modifiers::NONE);
delete(&ctx, &mut app);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([2])));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(set_names(&app), ["Default Actions", "Mine"]);
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "Nudge").center(), show);
frame_events(&ctx, &mut app, vec![], show);
delete(&ctx, &mut app);
assert!(app.ui.dialog.is_none(), "an action is deleted without a question");
let actions = app.ui.action_sets[1].actions.iter().map(|a| a.name.as_str()).collect::<Vec<_>>();
assert_eq!((set_names(&app).len(), actions), (2, vec!["Nudge Again"]), "the action goes, its set stays");
}
/// After New Set makes another set current, the panel menu's Delete asks about that set and
/// leaves the action selected before it.
#[test]
fn delete_after_new_set_targets_the_new_set() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = app_with_sets();
let ctx = egui::Context::default();
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "Nudge").center(), show);
crate::dialogs::action_set_options::open_new(&mut app);
crate::dialogs::confirm(&mut app).unwrap();
let texts = frame_events(&ctx, &mut app, vec![], menu);
let item = texts.iter().find(|(s, _)| s.trim() == "Delete").map(|(_, r)| *r).expect("Delete in the menu");
click(&ctx, &mut app, item.center(), menu);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([3])), "the new set, not the action");
assert_eq!(app.ui.action_sets[1].actions.len(), 2);
}
/// Shift-click on a set's name selects the sets from the current one to it, and one question
/// deletes them all; the set left out stays.
#[test]
fn shift_click_selects_sets_that_one_question_deletes() {
use super::super::tests_appearance::click;
let mut app = app_with_sets();
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "Mine", egui::Modifiers::NONE);
click_set(&ctx, &mut app, "Other", egui::Modifiers::SHIFT);
assert_eq!((app.ui.action_set, app.ui.action_sets_selected.clone()), (Some(1), vec![1, 2]), "the current set stays");
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert_eq!(asked(&app), Some(("Delete the selected action sets and their actions?".to_string(), json!([1, 2]))));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(set_names(&app), ["Default Actions"]);
// A plain click selects one set again.
app.ui.action_sets.extend([ActionSet::new("A", vec![]), ActionSet::new("B", vec![])]);
click_set(&ctx, &mut app, "A", egui::Modifiers::NONE);
click_set(&ctx, &mut app, "Default Actions", egui::Modifiers::SHIFT);
click_set(&ctx, &mut app, "B", egui::Modifiers::NONE);
assert_eq!((app.ui.action_set, app.ui.action_sets_selected.clone()), (Some(2), vec![2]));
}
/// The question holds the ids of the sets it asks about: when a set before the asked one is
/// deleted while it is open, OK deletes the asked set at its new index, not the set that moved
/// into the asked index.
#[test]
fn ok_deletes_the_asked_set_after_an_earlier_set_is_deleted_under_the_question() {
use super::super::tests_appearance::click;
let mut app = app_with_sets();
app.ui.action_sets.push(ActionSet::new("Third", vec![]));
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "Other", egui::Modifiers::NONE);
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([2])));
let id = app.ui.action_sets[2].id;
assert_eq!(app.ui.dialog.as_ref().map(|d| d.fields["__params"]["ids"].clone()), Some(json!([id])));
app.run(DELETE_SETS, json!({"sets": [1]})).unwrap();
assert_eq!(set_names(&app), ["Default Actions", "Other", "Third"]);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(set_names(&app), ["Default Actions", "Third"], "OK deletes Other, the asked set");
let p = json!({"sets": [0], "ids": [id]});
assert_eq!(app.run(DELETE_SETS, p).unwrap_err(), "the action sets changed; nothing was deleted");
assert_eq!(set_names(&app), ["Default Actions", "Third"], "an id that is gone deletes nothing, sets is not read");
}
/// Two sets with the same name: when the asked one is deleted elsewhere while the question is
/// open, OK does not delete the other one, which now has the asked index and name; when the
/// two trade places, OK deletes the asked one at its new index.
#[test]
fn ok_deletes_only_the_asked_one_of_two_sets_with_the_same_name() {
use super::super::tests_appearance::click;
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let x = |n: &str| ActionSet::new("X", vec![Action { name: n.into(), steps: vec![] }]);
app.ui.action_sets = vec![x("first"), x("second")];
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "X", egui::Modifiers::NONE);
assert_eq!(app.ui.action_set, Some(0));
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([0])));
app.run(DELETE_SETS, json!({"sets": [0]})).unwrap();
assert!(crate::dialogs::confirm(&mut app).is_err());
assert_eq!(app.ui.action_sets.iter().map(|s| s.actions[0].name.as_str()).collect::<Vec<_>>(), ["second"], "OK deletes nothing");
app.ui.action_sets = vec![x("first"), x("second")];
app.ui.action_set = Some(0);
app.ui.action_sets_selected = vec![0];
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([0])));
app.ui.action_sets.swap(0, 1);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.ui.action_sets.iter().map(|s| s.actions[0].name.as_str()).collect::<Vec<_>>(), ["second"], "the asked set goes");
}
/// When the asked set is deleted while the question is open and a new set with the same name
/// takes its place, OK does not delete the new set.
#[test]
fn ok_does_not_delete_a_new_set_made_in_the_asked_sets_place() {
use super::super::tests_appearance::click;
let mut app = app_with_sets();
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "Other", egui::Modifiers::NONE);
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
assert_eq!(asked(&app).map(|(_, sets)| sets), Some(json!([2])));
app.run(DELETE_SETS, json!({"sets": [2]})).unwrap();
app.ui.action_sets.push(ActionSet::new("Other", vec![]));
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"]);
assert!(crate::dialogs::confirm(&mut app).is_err());
assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"], "the new Other stays");
}
/// Every set has its own id, and sets read back from the saved preferences get new ids that
/// no other set has.
#[test]
fn action_sets_get_ids_of_their_own_when_made_and_loaded() {
let app = app_with_sets();
let saved = serde_json::to_value(&app.ui).unwrap();
assert!(saved["action_sets"][0].get("id").is_none(), "ids are not saved");
let restored = serde_json::from_value::<crate::state::UiState>(saved).unwrap();
let mut ids: Vec<u64> = app.ui.action_sets.iter().chain(&restored.action_sets).map(|s| s.id).collect();
assert_eq!(restored.action_sets.len(), 3);
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), 6, "six sets, six ids");
}
/// Shift-click on another set's name while an action is selected selects the sets from the
/// current one to it and clears the action.
#[test]
fn shift_click_on_a_set_with_an_action_selected_selects_the_range() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = app_with_sets();
let ctx = egui::Context::default();
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "Nudge").center(), show);
frame_events(&ctx, &mut app, vec![], show);
assert_eq!(pstate::<Option<(usize, usize)>>(&ctx, "actions-selected"), Some((1, 0)));
click_set(&ctx, &mut app, "Other", egui::Modifiers::SHIFT);
assert_eq!((app.ui.action_set, app.ui.action_sets_selected.clone()), (Some(1), vec![1, 2]));
assert_eq!(pstate::<Option<(usize, usize)>>(&ctx, "actions-selected"), None, "the action is no longer selected");
}
/// The default set can be deleted, and so can the last set: the panel is then empty, no set is
/// current, and New Set makes a set again.
#[test]
fn deleting_the_default_and_the_last_set_leaves_an_empty_panel() {
use super::super::tests_appearance::click;
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
click_set(&ctx, &mut app, "Default Actions", egui::Modifiers::NONE);
let delete = trash(&ctx, &mut app);
click(&ctx, &mut app, delete, show);
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.action_sets.is_empty());
assert_eq!(app.ui.action_set, None);
crate::dialogs::action_set_options::open_new(&mut app);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!((set_names(&app), app.ui.action_set), (vec!["Set 1".to_string()], Some(0)), "New Set works on an empty panel");
}
/// `ui.actionSet.delete` keeps a recording into a later set going in the same set, stops one
/// into a deleted set, and refuses an index past the last set.
#[test]
fn deleting_sets_keeps_a_recording_in_its_set() {
let mut app = app_with_sets();
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
app.ui.action_set = Some(2);
let start = app.session.journal.len();
app.session.journal.hold(Some(start));
app.ui.recording = Some((2, "Action 1".into(), start));
assert_eq!(app.run(DELETE_SETS, json!({"sets": [1]})).unwrap(), json!({"deleted": 1}));
assert_eq!(set_names(&app), ["Default Actions", "Other"]);
assert_eq!((app.ui.recording.as_ref().map(|r| r.0), app.ui.action_set), (Some(1), Some(1)), "still recording into Other");
assert_eq!(app.run(DELETE_SETS, json!({"sets": [5]})).unwrap_err(), "no such action set");
assert_eq!(app.run(DELETE_SETS, json!({"sets": [4_294_967_296u64]})).unwrap_err(), "no such action set", "above u32::MAX");
assert_eq!(set_names(&app), ["Default Actions", "Other"], "a refused index deletes nothing");
assert_eq!(app.run(DELETE_SETS, json!({})).unwrap(), json!({"deleted": 1}), "the current set by default");
assert_eq!(set_names(&app), ["Default Actions"]);
assert!(app.ui.recording.is_none(), "the recording into the deleted set stops");
assert_eq!(app.session.journal.held(), None);
}
/// `ui.actionSet.delete` while Set Options is open on a later set: OK renames that set, not
/// the one that moves into its old index.
#[test]
fn set_options_renames_its_own_set_after_an_earlier_set_is_deleted() {
let mut app = app_with_sets();
crate::dialogs::action_set_options::open(&mut app, 2);
app.run(DELETE_SETS, json!({"sets": [1]})).unwrap();
app.ui.dialog.as_mut().expect("Set Options stays open").fields.insert("name".into(), json!("Web"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(set_names(&app), ["Default Actions", "Web"], "the set that was Other is renamed");
}
/// `ui.actionSet.delete` closes a Batch dialog open on an action of a deleted set, and keeps
/// one open on a later set pointing at that set.
#[test]
fn deleting_a_set_closes_its_batch_dialog() {
let mut app = app_with_sets();
crate::dialogs::action_batch::open(&mut app, 1, 0).unwrap();
app.run(DELETE_SETS, json!({"sets": [1]})).unwrap();
assert!(app.ui.dialog.is_none(), "the Batch dialog of the deleted set closes");
let mut app = app_with_sets();
crate::dialogs::action_batch::open(&mut app, 1, 1).unwrap();
app.run(DELETE_SETS, json!({"sets": [0]})).unwrap();
let d = app.ui.dialog.as_ref().expect("the Batch dialog of a kept set stays open");
assert_eq!((d.fields["set"].clone(), d.fields["action"].clone()), (json!(0), json!(1)), "it follows Mine to index 0");
}
/// A recording keeps its steps when a canceled undo group drops journal entries made before the
/// recording began.
#[test]
+5
View File
@@ -448,6 +448,10 @@ pub struct UiState {
/// with New Set): a recording goes into it and Set Options renames it. Not saved.
#[serde(skip)]
pub action_set: Option<usize>,
/// The sets selected in the Actions panel, in index order: the current set and the sets a
/// Shift-click on a set's name added. Delete Selection deletes them. Not saved.
#[serde(skip)]
pub action_sets_selected: Vec<usize>,
/// Recording in progress: (set index, action name, journal length when recording started).
#[serde(skip)]
pub recording: Option<(usize, String, usize)>,
@@ -876,6 +880,7 @@ impl Default for UiState {
about: false,
action_sets: crate::panels::actions::default_sets(),
action_set: None,
action_sets_selected: vec![],
recording: None,
shortcut_overrides: Default::default(),
shortcut_set: default_shortcut_set(),
+8
View File
@@ -1650,6 +1650,14 @@ 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).
- 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
sets with these ids are deleted wherever they are now and `sets` is not read; when one of them no longer exists, the
command returns an error and deletes no set. A recording into a deleted set stops and keeps no steps; a recording into another set
goes on in that set. The panel's Delete Selection (the trash button and the panel menu's Delete) asks before it runs
this for the selected sets, and Shift-click on a set's name selects the sets from the current one to it. Undo does
not restore a deleted set.
- Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),
`imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`. A key inside `params` that is not one of the trace
settings, such as `colours`, is an error that names it and the nearest setting (`colors`), as for a command's own keys.
+1
View File
@@ -547,6 +547,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- 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 (record/playback, persisted), generic parameter dialogs for every "…" command.
- 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.
- 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.
- Batch… offers File + Number, File + Data Set Name, and Data Set Name for a Data Sets source. The dialog defaults to File + Number; `action.batch` without `datasetFileNaming` keeps File + Data Set Name. An output already in a destination folder the batch may read and write gets the next free `-2`, `-3` suffix. [Batch options](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/batch-options.html).
- Export for Screens: the dialog's Example line shows the path of the file the export writes first, sub-folder included, named as the export names it, for the first chosen artboard, asset, layer or object (`document.exportForScreens`).
+2 -1
View File
@@ -294,7 +294,8 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| 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: 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)) | partial | #3712; the New Action dialog (name, set menu, shortcut, colour), dragging actions between sets and deleting a set 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)) | 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 |
| Properties panel per context (every selection type's Quick Actions) | partial | one clip group: Edit Clipping Path and Edit Contents, at the top of the panel rather than in Quick Actions (G1); the overview's additional controls ([Properties panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html)): Blend Options (one blend selected, with any tool, #4817) and Merge Live Paint (a Live Paint group with more) in Quick Actions, the Width Profile list (Width tool or a variable-width stroke) and an applied effect's Delete icon in Appearance, Corner and Corner Radius for corners selected with Direct Selection; Variable Font was already there |
| Select › Start Global Edit / Stop Global Edit; Properties › Quick Actions with the Global Edit Options popover (Match: Appearance, Size; On Artboards: All Artboards or a range, Include Objects On Canvas; Result(s)); Escape stops it; the matched objects are highlighted in their layer colour while it runs | partial | #3521, `select.globalEdit` / `select.globalEditOptions`, `crates/engine/src/tests/globaledit.rs`; paths, compound paths, symbol instances and groups of them (similar groups, #4939: edits inside one group reach the same object in the others; [public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html)); the Control panel's copy of the options, Shift-click to leave a match out, rotated copies and highlighting matches before the start remain ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)) |
| Contextual task bar | done | |