Summary

First Beta, guarantted to be loaded with bugs!

This demo creates a widget with full "styled text editing" capabilities; sort of a mini-word processor. It runs "as is" on my WinXP machine with Python 2.5. The demo allows styling of any selected text via toolbars; just select the text, then select the styling. (Currently, it's not possible to select insertion styling). Also included are buttons to save and retrieve all content and styling information (works for the small tests I've tried myself, however, expect bugs!).

There are actualy three widgets created: StyledText (which contains my desired API), TextWriter (A StyledText object with toolbars all thrown into a Frame, and StyleEditor (A dialog box for allowing the user to create custom styles). StyledText widget subclasses the Text widget to give greater freedom in the assignment of any single styling attribute to any region. This differs from the Text widget in that the Text widget does not allow you to assign a family, size, weight or slant with tag_config(). That is, for example, you can't simply apply 'bold' to some region of text.

This demo was written quite quickly (I pounded it out over the weekend) so I'm sure it's full of bugs and may not be entirely tk-like. Future versions will strive to increase functionality, reduce bugs and make the widget more tk-like. Suggestions for imporvements are more than welcome.

API Notes

applyStyleAttribute( index1, index2, attributeName, attributeValue )

This method is the frontend to the business. attributeName and attributeValue may be any option accpeted by tag_config() of the Text widget along with appropriate value. There are also some new options:

Several previously existing options have some new values:

Implementation Details

--- These notes assume you understand the basics of using the text widget. --

To make the widget react more naturally when changing the value of 'offset' across a region, I found it convenient to make 'offset' A sub-option of the font object rather than a full option. In this way if we have some text which has (Ariel, 12, bold, superscript), the original size is preserved in the font. The actual tag in the text widget gets configured with  {font:('Ariel',5,bold), offset='10p'} . Now if the user decides to remove the superscript styling, the information of the original size (12) is still available in the font object so it's easily restored. To implement this I defined my own class Font to use with the StyledText in place of tkFont. This class includes the very useful method tagOptions() which returns a dictionary of options suitable for passing to Text's tag_config() to produce the desired appearance. class Font manages the options: family, size, weight, slant and offset. Underline and Overstrike can be set independently via tag_config(), so they are not managed by Font.

The primary attribute styling class is class Style. This class is a subclass of dict which is intended to hold values for all the style options not part of StylerFont. class Style also contains a _font member and like, StylerFont, has the very useful tagOptions() method. This tagOptions will combine its own values with any it gets from calling tagOptions() on _font to form a complete set of values suitable for tag_config().

To simplify the seeming chaos of layered tags and overlapping tagged regions, I decided to first define some restrictions on how tags can be used.

  1. The widget has a default styling (which assignes a value to every style attribute).
  2. A non-StylerFont tag may configure exactly one styling attribute.

  3. For any index in the underlying text widget, there may be at most one tag for any given non-StylerFont attribute.

  4. For any index in the underlying text widget, there may be at most one tag handling a StylerFont.

  5. For any unique attribute configuration in use there should exist exactly one tag.

These restrictions eliminate "Infinite depth tag stack" (criteria 2, 3 & 4) and "tagName redundancy" (criteria 5). Conceptually we now have just two styling layers: default styling on the bottom, custom styling on top. So, for example if I call applyStyleAttribute( begin, end, 'underline', Tkinter.TRUE ), any existing tags in the region (begin,end) which assign to 'underline', are first deleted if the new styling is something other than the default for the attribute being set, the new styling is applied.

With the tag chaos of the Text widget under control, it's now possible to design an algorithm which provides applyStyleAttribute() with a "map" of the current styling of the region in question. This map actually splits the region at every point that any tag in the region begins or ends (as well as the endpoints of the region itself). Here's an example to illustrate:

Sample Text:        Sample text to illustrate a styling map of a region of text.
Indecies:                     1         2         3         4         5         6
                    0123456789012345678901234567890123456789012345678901234567890

tag1:                    |<---------------->|          |<--------------->|
tag2:                              |<----------->|

Region to map:       |<-------------------------------------->|
Resulting map:       |<->|<------->|<------>|<-->|<--->|<---->|

So here we have some Sample Text. tag1 spans regions 1.5 - 1.24 and 1.35 - 1.53, and tag2 spans 1.15 - 1.29. mapRegion() slices at any tag begin or tag end and at its endpoints this results in a map with six segments. For each segment in the map, the list of active tags is also given. So mapRegion() in the above example returns the following list:

[
   ( '1.1',  '1.5',  [ ] ),                 # No tags in this region
   ( '1.5',  '1.15', [ 'tag1' ] ),          # tag1-bold in this region
   ( '1.15', '1.24', [ 'tag1', 'tag2' ] ),  # etc.
   ( '1.24', '1.29', [ 'tag2' ] ),
   ( '1.29', '1.35', [ ] ),
   ( '1.35', '1.42', [ 'tag1' ] )
]

Next is to determine which attributes and values are set in each subregion. The obvious way is to have a dict mapping tagName, to Style object, but it's not the only way. We need to avoid tag-name redundancy, criterion 5 (E.g. We don't want to define a new tag for underline every time the user selects a bit of text and applies the underline style. We should use the same underline tag for all cases of underline). To handle this I use a standard naming convention for tags such that the name of the tag encodes the tag's configuration - this is reasonable because of criterion 2 (the names won't get too long). This also eliminates the need for a dictionary since the name of each tag can be decoded to get its configuration. Any tag which sets a StylerFont attribute (family, size, weight, slant or offset) has the tag named ast: "-font-%(family)s-%(size)d-%(weight)s-%(slant)s-%(offset)". The remaining attributes have tags named as: "-attribute-%(attributeName)s-%(attributeValue)s".

Now if applyStyleAttribute() gets an assignment to any StylerFont attribute, it iterates throught the subregions returned by mapRegion() if it there's a tag that begins "-font-..." (and there can be only one per criterion 3), then it asks StylerFont to decode the tag name and create a new StylerFont instance, then it deletes the tag from the text widget. Next it call's StylerFont's deriveFont(attributeName,newAttributeValue) which returns a new StylerFont instance. It calls tagOptions() and fontName() on the new font object to get what's needed to define and configure a new "-font-..." tag for the subregion.

Similarly, if applyStyleAttribute() gets an assignment to any other attribute, it iterates through the subregions returned by mapRegion(), if there's a tag that begins "-attribute-%(attributeName)s-" (and there can be only one per criterion 4), then it decodes the tag name the tag is deleted from the subregion in the underlying text widget, a new tag name and Style instance is creatd which are used to define and configure the new tag in the underlying text widget.

In that cases where applyStyleAttribute() is iterating over the subregions returned by mapRegion, and the subregion is empty or does not contain any styling on attributeName, then applyStyleAttribute assumes the default styling (criterion 1).

The code follows:

   1 import Tix as Tk
   2 import tkFont as Font
   3 
   4 DIALOG_FONT       = 'Times 8'
   5 DEFAULT_TEXT_FONT = ('Lucida Sans Unicode', 12)
   6 
   7 class ImageFactory( object ):
   8    _images = { }
   9    
  10    @staticmethod
  11    def makeImage( filename ):
  12       if filename not in ImageFactory._images:
  13          ImageFactory._images[ filename ] = Tk.PhotoImage( file=filename )
  14       
  15       return ImageFactory._images[ filename ]
  16    
  17 
  18 class StylerFont( object ):
  19    FIELD_NAMES          = [ 'family', 'size', 'weight', 'slant', 'offset', 'bold', 'italic' ]
  20    FONT_TAG_FORMAT      = '-font-%(family)s-%(size)d-%(weight)s-%(slant)s-%(offset)s'
  21    FONT_TAG_PREFIX      = '-font-'
  22    FONT_LIBRARY         = { }
  23    DEFAULT_FONT         = None
  24 
  25    def __init__( self, fontOpts ):
  26       self._fontOpts = fontOpts
  27    
  28    def __setitem__( self, key, value ):
  29       if key == 'bold':
  30          self._fontOpts[ 'weight' ] = 'bold' if value else 'normal'
  31       elif key == 'italic':
  32          self._fontOpts[ 'slant'  ] = 'italic' if value else 'roman'
  33       else:
  34          self._fontOpts[ key ] = value
  35    
  36    def __getitem__( self, key ):
  37       if key == 'bold':
  38          return self._fontOpts[ 'weight' ] == 'bold'
  39       elif key == 'italic':
  40          return self._fontOpts[ 'slant' ] == 'italic'
  41       else:
  42          return self._fontOpts[ key ]
  43    
  44    def fontName( self ):
  45       return StylerFont.FONT_TAG_FORMAT % self._fontOpts
  46 
  47    def fontOptions( self ):
  48       return self._fontOpts
  49 
  50    def tagOptions( self ):
  51       options = { }
  52       
  53       styleList = [ ]
  54       if self._fontOpts['weight'] == 'bold':
  55          styleList.append( 'bold' )
  56       if self._fontOpts['slant'] == 'italic':
  57          styleList.append( 'italic' )
  58       
  59       if self._fontOpts['offset'] != 'normal':
  60          # If the region includes an offset, we need to reestablish that
  61          baseSize = self._fontOpts['size']
  62          size = int(baseSize * (3.0 / 5.0) + 0.5)
  63          
  64          if self._fontOpts['offset'] == 'superscript':
  65             options[ 'offset' ] = '%dp'  % int(baseSize / 2.0 + 0.5)
  66          else:
  67             options[ 'offset' ] = '-%dp' % int(baseSize / 5.0 + 0.5)
  68       else:
  69          size = self._fontOpts['size']
  70       
  71       options[ 'font' ] = ( self._fontOpts['family'], size, ' '.join(styleList) )
  72       
  73       return options
  74 
  75    def deriveFont( self, **newOpts ):
  76       newFont = copy.deepcopy( self )
  77       for key,val in newOpts.iteritems():
  78          newFont[ key ] = val
  79       
  80       fontName = newFont.fontName( )
  81       
  82       if fontName not in StylerFont.FONT_LIBRARY:
  83          StylerFont.FONT_LIBRARY[ fontName ] = newFont
  84       return StylerFont.FONT_LIBRARY[ fontName ]
  85 
  86    def tkFont( self ):
  87       return Font.Font( family=self._fontOpts['family'], size=self._fontOpts['size'], weight=self._fontOpts['weight'], slant=self._fontOpts['slant'] )
  88 
  89    @staticmethod
  90    def setup( aTextWidget ):
  91       StylerFont.DEFAULT_FONT = StylerFont.getFont( aTextWidget['font'] )
  92       StylerFont.FONT_LIBRARY[ 'default' ] = StylerFont.DEFAULT_FONT
  93    
  94    @staticmethod
  95    def getFont( aFontSpec=None, **options ):
  96       fontOpts = { }
  97       fontStyling = ''
  98       
  99       if aFontSpec:
 100          if isinstance( aFontSpec, (str,unicode) ):
 101             if aFontSpec == 'default':
 102                return StylerFont.FONT_LIBRARY[ 'default' ]
 103             elif aFontSpec.startswith( StylerFont.FONT_TAG_PREFIX ):
 104                # We have a font name
 105                try:
 106                   return StylerFont.FONT_LIBRARY[ aFontSpec ]
 107                except:
 108                   for key,val in zip( ['family','size','weight','slant','offset'], aFontSpec.split( '-' )[2:] ):
 109                      fontOpts[ key ] = val
 110                   fontOpts['size'] = int(fontOpts['size'])
 111                   StylerFont.FONT_LIBRARY[ aFontSpec ] = StylerFont( fontOpts )
 112                   return StylerFont.FONT_LIBRARY[ aFontSpec ]
 113             else:
 114                # We need to try to decode a tkinter font string
 115                if aFontSpec[0] == '{':
 116                   fontFamilyStringBegin = 1
 117                   fontFamilyStringEnd    = aFontSpec.find( '}' )
 118                   fontSize, sep, fontStyling = aFontSpec[ fontFamilyStringEnd + 2 : ].lower().partition( ' ' )
 119                else:
 120                   fontFamilyStringBegin = 0
 121                   fontFamilyStringEnd   = aFontSpec.find( ' ' )
 122                   fontSize, sep, fontStyling = aFontSpec[ fontFamilyStringEnd + 1 : ].lower().partition( ' ' )
 123                
 124                fontOpts['family'    ] = aFontSpec[ fontFamilyStringBegin : fontFamilyStringEnd ]
 125                fontOpts['size'      ] = int(fontSize)
 126          
 127          elif isinstance( aFontSpec, (list,tuple) ):
 128             # We need to decode a tkinter font tuple
 129             fontStyling = aFontSpec[2] if len(aFontSpec)==3 else ''
 130             fontOpts['family'    ] = aFontSpec[0]
 131             fontOpts['size'      ] = aFontSpec[1]
 132       
 133       elif options:
 134          if 'family' in options:
 135             if 'bold' in options:
 136                options[ 'weight' ] = 'bold' if options['bold'] else 'normal'
 137                del options[ 'bold' ]
 138             
 139             if 'italic' in options:
 140                options[ 'slant' ] = 'italic' if options['italic'] else 'roman'
 141                del options[ 'italic' ]
 142             
 143             fontName = StylerFont.FONT_TAG_FORMAT % options
 144             
 145             try:
 146                return StylerFont.FONT_LIBRARY[ fontName ]
 147             except:
 148                StylerFont.FONT_LIBRARY[ fontName ] = StylerFont( options )
 149                return StylerFont.FONT_LIBRARY[ fontName ]
 150          
 151          else:
 152             fontTuple = options[ 'font' ]
 153             
 154             fontStyling = aFontSpec[2] if len(aFontSpec)==3 else ''
 155             fontOpts['family'    ] = fontTuple[0]
 156             fontOpts['size'      ] = fontTuple[1]
 157       
 158       else:
 159          return StylerFont.getFont( 'default' )
 160       
 161       fontOpts['weight'    ] = 'bold' if 'bold' in fontStyling else 'normal'
 162       fontOpts['slant'     ] = 'italic' if 'italic' in fontStyling else 'roman'
 163       fontOpts['underline' ] = False
 164       fontOpts['overstrike'] = False
 165       fontOpts['offset'    ] = 'normal'
 166       
 167       fontName = StylerFont.FONT_TAG_FORMAT % fontOpts
 168       
 169       try:
 170          return StylerFont.FONT_LIBRARY[ fontName ]
 171       except:
 172          theFontObj = StylerFont( fontOpts )
 173          StylerFont.FONT_LIBRARY[ fontName ] = theFontObj
 174          return theFontObj
 175    
 176 
 177 class Style( dict ):
 178    FIELD_NAMES      = [ 'font', 'underline', 'overstrike', 'foreground', 'background' 'fgstipple', 'bgstipple', 'borderwidth', 'relief',
 179                         'justify', 'wrap', 'lmargin1', 'lmargin2', 'rmargin', 'spacing1', 'spacing2', 'spacing3', 'tabs' ]
 180    ATTRIBUTE_TAG_FORMAT  = '-attribute-%(name)s-%(value)s'
 181    ATTRIBUTE_TAG_PREFIX  = '-attribute-'
 182    DEFAULT_STYLE    =  None
 183    OFF_VALUES       =  { }
 184 
 185    '''Most fields have the exact same set of possible values as the related option for the text widget.
 186    'font' is a legitimate Style field.  While any subfield of 'font' is not a style field of Style,
 187    they are accepted by __init__, __setitem__ and __getitem__ but will simply be passed to the
 188    StylerFont subobject.  (e.g. family, may be requested of a style instance and will be passed to the
 189    StylerFont object).
 190    
 191    The 'offset' and the three 'spacing' options can take any of the usual values available to the text
 192    widget. However, several new values are also possible for these fields.
 193    
 194    font:          StylerFont
 195       family:     string.    A font family name
 196       size:       int.       A font size in points
 197       weight:     string.    One of:  'normal', 'bold'
 198       bold:       boolean.   An alternate for weight
 199       slant:      string.    One of:  'roman', 'italic'
 200       italic:     boolean.   An alternate for slant
 201       underline:  boolean.
 202       overstrike: boolean.
 203       offset:     Dimension or string.  One of: 'normal', 'subscript', 'superscript'
 204    
 205    foreground:    Color.
 206    background:    Color.
 207    fgstipple:     bitmap.
 208    bgstipple:     bitmap.
 209    borderwidth:   Dimension.
 210    relief:        string.    One of: 'flat', 'sunken', 'raised', 'groove', 'ridge', 'solid', ''
 211    
 212    justify:       string.    One of: 'left', 'right', 'center', ''
 213    wrap:          string.    One of: 'none', 'char', 'word', ''
 214    lmargin1:      Dimension.
 215    lmargin2:      Dimension.
 216    rmargin:       Dimension.
 217    spacing1:      Dimension or string.  One of: 'None', 'Half Line', 'One Line', 'Two Lines'
 218    spacing2:      Dimension or string.  One of: 'None', 'Half Line', 'One Line', 'Two Lines'
 219    spacing3:      Dimension or string.  One of: 'None', 'Half Line', 'One Line', 'Two Lines'
 220    tabs:          string of Dimension.
 221    '''
 222    def __init__( self, **options ):
 223       dict.__init__( self )
 224       self._font = None
 225       
 226       for key,value in options.iteritems( ):
 227          if key == 'font':
 228             if isinstance( value, StylerFont ):
 229                self._font = value
 230             else:
 231                self._font = StylerFont( value )
 232          else:
 233             dict.__setitem__( self, key, value )
 234 
 235    def __setitem__( self, key, value ):
 236       if key in StylerFont.FIELD_NAMES:
 237          if key == 'bold':
 238             key = 'weight'
 239             value = 'bold' if value else 'normal'
 240          elif key == 'italic':
 241             key = 'slant'
 242             value = 'italic' if value else 'roman'
 243          
 244          self._font[ key ] = value
 245       
 246       else:
 247          if (value in Style.OFF_VALUES[key]) or (value == Style.DEFAULT_STYLE[key]):
 248             if key in self:
 249                self.__delitem__( key )
 250          else:
 251             if key == 'font':
 252                if isinstance( value, StylerFont ):
 253                   self._font = value
 254                else:
 255                   self._font = StylerFont.getFont( value )
 256             else:
 257                dict.__setitem__( self, key, value )
 258 
 259    def __getitem__( self, key ):
 260       if key in StylerFont.FIELD_NAMES:
 261          return self._font[ key ]
 262       elif key in self:
 263          return dict.__getitem__( self, key )
 264       elif key == 'font':
 265          return self._font
 266    
 267    def tagOptions( self ):
 268       options  = { }
 269       options.update( self )
 270       try:
 271          options.update( self._font.tagOptions( ) )
 272          lineHeight = self._font.tkFont().metrics('linespace')
 273       except:
 274          lineHeight = Style.DEFAULT_STYLE._font.tkFont().metrics('linespace')
 275       
 276       # Adjust fields with non-tk options
 277       for spacingKind in [ 'spacing1', 'spacing2', 'spacing3' ]:
 278          try:
 279             selectedSpacing = options[ spacingKind ]
 280             if selectedSpacing == 'None':
 281                options[ spacingKind ] = '0'
 282             elif selectedSpacing == 'Half Line':
 283                options[ spacingKind ] = '%dp' % int(float(lineHeight * 0.5 + 0.5))
 284             elif selectedSpacing == 'One Line':
 285                options[ spacingKind ] = '%dp' % int(lineHeight)
 286             elif selectedSpacing == 'Two Lines':
 287                options[ spacingKind ] = '%dp' % int(lineHeight * 2)
 288             else:
 289                options[ spacingKind ] = selectedSpacing
 290          except:
 291             pass
 292       
 293       return options
 294    
 295    @staticmethod
 296    def setup( aTextWidget ):
 297       StylerFont.setup( aTextWidget )
 298       Style.DEFAULT_STYLE = Style( font        = StylerFont.DEFAULT_FONT,
 299                                    foreground  = aTextWidget[ 'foreground' ],
 300                                    background  = aTextWidget[ 'background' ],
 301                                    fgstipple   = '',
 302                                    bgstipple   = '',
 303                                    borderwidth = 0,
 304                                    relief      = Tk.FLAT,
 305                                    justify     = Tk.LEFT,
 306                                    wrap        = Tk.CHAR,
 307                                    lmargin1    = 0,
 308                                    lmargin2    = 0,
 309                                    rmargin     = 0,
 310                                    spacing1    = 0,
 311                                    spacing2    = 0,
 312                                    spacing3    = 0,
 313                                    tabs        = ''      
 314                                  )
 315       
 316       Style.OFF_VALUES = {
 317                           # Font Attributes
 318                           'family':     [ StylerFont.DEFAULT_FONT['family'] ],
 319                           'size':       [ StylerFont.DEFAULT_FONT['size'  ] ],
 320                           'weight':     [ Font.NORMAL, None          ],
 321                           'slant':      [ Font.ROMAN,  None          ],
 322                           'offset':     [ 'normal', '0', '0p', '0i', '0c', '0m', '', None          ],
 323                            
 324                           # Paragraph Attributes
 325                           'justify':    [ Tk.LEFT, '', None ],
 326                           'wrap':       [ Tk.CHAR, '', None ],
 327                           'lmargin1':   [ '0', '0p', '0i', '0c', '0m', '', None ],
 328                           'lmargin2':   [ '0', '0p', '0i', '0c', '0m', '', None ],
 329                           'rmargin':    [ '0', '0p', '0i', '0c', '0m', '', None ],
 330                           'spacing1':   [ '0', '0p', '0i', '0c', '0m', 'None', '', None ],
 331                           'spacing2':   [ '0', '0p', '0i', '0c', '0m', 'None', '', None ],
 332                           'spacing3':   [ '0', '0p', '0i', '0c', '0m', 'None', '', None ],
 333                           'tabs':       [ '', None ],
 334                            
 335                           # Other Attributes
 336                           'underline':  [ Tk.FALSE, '0', 'false', 'False', False, '', None ],
 337                           'overstrike': [ Tk.FALSE, '0', 'false', 'False', False, '', None ],
 338                           'foreground': [ aTextWidget[ 'foreground' ], '', None ],
 339                           'background': [ aTextWidget[ 'background' ], '', None ],
 340                           'fgstipple':  [ '', 'none', None ],
 341                           'bgstipple':  [ '', 'none', None ],
 342                           'borderwidth':[ '0', '0p', '0i', '0c', '0m', '', None ],
 343                           'relief':     [ Tk.FLAT, '', None ]
 344                           }
 345 
 346    @staticmethod
 347    def getStyle( aSpec=None, **options ):
 348       if aSpec:
 349          if isinstance( aSpec, str ):
 350             if aSpec.startswith(StylerFont.FONT_TAG_PREFIX):
 351                return StylerFont.getFont( aSpec )
 352             elif aSpec.startswith(Style.ATTRIBUTE_TAG_PREFIX):
 353                key,value = aSpec.replace( '_',' ' ).split('-')[2:]
 354                theStyle = Style( )
 355                theStyle[ key ] = value
 356                return theStyle
 357       if options:
 358          return Style( **options)
 359 
 360 
 361 class Styler( object ):
 362    ATTRIBUTE_TAG_FORMAT = '-attribute-%s-%s'  # name, value
 363    ATTRIBUTE_TAG_PREFIX = '-attribute-'
 364    
 365    def __init__( self, aTextWidget ):
 366       self.text       = aTextWidget
 367       
 368       Style.setup( self.text )
 369       self.reinitizlize( )
 370 
 371    def reinitizlize( self, styles=None ):
 372       self.text.delete( '1.0', Tk.END )
 373       
 374       if styles:
 375          self._styles = styles
 376       else:
 377          self._styles = { }
 378 
 379    def applyStyleAttribute( self, index1, index2, attributeName, attributeValue ):
 380       '''Applies an attribute name:value pair to a given region indicated by
 381       index1, index2.  Index1 is part of the region, index2 is not.  Valid
 382       values for name and value are the same as those accepted by Style.__setitem__().
 383       '''
 384       try:
 385          index1 = self.text.index( index1 )
 386          index2 = self.text.index( index2 )
 387       except:
 388          return
 389       
 390       # Manipulate the styles and tags in the region
 391       if attributeName in ( 'family', 'size', 'weight', 'slant', 'offset', 'bold', 'italic' ):
 392          for beg, end, tagNameList in self.mapRegion( index1, index2 ):
 393             # Get the old font info (oldFontOpts) and delete the styling tag
 394             for oldTagName in tagNameList:
 395                if oldTagName.startswith( StylerFont.FONT_TAG_PREFIX ):
 396                   currentFont = StylerFont.getFont( oldTagName )
 397                   self.text.tag_remove( oldTagName, beg, end )
 398                   break
 399             
 400             else:
 401                currentFont = StylerFont.getFont( )
 402             
 403             # Calculating & Install new values
 404             newFont = currentFont.deriveFont( **{ attributeName:attributeValue } )
 405             newTagName = newFont.fontName( )
 406             
 407             self.text.tag_add( newTagName, beg, end )
 408             self.text.tag_config( newTagName, **newFont.tagOptions() )
 409       
 410       else:
 411          for beg, end, tagNameList in self.mapRegion( index1, index2 ):
 412             # If the attribute already exists, remove it.
 413             for oldTagName in tagNameList:
 414                if oldTagName.startswith( Styler.ATTRIBUTE_TAG_PREFIX + attributeName + '-' ):
 415                   self.text.tag_remove( oldTagName, beg, end )
 416                   break
 417             
 418             # If we're just deactivating, then we're done
 419             if attributeValue in Style.OFF_VALUES[ attributeName ]:
 420                continue
 421             
 422             # Calculate the new values
 423             newTagName = Styler.ATTRIBUTE_TAG_FORMAT % ( attributeName, str(attributeValue) )
 424             
 425             # Install the new values
 426             self.text.tag_add( newTagName, beg, end )
 427             self.text.tag_config( newTagName, **{ attributeName : attributeValue } )
 428    
 429    def tagNames( self, index1, index2=None ):
 430       tagNames = list(self.text.tag_names( index ))
 431       if 'sel' in styles:
 432          tagNames.remove( 'sel' )
 433       
 434       if not index2:
 435          return tagNames
 436       
 437       for action, tagName, index in self.text.dump( '1.0', Tk.END, tag=True ):
 438          if action == 'tagon':
 439             tagNames.append( tagName )
 440       
 441       # Rearrange the names into tag stack order
 442       result = [ ]
 443       for name in self.text.tag_names( ):
 444          if name in tagNames:
 445             result.append( name )
 446       
 447       return result
 448 
 449    def attributesAtIndex( self, index ):
 450       index = self.text.index( index )
 451       
 452       styles = self.tagNames( index )
 453       
 454       tagOpts  = { }
 455       fontOpts = { }
 456       
 457       for styleName in styles:
 458          if styleName.startswith( StylerFont.FONT_TAG_PREFIX ):
 459             font = StylerFont.getFont( styleName )
 460             tagOpts.update( **font.tagOptions() )
 461             fontOpts = font.fontOptions( )
 462          else:
 463             attribStyleName  = styleName.replace( '_', ' ' )
 464             attribName, attribValue = attribStyleName.split( '-' )[ 2: ]
 465             
 466             tagOpts[ attribName ] = attribValue
 467       
 468       return tagOpts, fontOpts
 469 
 470    def mapRegion( self, index1, index2 ):
 471       '''This method returns a map of the styles for a given region.  The result
 472       is a list of 'subregion' style descriptions of the form:
 473          ( beginIndex, endIndex, [ activeStyleNames ] )
 474       The list is guaranteed complete.  beginIndex of the first subregion
 475       description will always equal index1, and endIndex of the last subregion
 476       will always equal index2.  Further, for any two adjacent subregion
 477       descriptions endIndex of the first will always equal beginIndex of the
 478       second.  Thus no gaps occur in the map.
 479       '''
 480       # Build a complete dump of the region
 481       index1 = self.text.index( index1 )
 482       index2 = self.text.index( index2 )
 483       theDump = self.text.dump( index1, index2, tag=True )
 484       
 485       if len(theDump) == 0:
 486          return theDump
 487       
 488       # Consolidate the Dump
 489       finalReport  = [ ]
 490       
 491       activeTags   = list( self.text.tag_names( index1 ) )
 492       if 'sel' in activeTags:
 493          activeTags.remove( 'sel' )
 494       
 495       currentIndex = index1
 496       for action, tag, index in theDump:
 497          if tag == 'sel':
 498             continue
 499          
 500          if index != currentIndex:
 501             finalReport.append( ( currentIndex, index, copy.copy(activeTags) ) )
 502             currentIndex = index
 503          
 504          if action == 'tagon':
 505             if tag not in activeTags:
 506                activeTags.append( tag )
 507          elif action == 'tagoff':
 508             activeTags.remove( tag )
 509       
 510       if len(finalReport) == 0:
 511          finalReport = [ ( index1, index2, activeTags ) ]
 512       
 513       else:
 514          if finalReport[-1][1] != index2:
 515             finalReport.append( ( finalReport[-1][1], index2, copy.copy(activeTags) ) )
 516       
 517       return finalReport
 518 
 519 
 520 class Edit( Tk.Frame ):
 521    NAME_COUNTER = 1
 522    
 523    def __init__( self, parent, **options ):
 524       Tk.Frame.__init__( self, parent )
 525       self.text               = None
 526       self.styler             = None
 527       
 528       self._images            = { }
 529       self._windows           = { }
 530       
 531       self._fgColorBtn        = None
 532       self._bgColorBtn        = None
 533       self._styleCombo        = None
 534       
 535       self._currentFontName   = Tk.StringVar( )
 536       self._currentFontSize   = Tk.StringVar( )
 537       self._currentFontOffset = Tk.StringVar( )
 538       self._currentFGStipple  = Tk.StringVar( )
 539       self._currentBGStipple  = Tk.StringVar( )
 540       self._currentBorder     = Tk.StringVar( )
 541       self._currentRelief     = Tk.StringVar( )
 542       self._currentJustify    = Tk.StringVar( )
 543       self._currentWrap       = Tk.StringVar( )
 544       self._currentLMargin1   = Tk.StringVar( )
 545       self._currentLMargin2   = Tk.StringVar( )
 546       self._currentRMargin    = Tk.StringVar( )
 547       self._currentSpacing1   = Tk.StringVar( )
 548       self._currentSpacing2   = Tk.StringVar( )
 549       self._currentSpacing3   = Tk.StringVar( )
 550       self._currentTabs       = Tk.StringVar( )
 551       self._currentStyle      = Tk.StringVar( )
 552       
 553       self.buildGUI( **options )
 554 
 555    def reinitialize( self, styles=None ):
 556       self.text.delete( '1.0', Tk.END )
 557       
 558       self.styler.reinitizlize( styles )
 559 
 560    # GUI Building
 561    def buildGUI( self, **options ):
 562       buildToolbars = True
 563       if 'toolbars' in options:
 564          buildToolbars = options['toolbars']
 565          del options['toolbars']
 566       
 567       # Build the main Text widget
 568       stxt = Tk.ScrolledText( self, scrollbar=Tk.AUTO )
 569       self.text = stxt.subwidget( 'text' )
 570       self.text.configure( **options )
 571       self.styler = Styler( self.text )
 572       
 573       # Build the toolbars
 574       if buildToolbars:
 575          tb1Frame = Tk.Frame( self, relief=Tk.RAISED, borderwidth=2 )
 576          tb2Frame = Tk.Frame( self, relief=Tk.RAISED, borderwidth=2 )
 577          paragraphFrame = self.paragraphAttributeToolbar( tb1Frame )
 578          fontFrame      = self.fontAttributeToolbar( tb2Frame )
 579          objectFrame    = self.objectToolbar( tb2Frame )
 580          
 581          # Pack Everything
 582          paragraphFrame.pack( side=Tk.LEFT, fill=Tk.Y, padx=4, pady=2 )
 583          fontFrame.pack( side=Tk.LEFT, fill=Tk.Y, padx=4, pady=2 )
 584          objectFrame.pack( side=Tk.LEFT, fill=Tk.Y, padx=4, pady=2 )
 585          tb1Frame.pack( side=Tk.TOP, fill=Tk.X, padx=2, pady=2 )
 586          tb2Frame.pack( side=Tk.TOP, fill=Tk.X, padx=2, pady=2 )
 587       
 588       stxt.pack( side=Tk.TOP, fill=Tk.BOTH )
 589    
 590    def toolbarNames( self ):
 591       return [ 'fontAttributes',
 592                'paragraphAttributes',
 593                'objects' ]
 594    
 595    def fontAttributeToolbar( self, parent ):
 596       # Create the toolbar widgets
 597       fontFrame = Tk.Frame( parent, borderwidth=2, relief=Tk.GROOVE )
 598       fontFamilyCombo = Tk.ComboBox( fontFrame, editable=False, dropdown=True,
 599                     fancy=False, variable=self._currentFontName,
 600                     history=False, selectmode=Tk.BROWSE, command=self.onFamilyChosen )
 601       fontFamilyCombo.subwidget('listbox').config( font=DIALOG_FONT, width=30, height=20 )
 602       fontFamilyCombo.subwidget('entry').  config( font=DIALOG_FONT )
 603       fontFamilyCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 604       fontSizeCombo   = Tk.ComboBox( fontFrame, editable=False, dropdown=True,
 605                     fancy=False, variable=self._currentFontSize,
 606                     history=False, selectmode=Tk.BROWSE, command=self.onSizeChosen )
 607       fontSizeCombo.subwidget('listbox').config( font=DIALOG_FONT, width=3 )
 608       fontSizeCombo.subwidget('entry').  config( font=DIALOG_FONT, width=3 )
 609       fontSizeCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 610       Tk.Button( fontFrame, relief=Tk.GROOVE, text='B', font=DIALOG_FONT + ' bold', command=self.onBold ).pack( side=Tk.LEFT, padx=2, pady=2 )
 611       Tk.Button( fontFrame, relief=Tk.GROOVE, text='I', font=DIALOG_FONT + ' italic', command=self.onItalic ).pack( side=Tk.LEFT, padx=2, pady=2 )
 612       Tk.Button( fontFrame, relief=Tk.GROOVE, text='U', font=DIALOG_FONT + ' underline',command=self.onUnderline ).pack(side=Tk.LEFT, padx=2, pady=2)
 613       Tk.Button( fontFrame, relief=Tk.GROOVE, text='O', font=DIALOG_FONT + ' overstrike',command=self.onOverstrike ).pack(side=Tk.LEFT, padx=2, pady=2)
 614       offsetCombo = Tk.ComboBox( fontFrame, editable=False, dropdown=True,
 615                     fancy=False, variable=self._currentFontOffset,
 616                     history=False, selectmode=Tk.BROWSE, command=self.onOffsetChosen )
 617       offsetCombo.subwidget('listbox').config( font=DIALOG_FONT, width=9 )
 618       offsetCombo.subwidget('entry').  config( font=DIALOG_FONT, width=9 )
 619       offsetCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 620       self._fgColorBtn = Tk.Button( fontFrame, relief=Tk.GROOVE, text='A', font=DIALOG_FONT + ' bold', command=self.onChangeFG )
 621       self._fgColorBtn.pack( side=Tk.LEFT, padx=2, pady=2 )
 622       self._bgColorBtn = Tk.Button( fontFrame, relief=Tk.GROOVE, text='A', font=DIALOG_FONT + ' bold', command=self.onChangeBG )
 623       self._bgColorBtn.pack( side=Tk.LEFT, padx=2, pady=2 )
 624       fgStippleCombo = Tk.ComboBox( fontFrame, editable=False, label='fg', dropdown=True,
 625                     fancy=False, variable=self._currentFGStipple,
 626                     history=False, selectmode=Tk.BROWSE, command=self.onFGStippleChosen )
 627       fgStippleCombo.subwidget('listbox').config( font=DIALOG_FONT, width=7 )
 628       fgStippleCombo.subwidget('entry').  config( font=DIALOG_FONT, width=7 )
 629       fgStippleCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 630       bgStippleCombo = Tk.ComboBox( fontFrame, editable=False, label='bg', dropdown=True,
 631                     fancy=False, variable=self._currentBGStipple,
 632                     history=False, selectmode=Tk.BROWSE, command=self.onBGStippleChosen )
 633       bgStippleCombo.subwidget('listbox').config( font=DIALOG_FONT, width=7 )
 634       bgStippleCombo.subwidget('entry').  config( font=DIALOG_FONT, width=7 )
 635       bgStippleCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 636       Tk.Label( fontFrame, text='border' ).pack( side=Tk.LEFT, padx=2, pady=2 )
 637       le = Tk.Entry( fontFrame, width=4, textvariable=self._currentBorder )
 638       le.bind( '<FocusOut>', self.onBorderChange )
 639       le.pack( side=Tk.LEFT, padx=2, pady=2 )
 640       reliefCombo = Tk.ComboBox( fontFrame, editable=False, dropdown=True,
 641                     fancy=False, variable=self._currentRelief,
 642                     history=False, selectmode=Tk.BROWSE, command=self.onReliefChosen )
 643       reliefCombo.subwidget('listbox').config( font=DIALOG_FONT, width=7 )
 644       reliefCombo.subwidget('entry').  config( font=DIALOG_FONT, width=7 )
 645       reliefCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 646       
 647       # Populate the widgets
 648       familyList = list(Font.families( ))
 649       familyList.sort()
 650       for family in familyList:
 651          if family[0] == '@':
 652             continue
 653          fontFamilyCombo.insert( Tk.END, family )
 654       self._currentFontName.set( StylerFont.DEFAULT_FONT['family'] )
 655       
 656       for size in range( 6,31 ):
 657          fontSizeCombo.insert( Tk.END, str(size) )
 658       self._currentFontSize.set( str(StylerFont.DEFAULT_FONT['size']) )
 659       
 660       for offset in [ 'normal', 'superscript', 'subscript' ]:
 661          offsetCombo.insert( Tk.END, offset )
 662       self._currentFontOffset.set( 'normal' )
 663       
 664       for stippling in [ 'gray12', 'gray25', 'gray50', 'gray75' ]:
 665          fgStippleCombo.insert( Tk.END, stippling )
 666          bgStippleCombo.insert( Tk.END, stippling )
 667       
 668       for relief in [ 'flat', 'sunken', 'raised', 'groove', 'ridge', 'solid' ]:
 669          reliefCombo.insert( Tk.END, relief )
 670       
 671       self._fgColorBtn.config( foreground=self.text['foreground'] )
 672       self._bgColorBtn.config( background=self.text['background'] )
 673       
 674       return fontFrame
 675    
 676    def paragraphAttributeToolbar( self, parent ):
 677       # Create the toolbar widgets
 678       paragraphFrame = Tk.Frame( parent, borderwidth=2, relief=Tk.GROOVE )
 679       justifyCombo = Tk.ComboBox( paragraphFrame, editable=False, dropdown=True,
 680                     fancy=False, variable=self._currentJustify,
 681                     history=False, selectmode=Tk.BROWSE, command=self.onJustifyChosen )
 682       justifyCombo.subwidget('listbox').config( font=DIALOG_FONT, width=5 )
 683       justifyCombo.subwidget('entry').  config( font=DIALOG_FONT, width=5 )
 684       justifyCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 685       wrapCombo = Tk.ComboBox( paragraphFrame, editable=False, dropdown=True,
 686                     fancy=False, variable=self._currentWrap,
 687                     history=False, selectmode=Tk.BROWSE, command=self.onWrapChosen )
 688       wrapCombo.subwidget('listbox').config( font=DIALOG_FONT, width=5 )
 689       wrapCombo.subwidget('entry').  config( font=DIALOG_FONT, width=5 )
 690       wrapCombo.pack( side=Tk.LEFT, padx=2, pady=2 )
 691       le = Tk.LabelEntry( paragraphFrame, label='L-Margin1' )
 692       le.label.configure( font=DIALOG_FONT )
 693       le.entry.configure( width=4, textvariable=self._currentLMargin1 )
 694       le.entry.bind( '<FocusOut>', self.onLMargin1Change )
 695       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 696       le = Tk.LabelEntry( paragraphFrame, label='L-Margin2' )
 697       le.label.configure( font=DIALOG_FONT )
 698       le.entry.configure( width=4, textvariable=self._currentLMargin2 )
 699       le.entry.bind( '<FocusOut>', self.onLMargin2Change )
 700       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 701       le = Tk.LabelEntry( paragraphFrame, label='R-Margin' )
 702       le.label.configure( font=DIALOG_FONT )
 703       le.entry.configure( width=4, textvariable=self._currentRMargin )
 704       le.entry.bind( '<FocusOut>', self.onRMarginChange )
 705       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 706       le = Tk.LabelEntry( paragraphFrame, label='Spacing1' )
 707       le.label.configure( font=DIALOG_FONT )
 708       le.entry.configure( width=4, textvariable=self._currentSpacing1 )
 709       le.entry.bind( '<FocusOut>', self.onSpacing1Change )
 710       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 711       le = Tk.LabelEntry( paragraphFrame, label='Spacing2' )
 712       le.label.configure( font=DIALOG_FONT )
 713       le.entry.configure( width=4, textvariable=self._currentSpacing2 )
 714       le.entry.bind( '<FocusOut>', self.onSpacing2Change )
 715       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 716       le = Tk.LabelEntry( paragraphFrame, label='Spacing3' )
 717       le.label.configure( font=DIALOG_FONT )
 718       le.entry.configure( width=4, textvariable=self._currentSpacing3 )
 719       le.entry.bind( '<FocusOut>', self.onSpacing3Change )
 720       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 721       le = Tk.LabelEntry( paragraphFrame, label='Tabs' )
 722       le.label.configure( font=DIALOG_FONT )
 723       le.entry.configure( textvariable=self._currentTabs )
 724       le.entry.bind( '<FocusOut>', self.onTabChange )
 725       le.pack( side=Tk.LEFT, padx=5, pady=5 )
 726       Tk.Button( paragraphFrame, font=DIALOG_FONT, text='Apply', command=self.onApplyParagraph ).pack( side=Tk.LEFT, padx=5, pady=5 )
 727       
 728       # Populate the widgets
 729       for justify in [ Tk.LEFT, Tk.CENTER, Tk.RIGHT ]:
 730          justifyCombo.insert( Tk.END, justify )
 731       self._currentJustify.set( Tk.LEFT )
 732       
 733       for wrap in [ Tk.NONE, Tk.CHAR, Tk.WORD ]:
 734          wrapCombo.insert( Tk.END, wrap )
 735       self._currentWrap.set( self.text[ 'wrap' ] )
 736       
 737       return paragraphFrame
 738    
 739    def objectToolbar( self, parent ):
 740       # Create the toolbar widgets
 741       objectFrame = Tk.Frame( parent, borderwidth=2, relief=Tk.GROOVE )
 742       
 743       Tk.Button( objectFrame, font=DIALOG_FONT, text='image', command=self.onInsertImage ).pack( side=Tk.LEFT, padx=2, pady=2 )
 744       Tk.Button( objectFrame, font=DIALOG_FONT, text='list',  command=self.onInsertList  ).pack( side=Tk.LEFT, padx=2, pady=2 )
 745       Tk.Button( objectFrame, font=DIALOG_FONT, text='table', command=self.onInsertTable ).pack( side=Tk.LEFT, padx=2, pady=2 )
 746       
 747       return objectFrame
 748    
 749    # Content
 750    def insert_image( self, index, filename=None, **options ):
 751       if filename:
 752          theImage = ImageFactory.makeImage( filename )
 753          
 754          if filename not in self._images:
 755             self._images[ filename ] = Tk.PhotoImage( file=filename )
 756          
 757          options[ 'image' ] = self._images[ filename ]
 758       
 759       if 'name' in options:
 760          theImageName = options[ 'name' ]
 761       else:
 762          theImageName = self._generateImageName( )
 763          options[ 'name' ] = theImageName
 764       
 765       self._images[ theImageName ] = [ filename, options ] 
 766       
 767       self.text.image_create( index, **options )
 768       #self.text.tag_config( theImageName, offset='-3i' )
 769       
 770       return theImageName
 771    
 772    def delete( self, index1, index2=None ):
 773       self.text.delete( index1, index2 )
 774    
 775    def image_configure( self, imageName, **options ):
 776       self._image[ imageName ][1].update( options )
 777       self.text.image_configure( imageName, **options )
 778 
 779    def image_configuration( self, index ):
 780       return self.text.image_configure( )
 781 
 782    def image_cget( self, index, option ):
 783       return self.text.image_cget( index, option )
 784 
 785    def image_names( self ):
 786       return self.text.image_names( )
 787 
 788    def getModel( self ):
 789       content = self.text.dump( '1.0', Tk.END )
 790       styles  = self.styler.styles( )
 791       images  = self._images
 792       windows = self._windows
 793       
 794       return content,styles,images,windows
 795 
 796    def setModel( self, content, styles=None, images=None, windows=None ):
 797       self.reinitialize( )
 798       
 799       if images:
 800          self._images = images
 801       
 802       if isinstance( content, (str,unicode) ):
 803          self.insert( '1.0', content )
 804       else:
 805          tags = { }
 806          for element in content:
 807             action = element[0]
 808             value  = element[1]
 809             index  = self.text.index( Tk.INSERT )
 810             
 811             if action == 'tagon':
 812                if value == 'sel':
 813                   continue
 814                tags[ value ] = index
 815             elif action == 'tagoff':
 816                if value == 'sel':
 817                   continue
 818                elif value not in tags:
 819                   raise Exception( 'tagoff not preceded by tagon.' )
 820                
 821                regionBegin = tags[ value ]
 822                regionEnd   = index
 823                tagName     = value
 824                
 825                try:
 826                   newTagOpts = self.styler.styleDefinition( tagName )
 827                except:
 828                   newStyle = Style.getStyle( tagName )
 829                   self.text.tag_add( tagName, regionBegin, regionEnd )
 830                   self.text.tag_config( tagName, **newStyle.tagOptions() )
 831                
 832                self.text.tag_add( tagName, regionBegin, regionEnd )
 833                
 834                del tags[ value ]
 835             elif action == 'text':
 836                self.text.insert( Tk.END, value )
 837             elif action == 'mark':
 838                if value in ( Tk.INSERT, Tk.CURRENT, Tk.SEL, Tk.SEL_FIRST, Tk.SEL_LAST, Tk.END, Tk.ANCHOR ):
 839                   continue
 840                self.text.mark_set( index, value )
 841             elif action == 'image':
 842                imageName = value
 843                filename,options = self._images[ imageName ]
 844                if ('name' in options) and options['name'].startswith( '_image_' ):
 845                   del options['name']
 846                self.insert_image( Tk.END, filename, **options )
 847             elif action == 'window':
 848                pass
 849             else:
 850                raise Exception( 'Unknown Action.' )
 851 
 852    def _generateImageName( self ):
 853       imageNameTemplate = '_image_%d'
 854       
 855       self.NAME_COUNTER += 1
 856       return imageNameTemplate % self.NAME_COUNTER
 857 
 858    # Event Handling
 859    def onFamilyChosen( self, familyName ):
 860       family = self._currentFontName.get( )
 861       if family and (family != ''):
 862          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'family', family )
 863    
 864    def onSizeChosen( self, size ):
 865       size = self._currentFontSize.get( )
 866       if size and (size != ''):
 867          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'size', size )
 868    
 869    def onBold( self ):
 870       tagOpts, fontOpts = self.styler.attributesAtIndex( Tk.SEL_FIRST )
 871       try:
 872          if fontOpts[ 'weight' ] == Font.BOLD:
 873             newValue = Font.NORMAL
 874          else:
 875             newValue = Font.BOLD
 876       except:
 877          newValue = Font.BOLD
 878       
 879       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'weight', newValue )
 880    
 881    def onItalic( self ):
 882       tagOpts, fontOpts = self.styler.attributesAtIndex( Tk.SEL_FIRST )
 883       try:
 884          if fontOpts[ 'slant' ] == Font.ITALIC:
 885             newValue = Font.ROMAN
 886          else:
 887             newValue = Font.ITALIC
 888       except:
 889          newValue = Font.ITALIC
 890       
 891       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'slant', newValue )
 892    
 893    def onUnderline( self ):
 894       tagOpts,fontOpts = self.styler.attributesAtIndex( Tk.SEL_FIRST )
 895       try:
 896          newValue = tagOpts[ 'underline' ]
 897          if newValue in [ True, '1', 'true', 'True' ]:
 898             newValue = False
 899          else:
 900             newValue = True
 901       except:
 902          newValue = True
 903       
 904       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'underline', newValue )
 905    
 906    def onOverstrike( self ):
 907       tagOpts, fontOpts = self.styler.attributesAtIndex( Tk.SEL_FIRST )
 908       try:
 909          newValue = tagOpts[ 'overstrike' ]
 910          if newValue in [ True, '1', 'true', 'True' ]:
 911             newValue = False
 912          else:
 913             newValue = True
 914       except:
 915          newValue = True
 916       
 917       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'overstrike', newValue )
 918    
 919    def onOffsetChosen( self, offset ):
 920       if offset == '':
 921          return
 922       
 923       try:
 924          index1 = self.text.index( 'sel.first' )
 925          index2 = self.text.index( 'sel.last'  )
 926       except:
 927          return
 928       
 929       self.styler.applyStyleAttribute( index1, index2, 'offset', offset )
 930    
 931    def onFGStippleChosen( self, stipple=None ):
 932       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'fgstipple', self._currentFGStipple.get() )
 933    
 934    def onBGStippleChosen( self, stipple ):
 935       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'bgstipple', self._currentBGStipple.get() )
 936    
 937    def onBorderChange( self, border ):
 938       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'borderwidth', self._currentBorder.get() )
 939    
 940    def onReliefChosen( self, border ):
 941       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'relief', self._currentRelief.get() )
 942    
 943    def onLMargin1Change( self, value=None ):
 944       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'lmargin1', self._currentLMargin1.get() )
 945    
 946    def onLMargin2Change( self, value=None ):
 947       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'lmargin2', self._currentLMargin2.get() )
 948    
 949    def onRMarginChange( self, value=None ):
 950       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'rmargin', self._currentRMargin.get() )
 951    
 952    def onSpacing1Change( self, value=None ):
 953       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'spacing1', self._currentSpacing1.get() )
 954    
 955    def onSpacing2Change( self, value=None ):
 956       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'spacing2', self._currentSpacing2.get() )
 957    
 958    def onSpacing3Change( self, value=None ):
 959       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'spacing3', self._currentSpacing3.get() )
 960    
 961    def onTabChange( self, value=None ):
 962       self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'tabs', self._currentTabs.get() )
 963    
 964    def onApplyParagraph( self ):
 965       self.onLMargin1Change( )
 966       self.onLMargin2Change( )
 967       self.onRMarginChange( )
 968       self.onSpacing1Change( )
 969       self.onSpacing2Change( )
 970       self.onSpacing3Change( )
 971       self.onTabChange( )
 972    
 973    def onChangeFG( self ):
 974       import tkColorChooser
 975       newColor = tkColorChooser.askcolor( parent=self )[1]
 976       if newColor:
 977          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'foreground', newColor )
 978    
 979    def onChangeBG( self ):
 980       import tkColorChooser
 981       newColor = tkColorChooser.askcolor( parent=self )[1]
 982       if newColor:
 983          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'background', newColor )
 984    
 985    def onJustifyChosen( self, justify ):
 986       justify = self._currentJustify.get( )
 987       if justify and (justify != ''):
 988          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'justify', justify )
 989    
 990    def onWrapChosen( self, wrap ):
 991       wrap = self._currentWrap.get( )
 992       if wrap and (wrap != ''):
 993          self.styler.applyStyleAttribute( Tk.SEL_FIRST, Tk.SEL_LAST, 'wrap', wrap )
 994    
 995    def onInsertImage( self ):
 996       import tkFileDialog
 997       import pickle
 998       filename = tkFileDialog.askopenfilename( parent=self, filetypes=[ ( 'GIF Image', '*.gif' ) ] )
 999       if not filename or (filename == ''):
1000          return
1001       
1002       self.insert_image( Tk.INSERT, filename=filename )
1003 
1004    def onInsertList( self ):
1005       pass
1006    
1007    def onInsertTable( self ):
1008       pass
1009    
1010 
1011 class WP( Tk.Tk ):
1012    def __init__( self ):
1013       Tk.Tk.__init__( self )
1014       
1015       self._toolBar    = Tk.Frame( self )
1016       self._openBtn    = Tk.Button( self._toolBar, text='open', command=self.onOpen )
1017       self._openBtn.pack( side=Tk.LEFT )
1018       self._saveBtn    = Tk.Button( self._toolBar, text='save', command=self.onSave )
1019       self._saveBtn.pack( side=Tk.LEFT )
1020       
1021       self._text       = Edit( self, font=DEFAULT_TEXT_FONT, toolbars=True )
1022       
1023       self._toolBar.pack( side=Tk.TOP, fill=Tk.X )
1024       self._text.pack( side=Tk.TOP, fill=Tk.X )
1025    
1026    def onOpen( self ):
1027       import tkFileDialog
1028       import pickle
1029       filename = tkFileDialog.askopenfilename( parent=self )
1030       if not filename or (filename == ''):
1031          return
1032       
1033       self._filename = filename
1034       theFile = file( filename, 'r' )
1035       docData = pickle.load( theFile )
1036       
1037       if isinstance( docData, (str,unicode) ):
1038          self._text.setModel( docData )
1039       else:
1040          self._text.setModel( *docData )
1041    
1042    def onSave( self ):
1043       import tkFileDialog
1044       import pickle
1045       filename = tkFileDialog.asksaveasfilename( parent=self )
1046       if not filename or (filename == ''):
1047          return
1048       
1049       theFile = file( filename, 'w' )
1050       docData = self._text.getModel( )
1051       pickle.dump( docData, theFile )
1052    
1053 
1054 if __name__=='__main__':
1055    wp = WP( )
1056    wp.mainloop( )

tkinter: StyledEditor (last edited 2010-07-26 11:59:11 by localhost)