Add: undo/redo for Mudlet editor#8469
Conversation
…nt-undo-redo-011CUUUB9JJCHvtGPZbHFrXK
When a trigger was undone/redone, the pattern UI fields would retain stale data if the trigger item couldn't be selected. This occurred in two scenarios: 1. Item was deleted and no longer exists in the tree 2. affectedItemIDs list was empty The fix ensures all 50 pattern fields and related UI elements (name, ID, command, script) are cleared when slot_itemsChanged() cannot find or select an item after undo/redo operations. Added comprehensive tests (Category 11) to verify: - Patterns are properly loaded when trigger is selected - Patterns are cleared when item not found after delete - Name/ID fields are cleared along with patterns - Patterns are cleared when affectedItemIDs is empty Location: dlgTriggerEditor.cpp:13084-13126
The undo/redo button tooltips now clearly indicate what action will be
undone or redone by prefixing the command text with "Undo: " or "Redo: ".
Before: "Activate trigger \"foo\""
After: "Undo: Activate trigger \"foo\"" or "Redo: Activate trigger \"foo\""
The prefixes are added using tr("Undo: %1").arg(text) and
tr("Redo: %1").arg(text) to keep the strings together for translators,
rather than concatenating separate strings.
Added translator comments explaining what %1 contains to help with
context during translation.
Location: dlgTriggerEditor.cpp:445-466
All undo/redo command text now uses lowercase action verbs to read naturally when prefixed with "Undo: " or "Redo: ". Before: "Undo: Activate trigger \"foo\"" After: "Undo: activate trigger \"foo\"" This improves readability as the action verbs are no longer starting a sentence but are part of a larger phrase. Changes apply to all command types: - activate/deactivate → now lowercase - add → now lowercase - delete → now lowercase - modify → now lowercase - move → now lowercase All affected files: - MudletToggleActiveCommand.cpp - MudletAddItemCommand.cpp - MudletDeleteItemCommand.cpp - MudletModifyPropertyCommand.cpp - MudletMoveItemCommand.cpp
All macro texts used for bulk operations (moves, adds) are now lowercase to match the lowercase action verbs used in individual commands. Changes: - "Move items" → "move items" (bulk move operation) - "Add %1" → "add %1" (adding new items, e.g., "add New trigger") When prefixed with "Undo: " or "Redo: ", these now read naturally: - "Undo: move items" - "Undo: add New trigger" This maintains consistency with individual command texts like: - "Undo: move trigger \"foo\"" - "Undo: add trigger \"bar\"" Added translator comments to the "add %1" macros to explain what %1 represents for better translation context.
Updates the undo/redo button tooltips to display the keyboard shortcuts (Ctrl+Z for undo, Ctrl+Y for redo) dynamically using the platform-native format. Tooltips now show the shortcut in both states: - With action: "Undo: <action> (Ctrl+Z)" - Without action: "Undo (Ctrl+Z)" The shortcuts are retrieved dynamically to ensure correct platform-specific representation (e.g., Cmd+Z on macOS).
Implements Option A for dual undo system management: After saving an item, the edbee text editor's undo stack is cleared, making Save a clear "commit point" in the editing workflow. This provides the best of both worlds: - Users can undo granular text edits while working on code - Once saved, the changes become part of the item history - Prevents overlap/confusion between text and item undo stacks - Clarifies that undo after save goes through item undo, not text undo Also removes spammy debug line from slot_updateUndoRedoButtonStates that was being called on every undo state change. Applied to all save functions: saveTrigger, saveTimer, saveAlias, saveScript, saveAction, and saveKey.
…JCHvtGPZbHFrXK Merge latest development changes into the undo/redo feature branch. Resolved merge conflicts by: - Combining includes from both branches (added QVBoxLayout and post_guard.h) - Keeping undo/redo functions from feature branch alongside pattern widget functions from development - Using cleaner banner settings functions from development (bannerSettingsKey/legacyBannerSettingsKey)
Enable AddressSanitizer, UndefinedBehaviorSanitizer, and LeakSanitizer for all pull request builds to catch memory errors and undefined behavior before merging. Production builds (pushes, tags, releases) remain without sanitizers for optimal performance.
Use environment variable to avoid shell interpretation of semicolons in the sanitizer list when passing to CMake.
CMake accepts both space and semicolon-separated lists. Using spaces avoids shell interpretation issues in GitHub Actions.
Add ASAN_OPTIONS=detect_leaks=0 to all test steps to disable leak detection while keeping AddressSanitizer and UndefinedBehaviorSanitizer active for catching memory errors and undefined behavior.
Replace hardcoded loop limit of 50 with mTriggerPatternEdit.size() to prevent reading past the end of the QList when clearing pattern editors. The bug occurred when undoing trigger addition - the code assumed 50 pattern editors existed, but the list was smaller, causing AddressSanitizer to detect a heap-buffer-overflow. Fixes two occurrences of this bug in slot_itemsChanged().
- Move closing parentheses to previous lines (whitespace/parens) - Add namespace termination comment for EditorViewTypes Addresses 26 CodeFactor warnings about closing parenthesis placement and namespace documentation.
Previously, pressing undo once after deleting multiple item groups (triggers, aliases, scripts) would undo all of them instead of just the most recent delete. This was caused by wasLastCommandValid() using stale index values to determine which command was executed. The indexChanged signal fired before wasLastCommandValid() was called, updating mPreviousIndex and making the index comparison logic incorrect. This caused slot_smartUndo() to think each command was "invalid" and continue undoing until it found a "valid" one. Fix by explicitly tracking the operation type (Undo/Redo/None) in undo() and redo() methods, then using this to determine which command to validate in wasLastCommandValid() instead of relying on index comparison. Now each undo operation correctly processes exactly one command.
Two critical fixes for undo/redo with hierarchical items:
1. Fix multi-level parent-child restoration
- Pass newID instead of oldID to updateChildIDs() after restoration
- Previously searched for children using old parent ID after it was
already updated to new ID, causing children to not be found
- Affected all item types with > 1 level nesting (aliases, triggers, etc)
2. Fix heap-use-after-free crash in redo()
- When redoing deletion of parent+child items, parent's destructor
automatically deletes children, then we tried to delete them again
- Now skip deleting items whose parent is also being deleted
- Prevents use-after-free when unregistering already-deleted children
Both fixes apply to all 6 item types: Triggers, Aliases, Timers,
Scripts, Keys, and Actions.
…rarchies When undoing deletion of nested items (e.g., parent folder with children), items were being restored out of order, causing children to appear at root level instead of under their parent. This happened because: 1. Children were restored before parents (wrong order) 2. When parent's ID changed during restoration, grandchildren's parentID references weren't updated, so they couldn't find their parent 3. Modified state was used for skipRestore check instead of original state Changes: - Implement topological sort to ensure parents are always restored before children, regardless of nesting depth - Update grandchildren's parentID references in mDeletedItems when a child's ID changes, so the recursive updateChildIDs() can find them - Track original parentIDs separately from sortedItems (which gets modified during iteration) for correct skipRestore determination Result: Nested item deletion/undo now correctly preserves the entire hierarchy with minimal ID remapping (only root items whose IDs are taken). Affects all 6 item types: Triggers, Aliases, Timers, Scripts, Keys, Actions Fixes parent-child restoration for arbitrary nesting levels
Tests that when a grandparent folder with nested children (grandparent→ parent→5 children) is deleted and undone, the entire hierarchy is correctly restored with all children nested under their parent (not appearing at root). This validates the fix for the issue where children would incorrectly appear at root level after undo because: 1. Items were restored in wrong order (children before parents) 2. Grandchildren's parentID references weren't updated when children got new IDs 3. skipRestore check used modified state instead of original state The test creates a 3-level hierarchy, deletes the root, undos, and verifies all children are properly nested (not at root level). Adds 6 tests (one per item type) to Category 9 (State Consistency)
- Add dlgTriggerEditorUndoRedoTest.cpp to mudlet.pro SOURCES to fix Windows linker error for runUndoRedoTestSuite() - Fix 26 CodeFactor warnings by moving closing parentheses to end of parameter line instead of standalone line
|
Interesting way to tackle the testing issue. Not sure why these are called "visual tests", maybe because they produce visual results in terminal? Also it seems like, these tests only check if a trigger is added to the visual tree, but do not (yet) check if a trigger is added to triggerUnit internally. Would be a great addition. |
|
#8272 chose that terminology but I am happy to change it |
|
/create links |
- Add decompression to all update*FromXML functions (Trigger, Alias, Timer, Script, Key, Action) - Add EditorItemXMLHelpers.h include to dlgTriggerEditor - Update trigger save to use exportTriggerToXML (compressed) instead of manual XML creation - Remaining item types (Alias, Timer, Script, Key, Action) will be updated in next commit
Update all 6 item types (Trigger, Alias, Timer, Script, Key, Action) to use compressed XML via export*ToXML functions instead of manual XML creation. Reduces memory usage for undo/redo snapshots.
- Add lifetime warning docs for getLastExecutedCommand() pointer safety - Fix mIsDropAction flag not resetting: add dragLeaveEvent() and reset after dropEvent() Prevents incorrect behavior when drag is cancelled
Each modification should be individually undoable. Add+Modify merging still works via EditorAddItemCommand. Added debug output to test for better diagnostics.
|
Seems like closing the script editor and reopening disables undo/redo entirely. Screencast_20251124_074953.webmAlso ran into another edge case which I can't reproduce, but it kind of got stuck, could be related. Screencast_20251124_073822.webm |
Only clear undo stack when profile closes, not when editor window is closed
|
@ZookaOnGit I was previously clearing the undo stack on dialog close, but that was only meant for profile close. Thanks, fixed! |
|
Yeah, that's much better. So close! But on doing one last run through it looks like we've lost the ability to undo each toggleable/settable change. e.g., the right side of the triggers. It just undoes the trigger as a whole. Same with aliases, script events, etc.. |
Each widget change now immediately saves to model and creates an undo entry. Rapid consecutive edits to the same property merge within 500ms.
|
Good one... Screencast.from.2025-11-25.08-28-24.webm |
|
Alias and keybinding per-property saves aren't working, and User Event in Scripts. |
|
I couldn't replicate the issue with aliases, what was wrong there? For keybindings, all properties were being saved (once you leave them) except for the keybinding itself - made that get pushed on the undo stack as well. Buttons are being odd - will fix, thanks. |
|
Fixed stylesheet and the number of columns in buttons not getting the undo. |
|
Add a new alias, undo/redo works fine. Change a property (Pattern in this video). Save. Undo/redo, the property is not restored, but also I would assume the property would get undone first, then the alias would be removed on the second undo. Screencast_20251127_092800.webm |
|
Dang, just crashed it; deleted 24 button toolbars, then undo, then redo. Screencast_20251127_093815.webm |
Merging modify commands into add commands created confusing UX where undoing a newly added item would undo all property changes at once instead of one at a time.
I tried to do something clever there and combine adding and modifying alias and then undoing it into one action, but that didn't work. Removed that. Should be good now. |
Move mpHost nullification from first loop (validation) to second loop (deletion) to prevent other items from having null mpHost when updateToolbar() is called during individual item deletion.
|
Fixed the crash on deleting buttons. |
|
@vadi2 Are you planning to fix the issue completely ? so I can stop working on this and focus on other issues |
|
I am, and while normally any other bounty solution is welcome, you have plagiarized in the past from other contributors' PRs. |
ZookaOnGit
left a comment
There was a problem hiding this comment.
All above concerns look fixed. Can't crash and general daily usage seems solid. Without any other testers putting eyes on it may as well get it out to PTB for broader testing. Great work on a long requested feature!
|
Good stuff. |
Brief overview of PR changes/additions
Adds undo/redo for Mudlet editor - allows you to undo adding/changing/deleting triggers, aliases and so on. This PR also adds visual tests that can be run inside the
Mudlet self-testprofile, and they are hooked up to run in the CI pipeline automatically. The deletion confirmation dialog has been itself deleted since we can now undo.Motivation for adding to Mudlet
Long-awaited and often requested feature. Fixes #707 and addresses #8272.
Other info (issues closed, discussion etc)
This test also adds 300+ tests that can be run in the Mudlet self-test profile (launch it from the CLI using
--profile "Mudlet self-test"):Screencast.from.2025-11-05.12-59-16.mp4
/claim #707