Switching from a quick and dirty callback to using python signal stuff
[doneit] / src / gtk_toolbox.py
index 0780ad1..fada4ed 100644 (file)
@@ -1,11 +1,22 @@
 #!/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 warnings
 import contextlib
+import datetime
 
 import gobject
 import gtk
 
+import toolbox
+import coroutines
+
 
 @contextlib.contextmanager
 def gtk_lock():
@@ -16,6 +27,104 @@ 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):
@@ -195,7 +304,9 @@ class MessageBox2(gtk.MessageDialog):
 
 class PopupCalendar(object):
 
-       def __init__(self, parent):
+       def __init__(self, parent, eventTarget = coroutines.null_sink()):
+               self._eventTarget = eventTarget
+
                self.__calendar = gtk.Calendar()
                self.__calendar.connect("day-selected-double-click", self._on_date_select)
 
@@ -208,17 +319,14 @@ class PopupCalendar(object):
        def get_date(self):
                year, month, day = self.__calendar.get_date()
                month += 1 # Seems to be 0 indexed
-               return year, month, day
+               return datetime.date(year, month, day)
 
        def run(self):
                self.__popupWindow.show_all()
 
        def _on_date_select(self, *args):
                self.__popupWindow.hide()
-               self.callback()
-
-       def callback(self):
-               pass
+               self._eventTarget.send((self, self.get_date()))
 
 
 class EditTaskDialog(object):
@@ -238,7 +346,7 @@ class EditTaskDialog(object):
                self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
                self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
 
-               self._popupCalendar = PopupCalendar(self._dialog)
+               self._popupCalendar = PopupCalendar(self._dialog, coroutines.func_sink(self._update_duedate))
 
        def enable(self, todoManager):
                self._populate_projects(todoManager)
@@ -249,11 +357,7 @@ class EditTaskDialog(object):
                self._addButton.connect("clicked", self._on_add_clicked)
                self._cancelButton.connect("clicked", self._on_cancel_clicked)
 
-               self._popupCalendar.callback = self._update_duedate
-
        def disable(self):
-               self._popupCalendar.callback = lambda: None
-
                self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
                self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
                self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
@@ -268,16 +372,16 @@ 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().strftime("%Y-%m-%d %H:%M:%S")
+               else:
+                       originalDue = ""
 
                self._dialog.set_default_response(gtk.RESPONSE_OK)
                self._taskName.set_text(originalName)
                self._set_active_proj(originalProjectName)
-               self._priorityChoiceCombo.set_active(originalPriority)
+               self._priorityChoiceCombo.set_active(int(originalPriority))
                self._dueDateDisplay.set_text(originalDue)
 
                try:
@@ -305,10 +409,20 @@ 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 = datetime.datetime.strptime(newDueDate, "%Y-%m-%d %H:%M:%S")
+                               due = toolbox.Optional(due)
+                       else:
+                               due = toolbox.Optional()
+
+                       todoManager.set_duedate(taskId, due)
                        print "DUE CHANGE"
 
                return {
@@ -348,14 +462,13 @@ 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, eventData):
+               widget, date = eventData
+               time = datetime.time()
+               dueDate = datetime.datetime.combine(date, time)
 
-       def _update_duedate(self):
-               self._dueDateDisplay.set_text(self._get_date())
+               formttedDate = dueDate.strftime("%Y-%m-%d %H:%M:%S")
+               self._dueDateDisplay.set_text(formttedDate)
 
        def _on_name_paste(self, *args):
                clipboard = gtk.clipboard_get()
@@ -375,3 +488,17 @@ class EditTaskDialog(object):
        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()
+               gtk.main()