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