Start of preferences dialog
[doneit] / src / gtk_toolbox.py
index 52d29c9..e6f6fc0 100644 (file)
 #!/usr/bin/python
 
+"""
+@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_enter()
+       try:
+               yield
+       finally:
+               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):
 
@@ -121,9 +242,15 @@ class ErrorDisplay(object):
                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):
@@ -185,30 +312,149 @@ class MessageBox2(gtk.MessageDialog):
 
 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, title = ""):
+               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(title)
+               self._popupWindow.add(self._calendar)
+               self._popupWindow.set_transient_for(parent)
+               self._popupWindow.set_modal(True)
+               self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+               self._popupWindow.set_skip_pager_hint(True)
+               self._popupWindow.set_skip_taskbar_hint(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._onCancelId)
+
+       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"].itervalues():
+                       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()
+
+               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 _on_date_select(self, *args):
-               self.__popupWindow.hide()
-               self.callback()
+       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 callback(self):
-               pass
+       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):
@@ -221,32 +467,31 @@ class EditTaskDialog(object):
                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._onCancelId)
 
-               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)
 
@@ -258,17 +503,24 @@ class EditTaskDialog(object):
                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()
@@ -280,7 +532,14 @@ class EditTaskDialog(object):
                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
@@ -295,10 +554,19 @@ class EditTaskDialog(object):
                        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 {
@@ -338,26 +606,14 @@ class EditTaskDialog(object):
                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)
@@ -365,3 +621,83 @@ class EditTaskDialog(object):
        def _on_cancel_clicked(self, *args):
                self._dialog.response(gtk.RESPONSE_CANCEL)
 
+
+class PreferencesDialog(object):
+
+       def __init__(self, widgetTree):
+               self._backendList = gtk.ListStore(gobject.TYPE_STRING)
+               self._backendCell = gtk.CellRendererText()
+
+               self._dialog = widgetTree.get_widget("preferencesDialog")
+               self._backendSelector = widgetTree.get_widget("prefsBackendSelector")
+               self._applyButton = widgetTree.get_widget("applyPrefsButton")
+               self._cancelButton = widgetTree.get_widget("cancelPrefsButton")
+
+               self._onApplyId = None
+               self._onCancelId = None
+
+       def enable(self):
+               self._dialog.set_default_size(800, 300)
+               self._onApplyId = self._applyButton.connect("clicked", self._on_apply_clicked)
+               self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked)
+
+               cell = self._backendCell
+               self._backendSelector.pack_start(cell, True)
+               self._backendSelector.add_attribute(cell, 'text', 0)
+               self._backendSelector.set_model(self._backendList)
+
+       def disable(self):
+               self._applyButton.disconnect(self._onApplyId)
+               self._cancelButton.disconnect(self._onCancelId)
+
+               self._backendList.clear()
+               self._backendSelector.set_model(None)
+
+       def run(self, app, parentWindow = None):
+               if parentWindow is not None:
+                       self._dialog.set_transient_for(parentWindow)
+
+               self._backendList.clear()
+               activeIndex = 0
+               for i, (uiName, ui) in enumerate(app.get_uis()):
+                       self._backendList.append((uiName, ))
+                       if uiName == app.get_default_ui():
+                               activeIndex = i
+               self._backendSelector.set_active(activeIndex)
+
+               try:
+                       response = self._dialog.run()
+                       if response != gtk.RESPONSE_OK:
+                               raise RuntimeError("Edit Cancelled")
+               finally:
+                       self._dialog.hide()
+
+               backendName = self._backendSelector.get_active_text()
+               app.switch_ui(backendName)
+
+       def _on_apply_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()