Fix Alt-drag duplicate cleanup on right-click abort#3782
Fix Alt-drag duplicate cleanup on right-click abort#3782jsjgdh wants to merge 6 commits intoGraphiteEditor:masterfrom
Conversation
|
/gemini review |
There was a problem hiding this comment.
Pull request overview
This PR aims to ensure Alt-drag node duplication is cleaned up correctly when the drag is aborted (notably via right-click), by tracking whether duplication occurred during the drag and adjusting transaction handling.
Changes:
- Introduces a
duplicated_in_dragflag to track Alt-drag duplication state across drag lifecycle. - Splits duplication into a regular path vs. an Alt-drag path (
DuplicateSelectedNodesForDrag) via a shared helper implementation. - Adjusts abort/commit behavior on right-click/Escape/PointerUp to try to undo or finalize Alt-drag duplication.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs | Adds duplicated_in_drag, introduces DuplicateSelectedNodesForDrag, and changes duplication + drag abort/commit behavior |
| editor/src/messages/portfolio/document/node_graph/node_graph_message.rs | Adds the DuplicateSelectedNodesForDrag message variant |
| editor/src/messages/portfolio/document/document_message_handler.rs | Changes Escape handling to attempt undoing Alt-drag duplication on abort |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs
Show resolved
Hide resolved
| if add_transaction { | ||
| responses.add(DocumentMessage::AddTransaction); | ||
| } else { | ||
| responses.add(DocumentMessage::StartTransaction); | ||
| } | ||
| responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); | ||
| responses.add(NodeGraphMessage::SelectedNodesSet { | ||
| nodes: new_ids.values().cloned().collect(), | ||
| }); | ||
| self.duplicated_in_drag = !add_transaction; |
There was a problem hiding this comment.
DuplicateSelectedNodesForDrag calls DocumentMessage::StartTransaction inside duplicate_selected_nodes_impl, but a drag already starts a transaction in PointerDown (see the StartTransaction queued at the end of the node click handling). Starting a second transaction mid-drag overwrites transaction_status and pushes an extra undo snapshot, which makes abort/undo semantics incorrect (it can require multiple undos and may preserve earlier drag movement). For the drag path, avoid starting/committing a separate transaction—just duplicate nodes within the existing drag transaction, and let the PointerUp EndTransaction commit/cancel the whole drag+duplicate as one unit.
| if add_transaction { | |
| responses.add(DocumentMessage::AddTransaction); | |
| } else { | |
| responses.add(DocumentMessage::StartTransaction); | |
| } | |
| responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); | |
| responses.add(NodeGraphMessage::SelectedNodesSet { | |
| nodes: new_ids.values().cloned().collect(), | |
| }); | |
| self.duplicated_in_drag = !add_transaction; | |
| // Only start or add a transaction when not already in a drag transaction. | |
| if self.drag_start.is_none() { | |
| if add_transaction { | |
| responses.add(DocumentMessage::AddTransaction); | |
| } else { | |
| responses.add(DocumentMessage::StartTransaction); | |
| } | |
| } | |
| responses.add(NodeGraphMessage::AddNodes { nodes, new_ids: new_ids.clone() }); | |
| responses.add(NodeGraphMessage::SelectedNodesSet { | |
| nodes: new_ids.values().cloned().collect(), | |
| }); | |
| // Track that duplication occurred during a drag so the drag transaction can handle undo/abort. | |
| self.duplicated_in_drag = self.drag_start.is_some(); |
There was a problem hiding this comment.
Same here, you can judge whether or not this makes any sense.
| responses.add(NodeGraphMessage::SendGraph); | ||
|
|
||
| if self.duplicated_in_drag { | ||
| responses.add(DocumentMessage::CommitTransaction); |
There was a problem hiding this comment.
This CommitTransaction on PointerUp (guarded by duplicated_in_drag) finishes the transaction before the unconditional EndTransaction at the end of PointerUp. That can prevent later drag mutations from being captured (since transaction_modified won’t transition from Finished) and makes the drag transaction lifecycle inconsistent. If duplication is meant to be part of the drag transaction, remove this explicit commit and rely on the existing EndTransaction at the end of PointerUp.
| responses.add(DocumentMessage::CommitTransaction); |
There was a problem hiding this comment.
Does this sound sensible or dumb? I'm not familiar enough with the code to tell so I'll leave that to you.
editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs
Show resolved
Hide resolved
| responses.add(DocumentMessage::AbortTransaction); | ||
| if self.node_graph_handler.duplicated_in_drag { | ||
| responses.add(DocumentMessage::AbortTransaction); | ||
| responses.add(DocumentMessage::Undo); |
There was a problem hiding this comment.
Escape handling aborts the transaction and then calls Undo when duplicated_in_drag is set. AbortTransaction already performs an undo when the transaction is modified (see DocumentMessage::AbortTransaction -> RepeatedAbortTransaction), so the extra Undo will roll back one additional history step and can undo unrelated user actions. Instead, ensure Alt-drag duplication occurs within the existing drag transaction so a single AbortTransaction reverts both move and duplication, and remove the extra Undo here.
| responses.add(DocumentMessage::Undo); |
There was a problem hiding this comment.
Same here, please check if this makes sense or not.
editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs
Show resolved
Hide resolved
| Cut, | ||
| DeleteSelectedNodes, | ||
| DuplicateSelectedNodes, | ||
| DuplicateSelectedNodesForDrag, |
There was a problem hiding this comment.
DuplicateSelectedNodesForDrag appears to be an internal helper message triggered by the Alt-drag logic, but it’s added to actions_additional_if_node_graph_is_open(). Since this action list is surfaced for input/action availability, exposing an internal-only message can allow it to be triggered in unsupported contexts (e.g., via keybinding) and complicate transaction semantics. Consider removing it from the public action list and keeping the duplication-for-drag behavior internal to the drag handler.
| DuplicateSelectedNodesForDrag, |
There was a problem hiding this comment.
Code Review
This pull request addresses an issue with cleaning up duplicated nodes when an Alt-drag operation is aborted. The changes introduce a duplicated_in_drag flag to track when a duplication has occurred during a drag, and uses this flag to ensure proper cleanup via transaction abortion and undos. The logic has been updated for both Escape key and right-click aborts. A good refactoring was done to extract the node duplication logic into a shared duplicate_selected_nodes_impl function.
I have one suggestion to improve code clarity by removing a redundant if/else block. Overall, the changes look good and correctly solve the issue.
| if self.duplicated_in_drag { | ||
| responses.add(DocumentMessage::AbortTransaction); | ||
| self.duplicated_in_drag = false; | ||
| } else { | ||
| responses.add(DocumentMessage::AbortTransaction); | ||
| } |
There was a problem hiding this comment.
This if/else block is redundant as responses.add(DocumentMessage::AbortTransaction) is called in both branches. This can be simplified by moving the common call out and unconditionally setting self.duplicated_in_drag to false, as it's a harmless no-op if it's already false.
responses.add(DocumentMessage::AbortTransaction);
self.duplicated_in_drag = false;| DuplicateSelectedNodes, | ||
| DuplicateSelectedNodesForDrag, |
There was a problem hiding this comment.
Neither of these are listed in input_mappings.rs so adding them here is wrong, unless the intention actually was to have them in input_mappings.rs.
discussion : https://discord.com/channels/@me/1461994742291234997/1473407462097948765 from DM with @Keavon