From 19f6af9b50b1e76ef81628c77b9fab9248cc8b52 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 25 Apr 2009 19:16:44 -0500 Subject: [PATCH] Reorging the code. Don't worry, no layoffs --- src/common_view.py | 467 +++++++++++++++++++++++++++++++++++++++++---------- src/doneit_glade.py | 65 ++++++- src/gtk_toolbox.py | 421 ---------------------------------------------- src/rtm_view.py | 409 +------------------------------------------- src/toolbox.py | 31 ++++ 5 files changed, 471 insertions(+), 922 deletions(-) diff --git a/src/common_view.py b/src/common_view.py index 3270106..2c6dd25 100644 --- a/src/common_view.py +++ b/src/common_view.py @@ -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): """ diff --git a/src/doneit_glade.py b/src/doneit_glade.py index 2ec55cf..a699ae5 100755 --- a/src/doneit_glade.py +++ b/src/doneit_glade.py @@ -15,6 +15,7 @@ import warnings import ConfigParser import socket +import gobject import gtk import gtk.glade @@ -29,6 +30,66 @@ import gtk_toolbox socket.setdefaulttimeout(10) +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) + + class DoneIt(object): __pretty_app_name__ = "DoneIt" @@ -71,7 +132,7 @@ class DoneIt(object): self._clipboard = gtk.clipboard_get() self.__window = self._widgetTree.get_widget("mainWindow") self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree) - self._prefsDialog = gtk_toolbox.PreferencesDialog(self._widgetTree) + self._prefsDialog = PreferencesDialog(self._widgetTree) self._app = None self._isFullScreen = False @@ -149,7 +210,7 @@ class DoneIt(object): # Setup costly backends import rtm_view with gtk_toolbox.gtk_lock(): - rtmView = rtm_view.GtkRtMilk(self._widgetTree, self.__errorDisplay) + rtmView = rtm_view.RtmView(self._widgetTree, self.__errorDisplay) self._todoUIs[rtmView.name()] = rtmView self._defaultUIName = rtmView.name() diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py index 4077473..c7738a3 100644 --- a/src/gtk_toolbox.py +++ b/src/gtk_toolbox.py @@ -16,7 +16,6 @@ import warnings import gobject import gtk -import toolbox import coroutines @@ -468,426 +467,6 @@ class QuickAddView(object): self._errorDisplay.push_exception() -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 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) - - -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 = QuickAddView(widgetTree, 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] - self._manager.set_project_visibility(projId, not oldValue) - row[self.VISIBILITY_IDX] = not oldValue - - def _on_proj_select(self, *args): - # @todo Implement project renaming - pass - - if __name__ == "__main__": if True: win = gtk.Window() diff --git a/src/rtm_view.py b/src/rtm_view.py index 3270106..706fdf7 100644 --- a/src/rtm_view.py +++ b/src/rtm_view.py @@ -1,64 +1,18 @@ """ -@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 Add a map view - Using new api widgets people are developing) - Integrate GPS w/ fallback to default location - Use locations for mapping -@todo Add a quick search (OR within a property type, and between property types) view - Drop down for multi selecting priority - Drop down for multi selecting tags - Drop down for multi selecting locations - Calendar selector for choosing due date range @todo Remove blocking operations from UI thread """ import webbrowser -import datetime -import urlparse import base64 -import gobject -import gtk +import common_view -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) @@ -79,277 +33,11 @@ def get_credentials(credentialsDialog): return username, password, token -def project_sort_by_type(projects): - sortedProjects = list(projects) - def advanced_key(proj): - if proj["name"] == "Inbox": - type = 0 - elif proj["name"] == "Sent": - type = 1 - elif not proj["isMeta"]: - type = 2 - else: - type = 3 - return type, proj["name"] - sortedProjects.sort(key=advanced_key) - return sortedProjects - - -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 - NAME_IDX = 2 - PRIORITY_IDX = 3 - DUE_IDX = 4 - FUZZY_IDX = 5 - LINK_IDX = 6 - NOTES_IDX = 7 - - 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._notesDialog = gtk_toolbox.NotesDialog(widgetTree) - - self._itemList = gtk.ListStore( - gobject.TYPE_STRING, # id - gobject.TYPE_BOOLEAN, # is complete - gobject.TYPE_STRING, # name - gobject.TYPE_STRING, # priority - gobject.TYPE_STRING, # due - gobject.TYPE_STRING, # fuzzy due - gobject.TYPE_STRING, # Link - gobject.TYPE_STRING, # Notes - ) - self._completionColumn = gtk.TreeViewColumn('') # Complete? - self._completionCell = gtk.CellRendererToggle() - self._completionCell.set_property("activatable", True) - self._completionCell.connect("toggled", self._on_completion_change) - self._completionColumn.pack_start(self._completionCell, False) - self._completionColumn.set_attributes(self._completionCell, active=self.COMPLETION_IDX) - self._priorityColumn = gtk.TreeViewColumn('') # Priority - self._priorityCell = gtk.CellRendererText() - self._priorityColumn.pack_start(self._priorityCell, False) - self._priorityColumn.set_attributes(self._priorityCell, text=self.PRIORITY_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._dueColumn = gtk.TreeViewColumn('Due') - self._dueCell = gtk.CellRendererText() - self._dueColumn.pack_start(self._nameCell, False) - self._dueColumn.set_attributes(self._nameCell, text=self.FUZZY_IDX) - self._linkColumn = gtk.TreeViewColumn('') # Link - self._linkCell = gtk.CellRendererText() - self._linkColumn.pack_start(self._nameCell, False) - self._linkColumn.set_attributes(self._nameCell, text=self.LINK_IDX) - self._notesColumn = gtk.TreeViewColumn('Notes') # Notes - self._notesCell = gtk.CellRendererText() - self._notesColumn.pack_start(self._nameCell, False) - self._notesColumn.set_attributes(self._nameCell, text=self.NOTES_IDX) - - self._todoBox = widgetTree.get_widget("todoBox") - self._todoItemScroll = gtk.ScrolledWindow() - self._todoItemScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - self._todoItemTree = gtk.TreeView() - self._todoItemTree.set_headers_visible(True) - self._todoItemTree.set_rules_hint(True) - self._todoItemTree.set_search_column(self.NAME_IDX) - self._todoItemTree.set_enable_search(True) - 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._todoItemTree.connect("row-activated", self._on_item_select) - self._todoItemScroll.add(self._todoItemTree) - - def enable(self, manager, projId): - self._manager = manager - self._projId = projId - - self._todoBox.pack_start(self._todoItemScroll) - self._todoItemScroll.show_all() - - self._itemList.clear() - try: - self.reset_task_list(self._projId) - except StandardError, e: - self._errorDisplay.push_exception() - - def disable(self): - self._manager = None - self._projId = None - - self._todoBox.remove(self._todoItemScroll) - self._todoItemScroll.hide_all() - - self._itemList.clear() - self._todoItemTree.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: - due = self._manager.get_task_details(taskId)["dueDate"] - if due.is_good(): - # @todo Pass to calendar the parent widget - calendar = gtk_toolbox.PopupCalendar(None, due.get(), "Due Date") - calendar.run() - elif viewColumn is self._linkColumn: - webbrowser.open(self._manager.get_task_details(taskId)["url"]) - elif viewColumn is self._notesColumn: - self._notesDialog.enable() - try: - # @todo Need to pass in parent window - self._notesDialog.run(self._manager, taskId) - finally: - self._notesDialog.disable() - 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 GtkRtMilk(object): +class RtmView(common_view.CommonView): def __init__(self, widgetTree, errorDisplay): - """ - @note Thread agnostic - """ - self._errorDisplay = errorDisplay - self._manager = None + super(RtmView, self).__init__(widgetTree, errorDisplay) self._credentials = "", "", "" - - self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree) - self._projDialog = gtk_toolbox.ProjectsDialog(widgetTree) - self._projectsList = gtk.ListStore(gobject.TYPE_STRING) - self._projectsCombo = widgetTree.get_widget("projectsCombo") - self._projectCell = gtk.CellRendererText() - self._onListActivateId = 0 - - self._projectMenuItem = widgetTree.get_widget("projectMenuItem") - self._onProjectMenuItemActivated = 0 - - self._itemView = ItemListView(widgetTree, self._errorDisplay) - 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, self._errorDisplay, addSink, "add") self._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree) @staticmethod @@ -403,94 +91,3 @@ class GtkRtMilk(object): """ self._credentials = "", "", "" self._manager = None - - def enable(self): - """ - @note UI Thread - """ - self._projectsList.clear() - self._populate_projects() - cell = self._projectCell - self._projectsCombo.pack_start(cell, True) - self._projectsCombo.add_attribute(cell, 'text', 0) - - currentProject = self._get_project() - projId = self._manager.lookup_project(currentProject)["id"] - self._addView.enable(self._manager) - self._itemView.enable(self._manager, projId) - - self._onListActivateId = self._projectsCombo.connect("changed", self._on_list_activate) - self._onProjectMenuItemActivated = self._projectMenuItem.connect("activate", self._on_proj_activate) - - def disable(self): - """ - @note UI Thread - """ - self._projectsCombo.disconnect(self._onListActivateId) - self._projectMenuItem.disconnect(self._onProjectMenuItemActivated) - self._onListActivateId = 0 - - self._addView.disable() - self._itemView.disable() - - self._projectsList.clear() - self._projectsCombo.set_model(None) - - def _populate_projects(self): - projects = self._manager.get_projects() - sortedProjects = project_sort_by_type(projects) - for project in sortedProjects: - projectName = project["name"] - isVisible = project["isVisible"] - row = (projectName, ) - if isVisible: - self._projectsList.append(row) - self._projectsCombo.set_model(self._projectsList) - self._projectsCombo.set_active(0) - - def _reset_task_list(self): - projectName = self._get_project() - projId = self._manager.lookup_project(projectName)["id"] - self._itemView.reset_task_list(projId) - - isMeta = self._manager.get_project(projId)["isMeta"] - # @todo RTM handles this by defaulting to a specific list - self._addView.set_addability(not isMeta) - - def _get_project(self): - currentProjectName = self._projectsCombo.get_active_text() - return currentProjectName - - def _on_list_activate(self, *args): - try: - self._reset_task_list() - except StandardError, e: - self._errorDisplay.push_exception() - - def _on_add(self, eventData): - eventName, taskName, = eventData - projectName = self._get_project() - projId = self._manager.lookup_project(projectName)["id"] - - taskId = self._manager.add_task(projId, taskName) - - self._itemView.reset_task_list(projId) - - def _on_add_edit(self, eventData): - eventName, taskName, = eventData - projectName = self._get_project() - projId = self._manager.lookup_project(projectName)["id"] - - taskId = self._manager.add_task(projId, taskName) - - self._editDialog.enable(self._manager) - try: - # @todo Need to pass in parent - self._editDialog.request_task(self._manager, taskId) - finally: - self._editDialog.disable() - self._itemView.reset_task_list(projId) - - def _on_proj_activate(self, *args): - # @todo Need to pass in parent - self._projDialog.enable(self._manager) diff --git a/src/toolbox.py b/src/toolbox.py index fe20e00..e21ca82 100644 --- a/src/toolbox.py +++ b/src/toolbox.py @@ -1,6 +1,7 @@ import sys import StringIO import urllib +import urlparse from xml.dom import minidom import datetime @@ -155,6 +156,36 @@ def load_xml(source, alternative=None): return xmldoc +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 is_same_year(targetDate, todaysDate = datetime.datetime.today()): return targetDate.year == todaysDate.year -- 1.7.9.5