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