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
22 @contextlib.contextmanager
24 gtk.gdk.threads_enter()
28 gtk.gdk.threads_leave()
31 class ContextHandler(object):
36 def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
37 self._actionWidget = actionWidget
38 self._eventTarget = eventTarget
40 self._actionPressId = None
41 self._actionReleaseId = None
42 self._motionNotifyId = None
43 self._popupMenuId = None
44 self._holdTimerId = None
46 self._respondOnRelease = False
47 self._startPosition = None
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)
57 self._actionWidget.disconnect(self._actionPressId)
58 self._actionWidget.disconnect(self._actionReleaseId)
59 self._actionWidget.disconnect(self._motionNotifyId)
60 self._actionWidget.disconnect(self._popupMenuId)
62 def _respond(self, position):
63 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
65 widgetPosition[0] + position[0],
66 widgetPosition[1] + position[1],
68 self._eventTarget.send((self._actionWidget, responsePosition))
71 if self._holdTimerId is not None:
72 gobject.source_remove(self._holdTimerId)
73 self._respondOnRelease = False
74 self._startPosition = None
76 def _is_cleared(self):
77 return self._startPosition is None
79 def _on_press(self, widget, event):
80 if not self._is_cleared():
83 self._startPosition = event.get_coords()
86 self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
89 self._respondOnRelease = True
91 def _on_release(self, widget, event):
92 if self._is_cleared():
95 if self._respondOnRelease:
96 position = self._startPosition
98 self._respond(position)
102 def _on_hold_timeout(self):
103 assert not self._is_cleared()
104 gobject.source_remove(self._holdTimerId)
105 self._holdTimerId = None
107 position = self._startPosition
109 self._respond(position)
111 def _on_motion(self, widget, event):
112 if self._is_cleared():
114 curPosition = event.get_coords()
116 curPosition[1] - self._startPosition[1],
117 curPosition[1] - self._startPosition[1],
119 delta = (dx ** 2 + dy ** 2) ** (0.5)
120 if self.MOVE_THRESHHOLD <= delta:
123 def _on_popup(self, widget):
126 self._respond(position)
129 def make_idler(func):
131 Decorator that makes a generator-function into a function that will continue execution on next call
135 @functools.wraps(func)
136 def decorated_func(*args, **kwds):
138 a.append(func(*args, **kwds))
142 except StopIteration:
146 return decorated_func
149 def asynchronous_gtk_message(original_func):
151 @note Idea came from http://www.aclevername.com/articles/python-webgui/
154 def execute(allArgs):
155 args, kwargs = allArgs
156 original_func(*args, **kwargs)
158 @functools.wraps(original_func)
159 def delayed_func(*args, **kwargs):
160 gobject.idle_add(execute, (args, kwargs))
165 def synchronous_gtk_message(original_func):
167 @note Idea came from http://www.aclevername.com/articles/python-webgui/
170 @functools.wraps(original_func)
171 def immediate_func(*args, **kwargs):
173 return original_func(*args, **kwargs)
175 return immediate_func
178 class LoginWindow(object):
180 def __init__(self, widgetTree):
182 @note Thread agnostic
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")
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)
198 "on_loginbutton_clicked": self._on_loginbutton_clicked,
199 "on_loginclose_clicked": self._on_loginclose_clicked,
201 widgetTree.signal_autoconnect(callbackMapping)
203 def request_credentials(self, parentWindow = None):
207 if parentWindow is None:
208 parentWindow = self._parentWindow
210 self._serviceCombo.hide()
211 self._serviceList.clear()
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")
220 username = self._usernameEntry.get_text()
221 password = self._passwordEntry.get_text()
222 self._passwordEntry.set_text("")
226 return username, password
228 def request_credentials_from(self, services, parentWindow = None):
232 if parentWindow is None:
233 parentWindow = self._parentWindow
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()
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")
248 username = self._usernameEntry.get_text()
249 password = self._passwordEntry.get_text()
250 self._passwordEntry.set_text("")
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
259 def _on_loginbutton_clicked(self, *args):
260 self._dialog.response(gtk.RESPONSE_OK)
262 def _on_loginclose_clicked(self, *args):
263 self._dialog.response(gtk.RESPONSE_CANCEL)
266 class ErrorDisplay(object):
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()
275 self.__errorBox.connect("button_release_event", self._on_close)
278 self.__parentBox.remove(self.__errorBox)
280 def push_message_with_lock(self, message):
281 gtk.gdk.threads_enter()
283 self.push_message(message)
285 gtk.gdk.threads_leave()
287 def push_message(self, message):
288 if 0 < len(self.__messages):
289 self.__messages.append(message)
291 self.__show_message(message)
293 def push_exception(self, exception = None):
294 if exception is None:
295 userMessage = str(sys.exc_value)
296 warningMessage = str(traceback.format_exc())
298 userMessage = str(exception)
299 warningMessage = str(exception)
300 self.push_message(userMessage)
301 warnings.warn(warningMessage, stacklevel=3)
303 def pop_message(self):
304 if 0 < len(self.__messages):
305 self.__show_message(self.__messages[0])
306 del self.__messages[0]
308 self.__hide_message()
310 def _on_close(self, *args):
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)
318 def __hide_message(self):
319 self.__errorDescription.set_text("")
320 self.__parentBox.remove(self.__errorBox)
323 class DummyErrorDisplay(object):
326 super(DummyErrorDisplay, self).__init__()
330 def push_message_with_lock(self, message):
331 self.push_message(message)
333 def push_message(self, message):
334 if 0 < len(self.__messages):
335 self.__messages.append(message)
337 self.__show_message(message)
339 def push_exception(self, exception = None):
340 if exception is None:
341 warningMessage = traceback.format_exc()
343 warningMessage = exception
344 warnings.warn(warningMessage, stacklevel=3)
346 def pop_message(self):
347 if 0 < len(self.__messages):
348 self.__show_message(self.__messages[0])
349 del self.__messages[0]
351 def __show_message(self, message):
352 warnings.warn(message, stacklevel=2)
355 class MessageBox(gtk.MessageDialog):
357 def __init__(self, message):
359 gtk.MessageDialog.__init__(
362 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
367 self.set_default_response(gtk.RESPONSE_OK)
368 self.connect('response', self._handle_clicked)
370 def _handle_clicked(self, *args):
374 class MessageBox2(gtk.MessageDialog):
376 def __init__(self, message):
378 gtk.MessageDialog.__init__(
381 gtk.DIALOG_DESTROY_WITH_PARENT,
386 self.set_default_response(gtk.RESPONSE_OK)
387 self.connect('response', self._handle_clicked)
389 def _handle_clicked(self, *args):
393 class PopupCalendar(object):
395 def __init__(self, parent, displayDate, title = ""):
396 self._displayDate = displayDate
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 |
407 self._calendar.connect("day-selected", self._on_day_selected)
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)
419 self._popupWindow.show_all()
421 def _on_day_selected(self, *args):
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)
429 class QuickAddView(object):
431 def __init__(self, widgetTree, errorDisplay, signalSink, prefix):
432 self._errorDisplay = errorDisplay
434 self._signalSink = signalSink
436 self._clipboard = gtk.clipboard_get()
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")
443 self._onAddClickedId = None
444 self._onAddReleasedId = None
445 self._addToEditTimerId = None
446 self._onClearId = None
447 self._onPasteId = None
449 def enable(self, manager):
450 self._manager = manager
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)
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)
467 def set_addability(self, addability):
468 self._addTaskButton.set_sensitive(addability)
470 def _on_add(self, *args):
472 name = self._taskNameEntry.get_text()
473 self._taskNameEntry.set_text("")
475 self._signalSink.stage.send(("add", name))
476 except StandardError, e:
477 self._errorDisplay.push_exception()
479 def _on_add_edit(self, *args):
481 name = self._taskNameEntry.get_text()
482 self._taskNameEntry.set_text("")
484 self._signalSink.stage.send(("add-edit", name))
485 except StandardError, e:
486 self._errorDisplay.push_exception()
488 def _on_add_pressed(self, widget):
490 self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
491 except StandardError, e:
492 self._errorDisplay.push_exception()
494 def _on_add_released(self, widget):
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()
502 def _on_paste(self, *args):
504 entry = self._taskNameEntry.get_text()
505 addedText = self._clipboard.wait_for_text()
508 self._taskNameEntry.set_text(entry)
509 except StandardError, e:
510 self._errorDisplay.push_exception()
512 def _on_clear(self, *args):
514 self._taskNameEntry.set_text("")
515 except StandardError, e:
516 self._errorDisplay.push_exception()
519 if __name__ == "__main__":
522 win.set_title("Tap'N'Hold")
523 eventBox = gtk.EventBox()
526 context = ContextHandler(eventBox, coroutines.printer_sink())
528 win.connect("destroy", lambda w: gtk.main_quit())
534 cal = PopupCalendar(None, datetime.datetime.now())
535 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())