70809496478475253431d9e3e315f0583a7823e9
[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 null_sink():
144         """
145         Good for uses like with cochain to pick up any slack
146         """
147         while True:
148                 item = yield
149
150
151 @autostart
152 def comap(function, target):
153         """
154         >>> p = printer_sink()
155         >>> cm = comap(lambda x: x+1, p)
156         >>> cm.send(0)
157         1
158         >>> cm.send(1.0)
159         2.0
160         >>> cm.send(-2)
161         -1
162         >>> # cm.throw(RuntimeError, "Goodbye")
163         >>> # cm.send(0)
164         >>> # cm.send(1.0)
165         >>> # cm.close()
166         """
167         while True:
168                 try:
169                         item = yield
170                         mappedItem = function(*item)
171                         target.send(mappedItem)
172                 except StandardError, e:
173                         target.throw(e.__class__, e.message)
174
175
176 @autostart
177 def queue_sink(queue):
178         """
179         >>> q = Queue.Queue()
180         >>> qs = queue_sink(q)
181         >>> qs.send("Hello")
182         >>> qs.send("World")
183         >>> qs.throw(RuntimeError, "Goodbye")
184         >>> qs.send("Meh")
185         >>> qs.close()
186         >>> print [i for i in _flush_queue(q)]
187         [(None, 'Hello'), (None, 'World'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, None)]
188         """
189         while True:
190                 try:
191                         item = yield
192                         queue.put((None, item))
193                 except StandardError, e:
194                         queue.put((e.__class__, e.message))
195                 except GeneratorExit:
196                         queue.put((GeneratorExit, None))
197                         raise
198
199
200 def decode_item(item, target):
201         if item[0] is None:
202                 target.send(item[1])
203                 return False
204         elif item[0] is GeneratorExit:
205                 target.close()
206                 return True
207         else:
208                 target.throw(item[0], item[1])
209                 return False
210
211
212 def nonqueue_source(queue, target):
213         """
214         >>> q = Queue.Queue()
215         >>> for i in [
216         ...     (None, 'Hello'),
217         ...     (None, 'World'),
218         ...     (GeneratorExit, None),
219         ...     ]:
220         ...     q.put(i)
221         >>> qs = queue_source(q, printer_sink())
222         Hello
223         """
224         isDone = False
225         while not isDone:
226                 item = queue.get()
227                 isDone = decode_item(item, target)
228                 while not queue.empty():
229                         queue.get_nowait()
230
231
232 def threaded_stage(target, thread_factory = threading.Thread):
233         messages = Queue.Queue()
234
235         run_source = functools.partial(nonqueue_source, messages, target)
236         thread = thread_factory(target=run_source)
237         thread.setDaemon(True)
238         thread.start()
239
240         # Sink running in current thread
241         return queue_sink(messages)
242
243
244 class LoginWindow(object):
245
246         def __init__(self, widgetTree):
247                 """
248                 @note Thread agnostic
249                 """
250                 self._dialog = widgetTree.get_widget("loginDialog")
251                 self._parentWindow = widgetTree.get_widget("mainWindow")
252                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
253                 self._usernameEntry = widgetTree.get_widget("usernameentry")
254                 self._passwordEntry = widgetTree.get_widget("passwordentry")
255
256                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
257                 self._serviceCombo.set_model(self._serviceList)
258                 cell = gtk.CellRendererText()
259                 self._serviceCombo.pack_start(cell, True)
260                 self._serviceCombo.add_attribute(cell, 'text', 1)
261                 self._serviceCombo.set_active(0)
262
263                 callbackMapping = {
264                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
265                         "on_loginclose_clicked": self._on_loginclose_clicked,
266                 }
267                 widgetTree.signal_autoconnect(callbackMapping)
268
269         def request_credentials(self, parentWindow = None):
270                 """
271                 @note UI Thread
272                 """
273                 if parentWindow is None:
274                         parentWindow = self._parentWindow
275
276                 self._serviceCombo.hide()
277                 self._serviceList.clear()
278
279                 try:
280                         self._dialog.set_transient_for(parentWindow)
281                         self._dialog.set_default_response(gtk.RESPONSE_OK)
282                         response = self._dialog.run()
283                         if response != gtk.RESPONSE_OK:
284                                 raise RuntimeError("Login Cancelled")
285
286                         username = self._usernameEntry.get_text()
287                         password = self._passwordEntry.get_text()
288                         self._passwordEntry.set_text("")
289                 finally:
290                         self._dialog.hide()
291
292                 return username, password
293
294         def request_credentials_from(self,
295                 services,
296                 parentWindow = None,
297                 defaultCredentials = ("", "")
298         ):
299                 """
300                 @note UI Thread
301                 """
302                 if parentWindow is None:
303                         parentWindow = self._parentWindow
304
305                 self._serviceList.clear()
306                 for serviceIdserviceName in services:
307                         self._serviceList.append(serviceIdserviceName)
308                 self._serviceCombo.set_active(0)
309                 self._serviceCombo.show()
310
311                 self._usernameEntry.set_text(defaultCredentials[0])
312                 self._passwordEntry.set_text(defaultCredentials[1])
313
314                 try:
315                         self._dialog.set_transient_for(parentWindow)
316                         self._dialog.set_default_response(gtk.RESPONSE_OK)
317                         response = self._dialog.run()
318                         if response != gtk.RESPONSE_OK:
319                                 raise RuntimeError("Login Cancelled")
320
321                         username = self._usernameEntry.get_text()
322                         password = self._passwordEntry.get_text()
323                 finally:
324                         self._dialog.hide()
325
326                 itr = self._serviceCombo.get_active_iter()
327                 serviceId = int(self._serviceList.get_value(itr, 0))
328                 self._serviceList.clear()
329                 return serviceId, username, password
330
331         def _on_loginbutton_clicked(self, *args):
332                 self._dialog.response(gtk.RESPONSE_OK)
333
334         def _on_loginclose_clicked(self, *args):
335                 self._dialog.response(gtk.RESPONSE_CANCEL)
336
337
338 def safecall(f, errorDisplay=None, default=None, exception=Exception):
339         '''
340         Returns modified f. When the modified f is called and throws an
341         exception, the default value is returned
342         '''
343         def _safecall(*args, **argv):
344                 try:
345                         return f(*args,**argv)
346                 except exception, e:
347                         if errorDisplay is not None:
348                                 errorDisplay.push_exception(e)
349                         return default
350         return _safecall
351
352
353 class ErrorDisplay(object):
354
355         def __init__(self, widgetTree):
356                 super(ErrorDisplay, self).__init__()
357                 self.__errorBox = widgetTree.get_widget("errorEventBox")
358                 self.__errorDescription = widgetTree.get_widget("errorDescription")
359                 self.__errorClose = widgetTree.get_widget("errorClose")
360                 self.__parentBox = self.__errorBox.get_parent()
361
362                 self.__errorBox.connect("button_release_event", self._on_close)
363
364                 self.__messages = []
365                 self.__parentBox.remove(self.__errorBox)
366
367         def push_message_with_lock(self, message):
368                 with gtk_lock():
369                         self.push_message(message)
370
371         def push_message(self, message):
372                 if 0 < len(self.__messages):
373                         self.__messages.append(message)
374                 else:
375                         self.__show_message(message)
376
377         def push_exception_with_lock(self, exception = None, stacklevel=3):
378                 with gtk_lock():
379                         self.push_exception(exception, stacklevel=stacklevel)
380
381         def push_exception(self, exception = None, stacklevel=2):
382                 if exception is None:
383                         userMessage = str(sys.exc_value)
384                         warningMessage = str(traceback.format_exc())
385                 else:
386                         userMessage = str(exception)
387                         warningMessage = str(exception)
388                 self.push_message(userMessage)
389                 warnings.warn(warningMessage, stacklevel=stacklevel)
390
391         def pop_message(self):
392                 if 0 < len(self.__messages):
393                         self.__show_message(self.__messages[0])
394                         del self.__messages[0]
395                 else:
396                         self.__hide_message()
397
398         def _on_close(self, *args):
399                 self.pop_message()
400
401         def __show_message(self, message):
402                 self.__errorDescription.set_text(message)
403                 self.__parentBox.pack_start(self.__errorBox, False, False)
404                 self.__parentBox.reorder_child(self.__errorBox, 1)
405
406         def __hide_message(self):
407                 self.__errorDescription.set_text("")
408                 self.__parentBox.remove(self.__errorBox)
409
410
411 class DummyErrorDisplay(object):
412
413         def __init__(self):
414                 super(DummyErrorDisplay, self).__init__()
415
416                 self.__messages = []
417
418         def push_message_with_lock(self, message):
419                 self.push_message(message)
420
421         def push_message(self, message):
422                 if 0 < len(self.__messages):
423                         self.__messages.append(message)
424                 else:
425                         self.__show_message(message)
426
427         def push_exception(self, exception = None):
428                 if exception is None:
429                         warningMessage = traceback.format_exc()
430                 else:
431                         warningMessage = exception
432                 warnings.warn(warningMessage, stacklevel=3)
433
434         def pop_message(self):
435                 if 0 < len(self.__messages):
436                         self.__show_message(self.__messages[0])
437                         del self.__messages[0]
438
439         def __show_message(self, message):
440                 warnings.warn(message, stacklevel=2)
441
442
443 class MessageBox(gtk.MessageDialog):
444
445         def __init__(self, message):
446                 parent = None
447                 gtk.MessageDialog.__init__(
448                         self,
449                         parent,
450                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
451                         gtk.MESSAGE_ERROR,
452                         gtk.BUTTONS_OK,
453                         message,
454                 )
455                 self.set_default_response(gtk.RESPONSE_OK)
456                 self.connect('response', self._handle_clicked)
457
458         def _handle_clicked(self, *args):
459                 self.destroy()
460
461
462 class MessageBox2(gtk.MessageDialog):
463
464         def __init__(self, message):
465                 parent = None
466                 gtk.MessageDialog.__init__(
467                         self,
468                         parent,
469                         gtk.DIALOG_DESTROY_WITH_PARENT,
470                         gtk.MESSAGE_ERROR,
471                         gtk.BUTTONS_OK,
472                         message,
473                 )
474                 self.set_default_response(gtk.RESPONSE_OK)
475                 self.connect('response', self._handle_clicked)
476
477         def _handle_clicked(self, *args):
478                 self.destroy()
479
480
481 class PopupCalendar(object):
482
483         def __init__(self, parent, displayDate, title = ""):
484                 self._displayDate = displayDate
485
486                 self._calendar = gtk.Calendar()
487                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
488                 self._calendar.select_day(self._displayDate.day)
489                 self._calendar.set_display_options(
490                         gtk.CALENDAR_SHOW_HEADING |
491                         gtk.CALENDAR_SHOW_DAY_NAMES |
492                         gtk.CALENDAR_NO_MONTH_CHANGE |
493                         0
494                 )
495                 self._calendar.connect("day-selected", self._on_day_selected)
496
497                 self._popupWindow = gtk.Window()
498                 self._popupWindow.set_title(title)
499                 self._popupWindow.add(self._calendar)
500                 self._popupWindow.set_transient_for(parent)
501                 self._popupWindow.set_modal(True)
502                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
503                 self._popupWindow.set_skip_pager_hint(True)
504                 self._popupWindow.set_skip_taskbar_hint(True)
505
506         def run(self):
507                 self._popupWindow.show_all()
508
509         def _on_day_selected(self, *args):
510                 try:
511                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
512                         self._calendar.select_day(self._displayDate.day)
513                 except StandardError, e:
514                         warnings.warn(e.message)
515
516
517 class QuickAddView(object):
518
519         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
520                 self._errorDisplay = errorDisplay
521                 self._manager = None
522                 self._signalSink = signalSink
523
524                 self._clipboard = gtk.clipboard_get()
525
526                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
527                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
528                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
529                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
530                 self._onAddId = None
531                 self._onAddClickedId = None
532                 self._onAddReleasedId = None
533                 self._addToEditTimerId = None
534                 self._onClearId = None
535                 self._onPasteId = None
536
537         def enable(self, manager):
538                 self._manager = manager
539
540                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
541                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
542                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
543                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
544                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
545
546         def disable(self):
547                 self._manager = None
548
549                 self._addTaskButton.disconnect(self._onAddId)
550                 self._addTaskButton.disconnect(self._onAddClickedId)
551                 self._addTaskButton.disconnect(self._onAddReleasedId)
552                 self._pasteTaskNameButton.disconnect(self._onPasteId)
553                 self._clearTaskNameButton.disconnect(self._onClearId)
554
555         def set_addability(self, addability):
556                 self._addTaskButton.set_sensitive(addability)
557
558         def _on_add(self, *args):
559                 try:
560                         name = self._taskNameEntry.get_text()
561                         self._taskNameEntry.set_text("")
562
563                         self._signalSink.stage.send(("add", name))
564                 except StandardError, e:
565                         self._errorDisplay.push_exception()
566
567         def _on_add_edit(self, *args):
568                 try:
569                         name = self._taskNameEntry.get_text()
570                         self._taskNameEntry.set_text("")
571
572                         self._signalSink.stage.send(("add-edit", name))
573                 except StandardError, e:
574                         self._errorDisplay.push_exception()
575
576         def _on_add_pressed(self, widget):
577                 try:
578                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
579                 except StandardError, e:
580                         self._errorDisplay.push_exception()
581
582         def _on_add_released(self, widget):
583                 try:
584                         if self._addToEditTimerId is not None:
585                                 gobject.source_remove(self._addToEditTimerId)
586                         self._addToEditTimerId = None
587                 except StandardError, e:
588                         self._errorDisplay.push_exception()
589
590         def _on_paste(self, *args):
591                 try:
592                         entry = self._taskNameEntry.get_text()
593                         addedText = self._clipboard.wait_for_text()
594                         if addedText:
595                                 entry += addedText
596                         self._taskNameEntry.set_text(entry)
597                 except StandardError, e:
598                         self._errorDisplay.push_exception()
599
600         def _on_clear(self, *args):
601                 try:
602                         self._taskNameEntry.set_text("")
603                 except StandardError, e:
604                         self._errorDisplay.push_exception()
605
606
607 class TapOrHold(object):
608
609         def __init__(self, widget):
610                 self._widget = widget
611                 self._isTap = True
612                 self._isPointerInside = True
613                 self._holdTimeoutId = None
614                 self._tapTimeoutId = None
615                 self._taps = 0
616
617                 self._bpeId = None
618                 self._breId = None
619                 self._eneId = None
620                 self._lneId = None
621
622         def enable(self):
623                 self._bpeId = self._widget.connect("button-press-event", self._on_button_press)
624                 self._breId = self._widget.connect("button-release-event", self._on_button_release)
625                 self._eneId = self._widget.connect("enter-notify-event", self._on_enter)
626                 self._lneId = self._widget.connect("leave-notify-event", self._on_leave)
627
628         def disable(self):
629                 self._widget.disconnect(self._bpeId)
630                 self._widget.disconnect(self._breId)
631                 self._widget.disconnect(self._eneId)
632                 self._widget.disconnect(self._lneId)
633
634         def on_tap(self, taps):
635                 print "TAP", taps
636
637         def on_hold(self, taps):
638                 print "HOLD", taps
639
640         def on_holding(self):
641                 print "HOLDING"
642
643         def on_cancel(self):
644                 print "CANCEL"
645
646         def _on_button_press(self, *args):
647                 # Hack to handle weird notebook behavior
648                 self._isPointerInside = True
649
650                 if self._tapTimeoutId is not None:
651                         gobject.source_remove(self._tapTimeoutId)
652                         self._tapTimeoutId = None
653
654                 # Handle double taps
655                 if self._holdTimeoutId is None:
656                         self._tapTimeoutId = None
657
658                         self._taps = 1
659                         self._holdTimeoutId = gobject.timeout_add(1000, self._on_hold_timeout)
660                 else:
661                         self._taps = 2
662
663         def _on_button_release(self, *args):
664                 assert self._tapTimeoutId is None
665                 # Handle release after timeout if user hasn't double-clicked
666                 self._tapTimeoutId = gobject.timeout_add(100, self._on_tap_timeout)
667
668         def _on_actual_press(self, *args):
669                 if self._holdTimeoutId is not None:
670                         gobject.source_remove(self._holdTimeoutId)
671                 self._holdTimeoutId = None
672
673                 if self._isPointerInside:
674                         if self._isTap:
675                                 self.on_tap(self._taps)
676                         else:
677                                 self.on_hold(self._taps)
678                 else:
679                         self.on_cancel()
680
681         def _on_tap_timeout(self, *args):
682                 self._tapTimeoutId = None
683                 self._on_actual_press()
684                 return False
685
686         def _on_hold_timeout(self, *args):
687                 self._holdTimeoutId = None
688                 self._isTap = False
689                 self.on_holding()
690                 return False
691
692         def _on_enter(self, *args):
693                 self._isPointerInside = True
694
695         def _on_leave(self, *args):
696                 self._isPointerInside = False
697
698
699 if __name__ == "__main__":
700         if False:
701                 import datetime
702                 cal = PopupCalendar(None, datetime.datetime.now())
703                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
704                 cal.run()
705
706         gtk.main()