Merge pull request #7104 from dotCooCoo/feat/actions-record-into-selected

Actions: Begin Recording adds steps to the selected action, and a drag moves a step
This commit is contained in:
BFlat
2026-11-14 01:54:47 +07:00
committed by GitHub
34 changed files with 715 additions and 35 deletions
+8 -2
View File
@@ -1477,13 +1477,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.action.record",
"Begin Recording",
"",
"{set?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. An explicit invalid set, an existing recording, or a set where the new action would give it more than 1,000 actions or an action set file larger than 8 MiB is an error and changes nothing. While an action records, a recorded command whose step would give the action more than 10,000 steps or its set's action set file more than 8 MiB is an error and does not run, and the recording goes on. A recorded command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit and result; the status bar names the limit, later recorded commands are an error and do not run, and ui.action.stop leaves its step out → {set, name}",
"{set?: index, action?: index, after?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. With action, record into the action at action in set instead: ui.action.stop puts the recorded steps after step after of that action, else after its last step, and the steps after them keep whether they are excluded. The panel's Begin Recording button passes the selected action and its selected step. action without set, after without action, an index past the last set, action or step, an explicit invalid set, an existing recording, or a set where the new action would give it more than 1,000 actions or an action set file larger than 8 MiB is an error and changes nothing. While an action records, a recorded command whose step would give the action more than 10,000 steps or its set's action set file more than 8 MiB is an error and does not run, and the recording goes on. A recorded command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit and result; the status bar names the limit, later recorded commands are an error and do not run, and ui.action.stop leaves its step out → {set, name}, or with action {set, action, name}",
),
(
"ui.action.stop",
"Stop Playing/Recording",
"",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. Recorded steps still past 10,000 in the action, or past 8 MiB in its set's action set file, are left out, and the status bar names the limit. With nothing recording or playing it changes nothing → {stopped}",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it or the action ui.action.record named, at the place it named, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. Recorded steps still past 10,000 in the action, or past 8 MiB in its set's action set file, are left out, and the status bar names the limit. With nothing recording or playing it changes nothing → {stopped}",
),
(
"ui.action.insertStop",
@@ -1521,6 +1521,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{set: index, action: index, step: index} delete one step, a stop or a command, of the Actions panel's action at action in set (0 is the top set, action or step). The steps after it keep whether they are excluded, and no step of that action stays selected. A Record Stop 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 changes nothing. Undo does not bring the step back. The panel's Delete Selection runs this for the selected step → {set, action, step}",
),
(
"ui.action.moveStep",
"Move Action Step",
"",
"{set: index, action: index, step: index, toSet?: index, toAction?: index, before: index} move one step, a stop or a command, of the Actions panel's action at action in set (0 is the top set, action or step) into the action at toAction in toSet (default: the same set and action), before the step at index before there, or after its last step when before is its number of steps; before counts the steps as they are before the move. The step keeps its params and whether it is excluded, and the other steps keep theirs. A selected step stays selected, and a moved selected step makes its new action the selected one and its set the current one. A Record Stop dialog open on a step follows the step, and the steps that the recording in progress adds to either action stay after the steps that were before them. A step put back just before or just after itself does not move. A missing or invalid index, or a move 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. Undo does not move the step back. Dragging a step row in the panel runs this → {set, action, step, moved}",
),
(
"ui.batchDialog",
"Batch…",
+1
View File
@@ -6018,6 +6018,7 @@ axis Y Axis Y ос
Continue Продължаване
Continue Action Продължаване на действието
Delete Action Step Изтриване на стъпка от действието
Move Action Step Преместване на стъпка от действието
Press Play Current Selection in the Actions panel to go on. За да продължите, натиснете „Изпълнение на текущия избор“ в панела „Действия“.
Stop: {message} Спиране: {message}
@msg no action is waiting at a stop няма действие, което чака при спиране
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6060,6 +6060,7 @@ axis Y Axis Y osa
Continue Nastavi
Continue Action Nastavi akciju
Delete Action Step Izbriši korak akcije
Move Action Step Premjesti korak akcije
Press Play Current Selection in the Actions panel to go on. Za nastavak pritisnite „Reprodukuj trenutni odabir“ na panelu Akcije.
Stop: {message} Zaustavljanje: {message}
@msg no action is waiting at a stop nijedna akcija ne čeka na zaustavljanju
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4056,6 +4056,7 @@ digits Malayalam Malaiàlam
Continue Continua
Continue Action Continua l’acció
Delete Action Step Suprimeix el pas de l’acció
Move Action Step Mou el pas de l’acció
Press Play Current Selection in the Actions panel to go on. Per continuar, premeu «Reprodueix la selecció actual» al tauler Accions.
Stop: {message} Aturada: {message}
@msg no action is waiting at a stop cap acció no espera en una aturada
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6556,6 +6556,7 @@ axis Y Axis osa Y
Continue Pokračovat
Continue Action Pokračovat v akci
Delete Action Step Odstranit krok akce
Move Action Step Přesunout krok akce
Press Play Current Selection in the Actions panel to go on. Chcete-li pokračovat, stiskněte na panelu Akce tlačítko Přehrát aktuální výběr.
Stop: {message} Zastavení: {message}
@msg no action is waiting at a stop žádná akce nečeká na zastavení
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6034,6 +6034,7 @@ axis Y Axis Y-akse
Continue Fortsæt
Continue Action Fortsæt handling
Delete Action Step Slet handlingstrin
Move Action Step Flyt handlingstrin
Press Play Current Selection in the Actions panel to go on. Tryk på Afspil aktuel markering i panelet Handlinger for at fortsætte.
Stop: {message} Stop: {message}
@msg no action is waiting at a stop ingen handling venter ved et stop
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6148,6 +6148,7 @@ axis Y Axis Y-Achse
Continue Fortsetzen
Continue Action Aktion fortsetzen
Delete Action Step Aktionsschritt löschen
Move Action Step Aktionsschritt verschieben
Press Play Current Selection in the Actions panel to go on. Klicken Sie im Bedienfeld „Aktionen“ auf „Aktuelle Auswahl abspielen“, um fortzufahren.
Stop: {message} Stopp: {message}
@msg no action is waiting at a stop keine Aktion wartet an einem Stopp
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6508,6 +6508,7 @@ axis Y Axis Άξονας Y
Continue Συνέχεια
Continue Action Συνέχεια ενέργειας
Delete Action Step Διαγραφή βήματος ενέργειας
Move Action Step Μετακίνηση βήματος ενέργειας
Press Play Current Selection in the Actions panel to go on. Για να συνεχίσετε, πατήστε «Αναπαραγωγή τρέχουσας επιλογής» στο πάνελ Ενέργειες.
Stop: {message} Διακοπή: {message}
@msg no action is waiting at a stop καμία ενέργεια δεν περιμένει σε διακοπή
Can't render this file because it is too large.
+1
View File
@@ -6499,6 +6499,7 @@ axis Y Axis Eje Y
Continue Continuar
Continue Action Continuar acción
Delete Action Step Eliminar paso de la acción
Move Action Step Mover paso de la acción
Press Play Current Selection in the Actions panel to go on. Para continuar, pulse Reproducir la selección actual en el panel Acciones.
Stop: {message} Parada: {message}
@msg no action is waiting at a stop ninguna acción está esperando en una parada
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6586,6 +6586,7 @@ axis Y Axis Y-akseli
Continue Jatka
Continue Action Jatka toimintoa
Delete Action Step Poista toiminnon vaihe
Move Action Step Siirrä toiminnon vaihe
Press Play Current Selection in the Actions panel to go on. Jatka valitsemalla Toiminnot-paneelista Toista nykyinen valinta.
Stop: {message} Pysäytys: {message}
@msg no action is waiting at a stop mikään toiminto ei odota pysäytyksessä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6551,6 +6551,7 @@ axis Y Axis Axe Y
Continue Continuer
Continue Action Continuer le script
Delete Action Step Supprimer l’étape du script
Move Action Step Déplacer l’étape du script
Press Play Current Selection in the Actions panel to go on. Pour continuer, cliquez sur Exécuter la sélection actuelle dans le panneau Scripts.
Stop: {message} Arrêt : {message}
@msg no action is waiting at a stop aucun script n’attend à un arrêt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3482,6 +3482,7 @@ antiAlias Sharp शार्प
Continue जारी रखें
Continue Action एक्शन जारी रखें
Delete Action Step एक्शन का चरण हटाएं
Move Action Step एक्शन का चरण खिसकाएं
Press Play Current Selection in the Actions panel to go on. आगे बढ़ने के लिए एक्शन्स पैनल में वर्तमान सिलेक्शन प्ले करें दबाएँ।
Stop: {message} स्टॉप: {message}
@msg no action is waiting at a stop कोई एक्शन स्टॉप पर प्रतीक्षा नहीं कर रहा है
Can't render this file because it is too large.
+1
View File
@@ -6539,6 +6539,7 @@ axis Y Axis Y tengely
Continue Folytatás
Continue Action Művelet folytatása
Delete Action Step Műveletlépés törlése
Move Action Step Műveletlépés áthelyezése
Press Play Current Selection in the Actions panel to go on. A folytatáshoz nyomja meg a Műveletek panelen a Kijelölt művelet lejátszása gombot.
Stop: {message} Megállás: {message}
@msg no action is waiting at a stop egyetlen művelet sem várakozik megállásnál
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6034,6 +6034,7 @@ axis Y Axis Sumbu Y
Continue Lanjutkan
Continue Action Lanjutkan tindakan
Delete Action Step Hapus langkah tindakan
Move Action Step Pindahkan langkah tindakan
Press Play Current Selection in the Actions panel to go on. Untuk melanjutkan, tekan Putar seleksi saat ini di panel Tindakan.
Stop: {message} Henti: {message}
@msg no action is waiting at a stop tidak ada tindakan yang menunggu di titik henti
Can't render this file because it contains an unexpected character in line 2422 and column 5.
+1
View File
@@ -6182,6 +6182,7 @@ axis Y Axis Asse Y
Continue Continua
Continue Action Continua azione
Delete Action Step Elimina passaggio dell’azione
Move Action Step Sposta passaggio dell’azione
Press Play Current Selection in the Actions panel to go on. Per continuare, premi Esegui selezione corrente nel pannello Azioni.
Stop: {message} Interruzione: {message}
@msg no action is waiting at a stop nessuna azione è in attesa a un'interruzione
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6248,6 +6248,7 @@ axis Y Axis Y 軸
Continue 続行
Continue Action アクションを続行
Delete Action Step アクションステップを削除
Move Action Step アクションステップを移動
Press Play Current Selection in the Actions panel to go on. 続行するには、アクションパネルの「選択項目を再生」を押します。
Stop: {message} 停止: {message}
@msg no action is waiting at a stop 停止で待機中のアクションはありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6507,6 +6507,7 @@ axis Y Axis Y ღერძი
Continue გაგრძელება
Continue Action მოქმედების გაგრძელება
Delete Action Step მოქმედების ნაბიჯის წაშლა
Move Action Step მოქმედების ნაბიჯის გადატანა
Press Play Current Selection in the Actions panel to go on. გასაგრძელებლად მოქმედებების პანელზე დააჭირეთ „მიმდინარე მონიშნულის შესრულებას“.
Stop: {message} გაჩერება: {message}
@msg no action is waiting at a stop გაჩერებაზე არცერთი მოქმედება არ ელოდება
Can't render this file because it is too large.
+1
View File
@@ -6143,6 +6143,7 @@ axis Y Axis Y축
Continue 계속
Continue Action 액션 계속
Delete Action Step 액션 단계 삭제
Move Action Step 액션 단계 이동
Press Play Current Selection in the Actions panel to go on. 계속하려면 액션 패널에서 현재 선택 항목 재생을 누르세요.
Stop: {message} 중지: {message}
@msg no action is waiting at a stop 중지에서 대기 중인 액션이 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6651,6 +6651,7 @@ axis Y Axis Y-as
Continue Doorgaan
Continue Action Actie voortzetten
Delete Action Step Actiestap verwijderen
Move Action Step Actiestap verplaatsen
Press Play Current Selection in the Actions panel to go on. Druk in het paneel Acties op Huidige selectie afspelen om door te gaan.
Stop: {message} Stop: {message}
@msg no action is waiting at a stop geen actie wacht bij een stop
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6411,6 +6411,7 @@ axis Y Axis Oś Y
Continue Kontynuuj
Continue Action Kontynuuj operację
Delete Action Step Usuń krok operacji
Move Action Step Przenieś krok operacji
Press Play Current Selection in the Actions panel to go on. Aby kontynuować, naciśnij Odtwórz bieżące zaznaczenie w panelu Operacje.
Stop: {message} Zatrzymanie: {message}
@msg no action is waiting at a stop żadna operacja nie czeka na zatrzymaniu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6471,6 +6471,7 @@ axis Y Axis Eixo Y
Continue Continuar
Continue Action Continuar Ação
Delete Action Step Excluir Etapa da Ação
Move Action Step Mover Etapa da Ação
Press Play Current Selection in the Actions panel to go on. Para continuar, pressione Reproduzir Seleção Atual no painel Ações.
Stop: {message} Parada: {message}
@msg no action is waiting at a stop nenhuma ação está aguardando em uma parada
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -6167,6 +6167,7 @@ axis Y Axis Ось Y
Continue Продолжить
Continue Action Продолжить действие
Delete Action Step Удалить шаг действия
Move Action Step Переместить шаг действия
Press Play Current Selection in the Actions panel to go on. Чтобы продолжить, нажмите «Воспроизвести текущее выделение» на панели «Действия».
Stop: {message} Остановка: {message}
@msg no action is waiting at a stop ни одно действие не ожидает на остановке
Can't render this file because it is too large.
+1
View File
@@ -6537,6 +6537,7 @@ axis Y Axis Y-axel
Continue Fortsätt
Continue Action Fortsätt åtgärd
Delete Action Step Radera åtgärdssteg
Move Action Step Flytta åtgärdssteg
Press Play Current Selection in the Actions panel to go on. Fortsätt genom att trycka på Spela upp aktuell markering i panelen Åtgärder.
Stop: {message} Stopp: {message}
@msg no action is waiting at a stop ingen åtgärd väntar vid ett stopp
Can't render this file because it is too large.
+1
View File
@@ -6512,6 +6512,7 @@ axis Y Axis Y ekseni
Continue Devam Et
Continue Action Eyleme Devam Et
Delete Action Step Eylem Adımını Sil
Move Action Step Eylem Adımını Taşı
Press Play Current Selection in the Actions panel to go on. Devam etmek için Eylemler panelinde Geçerli Seçimi Oynat'a basın.
Stop: {message} Durak: {message}
@msg no action is waiting at a stop durakta bekleyen eylem yok
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -6162,6 +6162,7 @@ axis Y Axis Вісь Y
Continue Продовжити
Continue Action Продовжити дію
Delete Action Step Видалити крок дії
Move Action Step Перемістити крок дії
Press Play Current Selection in the Actions panel to go on. Щоб продовжити, натисніть «Відтворити поточне виділення» на панелі «Дії».
Stop: {message} Зупинка: {message}
@msg no action is waiting at a stop жодна дія не очікує на зупинці
Can't render this file because it is too large.
+1
View File
@@ -6163,6 +6163,7 @@ axis Y Axis Trục Y
Continue Tiếp tục
Continue Action Tiếp tục hành động
Delete Action Step Xóa bước hành động
Move Action Step Di chuyển bước hành động
Press Play Current Selection in the Actions panel to go on. Để tiếp tục, nhấn Phát vùng chọn hiện tại trong bảng Hành động.
Stop: {message} Điểm dừng: {message}
@msg no action is waiting at a stop không có hành động nào đang chờ ở điểm dừng
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -6395,6 +6395,7 @@ axis Y Axis Y 轴
Continue 继续
Continue Action 继续动作
Delete Action Step 删除动作步骤
Move Action Step 移动动作步骤
Press Play Current Selection in the Actions panel to go on. 要继续,请按“动作”面板中的“播放当前选择项目”。
Stop: {message} 停止:{message}
@msg no action is waiting at a stop 没有动作在停止处等待
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6201,6 +6201,7 @@ axis Y Axis Y 軸
Continue 繼續
Continue Action 繼續動作
Delete Action Step 刪除動作步驟
Move Action Step 移動動作步驟
Press Play Current Selection in the Actions panel to go on. 若要繼續,請按「動作」面板中的「播放目前選取項目」。
Stop: {message} 停止:{message}
@msg no action is waiting at a stop 沒有動作在停止處等待
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -989,6 +989,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
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),
crate::panels::actions::MOVE_STEP => crate::panels::actions::move_step(app, p),
"ui.batchDialog" => {
let index = |k: &str| opt_index(p, k).map(|i| i.unwrap_or(0));
index("set").and_then(|set| Ok((set, index("action")?))).and_then(|(set, action)| crate::dialogs::action_batch::open(app, set, action))
+8 -2
View File
@@ -1478,13 +1478,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::START_RECORDING,
"Begin Recording",
"",
"{set?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. An explicit invalid set, an existing recording, or a set where the new action would give it more than 1,000 actions or an action set file larger than 8 MiB is an error and changes nothing. While an action records, a recorded command whose step would give the action more than 10,000 steps or its set's action set file more than 8 MiB is an error and does not run, and the recording goes on. A recorded command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit and result; the status bar names the limit, later recorded commands are an error and do not run, and ui.action.stop leaves its step out → {set, name}",
"{set?: index, action?: index, after?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. With action, record into the action at action in set instead: ui.action.stop puts the recorded steps after step after of that action, else after its last step, and the steps after them keep whether they are excluded. The panel's Begin Recording button passes the selected action and its selected step. action without set, after without action, an index past the last set, action or step, an explicit invalid set, an existing recording, or a set where the new action would give it more than 1,000 actions or an action set file larger than 8 MiB is an error and changes nothing. While an action records, a recorded command whose step would give the action more than 10,000 steps or its set's action set file more than 8 MiB is an error and does not run, and the recording goes on. A recorded command whose step passes a limit only as it runs, from values it adds to its step, keeps its edit and result; the status bar names the limit, later recorded commands are an error and do not run, and ui.action.stop leaves its step out → {set, name}, or with action {set, action, name}",
),
(
crate::panels::actions::STOP_RECORDING,
"Stop Playing/Recording",
"",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. Recorded steps still past 10,000 in the action, or past 8 MiB in its set's action set file, are left out, and the status bar names the limit. With nothing recording or playing it changes nothing → {stopped}",
"{} end the Actions panel's recording and keep its steps, in the action ui.action.new made for it or the action ui.action.record named, at the place it named, else in a new action after the last action of its set, as the panel's Stop Playing/Recording button does. It also ends an action playing step by step or with pauses, or one waiting at a stop, whose message box closes: the steps played stay and the rest do not run. Recorded steps still past 10,000 in the action, or past 8 MiB in its set's action set file, are left out, and the status bar names the limit. With nothing recording or playing it changes nothing → {stopped}",
),
(
crate::panels::actions::INSERT_STOP,
@@ -1522,6 +1522,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{set: index, action: index, step: index} delete one step, a stop or a command, of the Actions panel's action at action in set (0 is the top set, action or step). The steps after it keep whether they are excluded, and no step of that action stays selected. A Record Stop 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 changes nothing. Undo does not bring the step back. The panel's Delete Selection runs this for the selected step → {set, action, step}",
),
(
crate::panels::actions::MOVE_STEP,
"Move Action Step",
"",
"{set: index, action: index, step: index, toSet?: index, toAction?: index, before: index} move one step, a stop or a command, of the Actions panel's action at action in set (0 is the top set, action or step) into the action at toAction in toSet (default: the same set and action), before the step at index before there, or after its last step when before is its number of steps; before counts the steps as they are before the move. The step keeps its params and whether it is excluded, and the other steps keep theirs. A selected step stays selected, and a moved selected step makes its new action the selected one and its set the current one. A Record Stop dialog open on a step follows the step, and the steps that the recording in progress adds to either action stay after the steps that were before them. A step put back just before or just after itself does not move. A missing or invalid index, or a move 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. Undo does not move the step back. Dragging a step row in the panel runs this → {set, action, step, moved}",
),
(
"ui.batchDialog",
"Batch…",
+636 -22
View File
@@ -894,16 +894,27 @@ pub const STOP_RECORDING: &str = "ui.action.stop";
/// The command that begins a plain recording ([`record`]).
pub const START_RECORDING: &str = "ui.action.record";
/// `ui.action.record {set?}`: begins recording an automatically named Action N in `set`, or in
/// the current set (the first if none is current). With no sets, makes Set 1. Stop adds the action
/// after the set's existing actions. An explicit invalid set, an existing recording, or a set whose
/// new action would pass a limit of Load Actions… ([`edit_keeps_set_loadable`]: 1,000 actions,
/// 8 MiB) is an error and leaves the panel unchanged. While it records, a command whose step does not fit in the action
/// ([`recording_room`]) does not run. → {set, name}
/// `ui.action.record {set?, action?, after?}`: begins recording an automatically named Action N in
/// `set`, or in the current set (the first if none is current). With no sets, makes Set 1. Stop
/// adds the action after the set's existing actions. With `action`, it records into the action at
/// that index of set `set` instead: Stop puts the recorded steps after step `after` of that action,
/// else after its last step, and the steps after them keep whether they are excluded. The Begin
/// Recording button passes the selected action and step. `action` without `set`, `after` without
/// `action`, an index past the last set, action or step, an explicit invalid set, an existing
/// recording, or a set whose new action would pass a limit of Load Actions…
/// ([`edit_keeps_set_loadable`]: 1,000 actions, 8 MiB) is an error and leaves the panel unchanged.
/// While it records, a command whose step does not fit in the action ([`recording_room`]) does not
/// run. → {set, name}, and with `action` {set, action, name}
pub fn record(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
if app.ui.recording.is_some() {
return Err("an action is already recording".into());
}
if p.get("action").is_some() {
return record_into(app, p);
}
if p.get("after").is_some() {
return Err("no such action".into());
}
let set = match p.get("set") {
Some(v) => v.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < app.ui.action_sets.len()).ok_or("no such action set")?,
_ => {
@@ -931,8 +942,32 @@ pub fn record(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
Ok(json!({ "set": set, "name": name }))
}
/// [`record`] with `action`: records into the action at index `action` of set `set`, after step
/// `after` or after its last step. The set becomes current and the only selected set, and the
/// action stays selected. → {set, action, name}
fn record_into(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 (set, at) =
index("set").zip(index("action")).filter(|&(s, a)| app.ui.action_sets.get(s).is_some_and(|s| a < s.actions.len())).ok_or("no such action")?;
let action = &app.ui.action_sets[set].actions[at];
let slot = match p.get("after") {
None => None,
Some(_) => Some(index("after").filter(|&s| s < action.steps.len()).ok_or("no such action step")? + 1),
};
let (id, name) = (action.id, action.name.clone());
app.ui.action_set = Some(set);
app.ui.action_sets_selected = vec![set];
start_recording(app, set, name.clone(), Some(id));
app.ui.recording_at = slot;
if let Some(ctx) = app.ui_ctx.clone() {
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((set, at)));
}
Ok(json!({ "set": set, "action": at, "name": name }))
}
/// `ui.action.stop {}`: ends the recording in progress and keeps its steps, in the action New
/// Action made for it, else in a new action after the last action of its set, as the bottom bar's
/// Action made for it or the action [`record`] named, at the place it named, else in a new action
/// after the last action of its set, as the bottom bar's
/// Stop Playing/Recording does. It also ends a playback at Step by Step or Pause For, or one that
/// waits at a stop, whose message box closes: the steps already played stay, and the rest do not
/// run. With nothing recording or playing it changes nothing. Recorded steps past the limits of an
@@ -951,6 +986,13 @@ pub const INSERT_STOP: &str = "ui.action.insertStop";
/// The panel state that holds the selected step as (the action's [`Action::id`], the step's index).
const SELECTED_STEP: &str = "actions-selected-step";
/// The panel state that holds the step being dragged as a [`DraggedStep`].
const DRAGGED_STEP: &str = "actions-dragged-step";
/// A step being dragged: the action's [`Action::id`], the step's index, and the action's steps and
/// excluded steps as they were when the drag started.
type DraggedStep = (u64, usize, std::sync::Arc<(Vec<(String, Value)>, BTreeSet<usize>)>);
/// `ui.action.insertStop {set?, action?, step?, message?, allowContinue?}`: inserts a stop. With `set`
/// and `action`, it goes into that action, after step `step`, else after its last step. Without
/// them, it goes into the recording in progress, after the steps recorded so far. `message` is
@@ -1083,9 +1125,9 @@ fn recording_start(app: &VectorcraftApp) -> Option<usize> {
}
/// Where Stop Playing/Recording puts the steps of the recording in progress: the index of the set,
/// and of the action New Action made for it, or None for the new action Stop adds after the set's
/// last action. None when there is no recording, no set to put them in, or the action New Action
/// made for it was deleted, so that Stop keeps none of them.
/// and of the action it records into ([`crate::UiState::recording_into`]), or None for the new
/// action Stop adds after the set's last action. None when there is no recording, no set to put
/// them in, or the action it records into was deleted, so that Stop keeps none of them.
fn recording_target(app: &VectorcraftApp) -> Option<(usize, Option<usize>)> {
let (set, ..) = app.ui.recording.as_ref()?;
let Some(id) = app.ui.recording_into else {
@@ -1107,7 +1149,7 @@ fn set_with_recorded(app: &VectorcraftApp, steps: Vec<(String, Value)>) -> Optio
s.actions.len() - 1
}
};
extend_recorded(s.actions.get_mut(at)?, steps);
put_recorded(s.actions.get_mut(at)?, steps, app.ui.recording_at);
Some((s, at))
}
@@ -1144,7 +1186,7 @@ fn set_sizes(app: &VectorcraftApp, at: Option<usize>, set: &ActionSet) -> [usize
let mut s = set.clone();
let steps = app.session.journal_for_action(start);
match app.ui.recording_into.and_then(|id| s.actions.iter().position(|a| a.id == id)) {
Some(i) => extend_recorded(&mut s.actions[i], steps),
Some(i) => put_recorded(&mut s.actions[i], steps, app.ui.recording_at),
None => s.actions.push(Action::new(name, steps)),
}
with = s;
@@ -1359,12 +1401,17 @@ fn insert(app: &mut VectorcraftApp, p: &Value, step: (String, Value)) -> Result<
let excluded = action.excluded_steps.clone();
action.insert_step(after, step);
let id = action.id;
// The recording's steps stay after the steps that were before them.
let slot = match (&recorded, app.ui.recording_at) {
(Some(r), Some(slot)) if r.into == Some(at) && after <= slot => Some(slot + 1),
(_, slot) => slot,
};
let file_len = match recorded {
None => files::write_set(s).len(),
Some(Recorded { into, name, steps }) => {
let mut kept = s.clone();
match into {
Some(into) => kept.actions[into].steps.extend(steps),
Some(into) => put_recorded(&mut kept.actions[into], steps, slot),
None => kept.actions.push(Action::new(name, steps)),
}
files::write_set(&kept).len()
@@ -1376,6 +1423,7 @@ fn insert(app: &mut VectorcraftApp, p: &Value, step: (String, Value)) -> Result<
action.excluded_steps = excluded;
return Err("the action set would be larger than 8 MiB".into());
}
app.ui.recording_at = slot;
app.ui.action_set = Some(set);
app.ui.action_sets_selected = vec![set];
if let Some(ctx) = app.ui_ctx.clone() {
@@ -1415,6 +1463,12 @@ pub fn delete_step(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
let step = index("step").filter(|&s| s < action.steps.len()).ok_or("no such action step")?;
action.remove_step(step);
let id = action.id;
// The recording's steps stay after the steps that were before them.
if app.ui.recording_into == Some(id)
&& let Some(slot) = app.ui.recording_at.filter(|&slot| step < slot)
{
app.ui.recording_at = Some(slot - 1);
}
if let Some(ctx) = app.ui_ctx.clone()
&& pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP).is_some_and(|(selected, _)| selected == id)
{
@@ -1437,6 +1491,115 @@ pub fn delete_step(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
Ok(json!({"set": set, "action": at, "step": step}))
}
/// The command that moves one step of an action ([`move_step`]).
pub const MOVE_STEP: &str = "ui.action.moveStep";
/// `ui.action.moveStep {set, action, step, toSet?, toAction?, before}`: moves step `step`, a stop or
/// a command, of the action at index `action` of set `set` into the action at index `toAction` of
/// set `toSet` (default: the same set and action), before the step at index `before` there, or
/// after its last step when `before` is its number of steps. `before` counts the steps as they are
/// before the move. The step keeps its params and whether it is excluded, and the other steps keep
/// theirs. A selected step stays selected: a moved selected step makes its new action the selected
/// one and its set the current one. A Record Stop, Insert Menu Item or Set Selection dialog open on
/// a step follows the step, and the steps a recording adds to either action stay after the steps
/// that were before them. A step put back just before or just after itself does not move. A missing
/// or invalid index, or a move that would give a set a file that Load Actions… refuses
/// ([`edit_keeps_set_loadable`]: 10,000 steps in an action, 8 MiB), is an error, and then nothing
/// changes. Undo does not move the step back. A drag of a step row in the panel runs this.
/// → {set, action, step, moved}: where the step is now
pub fn move_step(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let number = |v: &Value| v.as_u64().and_then(|i| usize::try_from(i).ok());
let index = |k: &str| p.get(k).and_then(number);
let (set, at) = index("set").zip(index("action")).ok_or("no such action")?;
let source = app.ui.action_sets.get(set).and_then(|s| s.actions.get(at)).ok_or("no such action")?;
let step = index("step").filter(|&s| s < source.steps.len()).ok_or("no such action step")?;
let source_id = source.id;
let to_set = match p.get("toSet") {
None => set,
Some(v) => number(v).filter(|&s| s < app.ui.action_sets.len()).ok_or("no such action set")?,
};
let to_action = match p.get("toAction") {
None => at,
Some(v) => number(v).ok_or("no such action")?,
};
let dest = app.ui.action_sets[to_set].actions.get(to_action).ok_or("no such action")?;
let dest_id = dest.id;
let before = index("before").filter(|&b| b <= dest.steps.len()).ok_or("no such action step")?;
let same = (set, at) == (to_set, to_action);
if same && (before == step || before == step + 1) {
return Ok(json!({"set": set, "action": at, "step": step, "moved": false}));
}
// The step's index once it is in place, counted without it in its own action.
let to = if same && before > step { before - 1 } else { before };
let mut from_edit = app.ui.action_sets[set].clone();
let moved = from_edit.actions[at].steps[step].clone();
let excluded = from_edit.actions[at].excluded_steps.contains(&step);
from_edit.actions[at].remove_step(step);
let put = |a: &mut Action| {
a.insert_step(to, moved);
if excluded {
a.excluded_steps.insert(to);
}
};
let to_edit = if to_set == set {
put(&mut from_edit.actions[to_action]);
None
} else {
let mut edit = app.ui.action_sets[to_set].clone();
put(&mut edit.actions[to_action]);
Some(edit)
};
edit_keeps_set_loadable(app, Some(set), &from_edit)?;
if let Some(edit) = &to_edit {
edit_keeps_set_loadable(app, Some(to_set), edit)?;
}
app.ui.action_sets[set] = from_edit;
if let Some(edit) = to_edit {
app.ui.action_sets[to_set] = edit;
}
// A step index in `source_id` or `dest_id` as (the action's id, the step) after the move.
let follow = |(id, s): (u64, usize)| -> (u64, usize) {
if (id, s) == (source_id, step) {
return (dest_id, to);
}
let s = if id == source_id && s > step { s - 1 } else { s };
(id, if id == dest_id && s >= to { s + 1 } else { s })
};
// The recording's steps stay after the steps that were before them.
if let Some(slot) = app.ui.recording_at {
let slot = if app.ui.recording_into == Some(source_id) && step < slot { slot - 1 } else { slot };
app.ui.recording_at = Some(if app.ui.recording_into == Some(dest_id) && to <= slot { slot + 1 } else { slot });
}
if let Some(ctx) = app.ui_ctx.clone()
&& let Some(selected) = pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP)
{
set_pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP, Some(follow(selected)));
if selected == (source_id, step) {
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((to_set, to_action)));
let mut expanded: BTreeSet<u64> = pstate(&ctx, "actions-expanded");
expanded.insert(dest_id);
set_pstate(&ctx, "actions-expanded", expanded);
app.ui.action_set = Some(to_set);
app.ui.action_sets_selected = vec![to_set];
}
}
// A Record Stop, Insert Menu Item or Set Selection dialog open on a step of either action follows it.
let either = |s: usize, a: usize| (s, a) == (set, at) || (s, a) == (to_set, to_action);
if let Some(d) = app.ui.dialog.as_mut().filter(|d| holds_step(d))
&& let (Some(ds), Some(da), Some(ds_step)) =
(d.fields.get("set").and_then(number), d.fields.get("action").and_then(number), d.fields.get("step").and_then(number))
&& either(ds, da)
{
let id = if (ds, da) == (set, at) { source_id } else { dest_id };
let (id, s) = follow((id, ds_step));
let (ds, da) = if id == dest_id { (to_set, to_action) } else { (set, at) };
d.fields.insert("set".into(), json!(ds));
d.fields.insert("action".into(), json!(da));
d.fields.insert("step".into(), json!(s));
}
Ok(json!({"set": to_set, "action": to_action, "step": to, "moved": true}))
}
/// The command that `chord` runs under `km`, pressed once or pressed twice.
#[cfg(test)]
fn command_on(km: &craft_keymap::Keymap, chord: &craft_keymap::Chord) -> Option<craft_keymap::CommandRef> {
@@ -1471,6 +1634,7 @@ fn start_recording(app: &mut VectorcraftApp, set: usize, name: String, into: Opt
app.session.journal.hold(Some(start));
app.ui.recording = Some((set, name, start));
app.ui.recording_into = into;
app.ui.recording_at = None;
app.ui.recording_base = None;
app.ui.recording_tail = None;
app.ui.recording_checked = Some((app.session.journal.rewrites(), start));
@@ -1498,12 +1662,15 @@ fn steps_that_fit(app: &VectorcraftApp, steps: &[(String, Value)]) -> (usize, Op
(keep, (keep < steps.len()).then_some("the action already has 10,000 steps"))
}
/// Adds the recorded `steps` after the steps of `action`, included in playback. An exclusion past
/// the action's last step, which saved preferences can hold, goes.
fn extend_recorded(action: &mut Action, steps: Vec<(String, Value)>) {
/// Puts the recorded `steps` into `action` before its step at index `at`, or after its last step
/// when `at` is None or past it, included in playback. The steps after them keep whether they are
/// excluded. An exclusion past the action's last step, which saved preferences can hold, goes.
fn put_recorded(action: &mut Action, steps: Vec<(String, Value)>, at: Option<usize>) {
let len = action.steps.len();
action.excluded_steps.retain(|&i| i < len);
action.steps.extend(steps);
let at = at.map_or(len, |at| at.min(len));
let added = steps.len();
action.excluded_steps = action.excluded_steps.iter().filter(|&&i| i < len).map(|&i| if i >= at { i + added } else { i }).collect();
action.steps.splice(at..at, steps);
}
/// Stop Playing/Recording: ends the recording and keeps its steps (see [`start_recording`]). An
@@ -1522,10 +1689,16 @@ fn stop_recording(app: &mut VectorcraftApp) {
app.ui.recording_tail = None;
app.ui.recording_checked = None;
app.session.journal.hold(None);
let at = app.ui.recording_at.take();
match app.ui.recording_into.take() {
Some(id) => {
if let Some(a) = app.ui.action_sets.iter_mut().flat_map(|s| s.actions.iter_mut()).find(|a| a.id == id) {
extend_recorded(a, steps);
let found = app.ui.action_sets.iter().enumerate().find_map(|(s, set)| Some((s, set.actions.iter().position(|a| a.id == id)?)));
if let Some((s, a)) = found {
let action = &mut app.ui.action_sets[s].actions[a];
let len = action.steps.len();
let (from, added) = (at.map_or(len, |at| at.min(len)), steps.len());
put_recorded(action, steps, at);
follow_recorded(app, (s, a, id), from, added);
}
}
None => {
@@ -1536,6 +1709,28 @@ fn stop_recording(app: &mut VectorcraftApp) {
}
}
/// After Stop Playing/Recording put `added` steps into the action at `place` (set, action, id)
/// before its step at index `from`: the selected step and a Record Stop, Insert Menu Item or Set
/// Selection dialog open on a later step of the action follow their step down.
fn follow_recorded(app: &mut VectorcraftApp, (set, at, id): (usize, usize, u64), from: usize, added: usize) {
if added == 0 {
return;
}
if let Some(ctx) = app.ui_ctx.clone()
&& let Some((step_id, step)) = pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP).filter(|&(step_id, step)| step_id == id && step >= from)
{
set_pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP, Some((step_id, step + added)));
}
let number = |v: &Value| v.as_u64().and_then(|i| usize::try_from(i).ok());
if let Some(d) = app.ui.dialog.as_mut().filter(|d| holds_step(d))
&& d.fields.get("set").and_then(number) == Some(set)
&& d.fields.get("action").and_then(number) == Some(at)
&& let Some(step) = d.fields.get("step").and_then(number).filter(|&step| step >= from)
{
d.fields.insert("step".into(), json!(step + added));
}
}
crate::shortcut_editor::ui_mirror!(fn function_key_chords() -> Vec<craft_keymap::Chord>);
/// Mirrors the actions' function keys for code without app access (cheap when unchanged).
@@ -1673,6 +1868,7 @@ pub fn delete_sets(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
if sets.contains(rec) {
app.ui.recording = None;
app.ui.recording_into = None;
app.ui.recording_at = None;
app.session.journal.hold(None);
} else {
*rec -= sets.iter().filter(|&&s| s < *rec).count();
@@ -1926,6 +2122,34 @@ fn inclusion_box(
(response.on_hover_text(if included { tl!("Exclude from playback") } else { tl!("Include in playback") }).clicked(), hit)
}
/// While a step is dragged over the list: where it goes as (set, action, `before` of [`MOVE_STEP`]),
/// shown by a line in `color` on the row under the pointer, or None off every action's rows
/// (`slots`, as [`show`] lists them). Near the list's top or bottom edge, the list scrolls.
fn drop_target(ui: &mut egui::Ui, slots: &[(egui::Rect, usize, usize, usize, usize)], color: egui::Color32) -> Option<(usize, usize, usize)> {
let p = ui.ctx().pointer_interact_pos()?;
let clip = ui.clip_rect();
if !clip.contains(p) {
return None;
}
let edge = 12.0;
let scroll = if p.y < clip.top() + edge {
6.0
} else if p.y > clip.bottom() - edge {
-6.0
} else {
0.0
};
if scroll != 0.0 && ui.input(|i| i.pointer.any_down()) {
ui.scroll_with_delta(egui::vec2(0.0, scroll));
ui.ctx().request_repaint();
}
let half_gap = ui.spacing().item_spacing.y / 2.0;
let &(r, si, ai, upper, lower) = slots.iter().find(|(r, ..)| (r.top() - half_gap..r.bottom() + half_gap).contains(&p.y))?;
let (before, y) = if upper != lower && p.y < r.center().y { (upper, r.top()) } else { (lower, r.bottom()) };
ui.painter().line_segment([egui::pos2(r.left() + 42.0, y), egui::pos2(r.right(), y)], egui::Stroke::new(2.0, color));
Some((si, ai, before))
}
enum InclusionChange {
Set(usize),
Action(usize, usize),
@@ -1954,6 +2178,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
let mut expanded: BTreeSet<u64> = pstate(ui.ctx(), "actions-expanded");
let mut inclusion_changes = Vec::new();
let mut play_req = None;
// The step being dragged, from the frame its drag started. The drag ends without a move when a
// command since then changed its action's steps or which of them are excluded, or deleted the
// action.
let dragging = pstate::<Option<DraggedStep>>(ui.ctx(), DRAGGED_STEP).filter(|(id, _, was)| {
let action = app.ui.action_sets.iter().flat_map(|s| &s.actions).find(|a| a.id == *id);
action.is_some_and(|a| (&a.steps, &a.excluded_steps) == (&was.0, &was.1))
});
let mut drag_started = None;
let mut drag_stopped = false;
// The rows a dragged step can be dropped on: (row, set, action, where it goes on the row's upper
// half, where on its lower half), as `before` of [`MOVE_STEP`].
let mut slots: Vec<(egui::Rect, usize, usize, usize, usize)> = Vec::new();
let mut drop = None;
let list_height = widgets::panel_list_height(ui, 260.0, 0.0).min(260.0);
egui::ScrollArea::vertical().max_height(list_height).min_scrolled_height(list_height.min(64.0)).show(ui, |ui| {
for (si, set) in app.ui.action_sets.iter().enumerate() {
@@ -2039,9 +2276,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
if resp.double_clicked() && !resp.interact_pointer_pos().is_some_and(|p| check_rect.contains(p) || disclosure.contains(p)) {
play_req = Some((si, ai));
}
// A step dropped on an action's row goes first when the action shows its steps, else
// after its last step.
let first = if open && !a.steps.is_empty() { 0 } else { a.steps.len() };
slots.push((r, si, ai, first, first));
if open {
for (step, (command, params)) in a.steps.iter().enumerate() {
let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 20.0), egui::Sense::click());
let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 20.0), egui::Sense::click_and_drag());
slots.push((r, si, ai, step, step + 1));
if resp.drag_started() {
drag_started = Some((a.id, step, std::sync::Arc::new((a.steps.clone(), a.excluded_steps.clone()))));
}
drag_stopped |= resp.drag_stopped();
if step_selected == Some((a.id, step)) {
ui.painter().rect_filled(r, 0.0, t.row_selected);
} else if resp.hovered() {
@@ -2085,7 +2331,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
}
}
}
if dragging.is_some() {
drop = drop_target(ui, &slots, t.accent);
}
});
// A released drag moves the step once, to where the line showed.
let dragged = dragging.as_ref().map(|(id, step, _)| (*id, *step));
let move_req = drag_stopped.then_some(dragged.zip(drop)).flatten().and_then(|((id, step), (si, ai, before))| {
let (fs, fa) = app.ui.action_sets.iter().enumerate().find_map(|(fs, s)| Some((fs, s.actions.iter().position(|a| a.id == id)?)))?;
Some(json!({"set": fs, "action": fa, "step": step, "toSet": si, "toAction": ai, "before": before}))
});
let still_dragging = !drag_stopped && ui.input(|i| i.pointer.any_down());
set_pstate(ui.ctx(), DRAGGED_STEP, drag_started.or(dragging.filter(|_| still_dragging)));
if waiting_at_stop(app) {
dim_label(ui, tl!("Press Play Current Selection in the Actions panel to go on."));
}
@@ -2114,6 +2371,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
set_pstate(ui.ctx(), SELECTED_STEP, step_selected);
app.ui.action_set = current;
app.ui.action_sets_selected = sets_selected;
if let Some(params) = move_req
&& let Err(e) = app.run(MOVE_STEP, params)
{
app.status(e);
}
// Begin Recording records into the selected action, after its selected step.
let record_params = match selected.and_then(|(s, a)| Some((s, a, app.ui.action_sets.get(s)?.actions.get(a)?.id))) {
Some((s, a, id)) => match step_selected.filter(|&(step_id, _)| step_id == id) {
Some((_, step)) => json!({"set": s, "action": a, "after": step}),
None => json!({"set": s, "action": a}),
},
None => json!({}),
};
if app.ui.action_sets.is_empty() {
dim_label(ui, tl!("No actions. Press ● to record."));
}
@@ -2129,7 +2399,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
&& let Err(e) = app.run(START_RECORDING, json!({}))
&& let Err(e) = app.run(START_RECORDING, record_params.clone())
{
app.status(e);
}
@@ -5870,6 +6140,350 @@ mod tests {
assert_eq!(open_kind(&app), None, "deleting its step closes it");
}
/// The steps of action `action` in set `set`: each command, with its `dx` when it has one.
fn step_names(app: &VectorcraftApp, set: usize, action: usize) -> Vec<String> {
app.ui.action_sets[set].actions[action]
.steps
.iter()
.map(|(c, p)| match p.get("dx") {
Some(dx) => format!("{c} {dx}"),
None => c.clone(),
})
.collect()
}
/// The bottom bar's Stop and Begin Recording buttons, as drawn last.
fn stop_and_record(ctx: &egui::Context, app: &mut VectorcraftApp) -> (egui::Pos2, 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 [stop, record, ..] = bar[..] else { panic!("the bottom bar: {bar:?}") };
(stop.center(), record.center())
}
#[test]
fn begin_recording_selected_action_adds_steps_without_new_action() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let (mut app, _) = app_with_three_moves();
app.ui.action_set = Some(0);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
let (stop, record) = stop_and_record(&ctx, &mut app);
click(&ctx, &mut app, record, show);
app.run("object.move", json!({"dx": 8, "dy": 0})).unwrap();
click(&ctx, &mut app, stop, show);
assert_eq!(app.ui.action_sets[0].actions.len(), 1, "no new action");
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 2", "object.move 4", "object.move 8"], "added at the end");
// With a step selected, the recorded steps go after it.
click_first_step(&ctx, &mut app);
// The steps shown move the bottom bar down.
let (stop, record) = stop_and_record(&ctx, &mut app);
click(&ctx, &mut app, record, show);
app.run("object.move", json!({"dx": 16, "dy": 0})).unwrap();
click(&ctx, &mut app, stop, show);
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 16", "object.move 2", "object.move 4", "object.move 8"]);
// With no action selected, Begin Recording still adds a new action.
let texts = frame_events(&ctx, &mut app, vec![], show);
click(&ctx, &mut app, text_rect(&texts, "▾ Moves").center(), show);
let (stop, record) = stop_and_record(&ctx, &mut app);
click(&ctx, &mut app, record, show);
app.run("object.move", json!({"dx": 32, "dy": 0})).unwrap();
click(&ctx, &mut app, stop, show);
let names: Vec<&str> = app.ui.action_sets[0].actions.iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, ["Three Moves", "Action 2"]);
assert_eq!(step_names(&app, 0, 1), ["object.move 32"]);
}
#[test]
fn begin_recording_after_selected_step_preserves_order_and_exclusions() {
let (mut app, _) = app_with_three_moves();
app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([0, 2]);
for (params, want) in [
(json!({"action": 0}), "no such action"),
(json!({"set": 0, "after": 0}), "no such action"),
(json!({"set": 1, "action": 0}), "no such action"),
(json!({"set": 0, "action": 1}), "no such action"),
(json!({"set": 0, "action": "0"}), "no such action"),
(json!({"set": 0, "action": 0, "after": 3}), "no such action step"),
(json!({"set": 0, "action": 0, "after": null}), "no such action step"),
(json!({"set": 0, "action": 0, "after": -1}), "no such action step"),
] {
assert_eq!(app.run(START_RECORDING, params.clone()).unwrap_err(), want, "{params}");
assert!(app.ui.recording.is_none(), "{params}");
}
assert_eq!(
app.run(START_RECORDING, json!({"set": 0, "action": 0, "after": 0})).unwrap(),
json!({"set": 0, "action": 0, "name": "Three Moves"})
);
app.run("object.move", json!({"dx": 8, "dy": 0})).unwrap();
app.run("object.move", json!({"dx": 16, "dy": 0})).unwrap();
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(app.ui.action_sets[0].actions.len(), 1, "no new action");
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 8", "object.move 16", "object.move 2", "object.move 4"]);
assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([0, 4]), "the steps keep whether they are excluded");
}
#[test]
fn steps_recorded_into_the_middle_of_an_action_keep_the_selected_step_and_dialog_on_their_steps() {
let (mut app, _) = app_with_three_moves();
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
let id = app.ui.action_sets[0].actions[0].id;
app.run(START_RECORDING, json!({"set": 0, "action": 0, "after": 0})).unwrap();
app.run("object.move", json!({"dx": 8, "dy": 0})).unwrap();
app.run("object.move", json!({"dx": 16, "dy": 0})).unwrap();
set_pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP, Some((id, 2)));
crate::dialogs::action_stop::open_record(&mut app, Some((0, 0)), Some(1));
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 8", "object.move 16", "object.move 2", "object.move 4"]);
assert_eq!(pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP), Some((id, 4)), "the move by 4 stays selected");
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["step"], json!(3), "Record Stop stays after the move by 2");
}
#[test]
fn step_edits_while_recording_into_an_action_keep_the_recording_in_place() {
let (mut app, _) = app_with_three_moves();
// The recording goes between the moves by 2 and 4.
app.run(START_RECORDING, json!({"set": 0, "action": 0, "after": 1})).unwrap();
app.run("object.move", json!({"dx": 8, "dy": 0})).unwrap();
app.run(DELETE_STEP, json!({"set": 0, "action": 0, "step": 0})).unwrap();
app.run(INSERT_STOP, json!({"set": 0, "action": 0, "step": 0})).unwrap();
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 2, "before": 0})).unwrap();
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(step_names(&app, 0, 0), ["object.move 4", "object.move 2", STOP_STEP, "object.move 8"]);
let (mut app, _) = app_with_three_moves();
app.run(START_RECORDING, json!({"set": 0, "action": 0, "after": 1})).unwrap();
app.run("object.move", json!({"dx": 8, "dy": 0})).unwrap();
app.run(DELETE_STEP, json!({"set": 0, "action": 0, "step": 0})).unwrap();
app.run(STOP_RECORDING, json!({})).unwrap();
assert_eq!(step_names(&app, 0, 0), ["object.move 2", "object.move 8", "object.move 4"], "a step deleted before the place");
}
#[test]
fn move_step_within_action_updates_order_exclusions_and_selection() {
let (mut app, _) = app_with_three_moves();
app.ui.action_sets[0].actions[0].steps.push(("object.move".into(), json!({"dx": 8, "dy": 0})));
app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([1]);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
let id = app.ui.action_sets[0].actions[0].id;
set_pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP, Some((id, 3)));
let selected_step = |ctx: &egui::Context| pstate::<Option<(u64, usize)>>(ctx, SELECTED_STEP);
assert_eq!(
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 1, "before": 4})).unwrap(),
json!({"set": 0, "action": 0, "step": 3, "moved": true})
);
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 4", "object.move 8", "object.move 2"]);
assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([3]), "the moved step stays excluded");
assert_eq!(selected_step(&ctx), Some((id, 2)), "the selected step stays selected");
assert_eq!(
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 2, "before": 0})).unwrap(),
json!({"set": 0, "action": 0, "step": 0, "moved": true})
);
assert_eq!(step_names(&app, 0, 0), ["object.move 8", "object.move 1", "object.move 4", "object.move 2"]);
assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([3]));
assert_eq!(selected_step(&ctx), Some((id, 0)), "the moved selected step is selected where it went");
for before in [1, 2] {
assert_eq!(
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 1, "before": before})).unwrap(),
json!({"set": 0, "action": 0, "step": 1, "moved": false})
);
}
assert_eq!(step_names(&app, 0, 0), ["object.move 8", "object.move 1", "object.move 4", "object.move 2"], "a drop on itself moves nothing");
crate::dialogs::action_stop::open_record(&mut app, Some((0, 0)), Some(3));
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 3, "before": 0})).unwrap();
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["step"], json!(0), "an open Record Stop follows its step");
}
#[test]
fn move_step_between_actions_and_sets_keeps_file_roundtrip() {
let (mut app, _) = app_with_three_moves();
app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([2]);
let rotate = ("object.rotate".to_string(), json!({"angle": 5}));
app.ui.action_sets.push(ActionSet::new("Other", vec![Action::new("Empty", vec![]), Action::new("One", vec![rotate])]));
app.ui.action_set = Some(0);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
let (id, empty) = (app.ui.action_sets[0].actions[0].id, app.ui.action_sets[1].actions[0].id);
set_pstate::<Option<(usize, usize)>>(&ctx, "actions-selected", Some((0, 0)));
set_pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP, Some((id, 2)));
assert_eq!(
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 2, "toSet": 1, "toAction": 0, "before": 0})).unwrap(),
json!({"set": 1, "action": 0, "step": 0, "moved": true})
);
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 2"]);
assert!(app.ui.action_sets[0].actions[0].excluded_steps.is_empty());
assert_eq!(step_names(&app, 1, 0), ["object.move 4"], "an empty action takes a step");
assert_eq!(app.ui.action_sets[1].actions[0].excluded_steps, BTreeSet::from([0]), "the moved step stays excluded");
assert_eq!(pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP), Some((empty, 0)));
assert_eq!(pstate::<Option<(usize, usize)>>(&ctx, "actions-selected"), Some((1, 0)));
assert_eq!((app.ui.action_set, app.ui.action_sets_selected.as_slice()), (Some(1), &[1][..]));
assert!(pstate::<BTreeSet<u64>>(&ctx, "actions-expanded").contains(&empty), "the step's new action shows its steps");
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 0, "toSet": 1, "toAction": 1, "before": 0})).unwrap();
assert_eq!(step_names(&app, 1, 1), ["object.move 1", "object.rotate"]);
assert_eq!(pstate::<Option<(u64, usize)>>(&ctx, SELECTED_STEP), Some((empty, 0)), "an unmoved selected step stays");
let loaded = files::read_set(&files::write_set(&app.ui.action_sets[1]), "Other").unwrap();
for (a, b) in loaded.actions.iter().zip(&app.ui.action_sets[1].actions) {
assert_eq!((&a.name, &a.steps, &a.excluded_steps), (&b.name, &b.steps, &b.excluded_steps));
}
}
#[test]
fn move_step_rejects_invalid_and_oversize_destinations_without_changes() {
let (mut app, _) = app_with_three_moves();
let full = Action::new("Full", vec![("object.move".to_string(), json!({"dx": 0})); files::MAX_ACTION_STEPS]);
let big = |text: String| ActionSet::new("Big", vec![Action::new("Big", vec![("document.note".to_string(), json!({ "s": text }))])]);
let room = files::MAX_SET_FILE - files::write_set(&big(String::new())).len() - 4;
app.ui.action_sets.push(ActionSet::new("Full", vec![full]));
app.ui.action_sets.push(big("x".repeat(room)));
assert!(files::write_set(&app.ui.action_sets[2]).len() <= files::MAX_SET_FILE);
let before: Vec<String> = app.ui.action_sets.iter().map(files::write_set).collect();
for (params, want) in [
(json!({}), "no such action"),
(json!({"set": 0, "action": 1, "step": 0, "before": 0}), "no such action"),
(json!({"set": 3, "action": 0, "step": 0, "before": 0}), "no such action"),
(json!({"set": 0, "action": 0, "step": 3, "before": 0}), "no such action step"),
(json!({"set": 0, "action": 0, "before": 0}), "no such action step"),
(json!({"set": 0, "action": 0, "step": 0}), "no such action step"),
(json!({"set": 0, "action": 0, "step": 0, "before": 4}), "no such action step"),
(json!({"set": 0, "action": 0, "step": 0, "before": "1"}), "no such action step"),
(json!({"set": 0, "action": 0, "step": 0, "toSet": 3, "before": 0}), "no such action set"),
(json!({"set": 0, "action": 0, "step": 0, "toSet": null, "before": 0}), "no such action set"),
(json!({"set": 0, "action": 0, "step": 0, "toSet": 1, "toAction": 1, "before": 0}), "no such action"),
(json!({"set": 0, "action": 0, "step": 0, "toSet": 1, "toAction": 0, "before": 0}), "the action already has 10,000 steps"),
(json!({"set": 0, "action": 0, "step": 0, "toSet": 2, "toAction": 0, "before": 1}), "the action set would be larger than 8 MiB"),
] {
assert_eq!(app.run(MOVE_STEP, params.clone()).unwrap_err(), want, "{params}");
assert_eq!(app.ui.action_sets.iter().map(files::write_set).collect::<Vec<_>>(), before, "{params}");
}
}
#[test]
fn drag_step_shows_insertion_line_and_moves_once() {
use egui::{Event, PointerButton};
let (mut app, _) = app_with_three_moves();
app.ui.action_set = Some(0);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
set_pstate(&ctx, "actions-expanded", BTreeSet::from([app.ui.action_sets[0].actions[0].id]));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], show);
let mut rows: Vec<egui::Rect> = texts.iter().filter(|(s, _)| s == "object.move").map(|(_, r)| *r).collect();
rows.sort_by(|a, b| a.top().total_cmp(&b.top()));
let (from, to) = (rows[0].center(), egui::pos2(rows[0].center().x, rows[2].center().y + 4.0));
let accent = Tokens::get(&ctx).accent;
// Whether a frame with `events` draws the insertion line.
let frame = |app: &mut VectorcraftApp, events: Vec<Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui));
out.textures_delta.clear();
fn line(s: &egui::Shape, accent: egui::Color32) -> bool {
match s {
egui::Shape::LineSegment { stroke, .. } => stroke.width == 2.0 && stroke.color == accent,
egui::Shape::Vec(v) => v.iter().any(|s| line(s, accent)),
_ => false,
}
}
out.shapes.iter().any(|c| line(&c.shape, accent))
};
let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() };
assert!(!frame(&mut app, vec![Event::PointerMoved(from)]), "no line before a drag");
frame(&mut app, vec![button(from, true)]);
frame(&mut app, vec![Event::PointerMoved(from + egui::vec2(0.0, 12.0))]);
assert!(frame(&mut app, vec![Event::PointerMoved(to)]), "the line shows where the step goes");
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 2", "object.move 4"], "nothing moves while dragging");
frame(&mut app, vec![button(to, false)]);
assert_eq!(step_names(&app, 0, 0), ["object.move 2", "object.move 4", "object.move 1"], "the release moves the step");
for _ in 0..3 {
assert!(!frame(&mut app, vec![]), "no line after the drop");
}
assert_eq!(step_names(&app, 0, 0), ["object.move 2", "object.move 4", "object.move 1"], "the step moves once");
}
#[test]
fn drag_step_released_off_the_rows_leaves_it_in_place_without_an_undo_step() {
use egui::{Event, PointerButton};
let (mut app, _) = app_with_three_moves();
app.ui.action_set = Some(0);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
set_pstate(&ctx, "actions-expanded", BTreeSet::from([app.ui.action_sets[0].actions[0].id]));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], show);
let mut rows: Vec<egui::Rect> = texts.iter().filter(|(s, _)| s == "object.move").map(|(_, r)| *r).collect();
rows.sort_by(|a, b| a.top().total_cmp(&b.top()));
let from = rows[0].center();
let over = egui::pos2(from.x, rows[2].center().y + 4.0);
let off = egui::pos2(from.x, rows[2].bottom() + 2_000.0);
let undo = undo_steps(&app);
let frame = |app: &mut VectorcraftApp, events: Vec<Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui));
out.textures_delta.clear();
};
let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, vec![button(from, true)]);
frame(&mut app, vec![Event::PointerMoved(from + egui::vec2(0.0, 12.0))]);
frame(&mut app, vec![Event::PointerMoved(over)]);
frame(&mut app, vec![Event::PointerMoved(off)]);
frame(&mut app, vec![button(off, false)]);
for _ in 0..3 {
frame(&mut app, vec![]);
}
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 2", "object.move 4"], "the step stays where it was");
assert_eq!(undo_steps(&app), undo, "no undo step");
assert_eq!(pstate::<Option<DraggedStep>>(&ctx, DRAGGED_STEP), None, "the drag ends");
}
#[test]
fn drag_step_ends_without_a_move_when_a_command_changes_its_action() {
use egui::{Event, PointerButton};
let (mut app, _) = app_with_three_moves();
let rotate = |angle: u32| ("object.rotate".to_string(), json!({"angle": angle}));
app.ui.action_sets[0].actions.push(Action::new("Turns", vec![rotate(5), rotate(6)]));
app.ui.action_set = Some(0);
let ctx = egui::Context::default();
app.ui_ctx = Some(ctx.clone());
set_pstate(&ctx, "actions-expanded", BTreeSet::from([app.ui.action_sets[0].actions[0].id]));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], show);
let mut rows: Vec<egui::Rect> = texts.iter().filter(|(s, _)| s == "object.move").map(|(_, r)| *r).collect();
rows.sort_by(|a, b| a.top().total_cmp(&b.top()));
let (from, to) = (rows[0].center(), egui::pos2(rows[0].center().x, rows[1].center().y + 4.0));
let frame = |app: &mut VectorcraftApp, events: Vec<Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui));
out.textures_delta.clear();
};
let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() };
let start_drag = |app: &mut VectorcraftApp| {
frame(app, vec![button(from, true)]);
frame(app, vec![Event::PointerMoved(from + egui::vec2(0.0, 12.0))]);
};
// A command on another action leaves the drag as it is.
start_drag(&mut app);
app.run(DELETE_STEP, json!({"set": 0, "action": 1, "step": 0})).unwrap();
frame(&mut app, vec![Event::PointerMoved(to)]);
frame(&mut app, vec![button(to, false)]);
assert_eq!(step_names(&app, 0, 0), ["object.move 2", "object.move 1", "object.move 4"], "the dragged step moves");
// A command that deletes the dragged step's action's first step puts another step at the
// dragged index.
start_drag(&mut app);
app.run(DELETE_STEP, json!({"set": 0, "action": 0, "step": 0})).unwrap();
frame(&mut app, vec![Event::PointerMoved(to)]);
assert_eq!(pstate::<Option<DraggedStep>>(&ctx, DRAGGED_STEP), None, "the drag ends");
frame(&mut app, vec![button(to, false)]);
assert_eq!(step_names(&app, 0, 0), ["object.move 1", "object.move 4"], "no other step moves");
// Two equal steps, one excluded, that change places put the other one at the dragged index.
let same = ("object.move".to_string(), json!({"dx": 1, "dy": 0}));
app.ui.action_sets[0].actions[0].steps = vec![same.clone(), same];
app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([1]);
start_drag(&mut app);
app.run(MOVE_STEP, json!({"set": 0, "action": 0, "step": 1, "before": 0})).unwrap();
frame(&mut app, vec![Event::PointerMoved(to)]);
frame(&mut app, vec![button(to, false)]);
assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([0]), "the excluded step stays first");
}
#[test]
fn step_and_set_edits_keep_an_open_insert_menu_item_on_its_place() {
let (mut app, _) = app_with_three_moves();
+9 -3
View File
@@ -621,11 +621,16 @@ pub struct UiState {
/// Recording in progress: (set index, action name, journal length when recording started).
#[serde(skip)]
pub recording: Option<(usize, String, usize)>,
/// The id of the action New Action made for the recording in progress
/// ([`crate::panels::actions::Action::id`]): Stop gives it the recorded steps. None when Stop
/// adds a new action, as after Begin Recording.
/// The id of the action the recording in progress records into, the one New Action made or the
/// one selected when Begin Recording started it ([`crate::panels::actions::Action::id`]): Stop
/// gives it the recorded steps. None when Stop adds a new action, as after Begin Recording with
/// no action selected.
#[serde(skip)]
pub recording_into: Option<u64>,
/// Where in the action [`UiState::recording_into`] names Stop puts the recorded steps: before
/// the step at this index, or None after its last step.
#[serde(skip)]
pub recording_at: Option<usize>,
/// The length of the action set file that Stop Playing/Recording would leave without the steps
/// recorded so far, once measured; None until then, and again after an app command, which may
/// change the sets.
@@ -1168,6 +1173,7 @@ impl Default for UiState {
action_sets_selected: vec![],
recording: None,
recording_into: None,
recording_at: None,
recording_base: None,
recording_tail: None,
recording_checked: None,
+25 -6
View File
@@ -3196,10 +3196,15 @@ already walks Image Trace. Short agent entry points:
would give the set more than 1,000 actions (`the action set file holds more than 1,000 actions`) or a file larger
than 8 MiB (`the action set would be larger than 8 MiB`), and then no recording starts. The panel's Create
New Action button and menu's New Action… open the `actionNew` dialog, whose Record runs this.
- Actions: `ui.action.record {set?}` starts recording a new action in the chosen set, or the current set when `set`
is left out, and returns `{set, name}`. With no sets, it creates Set 1. The action gets the next available
`Action N` name in that set. The Actions panel's Begin Recording button runs this command. An invalid set or an
existing recording is an error and leaves the sets and recording unchanged, and so is a set where the new action
- Actions: `ui.action.record {set?, action?, after?}` starts recording a new action in the chosen set, or the current
set when `set` is left out, and returns `{set, name}`. With no sets, it creates Set 1. The action gets the next
available `Action N` name in that set. With `action`, it records into the action at `action` in set `set` instead
and returns `{set, action, name}`: `ui.action.stop` puts the recorded steps after step `after` of that action, or
after its last step when `after` is left out, and the steps after them keep whether they are excluded. The Actions
panel's Begin Recording button runs this command with the selected action and its selected step, and with neither
when no action is selected. `action` without `set`, `after` without `action`, an index past the last set, action or
step, an invalid set or an existing recording is an error and leaves the sets and recording unchanged, and so is a
set where the new action
would give it more than 1,000 actions or an action set file larger than 8 MiB. While a recording (this one or
`ui.action.new`'s) is in progress, a recorded command whose step would give the action more than 10,000 steps, or
its set's action set file more than 8 MiB, is an error with the reason (`the action already has 10,000 steps`, `the
@@ -3325,6 +3330,20 @@ already walks Image Trace. Short agent entry points:
they are excluded, and no step of that action stays selected. A Record Stop 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 restore a deleted step. The panel's Delete Selection runs this for the selected step.
- Actions: `ui.action.moveStep {set, action, step, toSet?, toAction?, before}` moves one step, a stop or a command, of
the action at `action` in set `set` into the action at `toAction` in set `toSet` (by default the same set and
action), before the step at index `before` there, or after its last step when `before` is its number of steps.
`before` counts the steps as they are before the move. It returns `{set, action, step, moved}` with the step's new
place. The step keeps its params and whether it is excluded, and the other steps keep theirs. A selected step stays
selected; a moved selected step makes its new action the selected one and its set the current one. A Record Stop
dialog open on a step follows the step, and the steps that the recording in progress adds to either action stay
after the steps that were before them. A step put back just before or just after itself does not move, and
`moved` is `false`. A missing or invalid index, or a move that would give the action more than 10,000 steps or its
set's action set file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an
error, and then nothing changes. Undo does not move the step back. In the Actions panel, dragging a step row moves
the step: a line shows where it goes, before or after the row under the pointer, or first in an action whose name
row it is over (after the last step when the action's steps are hidden), and releasing the button off those rows
leaves the step where it was.
- Actions: `ui.action.playbackOptions {speed?, seconds?}` sets how the Actions panel plays actions and returns
`{speed, seconds}`. `speed` is `accelerated` (the default: every step at once), `step` (one step at a time, with the
window drawn after each step before the next runs) or `pause` (a pause of `seconds` between steps). `seconds` is a
@@ -3333,8 +3352,8 @@ already walks Image Trace. Short agent entry points:
preferences. The panel menu's Playback Options… opens the `actionPlayback` dialog (`speed`, `seconds`), whose OK runs
this. Batch… (`action.batch`) plays every step at once whatever the setting.
- Actions: `ui.action.stop {}` ends the recording in progress and keeps its steps, in the action `ui.action.new` made
for it, else in a new action after the last action of its set, and returns `{stopped}`. It also ends an action
playing at Step by Step or Pause For, or one waiting at a stop, whose message box closes: the steps already played
for it or the action `ui.action.record` named, at the place it named, else in a new action after the last action of
its set, and returns `{stopped}`. It also ends an action playing at Step by Step or Pause For, or one waiting at a stop, whose message box closes: the steps already played
stay, and the rest do not play. With nothing
recording or playing it changes nothing. The panel's Stop Playing/Recording button runs this.
- Action sets: `ui.actionSet.delete {sets?: [index], ids?: [id]}` deletes the Actions panel's sets at these indexes
+1
View File
@@ -750,6 +750,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Actions panel menu: Load Actions… also reads action sets saved as `.aia` files ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `actionSet.read`, `ui.actionSet.load`). VectorCraft's own rules: the set comes in with all its actions. A step with a label and no recorded values comes in as the one menu command with the step's label, when that command is directly in a top menu, is what its menu bar item runs with no values and opens no dialog. A step that is off in the file stays excluded from playback. The other steps, recorded menu items included, are left out, and the status bar says how many.
- 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): the menu item opens a dialog that renames the action or gives it a function key and color; `ui.action.options`). It also sets the action's function key and color.
- Actions panel: Create New Action (bottom bar) and the panel menu's New Action… ask for the new action's name, set, function key and color, and Record adds the action and starts recording into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html): the New Action dialog takes the action's name and the set it goes in; `ui.action.new`, `ui.action.play`). The function key (F1 to F15, with Ctrl or Command and Shift) plays the action. An action's function key plays the action even when a command has the same shortcut, and the command keeps it. VectorCraft's own rules, which the page does not cover: when several actions have the same key the first in panel order plays; a copied action has no key; New Action does not open while an action is recording. The color is kept with the action for Button Mode, which VectorCraft does not have yet.
- Actions panel: with an action selected, Begin Recording adds the commands recorded to that action, after its selected step or at its end, and a step dragged within its action or to another action goes where a highlighted line shows when the button is released ([the help page](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/add-commands-to-actions.html); `ui.action.record`, `ui.action.moveStep`). VectorCraft's own rules, which the page does not cover: with no action selected Begin Recording adds a new action; a step can go into an empty action or an action in another set; a release off the actions' rows leaves the step where it was; a moved step keeps whether it is excluded; Undo does not move it back.
- 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: expand an action to show its recorded steps, then clear a step's checkmark to keep it in the action but skip it during playback. The action and set checkmarks include or exclude all their steps; a mixed checkmark includes all. Alt/Option-clicking a step checkmark keeps that step as it was and applies the opposite inclusion state to the other steps in its action. This follows the public [Exclude commands from an action](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html) guide; the scope of the mixed-state and Alt-click behavior is VectorCraft's design. The exclusion survives UI restart, action duplication, and `.vcactions` save/load. Existing version 1 action set files load with every step included.