]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Tk.py
/offlineimap/head: changeset 321
[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 class PasswordDialog:
33 def __init__(self, accountname, config, master=None, errmsg = None):
34 self.top = Toplevel(master)
35 self.top.title(version.productname + " Password Entry")
36 text = ''
37 if errmsg:
38 text = '%s: %s\n' % (accountname, errmsg)
39 text += "%s: Enter password for %s on %s: " % \
40 (accountname, config.get(accountname, "remoteuser"),
41 config.get(accountname, "remotehost"))
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 try:
152 Tk().destroy()
153 return 1
154 except TclError:
155 return 0
156
157 def _createTopWindow(self, doidlevac = 1):
158 self.notdeleted = 1
159 self.created = threading.Event()
160
161 self.af = {}
162 self.aflock = Lock()
163
164 t = threadutil.ExitNotifyThread(target = self._runmainloop,
165 name = "Tk Mainloop")
166 t.setDaemon(1)
167 t.start()
168
169 self.created.wait()
170 del self.created
171
172 if doidlevac:
173 t = threadutil.ExitNotifyThread(target = self.idlevacuum,
174 name = "Tk idle vacuum")
175 t.setDaemon(1)
176 t.start()
177
178 def _runmainloop(s):
179 s.top = Tk()
180 s.top.title(version.productname + " " + version.versionstr)
181 s.top.after_idle(s.created.set)
182 s.top.mainloop()
183 s.notdeleted = 0
184
185 def getaccountframe(s):
186 accountname = s.getthreadaccount()
187 s.aflock.acquire()
188 try:
189 if accountname in s.af:
190 return s.af[accountname]
191
192 s.af[accountname] = LEDAccountFrame(s.top, accountname,
193 s.fontfamily, s.fontsize)
194 finally:
195 s.aflock.release()
196 return s.af[accountname]
197
198 def getpass(s, accountname, config, errmsg = None):
199 pd = PasswordDialog(accountname, config, errmsg = errmsg)
200 return pd.getpassword()
201
202 def gettf(s, newtype=ThreadFrame, master = None):
203 if master == None:
204 master = s.top
205 threadid = thread.get_ident()
206 s.tflock.acquire()
207 try:
208 if threadid in s.threadframes:
209 return s.threadframes[threadid]
210 if len(s.availablethreadframes):
211 tf = s.availablethreadframes.pop(0)
212 tf.setthread(currentThread())
213 else:
214 tf = newtype(master)
215 s.threadframes[threadid] = tf
216 return tf
217 finally:
218 s.tflock.release()
219
220 def _msg(s, msg):
221 s.gettf().setmessage(msg)
222
223 def threadExited(s, thread):
224 threadid = thread.threadid
225 s.tflock.acquire()
226 if threadid in s.threadframes:
227 tf = s.threadframes[threadid]
228 tf.setthread(None)
229 tf.setaccount("Unknown")
230 tf.setmessage("Idle")
231 s.availablethreadframes.append(tf)
232 del s.threadframes[threadid]
233 s.tflock.release()
234 UIBase.threadExited(s, thread)
235
236 def idlevacuum(s):
237 while s.notdeleted:
238 time.sleep(10)
239 s.tflock.acquire()
240 while len(s.availablethreadframes):
241 tf = s.availablethreadframes.pop()
242 tf.destroy()
243 s.tflock.release()
244
245 def threadException(s, thread):
246 exceptionstr = s.getThreadExceptionString(thread)
247 print exceptionstr
248
249 s.top.destroy()
250 s.top = None
251 TextOKDialog("Thread Exception", exceptionstr)
252 s.delThreadDebugLog(thread)
253 s.terminate(100)
254
255 def mainException(s):
256 exceptionstr = s.getMainExceptionString()
257 print exceptionstr
258
259 s.top.destroy()
260 s.top = None
261 TextOKDialog("Main Program Exception", exceptionstr)
262
263 def warn(s, msg, minor):
264 if minor:
265 # Just let the default handler catch it
266 UIBase.warn(s, msg, minor)
267 else:
268 TextOKDialog("OfflineIMAP Warning", msg)
269
270 def showlicense(s):
271 TextOKDialog(version.productname + " License",
272 version.bigcopyright + "\n" +
273 version.homepage + "\n\n" + version.license,
274 blocking = 0, master = s.top)
275
276
277 def init_banner(s):
278 s._createTopWindow()
279 s._msg(version.productname + " " + version.versionstr + ", " +\
280 version.copyright)
281 tf = s.gettf().getthreadextraframe()
282
283 b = Button(tf, text = "About", command = s.showlicense)
284 b.pack(side = LEFT)
285
286 b = Button(tf, text = "Exit", command = s.terminate)
287 b.pack(side = RIGHT)
288 s.sleeping_abort = {}
289
290 def deletingmessages(s, uidlist, destlist):
291 ds = s.folderlist(destlist)
292 s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
293
294 def _sleep_cancel(s, args = None):
295 s.sleeping_abort[thread.get_ident()] = 1
296
297 def sleep(s, sleepsecs):
298 threadid = thread.get_ident()
299 s.sleeping_abort[threadid] = 0
300 tf = s.gettf().getthreadextraframe()
301
302 def sleep_cancel():
303 s.sleeping_abort[threadid] = 1
304
305 sleepbut = Button(tf, text = 'Sync immediately',
306 command = sleep_cancel)
307 sleepbut.pack()
308 UIBase.sleep(s, sleepsecs)
309
310 def sleeping(s, sleepsecs, remainingsecs):
311 retval = s.sleeping_abort[thread.get_ident()]
312 if remainingsecs:
313 s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
314 remainingsecs % 60))
315 else:
316 s._msg("Wait done; synchronizing now.")
317 s.gettf().destroythreadextraframe()
318 del s.sleeping_abort[thread.get_ident()]
319 time.sleep(sleepsecs)
320 return retval
321
322 TkUI = VerboseUI
323
324 ################################################## Blinkenlights
325
326 class LEDAccountFrame:
327 def __init__(self, top, accountname, fontfamily, fontsize):
328 self.top = top
329 self.accountname = accountname
330 self.fontfamily = fontfamily
331 self.fontsize = fontsize
332 self.frame = Frame(self.top, background = 'black')
333 self.frame.pack(side = BOTTOM, expand = 1, fill = X)
334 self._createcanvas(self.frame)
335
336 self.label = Label(self.frame, text = accountname,
337 background = "black", foreground = "blue",
338 font = (self.fontfamily, self.fontsize))
339 self.label.grid(sticky = E, row = 0, column = 1)
340
341 def getnewthreadframe(s):
342 return LEDThreadFrame(s.canvas)
343
344 def _createcanvas(self, parent):
345 c = LEDFrame(parent)
346 self.canvas = c
347 c.grid(sticky = E, row = 0, column = 0)
348 parent.grid_columnconfigure(1, weight = 1)
349 #c.pack(side = LEFT, expand = 0, fill = X)
350
351 def startsleep(s, sleepsecs):
352 s.sleeping_abort = 0
353 s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
354 background = "black", activebackground = "black",
355 activeforeground = "white",
356 foreground = "blue", highlightthickness = 0,
357 padx = 0, pady = 0,
358 font = (s.fontfamily, s.fontsize), borderwidth = 0,
359 relief = 'solid')
360 s.button.grid(sticky = E, row = 0, column = 2)
361
362 def syncnow(s):
363 s.sleeping_abort = 1
364
365 def sleeping(s, sleepsecs, remainingsecs):
366 if remainingsecs:
367 s.button.config(text = 'Sync now (%d:%02d remain)' % \
368 (remainingsecs / 60, remainingsecs % 60))
369 time.sleep(sleepsecs)
370 else:
371 s.button.destroy()
372 del s.button
373 return s.sleeping_abort
374
375 class LEDFrame(Frame):
376 """This holds the different lights."""
377 def getnewobj(self):
378 retval = Canvas(self, background = 'black', height = 20, bd = 0,
379 highlightthickness = 0, width = 10)
380 retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
381 return retval
382
383 class LEDThreadFrame:
384 """There is one of these for each little light."""
385 def __init__(self, master):
386 self.canvas = master.getnewobj()
387 self.color = ''
388 self.ovalid = self.canvas.create_oval(5, 5, 10,
389 10, fill = 'gray',
390 outline = '#303030')
391
392 def setcolor(self, newcolor):
393 if newcolor != self.color:
394 self.canvas.itemconfigure(self.ovalid, fill = newcolor)
395 self.color = newcolor
396
397 def getcolor(self):
398 return self.color
399
400 def setthread(self, newthread):
401 if newthread:
402 self.setcolor('gray')
403 else:
404 self.setcolor('black')
405
406
407 class Blinkenlights(BlinkenBase, VerboseUI):
408 def __init__(s, config, verbose = 0):
409 VerboseUI.__init__(s, config, verbose)
410 s.fontfamily = 'Helvetica'
411 s.fontsize = 8
412 if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
413 s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
414 if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
415 s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')
416
417 def _createTopWindow(self):
418 VerboseUI._createTopWindow(self, 0)
419 #self.top.resizable(width = 0, height = 0)
420 self.top.configure(background = 'black', bd = 0)
421
422 widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
423 self.loglines = 5
424 if self.config.has_option("ui.Tk.Blinkenlights", "loglines"):
425 self.loglines = self.config.getint("ui.Tk.Blinkenlights",
426 "loglines")
427 self.bufferlines = 500
428 if self.config.has_option("ui.Tk.Blinkenlights", "bufferlines"):
429 self.bufferlines = self.config.getint("ui.Tk.Blinkenlights",
430 "bufferlines")
431 self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
432 font = (self.fontfamily, self.fontsize),
433 bd = 0, highlightthickness = 0, setgrid = 0,
434 state = DISABLED, height = self.loglines,
435 wrap = NONE, width = 60)
436 self.text.vbar.configure(background = '#000050',
437 activebackground = 'blue',
438 highlightbackground = 'black',
439 troughcolor = "black", bd = 0,
440 elementborderwidth = 2)
441
442 self.textenabled = 0
443 self.tags = []
444 self.textlock = Lock()
445
446 def init_banner(s):
447 BlinkenBase.init_banner(s)
448 s._createTopWindow()
449 menubar = Menu(s.top, activebackground = "black",
450 activeforeground = "white",
451 activeborderwidth = 0,
452 background = "black", foreground = "blue",
453 font = (s.fontfamily, s.fontsize), bd = 0)
454 menubar.add_command(label = "About", command = s.showlicense)
455 menubar.add_command(label = "Show Log", command = s._togglelog)
456 menubar.add_command(label = "Exit", command = s.terminate)
457 s.top.config(menu = menubar)
458 s.menubar = menubar
459 s.text.see(END)
460 if s.config.has_option("ui.Tk.Blinkenlights", "showlog") and \
461 s.config.getboolean("ui.Tk.Blinkenlights", "showlog"):
462 s._togglelog()
463 s.gettf().setcolor('red')
464 s.top.resizable(width = 0, height = 0)
465 s._msg(version.banner)
466
467 def _togglelog(s):
468 if s.textenabled:
469 s.oldtextheight = s.text.winfo_height()
470 s.text.pack_forget()
471 s.textenabled = 0
472 s.menubar.entryconfig('Hide Log', label = 'Show Log')
473 s.top.update()
474 s.top.geometry("")
475 s.top.update()
476 s.top.resizable(width = 0, height = 0)
477 s.top.update()
478
479 else:
480 s.text.pack(side = TOP, expand = 1, fill = BOTH)
481 s.textenabled = 1
482 s.top.update()
483 s.top.geometry("")
484 s.menubar.entryconfig('Show Log', label = 'Hide Log')
485 s._rescroll()
486 s.top.resizable(width = 1, height = 1)
487
488 def sleep(s, sleepsecs):
489 s.gettf().setcolor('red')
490 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
491 BlinkenBase.sleep(s, sleepsecs)
492
493 def sleeping(s, sleepsecs, remainingsecs):
494 return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
495
496 def _rescroll(s):
497 s.text.see(END)
498 lo, hi = s.text.vbar.get()
499 s.text.vbar.set(1.0 - (hi - lo), 1.0)
500
501 def _msg(s, msg):
502 if "\n" in msg:
503 for thisline in msg.split("\n"):
504 s._msg(thisline)
505 return
506 #VerboseUI._msg(s, msg)
507 color = s.gettf().getcolor()
508 rescroll = 1
509 s.textlock.acquire()
510 try:
511 if s.text.vbar.get()[1] != 1.0:
512 rescroll = 0
513 s.text.config(state = NORMAL)
514 if not color in s.tags:
515 s.text.tag_config(color, foreground = color)
516 s.tags.append(color)
517 s.text.insert(END, "\n" + msg, color)
518
519 # Trim down. Not quite sure why I have to say 7 instead of 5,
520 # but so it is.
521 while float(s.text.index(END)) > s.bufferlines + 2.0:
522 s.text.delete(1.0, 2.0)
523
524 if rescroll:
525 s._rescroll()
526 finally:
527 s.text.config(state = DISABLED)
528 s.textlock.release()
529
530