# Tk UI # Copyright (C) 2002, 2003 John Goerzen # # # 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 useful, # 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; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import nested_scopes from Tkinter import * import tkFont from threading import * import thread, traceback, time, threading from StringIO import StringIO from ScrolledText import ScrolledText from offlineimap import threadutil, version from Queue import Queue from UIBase import UIBase from offlineimap.ui.Blinkenlights import BlinkenBase usabletest = None class PasswordDialog: def __init__(self, accountname, config, master=None, errmsg = None): self.top = Toplevel(master) self.top.title(version.productname + " Password Entry") text = '' if errmsg: text = '%s: %s\n' % (accountname, errmsg) text += "%s: Enter password: " % accountname self.label = Label(self.top, text = text) self.label.pack() self.entry = Entry(self.top, show='*') self.entry.bind("", self.ok) self.entry.pack() self.entry.focus_force() self.button = Button(self.top, text = "OK", command=self.ok) self.button.pack() self.entry.focus_force() self.top.wait_window(self.label) def ok(self, args = None): self.password = self.entry.get() self.top.destroy() def getpassword(self): return self.password class TextOKDialog: def __init__(self, title, message, blocking = 1, master = None): if not master: self.top = Tk() else: self.top = Toplevel(master) self.top.title(title) self.text = ScrolledText(self.top, font = "Courier 10") self.text.pack() self.text.insert(END, message) self.text['state'] = DISABLED self.button = Button(self.top, text = "OK", command=self.ok) self.button.pack() if blocking: self.top.wait_window(self.button) def ok(self): self.top.destroy() class ThreadFrame(Frame): def __init__(self, master=None): self.threadextraframe = None self.thread = currentThread() self.threadid = thread.get_ident() Frame.__init__(self, master, relief = RIDGE, borderwidth = 2) self.pack(fill = 'x') self.threadlabel = Label(self, foreground = '#FF0000', text ="Thread %d (%s)" % (self.threadid, self.thread.getName())) self.threadlabel.pack() self.setthread(currentThread()) self.account = "Unknown" self.mailbox = "Unknown" self.loclabel = Label(self, text = "Account/mailbox information unknown") #self.loclabel.pack() self.updateloclabel() self.message = Label(self, text="Messages will appear here.\n", foreground = '#0000FF') self.message.pack(fill = 'x') def setthread(self, newthread): if newthread: self.threadlabel['text'] = newthread.getName() else: self.threadlabel['text'] = "No thread" self.destroythreadextraframe() def destroythreadextraframe(self): if self.threadextraframe: self.threadextraframe.destroy() self.threadextraframe = None def getthreadextraframe(self): if self.threadextraframe: return self.threadextraframe self.threadextraframe = Frame(self) self.threadextraframe.pack(fill = 'x') return self.threadextraframe def setaccount(self, account): self.account = account self.mailbox = "Unknown" self.updateloclabel() def setmailbox(self, mailbox): self.mailbox = mailbox self.updateloclabel() def updateloclabel(self): self.loclabel['text'] = "Processing %s: %s" % (self.account, self.mailbox) def appendmessage(self, newtext): self.message['text'] += "\n" + newtext def setmessage(self, newtext): self.message['text'] = newtext class VerboseUI(UIBase): def isusable(s): global usabletest if usabletest != None: return usabletest try: Tk().destroy() usabletest = 1 except TclError: usabletest = 0 return usabletest def _createTopWindow(self, doidlevac = 1): self.notdeleted = 1 self.created = threading.Event() self.af = {} self.aflock = Lock() t = threadutil.ExitNotifyThread(target = self._runmainloop, name = "Tk Mainloop") t.setDaemon(1) t.start() self.created.wait() del self.created if doidlevac: t = threadutil.ExitNotifyThread(target = self.idlevacuum, name = "Tk idle vacuum") t.setDaemon(1) t.start() def _runmainloop(s): s.top = Tk() s.top.title(version.productname + " " + version.versionstr) s.top.after_idle(s.created.set) s.top.mainloop() s.notdeleted = 0 def getaccountframe(s): accountname = s.getthreadaccount() s.aflock.acquire() try: if accountname in s.af: return s.af[accountname] s.af[accountname] = LEDAccountFrame(s.top, accountname, s.fontfamily, s.fontsize) finally: s.aflock.release() return s.af[accountname] def getpass(s, accountname, config, errmsg = None): pd = PasswordDialog(accountname, config, errmsg = errmsg) return pd.getpassword() def gettf(s, newtype=ThreadFrame, master = None): if master == None: master = s.top threadid = thread.get_ident() s.tflock.acquire() try: if threadid in s.threadframes: return s.threadframes[threadid] if len(s.availablethreadframes): tf = s.availablethreadframes.pop(0) tf.setthread(currentThread()) else: tf = newtype(master) s.threadframes[threadid] = tf return tf finally: s.tflock.release() def _display(s, msg): s.gettf().setmessage(msg) def threadExited(s, thread): threadid = thread.threadid s.tflock.acquire() if threadid in s.threadframes: tf = s.threadframes[threadid] tf.setthread(None) tf.setaccount("Unknown") tf.setmessage("Idle") s.availablethreadframes.append(tf) del s.threadframes[threadid] s.tflock.release() UIBase.threadExited(s, thread) def idlevacuum(s): while s.notdeleted: time.sleep(10) s.tflock.acquire() while len(s.availablethreadframes): tf = s.availablethreadframes.pop() tf.destroy() s.tflock.release() def threadException(s, thread): exceptionstr = s.getThreadExceptionString(thread) print exceptionstr s.top.destroy() s.top = None TextOKDialog("Thread Exception", exceptionstr) s.delThreadDebugLog(thread) s.terminate(100) def mainException(s): exceptionstr = s.getMainExceptionString() print exceptionstr s.top.destroy() s.top = None TextOKDialog("Main Program Exception", exceptionstr) def warn(s, msg, minor = 0): if minor: # Just let the default handler catch it UIBase.warn(s, msg, minor) else: TextOKDialog("OfflineIMAP Warning", msg) def showlicense(s): TextOKDialog(version.productname + " License", version.bigcopyright + "\n" + version.homepage + "\n\n" + version.license, blocking = 0, master = s.top) def init_banner(s): s.threadframes = {} s.availablethreadframes = [] s.tflock = Lock() s._createTopWindow() s._msg(version.productname + " " + version.versionstr + ", " +\ version.copyright) tf = s.gettf().getthreadextraframe() b = Button(tf, text = "About", command = s.showlicense) b.pack(side = LEFT) b = Button(tf, text = "Exit", command = s.terminate) b.pack(side = RIGHT) s.sleeping_abort = {} def deletingmessages(s, uidlist, destlist): ds = s.folderlist(destlist) s._msg("Deleting %d messages in %s" % (len(uidlist), ds)) def _sleep_cancel(s, args = None): s.sleeping_abort[thread.get_ident()] = 1 def sleep(s, sleepsecs): threadid = thread.get_ident() s.sleeping_abort[threadid] = 0 tf = s.gettf().getthreadextraframe() def sleep_cancel(): s.sleeping_abort[threadid] = 1 sleepbut = Button(tf, text = 'Sync immediately', command = sleep_cancel) sleepbut.pack() UIBase.sleep(s, sleepsecs) def sleeping(s, sleepsecs, remainingsecs): retval = s.sleeping_abort[thread.get_ident()] if remainingsecs: s._msg("Next sync in %d:%02d" % (remainingsecs / 60, remainingsecs % 60)) else: s._msg("Wait done; synchronizing now.") s.gettf().destroythreadextraframe() del s.sleeping_abort[thread.get_ident()] time.sleep(sleepsecs) return retval TkUI = VerboseUI ################################################## Blinkenlights class LEDAccountFrame: def __init__(self, top, accountname, fontfamily, fontsize): self.top = top self.accountname = accountname self.fontfamily = fontfamily self.fontsize = fontsize self.frame = Frame(self.top, background = 'black') self.frame.pack(side = BOTTOM, expand = 1, fill = X) self._createcanvas(self.frame) self.label = Label(self.frame, text = accountname, background = "black", foreground = "blue", font = (self.fontfamily, self.fontsize)) self.label.grid(sticky = E, row = 0, column = 1) def getnewthreadframe(s): return LEDThreadFrame(s.canvas) def _createcanvas(self, parent): c = LEDFrame(parent) self.canvas = c c.grid(sticky = E, row = 0, column = 0) parent.grid_columnconfigure(1, weight = 1) #c.pack(side = LEFT, expand = 0, fill = X) def startsleep(s, sleepsecs): s.sleeping_abort = 0 s.button = Button(s.frame, text = "Sync now", command = s.syncnow, background = "black", activebackground = "black", activeforeground = "white", foreground = "blue", highlightthickness = 0, padx = 0, pady = 0, font = (s.fontfamily, s.fontsize), borderwidth = 0, relief = 'solid') s.button.grid(sticky = E, row = 0, column = 2) def syncnow(s): s.sleeping_abort = 1 def sleeping(s, sleepsecs, remainingsecs): if remainingsecs: s.button.config(text = 'Sync now (%d:%02d remain)' % \ (remainingsecs / 60, remainingsecs % 60)) time.sleep(sleepsecs) else: s.button.destroy() del s.button return s.sleeping_abort class LEDFrame(Frame): """This holds the different lights.""" def getnewobj(self): retval = Canvas(self, background = 'black', height = 20, bd = 0, highlightthickness = 0, width = 10) retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0) return retval class LEDThreadFrame: """There is one of these for each little light.""" def __init__(self, master): self.canvas = master.getnewobj() self.color = '' self.ovalid = self.canvas.create_oval(4, 4, 9, 9, fill = 'gray', outline = '#303030') def setcolor(self, newcolor): if newcolor != self.color: self.canvas.itemconfigure(self.ovalid, fill = newcolor) self.color = newcolor def getcolor(self): return self.color def setthread(self, newthread): if newthread: self.setcolor('gray') else: self.setcolor('black') class Blinkenlights(BlinkenBase, VerboseUI): def __init__(s, config, verbose = 0): VerboseUI.__init__(s, config, verbose) s.fontfamily = 'Helvetica' s.fontsize = 8 if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'): s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily') if config.has_option('ui.Tk.Blinkenlights', 'fontsize'): s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize') def isusable(s): return VerboseUI.isusable(s) def _createTopWindow(self): VerboseUI._createTopWindow(self, 0) #self.top.resizable(width = 0, height = 0) self.top.configure(background = 'black', bd = 0) widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0") self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights", "loglines", 5) self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights", "bufferlines", 500) self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y', font = (self.fontfamily, self.fontsize), bd = 0, highlightthickness = 0, setgrid = 0, state = DISABLED, height = self.loglines, wrap = NONE, width = 60) self.text.vbar.configure(background = '#000050', activebackground = 'blue', highlightbackground = 'black', troughcolor = "black", bd = 0, elementborderwidth = 2) self.textenabled = 0 self.tags = [] self.textlock = Lock() def init_banner(s): BlinkenBase.init_banner(s) s._createTopWindow() menubar = Menu(s.top, activebackground = "black", activeforeground = "white", activeborderwidth = 0, background = "black", foreground = "blue", font = (s.fontfamily, s.fontsize), bd = 0) menubar.add_command(label = "About", command = s.showlicense) menubar.add_command(label = "Show Log", command = s._togglelog) menubar.add_command(label = "Exit", command = s.terminate) s.top.config(menu = menubar) s.menubar = menubar s.text.see(END) if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1): s._togglelog() s.gettf().setcolor('red') s.top.resizable(width = 0, height = 0) s._msg(version.banner) def _togglelog(s): if s.textenabled: s.oldtextheight = s.text.winfo_height() s.text.pack_forget() s.textenabled = 0 s.menubar.entryconfig('Hide Log', label = 'Show Log') s.top.update() s.top.geometry("") s.top.update() s.top.resizable(width = 0, height = 0) s.top.update() else: s.text.pack(side = TOP, expand = 1, fill = BOTH) s.textenabled = 1 s.top.update() s.top.geometry("") s.menubar.entryconfig('Show Log', label = 'Hide Log') s._rescroll() s.top.resizable(width = 1, height = 1) def sleep(s, sleepsecs): s.gettf().setcolor('red') s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) BlinkenBase.sleep(s, sleepsecs) def sleeping(s, sleepsecs, remainingsecs): return BlinkenBase.sleeping(s, sleepsecs, remainingsecs) def _rescroll(s): s.text.see(END) lo, hi = s.text.vbar.get() s.text.vbar.set(1.0 - (hi - lo), 1.0) def _display(s, msg): if "\n" in msg: for thisline in msg.split("\n"): s._msg(thisline) return #VerboseUI._msg(s, msg) color = s.gettf().getcolor() rescroll = 1 s.textlock.acquire() try: if s.text.vbar.get()[1] != 1.0: rescroll = 0 s.text.config(state = NORMAL) if not color in s.tags: s.text.tag_config(color, foreground = color) s.tags.append(color) s.text.insert(END, "\n" + msg, color) # Trim down. Not quite sure why I have to say 7 instead of 5, # but so it is. while float(s.text.index(END)) > s.bufferlines + 2.0: s.text.delete(1.0, 2.0) if rescroll: s._rescroll() finally: s.text.config(state = DISABLED) s.textlock.release()