Reorging the code. Don't worry, no layoffs
[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 datetime
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 class LoginWindow(object):
130
131         def __init__(self, widgetTree):
132                 """
133                 @note Thread agnostic
134                 """
135                 self._dialog = widgetTree.get_widget("loginDialog")
136                 self._parentWindow = widgetTree.get_widget("mainWindow")
137                 self._serviceCombo = widgetTree.get_widget("serviceCombo")
138                 self._usernameEntry = widgetTree.get_widget("usernameentry")
139                 self._passwordEntry = widgetTree.get_widget("passwordentry")
140
141                 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
142                 self._serviceCombo.set_model(self._serviceList)
143                 cell = gtk.CellRendererText()
144                 self._serviceCombo.pack_start(cell, True)
145                 self._serviceCombo.add_attribute(cell, 'text', 1)
146                 self._serviceCombo.set_active(0)
147
148                 callbackMapping = {
149                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
150                         "on_loginclose_clicked": self._on_loginclose_clicked,
151                 }
152                 widgetTree.signal_autoconnect(callbackMapping)
153
154         def request_credentials(self, parentWindow = None):
155                 """
156                 @note UI Thread
157                 """
158                 if parentWindow is None:
159                         parentWindow = self._parentWindow
160
161                 self._serviceCombo.hide()
162                 self._serviceList.clear()
163
164                 try:
165                         self._dialog.set_transient_for(parentWindow)
166                         self._dialog.set_default_response(gtk.RESPONSE_OK)
167                         response = self._dialog.run()
168                         if response != gtk.RESPONSE_OK:
169                                 raise RuntimeError("Login Cancelled")
170
171                         username = self._usernameEntry.get_text()
172                         password = self._passwordEntry.get_text()
173                         self._passwordEntry.set_text("")
174                 finally:
175                         self._dialog.hide()
176
177                 return username, password
178
179         def request_credentials_from(self, services, parentWindow = None):
180                 """
181                 @note UI Thread
182                 """
183                 if parentWindow is None:
184                         parentWindow = self._parentWindow
185
186                 self._serviceList.clear()
187                 for serviceIdserviceName in services.iteritems():
188                         self._serviceList.append(serviceIdserviceName)
189                 self._serviceCombo.set_active(0)
190                 self._serviceCombo.show()
191
192                 try:
193                         self._dialog.set_transient_for(parentWindow)
194                         self._dialog.set_default_response(gtk.RESPONSE_OK)
195                         response = self._dialog.run()
196                         if response != gtk.RESPONSE_OK:
197                                 raise RuntimeError("Login Cancelled")
198
199                         username = self._usernameEntry.get_text()
200                         password = self._passwordEntry.get_text()
201                         self._passwordEntry.set_text("")
202                 finally:
203                         self._dialog.hide()
204
205                 itr = self._serviceCombo.get_active_iter()
206                 serviceId = int(self._serviceList.get_value(itr, 0))
207                 self._serviceList.clear()
208                 return serviceId, username, password
209
210         def _on_loginbutton_clicked(self, *args):
211                 self._dialog.response(gtk.RESPONSE_OK)
212
213         def _on_loginclose_clicked(self, *args):
214                 self._dialog.response(gtk.RESPONSE_CANCEL)
215
216
217 class ErrorDisplay(object):
218
219         def __init__(self, widgetTree):
220                 super(ErrorDisplay, self).__init__()
221                 self.__errorBox = widgetTree.get_widget("errorEventBox")
222                 self.__errorDescription = widgetTree.get_widget("errorDescription")
223                 self.__errorClose = widgetTree.get_widget("errorClose")
224                 self.__parentBox = self.__errorBox.get_parent()
225
226                 self.__errorBox.connect("button_release_event", self._on_close)
227
228                 self.__messages = []
229                 self.__parentBox.remove(self.__errorBox)
230
231         def push_message_with_lock(self, message):
232                 gtk.gdk.threads_enter()
233                 try:
234                         self.push_message(message)
235                 finally:
236                         gtk.gdk.threads_leave()
237
238         def push_message(self, message):
239                 if 0 < len(self.__messages):
240                         self.__messages.append(message)
241                 else:
242                         self.__show_message(message)
243
244         def push_exception(self, exception = None):
245                 if exception is None:
246                         userMessage = sys.exc_value.message
247                         warningMessage = traceback.format_exc()
248                 else:
249                         userMessage = exception.message
250                         warningMessage = exception
251                 self.push_message(userMessage)
252                 warnings.warn(warningMessage, stacklevel=3)
253
254         def pop_message(self):
255                 if 0 < len(self.__messages):
256                         self.__show_message(self.__messages[0])
257                         del self.__messages[0]
258                 else:
259                         self.__hide_message()
260
261         def _on_close(self, *args):
262                 self.pop_message()
263
264         def __show_message(self, message):
265                 self.__errorDescription.set_text(message)
266                 self.__parentBox.pack_start(self.__errorBox, False, False)
267                 self.__parentBox.reorder_child(self.__errorBox, 1)
268
269         def __hide_message(self):
270                 self.__errorDescription.set_text("")
271                 self.__parentBox.remove(self.__errorBox)
272
273
274 class DummyErrorDisplay(object):
275
276         def __init__(self):
277                 super(DummyErrorDisplay, self).__init__()
278
279                 self.__messages = []
280
281         def push_message_with_lock(self, message):
282                 self.push_message(message)
283
284         def push_message(self, message):
285                 if 0 < len(self.__messages):
286                         self.__messages.append(message)
287                 else:
288                         self.__show_message(message)
289
290         def push_exception(self, exception = None):
291                 if exception is None:
292                         warningMessage = traceback.format_exc()
293                 else:
294                         warningMessage = exception
295                 warnings.warn(warningMessage, stacklevel=3)
296
297         def pop_message(self):
298                 if 0 < len(self.__messages):
299                         self.__show_message(self.__messages[0])
300                         del self.__messages[0]
301
302         def __show_message(self, message):
303                 warnings.warn(message, stacklevel=2)
304
305
306 class MessageBox(gtk.MessageDialog):
307
308         def __init__(self, message):
309                 parent = None
310                 gtk.MessageDialog.__init__(
311                         self,
312                         parent,
313                         gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
314                         gtk.MESSAGE_ERROR,
315                         gtk.BUTTONS_OK,
316                         message,
317                 )
318                 self.set_default_response(gtk.RESPONSE_OK)
319                 self.connect('response', self._handle_clicked)
320
321         def _handle_clicked(self, *args):
322                 self.destroy()
323
324
325 class MessageBox2(gtk.MessageDialog):
326
327         def __init__(self, message):
328                 parent = None
329                 gtk.MessageDialog.__init__(
330                         self,
331                         parent,
332                         gtk.DIALOG_DESTROY_WITH_PARENT,
333                         gtk.MESSAGE_ERROR,
334                         gtk.BUTTONS_OK,
335                         message,
336                 )
337                 self.set_default_response(gtk.RESPONSE_OK)
338                 self.connect('response', self._handle_clicked)
339
340         def _handle_clicked(self, *args):
341                 self.destroy()
342
343
344 class PopupCalendar(object):
345
346         def __init__(self, parent, displayDate, title = ""):
347                 self._displayDate = displayDate
348
349                 self._calendar = gtk.Calendar()
350                 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
351                 self._calendar.select_day(self._displayDate.day)
352                 self._calendar.set_display_options(
353                         gtk.CALENDAR_SHOW_HEADING |
354                         gtk.CALENDAR_SHOW_DAY_NAMES |
355                         gtk.CALENDAR_NO_MONTH_CHANGE |
356                         0
357                 )
358                 self._calendar.connect("day-selected", self._on_day_selected)
359
360                 self._popupWindow = gtk.Window()
361                 self._popupWindow.set_title(title)
362                 self._popupWindow.add(self._calendar)
363                 self._popupWindow.set_transient_for(parent)
364                 self._popupWindow.set_modal(True)
365                 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
366                 self._popupWindow.set_skip_pager_hint(True)
367                 self._popupWindow.set_skip_taskbar_hint(True)
368
369         def run(self):
370                 self._popupWindow.show_all()
371
372         def _on_day_selected(self, *args):
373                 try:
374                         self._calendar.select_month(self._displayDate.month, self._displayDate.year)
375                         self._calendar.select_day(self._displayDate.day)
376                 except StandardError, e:
377                         warnings.warn(e.message)
378
379
380 class QuickAddView(object):
381
382         def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
383                 self._errorDisplay = errorDisplay
384                 self._manager = None
385                 self._signalSink = signalSink
386
387                 self._clipboard = gtk.clipboard_get()
388
389                 self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry")
390                 self._addTaskButton = widgetTree.get_widget(prefix+"-addButton")
391                 self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton")
392                 self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton")
393                 self._onAddId = None
394                 self._onAddClickedId = None
395                 self._onAddReleasedId = None
396                 self._addToEditTimerId = None
397                 self._onClearId = None
398                 self._onPasteId = None
399
400         def enable(self, manager):
401                 self._manager = manager
402
403                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
404                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
405                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
406                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
407                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
408
409         def disable(self):
410                 self._manager = None
411
412                 self._addTaskButton.disconnect(self._onAddId)
413                 self._addTaskButton.disconnect(self._onAddClickedId)
414                 self._addTaskButton.disconnect(self._onAddReleasedId)
415                 self._pasteTaskNameButton.disconnect(self._onPasteId)
416                 self._clearTaskNameButton.disconnect(self._onClearId)
417
418         def set_addability(self, addability):
419                 self._addTaskButton.set_sensitive(addability)
420
421         def _on_add(self, *args):
422                 try:
423                         name = self._taskNameEntry.get_text()
424                         self._taskNameEntry.set_text("")
425
426                         self._signalSink.stage.send(("add", name))
427                 except StandardError, e:
428                         self._errorDisplay.push_exception()
429
430         def _on_add_edit(self, *args):
431                 try:
432                         name = self._taskNameEntry.get_text()
433                         self._taskNameEntry.set_text("")
434
435                         self._signalSink.stage.send(("add-edit", name))
436                 except StandardError, e:
437                         self._errorDisplay.push_exception()
438
439         def _on_add_pressed(self, widget):
440                 try:
441                         self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
442                 except StandardError, e:
443                         self._errorDisplay.push_exception()
444
445         def _on_add_released(self, widget):
446                 try:
447                         if self._addToEditTimerId is not None:
448                                 gobject.source_remove(self._addToEditTimerId)
449                         self._addToEditTimerId = None
450                 except StandardError, e:
451                         self._errorDisplay.push_exception()
452
453         def _on_paste(self, *args):
454                 try:
455                         entry = self._taskNameEntry.get_text()
456                         addedText = self._clipboard.wait_for_text()
457                         if addedText:
458                                 entry += addedText
459                         self._taskNameEntry.set_text(entry)
460                 except StandardError, e:
461                         self._errorDisplay.push_exception()
462
463         def _on_clear(self, *args):
464                 try:
465                         self._taskNameEntry.set_text("")
466                 except StandardError, e:
467                         self._errorDisplay.push_exception()
468
469
470 if __name__ == "__main__":
471         if True:
472                 win = gtk.Window()
473                 win.set_title("Tap'N'Hold")
474                 eventBox = gtk.EventBox()
475                 win.add(eventBox)
476
477                 context = ContextHandler(eventBox, coroutines.printer_sink())
478                 context.enable()
479                 win.connect("destroy", lambda w: gtk.main_quit())
480
481                 win.show_all()
482
483         if False:
484                 cal = PopupCalendar(None, datetime.datetime.now())
485                 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
486                 cal.run()
487
488         gtk.main()