]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Tk.py
/offlineimap/head: changeset 367
[offlineimap] / offlineimap / head / offlineimap / ui / Tk.py
1 # Tk UI
2 # Copyright (C) 2002, 2003 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
19 from __future__ import nested_scopes
20
21 from Tkinter import *
22 import tkFont
23 from threading import *
24 import thread, traceback, time, threading
25 from StringIO import StringIO
26 from ScrolledText import ScrolledText
27 from offlineimap import threadutil, version
28 from Queue import Queue
29 from UIBase import UIBase
30 from offlineimap.ui.Blinkenlights import BlinkenBase
31
32 usabletest = None
33
34 class PasswordDialog:
35 def __init__(self, accountname, config, master=None, errmsg = None):
36 self.top = Toplevel(master)
37 self.top.title(version.productname + " Password Entry")
38 text = ''
39 if errmsg:
40 text = '%s: %s\n' % (accountname, errmsg)
41 text += "%s: Enter password for %s on %s: " % \
42 (accountname, config.get(accountname, "remoteuser"),
43 config.get(accountname, "remotehost"))
44 self.label = Label(self.top, text = text)
45 self.label.pack()
46
47 self.entry = Entry(self.top, show='*')
48 self.entry.bind("<Return>", self.ok)
49 self.entry.pack()
50 self.entry.focus_force()
51
52 self.button = Button(self.top, text = "OK", command=self.ok)
53 self.button.pack()
54
55 self.entry.focus_force()
56 self.top.wait_window(self.label)
57
58 def ok(self, args = None):
59 self.password = self.entry.get()
60 self.top.destroy()
61
62 def getpassword(self):
63 return self.password
64
65 class TextOKDialog:
66 def __init__(self, title, message, blocking = 1, master = None):
67 if not master:
68 self.top = Tk()
69 else:
70 self.top = Toplevel(master)
71 self.top.title(title)
72 self.text = ScrolledText(self.top, font = "Courier 10")
73 self.text.pack()
74 self.text.insert(END, message)
75 self.text['state'] = DISABLED
76 self.button = Button(self.top, text = "OK", command=self.ok)
77 self.button.pack()
78
79 if blocking:
80 self.top.wait_window(self.button)
81
82 def ok(self):
83 self.top.destroy()
84
85
86
87 class ThreadFrame(Frame):
88 def __init__(self, master=None):
89 self.threadextraframe = None
90 self.thread = currentThread()
91 self.threadid = thread.get_ident()
92 Frame.__init__(self, master, relief = RIDGE, borderwidth = 2)
93 self.pack(fill = 'x')
94 self.threadlabel = Label(self, foreground = '#FF0000',
95 text ="Thread %d (%s)" % (self.threadid,
96 self.thread.getName()))
97 self.threadlabel.pack()
98 self.setthread(currentThread())
99
100 self.account = "Unknown"
101 self.mailbox = "Unknown"
102 self.loclabel = Label(self,
103 text = "Account/mailbox information unknown")
104 #self.loclabel.pack()
105
106 self.updateloclabel()
107
108 self.message = Label(self, text="Messages will appear here.\n",
109 foreground = '#0000FF')
110 self.message.pack(fill = 'x')
111
112 def setthread(self, newthread):
113 if newthread:
114 self.threadlabel['text'] = newthread.getName()
115 else:
116 self.threadlabel['text'] = "No thread"
117 self.destroythreadextraframe()
118
119 def destroythreadextraframe(self):
120 if self.threadextraframe:
121 self.threadextraframe.destroy()
122 self.threadextraframe = None
123
124 def getthreadextraframe(self):
125 if self.threadextraframe:
126 return self.threadextraframe
127 self.threadextraframe = Frame(self)
128 self.threadextraframe.pack(fill = 'x')
129 return self.threadextraframe
130
131 def setaccount(self, account):
132 self.account = account
133 self.mailbox = "Unknown"
134 self.updateloclabel()
135
136 def setmailbox(self, mailbox):
137 self.mailbox = mailbox
138 self.updateloclabel()
139
140 def updateloclabel(self):
141 self.loclabel['text'] = "Processing %s: %s" % (self.account,
142 self.mailbox)
143
144 def appendmessage(self, newtext):
145 self.message['text'] += "\n" + newtext
146
147 def setmessage(self, newtext):
148 self.message['text'] = newtext
149
150
151 class VerboseUI(UIBase):
152 def isusable(s):
153 global usabletest
154 if usabletest != None:
155 return usabletest
156
157 try:
158 Tk().destroy()
159 usabletest = 1
160 except TclError:
161 usabletest = 0
162 return usabletest
163
164 def _createTopWindow(self, doidlevac = 1):
165 self.notdeleted = 1
166 self.created = threading.Event()
167
168 self.af = {}
169 self.aflock = Lock()
170
171 t = threadutil.ExitNotifyThread(target = self._runmainloop,
172 name = "Tk Mainloop")
173 t.setDaemon(1)
174 t.start()
175
176 self.created.wait()
177 del self.created
178
179 if doidlevac:
180 t = threadutil.ExitNotifyThread(target = self.idlevacuum,
181 name = "Tk idle vacuum")
182 t.setDaemon(1)
183 t.start()
184
185 def _runmainloop(s):
186 s.top = Tk()
187 s.top.title(version.productname + " " + version.versionstr)
188 s.top.after_idle(s.created.set)
189 s.top.mainloop()
190 s.notdeleted = 0
191
192 def getaccountframe(s):
193 accountname = s.getthreadaccount()
194 s.aflock.acquire()
195 try:
196 if accountname in s.af:
197 return s.af[accountname]
198
199 s.af[accountname] = LEDAccountFrame(s.top, accountname,
200 s.fontfamily, s.fontsize)
201 finally:
202 s.aflock.release()
203 return s.af[accountname]
204
205 def getpass(s, accountname, config, errmsg = None):
206 pd = PasswordDialog(accountname, config, errmsg = errmsg)
207 return pd.getpassword()
208
209 def gettf(s, newtype=ThreadFrame, master = None):
210 if master == None:
211 master = s.top
212 threadid = thread.get_ident()
213 s.tflock.acquire()
214 try:
215 if threadid in s.threadframes:
216 return s.threadframes[threadid]
217 if len(s.availablethreadframes):
218 tf = s.availablethreadframes.pop(0)
219 tf.setthread(currentThread())
220 else:
221 tf = newtype(master)
222 s.threadframes[threadid] = tf
223 return tf
224 finally:
225 s.tflock.release()
226
227 def _msg(s, msg):
228 s.gettf().setmessage(msg)
229
230 def threadExited(s, thread):
231 threadid = thread.threadid
232 s.tflock.acquire()
233 if threadid in s.threadframes:
234 tf = s.threadframes[threadid]
235 tf.setthread(None)
236 tf.setaccount("Unknown")
237 tf.setmessage("Idle")
238 s.availablethreadframes.append(tf)
239 del s.threadframes[threadid]
240 s.tflock.release()
241 UIBase.threadExited(s, thread)
242
243 def idlevacuum(s):
244 while s.notdeleted:
245 time.sleep(10)
246 s.tflock.acquire()
247 while len(s.availablethreadframes):
248 tf = s.availablethreadframes.pop()
249 tf.destroy()
250 s.tflock.release()
251
252 def threadException(s, thread):
253 exceptionstr = s.getThreadExceptionString(thread)
254 print exceptionstr
255
256 s.top.destroy()
257 s.top = None
258 TextOKDialog("Thread Exception", exceptionstr)
259 s.delThreadDebugLog(thread)
260 s.terminate(100)
261
262 def mainException(s):
263 exceptionstr = s.getMainExceptionString()
264 print exceptionstr
265
266 s.top.destroy()
267 s.top = None
268 TextOKDialog("Main Program Exception", exceptionstr)
269
270 def warn(s, msg, minor):
271 if minor:
272 # Just let the default handler catch it
273 UIBase.warn(s, msg, minor)
274 else:
275 TextOKDialog("OfflineIMAP Warning", msg)
276
277 def showlicense(s):
278 TextOKDialog(version.productname + " License",
279 version.bigcopyright + "\n" +
280 version.homepage + "\n\n" + version.license,
281 blocking = 0, master = s.top)
282
283
284 def init_banner(s):
285 s.threadframes = {}
286 s.availablethreadframes = []
287 s.tflock = Lock()
288 s._createTopWindow()
289 s._msg(version.productname + " " + version.versionstr + ", " +\
290 version.copyright)
291 tf = s.gettf().getthreadextraframe()
292
293 b = Button(tf, text = "About", command = s.showlicense)
294 b.pack(side = LEFT)
295
296 b = Button(tf, text = "Exit", command = s.terminate)
297 b.pack(side = RIGHT)
298 s.sleeping_abort = {}
299
300 def deletingmessages(s, uidlist, destlist):
301 ds = s.folderlist(destlist)
302 s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
303
304 def _sleep_cancel(s, args = None):
305 s.sleeping_abort[thread.get_ident()] = 1
306
307 def sleep(s, sleepsecs):
308 threadid = thread.get_ident()
309 s.sleeping_abort[threadid] = 0
310 tf = s.gettf().getthreadextraframe()
311
312 def sleep_cancel():
313 s.sleeping_abort[threadid] = 1
314
315 sleepbut = Button(tf, text = 'Sync immediately',
316 command = sleep_cancel)
317 sleepbut.pack()
318 UIBase.sleep(s, sleepsecs)
319
320 def sleeping(s, sleepsecs, remainingsecs):
321 retval = s.sleeping_abort[thread.get_ident()]
322 if remainingsecs:
323 s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
324 remainingsecs % 60))
325 else:
326 s._msg("Wait done; synchronizing now.")
327 s.gettf().destroythreadextraframe()
328 del s.sleeping_abort[thread.get_ident()]
329 time.sleep(sleepsecs)
330 return retval
331
332 TkUI = VerboseUI
333
334 ################################################## Blinkenlights
335
336 class LEDAccountFrame:
337 def __init__(self, top, accountname, fontfamily, fontsize):
338 self.top = top
339 self.accountname = accountname
340 self.fontfamily = fontfamily
341 self.fontsize = fontsize
342 self.frame = Frame(self.top, background = 'black')
343 self.frame.pack(side = BOTTOM, expand = 1, fill = X)
344 self._createcanvas(self.frame)
345
346 self.label = Label(self.frame, text = accountname,
347 background = "black", foreground = "blue",
348 font = (self.fontfamily, self.fontsize))
349 self.label.grid(sticky = E, row = 0, column = 1)
350
351 def getnewthreadframe(s):
352 return LEDThreadFrame(s.canvas)
353
354 def _createcanvas(self, parent):
355 c = LEDFrame(parent)
356 self.canvas = c
357 c.grid(sticky = E, row = 0, column = 0)
358 parent.grid_columnconfigure(1, weight = 1)
359 #c.pack(side = LEFT, expand = 0, fill = X)
360
361 def startsleep(s, sleepsecs):
362 s.sleeping_abort = 0
363 s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
364 background = "black", activebackground = "black",
365 activeforeground = "white",
366 foreground = "blue", highlightthickness = 0,
367 padx = 0, pady = 0,
368 font = (s.fontfamily, s.fontsize), borderwidth = 0,
369 relief = 'solid')
370 s.button.grid(sticky = E, row = 0, column = 2)
371
372 def syncnow(s):
373 s.sleeping_abort = 1
374
375 def sleeping(s, sleepsecs, remainingsecs):
376 if remainingsecs:
377 s.button.config(text = 'Sync now (%d:%02d remain)' % \
378 (remainingsecs / 60, remainingsecs % 60))
379 time.sleep(sleepsecs)
380 else:
381 s.button.destroy()
382 del s.button
383 return s.sleeping_abort
384
385 class LEDFrame(Frame):
386 """This holds the different lights."""
387 def getnewobj(self):
388 retval = Canvas(self, background = 'black', height = 20, bd = 0,
389 highlightthickness = 0, width = 10)
390 retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
391 return retval
392
393 class LEDThreadFrame:
394 """There is one of these for each little light."""
395 def __init__(self, master):
396 self.canvas = master.getnewobj()
397 self.color = ''
398 self.ovalid = self.canvas.create_oval(4, 4, 9,
399 9, fill = 'gray',
400 outline = '#303030')
401
402 def setcolor(self, newcolor):
403 if newcolor != self.color:
404 self.canvas.itemconfigure(self.ovalid, fill = newcolor)
405 self.color = newcolor
406
407 def getcolor(self):
408 return self.color
409
410 def setthread(self, newthread):
411 if newthread:
412 self.setcolor('gray')
413 else:
414 self.setcolor('black')
415
416
417 class Blinkenlights(BlinkenBase, VerboseUI):
418 def __init__(s, config, verbose = 0):
419 VerboseUI.__init__(s, config, verbose)
420 s.fontfamily = 'Helvetica'
421 s.fontsize = 8
422 if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
423 s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
424 if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
425 s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')
426
427 def isusable(s):
428 return VerboseUI.isusable(s)
429
430 def _createTopWindow(self):
431 VerboseUI._createTopWindow(self, 0)
432 #self.top.resizable(width = 0, height = 0)
433 self.top.configure(background = 'black', bd = 0)
434
435 widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
436 self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights",
437 "loglines", 5)
438 self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights",
439 "bufferlines", 500)
440 self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
441 font = (self.fontfamily, self.fontsize),
442 bd = 0, highlightthickness = 0, setgrid = 0,
443 state = DISABLED, height = self.loglines,
444 wrap = NONE, width = 60)
445 self.text.vbar.configure(background = '#000050',
446 activebackground = 'blue',
447 highlightbackground = 'black',
448 troughcolor = "black", bd = 0,
449 elementborderwidth = 2)
450
451 self.textenabled = 0
452 self.tags = []
453 self.textlock = Lock()
454
455 def init_banner(s):
456 BlinkenBase.init_banner(s)
457 s._createTopWindow()
458 menubar = Menu(s.top, activebackground = "black",
459 activeforeground = "white",
460 activeborderwidth = 0,
461 background = "black", foreground = "blue",
462 font = (s.fontfamily, s.fontsize), bd = 0)
463 menubar.add_command(label = "About", command = s.showlicense)
464 menubar.add_command(label = "Show Log", command = s._togglelog)
465 menubar.add_command(label = "Exit", command = s.terminate)
466 s.top.config(menu = menubar)
467 s.menubar = menubar
468 s.text.see(END)
469 if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1):
470 s._togglelog()
471 s.gettf().setcolor('red')
472 s.top.resizable(width = 0, height = 0)
473 s._msg(version.banner)
474
475 def _togglelog(s):
476 if s.textenabled:
477 s.oldtextheight = s.text.winfo_height()
478 s.text.pack_forget()
479 s.textenabled = 0
480 s.menubar.entryconfig('Hide Log', label = 'Show Log')
481 s.top.update()
482 s.top.geometry("")
483 s.top.update()
484 s.top.resizable(width = 0, height = 0)
485 s.top.update()
486
487 else:
488 s.text.pack(side = TOP, expand = 1, fill = BOTH)
489 s.textenabled = 1
490 s.top.update()
491 s.top.geometry("")
492 s.menubar.entryconfig('Show Log', label = 'Hide Log')
493 s._rescroll()
494 s.top.resizable(width = 1, height = 1)
495
496 def sleep(s, sleepsecs):
497 s.gettf().setcolor('red')
498 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
499 BlinkenBase.sleep(s, sleepsecs)
500
501 def sleeping(s, sleepsecs, remainingsecs):
502 return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
503
504 def _rescroll(s):
505 s.text.see(END)
506 lo, hi = s.text.vbar.get()
507 s.text.vbar.set(1.0 - (hi - lo), 1.0)
508
509 def _msg(s, msg):
510 if "\n" in msg:
511 for thisline in msg.split("\n"):
512 s._msg(thisline)
513 return
514 #VerboseUI._msg(s, msg)
515 color = s.gettf().getcolor()
516 rescroll = 1
517 s.textlock.acquire()
518 try:
519 if s.text.vbar.get()[1] != 1.0:
520 rescroll = 0
521 s.text.config(state = NORMAL)
522 if not color in s.tags:
523 s.text.tag_config(color, foreground = color)
524 s.tags.append(color)
525 s.text.insert(END, "\n" + msg, color)
526
527 # Trim down. Not quite sure why I have to say 7 instead of 5,
528 # but so it is.
529 while float(s.text.index(END)) > s.bufferlines + 2.0:
530 s.text.delete(1.0, 2.0)
531
532 if rescroll:
533 s._rescroll()
534 finally:
535 s.text.config(state = DISABLED)
536 s.textlock.release()
537
538