##---------------------------------------------------------------------------## ## ## 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 ## ## 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':''}] if self.index: #we want an index button btns.append({'name':'buttonIndex','title':'Index', 'binding':'Index','underline':0, 'hotKey':''}) else: #special hexagram info browser buttons btns=[ {'name':'buttonPrev','title':'< Prev', 'binding':'Prev','underline':2, 'hotKey':''}, {'name':'buttonNext','title':'Next >', 'binding':'Next','underline':0, 'hotKey':''}, {'name':'buttonGoTo','title':'Go To Number', 'binding':'GoTo','underline':0, 'hotKey':''}, {'name':'buttonQuit','title':'Quit', 'binding':'Cancel','underline':0, 'hotKey':''} ] 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 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, "", self.createCallback(url)) self.textWidget.tag_bind(tag, "", self.anchor_enter) self.textWidget.tag_bind(tag, "", 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)