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