Logo Search packages:      
Sourcecode: matplotlib version File versions  Download package

undo.py

#------------------------------------------------------------------------------
# Copyright (c) 2005, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license.  The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#
# Author: David C. Morrill
# Date: 10/07/2004
#------------------------------------------------------------------------------
""" Manager for Undo and Redo history for Traits user interface support.
"""
#-------------------------------------------------------------------------------
#  Imports:
#-------------------------------------------------------------------------------

from operator \
    import isSequenceType

from enthought.traits.api \
    import HasStrictTraits, HasPrivateTraits, HasTraits, Trait, List, Int, Str,\
           Any, Event, Property, Instance, false

from enthought.traits.trait_base \
    import enumerate

#-------------------------------------------------------------------------------
#  Constants:
#-------------------------------------------------------------------------------

NumericTypes = ( int, long, float, complex )
SimpleTypes  = ( str, unicode, int, long, float, complex )

#-------------------------------------------------------------------------------
#  'AbstractUndoItem' class:
#-------------------------------------------------------------------------------

00041 class AbstractUndoItem ( HasPrivateTraits ):
    """ Abstract base class for undo items.
    """
    #---------------------------------------------------------------------------
    #  Undoes the change:
    #---------------------------------------------------------------------------

00048     def undo ( self ):
        """ Undoes the change.
        """
        raise NotImplementedError

    #---------------------------------------------------------------------------
    #  Re-does the change:
    #---------------------------------------------------------------------------

00057     def redo ( self ):
        """ Re-does the change.
        """
        raise NotImplementedError

    #---------------------------------------------------------------------------
    #  Merges two undo items if possible:
    #---------------------------------------------------------------------------

00066     def merge_undo ( self, undo_item ):
        """ Merges two undo items if possible.
        """
        return False

#-------------------------------------------------------------------------------
#  'UndoItem' class:
#-------------------------------------------------------------------------------

00075 class UndoItem ( AbstractUndoItem ):
    """ A change to an object trait, which can be undone.
    """
    #---------------------------------------------------------------------------
    #  Trait definitions:
    #---------------------------------------------------------------------------

    # Object the change occurred on
    object    = Trait( HasTraits )  
    # Name of the trait that changed
    name      = Str                 
    # Old value of the changed trait
    old_value = Property            
    # New value of the changed trait
    new_value = Property            

    #---------------------------------------------------------------------------
    #  Implementation of the 'old_value' and 'new_value' properties:
    #---------------------------------------------------------------------------

    def _get_old_value ( self ):
        return self._old_value

    def _set_old_value ( self, value ):
        if isinstance( value, list ):
            value = value[:]
        self._old_value = value

    def _get_new_value ( self ):
        return self._new_value

    def _set_new_value ( self, value ):
        if isinstance( value, list ):
            value = value[:]
        self._new_value = value

    #---------------------------------------------------------------------------
    #  Undoes the change:
    #---------------------------------------------------------------------------

00115     def undo ( self ):
        """ Undoes the change.
        """
        try:
            setattr( self.object, self.name, self.old_value )
        except:
            pass

    #---------------------------------------------------------------------------
    #  Re-does the change:
    #---------------------------------------------------------------------------

00127     def redo ( self ):
        """ Re-does the change.
        """
        try:
            setattr( self.object, self.name, self.new_value )
        except:
            pass

    #---------------------------------------------------------------------------
    #  Merges two undo items if possible:
    #---------------------------------------------------------------------------

00139     def merge_undo ( self, undo_item ):
        """ Merges two undo items if possible.
        """
        # Undo items are potentially mergeable only if they are of the same
        # class and refer to the same object trait, so check that first:
        if (isinstance( undo_item, self.__class__ ) and
           (self.object is undo_item.object) and
           (self.name == undo_item.name)):
            v1 = self.new_value
            v2 = undo_item.new_value
            t1 = type( v1 )
            if t1 is type( v2 ):

                if isinstance(t1, basestring):
                    # Merge two undo items if they have new values which are
                    # strings which only differ by one character (corresponding
                    # to a single character insertion, deletion or replacement
                    # operation in a text editor):
                    n1 = len( v1 )
                    n2 = len( v2 )
                    n  = min( n1, n2 )
                    i  = 0
                    while (i < n) and (v1[i] == v2[i]):
                        i += 1
                    if v1[i + (n2 <= n1):] == v2[i + (n2 >= n1):]:
                        self.new_value = v2
                        return True

                elif isSequenceType( v1 ):
                    # Merge sequence types only if a single element has changed
                    # from the 'original' value, and the element type is a
                    # simple Python type:
                    v1 = self.old_value
                    if isSequenceType( v1 ):
                        # Note: wxColour says it's a sequence type, but it
                        # doesn't support 'len', so we handle the exception
                        # just in case other classes have similar behavior:
                        try:
                            if len( v1 ) == len( v2 ):
                                diffs = 0
                                for i, item in enumerate( v1 ):
                                    titem = type( item )
                                    item2 = v2[i]
                                    if ((titem not in SimpleTypes)   or
                                        (titem is not type( item2 )) or
                                        (item != item2)):
                                        diffs += 1
                                        if diffs >= 2:
                                            return False
                                if diffs == 0:
                                    return False
                                self.new_value = v2
                                return True
                        except:
                            pass

                elif t1 in NumericTypes:
                    # Always merge simple numeric trait changes:
                    self.new_value = v2
                    return True
        return False

    #---------------------------------------------------------------------------
    #  Returns a 'pretty print' form of the object:
    #---------------------------------------------------------------------------

00205     def __repr__ ( self ):
        """ Returns a "pretty print" form of the object.
        """
        n  = self.name
        cn = self.object.__class__.__name__
        return 'undo( %s.%s = %s )\nredo( %s.%s = %s )' % (
                      cn, n, self.old_value, cn, n, self.new_value )

#-------------------------------------------------------------------------------
#  'ListUndoItem' class:
#-------------------------------------------------------------------------------

00217 class ListUndoItem ( AbstractUndoItem ):
    """ A change to a list, which can be undone.
    """
    #---------------------------------------------------------------------------
    #  Trait definitions:
    #---------------------------------------------------------------------------

    # Object that the change occurred on
    object    = Trait( HasTraits )  
    # Name of the trait that changed
    name      = Str                 
    # Starting index
    index     = Int                 
    # Items added to the list
    added     = List                
    # Items removed from the list
    removed   = List                

    #---------------------------------------------------------------------------
    #  Undoes the change:
    #---------------------------------------------------------------------------

00239     def undo ( self ):
        """ Undoes the change.
        """
        try:
            list = getattr( self.object, self.name )
            list[ self.index: (self.index + len( self.added )) ] = self.removed
        except:
            pass

    #---------------------------------------------------------------------------
    #  Re-does the change:
    #---------------------------------------------------------------------------

00252     def redo ( self ):
        """ Re-does the change.
        """
        try:
            list = getattr( self.object, self.name )
            list[ self.index: (self.index + len( self.removed )) ] = self.added
        except:
            pass

    #---------------------------------------------------------------------------
    #  Merges two undo items if possible:
    #---------------------------------------------------------------------------

00265     def merge_undo ( self, undo_item ):
        """ Merges two undo items if possible.
        """
        # Discard undo items that are identical to us. This is to eliminate
        # the same undo item being created by multiple listeners monitoring the
        # same list for changes:
        if (isinstance( undo_item, self.__class__ )        and
           (self.object is undo_item.object)               and
           (self.name  == undo_item.name)                  and
           (self.index == undo_item.index)):
            added   = undo_item.added
            removed = undo_item.removed
            if ((len( self.added )   == len( added )) and
                (len( self.removed ) == len( removed ))):
                for i, item in enumerate( self.added ):
                    if item is not added[i]:
                        break
                else:
                    for i, item in enumerate( self.removed ):
                        if item is not removed[i]:
                            break
                    else:
                        return True
        return False

    #---------------------------------------------------------------------------
    #  Returns a 'pretty print' form of the object:
    #---------------------------------------------------------------------------

00294     def __repr__ ( self ):
        """ Returns a 'pretty print' form of the object.
        """
        return 'undo( %s.%s[%d:%d] = %s )' % (
                self.object.__class__.__name__, self.name, self.index,
                self.index + len( self.removed ), self.added )

#-------------------------------------------------------------------------------
#  'UndoHistory' class:
#-------------------------------------------------------------------------------

00305 class UndoHistory ( HasStrictTraits ):
    """ Manages a list of undoable changes.
    """
    #---------------------------------------------------------------------------
    #  Trait definitions:
    #---------------------------------------------------------------------------

    # List of accumulated undo changes
    history  = List           
    # The current position in the list
    now      = Int            
    # Fired when state changes to undoable
    undoable = Event( False ) 
    # Fired when state changes to redoable
    redoable = Event( False ) 
    # Can an action be undone?
    can_undo = Property       
    # Can an action be redone?
    can_redo = Property       

    #---------------------------------------------------------------------------
    #  Adds an UndoItem to the history:
    #---------------------------------------------------------------------------

00329     def add ( self, undo_item, extend = False ):
        """ Adds an UndoItem to the history.
        """
        if extend:
            self.extend( undo_item )
            return

        # Try to merge the new undo item with the previous item if allowed:
        now = self.now
        if now > 0:
            previous = self.history[ now - 1 ]
            if (len( previous ) == 1) and previous[0].merge_undo( undo_item ):
                self.history[ now: ] = []
                return

        old_len = len( self.history )
        self.history[ now: ] = [ [ undo_item ] ]
        self.now += 1
        if self.now == 1:
            self.undoable = True
        if self.now <= old_len:
            self.redoable = False

    #---------------------------------------------------------------------------
    #  Extends the most recent 'undo' item:
    #---------------------------------------------------------------------------

00356     def extend ( self, undo_item ):
        """ Extends the undo history. 
        
        If possible the method merges the new UndoItem with the last item in 
        the history; otherwise, it appends the new item.
        """
        if self.now > 0:
            undo_list =  self.history[ self.now - 1 ]
            if not undo_list[-1].merge_undo( undo_item ):
                undo_list.append( undo_item )

    #---------------------------------------------------------------------------
    #  Undo an operation:
    #---------------------------------------------------------------------------

00371     def undo ( self ):
        """ Undoes an operation.
        """
        if self.can_undo:
            self.now -= 1
            items = self.history[ self.now ]
            for i in range( len( items ) - 1, -1, -1 ):
                items[i].undo()
            if self.now == 0:
                self.undoable = False
            if self.now == (len( self.history ) - 1):
                self.redoable = True

    #---------------------------------------------------------------------------
    #  Redo an operation:
    #---------------------------------------------------------------------------

00388     def redo ( self ):
        """ Redoes an operation.
        """
        if self.can_redo:
            self.now += 1
            for item in self.history[ self.now - 1 ]:
                item.redo()
            if self.now == 1:
                self.undoable = True
            if self.now == len( self.history ):
                self.redoable = False

    #---------------------------------------------------------------------------
    #  Reverts all changes made so far and clears the history:
    #---------------------------------------------------------------------------

00404     def revert ( self ):
        """ Reverts all changes made so far and clears the history.
        """
        history = self.history[ : self.now ]
        self.clear()
        for i in range( len( history ) - 1, -1, -1 ):
            items = history[i]
            for j in range( len( items ) - 1, -1, -1 ):
                items[j].undo()

    #---------------------------------------------------------------------------
    #  Clears the undo history
    #---------------------------------------------------------------------------

00418     def clear ( self ):
        """ Clears the undo history.
        """
        old_len  = len( self.history )
        old_now  = self.now
        self.now = 0
        del self.history[:]
        if old_now > 0:
            self.undoable = False
        if old_now < old_len:
            self.redoable = False

    #---------------------------------------------------------------------------
    #  Are there any undoable operations?
    #---------------------------------------------------------------------------

00434     def _get_can_undo ( self ):
        """ Are there any undoable operations?
        """
        return self.now > 0

    #---------------------------------------------------------------------------
    #  Are there any redoable operations?
    #---------------------------------------------------------------------------

00443     def _get_can_redo ( self ):
        """ Are there any redoable operations?
        """
        return self.now < len( self.history )

#-------------------------------------------------------------------------------
#  'UndoHistoryUndoItem' class:
#-------------------------------------------------------------------------------

00452 class UndoHistoryUndoItem ( AbstractUndoItem ):
    """ An undo item for the undo history.
    """
    #---------------------------------------------------------------------------
    #  Trait definitions:
    #---------------------------------------------------------------------------

    # The undo history to undo or redo
    history = Instance( UndoHistory )

    #---------------------------------------------------------------------------
    #  Undoes the change:
    #---------------------------------------------------------------------------

00466     def undo ( self ):
        """ Undoes the change.
        """
        history = self.history
        for i in range( history.now - 1, -1, -1 ):
            items = history.history[i]
            for j in range( len( items ) - 1, -1, -1 ):
                items[j].undo()

    #---------------------------------------------------------------------------
    #  Re-does the change:
    #---------------------------------------------------------------------------

00479     def redo ( self ):
        """ Re-does the change.
        """
        history = self.history
        for i in range( 0, history.now ):
            for item in history.history[i]:
                item.redo()


Generated by  Doxygen 1.6.0   Back to index