#
# 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