]> code.delx.au - offlineimap/blob - offlineimap/ui/Tk.py
Update FSF address
[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 threadException(s, thread):
251 exceptionstr = s.getThreadExceptionString(thread)
252 print exceptionstr
253
254 s.top.destroy()
255 s.top = None
256 TextOKDialog("Thread Exception", exceptionstr)
257 s.delThreadDebugLog(thread)
258 s.terminate(100)
259
260 def mainException(s):
261 exceptionstr = s.getMainExceptionString()
262 print exceptionstr
263
264 s.top.destroy()
265 s.top = None
266 TextOKDialog("Main Program Exception", exceptionstr)
267
268 def warn(s, msg, minor = 0):
269 if minor:
270 # Just let the default handler catch it
271 UIBase.warn(s, msg, minor)
272 else:
273 TextOKDialog("OfflineIMAP Warning", msg)
274
275 def showlicense(s):
276 TextOKDialog(version.productname + " License",
277 version.bigcopyright + "\n" +
278 version.homepage + "\n\n" + version.license,
279 blocking = 0, master = s.top)
280
281
282 def init_banner(s):
283 s.threadframes = {}
284 s.availablethreadframes = []
285 s.tflock = Lock()
286 s._createTopWindow()
287 s._msg(version.productname + " " + version.versionstr + ", " +\
288 version.copyright)
289 tf = s.gettf().getthreadextraframe()
290
291 b = Button(tf, text = "About", command = s.showlicense)
292 b.pack(side = LEFT)
293
294 b = Button(tf, text = "Exit", command = s.terminate)
295 b.pack(side = RIGHT)
296 s.sleeping_abort = {}
297
298 def deletingmessages(s, uidlist, destlist):
299 ds = s.folderlist(destlist)
300 s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
301
302 def _sleep_cancel(s, args = None):
303 s.sleeping_abort[thread.get_ident()] = 1
304
305 def sleep(s, sleepsecs):
306 threadid = thread.get_ident()
307 s.sleeping_abort[threadid] = 0
308 tf = s.gettf().getthreadextraframe()
309
310 def sleep_cancel():
311 s.sleeping_abort[threadid] = 1
312
313 sleepbut = Button(tf, text = 'Sync immediately',
314 command = sleep_cancel)
315 sleepbut.pack()
316 UIBase.sleep(s, sleepsecs)
317
318 def sleeping(s, sleepsecs, remainingsecs):
319 retval = s.sleeping_abort[thread.get_ident()]
320 if remainingsecs:
321 s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
322 remainingsecs % 60))
323 else:
324 s._msg("Wait done; synchronizing now.")
325 s.gettf().destroythreadextraframe()
326 del s.sleeping_abort[thread.get_ident()]
327 time.sleep(sleepsecs)
328 return retval
329
330 TkUI = VerboseUI
331
332 ################################################## Blinkenlights
333
334 class LEDAccountFrame:
335 def __init__(self, top, accountname, fontfamily, fontsize):
336 self.top = top
337 self.accountname = accountname
338 self.fontfamily = fontfamily
339 self.fontsize = fontsize
340 self.frame = Frame(self.top, background = 'black')
341 self.frame.pack(side = BOTTOM, expand = 1, fill = X)
342 self._createcanvas(self.frame)
343
344 self.label = Label(self.frame, text = accountname,
345 background = "black", foreground = "blue",
346 font = (self.fontfamily, self.fontsize))
347 self.label.grid(sticky = E, row = 0, column = 1)
348
349 def getnewthreadframe(s):
350 return LEDThreadFrame(s.canvas)
351
352 def _createcanvas(self, parent):
353 c = LEDFrame(parent)
354 self.canvas = c
355 c.grid(sticky = E, row = 0, column = 0)
356 parent.grid_columnconfigure(1, weight = 1)
357 #c.pack(side = LEFT, expand = 0, fill = X)
358
359 def startsleep(s, sleepsecs):
360 s.sleeping_abort = 0
361 s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
362 background = "black", activebackground = "black",
363 activeforeground = "white",
364 foreground = "blue", highlightthickness = 0,
365 padx = 0, pady = 0,
366 font = (s.fontfamily, s.fontsize), borderwidth = 0,
367 relief = 'solid')
368 s.button.grid(sticky = E, row = 0, column = 2)
369
370 def syncnow(s):
371 s.sleeping_abort = 1
372
373 def sleeping(s, sleepsecs, remainingsecs):
374 if remainingsecs:
375 s.button.config(text = 'Sync now (%d:%02d remain)' % \
376 (remainingsecs / 60, remainingsecs % 60))
377 time.sleep(sleepsecs)
378 else:
379 s.button.destroy()
380 del s.button
381 return s.sleeping_abort
382
383 class LEDFrame(Frame):
384 """This holds the different lights."""
385 def getnewobj(self):
386 retval = Canvas(self, background = 'black', height = 20, bd = 0,
387 highlightthickness = 0, width = 10)
388 retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
389 return retval
390
391 class LEDThreadFrame:
392 """There is one of these for each little light."""
393 def __init__(self, master):
394 self.canvas = master.getnewobj()
395 self.color = ''
396 self.ovalid = self.canvas.create_oval(4, 4, 9,
397 9, fill = 'gray',
398 outline = '#303030')
399
400 def setcolor(self, newcolor):
401 if newcolor != self.color:
402 self.canvas.itemconfigure(self.ovalid, fill = newcolor)
403 self.color = newcolor
404
405 def getcolor(self):
406 return self.color
407
408 def setthread(self, newthread):
409 if newthread:
410 self.setcolor('gray')
411 else:
412 self.setcolor('black')
413
414
415 class Blinkenlights(BlinkenBase, VerboseUI):
416 def __init__(s, config, verbose = 0):
417 VerboseUI.__init__(s, config, verbose)
418 s.fontfamily = 'Helvetica'
419 s.fontsize = 8
420 if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
421 s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
422 if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
423 s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')
424
425 def isusable(s):
426 return VerboseUI.isusable(s)
427
428 def _createTopWindow(self):
429 VerboseUI._createTopWindow(self, 0)
430 #self.top.resizable(width = 0, height = 0)
431 self.top.configure(background = 'black', bd = 0)
432
433 widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
434 self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights",
435 "loglines", 5)
436 self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights",
437 "bufferlines", 500)
438 self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
439 font = (self.fontfamily, self.fontsize),
440 bd = 0, highlightthickness = 0, setgrid = 0,
441 state = DISABLED, height = self.loglines,
442 wrap = NONE, width = 60)
443 self.text.vbar.configure(background = '#000050',
444 activebackground = 'blue',
445 highlightbackground = 'black',
446 troughcolor = "black", bd = 0,
447 elementborderwidth = 2)
448
449 self.textenabled = 0
450 self.tags = []
451 self.textlock = Lock()
452
453 def init_banner(s):
454 BlinkenBase.init_banner(s)
455 s._createTopWindow()
456 menubar = Menu(s.top, activebackground = "black",
457 activeforeground = "white",
458 activeborderwidth = 0,
459 background = "black", foreground = "blue",
460 font = (s.fontfamily, s.fontsize), bd = 0)
461 menubar.add_command(label = "About", command = s.showlicense)
462 menubar.add_command(label = "Show Log", command = s._togglelog)
463 menubar.add_command(label = "Exit", command = s.terminate)
464 s.top.config(menu = menubar)
465 s.menubar = menubar
466 s.text.see(END)
467 if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1):
468 s._togglelog()
469 s.gettf().setcolor('red')
470 s.top.resizable(width = 0, height = 0)
471 s._msg(version.banner)
472
473 def _togglelog(s):
474 if s.textenabled:
475 s.oldtextheight = s.text.winfo_height()
476 s.text.pack_forget()
477 s.textenabled = 0
478 s.menubar.entryconfig('Hide Log', label = 'Show Log')
479 s.top.update()
480 s.top.geometry("")
481 s.top.update()
482 s.top.resizable(width = 0, height = 0)
483 s.top.update()
484
485 else:
486 s.text.pack(side = TOP, expand = 1, fill = BOTH)
487 s.textenabled = 1
488 s.top.update()
489 s.top.geometry("")
490 s.menubar.entryconfig('Show Log', label = 'Hide Log')
491 s._rescroll()
492 s.top.resizable(width = 1, height = 1)
493
494 def sleep(s, sleepsecs):
495 s.gettf().setcolor('red')
496 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
497 BlinkenBase.sleep(s, sleepsecs)
498
499 def sleeping(s, sleepsecs, remainingsecs):
500 return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
501
502 def _rescroll(s):
503 s.text.see(END)
504 lo, hi = s.text.vbar.get()
505 s.text.vbar.set(1.0 - (hi - lo), 1.0)
506
507 def _display(s, msg):
508 if "\n" in msg:
509 for thisline in msg.split("\n"):
510 s._msg(thisline)
511 return
512 #VerboseUI._msg(s, msg)
513 color = s.gettf().getcolor()
514 rescroll = 1
515 s.textlock.acquire()
516 try:
517 if s.text.vbar.get()[1] != 1.0:
518 rescroll = 0
519 s.text.config(state = NORMAL)
520 if not color in s.tags:
521 s.text.tag_config(color, foreground = color)
522 s.tags.append(color)
523 s.text.insert(END, "\n" + msg, color)
524
525 # Trim down. Not quite sure why I have to say 7 instead of 5,
526 # but so it is.
527 while float(s.text.index(END)) > s.bufferlines + 2.0:
528 s.text.delete(1.0, 2.0)
529
530 if rescroll:
531 s._rescroll()
532 finally:
533 s.text.config(state = DISABLED)
534 s.textlock.release()
535
536