Skip to content
Snippets Groups Projects
undolist.rb 11.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • #
    # This is a "pure Ruby" implementation of the FXUndoList and
    # FXCommand classes from the standard FOX distribution. Since those
    # classes are independent of the rest of FOX this is a simpler (and probably
    # more efficient) approach than trying to wrap the original C++ classes.
    
    # Notes (by Jeroen, lifted from FXUndoList.cpp):
    
    # * When a command is undone, it's moved to the redo list.
    # * When a command is redone, it's moved back to the undo list.
    # * Whenever adding a new command, the redo list is deleted.
    # * At any time, you can trim down the undo list down to a given
    #   maximum size or a given number of undo records. This should
    #   keep the memory overhead within sensible bounds.
    # * To keep track of when we get back to an "unmodified" state, a mark
    #   can be set. The <em>mark</em> is basically a counter which is incremented
    #   with every undo record added, and decremented when undoing a command.
    #   When we get back to 0, we are back to the unmodified state.
    
    #   If, after setting the mark, we have called FXUndoList#undo, then
    #   the mark can be reached by calling FXUndoList#redo.
    
    #   If the marked position is in the redo-list, then adding a new undo
    #   record will cause the redo-list to be deleted, and the marked position
    #   will become unreachable.
    
    #   The marked state may also become unreachable when the undo list is trimmed.
    
    # * You can call also kill the redo list without adding a new command
    #   to the undo list, although this may cause the marked position to
    #   become unreachable.
    # * We measure the size of the undo-records in the undo-list; when the
    #   records are moved to the redo-list, they usually contain different
    #   information!
    
    require 'fox16/responder'
    
    module Fox
    
      #
      # The undo list manages a list of undoable (and redoable) commands for a FOX
      # application; it works hand-in-hand with subclasses of FXCommand and is
      # an application of the well-known <em>Command</em> pattern. Your application
      # code should implement any number of command classes and then add then to an
      # FXUndoList instance. For an example of how this works, see the textedit
      # example program from the FXRuby distribution.
    
      # == Class Constants
    
      # [FXUndoList::ID_UNDO]	Message identifier for the undo method.
      #				When a +SEL_COMMAND+ message with this identifier
      #				is sent to an undo list, it undoes the last command.
      #				FXUndoList also provides a +SEL_UPDATE+ handler for this
      #				identifier, that enables or disables the sender
      #				depending on whether it's possible to undo.
      #
      # [FXUndoList::ID\_UNDO\_ALL]	Message identifier for the "undo all" method. FXUndoList handles both
      #				the +SEL_COMMAND+ and +SEL_UPDATE+ messages for this message
      #				identifier.
      #
      # [FXUndoList::ID_REDO]	Message identifier for the redo method. When a +SEL_COMMAND+ message
      #				with this identifier is sent to an undo list, it redoes the last command.
      #				FXUndoList also provides a +SEL_UPDATE+ handler for this identifier,
      #				that enables or disables the sender depending on whether it's possible to
      #				redo.
      #
      # [FXUndoList::ID\_REDO\_ALL]	Message identifier for the "redo all" method. FXUndoList handles both
      #				the +SEL_COMMAND+ and +SEL_UPDATE+ messages for this message
      #				identifier.
      #
      # [FXUndoList::ID_CLEAR]	Message identifier for the "clear" method. FXUndoList handles both
      #				the +SEL_COMMAND+ and +SEL_UPDATE+ messages for this message
      #				identifier.
      #
      # [FXUndoList::ID_REVERT]	Message identifier for the "revert" method. FXUndoList handles both
      #				the +SEL_COMMAND+ and +SEL_UPDATE+ messages for this message
      #				identifier.
      #
      class FXUndoList < FXObject
    
        include Responder
    
        ID_CLEAR,
        ID_REVERT,
        ID_UNDO,
        ID_REDO,
        ID_UNDO_ALL,
        ID_REDO_ALL,
        ID_LAST = enum(0, 7)
    
        #
        # Returns an initialized FXUndoList instance.
        #
        def initialize
          # Be sure to call base class initialize
          super
    
          # Set up the message map for this instance
          FXMAPFUNC(SEL_COMMAND, ID_CLEAR,    "onCmdClear")
          FXMAPFUNC(SEL_UPDATE,  ID_CLEAR,    "onUpdClear")
          FXMAPFUNC(SEL_COMMAND, ID_REVERT,   "onCmdRevert")
          FXMAPFUNC(SEL_UPDATE,  ID_REVERT,   "onUpdRevert")
          FXMAPFUNC(SEL_COMMAND, ID_UNDO,     "onCmdUndo")
          FXMAPFUNC(SEL_UPDATE,  ID_UNDO,     "onUpdUndo")
          FXMAPFUNC(SEL_COMMAND, ID_REDO,     "onCmdRedo")
          FXMAPFUNC(SEL_UPDATE,  ID_REDO,     "onUpdRedo")
          FXMAPFUNC(SEL_COMMAND, ID_UNDO_ALL, "onCmdUndoAll")
          FXMAPFUNC(SEL_UPDATE,  ID_UNDO_ALL, "onUpdUndo")
          FXMAPFUNC(SEL_COMMAND, ID_REDO_ALL, "onCmdRedoAll")
          FXMAPFUNC(SEL_UPDATE,  ID_REDO_ALL, "onUpdRedo")
    
          # Instance variables
          @undolist = []
          @redolist = []
          @marker = nil
          @size = 0
        end
    
        #
        # Cut the redo list
        #
        def cut
          @redolist.clear
          unless @marker.nil?
            @marker = nil if @marker < 0
          end
        end
    
        #
        # Add new _command_ (an FXCommand instance) to the list.
        # If _doit_ is +true+, the command is also executed.
        #
        def add(command, doit=false)
          # Cut redo list
          cut
    
          # No command given?
          return true if command.nil?
    
          # Add it to the end of the undo list
          @undolist.push(command)
    
          # Execute it right now?
          command.redo if doit
    
          # Update size
          @size += command.size	# measured after redo
    
          # Update the mark distance
          @marker = @marker + 1 unless @marker.nil?
    
          # Done
          return true
        end
    
        #
        # Undo last command.
        #
        def undo
          unless @undolist.empty?
    	command = @undolist.pop
    	@size -= command.size
    	command.undo
    	@redolist.push(command)
    	@marker = @marker - 1 unless @marker.nil?
    	return true
          end
          return false
        end
    
        #
        # Redo next command
        #
        def redo
          unless @redolist.empty?
    	command = @redolist.pop
    	command.redo
    	@undolist.push(command)
    	@size += command.size
    	@marker = @marker + 1 unless @marker.nil?
    	return true
          end
          return false
        end
    
        #
        # Undo all commands
        #
        def undoAll
          undo while canUndo?
        end
    
        #
        # Redo all commands
        #
        def redoAll
          redo while canRedo?
        end
    
        #
        # Revert to marked
        #
        def revert
          unless @marker.nil?
    	undo while (@marker > 0)
    	redo while (@marker < 0)
    	return true
          end
          return false
        end
    
        #
        # Return +true+ if we can still undo some commands
        # (i.e. the undo list is not empty).
        #
        def canUndo?
          (@undolist.empty? == false)
        end
    
        #
        # Return +true+ if we can still redo some commands
        # (i.e. the redo list is not empty).
        #
        def canRedo?
          (@redolist.empty? == false)
        end
    
        #
        # Return +true+ if there is a previously marked
        # state that we can revert to.
        #
        def canRevert?
          (@marker != nil) && (@marker != 0)
        end
    
        #
        # Returns the current undo command.
        #
        def current
          @undolist.last
        end
    
        #
        # Return the name of the first available undo command.
        # If no undo command is available, returns +nil+.
        #
        def undoName
          if canUndo?
            current.undoName
          else
            nil
          end
        end
    
        #
        # Return the name of the first available redo command.
        # If no redo command is available, returns +nil+.
        #
        def redoName
          if canRedo?
            @redolist.last.redoName
          else
            nil
          end
        end
    
        #
        # Returns the number of undo records.
        #
        def undoCount
          @undolist.size
        end
    
        #
        # Returns the total size of undo information.
        #
        def undoSize
          @size
        end
    
        #
        # Clear the list
        #
        def clear
          @undolist.clear
          @redolist.clear
          @marker = nil
          @size = 0
        end
    
        #
        # Trim undo list down to at most _nc_ commands.
        #
        def trimCount(nc)
          if @undolist.size > nc
    	numRemoved = @undolist.size - nc
    	@undolist[0, numRemoved].each { |command| @size -= command.size }
    	@undolist[0, numRemoved] = nil
    	@marker = nil if (@marker != nil && @marker > @undolist.size)
          end
        end
    
        #
        # Trim undo list down to at most _size_.
        #
        def trimSize(sz)
          if @size > sz
    	s = 0
    	@undolist.reverse.each_index { |i|
              j = @undolist.size - (i + 1)
              s += @undolist[j].size
              @undolist[j] = nil if (s > sz)
    	}
    	@undolist.compact!
    	@marker = nil if (@marker != nil && @marker > @undolist.size)
          end
        end
    
        #
        # Mark current state
        #
        def mark
          @marker = 0
        end
    
        #
        # Unmark undo list
        #
        def unmark
          @marker = nil
        end
    
        #
        # Return +true+ if the undo list is marked.
        #
        def marked?
          @marker == 0
        end
    
        def onCmdUndo(sender, sel, ptr) # :nodoc:
          undo
          return 1
        end
    
        def onUpdUndo(sender, sel, ptr) # :nodoc:
          if canUndo?
    	sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
          else
    	sender.handle(self, MKUINT(FXWindow::ID_DISABLE, SEL_COMMAND), nil)
          end
          return 1
        end
    
        def onCmdRedo(sender, sel, ptr) # :nodoc:
          self.redo
          return 1
        end
    
        def onUpdRedo(sender, sel, ptr) # :nodoc:
          if canRedo?
    	sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
          else
    	sender.handle(self, MKUINT(FXWindow::ID_DISABLE, SEL_COMMAND), nil)
          end
          return 1
        end
    
        def onCmdClear(sender, sel, ptr) # :nodoc:
          clear
          return 1
        end
    
        def onUpdClear(sender, sel, ptr) # :nodoc:
          if canUndo? || canRedo?
    	sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
          else
    	sender.handle(self, MKUINT(FXWindow::ID_DISABLE, SEL_COMMAND), nil)
          end
          return 1
        end
    
        def onCmdRevert(sender, sel, ptr) # :nodoc:
          revert
          return 1
        end
    
        def onUpdRevert(sender, sel, ptr) # :nodoc:
          if canRevert?
    	sender.handle(self, MKUINT(FXWindow::ID_ENABLE, SEL_COMMAND), nil)
          else
    	sender.handle(self, MKUINT(FXWindow::ID_DISABLE, SEL_COMMAND), nil)
          end
          return 1
        end
    
        def onCmdUndoAll(sender, sel, ptr) # :nodoc:
          undoAll
          return 1
        end
    
        def onCmdRedoAll(sender, sel, ptr) # :nodoc:
          redoAll
          return 1
        end
      end
    
      #
      # FXCommand is an "abstract" base class for your application's commands. At a
      # minimum, your concrete subclasses of FXCommand should implement the
      # #undo, #redo, #undoName, and #redoName methods.
      #
      class FXCommand
        #
        # Undo this command; this should save enough information for a
        # subsequent redo.
        #
        def undo
          raise NotImpError
        end
    
        #
        # Redo this command; this should save enough information for a
        # subsequent undo.
        #
        def redo
          raise NotImpError
        end
    
        #
        # Name of the undo command to be shown on a button or menu command;
        # for example, "Undo Delete".
        #
        def undoName
          raise NotImpError
        end
    
        #
        # Name of the redo command to be shown on a button or menu command;
        # for example, "Redo Delete".
        #
        def redoName
          raise NotImpError
        end
    
        #
        # Returns the size of the information in the undo record, i.e. the
        # number of bytes required to store it in memory. This is only used
        # by the FXUndoList#trimSize method, which can be called to reduce
        # the memory use of the undo list to a certain size.
        #
        def size
          0
        end
      end
    end