Taking in changes from other projects
[doneit] / src / gtk_toolbox.py
1 #!/usr/bin/python
2
3 """
4 @todo Implement a custom label that encodes random info (Not set in stone, just throwing these out there)
5         Background color based on Location
6         Text intensity based on time estimate
7         Short/long version with long including tags colored specially
8 """
9
10 import sys
11 import traceback
12 import functools
13 import contextlib
14 import warnings
15
16 import gobject
17 import gtk
18
19 import coroutines
20
21
22 @contextlib.contextmanager
23 def gtk_lock():
24         gtk.gdk.threads_enter()
25         try:
26                 yield
27         finally:
28                 gtk.gdk.threads_leave()
29
30
31 class ContextHandler(object):
32
33         HOLD_TIMEOUT = 1000
34         MOVE_THRESHHOLD = 10
35
36         def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
37                 self._actionWidget = actionWidget
38                 self._eventTarget = eventTarget
39
40                 self._actionPressId = None
41                 self._actionReleaseId = None
42                 self._motionNotifyId = None
43                 self._popupMenuId = None
44                 self._holdTimerId = None
45
46                 self._respondOnRelease = False
47                 self._startPosition = None
48
49         def enable(self):
50                 self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
51                 self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
52                 self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
53                 self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
54
55         def disable(self):
56                 self._clear()
57                 self._actionWidget.disconnect(self._actionPressId)
58                 self._actionWidget.disconnect(self._actionReleaseId)
59                 self._actionWidget.disconnect(self._motionNotifyId)
60                 self._actionWidget.disconnect(self._popupMenuId)
61
62         def _respond(self, position):
63                 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
64                 responsePosition = (
65                         widgetPosition[0] + position[0],
66                         widgetPosition[1] + position[1],
67                 )
68                 self._eventTarget.send((self._actionWidget, responsePosition))
69
70         def _clear(self):
71                 if self._holdTimerId is not None:
72                         gobject.source_remove(self._holdTimerId)
73                 self._respondOnRelease = False
74                 self._startPosition = None
75
76         def _is_cleared(self):
77                 return self._startPosition is None
78
79         def _on_press(self, widget, event):
80                 if not self._is_cleared():
81                         return
82
83                 self._startPosition = event.get_coords()
84                 if event.button == 1:
85                         # left click
86                         self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
87                 else:
88                         # any other click
89                         self._respondOnRelease = True
90
91         def _on_release(self, widget, event):
92                 if self._is_cleared():
93                         return
94
95                 if self._respondOnRelease:
96                         position = self._startPosition
97                         self._clear()
98                         self._respond(position)
99                 else:
100                         self._clear()
101
102         def _on_hold_timeout(self):
103                 assert not self._is_cleared()
104                 gobject.source_remove(self._holdTimerId)
105                 self._holdTimerId = None
106
107                 position = self._startPosition
108                 self._clear()
109                 self._respond(position)
110
111         def _on_motion(self, widget, event):
112                 if self._is_cleared():
113                         return
114                 curPosition = event.get_coords()
115                 dx, dy = (
116                         curPosition[1] - self._startPosition[1],
117                         curPosition[1] - self._startPosition[1],
118                 )
119                 delta = (dx ** 2 + dy ** 2) ** (0.5)
120                 if self.MOVE_THRESHHOLD <= delta:
121                         self._clear()
122
123         def _on_popup(self, widget):
124                 self._clear()
125                 position = 0, 0
126                 self._respond(position)
127
128
129 def make_idler(func):
130         """
131         Decorator that makes a generator-function into a function that will continue execution on next call
132         """
133         a = []
134
135         @functools.wraps(func)
136         def decorated_func(*args, **kwds):
137                 if not a:
138                         a.append(func(*args, **kwds))
139                 try:
140                         a[0].next()
141                         return True
142                 except StopIteration:
143                         del a[:]
144                         return False
145
146         return decorated_func
147
148
149 def asynchronous_gtk_message(original_func):
150         """
151         @note Idea came from http://www.aclevername.com/articles/python-webgui/
152         """
153
154         def execute(allArgs):
155                 args, kwargs = allArgs
156                 original_func(*args, **kwargs)
157
158         @functools.wraps(original_func)
159         def delayed_func(*args, **kwargs):
160                 gobject.idle_add(execute, (args, kwargs))
161
162         return delayed_func
163
164
165 def synchronous_gtk_message(original_func):
166         """
167         @note Idea came from http://www.aclevername.com/articles/python-webgui/
168         """
169
170         @functools.wraps(original_func)
171         def immediate_func(*args, **kwargs):
172                 with gtk_lock():
173                         return original_func(*args, **kwargs)
174
175         return immediate_func
176
177
178 class LoginWindow(object):
179
180         def __init__(self, widgetTree):
181                 """
182                 @note Thread agnostic
183                 """
184                 self._dialog = widgetTree.get_widget("loginDialog")
185                 self._parentWindow = widgetTree.get_widget("mainWindow")
186                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
187                 self._usernameEntry = widgetTree.get_widget("usernameentry")
188                 self._passwordEntry = widgetTree.get_widget("passwordentry")
189
190                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
191                 self._serviceCombo.set_model(self._serviceList)
192                 cell = gtk.CellRendererText()
193                 self._serviceCombo.pack_start(cell, True)
194                 self._serviceCombo.add_attribute(cell, 'text', 1)
195                 self._serviceCombo.set_active(0)
196
197                 callbackMapping = {
198                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
199                         "on_loginclose_clicked": self._on_loginclose_clicked,
200                 }
201                 widgetTree.signal_autoconnect(callbackMapping)
202
203         def request_credentials(self, parentWindow = None):
204                 """
205                 @note UI Thread
206                 """
207                 if parentWindow is None:
208                         parentWindow = self._parentWindow
209
210                 self._serviceCombo.hide()
211                 self._serviceList.clear()
212
213                 try:
214                         self._dialog.set_transient_for(parentWindow)
215                         self._dialog.set_default_response(gtk.RESPONSE_OK)
216                         response = self._dialog.run()
217                         if response != gtk.RESPONSE_OK:
218                                 raise RuntimeError("Login Cancelled")
219
220                         username = self._usernameEntry.get_text()
221                         password = self._passwordEntry.get_text()
222                         self._passwordEntry.set_text("")
223                 finally:
224                         self._dialog.hide()
225
226                 return username, password
227
228         def request_credentials_from(self, services, parentWindow = None):
229                 """
230                 @note UI Thread
231                 """
232                 if parentWindow is None:
233                         parentWindow = self._parentWindow
234
235                 self._serviceList.clear()
236                 for serviceIdserviceName in services.iteritems():
237                         self._serviceList.append(serviceIdserviceName)
238                 self._serviceCombo.set_active(0)
239                 self._serviceCombo.show()
240
241                 try:
242                         self._dialog.set_transient_for(parentWindow)
243                         self._dialog.set_default_response(gtk.RESPONSE_OK)
244                         response = self._dialog.run()
245                         if response != gtk.RESPONSE_OK:
246                                 raise RuntimeError("Login Cancelled")
247
248                         username = self._usernameEntry.get_text()
249                         password = self._passwordEntry.get_text()
250                         self._passwordEntry.set_text("")
251                 finally:
252                         self._dialog.hide()
253
254                 itr = self._serviceCombo.get_active_iter()
255                 serviceId = int(self._serviceList.get_value(itr, 0))
256                 self._serviceList.clear()
257                 return serviceId, username, password
258
259         def _on_loginbutton_clicked(self, *args):
260                 self._dialog.response(gtk.RESPONSE_OK)
261
262         def _on_loginclose_clicked(self, *args):
263                 self._dialog.response(gtk.RESPONSE_CANCEL)
264
265
266 class ErrorDisplay(object):
267
268         def __init__(self, widgetTree):
269                 super(ErrorDisplay, self).__init__()
270                 self.__errorBox = widgetTree.get_widget("errorEventBox")
271                 self.__errorDescription = widgetTree.get_widget("errorDescription")
272                 self.__errorClose = widgetTree.get_widget("errorClose")
273                 self.__parentBox = self.__errorBox.get_parent()
274
275                 self.__errorBox.connect("button_release_event", self._on_close)
276
277                 self.__messages = []
278                 self.__parentBox.remove(self.__errorBox)
279
280         def push_message_with_lock(self, message):
281                 gtk.gdk.threads_enter()
282                 try:
283                         self.push_message(message)
284                 finally:
285                         gtk.gdk.threads_leave()
286
287         def push_message(self, message):
288                 if 0 < len(self.__messages):
289                         self.__messages.append(message)
290                 else:
291                         self.__show_message(message)
292
293         def push_exception(self, exception = None):
294                 if exception is None:
295                         userMessage = str(sys.exc_value)
296                         warningMessage = str(traceback.format_exc())
297                 else:
298                         userMessage = str(exception)
299                         warningMessage = str(exception)
300                 self.push_message(userMessage)
301                 warnings.warn(warningMessage, stacklevel=3)
302
303         def pop_message(self):
304                 if 0 < len(self.__messages):
305                         self.__show_message(self.__messages[0])
306                         del self.__messages[0]
307                 else:
308                         self.__hide_message()
309
310         def _on_close(self, *args):
311                 self.pop_message()
312
313         def __show_message(self, message):
314                 self.__errorDescription.set_text(message)
315                 self.__parentBox.pack_start(self.__errorBox, False, False)
316                 self.__parentBox.reorder_child(self.__errorBox, 1)
317
318         def __hide_message(self):
319                 self.__errorDescription.set_text("")
320                 self.__parentBox.remove(self.__errorBox)
321
322
323 class DummyErrorDisplay(object):
324
325         def __init__(self):
326                 super(DummyErrorDisplay, self).__init__()
327
328                 self.__messages = []
329
330         def push_message_with_lock(self, message):
331                 self.push_message(message)
332
333         def push_message(self, message):
334                 if 0 < len(self.__messages):
335                         self.__messages.append(message)
336                 else:
337                         self.__show_message(message)
338
339         def push_exception(self, exception = None):
340                 if exception is None:
341                         warningMessage = traceback.format_exc()
342                 else:
343                         warningMessage = exception
344                 warnings.warn(warningMessage, stacklevel=3)
345
346         def pop_message(self):
347                 if 0 < len(self.__messages):
348                         self.__show_message(self.__messages[0])
349                         del self.__messages[0]
350
351         def __show_message(self, message):
352                 warnings.warn(message, stacklevel=2)
353
354
355 class MessageBox(gtk.MessageDialog):
356
357         def __init__(self, message):
358                 parent = None
359                 gtk.MessageDialog.__init__(
360                         self,
361                         parent,
362                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
363                         gtk.MESSAGE_ERROR,
364                         gtk.BUTTONS_OK,
365                         message,
366                 )
367                 self.set_default_response(gtk.RESPONSE_OK)
368                 self.connect('response', self._handle_clicked)
369
370         def _handle_clicked(self, *args):
371                 self.destroy()
372
373
374 class MessageBox2(gtk.MessageDialog):
375
376         def __init__(self, message):
377                 parent = None
378                 gtk.MessageDialog.__init__(
379                         self,
380                         parent,
381                         gtk.DIALOG_DESTROY_WITH_PARENT,
382                         gtk.MESSAGE_ERROR,
383                         gtk.BUTTONS_OK,
384                         message,
385                 )
386                 self.set_default_response(gtk.RESPONSE_OK)
387                 self.connect('response', self._handle_clicked)
388
389         def _handle_clicked(self, *args):
390                 self.destroy()
391
392
393 class PopupCalendar(object):
394
395         def __init__(self, parent, displayDate, title = ""):
396                 self._displayDate = displayDate
397
398                 self._calendar = gtk.Calendar()
399                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
400                 self._calendar.select_day(self._displayDate.day)
401                 self._calendar.set_display_options(
402                         gtk.CALENDAR_SHOW_HEADING |
403                         gtk.CALENDAR_SHOW_DAY_NAMES |
404                         gtk.CALENDAR_NO_MONTH_CHANGE |
405                         0
406                 )
407                 self._calendar.connect("day-selected", self._on_day_selected)
408
409                 self._popupWindow = gtk.Window()
410                 self._popupWindow.set_title(title)
411                 self._popupWindow.add(self._calendar)
412                 self._popupWindow.set_transient_for(parent)
413                 self._popupWindow.set_modal(True)
414                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
415                 self._popupWindow.set_skip_pager_hint(True)
416                 self._popupWindow.set_skip_taskbar_hint(True)
417
418         def run(self):
419                 self._popupWindow.show_all()
420
421         def _on_day_selected(self, *args):
422                 try:
423                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
424                         self._calendar.select_day(self._displayDate.day)
425                 except StandardError, e:
426                         warnings.warn(e.message)
427
428
429 class QuickAddView(object):
430
431         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
432                 self._errorDisplay = errorDisplay
433                 self._manager = None
434                 self._signalSink = signalSink
435
436                 self._clipboard = gtk.clipboard_get()
437
438                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
439                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
440                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
441                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
442                 self._onAddId = None
443                 self._onAddClickedId = None
444                 self._onAddReleasedId = None
445                 self._addToEditTimerId = None
446                 self._onClearId = None
447                 self._onPasteId = None
448
449         def enable(self, manager):
450                 self._manager = manager
451
452                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
453                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
454                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
455                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
456                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
457
458         def disable(self):
459                 self._manager = None
460
461                 self._addTaskButton.disconnect(self._onAddId)
462                 self._addTaskButton.disconnect(self._onAddClickedId)
463                 self._addTaskButton.disconnect(self._onAddReleasedId)
464                 self._pasteTaskNameButton.disconnect(self._onPasteId)
465                 self._clearTaskNameButton.disconnect(self._onClearId)
466
467         def set_addability(self, addability):
468                 self._addTaskButton.set_sensitive(addability)
469
470         def _on_add(self, *args):
471                 try:
472                         name = self._taskNameEntry.get_text()
473                         self._taskNameEntry.set_text("")
474
475                         self._signalSink.stage.send(("add", name))
476                 except StandardError, e:
477                         self._errorDisplay.push_exception()
478
479         def _on_add_edit(self, *args):
480                 try:
481                         name = self._taskNameEntry.get_text()
482                         self._taskNameEntry.set_text("")
483
484                         self._signalSink.stage.send(("add-edit", name))
485                 except StandardError, e:
486                         self._errorDisplay.push_exception()
487
488         def _on_add_pressed(self, widget):
489                 try:
490                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
491                 except StandardError, e:
492                         self._errorDisplay.push_exception()
493
494         def _on_add_released(self, widget):
495                 try:
496                         if self._addToEditTimerId is not None:
497                                 gobject.source_remove(self._addToEditTimerId)
498                         self._addToEditTimerId = None
499                 except StandardError, e:
500                         self._errorDisplay.push_exception()
501
502         def _on_paste(self, *args):
503                 try:
504                         entry = self._taskNameEntry.get_text()
505                         addedText = self._clipboard.wait_for_text()
506                         if addedText:
507                                 entry += addedText
508                         self._taskNameEntry.set_text(entry)
509                 except StandardError, e:
510                         self._errorDisplay.push_exception()
511
512         def _on_clear(self, *args):
513                 try:
514                         self._taskNameEntry.set_text("")
515                 except StandardError, e:
516                         self._errorDisplay.push_exception()
517
518
519 if __name__ == "__main__":
520         if True:
521                 win = gtk.Window()
522                 win.set_title("Tap'N'Hold")
523                 eventBox = gtk.EventBox()
524                 win.add(eventBox)
525
526                 context = ContextHandler(eventBox, coroutines.printer_sink())
527                 context.enable()
528                 win.connect("destroy", lambda w: gtk.main_quit())
529
530                 win.show_all()
531
532         if False:
533                 import datetime
534                 cal = PopupCalendar(None, datetime.datetime.now())
535                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
536                 cal.run()
537
538         gtk.main()