Reorging the code. Don't worry, no layoffs
[doneit] / src / common_view.py
index 3270106..2c6dd25 100644 (file)
@@ -15,8 +15,6 @@
 
 import webbrowser
 import datetime
-import urlparse
-import base64
 
 import gobject
 import gtk
@@ -24,59 +22,6 @@ import gtk
 import coroutines
 import toolbox
 import gtk_toolbox
-import rtm_backend
-import cache_backend
-import rtm_api
-
-
-def abbreviate(text, expectedLen):
-       singleLine = " ".join(text.split("\n"))
-       lineLen = len(singleLine)
-       if lineLen <= expectedLen:
-               return singleLine
-
-       abbrev = "..."
-
-       leftLen = expectedLen // 2 - 1
-       rightLen = max(expectedLen - leftLen - len(abbrev) + 1, 1)
-
-       abbrevText =  singleLine[0:leftLen] + abbrev + singleLine[-rightLen:-1]
-       assert len(abbrevText) <= expectedLen, "Too long: '%s'" % abbrevText
-       return abbrevText
-
-
-def abbreviate_url(url, domainLength, pathLength):
-       urlParts = urlparse.urlparse(url)
-
-       netloc = urlParts.netloc
-       path = urlParts.path
-
-       pathLength += max(domainLength - len(netloc), 0)
-       domainLength += max(pathLength - len(path), 0)
-
-       netloc = abbreviate(netloc, domainLength)
-       path = abbreviate(path, pathLength)
-       return netloc + path
-
-
-def get_token(username, apiKey, secret):
-       token = None
-       rtm = rtm_api.RTMapi(username, apiKey, secret, token)
-
-       authURL = rtm.getAuthURL()
-       webbrowser.open(authURL)
-       mb = gtk_toolbox.MessageBox2("You need to authorize DoneIt with\nRemember The Milk.\nClick OK after you authorize.")
-       mb.run()
-
-       token = rtm.getToken()
-       return token
-
-
-def get_credentials(credentialsDialog):
-       # @todo Pass in parent window
-       username, password = credentialsDialog.request_credentials()
-       token = get_token(username, rtm_backend.RtmBackend.API_KEY, rtm_backend.RtmBackend.SECRET)
-       return username, password, token
 
 
 def project_sort_by_type(projects):
@@ -163,8 +108,8 @@ class ItemListView(object):
                self._showCompleted = False
                self._showIncomplete = True
 
-               self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree)
-               self._notesDialog = gtk_toolbox.NotesDialog(widgetTree)
+               self._editDialog = EditTaskDialog(widgetTree)
+               self._notesDialog = NotesDialog(widgetTree)
 
                self._itemList = gtk.ListStore(
                        gobject.TYPE_STRING, # id
@@ -264,7 +209,7 @@ class ItemListView(object):
                for taskDetails in sortedTasks:
                        id = taskDetails["id"]
                        isCompleted = taskDetails["isCompleted"]
-                       name = abbreviate(taskDetails["name"], 100)
+                       name = toolbox.abbreviate(taskDetails["name"], 100)
                        priority = str(taskDetails["priority"].get_nothrow(""))
                        if taskDetails["dueDate"].is_good():
                                dueDate = taskDetails["dueDate"].get()
@@ -275,7 +220,7 @@ class ItemListView(object):
                                fuzzyDue = ""
 
                        linkDisplay = taskDetails["url"]
-                       linkDisplay = abbreviate_url(linkDisplay, 20, 10)
+                       linkDisplay = toolbox.abbreviate_url(linkDisplay, 20, 10)
 
                        notes = taskDetails["notes"]
                        notesDisplay = "%d Notes" % len(notes) if notes else ""
@@ -325,7 +270,369 @@ class ItemListView(object):
                        self._errorDisplay.push_exception()
 
 
-class GtkRtMilk(object):
+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_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):
+
+       def __init__(self, widgetTree):
+               self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
+
+               self._dialog = widgetTree.get_widget("editTaskDialog")
+               self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
+               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-dueDateCalendar")
+               self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
+
+               self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
+               self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
+
+               self._onPasteTaskId = None
+               self._onClearDueDateId = None
+               self._onAddId = None
+               self._onCancelId = None
+
+       def enable(self, todoManager):
+               self._populate_projects(todoManager)
+
+               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._pasteTaskNameButton.disconnect(self._onPasteTaskId)
+               self._clearDueDate.disconnect(self._onClearDueDateId)
+               self._addButton.disconnect(self._onAddId)
+               self._cancelButton.disconnect(self._onCancelId)
+
+               self._projectsList.clear()
+               self._projectCombo.set_model(None)
+
+       def request_task(self, todoManager, taskId, parentWindow = None):
+               if parentWindow is not None:
+                       self._dialog.set_transient_for(parentWindow)
+
+               taskDetails = todoManager.get_task_details(taskId)
+               originalProjectId = taskDetails["projId"]
+               originalProjectName = todoManager.get_project(originalProjectId)["name"]
+               originalName = taskDetails["name"]
+               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(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()
+                       if response != gtk.RESPONSE_OK:
+                               raise RuntimeError("Edit Cancelled")
+               finally:
+                       self._dialog.hide()
+
+               newProjectName = self._get_project(todoManager)
+               newName = self._taskName.get_text()
+               newPriority = self._get_priority()
+               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
+               isPriorityDifferent = newPriority != originalPriority
+               isDueDifferent = newDueDate != originalDue
+
+               if isProjDifferent:
+                       newProjectId = todoManager.lookup_project(newProjectName)
+                       todoManager.set_project(taskId, newProjectId)
+                       print "PROJ CHANGE"
+               if isNameDifferent:
+                       todoManager.set_name(taskId, newName)
+                       print "NAME CHANGE"
+               if isPriorityDifferent:
+                       try:
+                               priority = toolbox.Optional(int(newPriority))
+                       except ValueError:
+                               priority = toolbox.Optional()
+                       todoManager.set_priority(taskId, priority)
+                       print "PRIO CHANGE"
+               if isDueDifferent:
+                       if newDueDate:
+                               due = toolbox.Optional(newDueDate)
+                       else:
+                               due = toolbox.Optional()
+
+                       todoManager.set_duedate(taskId, due)
+                       print "DUE CHANGE"
+
+               return {
+                       "projId": isProjDifferent,
+                       "name": isNameDifferent,
+                       "priority": isPriorityDifferent,
+                       "due": isDueDifferent,
+               }
+
+       def _populate_projects(self, todoManager):
+               for projectName in todoManager.get_projects():
+                       row = (projectName["name"], )
+                       self._projectsList.append(row)
+               self._projectCombo.set_model(self._projectsList)
+               cell = gtk.CellRendererText()
+               self._projectCombo.pack_start(cell, True)
+               self._projectCombo.add_attribute(cell, 'text', 0)
+               self._projectCombo.set_active(0)
+
+       def _set_active_proj(self, projName):
+               for i, row in enumerate(self._projectsList):
+                       if row[0] == projName:
+                               self._projectCombo.set_active(i)
+                               break
+               else:
+                       raise ValueError("%s not in list" % projName)
+
+       def _get_project(self, todoManager):
+               name = self._projectCombo.get_active_text()
+               return name
+
+       def _get_priority(self):
+               index = self._priorityChoiceCombo.get_active()
+               assert index != -1
+               if index < 1:
+                       return ""
+               else:
+                       return str(index)
+
+       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_clear_duedate(self, *args):
+               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)
+
+
+class ProjectsDialog(object):
+
+       ID_IDX = 0
+       NAME_IDX = 1
+       VISIBILITY_IDX = 2
+
+       def __init__(self, widgetTree):
+               self._manager = None
+
+               self._dialog = widgetTree.get_widget("projectsDialog")
+               self._projView = widgetTree.get_widget("proj-projectView")
+
+               addSink = coroutines.CoSwitch(["add", "add-edit"])
+               addSink.register_sink("add", coroutines.func_sink(self._on_add))
+               addSink.register_sink("add-edit", coroutines.func_sink(self._on_add_edit))
+               self._addView = gtk_toolbox.QuickAddView(widgetTree, gtk_toolbox.DummyErrorDisplay(), addSink, "proj")
+
+               self._projList = gtk.ListStore(
+                       gobject.TYPE_STRING, # id
+                       gobject.TYPE_STRING, # name
+                       gobject.TYPE_BOOLEAN, # is visible
+               )
+               self._visibilityColumn = gtk.TreeViewColumn('') # Complete?
+               self._visibilityCell = gtk.CellRendererToggle()
+               self._visibilityCell.set_property("activatable", True)
+               self._visibilityCell.connect("toggled", self._on_toggle_visibility)
+               self._visibilityColumn.pack_start(self._visibilityCell, False)
+               self._visibilityColumn.set_attributes(self._visibilityCell, active=self.VISIBILITY_IDX)
+               self._nameColumn = gtk.TreeViewColumn('Name')
+               self._nameCell = gtk.CellRendererText()
+               self._nameColumn.pack_start(self._nameCell, True)
+               self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
+               self._nameColumn.set_expand(True)
+
+               self._projView.append_column(self._visibilityColumn)
+               self._projView.append_column(self._nameColumn)
+               self._projView.connect("row-activated", self._on_proj_select)
+
+       def enable(self, manager):
+               self._manager = manager
+
+               self._populate_projects()
+               self._dialog.show_all()
+
+       def disable(self):
+               self._dialog.hide_all()
+               self._manager = None
+
+               self._projList.clear()
+               self._projView.set_model(None)
+
+       def _populate_projects(self):
+               self._projList.clear()
+
+               projects = self._manager.get_projects()
+               for project in projects:
+                       projectId = project["id"]
+                       projectName = project["name"]
+                       isVisible = project["isVisible"]
+                       row = (projectId, projectName, isVisible)
+                       self._projList.append(row)
+               self._projView.set_model(self._projList)
+
+       def _on_add(self, eventData):
+               eventName, projectName, = eventData
+               self._manager.add_project(projectName)
+               self._populate_projects()
+
+       def _on_add_edit(self, eventData):
+               self._on_add(eventData)
+
+       def _on_toggle_visibility(self, cell, path):
+               listIndex = path[0]
+               row = self._projList[listIndex]
+               projId = row[self.ID_IDX]
+               oldValue = row[self.VISIBILITY_IDX]
+               newValue = not oldValue
+               print oldValue, newValue
+               self._manager.set_project_visibility(projId, newValue)
+               row[self.VISIBILITY_IDX] = newValue
+
+       def _on_proj_select(self, *args):
+               # @todo Implement project renaming
+               pass
+
+
+class CommonView(object):
 
        def __init__(self, widgetTree, errorDisplay):
                """
@@ -333,10 +640,9 @@ class GtkRtMilk(object):
                """
                self._errorDisplay = errorDisplay
                self._manager = None
-               self._credentials = "", "", ""
 
-               self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree)
-               self._projDialog = gtk_toolbox.ProjectsDialog(widgetTree)
+               self._editDialog = EditTaskDialog(widgetTree)
+               self._projDialog = ProjectsDialog(widgetTree)
                self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
                self._projectsCombo = widgetTree.get_widget("projectsCombo")
                self._projectCell = gtk.CellRendererText()
@@ -350,59 +656,34 @@ class GtkRtMilk(object):
                addSink.register_sink("add", coroutines.func_sink(self._on_add))
                addSink.register_sink("add-edit", coroutines.func_sink(self._on_add_edit))
                self._addView = gtk_toolbox.QuickAddView(widgetTree, self._errorDisplay, addSink, "add")
-               self._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree)
 
        @staticmethod
        def name():
-               return "Remember The Milk"
+               raise NotImplementedError
 
        def load_settings(self, config):
                """
                @note Thread Agnostic
                """
-               blobs = (
-                       config.get(self.name(), "bin_blob_%i" % i)
-                       for i in xrange(len(self._credentials))
-               )
-               creds = (
-                       base64.b64decode(blob)
-                       for blob in blobs
-               )
-               self._credentials = tuple(creds)
+               raise NotImplementedError
 
        def save_settings(self, config):
                """
                @note Thread Agnostic
                """
-               config.add_section(self.name())
-               for i, value in enumerate(self._credentials):
-                       blob = base64.b64encode(value)
-                       config.set(self.name(), "bin_blob_%i" % i, blob)
+               raise NotImplementedError
 
        def login(self):
                """
                @note UI Thread
                """
-               if self._manager is not None:
-                       return
-
-               credentials = self._credentials
-               while True:
-                       try:
-                               self._manager = rtm_backend.RtmBackend(*credentials)
-                               self._manager = cache_backend.LazyCacheBackend(self._manager)
-                               self._credentials = credentials
-                               return # Login succeeded
-                       except rtm_api.AuthStateMachine.NoData:
-                               # Login failed, grab new credentials
-                               credentials = get_credentials(self._credentialsDialog)
+               raise NotImplementedError
 
        def logout(self):
                """
                @note Thread Agnostic
                """
-               self._credentials = "", "", ""
-               self._manager = None
+               raise NotImplementedError
 
        def enable(self):
                """