#!/usr/bin/python
-import warnings
+"""
+@todo Implement a custom label that encodes random info (Not set in stone, just throwing these out there)
+ Background color based on Location
+ Text intensity based on time estimate
+ Short/long version with long including tags colored specially
+"""
+
+import sys
+import traceback
+import datetime
import contextlib
+import warnings
import gobject
import gtk
+import toolbox
+import coroutines
+
@contextlib.contextmanager
def gtk_lock():
gtk.gdk.threads_leave()
+class ContextHandler(object):
+
+ HOLD_TIMEOUT = 1000
+ MOVE_THRESHHOLD = 10
+
+ def __init__(self, actionWidget, eventTarget = coroutines.null_sink()):
+ self._actionWidget = actionWidget
+ self._eventTarget = eventTarget
+
+ self._actionPressId = None
+ self._actionReleaseId = None
+ self._motionNotifyId = None
+ self._popupMenuId = None
+ self._holdTimerId = None
+
+ self._respondOnRelease = False
+ self._startPosition = None
+
+ def enable(self):
+ self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press)
+ self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release)
+ self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion)
+ self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup)
+
+ def disable(self):
+ self._clear()
+ self._actionWidget.disconnect(self._actionPressId)
+ self._actionWidget.disconnect(self._actionReleaseId)
+ self._actionWidget.disconnect(self._motionNotifyId)
+ self._actionWidget.disconnect(self._popupMenuId)
+
+ def _respond(self, position):
+ widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position
+ responsePosition = (
+ widgetPosition[0] + position[0],
+ widgetPosition[1] + position[1],
+ )
+ self._eventTarget.send((self._actionWidget, responsePosition))
+
+ def _clear(self):
+ if self._holdTimerId is not None:
+ gobject.source_remove(self._holdTimerId)
+ self._respondOnRelease = False
+ self._startPosition = None
+
+ def _is_cleared(self):
+ return self._startPosition is None
+
+ def _on_press(self, widget, event):
+ if not self._is_cleared():
+ return
+
+ self._startPosition = event.get_coords()
+ if event.button == 1:
+ # left click
+ self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout)
+ else:
+ # any other click
+ self._respondOnRelease = True
+
+ def _on_release(self, widget, event):
+ if self._is_cleared():
+ return
+
+ if self._respondOnRelease:
+ position = self._startPosition
+ self._clear()
+ self._respond(position)
+ else:
+ self._clear()
+
+ def _on_hold_timeout(self):
+ assert not self._is_cleared()
+ gobject.source_remove(self._holdTimerId)
+ self._holdTimerId = None
+
+ position = self._startPosition
+ self._clear()
+ self._respond(position)
+
+ def _on_motion(self, widget, event):
+ if self._is_cleared():
+ return
+ curPosition = event.get_coords()
+ dx, dy = (
+ curPosition[1] - self._startPosition[1],
+ curPosition[1] - self._startPosition[1],
+ )
+ delta = (dx ** 2 + dy ** 2) ** (0.5)
+ if self.MOVE_THRESHHOLD <= delta:
+ self._clear()
+
+ def _on_popup(self, widget):
+ self._clear()
+ position = 0, 0
+ self._respond(position)
+
+
class LoginWindow(object):
def __init__(self, widgetTree):
else:
self.__show_message(message)
- def push_exception(self, exception):
- self.push_message(exception.message)
- warnings.warn(exception, stacklevel=3)
+ def push_exception(self, exception = None):
+ if exception is None:
+ userMessage = sys.exc_value.message
+ warningMessage = traceback.format_exc()
+ else:
+ userMessage = exception.message
+ warningMessage = exception
+ self.push_message(userMessage)
+ warnings.warn(warningMessage, stacklevel=3)
def pop_message(self):
if 0 < len(self.__messages):
class PopupCalendar(object):
- def __init__(self, parent):
- self.__calendar = gtk.Calendar()
- self.__calendar.connect("day-selected-double-click", self._on_date_select)
-
- self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
- self.__popupWindow.set_title("")
- self.__popupWindow.add(self.__calendar)
- self.__popupWindow.set_transient_for(parent)
- self.__popupWindow.set_modal(True)
+ def __init__(self, parent, displayDate):
+ self._displayDate = displayDate
+
+ self._calendar = gtk.Calendar()
+ self._calendar.select_month(self._displayDate.month, self._displayDate.year)
+ self._calendar.select_day(self._displayDate.day)
+ self._calendar.set_display_options(
+ gtk.CALENDAR_SHOW_HEADING |
+ gtk.CALENDAR_SHOW_DAY_NAMES |
+ gtk.CALENDAR_NO_MONTH_CHANGE |
+ 0
+ )
+ self._calendar.connect("day-selected", self._on_day_selected)
- def get_date(self):
- year, month, day = self.__calendar.get_date()
- month += 1 # Seems to be 0 indexed
- return year, month, day
+ self._popupWindow = gtk.Window()
+ self._popupWindow.set_title("")
+ self._popupWindow.add(self._calendar)
+ self._popupWindow.set_transient_for(parent)
+ self._popupWindow.set_modal(True)
def run(self):
- self.__popupWindow.show_all()
+ self._popupWindow.show_all()
+
+ def _on_day_selected(self, *args):
+ try:
+ self._calendar.select_month(self._displayDate.month, self._displayDate.year)
+ self._calendar.select_day(self._displayDate.day)
+ except StandardError, e:
+ warnings.warn(e.message)
+
+
+class NotesDialog(object):
+
+ def __init__(self, widgetTree):
+ self._dialog = widgetTree.get_widget("notesDialog")
+ self._notesBox = widgetTree.get_widget("notes-notesBox")
+ self._addButton = widgetTree.get_widget("notes-addButton")
+ self._saveButton = widgetTree.get_widget("notes-saveButton")
+ self._cancelButton = widgetTree.get_widget("notes-cancelButton")
+ self._onAddId = None
+ self._onSaveId = None
+ self._onCancelId = None
+
+ self._notes = []
+ self._notesToDelete = []
+
+ def enable(self):
+ self._dialog.set_default_size(800, 300)
+ self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
+ self._onSaveId = self._saveButton.connect("clicked", self._on_save_clicked)
+ self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
+
+ def disable(self):
+ self._addButton.disconnect(self._onAddId)
+ self._saveButton.disconnect(self._onSaveId)
+ self._cancelButton.disconnect(self._onAddId)
+
+ def run(self, todoManager, taskId, parentWindow = None):
+ if parentWindow is not None:
+ self._dialog.set_transient_for(parentWindow)
+
+ taskDetails = todoManager.get_task_details(taskId)
+
+ self._dialog.set_default_response(gtk.RESPONSE_OK)
+ for note in taskDetails["notes"]:
+ noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox(note)
+ noteDeleteButton.connect("clicked", self._on_delete_existing, note["id"], noteBox)
+
+ try:
+ response = self._dialog.run()
+ if response != gtk.RESPONSE_OK:
+ raise RuntimeError("Edit Cancelled")
+ finally:
+ self._dialog.hide()
- def _on_date_select(self, *args):
- self.__popupWindow.hide()
- self.callback()
+ for note in self._notes:
+ noteId = note[0]
+ noteTitle = note[2].get_text()
+ noteBody = note[4].get_buffer().get_text()
+ if noteId is None:
+ print "New note:", note
+ todoManager.add_note(taskId, noteTitle, noteBody)
+ else:
+ # @todo Provide way to only update on change
+ print "Updating note:", note
+ todoManager.update_note(noteId, noteTitle, noteBody)
+
+ for deletedNoteId in self._notesToDelete:
+ print "Deleted note:", deletedNoteId
+ todoManager.delete_note(noteId)
+
+ def _append_notebox(self, noteDetails = None):
+ if noteDetails is None:
+ noteDetails = {"id": None, "title": "", "body": ""}
+
+ noteBox = gtk.VBox()
+
+ titleBox = gtk.HBox()
+ titleEntry = gtk.Entry()
+ titleEntry.set_text(noteDetails["title"])
+ titleBox.pack_start(titleEntry, True, True)
+ noteDeleteButton = gtk.Button(stock=gtk.STOCK_DELETE)
+ titleBox.pack_end(noteDeleteButton, False, False)
+ noteBox.pack_start(titleBox, False, True)
+
+ noteEntryScroll = gtk.ScrolledWindow()
+ noteEntryScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ noteEntry = gtk.TextView()
+ noteEntry.set_editable(True)
+ noteEntry.set_wrap_mode(gtk.WRAP_WORD)
+ noteEntry.get_buffer().set_text(noteDetails["body"])
+ noteEntry.set_size_request(-1, 150)
+ noteEntryScroll.add(noteEntry)
+ noteBox.pack_start(noteEntryScroll, True, True)
+
+ self._notesBox.pack_start(noteBox, True, True)
+ noteBox.show_all()
+
+ note = noteDetails["id"], noteBox, titleEntry, noteDeleteButton, noteEntry
+ self._notes.append(note)
+ return note[1:]
- def callback(self):
- pass
+ def _on_add_clicked(self, *args):
+ noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox()
+ noteDeleteButton.connect("clicked", self._on_delete_new, noteBox)
+
+ def _on_save_clicked(self, *args):
+ self._dialog.response(gtk.RESPONSE_OK)
+
+ def _on_cancel_clicked(self, *args):
+ self._dialog.response(gtk.RESPONSE_CANCEL)
+
+ def _on_delete_new(self, widget, noteBox):
+ self._notesBox.remove(noteBox)
+ self._notes = [note for note in self._notes if note[1] is not noteBox]
+
+ def _on_delete_existing(self, widget, noteId, noteBox):
+ self._notesBox.remove(noteBox)
+ self._notes = [note for note in self._notes if note[1] is not noteBox]
+ self._notesToDelete.append(noteId)
class EditTaskDialog(object):
+ """
+ @bug The dialog doens't fit well on the maemo screen
+ """
def __init__(self, widgetTree):
self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
self._taskName = widgetTree.get_widget("edit-taskNameEntry")
self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
- self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
- self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
+ self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar")
self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
- self._popupCalendar = PopupCalendar(self._dialog)
+ self._onPasteTaskId = None
+ self._onClearDueDateId = None
+ self._onAddId = None
+ self._onCancelId = None
def enable(self, todoManager):
self._populate_projects(todoManager)
- self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
- self._dueDateProperties.connect("clicked", self._on_choose_duedate)
- self._clearDueDate.connect("clicked", self._on_clear_duedate)
- self._addButton.connect("clicked", self._on_add_clicked)
- self._cancelButton.connect("clicked", self._on_cancel_clicked)
-
- self._popupCalendar.callback = self._update_duedate
+ self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
+ self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate)
+ self._onAddId = self._addButton.connect("clicked", self._on_add_clicked)
+ self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
def disable(self):
- self._popupCalendar.callback = lambda: None
+ self._pasteTaskNameButton.disconnect(self._onPasteTaskId)
+ self._clearDueDate.disconnect(self._onClearDueDateId)
+ self._addButton.disconnect(self._onAddId)
+ self._cancelButton.disconnect(self._onAddId)
- self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
- self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
- self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
self._projectsList.clear()
self._projectCombo.set_model(None)
originalProjectId = taskDetails["projId"]
originalProjectName = todoManager.get_project(originalProjectId)["name"]
originalName = taskDetails["name"]
- try:
- originalPriority = int(taskDetails["priority"])
- except ValueError:
- originalPriority = 0
- originalDue = taskDetails["due"]
+ originalPriority = str(taskDetails["priority"].get_nothrow(0))
+ if taskDetails["dueDate"].is_good():
+ originalDue = taskDetails["dueDate"].get()
+ else:
+ originalDue = None
self._dialog.set_default_response(gtk.RESPONSE_OK)
self._taskName.set_text(originalName)
self._set_active_proj(originalProjectName)
- self._priorityChoiceCombo.set_active(originalPriority)
- self._dueDateDisplay.set_text(originalDue)
+ self._priorityChoiceCombo.set_active(int(originalPriority))
+ if originalDue is not None:
+ # Months are 0 indexed
+ self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year)
+ self._dueDateDisplay.select_day(originalDue.day)
+ else:
+ now = datetime.datetime.now()
+ self._dueDateDisplay.select_month(now.month, now.year)
+ self._dueDateDisplay.select_day(0)
try:
response = self._dialog.run()
newProjectName = self._get_project(todoManager)
newName = self._taskName.get_text()
newPriority = self._get_priority()
- newDueDate = self._dueDateDisplay.get_text()
+ year, month, day = self._dueDateDisplay.get_date()
+ if day != 0:
+ # Months are 0 indexed
+ date = datetime.date(year, month + 1, day)
+ time = datetime.time()
+ newDueDate = datetime.datetime.combine(date, time)
+ else:
+ newDueDate = None
isProjDifferent = newProjectName != originalProjectName
isNameDifferent = newName != originalName
todoManager.set_name(taskId, newName)
print "NAME CHANGE"
if isPriorityDifferent:
- todoManager.set_priority(taskId, newPriority)
+ try:
+ priority = toolbox.Optional(int(newPriority))
+ except ValueError:
+ priority = toolbox.Optional()
+ todoManager.set_priority(taskId, priority)
print "PRIO CHANGE"
if isDueDifferent:
- todoManager.set_duedate(taskId, newDueDate)
+ if newDueDate:
+ due = toolbox.Optional(newDueDate)
+ else:
+ due = toolbox.Optional()
+
+ todoManager.set_duedate(taskId, due)
print "DUE CHANGE"
return {
else:
return str(index)
- def _get_date(self):
- # @bug The date is not used in a consistent manner causing ... issues
- dateParts = self._popupCalendar.get_date()
- dateParts = (str(part) for part in dateParts)
- return "-".join(dateParts)
-
- def _update_duedate(self):
- self._dueDateDisplay.set_text(self._get_date())
-
def _on_name_paste(self, *args):
clipboard = gtk.clipboard_get()
contents = clipboard.wait_for_text()
if contents is not None:
self._taskName.set_text(contents)
- def _on_choose_duedate(self, *args):
- self._popupCalendar.run()
-
def _on_clear_duedate(self, *args):
- self._dueDateDisplay.set_text("")
+ self._dueDateDisplay.select_day(0)
def _on_add_clicked(self, *args):
self._dialog.response(gtk.RESPONSE_OK)
def _on_cancel_clicked(self, *args):
self._dialog.response(gtk.RESPONSE_CANCEL)
+
+if __name__ == "__main__":
+ if True:
+ win = gtk.Window()
+ win.set_title("Tap'N'Hold")
+ eventBox = gtk.EventBox()
+ win.add(eventBox)
+
+ context = ContextHandler(eventBox, coroutines.printer_sink())
+ context.enable()
+ win.connect("destroy", lambda w: gtk.main_quit())
+
+ win.show_all()
+
+ if False:
+ cal = PopupCalendar(None, datetime.datetime.now())
+ cal._popupWindow.connect("destroy", lambda w: gtk.main_quit())
+ cal.run()
+
+ gtk.main()