published
08.02.10
author
ecin
Doing the Undoable

I swear the title for this post wrote itself.

Matt Aimonetti, core commiter to MacRuby, has been writing up posts on using the Mac OS X Foundation libraries in our Ruby code, in preparation for his MacRuby book that should be out in stores soon. His latest suggestion was wrapping the NSUndoManager class to provide some pleasurable undo/redo capabilities to the general MacRubyist population.

As per his example, an Undoable module should provide three things as a bare minimum: #undo, #redo, and a way to add to the undo stack. In the very comments of his post, someone suggested using blocks for the undo stack, and who am I to deny good ideas:

        module Undoable  
        
          # Methods for initializing @undo_manager, and forwarding
          # #undo and #redo to it.
          [...]
        
          # This allows for:
          # add_to_undo_stack {|inst| inst.right } 
          def add_to_undo_stack(&block)
            # Ideally, object should be self so the block passed in
            # can reference this instance. However, that causes a segfault
            # somewhere.
            undo_manager.registerUndoWithTarget block, selector:'call', object:self
          end
        end
      

Since we pass self to the block, it has the ability to modify the object instance to a previous state. It’s an elegant solution, but only leads to segfaults in its current form. I think it may have something to do with passing self around as an argument, but that’s a superficial guess. At this point I gave up for a few hours…

… but upon reading a recent post by Yehuda Katz on blocks, I wanted to try something else. [NSUndoManager prepareWithInvocationTarget], the method that Matt Aimonetti uses in his example, returns a proxy object; all the methods that are called on this object are passed to the target argument when an undo message is received. So, yield-ing that proxy object to the block should work, right?

        def add_to_undo_stack
          yield undo_manager.prepareWithInvocationTarget(self)
        end
      

Luckily it does. Now, I don’t know if Mr. Aimonetti was aware of this, but his code example only allows for one undo; maybe he overlooked it. The following shows the unexpected behaviour:

        # macirb
        
        # Matt Aimonetti's example Player class
        [...]
        
        lori = Player.new
        lori.left # => -1
        lori.left # => -2
        
        # So if we undo now, we should be back at -1, right?
        lori.undo_manager.undo
        lori.x
        # => 0
      
        # Well, that's unexpected...
      

I think this is where I spent most of my time reading documentation and trying out new things. Here’s how it works: NSUndoManager keeps a stack of the undoable actions. It also allows grouping of several actions, in case rolling back a change requires several steps. I didn’t really understand how the grouping worked until I was able to peer into the stack with an undocumented method :

        # macirb
        
        # Continuing from our last code snippet
        
        lori.left
        lori.left
        
        puts lori.undo_manager._undoStack.description
        # => Stack: 0x200270220
             0: target: Player 0x20001e7a0 -- selector:right
             1: target: Player 0x20001e7a0 -- selector:right
             2: beginUndoGrouping
      

As you can see, NSUndoManager starts a group by default, into which all the actions go into. Since it never closes the group, a single #undo takes us back to the beginning. The solution? That group should never be started in the first place!

        module Undoable
          # Rest of the code
          [...]
        
          # Don't start a group by default.
          def undo_manager
            @undo_manager ||= ::NSUndoManager.new.setGroupsByEvent(false)
          end
          
          # Instead we handle the groups ourselves.
          # Open and close a group for every undo action.
          def add_to_undo_stack
            begin
              undo_manager.beginUndoGrouping
              yield undo_manager.prepareWithInvocationTarget(self)
            ensure
              undo_manager.endUndoGrouping
            end
          end
        end  
      

The full module is up on Github, where patches are tokens of friendship. There are certainly places to improve, such as overwriting the attr_* methods so they’re automatically undoable, or perhaps thinking of a better API; I’m really not happy with the name #add_to_undo_stack. I even thought of aliasing #undo and #redo , so we could have something like:

        class Person < Struct.new(:name)
           include Undoable
      
           def name=(string)
             # Not necessary to use a separate variable,
             # but it clears up the code a bit.
             current_name = self.name
             add_to_undo_stack {|inst| inst.name = current_name}
             @name = string
           end
         end
         
         person = Person.new "Lori"
         person.name = "Erin"
         person.name = "Rachel"
         
         # minus (-) => undo
         -person.name # => Erin
         # plus  (+) => redo
         +person.name # => Rachel
         
         # We can even chain them
         (--person).name # => Lori
      

… but my pair programmer deemed it too confusing (rightly so… probably).

So that’s two Ruby implementations in as many weeks. I wonder if Rubinius will entice my curiosity next.