Skip to content

Add undo redo #232

@eonist

Description

@eonist

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

  1. 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.

  2. Event Handling: You can register callbacks for various events using figma.on() or figma.once() methods[2]. This might be useful for more complex undo/redo implementations.

  3. 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].

  4. 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:

  1. Create a singleton instance of the UndoRedoManager
  2. Add undo/redo buttons to the UI
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions