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
23 @contextlib.contextmanager
25 gtk.gdk.threads_enter()
29 gtk.gdk.threads_leave()
32 class ContextHandler(object):
37 def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
38 self._actionWidget = actionWidget
39 self._eventTarget = eventTarget
41 self._actionPressId = None
42 self._actionReleaseId = None
43 self._motionNotifyId = None
44 self._popupMenuId = None
45 self._holdTimerId = None
47 self._respondOnRelease = False
48 self._startPosition = None
51 self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
52 self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
53 self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
54 self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
58 self._actionWidget.disconnect(self._actionPressId)
59 self._actionWidget.disconnect(self._actionReleaseId)
60 self._actionWidget.disconnect(self._motionNotifyId)
61 self._actionWidget.disconnect(self._popupMenuId)
63 def _respond(self, position):
64 widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
66 widgetPosition[0] + position[0],
67 widgetPosition[1] + position[1],
69 self._eventTarget.send((self._actionWidget, responsePosition))
72 if self._holdTimerId is not None:
73 gobject.source_remove(self._holdTimerId)
74 self._respondOnRelease = False
75 self._startPosition = None
77 def _is_cleared(self):
78 return self._startPosition is None
80 def _on_press(self, widget, event):
81 if not self._is_cleared():
84 self._startPosition = event.get_coords()
87 self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
90 self._respondOnRelease = True
92 def _on_release(self, widget, event):
93 if self._is_cleared():
96 if self._respondOnRelease:
97 position = self._startPosition
99 self._respond(position)
103 def _on_hold_timeout(self):
104 assert not self._is_cleared()
105 gobject.source_remove(self._holdTimerId)
106 self._holdTimerId = None
108 position = self._startPosition
110 self._respond(position)
112 def _on_motion(self, widget, event):
113 if self._is_cleared():
115 curPosition = event.get_coords()
117 curPosition[1] - self._startPosition[1],
118 curPosition[1] - self._startPosition[1],
120 delta = (dx ** 2 + dy ** 2) ** (0.5)
121 if self.MOVE_THRESHHOLD <= delta:
124 def _on_popup(self, widget):
127 self._respond(position)
130 class LoginWindow(object):
132 def __init__(self, widgetTree):
134 @note Thread agnostic
136 self._dialog = widgetTree.get_widget("loginDialog")
137 self._parentWindow = widgetTree.get_widget("mainWindow")
138 self._serviceCombo = widgetTree.get_widget("serviceCombo")
139 self._usernameEntry = widgetTree.get_widget("usernameentry")
140 self._passwordEntry = widgetTree.get_widget("passwordentry")
142 self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
143 self._serviceCombo.set_model(self._serviceList)
144 cell = gtk.CellRendererText()
145 self._serviceCombo.pack_start(cell, True)
146 self._serviceCombo.add_attribute(cell, 'text', 1)
147 self._serviceCombo.set_active(0)
150 "on_loginbutton_clicked": self._on_loginbutton_clicked,
151 "on_loginclose_clicked": self._on_loginclose_clicked,
153 widgetTree.signal_autoconnect(callbackMapping)
155 def request_credentials(self, parentWindow = None):
159 if parentWindow is None:
160 parentWindow = self._parentWindow
162 self._serviceCombo.hide()
163 self._serviceList.clear()
166 self._dialog.set_transient_for(parentWindow)
167 self._dialog.set_default_response(gtk.RESPONSE_OK)
168 response = self._dialog.run()
169 if response != gtk.RESPONSE_OK:
170 raise RuntimeError("Login Cancelled")
172 username = self._usernameEntry.get_text()
173 password = self._passwordEntry.get_text()
174 self._passwordEntry.set_text("")
178 return username, password
180 def request_credentials_from(self, services, parentWindow = None):
184 if parentWindow is None:
185 parentWindow = self._parentWindow
187 self._serviceList.clear()
188 for serviceIdserviceName in services.iteritems():
189 self._serviceList.append(serviceIdserviceName)
190 self._serviceCombo.set_active(0)
191 self._serviceCombo.show()
194 self._dialog.set_transient_for(parentWindow)
195 self._dialog.set_default_response(gtk.RESPONSE_OK)
196 response = self._dialog.run()
197 if response != gtk.RESPONSE_OK:
198 raise RuntimeError("Login Cancelled")
200 username = self._usernameEntry.get_text()
201 password = self._passwordEntry.get_text()
202 self._passwordEntry.set_text("")
206 itr = self._serviceCombo.get_active_iter()
207 serviceId = int(self._serviceList.get_value(itr, 0))
208 self._serviceList.clear()
209 return serviceId, username, password
211 def _on_loginbutton_clicked(self, *args):
212 self._dialog.response(gtk.RESPONSE_OK)
214 def _on_loginclose_clicked(self, *args):
215 self._dialog.response(gtk.RESPONSE_CANCEL)
218 class ErrorDisplay(object):
220 def __init__(self, widgetTree):
221 super(ErrorDisplay, self).__init__()
222 self.__errorBox = widgetTree.get_widget("errorEventBox")
223 self.__errorDescription = widgetTree.get_widget("errorDescription")
224 self.__errorClose = widgetTree.get_widget("errorClose")
225 self.__parentBox = self.__errorBox.get_parent()
227 self.__errorBox.connect("button_release_event", self._on_close)
230 self.__parentBox.remove(self.__errorBox)
232 def push_message_with_lock(self, message):
233 gtk.gdk.threads_enter()
235 self.push_message(message)
237 gtk.gdk.threads_leave()
239 def push_message(self, message):
240 if 0 < len(self.__messages):
241 self.__messages.append(message)
243 self.__show_message(message)
245 def push_exception(self, exception = None):
246 if exception is None:
247 userMessage = sys.exc_value.message
248 warningMessage = traceback.format_exc()
250 userMessage = exception.message
251 warningMessage = exception
252 self.push_message(userMessage)
253 warnings.warn(warningMessage, stacklevel=3)
255 def pop_message(self):
256 if 0 < len(self.__messages):
257 self.__show_message(self.__messages[0])
258 del self.__messages[0]
260 self.__hide_message()
262 def _on_close(self, *args):
265 def __show_message(self, message):
266 self.__errorDescription.set_text(message)
267 self.__parentBox.pack_start(self.__errorBox, False, False)
268 self.__parentBox.reorder_child(self.__errorBox, 1)
270 def __hide_message(self):
271 self.__errorDescription.set_text("")
272 self.__parentBox.remove(self.__errorBox)
275 class MessageBox(gtk.MessageDialog):
277 def __init__(self, message):
279 gtk.MessageDialog.__init__(
282 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
287 self.set_default_response(gtk.RESPONSE_OK)
288 self.connect('response', self._handle_clicked)
290 def _handle_clicked(self, *args):
294 class MessageBox2(gtk.MessageDialog):
296 def __init__(self, message):
298 gtk.MessageDialog.__init__(
301 gtk.DIALOG_DESTROY_WITH_PARENT,
306 self.set_default_response(gtk.RESPONSE_OK)
307 self.connect('response', self._handle_clicked)
309 def _handle_clicked(self, *args):
313 class PopupCalendar(object):
315 def __init__(self, parent, displayDate):
316 self._displayDate = displayDate
318 self._calendar = gtk.Calendar()
319 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
320 self._calendar.select_day(self._displayDate.day)
321 self._calendar.set_display_options(
322 gtk.CALENDAR_SHOW_HEADING |
323 gtk.CALENDAR_SHOW_DAY_NAMES |
324 gtk.CALENDAR_NO_MONTH_CHANGE |
327 self._calendar.connect("day-selected", self._on_day_selected)
329 self._popupWindow = gtk.Window()
330 self._popupWindow.set_title("")
331 self._popupWindow.add(self._calendar)
332 self._popupWindow.set_transient_for(parent)
333 self._popupWindow.set_modal(True)
334 self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
335 self._popupWindow.set_skip_pager_hint(True)
336 self._popupWindow.set_skip_taskbar_hint(True)
339 self._popupWindow.show_all()
341 def _on_day_selected(self, *args):
343 self._calendar.select_month(self._displayDate.month, self._displayDate.year)
344 self._calendar.select_day(self._displayDate.day)
345 except StandardError, e:
346 warnings.warn(e.message)
349 class NotesDialog(object):
351 def __init__(self, widgetTree):
352 self._dialog = widgetTree.get_widget("notesDialog")
353 self._notesBox = widgetTree.get_widget("notes-notesBox")
354 self._addButton = widgetTree.get_widget("notes-addButton")
355 self._saveButton = widgetTree.get_widget("notes-saveButton")
356 self._cancelButton = widgetTree.get_widget("notes-cancelButton")
358 self._onSaveId = None
359 self._onCancelId = None
362 self._notesToDelete = []
365 self._dialog.set_default_size(800, 300)
366 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
367 self._onSaveId = self._saveButton.connect("clicked", self._on_save_clicked)
368 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
371 self._addButton.disconnect(self._onAddId)
372 self._saveButton.disconnect(self._onSaveId)
373 self._cancelButton.disconnect(self._onAddId)
375 def run(self, todoManager, taskId, parentWindow = None):
376 if parentWindow is not None:
377 self._dialog.set_transient_for(parentWindow)
379 taskDetails = todoManager.get_task_details(taskId)
381 self._dialog.set_default_response(gtk.RESPONSE_OK)
382 for note in taskDetails["notes"]:
383 noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox(note)
384 noteDeleteButton.connect("clicked", self._on_delete_existing, note["id"], noteBox)
387 response = self._dialog.run()
388 if response != gtk.RESPONSE_OK:
389 raise RuntimeError("Edit Cancelled")
393 for note in self._notes:
395 noteTitle = note[2].get_text()
396 noteBody = note[4].get_buffer().get_text()
398 print "New note:", note
399 todoManager.add_note(taskId, noteTitle, noteBody)
401 # @todo Provide way to only update on change
402 print "Updating note:", note
403 todoManager.update_note(noteId, noteTitle, noteBody)
405 for deletedNoteId in self._notesToDelete:
406 print "Deleted note:", deletedNoteId
407 todoManager.delete_note(noteId)
409 def _append_notebox(self, noteDetails = None):
410 if noteDetails is None:
411 noteDetails = {"id": None, "title": "", "body": ""}
415 titleBox = gtk.HBox()
416 titleEntry = gtk.Entry()
417 titleEntry.set_text(noteDetails["title"])
418 titleBox.pack_start(titleEntry, True, True)
419 noteDeleteButton = gtk.Button(stock=gtk.STOCK_DELETE)
420 titleBox.pack_end(noteDeleteButton, False, False)
421 noteBox.pack_start(titleBox, False, True)
423 noteEntryScroll = gtk.ScrolledWindow()
424 noteEntryScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
425 noteEntry = gtk.TextView()
426 noteEntry.set_editable(True)
427 noteEntry.set_wrap_mode(gtk.WRAP_WORD)
428 noteEntry.get_buffer().set_text(noteDetails["body"])
429 noteEntry.set_size_request(-1, 150)
430 noteEntryScroll.add(noteEntry)
431 noteBox.pack_start(noteEntryScroll, True, True)
433 self._notesBox.pack_start(noteBox, True, True)
436 note = noteDetails["id"], noteBox, titleEntry, noteDeleteButton, noteEntry
437 self._notes.append(note)
440 def _on_add_clicked(self, *args):
441 noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox()
442 noteDeleteButton.connect("clicked", self._on_delete_new, noteBox)
444 def _on_save_clicked(self, *args):
445 self._dialog.response(gtk.RESPONSE_OK)
447 def _on_cancel_clicked(self, *args):
448 self._dialog.response(gtk.RESPONSE_CANCEL)
450 def _on_delete_new(self, widget, noteBox):
451 self._notesBox.remove(noteBox)
452 self._notes = [note for note in self._notes if note[1] is not noteBox]
454 def _on_delete_existing(self, widget, noteId, noteBox):
455 self._notesBox.remove(noteBox)
456 self._notes = [note for note in self._notes if note[1] is not noteBox]
457 self._notesToDelete.append(noteId)
460 class EditTaskDialog(object):
462 @bug The dialog doens't fit well on the maemo screen
465 def __init__(self, widgetTree):
466 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
468 self._dialog = widgetTree.get_widget("editTaskDialog")
469 self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
470 self._taskName = widgetTree.get_widget("edit-taskNameEntry")
471 self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
472 self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
473 self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar")
474 self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
476 self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
477 self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
479 self._onPasteTaskId = None
480 self._onClearDueDateId = None
482 self._onCancelId = None
484 def enable(self, todoManager):
485 self._populate_projects(todoManager)
487 self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
488 self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate)
489 self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
490 self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
493 self._pasteTaskNameButton.disconnect(self._onPasteTaskId)
494 self._clearDueDate.disconnect(self._onClearDueDateId)
495 self._addButton.disconnect(self._onAddId)
496 self._cancelButton.disconnect(self._onAddId)
498 self._projectsList.clear()
499 self._projectCombo.set_model(None)
501 def request_task(self, todoManager, taskId, parentWindow = None):
502 if parentWindow is not None:
503 self._dialog.set_transient_for(parentWindow)
505 taskDetails = todoManager.get_task_details(taskId)
506 originalProjectId = taskDetails["projId"]
507 originalProjectName = todoManager.get_project(originalProjectId)["name"]
508 originalName = taskDetails["name"]
509 originalPriority = str(taskDetails["priority"].get_nothrow(0))
510 if taskDetails["dueDate"].is_good():
511 originalDue = taskDetails["dueDate"].get()
515 self._dialog.set_default_response(gtk.RESPONSE_OK)
516 self._taskName.set_text(originalName)
517 self._set_active_proj(originalProjectName)
518 self._priorityChoiceCombo.set_active(int(originalPriority))
519 if originalDue is not None:
520 # Months are 0 indexed
521 self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year)
522 self._dueDateDisplay.select_day(originalDue.day)
524 now = datetime.datetime.now()
525 self._dueDateDisplay.select_month(now.month, now.year)
526 self._dueDateDisplay.select_day(0)
529 response = self._dialog.run()
530 if response != gtk.RESPONSE_OK:
531 raise RuntimeError("Edit Cancelled")
535 newProjectName = self._get_project(todoManager)
536 newName = self._taskName.get_text()
537 newPriority = self._get_priority()
538 year, month, day = self._dueDateDisplay.get_date()
540 # Months are 0 indexed
541 date = datetime.date(year, month + 1, day)
542 time = datetime.time()
543 newDueDate = datetime.datetime.combine(date, time)
547 isProjDifferent = newProjectName != originalProjectName
548 isNameDifferent = newName != originalName
549 isPriorityDifferent = newPriority != originalPriority
550 isDueDifferent = newDueDate != originalDue
553 newProjectId = todoManager.lookup_project(newProjectName)
554 todoManager.set_project(taskId, newProjectId)
557 todoManager.set_name(taskId, newName)
559 if isPriorityDifferent:
561 priority = toolbox.Optional(int(newPriority))
563 priority = toolbox.Optional()
564 todoManager.set_priority(taskId, priority)
568 due = toolbox.Optional(newDueDate)
570 due = toolbox.Optional()
572 todoManager.set_duedate(taskId, due)
576 "projId": isProjDifferent,
577 "name": isNameDifferent,
578 "priority": isPriorityDifferent,
579 "due": isDueDifferent,
582 def _populate_projects(self, todoManager):
583 for projectName in todoManager.get_projects():
584 row = (projectName["name"], )
585 self._projectsList.append(row)
586 self._projectCombo.set_model(self._projectsList)
587 cell = gtk.CellRendererText()
588 self._projectCombo.pack_start(cell, True)
589 self._projectCombo.add_attribute(cell, 'text', 0)
590 self._projectCombo.set_active(0)
592 def _set_active_proj(self, projName):
593 for i, row in enumerate(self._projectsList):
594 if row[0] == projName:
595 self._projectCombo.set_active(i)
598 raise ValueError("%s not in list" % projName)
600 def _get_project(self, todoManager):
601 name = self._projectCombo.get_active_text()
604 def _get_priority(self):
605 index = self._priorityChoiceCombo.get_active()
612 def _on_name_paste(self, *args):
613 clipboard = gtk.clipboard_get()
614 contents = clipboard.wait_for_text()
615 if contents is not None:
616 self._taskName.set_text(contents)
618 def _on_clear_duedate(self, *args):
619 self._dueDateDisplay.select_day(0)
621 def _on_add_clicked(self, *args):
622 self._dialog.response(gtk.RESPONSE_OK)
624 def _on_cancel_clicked(self, *args):
625 self._dialog.response(gtk.RESPONSE_CANCEL)
628 if __name__ == "__main__":
631 win.set_title("Tap'N'Hold")
632 eventBox = gtk.EventBox()
635 context = ContextHandler(eventBox, coroutines.printer_sink())
637 win.connect("destroy", lambda w: gtk.main_quit())
642 cal = PopupCalendar(None, datetime.datetime.now())
643 cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())