File: smgHtmlView.py

package info (click to toggle)
pyching 1.2.1-2
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 480 kB
  • ctags: 287
  • sloc: python: 4,066; makefile: 47; sh: 4
file content (421 lines) | stat: -rw-r--r-- 18,265 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
##---------------------------------------------------------------------------##
##
## Python/Tkinter base module/classes for a html viewer dialog 
##
## Copyright (C) 1999-2003 Stephen M. Gava
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be of some
## interest to somebody, but WITHOUT ANY WARRANTY; without even the 
## implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
## See the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; see the file COPYING or COPYING.txt. If not, 
##  write to the Free Software Foundation, Inc.,
## 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
## The license can also be found at the GNU/FSF website: http://www.gnu.org
##
## Stephen M. Gava
## <elguavas@users.sourceforge.net>
## http://pyching.sourgeforge.net/elguavas-soft.html
##
##---------------------------------------------------------------------------##
"""
tkinter html viewer dialog module
"""

#python library imports
import os, string, htmllib, formatter

#tkinter imports
from Tkinter import *
import tkMessageBox
import tkSimpleDialog

#smg library module imports
from smgDialog import smgDialog

class smgHtmlView(smgDialog):
    """
    display a html file (or a plain text file if plainText=1), or html data 
    from a string (which may be a repr of a function name). optionally show 
    an 'index' button, which jumps straight to indexFile (which can also be a 
    disk file or a string as above) if specified. only a small subset of html 
    tags are rendered. ony links to files, specified without any urltype, or 
    internal html and image data returned by functions are supported.
    """
    def __init__(self,parent,title=None,htmlSource=None,sourceIsStr=1,
                internalLink=None,index=None,plainText=0,modal=1,hexBrowser=0,
                imageModule=None,bg='#e8e8e8',fg='#000000'):
        """
        title - string, dialog title
        htmlSource - either a filename or a string containing html data
        sourceIsStr - bool - true if htmlSource is an html string, or a string
                      repr of a function name that will return an html string,
                      false if htmSource is a filename
        internalLink - an url to jump to within the source (not yet implemented)
        index - either a filename or a function that returns html data for the index
        plainText - boolean, true if source is plain text rather than html
        modal - boolean, true if viewer dialog should be modal
        imageModule - string, name of internal image data module
        bg, fg - background and foreground colours of html display area
        """
        self.colorViewerFg = fg
        self.colorViewerBg = bg
        self.index = index
        self.plainText = plainText
        self.hexNum=0
        if hexBrowser:
            htmlSource=self.MakeBrowseSource(1) #start browser at hexagram 1
        self.htmlSource = htmlSource
        self.hexBrowser=hexBrowser
        self.sourceIsStr=sourceIsStr
        self.internalLink = internalLink
        self.internalImageExt = '.#@~' #hack to indicate internal image data
        
        #if imageModule: #import image data module, if any
        #  eval('import ' + imageModule)
        
        #if self.htmlFile: #open disk file 
        #  self.displayFile = self.openDataFile(htmlFile)
        #else: #import html data module
        #  import self.displayFile
        #  #if any image data import it
        #  if self.imageData: import self.imageData 
        
        #configure buttons
        if not hexBrowser: #regular buttons
            btns=[{'name':'buttonOk','title':'Ok','binding':'Ok',
                        'underline':None,
                        'hotKey':'<Return>'}]
            if self.index: #we want an index button
                btns.append({'name':'buttonIndex','title':'Index',
                            'binding':'Index','underline':0,
                            'hotKey':'<Alt-i>'})
        else: #special hexagram info browser buttons
            btns=[  {'name':'buttonPrev','title':'< Prev',
                            'binding':'Prev','underline':2,
                            'hotKey':'<Alt-p>'},
                    {'name':'buttonNext','title':'Next >',
                            'binding':'Next','underline':0,
                            'hotKey':'<Alt-n>'},   
                    {'name':'buttonGoTo','title':'Go To Number',
                            'binding':'GoTo','underline':0,
                            'hotKey':'<Alt-g>'},
                    {'name':'buttonQuit','title':'Quit',
                            'binding':'Cancel','underline':0,
                            'hotKey':'<Alt-q>'}  ]

        smgDialog.__init__(self,parent,title=title, buttons=btns,
                    buttonsDef=-1, buttonsWidth=0, buttonsPad=5, 
                    resizeable=1, transient=1, wait=1) #buttonsPos='BOTTOM'
 
    #def ImportDataModules(self, dataModules=[]):
    #  """
    #  dataModules - a list of strings containing the names of the
    #                modules to be imported
    #  """
    #  for module in dataModules:
    #    exec 'import ' + module
    
    def Index(self, event=None):
        #this may be called by a key binding, thus the 'event=None' and the 
        #enabled check
        if self.buttonIndex.cget('state') in ('normal','active'): #if index button enabled
            #print self.buttonIndex.cget('state')
            self.showHtml(self.index)

    def Prev(self, event=None):
        #this may be called by a key binding, thus the 'event=None' and the 
        #enabled check
        if self.buttonPrev.cget('state') in ('normal','active'): #if index button enabled
            #print self.buttonIndex.cget('state')
            self.showHtml(self.MakeBrowseSource(self.hexNum-1))

    def Next(self, event=None):
        #this may be called by a key binding, thus the 'event=None' and the 
        #enabled check
        if self.buttonNext.cget('state') in ('normal','active'): #if index button enabled
            #print self.buttonIndex.cget('state')
            self.showHtml(self.MakeBrowseSource(self.hexNum+1))

    def GoTo(self, event=None):
        #this may be called by a key binding, thus the 'event=None' and the 
        #enabled check
        if self.buttonGoTo.cget('state') in ('normal','active'): #if index button enabled
            #print self.buttonIndex.cget('state')
            hexNum=tkSimpleDialog.askinteger('Go To Hexagram Number', 
                    'Enter hexagram number.',
                    parent=self,initialvalue=self.hexNum,
                    minvalue=1,maxvalue=64)
            self.showHtml(self.MakeBrowseSource(hexNum))

    def MakeBrowseSource(self,hexNum):
        self.hexNum=hexNum
        return 'pyching_int_data.in%sdata()'%(hexNum)
    
    def openDataFile(self, fileName):
        displayFile = None
        try:
            displayFile = open(fileName, 'r')
        except IOError:
            tkMessageBox.showerror(title='File Load Error',
                    message='Unable to load data file '+`fileName`+' .')
        #else:
        return displayFile #will be = None if there was an error
    
    def Body(self,master):
        self.configure(borderwidth=4)
        # create the text widget for html display
        baseFont = ("Times", 12)
        if os.name == "nt": baseFont = ("Times New Roman", 12)
        self.textDisplay = Text(master,height=20,width=74,wrap=WORD,
                insertofftime=0,font=baseFont,highlightthickness=0,
                padx=4,pady=8,fg=self.colorViewerFg,bg=self.colorViewerBg)
        scrollbarY=Scrollbar(master,orient=VERTICAL,width=13,
                highlightthickness=0,command=self.textDisplay.yview)
        #scrollbarX=Scrollbar(master,orient=HORIZONTAL,width=15,highlightthickness=0,
        #        command=textDisplay.xview)
        #textDisplay.configure(yscrollcommand=scrollbarY.set,
        #               xscrollcommand=scrollbarX.set)
        self.textDisplay.configure(yscrollcommand=scrollbarY.set)
        #scrollbarX.grid(row=1,column=0,sticky=(E,W))
        scrollbarY.grid(row=0,column=1,sticky=(N,S))
        
        self.textDisplay.grid(row=0,column=0,sticky=(N,S,E,W))
        master.grid_location(0,0)
        master.columnconfigure(0,weight=1)
        master.rowconfigure(0,weight=1)
        self.showHtml(source=self.htmlSource,iLink=self.internalLink,
                    plainText=self.plainText)
        
        return self.textDisplay

    def showImage(self,source,alt,align):
        #print source,source[-4:],source[:-4],alt,align
        imageType = source[-4:] #image type indicator
        if imageType[-2:] == '()': #internal image data 
            try:
                exec ( 'import ' + string.split(source,'.',1)[0] )
                self.images.append(Image('photo', data=eval(source) ) )
            except (NameError,AttributeError): #no such image data
                self.textDisplay.insert("insert", ' [image error] ')
                print "no such image data:", source
                return #get out
        else: #image file stored on disk
            if imageType in ('.gif','.xbm'): #supported disk file image formats
                try:
                    if imageType == '.gif': #a gif file
                        self.images.append(Image('photo',file=source) )
                    elif imageCheck == '.xbm': #an x bitmap file  
                        self.images.append(Image('bitmap',file=source) )
                except TclError: #most likely no such image file
                    self.textDisplay.insert("insert", ' [image error] ')
                    print "image display error:", source
                    return #get out
            else: #can't handle this image type
                self.textDisplay.insert("insert", ' [unknown image type] ') #:'+source+'
                print "can't display image type:", source
                return #skip the image creation
        #if we got here then insert the new image in the document
        self.textDisplay.image_create(index='insert',
                    image=self.images[(len(self.images)-1)],padx=10,pady=10)
    
    def showHtml(self,source=None,iLink=None,plainText=0):
        self.images = [] #holds image references for this document 
                         #(used above in showImages)
        htmlData='[html data error]'
        if self.sourceIsStr:
            #if source ends in '()' it is a string representing a function name
            #that will return the html data string
            #print source #debug
            sourceIsData = ( source[-2:] == '()' )
            if sourceIsData: 
                try:
                    #module = 
                    exec ( 'import ' + string.split(source,'.',1)[0] )
                    exec ( 'htmlData = ' + source )       
                except (ImportError,NameError,AttributeError): #no such html data
                    self.textDisplay.insert("insert", ' [hypertext data error] ')
                    print "html data module or function error:", source
#                 return #get out
            else: #the source is a plain string holding html data
               htmlData=source
        else:
            displayFile = None
            displayFile = self.openDataFile(source) #open disk file
            htmlData = displayFile.read()
        if self.sourceIsStr or displayFile:
            self.oldCursor = self.cget("cursor")
            self.textDisplay.config(cursor="watch")
            self.textDisplay.update_idletasks()
            self.config(cursor="watch")
            self.update_idletasks()
            self.textDisplay.config(state=NORMAL)
            self.textDisplay.delete("1.0", "end")
            if self.sourceIsStr or (not plainText): #render html
                htmlWriter = HtmlWriter(self.textDisplay, self)
                htmlFormatter = formatter.AbstractFormatter(htmlWriter)
                htmlParser = HtmlParser(htmlFormatter)
                #print source #debug
                htmlParser.feed(htmlData)
                htmlParser.close()
            else: #show plain text
                self.textDisplay.insert(1.0,htmlData)  
            self.textDisplay.configure(state=DISABLED)
            self.textDisplay.config(cursor=self.oldCursor)
            self.config(cursor=self.oldCursor)
        else: #no html data
            self.textDisplay.insert("insert", ' [hypertext data error] ')
            print "no html data available:", source
            return #get out

        if self.index: #we have an index button
            if source == self.index: #disable index button, this _is_ the index
                self.buttonIndex.configure(state=DISABLED)
            else: #enable index button
                self.buttonIndex.configure(state=NORMAL)
        
        if self.hexBrowser: #hex broser buttons        
            if self.hexNum == 1: #at 1st page
                self.buttonPrev.configure(state=DISABLED)
            else: #enable index button
                self.buttonPrev.configure(state=NORMAL)
            if self.hexNum == 64: #at last page
                self.buttonNext.configure(state=DISABLED)
            else: #enable index button
                self.buttonNext.configure(state=NORMAL)
        

class HtmlWriter(formatter.DumbWriter):
    def __init__(self, textWidget, htmlViewer):
        formatter.DumbWriter.__init__(self, self, maxcol=9999)
        self.textWidget = textWidget
        self.htmlViewer = htmlViewer
        font, size = "Times", 12
        fixed = "Courier"
        self.fontmap = {
                "h1"      : (font, size + 12, "bold"),
                "h2"      : (font, size +  7, "bold"),
                "h3"      : (font, size +  4, "bold"),
                "h4"      : (font, size +  2, "bold"),
                "h5"      : (font, size +  2, "bold"),
                "h6"      : (font, size +  1, "bold"),
                "bold"    : (font, size, "bold"),
                "italic"  : (font, size, "italic"),
                "pre"     : (fixed, size),
        }
        for f in self.fontmap.keys():
                self.textWidget.tag_config(f, font=self.fontmap[f])
        self.anchor = None
        self.anchor_mark = None
        self.font = None
        self.font_mark = None
        self.indent = ""

    def handleImage(self, source, alt, align):
        self.htmlViewer.showImage(source, alt, align)
    
    def createCallback(self, href):
        class Functor:
            def __init__(self, htmlViewer, arg):
                self.viewer = htmlViewer
                self.arg = arg
            def __call__(self, *args):
                #self.viewer.updateHistoryXYView()
                return self.viewer.showHtml(self.arg)
        return Functor(self.htmlViewer, href)

    def write(self, data):
        self.textWidget.insert("insert", data)

    def __write(self, data):
        self.textWidget.insert("insert", data)

    def anchor_bgn(self, href, name, type):
        if href:
            #self.text.update_idletasks()   # update display during parsing
            self.anchor = (href, name, type)
            self.anchor_mark = self.textWidget.index("insert")

    def anchor_end(self):
        if self.anchor:
            url = self.anchor[0]
            tag = "href_" + url
            self.textWidget.tag_add(tag, self.anchor_mark, "insert")
            self.textWidget.tag_bind(tag, "<ButtonPress>", self.createCallback(url))
            self.textWidget.tag_bind(tag, "<Enter>", self.anchor_enter)
            self.textWidget.tag_bind(tag, "<Leave>", self.anchor_leave)
            self.textWidget.tag_config(tag, foreground="blue", underline=1)
            self.anchor = None

    def anchor_enter(self, *args):
        self.textWidget.config(cursor = "hand2")

    def anchor_leave(self, *args):
        self.textWidget.config(cursor = self.htmlViewer.oldCursor)

    def new_font(self, font):
        # end the current font
        if self.font:
                self.textWidget.tag_add(self.font, self.font_mark, "insert")
                self.font = None
        # start the new font
        if font:
                self.font_mark = self.textWidget.index("insert")
                if self.fontmap.has_key(font[0]):
                        self.font = font[0]
                elif font[3]:
                        self.font = "pre"
                elif font[2]:
                        self.font = "bold"
                elif font[1]:
                        self.font = "italic"
                else:
                        self.font = None

    def new_margin(self, margin, level):
        self.indent = "    " * level
        #print 'new_margin called'

    def send_label_data(self, data):
        self.__write(self.indent + data + " ")
        #print 'send_label_data called:', data

    def send_paragraph(self, blankline):
        if self.col > 0:
                self.__write("\n")
        if blankline > 0:
                self.__write("\n" * blankline)
        self.col = 0
        self.atbreak = 0
        #print 'send_paragraph called'

    def send_hor_rule(self, *args):
        width = int(int(self.textWidget["width"]) * 0.9)
        self.__write("_" * width)
        self.__write("\n")
        self.col = 0
        self.atbreak = 0
        #print 'send_hor_rule called'

class HtmlParser(htmllib.HTMLParser):
    def anchor_bgn(self, href, name, type):
        htmllib.HTMLParser.anchor_bgn(self, href, name, type)
        self.formatter.writer.anchor_bgn(href, name, type)

    def anchor_end(self):
        if self.anchor: self.anchor = None
        self.formatter.writer.anchor_end()

    def do_dt(self, attrs):
        self.formatter.end_paragraph(1)
        self.ddpop()
    
    def handle_image(self, source, alt, ismap, align, width, height):
        self.formatter.writer.handleImage(source, alt, align)