Various bug fixes and tweaks found through 0, 1, and 2
[gc-dialer] / src / gtk_toolbox.py
1 #!/usr/bin/python
2
3 from __future__ import with_statement
4
5 import os
6 import errno
7 import sys
8 import time
9 import traceback
10 import functools
11 import contextlib
12 import warnings
13 import threading
14 import Queue
15
16 import gobject
17 import gtk
18
19
20 @contextlib.contextmanager
21 def flock(path, timeout=-1):
22         WAIT_FOREVER = -1
23         DELAY = 0.1
24         timeSpent = 0
25
26         acquired = False
27
28         while timeSpent <= timeout or timeout == WAIT_FOREVER:
29                 try:
30                         fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
31                         acquired = True
32                         break
33                 except OSError, e:
34                         if e.errno != errno.EEXIST:
35                                 raise
36                 time.sleep(DELAY)
37                 timeSpent += DELAY
38
39         assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
40
41         try:
42                 yield fd
43         finally:
44                 os.unlink(path)
45
46
47 @contextlib.contextmanager
48 def gtk_lock():
49         gtk.gdk.threads_enter()
50         try:
51                 yield
52         finally:
53                 gtk.gdk.threads_leave()
54
55
56 def find_parent_window(widget):
57         while True:
58                 parent = widget.get_parent()
59                 if isinstance(parent, gtk.Window):
60                         return parent
61                 widget = parent
62
63
64 def make_idler(func):
65         """
66         Decorator that makes a generator-function into a function that will continue execution on next call
67         """
68         a = []
69
70         @functools.wraps(func)
71         def decorated_func(*args, **kwds):
72                 if not a:
73                         a.append(func(*args, **kwds))
74                 try:
75                         a[0].next()
76                         return True
77                 except StopIteration:
78                         del a[:]
79                         return False
80
81         return decorated_func
82
83
84 def asynchronous_gtk_message(original_func):
85         """
86         @note Idea came from http://www.aclevername.com/articles/python-webgui/
87         """
88
89         def execute(allArgs):
90                 args, kwargs = allArgs
91                 with gtk_lock():
92                         original_func(*args, **kwargs)
93                 return False
94
95         @functools.wraps(original_func)
96         def delayed_func(*args, **kwargs):
97                 gobject.idle_add(execute, (args, kwargs))
98
99         return delayed_func
100
101
102 def synchronous_gtk_message(original_func):
103         """
104         @note Idea came from http://www.aclevername.com/articles/python-webgui/
105         """
106
107         @functools.wraps(original_func)
108         def immediate_func(*args, **kwargs):
109                 with gtk_lock():
110                         return original_func(*args, **kwargs)
111
112         return immediate_func
113
114
115 def autostart(func):
116         """
117         >>> @autostart
118         ... def grep_sink(pattern):
119         ...     print "Looking for %s" % pattern
120         ...     while True:
121         ...             line = yield
122         ...             if pattern in line:
123         ...                     print line,
124         >>> g = grep_sink("python")
125         Looking for python
126         >>> g.send("Yeah but no but yeah but no")
127         >>> g.send("A series of tubes")
128         >>> g.send("python generators rock!")
129         python generators rock!
130         >>> g.close()
131         """
132
133         @functools.wraps(func)
134         def start(*args, **kwargs):
135                 cr = func(*args, **kwargs)
136                 cr.next()
137                 return cr
138
139         return start
140
141
142 @autostart
143 def printer_sink(format = "%s"):
144         """
145         >>> pr = printer_sink("%r")
146         >>> pr.send("Hello")
147         'Hello'
148         >>> pr.send("5")
149         '5'
150         >>> pr.send(5)
151         5
152         >>> p = printer_sink()
153         >>> p.send("Hello")
154         Hello
155         >>> p.send("World")
156         World
157         >>> # p.throw(RuntimeError, "Goodbye")
158         >>> # p.send("Meh")
159         >>> # p.close()
160         """
161         while True:
162                 item = yield
163                 print format % (item, )
164
165
166 @autostart
167 def null_sink():
168         """
169         Good for uses like with cochain to pick up any slack
170         """
171         while True:
172                 item = yield
173
174
175 @autostart
176 def comap(function, target):
177         """
178         >>> p = printer_sink()
179         >>> cm = comap(lambda x: x+1, p)
180         >>> cm.send((0, ))
181         1
182         >>> cm.send((1.0, ))
183         2.0
184         >>> cm.send((-2, ))
185         -1
186         """
187         while True:
188                 try:
189                         item = yield
190                         mappedItem = function(*item)
191                         target.send(mappedItem)
192                 except StandardError, e:
193                         target.throw(e.__class__, e.message)
194
195
196 def _flush_queue(queue):
197         while not queue.empty():
198                 yield queue.get()
199
200
201 @autostart
202 def queue_sink(queue):
203         """
204         >>> q = Queue.Queue()
205         >>> qs = queue_sink(q)
206         >>> qs.send("Hello")
207         >>> qs.send("World")
208         >>> qs.throw(RuntimeError, "Goodbye")
209         >>> qs.send("Meh")
210         >>> qs.close()
211         >>> print [i for i in _flush_queue(q)]
212         [(None, 'Hello'), (None, 'World'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]
213         """
214         while True:
215                 try:
216                         item = yield
217                         queue.put((None, item))
218                 except StandardError, e:
219                         queue.put((e.__class__, str(e)))
220                 except GeneratorExit:
221                         queue.put((GeneratorExit, None))
222                         raise
223
224
225 def decode_item(item, target):
226         if item[0] is None:
227                 target.send(item[1])
228                 return False
229         elif item[0] is GeneratorExit:
230                 target.close()
231                 return True
232         else:
233                 target.throw(item[0], item[1])
234                 return False
235
236
237 def nonqueue_source(queue, target):
238         isDone = False
239         while not isDone:
240                 item = queue.get()
241                 isDone = decode_item(item, target)
242                 while not queue.empty():
243                         queue.get_nowait()
244
245
246 def threaded_stage(target, thread_factory = threading.Thread):
247         messages = Queue.Queue()
248
249         run_source = functools.partial(nonqueue_source, messages, target)
250         thread = thread_factory(target=run_source)
251         thread.setDaemon(True)
252         thread.start()
253
254         # Sink running in current thread
255         return queue_sink(messages)
256
257
258 class LoginWindow(object):
259
260         def __init__(self, widgetTree):
261                 """
262                 @note Thread agnostic
263                 """
264                 self._dialog = widgetTree.get_widget("loginDialog")
265                 self._parentWindow = widgetTree.get_widget("mainWindow")
266                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
267                 self._usernameEntry = widgetTree.get_widget("usernameentry")
268                 self._passwordEntry = widgetTree.get_widget("passwordentry")
269
270                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
271                 self._serviceCombo.set_model(self._serviceList)
272                 cell = gtk.CellRendererText()
273                 self._serviceCombo.pack_start(cell, True)
274                 self._serviceCombo.add_attribute(cell, 'text', 1)
275                 self._serviceCombo.set_active(0)
276
277                 callbackMapping = {
278                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
279                         "on_loginclose_clicked": self._on_loginclose_clicked,
280                 }
281                 widgetTree.signal_autoconnect(callbackMapping)
282
283         def request_credentials(self,
284                 parentWindow = None,
285                 defaultCredentials = ("", "")
286         ):
287                 """
288                 @note UI Thread
289                 """
290                 if parentWindow is None:
291                         parentWindow = self._parentWindow
292
293                 self._serviceCombo.hide()
294                 self._serviceList.clear()
295
296                 self._usernameEntry.set_text(defaultCredentials[0])
297                 self._passwordEntry.set_text(defaultCredentials[1])
298
299                 try:
300                         self._dialog.set_transient_for(parentWindow)
301                         self._dialog.set_default_response(gtk.RESPONSE_OK)
302                         response = self._dialog.run()
303                         if response != gtk.RESPONSE_OK:
304                                 raise RuntimeError("Login Cancelled")
305
306                         username = self._usernameEntry.get_text()
307                         password = self._passwordEntry.get_text()
308                         self._passwordEntry.set_text("")
309                 finally:
310                         self._dialog.hide()
311
312                 return username, password
313
314         def request_credentials_from(self,
315                 services,
316                 parentWindow = None,
317                 defaultCredentials = ("", "")
318         ):
319                 """
320                 @note UI Thread
321                 """
322                 if parentWindow is None:
323                         parentWindow = self._parentWindow
324
325                 self._serviceList.clear()
326                 for serviceIdserviceName in services:
327                         self._serviceList.append(serviceIdserviceName)
328                 self._serviceCombo.set_active(0)
329                 self._serviceCombo.show()
330
331                 self._usernameEntry.set_text(defaultCredentials[0])
332                 self._passwordEntry.set_text(defaultCredentials[1])
333
334                 try:
335                         self._dialog.set_transient_for(parentWindow)
336                         self._dialog.set_default_response(gtk.RESPONSE_OK)
337                         response = self._dialog.run()
338                         if response != gtk.RESPONSE_OK:
339                                 raise RuntimeError("Login Cancelled")
340
341                         username = self._usernameEntry.get_text()
342                         password = self._passwordEntry.get_text()
343                 finally:
344                         self._dialog.hide()
345
346                 itr = self._serviceCombo.get_active_iter()
347                 serviceId = int(self._serviceList.get_value(itr, 0))
348                 self._serviceList.clear()
349                 return serviceId, username, password
350
351         def _on_loginbutton_clicked(self, *args):
352                 self._dialog.response(gtk.RESPONSE_OK)
353
354         def _on_loginclose_clicked(self, *args):
355                 self._dialog.response(gtk.RESPONSE_CANCEL)
356
357
358 def safecall(f, errorDisplay=None, default=None, exception=Exception):
359         '''
360         Returns modified f. When the modified f is called and throws an
361         exception, the default value is returned
362         '''
363         def _safecall(*args, **argv):
364                 try:
365                         return f(*args,**argv)
366                 except exception, e:
367                         if errorDisplay is not None:
368                                 errorDisplay.push_exception(e)
369                         return default
370         return _safecall
371
372
373 class ErrorDisplay(object):
374
375         def __init__(self, widgetTree):
376                 super(ErrorDisplay, self).__init__()
377                 self.__errorBox = widgetTree.get_widget("errorEventBox")
378                 self.__errorDescription = widgetTree.get_widget("errorDescription")
379                 self.__errorClose = widgetTree.get_widget("errorClose")
380                 self.__parentBox = self.__errorBox.get_parent()
381
382                 self.__errorBox.connect("button_release_event", self._on_close)
383
384                 self.__messages = []
385                 self.__parentBox.remove(self.__errorBox)
386
387         def push_message_with_lock(self, message):
388                 with gtk_lock():
389                         self.push_message(message)
390
391         def push_message(self, message):
392                 if 0 < len(self.__messages):
393                         self.__messages.append(message)
394                 else:
395                         self.__show_message(message)
396
397         def push_exception_with_lock(self, exception = None, stacklevel=3):
398                 with gtk_lock():
399                         self.push_exception(exception, stacklevel=stacklevel)
400
401         def push_exception(self, exception = None, stacklevel=2):
402                 if exception is None:
403                         userMessage = str(sys.exc_value)
404                         warningMessage = str(traceback.format_exc())
405                 else:
406                         userMessage = str(exception)
407                         warningMessage = str(exception)
408                 self.push_message(userMessage)
409                 warnings.warn(warningMessage, stacklevel=stacklevel)
410
411         def pop_message(self):
412                 if 0 < len(self.__messages):
413                         self.__show_message(self.__messages[0])
414                         del self.__messages[0]
415                 else:
416                         self.__hide_message()
417
418         def _on_close(self, *args):
419                 self.pop_message()
420
421         def __show_message(self, message):
422                 self.__errorDescription.set_text(message)
423                 self.__parentBox.pack_start(self.__errorBox, False, False)
424                 self.__parentBox.reorder_child(self.__errorBox, 1)
425
426         def __hide_message(self):
427                 self.__errorDescription.set_text("")
428                 self.__parentBox.remove(self.__errorBox)
429
430
431 class DummyErrorDisplay(object):
432
433         def __init__(self):
434                 super(DummyErrorDisplay, self).__init__()
435
436                 self.__messages = []
437
438         def push_message_with_lock(self, message):
439                 self.push_message(message)
440
441         def push_message(self, message):
442                 if 0 < len(self.__messages):
443                         self.__messages.append(message)
444                 else:
445                         self.__show_message(message)
446
447         def push_exception(self, exception = None):
448                 if exception is None:
449                         warningMessage = traceback.format_exc()
450                 else:
451                         warningMessage = exception
452                 warnings.warn(warningMessage, stacklevel=3)
453
454         def pop_message(self):
455                 if 0 < len(self.__messages):
456                         self.__show_message(self.__messages[0])
457                         del self.__messages[0]
458
459         def __show_message(self, message):
460                 warnings.warn(message, stacklevel=2)
461
462
463 class MessageBox(gtk.MessageDialog):
464
465         def __init__(self, message):
466                 parent = None
467                 gtk.MessageDialog.__init__(
468                         self,
469                         parent,
470                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
471                         gtk.MESSAGE_ERROR,
472                         gtk.BUTTONS_OK,
473                         message,
474                 )
475                 self.set_default_response(gtk.RESPONSE_OK)
476                 self.connect('response', self._handle_clicked)
477
478         def _handle_clicked(self, *args):
479                 self.destroy()
480
481
482 class MessageBox2(gtk.MessageDialog):
483
484         def __init__(self, message):
485                 parent = None
486                 gtk.MessageDialog.__init__(
487                         self,
488                         parent,
489                         gtk.DIALOG_DESTROY_WITH_PARENT,
490                         gtk.MESSAGE_ERROR,
491                         gtk.BUTTONS_OK,
492                         message,
493                 )
494                 self.set_default_response(gtk.RESPONSE_OK)
495                 self.connect('response', self._handle_clicked)
496
497         def _handle_clicked(self, *args):
498                 self.destroy()
499
500
501 class PopupCalendar(object):
502
503         def __init__(self, parent, displayDate, title = ""):
504                 self._displayDate = displayDate
505
506                 self._calendar = gtk.Calendar()
507                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
508                 self._calendar.select_day(self._displayDate.day)
509                 self._calendar.set_display_options(
510                         gtk.CALENDAR_SHOW_HEADING |
511                         gtk.CALENDAR_SHOW_DAY_NAMES |
512                         gtk.CALENDAR_NO_MONTH_CHANGE |
513                         0
514                 )
515                 self._calendar.connect("day-selected", self._on_day_selected)
516
517                 self._popupWindow = gtk.Window()
518                 self._popupWindow.set_title(title)
519                 self._popupWindow.add(self._calendar)
520                 self._popupWindow.set_transient_for(parent)
521                 self._popupWindow.set_modal(True)
522                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
523                 self._popupWindow.set_skip_pager_hint(True)
524                 self._popupWindow.set_skip_taskbar_hint(True)
525
526         def run(self):
527                 self._popupWindow.show_all()
528
529         def _on_day_selected(self, *args):
530                 try:
531                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
532                         self._calendar.select_day(self._displayDate.day)
533                 except StandardError, e:
534                         warnings.warn(e.message)
535
536
537 class QuickAddView(object):
538
539         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
540                 self._errorDisplay = errorDisplay
541                 self._manager = None
542                 self._signalSink = signalSink
543
544                 self._clipboard = gtk.clipboard_get()
545
546                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
547                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
548                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
549                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
550                 self._onAddId = None
551                 self._onAddClickedId = None
552                 self._onAddReleasedId = None
553                 self._addToEditTimerId = None
554                 self._onClearId = None
555                 self._onPasteId = None
556
557         def enable(self, manager):
558                 self._manager = manager
559
560                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
561                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
562                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
563                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
564                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
565
566         def disable(self):
567                 self._manager = None
568
569                 self._addTaskButton.disconnect(self._onAddId)
570                 self._addTaskButton.disconnect(self._onAddClickedId)
571                 self._addTaskButton.disconnect(self._onAddReleasedId)
572                 self._pasteTaskNameButton.disconnect(self._onPasteId)
573                 self._clearTaskNameButton.disconnect(self._onClearId)
574
575         def set_addability(self, addability):
576                 self._addTaskButton.set_sensitive(addability)
577
578         def _on_add(self, *args):
579                 try:
580                         name = self._taskNameEntry.get_text()
581                         self._taskNameEntry.set_text("")
582
583                         self._signalSink.stage.send(("add", name))
584                 except StandardError, e:
585                         self._errorDisplay.push_exception()
586
587         def _on_add_edit(self, *args):
588                 try:
589                         name = self._taskNameEntry.get_text()
590                         self._taskNameEntry.set_text("")
591
592                         self._signalSink.stage.send(("add-edit", name))
593                 except StandardError, e:
594                         self._errorDisplay.push_exception()
595
596         def _on_add_pressed(self, widget):
597                 try:
598                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
599                 except StandardError, e:
600                         self._errorDisplay.push_exception()
601
602         def _on_add_released(self, widget):
603                 try:
604                         if self._addToEditTimerId is not None:
605                                 gobject.source_remove(self._addToEditTimerId)
606                         self._addToEditTimerId = None
607                 except StandardError, e:
608                         self._errorDisplay.push_exception()
609
610         def _on_paste(self, *args):
611                 try:
612                         entry = self._taskNameEntry.get_text()
613                         addedText = self._clipboard.wait_for_text()
614                         if addedText:
615                                 entry += addedText
616                         self._taskNameEntry.set_text(entry)
617                 except StandardError, e:
618                         self._errorDisplay.push_exception()
619
620         def _on_clear(self, *args):
621                 try:
622                         self._taskNameEntry.set_text("")
623                 except StandardError, e:
624                         self._errorDisplay.push_exception()
625
626
627 class TapOrHold(object):
628
629         def __init__(self, widget):
630                 self._widget = widget
631                 self._isTap = True
632                 self._isPointerInside = True
633                 self._holdTimeoutId = None
634                 self._tapTimeoutId = None
635                 self._taps = 0
636
637                 self._bpeId = None
638                 self._breId = None
639                 self._eneId = None
640                 self._lneId = None
641
642         def enable(self):
643                 self._bpeId = self._widget.connect("button-press-event", self._on_button_press)
644                 self._breId = self._widget.connect("button-release-event", self._on_button_release)
645                 self._eneId = self._widget.connect("enter-notify-event", self._on_enter)
646                 self._lneId = self._widget.connect("leave-notify-event", self._on_leave)
647
648         def disable(self):
649                 self._widget.disconnect(self._bpeId)
650                 self._widget.disconnect(self._breId)
651                 self._widget.disconnect(self._eneId)
652                 self._widget.disconnect(self._lneId)
653
654         def on_tap(self, taps):
655                 print "TAP", taps
656
657         def on_hold(self, taps):
658                 print "HOLD", taps
659
660         def on_holding(self):
661                 print "HOLDING"
662
663         def on_cancel(self):
664                 print "CANCEL"
665
666         def _on_button_press(self, *args):
667                 # Hack to handle weird notebook behavior
668                 self._isPointerInside = True
669                 self._isTap = True
670
671                 if self._tapTimeoutId is not None:
672                         gobject.source_remove(self._tapTimeoutId)
673                         self._tapTimeoutId = None
674
675                 # Handle double taps
676                 if self._holdTimeoutId is None:
677                         self._tapTimeoutId = None
678
679                         self._taps = 1
680                         self._holdTimeoutId = gobject.timeout_add(1000, self._on_hold_timeout)
681                 else:
682                         self._taps = 2
683
684         def _on_button_release(self, *args):
685                 assert self._tapTimeoutId is None
686                 # Handle release after timeout if user hasn't double-clicked
687                 self._tapTimeoutId = gobject.timeout_add(100, self._on_tap_timeout)
688
689         def _on_actual_press(self, *args):
690                 if self._holdTimeoutId is not None:
691                         gobject.source_remove(self._holdTimeoutId)
692                 self._holdTimeoutId = None
693
694                 if self._isPointerInside:
695                         if self._isTap:
696                                 self.on_tap(self._taps)
697                         else:
698                                 self.on_hold(self._taps)
699                 else:
700                         self.on_cancel()
701
702         def _on_tap_timeout(self, *args):
703                 self._tapTimeoutId = None
704                 self._on_actual_press()
705                 return False
706
707         def _on_hold_timeout(self, *args):
708                 self._holdTimeoutId = None
709                 self._isTap = False
710                 self.on_holding()
711                 return False
712
713         def _on_enter(self, *args):
714                 self._isPointerInside = True
715
716         def _on_leave(self, *args):
717                 self._isPointerInside = False
718
719
720 if __name__ == "__main__":
721         if False:
722                 import datetime
723                 cal = PopupCalendar(None, datetime.datetime.now())
724                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
725                 cal.run()
726
727         gtk.main()