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:
'family', attributeValue is a font family name
'size', attributeValue is a font point size
'weight', attributeValue is one of: 'normal', 'bold'
'bold', attributeValue is a boolean (alternative to using 'weight')
'slant', attributeValue is one of: 'roman', 'italic'
'italic', attributeValue is boolean (alternative to using 'slant')
Several previously existing options have some new values:
- 'spacing1', 'spacing2' and 'spacing3' may take 'None', 'Half Line', 'One Line' or 'Two Lines' in addition to any of the values acceptable by tag_config().
'offset' may take 'normal', 'superscript' or 'subscript' in addition to any value acceptable by tag_config().
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.
- The widget has a default styling (which assignes a value to every style attribute).
A non-StylerFont tag may configure exactly one styling attribute.
For any index in the underlying text widget, there may be at most one tag for any given non-StylerFont attribute.
For any index in the underlying text widget, there may be at most one tag handling a StylerFont.
- 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( )
1057