# 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='*')
class VerboseUI(UIBase):
def isusable(s):
+ global usabletest
+ if usabletest != None:
+ return usabletest
+
try:
Tk().destroy()
- return 1
+ usabletest = 1
except TclError:
- return 0
-
- def _createTopWindow(self):
- self.top = Tk()
- self.top.title(version.productname + " " + version.versionstr)
- self.threadframes = {}
- self.availablethreadframes = []
- self.tflock = Lock()
+ 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()
- t = threadutil.ExitNotifyThread(target = self.idlevacuum,
- name = "Tk idle vacuum")
- 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):
- 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):
s.availablethreadframes.append(tf)
del s.threadframes[threadid]
s.tflock.release()
+ UIBase.threadExited(s, thread)
def idlevacuum(s):
while s.notdeleted:
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, 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",
+ version.bigcopyright + "\n" +
+ version.homepage + "\n\n" + version.license,
+ blocking = 0, master = s.top)
- def warn(s, msg):
- TextOKDialog("OfflineIMAP Warning", msg)
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()
- def showlicense():
- TextOKDialog(version.productname + " License",
- version.bigcopyright + "\n" +
- version.homepage + "\n\n" + version.license,
- blocking = 0, master = tf)
- b = Button(tf, text = "About", command = showlicense)
+ 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 = 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
- 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.canvas = master.getnewobj()
+ self.color = ''
+ self.ovalid = self.canvas.create_oval(4, 4, 9,
+ 9, fill = 'gray',
outline = '#303030')
- def _setcolor(self, newcolor):
- self.canvas.itemconfigure(self.ovalid, fill = newcolor)
+ 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')
+ self.setcolor('gray')
else:
- self._setcolor('black')
+ 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)
- c = LEDCanvas(self.top, background = 'black', height = 20)
- c.setLEDCount(0)
- c.createLEDLock()
- self.canvas = c
- c.pack(side = BOTTOM)
-
- def gettf(s, newtype=LEDThreadFrame):
- return VerboseUI.gettf(s, newtype, s.canvas)
+ 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 threadExited(s, thread):
- threadid = thread.threadid
- s.tflock.acquire()
+ 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 _msg(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 threadid in s.threadframes:
- tf = s.threadframes[threadid]
- del s.threadframes[threadid]
- s.availablethreadframes.append(tf)
- tf.setthread(None)
+ 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.tflock.release()
+ s.text.config(state = DISABLED)
+ s.textlock.release()
+
+