X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Frtm_view.py;h=6a0ab368b0446c0ef08aa1ec147c9fdc30721869;hb=f06f9fe45f3e4c2c800cc0db1ec8873fa6fa4667;hp=987f34a9e663bb548c53f38dba6a0ff26407f9d8;hpb=c3e50fa6ff0456c4397a2248eac33bf9fe6dc8ba;p=doneit diff --git a/src/rtm_view.py b/src/rtm_view.py index 987f34a..6a0ab36 100644 --- a/src/rtm_view.py +++ b/src/rtm_view.py @@ -1,6 +1,7 @@ """ @todo Add an agenda view to the task list Tree of days, with each successive 7 days dropping the visibility of further lower priority items +@todo Remove blocking operations from UI thread """ import webbrowser @@ -11,6 +12,7 @@ import base64 import gobject import gtk +import coroutines import toolbox import gtk_toolbox import rtm_backend @@ -66,7 +68,57 @@ def get_credentials(credentialsDialog): return username, password, token -class GtkRtMilk(object): +def item_sort_by_priority_then_date(items): + sortedTasks = list(items) + sortedTasks.sort( + key = lambda taskDetails: ( + taskDetails["priority"].get_nothrow(4), + taskDetails["dueDate"].get_nothrow(datetime.datetime.max), + ), + ) + return sortedTasks + + +def item_sort_by_date_then_priority(items): + sortedTasks = list(items) + sortedTasks.sort( + key = lambda taskDetails: ( + taskDetails["dueDate"].get_nothrow(datetime.datetime.max), + taskDetails["priority"].get_nothrow(4), + ), + ) + return sortedTasks + + +def item_in_agenda(item): + taskDate = item["dueDate"].get_nothrow(datetime.datetime.max) + today = datetime.datetime.now() + delta = taskDate - today + dayDelta = abs(delta.days) + + priority = item["priority"].get_nothrow(4) + weeksVisible = 5 - priority + + isVisible = not bool(dayDelta / (weeksVisible * 7)) + return isVisible + + +def item_sort_by_fuzzydate_then_priority(items): + sortedTasks = list(items) + + def advanced_key(taskDetails): + dueDate = taskDetails["dueDate"].get_nothrow(datetime.datetime.max) + priority = taskDetails["priority"].get_nothrow(4) + isNotSameYear = not toolbox.is_same_year(dueDate) + isNotSameMonth = not toolbox.is_same_month(dueDate) + isNotSameDay = not toolbox.is_same_day(dueDate) + return isNotSameDay, isNotSameMonth, isNotSameYear, priority, dueDate + + sortedTasks.sort(key=advanced_key) + return sortedTasks + + +class ItemListView(object): ID_IDX = 0 COMPLETION_IDX = 1 @@ -77,21 +129,15 @@ class GtkRtMilk(object): LINK_IDX = 6 NOTES_IDX = 7 - def __init__(self, widgetTree): - """ - @note Thread agnostic - """ - self._clipboard = gtk.clipboard_get() - + def __init__(self, widgetTree, errorDisplay): + self._errorDisplay = errorDisplay + self._manager = None + self._projId = None self._showCompleted = False self._showIncomplete = True self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree) - self._projectsList = gtk.ListStore(gobject.TYPE_STRING) - self._projectsCombo = widgetTree.get_widget("projectsCombo") - self._onListActivateId = 0 - self._itemList = gtk.ListStore( gobject.TYPE_STRING, # id gobject.TYPE_BOOLEAN, # is complete @@ -136,6 +182,135 @@ class GtkRtMilk(object): self._todoItemScroll.add(self._todoItemTree) self._onItemSelectId = 0 + def enable(self, manager, projId): + self._manager = manager + self._projId = projId + + self._todoBox.pack_start(self._todoItemScroll) + self._todoItemScroll.show_all() + + self._itemList.clear() + self._todoItemTree.append_column(self._completionColumn) + self._todoItemTree.append_column(self._priorityColumn) + self._todoItemTree.append_column(self._nameColumn) + self._todoItemTree.append_column(self._dueColumn) + self._todoItemTree.append_column(self._linkColumn) + self._todoItemTree.append_column(self._notesColumn) + try: + self.reset_task_list(self._projId) + except StandardError, e: + self._errorDisplay.push_exception() + + self._todoItemTree.set_headers_visible(False) + self._nameColumn.set_expand(True) + + self._onItemSelectId = self._todoItemTree.connect("row-activated", self._on_item_select) + + def disable(self): + self._manager = None + self._projId = None + + self._todoBox.remove(self._todoItemScroll) + self._todoItemScroll.hide_all() + self._todoItemTree.disconnect(self._onItemSelectId) + + self._todoItemTree.remove_column(self._completionColumn) + self._todoItemTree.remove_column(self._priorityColumn) + self._todoItemTree.remove_column(self._nameColumn) + self._todoItemTree.remove_column(self._dueColumn) + self._todoItemTree.remove_column(self._linkColumn) + self._todoItemTree.remove_column(self._notesColumn) + self._itemList.clear() + self._itemList.set_model(None) + + def reset_task_list(self, projId): + self._projId = projId + self._itemList.clear() + self._populate_items() + + def _populate_items(self): + projId = self._projId + rawTasks = self._manager.get_tasks_with_details(projId) + filteredTasks = ( + taskDetails + for taskDetails in rawTasks + if self._showCompleted and taskDetails["isCompleted"] or self._showIncomplete and not taskDetails["isCompleted"] + ) + # filteredTasks = (taskDetails for taskDetails in filteredTasks if item_in_agenda(taskDetails)) + sortedTasks = item_sort_by_priority_then_date(filteredTasks) + # sortedTasks = item_sort_by_date_then_priority(filteredTasks) + # sortedTasks = item_sort_by_fuzzydate_then_priority(filteredTasks) + for taskDetails in sortedTasks: + id = taskDetails["id"] + isCompleted = taskDetails["isCompleted"] + name = abbreviate(taskDetails["name"], 100) + priority = str(taskDetails["priority"].get_nothrow("")) + if taskDetails["dueDate"].is_good(): + dueDate = taskDetails["dueDate"].get() + dueDescription = dueDate.strftime("%Y-%m-%d %H:%M:%S") + fuzzyDue = toolbox.to_fuzzy_date(dueDate) + else: + dueDescription = "" + fuzzyDue = "" + + linkDisplay = taskDetails["url"] + linkDisplay = abbreviate_url(linkDisplay, 20, 10) + + notes = taskDetails["notes"] + notesDisplay = "%d Notes" % len(notes) if notes else "" + + row = (id, isCompleted, name, priority, dueDescription, fuzzyDue, linkDisplay, notesDisplay) + self._itemList.append(row) + self._todoItemTree.set_model(self._itemList) + + def _on_item_select(self, treeView, path, viewColumn): + try: + # @todo See if there is a way to use the new gtk_toolbox.ContextHandler + taskId = self._itemList[path[0]][self.ID_IDX] + + if viewColumn is self._priorityColumn: + pass + elif viewColumn is self._nameColumn: + self._editDialog.enable(self._manager) + try: + self._editDialog.request_task(self._manager, taskId) + finally: + self._editDialog.disable() + self.reset_task_list(self._projId) + elif viewColumn is self._dueColumn: + self._editDialog.enable(self._manager) + try: + self._editDialog.request_task(self._manager, taskId) + finally: + self._editDialog.disable() + self.reset_task_list(self._projId) + elif viewColumn is self._linkColumn: + webbrowser.open(self._manager.get_task_details(taskId)["url"]) + elif viewColumn is self._notesColumn: + pass + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_completion_change(self, cell, path): + try: + taskId = self._itemList[path[0]][self.ID_IDX] + self._manager.complete_task(taskId) + self.reset_task_list(self._projId) + except StandardError, e: + self._errorDisplay.push_exception() + + +class QuickAddView(object): + + def __init__(self, widgetTree, errorDisplay, addSink): + self._errorDisplay = errorDisplay + self._manager = None + self._projId = None + self._addSink = addSink + + self._clipboard = gtk.clipboard_get() + self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree) + self._taskNameEntry = widgetTree.get_widget("add-taskNameEntry") self._addTaskButton = widgetTree.get_widget("add-addTaskButton") self._pasteTaskNameButton = widgetTree.get_widget("add-pasteTaskNameButton") @@ -147,9 +322,110 @@ class GtkRtMilk(object): self._onClearId = None self._onPasteId = None - self._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree) - self._credentials = "", "", "" + def enable(self, manager, projId): + self._manager = manager + self._projId = projId + + self._onAddId = self._addTaskButton.connect("clicked", self._on_add) + self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed) + self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released) + self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste) + self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear) + + def disable(self): self._manager = None + self._projId = None + + self._addTaskButton.disconnect(self._onAddId) + self._addTaskButton.disconnect(self._onAddClickedId) + self._addTaskButton.disconnect(self._onAddReleasedId) + self._pasteTaskNameButton.disconnect(self._onPasteId) + self._clearTaskNameButton.disconnect(self._onClearId) + + def reset_task_list(self, projId): + self._projId = projId + isMeta = self._manager.get_project(self._projId)["isMeta"] + # @todo RTM handles this by defaulting to a specific list + self._addTaskButton.set_sensitive(not isMeta) + + def _on_add(self, *args): + try: + name = self._taskNameEntry.get_text() + + projId = self._projId + taskId = self._manager.add_task(projId, name) + + self._taskNameEntry.set_text("") + self._addSink.send((projId, taskId)) + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_add_edit(self, *args): + try: + name = self._taskNameEntry.get_text() + + projId = self._projId + taskId = self._manager.add_task(projId, name) + + try: + self._editDialog.enable(self._manager) + try: + self._editDialog.request_task(self._manager, taskId) + finally: + self._editDialog.disable() + finally: + self._taskNameEntry.set_text("") + self._addSink.send((projId, taskId)) + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_add_pressed(self, widget): + try: + self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit) + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_add_released(self, widget): + try: + if self._addToEditTimerId is not None: + gobject.source_remove(self._addToEditTimerId) + self._addToEditTimerId = None + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_paste(self, *args): + try: + entry = self._taskNameEntry.get_text() + entry += self._clipboard.wait_for_text() + self._taskNameEntry.set_text(entry) + except StandardError, e: + self._errorDisplay.push_exception() + + def _on_clear(self, *args): + try: + self._taskNameEntry.set_text("") + except StandardError, e: + self._errorDisplay.push_exception() + + +class GtkRtMilk(object): + + def __init__(self, widgetTree, errorDisplay): + """ + @note Thread agnostic + """ + self._errorDisplay = errorDisplay + self._manager = None + self._credentials = "", "", "" + + self._projectsList = gtk.ListStore(gobject.TYPE_STRING) + self._projectsCombo = widgetTree.get_widget("projectsCombo") + self._onListActivateId = 0 + + self._itemView = ItemListView(widgetTree, self._errorDisplay) + addSink = coroutines.func_sink(lambda eventData: self._itemView.reset_task_list(eventData[0])) + self._addView = QuickAddView(widgetTree, self._errorDisplay, addSink) + self._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree) @staticmethod def name(): @@ -206,64 +482,29 @@ class GtkRtMilk(object): """ @note UI Thread """ - self._todoBox.pack_start(self._todoItemScroll) - self._todoItemScroll.show_all() - self._projectsList.clear() self._populate_projects() - self._itemList.clear() - self._todoItemTree.append_column(self._completionColumn) - self._todoItemTree.append_column(self._priorityColumn) - self._todoItemTree.append_column(self._nameColumn) - self._todoItemTree.append_column(self._dueColumn) - self._todoItemTree.append_column(self._linkColumn) - self._todoItemTree.append_column(self._notesColumn) - self._reset_task_list() - - self._todoItemTree.set_headers_visible(False) - self._nameColumn.set_expand(True) - - self._editDialog.enable(self._manager) + currentProject = self._get_project() + projId = self._manager.lookup_project(currentProject)["id"] + self._addView.enable(self._manager, projId) + self._itemView.enable(self._manager, projId) self._onListActivateId = self._projectsCombo.connect("changed", self._on_list_activate) - self._onItemSelectId = self._todoItemTree.connect("row-activated", self._on_item_select) - self._onAddId = self._addTaskButton.connect("clicked", self._on_add) - self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed) - self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released) - self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste) - self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear) def disable(self): """ @note UI Thread """ - self._todoBox.remove(self._todoItemScroll) - self._todoItemScroll.hide_all() - self._projectsCombo.disconnect(self._onListActivateId) - self._todoItemTree.disconnect(self._onItemSelectId) - self._addTaskButton.disconnect(self._onAddId) - self._addTaskButton.disconnect(self._onAddClickedId) - self._addTaskButton.disconnect(self._onAddReleasedId) - self._pasteTaskNameButton.disconnect(self._onPasteId) - self._clearTaskNameButton.disconnect(self._onClearId) - self._editDialog.disable() + self._addView.disable() + self._itemView.disable() self._projectsList.clear() self._projectsCombo.set_model(None) self._projectsCombo.disconnect("changed", self._on_list_activate) - self._todoItemTree.remove_column(self._completionColumn) - self._todoItemTree.remove_column(self._priorityColumn) - self._todoItemTree.remove_column(self._nameColumn) - self._todoItemTree.remove_column(self._dueColumn) - self._todoItemTree.remove_column(self._linkColumn) - self._todoItemTree.remove_column(self._notesColumn) - self._itemList.clear() - self._itemList.set_model(None) - self._manager = None def _populate_projects(self): @@ -280,111 +521,17 @@ class GtkRtMilk(object): self._projectsCombo.set_active(0) def _reset_task_list(self): - currentProject = self._get_project() - projId = self._manager.lookup_project(currentProject)["id"] - isMeta = self._manager.get_project(projId)["isMeta"] - # @todo RTM handles this by defaulting to a specific list - self._addTaskButton.set_sensitive(not isMeta) - - self._itemList.clear() - self._populate_items() + projectName = self._get_project() + projId = self._manager.lookup_project(projectName)["id"] + self._addView.reset_task_list(projId) + self._itemView.reset_task_list(projId) def _get_project(self): currentProjectName = self._projectsCombo.get_active_text() return currentProjectName - def _populate_items(self): - # @todo Look into using a button for notes and links, and labels for all else - currentProject = self._get_project() - projId = self._manager.lookup_project(currentProject)["id"] - sortedTasks = list(self._manager.get_tasks_with_details(projId)) - sortedTasks.sort(key = lambda taskDetails: (taskDetails["priority"].get_nothrow(1000), taskDetails["dueDate"].get_nothrow(datetime.datetime.max))) - for taskDetails in sortedTasks: - show = self._showCompleted if taskDetails["isCompleted"] else self._showIncomplete - if not show: - continue - id = taskDetails["id"] - isCompleted = taskDetails["isCompleted"] - name = abbreviate(taskDetails["name"], 100) - priority = str(taskDetails["priority"].get_nothrow("")) - if taskDetails["dueDate"].is_good(): - dueDate = taskDetails["dueDate"].get() - dueDescription = dueDate.strftime("%Y-%m-%d %H:%M:%S") - fuzzyDue = toolbox.to_fuzzy_date(dueDate) - else: - dueDescription = "" - fuzzyDue = "" - - linkDisplay = taskDetails["url"] - linkDisplay = abbreviate_url(linkDisplay, 20, 10) - - notes = taskDetails["notes"] - notesDisplay = "%d Notes" % len(notes) if notes else "" - - row = (id, isCompleted, name, priority, dueDescription, fuzzyDue, linkDisplay, notesDisplay) - self._itemList.append(row) - self._todoItemTree.set_model(self._itemList) - def _on_list_activate(self, *args): - self._reset_task_list() - - def _on_item_select(self, treeView, path, viewColumn): - # @todo See if there is a way to use the new gtk_toolbox.ContextHandler - taskId = self._itemList[path[0]][self.ID_IDX] - - if viewColumn is self._priorityColumn: - pass - elif viewColumn is self._nameColumn: - self._editDialog.request_task(self._manager, taskId) - self._reset_task_list() - elif viewColumn is self._dueColumn: - self._editDialog.request_task(self._manager, taskId) - self._reset_task_list() - elif viewColumn is self._linkColumn: - webbrowser.open(self._manager.get_task_details(taskId)["url"]) - elif viewColumn is self._notesColumn: - pass - - def _on_add(self, *args): - name = self._taskNameEntry.get_text() - - currentProject = self._get_project() - projId = self._manager.lookup_project(currentProject)["id"] - taskId = self._manager.add_task(projId, name) - - self._taskNameEntry.set_text("") - self._reset_task_list() - - def _on_add_edit(self, *args): - name = self._taskNameEntry.get_text() - - currentProject = self._get_project() - projId = self._manager.lookup_project(currentProject)["id"] - taskId = self._manager.add_task(projId, name) - try: - self._editDialog.request_task(self._manager, taskId) - finally: - self._taskNameEntry.set_text("") self._reset_task_list() - - def _on_add_pressed(self, widget): - self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit) - - def _on_add_released(self, widget): - if self._addToEditTimerId is not None: - gobject.source_remove(self._addToEditTimerId) - self._addToEditTimerId = None - - def _on_paste(self, *args): - entry = self._taskNameEntry.get_text() - entry += self._clipboard.wait_for_text() - self._taskNameEntry.set_text(entry) - - def _on_clear(self, *args): - self._taskNameEntry.set_text("") - - def _on_completion_change(self, cell, path): - taskId = self._itemList[path[0]][self.ID_IDX] - self._manager.complete_task(taskId) - self._reset_task_list() + except StandardError, e: + self._errorDisplay.push_exception()