-
Notifications
You must be signed in to change notification settings - Fork 2
Add undo redo #232
Description
Yes, you can implement redo and undo functionality in your Figma plugin programmatically using the figma.commitUndo() method. This method allows you to control how your plugin actions are grouped in Figma's undo history.
How Undo/Redo Works in Figma Plugins
By default, all actions performed by a plugin are grouped together as a single undo step[4]. When a user triggers an undo after your plugin runs, it will undo all plugin actions at once. However, you can create more granular undo steps by using figma.commitUndo().
The figma.commitUndo() method commits the previous actions to the undo history, creating a checkpoint[4]. Any actions performed after calling this method will be part of a new undo step.
For example:
// This rectangle will be in the first undo step
figma.createRectangle();
// Create a checkpoint in the undo history
figma.commitUndo();
// This ellipse will be in a separate undo step
figma.createEllipse();
figma.closePlugin();With this code, when the user presses Ctrl+Z (or Cmd+Z) the first time, only the ellipse will be undone[4]. Pressing it again will undo the rectangle.
Important Considerations
-
Plugin Closure: Always remember to call
figma.closePlugin()when your plugin is done running[2]. This ensures that any UI is closed and timers are cancelled. -
Event Handling: You can register callbacks for various events using
figma.on()orfigma.once()methods[2]. This might be useful for more complex undo/redo implementations. -
Widget Differences: Note that if you're developing a widget (not a plugin), undo/redo works differently because widgets are rendered versions of their synced state[1].
-
Error Handling: Be careful with how you implement undo functionality, as errors in plugins can sometimes cause issues with Figma's native functionality, as reported in the forum where a user experienced problems with the "Go to main component" function after a plugin error[3].
By properly implementing figma.commitUndo() in your plugin, you can provide users with a more intuitive undo/redo experience that aligns with their expectations of how these operations should work.
Citations:
[1] https://www.figma.com/widget-docs/undo-redo/
[2] https://www.figma.com/plugin-docs/api/figma/
[3] https://forum.figma.com/archive-21/cannot-undo-plugin-actions-15336
[4] https://www.figma.com/plugin-docs/api/properties/figma-commitundo/
[5] https://www.figma.com/developers/api
[6] https://gist.github.com/jverkoey/6bcb7a63b1b73d558841094d84304b86
[7] https://www.layerpath.com/learn/how-to-undo-in-figma
[8] https://www.figma.com/plugin-docs/api/figma/
[9] https://wpdean.com/how-to-undo-in-figma/
[10] https://www.figma.com/plugin-docs/api/properties/figma-on/
[11] https://forum.figma.com/ask-the-community-7/can-i-edit-a-specific-history-via-plugin-api-20568
[12] https://www.saasdesign.io/learn/figma-undo
[13] https://forum.figma.com/archive-21/undo-redo-an-edit-14571
[14] https://www.youtube.com/watch?v=qPSgKtdn71Y
[15] https://www.youtube.com/watch?v=Sm3EmZn0EFI
[16] https://www.layerpath.com/learn/how-to-redo-in-figma
[17] https://www.pttrns.com/how-to-undo-in-figma/
[18] https://github.com/CodelyTV/figma-plugin-skeleton
Answer from Perplexity: pplx.ai/share
To implement undo/redo functionality for the conduit repository, I'll provide a comprehensive solution based on the command pattern, which is widely used for this purpose.
Implementation Strategy
The undo/redo functionality can be implemented using a stack-based approach where each action is recorded as a command that can be undone or redone. This pattern is particularly well-suited for applications that need to track state changes.
Core Components
1. Command Interface
First, let's create a basic command interface that all undoable actions will implement:
protocol Command {
func execute()
func undo()
}2. Command Manager
Next, we'll implement a command manager to handle the undo/redo stacks:
class UndoRedoManager {
private var undoStack: [Command] = []
private var redoStack: [Command] = []
func executeCommand(_ command: Command) {
command.execute()
undoStack.append(command)
redoStack.removeAll() // Clear redo stack when new command is executed
}
func undo() {
guard let command = undoStack.popLast() else { return }
command.undo()
redoStack.append(command)
}
func redo() {
guard let command = redoStack.popLast() else { return }
command.execute()
undoStack.append(command)
}
func canUndo() -> Bool {
return !undoStack.isEmpty
}
func canRedo() -> Bool {
return !redoStack.isEmpty
}
}3. Example Command Implementation
Here's an example of how to implement a specific command for the conduit repository:
class EditPropertyCommand: Command {
private let object: AnyObject
private let keyPath: String
private let oldValue: Any
private let newValue: Any
init(object: AnyObject, keyPath: String, oldValue: Any, newValue: Any) {
self.object = object
self.keyPath = keyPath
self.oldValue = oldValue
self.newValue = newValue
}
func execute() {
object.setValue(newValue, forKeyPath: keyPath)
}
func undo() {
object.setValue(oldValue, forKeyPath: keyPath)
}
}Integration with UI
To connect this with the UI, we need to:
- Create a singleton instance of the UndoRedoManager
- Add undo/redo buttons to the UI
- Update button states based on canUndo() and canRedo()
// In your main controller
let undoRedoManager = UndoRedoManager()
@IBAction func undoButtonTapped(_ sender: Any) {
undoRedoManager.undo()
updateButtonStates()
}
@IBAction func redoButtonTapped(_ sender: Any) {
undoRedoManager.redo()
updateButtonStates()
}
func updateButtonStates() {
undoButton.isEnabled = undoRedoManager.canUndo()
redoButton.isEnabled = undoRedoManager.canRedo()
}Example Usage
When making changes to your data model, wrap the changes in commands:
// Instead of directly changing a property
// myObject.name = "New Name"
// Create and execute a command
let oldName = myObject.name
let newName = "New Name"
let command = EditPropertyCommand(object: myObject, keyPath: "name", oldValue: oldName, newValue: newName)
undoRedoManager.executeCommand(command)Advanced Features
Compound Commands
For operations that involve multiple changes that should be undone/redone together:
class CompoundCommand: Command {
private var commands: [Command] = []
func addCommand(_ command: Command) {
commands.append(command)
}
func execute() {
for command in commands {
command.execute()
}
}
func undo() {
for command in commands.reversed() {
command.undo()
}
}
}Memory Management
For large applications, consider implementing a limit on the undo history:
// Add to UndoRedoManager
private let maxUndoStackSize = 50
func executeCommand(_ command: Command) {
command.execute()
undoStack.append(command)
if undoStack.count > maxUndoStackSize {
undoStack.removeFirst()
}
redoStack.removeAll()
}Keyboard Shortcuts
Don't forget to implement standard keyboard shortcuts:
override func keyDown(with event: NSEvent) {
if event.modifierFlags.contains(.command) {
if event.characters == "z" {
if event.modifierFlags.contains(.shift) {
undoRedoManager.redo()
} else {
undoRedoManager.undo()
}
updateButtonStates()
return
}
}
super.keyDown(with: event)
}This implementation provides a robust foundation for undo/redo functionality in the conduit repository. It follows the command pattern which is used by many frameworks including Qt[6], and can be easily extended to handle more complex operations as needed.
Citations:
[1] https://github.com/eonist
[2] https://package.elm-lang.org/packages/elm-community/undo-redo/latest/UndoList
[3] https://github.com/ConduitIO
[4] https://elixirforum.com/t/implement-undo-redo-with-ecto/54217
[5] https://dev.to/milewski/conduit-a-ui-less-node-based-system-3nkh
[6] https://stackoverflow.com/questions/14998836/implementing-undo-redo-functionality-in-qt
[7] https://elixirforum.com/t/conduit-a-framework-for-building-reliable-event-based-systems/16357
[8] mapbox/mapbox-gl-draw#791
[9] https://getconduit.dev/
[10] https://www.yesodweb.com/blog/2013/02/upcoming-conduit-1-0
[11] https://www.reddit.com/r/emacs/comments/v819tn/whats_the_best_way_to_use_undoredo_on_emacs/
[12] SleepyTrousers/EnderIO-1.5-1.12#5275
[13] https://docs.github.com/ru/enterprise-server@3.3/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/reverting-a-pull-request
[14] https://gitlab.com/famedly/conduit
[15] https://itnext.io/implementing-a-todo-list-with-undo-redo-4b38958dbd05
[16] https://gist.github.com/eonist/ffe4506e030e460bfb3836d122f7bd6a
[17] https://conduit.io/docs/scaling/conduit-operator/
[18] https://forums.autodesk.com/t5/revit-api-forum/how-to-execute-undo/td-p/12645837
[19] https://llnl-conduit.readthedocs.io/en/latest/building.html
[20] https://www.reddit.com/r/ObsidianMD/comments/1eg2sfu/undo_and_redo_buttons/
[21] https://usehooks.com/usehistorystate
[22] https://anylogic.help/anylogic/ui/undo-and-redo.html
[23] https://github.com/orgs/community/discussions/20823
[24] https://llnl-conduit.readthedocs.io/en/v0.9.1/developer_source_layout.html
[25] https://www.reddit.com/r/AzureBicep/comments/z31ba2/how_to_structure_code_repositories/
[26] https://github.com/ConduitPlatform/Conduit/blob/main/.github/CONTRIBUTING.md
[27] https://fluxcd.io/flux/guides/repository-structure/
Answer from Perplexity: pplx.ai/share