]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Tk.py
/offlineimap/head: changeset 194
[offlineimap] / offlineimap / head / offlineimap / ui / Tk.py
1 # Tk UI
2 # Copyright (C) 2002 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 Tkinter import *
20 import tkFont
21 from threading import *
22 import thread, traceback, time
23 from StringIO import StringIO
24 from ScrolledText import ScrolledText
25 from offlineimap import threadutil, version
26 from Queue import Queue
27 from UIBase import UIBase
28
29 class PasswordDialog:
30 def __init__(self, accountname, config, master=None):
31 self.top = Toplevel(master)
32 self.top.title(version.productname + " Password Entry")
33 self.label = Label(self.top,
34 text = "%s: Enter password for %s on %s: " % \
35 (accountname, config.get(accountname, "remoteuser"),
36 config.get(accountname, "remotehost")))
37 self.label.pack()
38
39 self.entry = Entry(self.top, show='*')
40 self.entry.bind("<Return>", self.ok)
41 self.entry.pack()
42 self.entry.focus_force()
43
44 self.button = Button(self.top, text = "OK", command=self.ok)
45 self.button.pack()
46
47 self.entry.focus_force()
48 self.top.wait_window(self.label)
49
50 def ok(self, args = None):
51 self.password = self.entry.get()
52 self.top.destroy()
53
54 def getpassword(self):
55 return self.password
56
57 class TextOKDialog:
58 def __init__(self, title, message, blocking = 1, master = None):
59 if not master:
60 self.top = Tk()
61 else:
62 self.top = Toplevel(master)
63 self.top.title(title)
64 self.text = ScrolledText(self.top, font = "Courier 10")
65 self.text.pack()
66 self.text.insert(END, message)
67 self.text['state'] = DISABLED
68 self.button = Button(self.top, text = "OK", command=self.ok)
69 self.button.pack()
70
71 if blocking:
72 self.top.wait_window(self.button)
73
74 def ok(self):
75 self.top.destroy()
76
77
78
79 class ThreadFrame(Frame):
80 def __init__(self, master=None):
81 self.threadextraframe = None
82 self.thread = currentThread()
83 self.threadid = thread.get_ident()
84 Frame.__init__(self, master, relief = RIDGE, borderwidth = 2)
85 self.pack(fill = 'x')
86 self.threadlabel = Label(self, foreground = '#FF0000',
87 text ="Thread %d (%s)" % (self.threadid,
88 self.thread.getName()))
89 self.threadlabel.pack()
90 self.setthread(currentThread())
91
92 self.account = "Unknown"
93 self.mailbox = "Unknown"
94 self.loclabel = Label(self,
95 text = "Account/mailbox information unknown")
96 #self.loclabel.pack()
97
98 self.updateloclabel()
99
100 self.message = Label(self, text="Messages will appear here.\n",
101 foreground = '#0000FF')
102 self.message.pack(fill = 'x')
103
104 def setthread(self, newthread):
105 if newthread:
106 self.threadlabel['text'] = newthread.getName()
107 else:
108 self.threadlabel['text'] = "No thread"
109 self.destroythreadextraframe()
110
111 def destroythreadextraframe(self):
112 if self.threadextraframe:
113 self.threadextraframe.destroy()
114 self.threadextraframe = None
115
116 def getthreadextraframe(self):
117 if self.threadextraframe:
118 return self.threadextraframe
119 self.threadextraframe = Frame(self)
120 self.threadextraframe.pack(fill = 'x')
121 return self.threadextraframe
122
123 def setaccount(self, account):
124 self.account = account
125 self.mailbox = "Unknown"
126 self.updateloclabel()
127
128 def setmailbox(self, mailbox):
129 self.mailbox = mailbox
130 self.updateloclabel()
131
132 def updateloclabel(self):
133 self.loclabel['text'] = "Processing %s: %s" % (self.account,
134 self.mailbox)
135
136 def appendmessage(self, newtext):
137 self.message['text'] += "\n" + newtext
138
139 def setmessage(self, newtext):
140 self.message['text'] = newtext
141
142
143 class VerboseUI(UIBase):
144 def isusable(s):
145 try:
146 Tk().destroy()
147 return 1
148 except TclError:
149 return 0
150
151 def _createTopWindow(self, doidlevac = 1):
152 self.top = Tk()
153 self.top.title(version.productname + " " + version.versionstr)
154 self.threadframes = {}
155 self.availablethreadframes = []
156 self.tflock = Lock()
157 self.notdeleted = 1
158
159 t = threadutil.ExitNotifyThread(target = self._runmainloop,
160 name = "Tk Mainloop")
161 t.setDaemon(1)
162 t.start()
163
164 if doidlevac:
165 t = threadutil.ExitNotifyThread(target = self.idlevacuum,
166 name = "Tk idle vacuum")
167 t.setDaemon(1)
168 t.start()
169
170 def _runmainloop(s):
171 s.top.mainloop()
172 s.notdeleted = 0
173
174 def getpass(s, accountname, config):
175 pd = PasswordDialog(accountname, config)
176 return pd.getpassword()
177
178 def gettf(s, newtype=ThreadFrame, master = None):
179 if master == None:
180 master = s.top
181 threadid = thread.get_ident()
182 s.tflock.acquire()
183 try:
184 if threadid in s.threadframes:
185 return s.threadframes[threadid]
186 if len(s.availablethreadframes):
187 tf = s.availablethreadframes.pop(0)
188 tf.setthread(currentThread())
189 else:
190 tf = newtype(master)
191 s.threadframes[threadid] = tf
192 return tf
193 finally:
194 s.tflock.release()
195
196 def _msg(s, msg):
197 s.gettf().setmessage(msg)
198
199 def threadExited(s, thread):
200 threadid = thread.threadid
201 s.tflock.acquire()
202 if threadid in s.threadframes:
203 tf = s.threadframes[threadid]
204 tf.setthread(None)
205 tf.setaccount("Unknown")
206 tf.setmessage("Idle")
207 s.availablethreadframes.append(tf)
208 del s.threadframes[threadid]
209 s.tflock.release()
210
211 def idlevacuum(s):
212 while s.notdeleted:
213 time.sleep(10)
214 s.tflock.acquire()
215 while len(s.availablethreadframes):
216 tf = s.availablethreadframes.pop()
217 tf.destroy()
218 s.tflock.release()
219
220 def threadException(s, thread):
221 msg = "Thread '%s' terminated with exception:\n%s" % \
222 (thread.getName(), thread.getExitStackTrace())
223 print msg
224
225 s.top.destroy()
226 s.top = None
227 TextOKDialog("Thread Exception", msg)
228 s.terminate(100)
229
230 def mainException(s):
231 sbuf = StringIO()
232 traceback.print_exc(file = sbuf)
233 msg = "Main program terminated with exception:\n" + sbuf.getvalue()
234 print msg
235
236 s.top.destroy()
237 s.top = None
238 TextOKDialog("Main Program Exception", msg)
239
240 def warn(s, msg):
241 TextOKDialog("OfflineIMAP Warning", msg)
242
243 def showlicense(s):
244 TextOKDialog(version.productname + " License",
245 version.bigcopyright + "\n" +
246 version.homepage + "\n\n" + version.license,
247 blocking = 0, master = s.top)
248
249
250 def init_banner(s):
251 s._createTopWindow()
252 s._msg(version.productname + " " + version.versionstr + ", " +\
253 version.copyright)
254 tf = s.gettf().getthreadextraframe()
255
256 b = Button(tf, text = "About", command = s.showlicense)
257 b.pack(side = LEFT)
258
259 b = Button(tf, text = "Exit", command = s.terminate)
260 b.pack(side = RIGHT)
261
262 def deletingmessages(s, uidlist, destlist):
263 ds = s.folderlist(destlist)
264 s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
265
266 def _sleep_cancel(s, args = None):
267 s.sleeping_abort = 1
268
269 def sleep(s, sleepsecs):
270 s.sleeping_abort = 0
271 tf = s.gettf().getthreadextraframe()
272
273 sleepbut = Button(tf, text = 'Sync immediately',
274 command = s._sleep_cancel)
275 sleepbut.pack()
276 UIBase.sleep(s, sleepsecs)
277
278 def sleeping(s, sleepsecs, remainingsecs):
279 if remainingsecs:
280 s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
281 remainingsecs % 60))
282 else:
283 s._msg("Wait done; synchronizing now.")
284 s.gettf().destroythreadextraframe()
285 time.sleep(sleepsecs)
286 return s.sleeping_abort
287
288 TkUI = VerboseUI
289
290 class LEDCanvas(Canvas):
291 def createLEDLock(self):
292 self.ledlock = Lock()
293 def acquireLEDLock(self):
294 self.ledlock.acquire()
295 def releaseLEDLock(self):
296 self.ledlock.release()
297 def setLEDCount(self, arg):
298 self.ledcount = arg
299 def getLEDCount(self):
300 return self.ledcount
301 def incLEDCount(self):
302 self.ledcount += 1
303
304 class LEDThreadFrame:
305 def __init__(self, master):
306 self.canvas = master
307 self.color = ''
308 try:
309 self.canvas.acquireLEDLock()
310 startpos = 5 + self.canvas.getLEDCount() * 10
311 self.canvas.incLEDCount()
312 finally:
313 self.canvas.releaseLEDLock()
314 self.ovalid = self.canvas.create_oval(startpos, 5, startpos + 5,
315 10, fill = 'gray',
316 outline = '#303030')
317
318 def setcolor(self, newcolor):
319 if newcolor != self.color:
320 self.canvas.itemconfigure(self.ovalid, fill = newcolor)
321 self.color = newcolor
322
323 def getcolor(self):
324 return self.color
325
326 def setthread(self, newthread):
327 if newthread:
328 self.setcolor('gray')
329 else:
330 self.setcolor('black')
331
332 def destroythreadextraframe(self):
333 pass
334
335 def getthreadextraframe(self):
336 raise NotImplementedError
337
338 def setaccount(self, account):
339 pass
340 def setmailbox(self, mailbox):
341 pass
342 def updateloclabel(self):
343 pass
344 def appendmessage(self, newtext):
345 pass
346 def setmessage(self, newtext):
347 pass
348
349
350 class Blinkenlights(VerboseUI):
351 def _createTopWindow(self):
352 VerboseUI._createTopWindow(self, 0)
353 #self.top.resizable(width = 0, height = 0)
354 self.top.configure(background = 'black', bd = 0)
355 c = LEDCanvas(self.top, background = 'black', height = 20, bd = 0,
356 highlightthickness = 0)
357 c.setLEDCount(0)
358 c.createLEDLock()
359 self.canvas = c
360 c.pack(side = BOTTOM, expand = 0, fill = X)
361 widthmetric = tkFont.Font(family = 'Helvetica', size = 8).measure("0")
362 self.loglines = 5
363 if self.config.has_option("ui.Tk.Blinkenlights", "loglines"):
364 self.loglines = self.config.getint("ui.Tk.Blinkenlights",
365 "loglines")
366 self.bufferlines = 500
367 if self.config.has_option("ui.Tk.Blinkenlights", "bufferlines"):
368 self.bufferlines = self.config.getint("ui.tk.Blinkenlights",
369 "bufferlines")
370 self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
371 font = ("Helvetica", 8),
372 bd = 0, highlightthickness = 0, setgrid = 0,
373 state = DISABLED, height = self.loglines,
374 wrap = NONE, width = 10)
375 self.text.vbar.configure(background = '#000050',
376 activebackground = 'blue',
377 highlightbackground = 'black',
378 troughcolor = "black", bd = 0,
379 elementborderwidth = 2)
380
381 self.textenabled = 0
382 self.tags = []
383 self.textlock = Lock()
384
385 def gettf(s, newtype=LEDThreadFrame):
386 return VerboseUI.gettf(s, newtype, s.canvas)
387
388 def init_banner(s):
389 s._createTopWindow()
390 menubar = Menu(s.top, activebackground = "black",
391 activeforeground = "white",
392 activeborderwidth = 0,
393 background = "black", foreground = "blue",
394 font = ("Helvetica", 8), bd = 0)
395 menubar.add_command(label = "About", command = s.showlicense)
396 menubar.add_command(label = "Show Log", command = s._togglelog)
397 menubar.add_command(label = "Exit", command = s.terminate)
398 s.top.config(menu = menubar)
399 s.menubar = menubar
400 s.gettf().setcolor('red')
401 s._msg(version.banner)
402 s.text.see(END)
403 s.top.resizable(width = 0, height = 0)
404 if s.config.has_option("ui.Tk.Blinkenlights", "showlog") and \
405 s.config.getboolean("ui.Tk.Blinkenlights", "showlog"):
406 s._togglelog()
407
408 def _togglelog(s):
409 if s.textenabled:
410 s.oldtextheight = s.text.winfo_height()
411 s.text.pack_forget()
412 s.textenabled = 0
413 s.menubar.entryconfig('Hide Log', label = 'Show Log')
414 s.top.update()
415 s.top.geometry("")
416 s.top.update()
417 s.top.resizable(width = 0, height = 0)
418 s.top.update()
419
420 else:
421 s.text.pack(side = BOTTOM, expand = 1, fill = BOTH)
422 s.textenabled = 1
423 s.top.update()
424 s.top.geometry("")
425 s.menubar.entryconfig('Show Log', label = 'Hide Log')
426 s._rescroll()
427 s.top.resizable(width = 1, height = 1)
428
429
430 def acct(s, accountname):
431 s.gettf().setcolor('purple')
432 VerboseUI.acct(s, accountname)
433
434 def connecting(s, hostname, port):
435 s.gettf().setcolor('gray')
436 VerboseUI.connecting(s, hostname, port)
437
438 def syncfolders(s, srcrepos, destrepos):
439 s.gettf().setcolor('blue')
440 VerboseUI.syncfolders(s, srcrepos, destrepos)
441
442 def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
443 s.gettf().setcolor('cyan')
444 VerboseUI.syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder)
445
446 def loadmessagelist(s, repos, folder):
447 s.gettf().setcolor('green')
448 s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
449 folder.getvisiblename()))
450
451 def syncingmessages(s, sr, sf, dr, df):
452 s.gettf().setcolor('blue')
453 VerboseUI.syncingmessages(s, sr, sf, dr, df)
454
455 def copyingmessage(s, uid, src, destlist):
456 s.gettf().setcolor('orange')
457 VerboseUI.copyingmessage(s, uid, src, destlist)
458
459 def deletingmessages(s, uidlist, destlist):
460 s.gettf().setcolor('red')
461 VerboseUI.deletingmessages(s, uidlist, destlist)
462
463 def deletingmessage(s, uid, destlist):
464 s.gettf().setcolor('red')
465 VerboseUI.deletingmessage(s, uid, destlist)
466
467 def addingflags(s, uid, flags, destlist):
468 s.gettf().setcolor('yellow')
469 VerboseUI.addingflags(s, uid, flags, destlist)
470
471 def deletingflags(s, uid, flags, destlist):
472 s.gettf().setcolor('pink')
473 VerboseUI.deletingflags(s, uid, flags, destlist)
474
475 def threadExited(s, thread):
476 threadid = thread.threadid
477 s.tflock.acquire()
478 try:
479 if threadid in s.threadframes:
480 tf = s.threadframes[threadid]
481 del s.threadframes[threadid]
482 s.availablethreadframes.append(tf)
483 tf.setthread(None)
484 finally:
485 s.tflock.release()
486
487 def sleep(s, sleepsecs):
488 s.sleeping_abort = 0
489 s.menubar.add_command(label = "Sync now", command = s._sleep_cancel)
490 s.gettf().setcolor('red')
491 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
492 UIBase.sleep(s, sleepsecs)
493
494 def _rescroll(s):
495 s.text.see(END)
496 lo, hi = s.text.vbar.get()
497 s.text.vbar.set(1.0 - (hi - lo), 1.0)
498
499 def _msg(s, msg):
500 if "\n" in msg:
501 for thisline in msg.split("\n"):
502 s._msg(thisline)
503 return
504 VerboseUI._msg(s, msg)
505 color = s.gettf().getcolor()
506 rescroll = 1
507 #print s.text.vbar.get()[1]
508 if s.text.vbar.get()[1] != 1.0:
509 rescroll = 0
510
511 s.textlock.acquire()
512 try:
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 def sleeping(s, sleepsecs, remainingsecs):
531 if remainingsecs:
532 s.menubar.entryconfig('end', label = "Sync now (%d:%02d remain)" % \
533 (remainingsecs / 60, remainingsecs % 60))
534 else:
535 s.menubar.delete('end')
536 s.gettf().setcolor('black')
537 if s.gettf().getcolor() == 'red':
538 s.gettf().setcolor('black')
539 else:
540 s.gettf().setcolor('red')
541 time.sleep(sleepsecs)
542 return s.sleeping_abort
543