]> code.delx.au - offlineimap/blobdiff - offlineimap/head/offlineimap/ui/Tk.py
/offlineimap/head: changeset 367
[offlineimap] / offlineimap / head / offlineimap / ui / Tk.py
index 6b30dfcb5e17d05d49f809582d5c7aec88c18d7f..3e8201a8fc018c522cd69fa64b29c185eebbb20e 100644 (file)
@@ -1,5 +1,5 @@
 # Tk UI
-# Copyright (C) 2002 John Goerzen
+# Copyright (C) 2002, 2003 John Goerzen
 # <jgoerzen@complete.org>
 #
 #    This program is free software; you can redistribute it and/or modify
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import nested_scopes
+
 from Tkinter import *
 import tkFont
 from threading import *
-import thread, traceback, time
+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):
+    def __init__(self, accountname, config, master=None, errmsg = None):
         self.top = Toplevel(master)
         self.top.title(version.productname + " Password Entry")
-        self.label = Label(self.top,
-                           text = "%s: Enter password for %s on %s: " % \
-                           (accountname, config.get(accountname, "remoteuser"),
-                            config.get(accountname, "remotehost")))
+        text = ''
+        if errmsg:
+            text = '%s: %s\n' % (accountname, errmsg)
+        text += "%s: Enter password for %s on %s: " % \
+                (accountname, config.get(accountname, "remoteuser"),
+                 config.get(accountname, "remotehost"))
+        self.label = Label(self.top, text = text)
         self.label.pack()
 
         self.entry = Entry(self.top, show='*')
@@ -142,25 +150,32 @@ class ThreadFrame(Frame):
 
 class VerboseUI(UIBase):
     def isusable(s):
+        global usabletest
+        if usabletest != None:
+            return usabletest
+
         try:
             Tk().destroy()
-            return 1
+            usabletest = 1
         except TclError:
-            return 0
-
+            usabletest = 0
+        return usabletest
+    
     def _createTopWindow(self, doidlevac = 1):
-        self.top = Tk()
-        self.top.title(version.productname + " " + version.versionstr)
-        self.threadframes = {}
-        self.availablethreadframes = []
-        self.tflock = Lock()
         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")
@@ -168,11 +183,27 @@ class VerboseUI(UIBase):
             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):
-        pd = PasswordDialog(accountname, config)
+    def getpass(s, accountname, config, errmsg = None):
+        pd = PasswordDialog(accountname, config, errmsg = errmsg)
         return pd.getpassword()
 
     def gettf(s, newtype=ThreadFrame, master = None):
@@ -207,6 +238,7 @@ class VerboseUI(UIBase):
             s.availablethreadframes.append(tf)
             del s.threadframes[threadid]
         s.tflock.release()
+        UIBase.threadExited(s, thread)
 
     def idlevacuum(s):
         while s.notdeleted:
@@ -218,27 +250,29 @@ class VerboseUI(UIBase):
             s.tflock.release()
             
     def threadException(s, thread):
-        msg =  "Thread '%s' terminated with exception:\n%s" % \
-              (thread.getName(), thread.getExitStackTrace())
-        print msg
+        exceptionstr = s.getThreadExceptionString(thread)
+        print exceptionstr
     
         s.top.destroy()
         s.top = None
-        TextOKDialog("Thread Exception", msg)
+        TextOKDialog("Thread Exception", exceptionstr)
+        s.delThreadDebugLog(thread)
         s.terminate(100)
 
     def mainException(s):
-        sbuf = StringIO()
-        traceback.print_exc(file = sbuf)
-        msg = "Main program terminated with exception:\n" + sbuf.getvalue()
-        print msg
+        exceptionstr = s.getMainExceptionString()
+        print exceptionstr
 
         s.top.destroy()
         s.top = None
-        TextOKDialog("Main Program Exception", msg)
+        TextOKDialog("Main Program Exception", exceptionstr)
 
-    def warn(s, msg):
-        TextOKDialog("OfflineIMAP Warning", msg)
+    def warn(s, msg, minor):
+        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",
@@ -248,6 +282,9 @@ class VerboseUI(UIBase):
 
 
     def init_banner(s):
+        s.threadframes = {}
+        s.availablethreadframes = []
+        s.tflock = Lock()
         s._createTopWindow()
         s._msg(version.productname + " " + version.versionstr + ", " +\
                version.copyright)
@@ -258,61 +295,108 @@ class VerboseUI(UIBase):
         
         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 = 1
+        s.sleeping_abort[thread.get_ident()] = 1
 
     def sleep(s, sleepsecs):
-        s.sleeping_abort = 0
+        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 = s._sleep_cancel)
+                          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 s.sleeping_abort
+        return retval
 
 TkUI = VerboseUI
 
-class LEDCanvas(Canvas):
-    def createLEDLock(self):
-        self.ledlock = Lock()
-    def acquireLEDLock(self):
-        self.ledlock.acquire()
-    def releaseLEDLock(self):
-        self.ledlock.release()
-    def setLEDCount(self, arg):
-        self.ledcount = arg
-    def getLEDCount(self):
-        return self.ledcount
-    def incLEDCount(self):
-        self.ledcount += 1
+################################################## 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
+        self.canvas = master.getnewobj()
         self.color = ''
-        try:
-            self.canvas.acquireLEDLock()
-            startpos = 5 + self.canvas.getLEDCount() * 10
-            self.canvas.incLEDCount()
-        finally:
-            self.canvas.releaseLEDLock()
-        self.ovalid = self.canvas.create_oval(startpos, 5, startpos + 5,
-                                              10, fill = 'gray',
+        self.ovalid = self.canvas.create_oval(4, 4, 9,
+                                              9, fill = 'gray',
                                               outline = '#303030')
 
     def setcolor(self, newcolor):
@@ -329,191 +413,126 @@ class LEDThreadFrame:
         else:
             self.setcolor('black')
 
-    def destroythreadextraframe(self):
-        pass
 
-    def getthreadextraframe(self):
-        raise NotImplementedError
+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 setaccount(self, account):
-        pass
-    def setmailbox(self, mailbox):
-        pass
-    def updateloclabel(self):
-        pass
-    def appendmessage(self, newtext):
-        pass
-    def setmessage(self, newtext):
-        pass
-         
+    def isusable(s):
+        return VerboseUI.isusable(s)
 
-class Blinkenlights(VerboseUI):
     def _createTopWindow(self):
         VerboseUI._createTopWindow(self, 0)
+        #self.top.resizable(width = 0, height = 0)
         self.top.configure(background = 'black', bd = 0)
-        c = LEDCanvas(self.top, background = 'black', height = 20, bd = 0,
-                      highlightthickness = 0)
-        c.setLEDCount(0)
-        c.createLEDLock()
-        self.canvas = c
-        c.pack(side = BOTTOM, expand = 1)
-        widthmetric = tkFont.Font(family = 'Helvetica', size = 8).measure("0")
-        self.loglines = 5
-        if self.config.has_option("ui.Tk.Blinkenlights", "loglines"):
-            self.loglines = self.config.getint("ui.Tk.Blinkenlights",
-                                               "loglines")
-        self.text = Text(self.top, bg = 'black', font = ("Helvetica", 8),
-                         bd = 0, highlightthickness = 0, setgrid = 0,
-                         state = DISABLED, height = self.loglines, wrap = NONE,
-                         width = int(c.cget('width')) / widthmetric)
+
+        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 gettf(s, newtype=LEDThreadFrame):
-        return VerboseUI.gettf(s, newtype, s.canvas)
-
     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 = ("Helvetica", 8), bd = 0)
+                       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)
-        if s.config.has_option("ui.Tk.Blinkenlights", "showlog") and \
-           s.config.getboolean("ui.Tk.Blinkenlights", "showlog"):
-            s._togglelog()
-
-    def _largerlog(s):
-        s.loglines += 1
-        s.text.configure(height = s.loglines)
-
-    def _smallerlog(s):
-        if s.loglines >= 2:
-            s.loglines -= 1
-            s.text.configure(height = s.loglines)
 
     def _togglelog(s):
         if s.textenabled:
+            s.oldtextheight = s.text.winfo_height()
             s.text.pack_forget()
             s.textenabled = 0
-            s.menubar.delete('Log')
-            s.menubar.insert_command('Exit', label = 'Show Log',
-                                     command = s._togglelog)
-        else:
-            s.text.pack(side = BOTTOM, expand = 1)
+            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.menubar.delete('Show Log')
-
-            logmenu = Menu(s.menubar, tearoff = 0,
-                           activebackground = "#000030",
-                           background = "#000030", foreground = "blue",
-                           activeforeground = "white",
-                           font = ("Helvetica", 8))
-            logmenu.add_command(label = "Hide Log", command = s._togglelog)
-            logmenu.add_command(label = "Larger Log", command = s._largerlog)
-            logmenu.add_command(label = "Smaller Log", command = s._smallerlog)
-            s.menubar.insert_cascade('Exit', label = "Log", menu = logmenu)
-
-    def acct(s, accountname):
-        s.gettf().setcolor('purple')
-        VerboseUI.acct(s, accountname)
-
-    def syncfolders(s, srcrepos, destrepos):
-        s.gettf().setcolor('blue')
-        VerboseUI.syncfolders(s, srcrepos, destrepos)
-
-    def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
-        s.gettf().setcolor('cyan')
-        VerboseUI.syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder)
-
-    def loadmessagelist(s, repos, folder):
-        s.gettf().setcolor('green')
-        s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
-                                            folder.getvisiblename()))
-
-    def syncingmessages(s, sr, sf, dr, df):
-        s.gettf().setcolor('blue')
-        VerboseUI.syncingmessages(s, sr, sf, dr, df)
-
-    def copyingmessage(s, uid, src, destlist):
-        s.gettf().setcolor('orange')
-        VerboseUI.copyingmessage(s, uid, src, destlist)
-
-    def deletingmessages(s, uidlist, destlist):
-        s.gettf().setcolor('red')
-        VerboseUI.deletingmessages(s, uidlist, destlist)
+            s.top.update()
+            s.top.geometry("")
+            s.menubar.entryconfig('Show Log', label = 'Hide Log')
+            s._rescroll()
+            s.top.resizable(width = 1, height = 1)
 
-    def deletingmessage(s, uid, destlist):
+    def sleep(s, sleepsecs):
         s.gettf().setcolor('red')
-        VerboseUI.deletingmessage(s, uid, destlist)
-
-    def addingflags(s, uid, flags, destlist):
-        s.gettf().setcolor('yellow')
-        VerboseUI.addingflags(s, uid, flags, destlist)
-
-    def deletingflags(s, uid, flags, destlist):
-        s.gettf().setcolor('pink')
-        VerboseUI.deletingflags(s, uid, flags, destlist)
+        s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
+        BlinkenBase.sleep(s, sleepsecs)
 
-    def threadExited(s, thread):
-        threadid = thread.threadid
-        s.tflock.acquire()
-        try:
-            if threadid in s.threadframes:
-                tf = s.threadframes[threadid]
-                del s.threadframes[threadid]
-                s.availablethreadframes.append(tf)
-                tf.setthread(None)
-        finally:
-            s.tflock.release()
+    def sleeping(s, sleepsecs, remainingsecs):
+        return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
 
-    def sleep(s, sleepsecs):
-        s.sleeping_abort = 0
-        s.menubar.add_command(label = "Sync now", command = s._sleep_cancel)
-        s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
-        UIBase.sleep(s, sleepsecs)
+    def _rescroll(s):
+        s.text.see(END)
+        lo, hi = s.text.vbar.get()
+        s.text.vbar.set(1.0 - (hi - lo), 1.0)
 
     def _msg(s, msg):
         if "\n" in msg:
             for thisline in msg.split("\n"):
                 s._msg(thisline)
             return
-        VerboseUI._msg(s, msg)
+        #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, msg + "\n", 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.loglines + 2.0:
+            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()
 
-    def sleeping(s, sleepsecs, remainingsecs):
-        if remainingsecs:
-            s.menubar.entryconfig('end', label = "Sync now (%d:%02d remain)" % \
-                          (remainingsecs / 60, remainingsecs % 60))
-        else:
-            s.menubar.delete('end')
-        if s.gettf().getcolor() == 'red':
-            s.gettf().setcolor('black')
-        else:
-            s.gettf().setcolor('red')
-        time.sleep(sleepsecs)
-        return s.sleeping_abort