X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgtk_toolbox.py;h=e6f6fc09c0ed7b0874b9e0be044e7d2244a3ea75;hb=e3f888001c80dabac477ece919e52185affbf404;hp=52d29c9d03fdbd41cd05deddddb6f88da061a961;hpb=32166ec5b7ba3f120cb2a4d6239caaf6545da9b5;p=doneit diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py index 52d29c9..e6f6fc0 100644 --- a/src/gtk_toolbox.py +++ b/src/gtk_toolbox.py @@ -1,10 +1,131 @@ #!/usr/bin/python +""" +@todo Implement a custom label that encodes random info (Not set in stone, just throwing these out there) + Background color based on Location + Text intensity based on time estimate + Short/long version with long including tags colored specially +""" + +import sys +import traceback +import datetime +import contextlib import warnings import gobject import gtk +import toolbox +import coroutines + + +@contextlib.contextmanager +def gtk_lock(): + gtk.gdk.threads_enter() + try: + yield + finally: + gtk.gdk.threads_leave() + + +class ContextHandler(object): + + HOLD_TIMEOUT = 1000 + MOVE_THRESHHOLD = 10 + + def __init__(self, actionWidget, eventTarget = coroutines.null_sink()): + self._actionWidget = actionWidget + self._eventTarget = eventTarget + + self._actionPressId = None + self._actionReleaseId = None + self._motionNotifyId = None + self._popupMenuId = None + self._holdTimerId = None + + self._respondOnRelease = False + self._startPosition = None + + def enable(self): + self._actionPressId = self._actionWidget.connect("button-press-event", self._on_press) + self._actionReleaseId = self._actionWidget.connect("button-release-event", self._on_release) + self._motionNotifyId = self._actionWidget.connect("motion-notify-event", self._on_motion) + self._popupMenuId = self._actionWidget.connect("popup-menu", self._on_popup) + + def disable(self): + self._clear() + self._actionWidget.disconnect(self._actionPressId) + self._actionWidget.disconnect(self._actionReleaseId) + self._actionWidget.disconnect(self._motionNotifyId) + self._actionWidget.disconnect(self._popupMenuId) + + def _respond(self, position): + widgetPosition = 0, 0 # @todo Figure out how to get a widgets root position + responsePosition = ( + widgetPosition[0] + position[0], + widgetPosition[1] + position[1], + ) + self._eventTarget.send((self._actionWidget, responsePosition)) + + def _clear(self): + if self._holdTimerId is not None: + gobject.source_remove(self._holdTimerId) + self._respondOnRelease = False + self._startPosition = None + + def _is_cleared(self): + return self._startPosition is None + + def _on_press(self, widget, event): + if not self._is_cleared(): + return + + self._startPosition = event.get_coords() + if event.button == 1: + # left click + self._holdTimerId = gobject.timeout_add(self.HOLD_TIMEOUT, self._on_hold_timeout) + else: + # any other click + self._respondOnRelease = True + + def _on_release(self, widget, event): + if self._is_cleared(): + return + + if self._respondOnRelease: + position = self._startPosition + self._clear() + self._respond(position) + else: + self._clear() + + def _on_hold_timeout(self): + assert not self._is_cleared() + gobject.source_remove(self._holdTimerId) + self._holdTimerId = None + + position = self._startPosition + self._clear() + self._respond(position) + + def _on_motion(self, widget, event): + if self._is_cleared(): + return + curPosition = event.get_coords() + dx, dy = ( + curPosition[1] - self._startPosition[1], + curPosition[1] - self._startPosition[1], + ) + delta = (dx ** 2 + dy ** 2) ** (0.5) + if self.MOVE_THRESHHOLD <= delta: + self._clear() + + def _on_popup(self, widget): + self._clear() + position = 0, 0 + self._respond(position) + class LoginWindow(object): @@ -121,9 +242,15 @@ class ErrorDisplay(object): else: self.__show_message(message) - def push_exception(self, exception): - self.push_message(exception.message) - warnings.warn(exception, stacklevel=3) + def push_exception(self, exception = None): + if exception is None: + userMessage = sys.exc_value.message + warningMessage = traceback.format_exc() + else: + userMessage = exception.message + warningMessage = exception + self.push_message(userMessage) + warnings.warn(warningMessage, stacklevel=3) def pop_message(self): if 0 < len(self.__messages): @@ -185,30 +312,149 @@ class MessageBox2(gtk.MessageDialog): class PopupCalendar(object): - def __init__(self, parent): - self.__calendar = gtk.Calendar() - self.__calendar.connect("day-selected-double-click", self._on_date_select) - - self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP) - self.__popupWindow.set_title("") - self.__popupWindow.add(self.__calendar) - self.__popupWindow.set_transient_for(parent) - self.__popupWindow.set_modal(True) + def __init__(self, parent, displayDate, title = ""): + self._displayDate = displayDate + + self._calendar = gtk.Calendar() + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + self._calendar.set_display_options( + gtk.CALENDAR_SHOW_HEADING | + gtk.CALENDAR_SHOW_DAY_NAMES | + gtk.CALENDAR_NO_MONTH_CHANGE | + 0 + ) + self._calendar.connect("day-selected", self._on_day_selected) - def get_date(self): - year, month, day = self.__calendar.get_date() - month += 1 # Seems to be 0 indexed - return year, month, day + self._popupWindow = gtk.Window() + self._popupWindow.set_title(title) + self._popupWindow.add(self._calendar) + self._popupWindow.set_transient_for(parent) + self._popupWindow.set_modal(True) + self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self._popupWindow.set_skip_pager_hint(True) + self._popupWindow.set_skip_taskbar_hint(True) def run(self): - self.__popupWindow.show_all() + self._popupWindow.show_all() + + def _on_day_selected(self, *args): + try: + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + except StandardError, e: + warnings.warn(e.message) + + +class NotesDialog(object): + + def __init__(self, widgetTree): + self._dialog = widgetTree.get_widget("notesDialog") + self._notesBox = widgetTree.get_widget("notes-notesBox") + self._addButton = widgetTree.get_widget("notes-addButton") + self._saveButton = widgetTree.get_widget("notes-saveButton") + self._cancelButton = widgetTree.get_widget("notes-cancelButton") + self._onAddId = None + self._onSaveId = None + self._onCancelId = None + + self._notes = [] + self._notesToDelete = [] + + def enable(self): + self._dialog.set_default_size(800, 300) + self._onAddId = self._addButton.connect("clicked", self._on_add_clicked) + self._onSaveId = self._saveButton.connect("clicked", self._on_save_clicked) + self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked) + + def disable(self): + self._addButton.disconnect(self._onAddId) + self._saveButton.disconnect(self._onSaveId) + self._cancelButton.disconnect(self._onCancelId) + + def run(self, todoManager, taskId, parentWindow = None): + if parentWindow is not None: + self._dialog.set_transient_for(parentWindow) + + taskDetails = todoManager.get_task_details(taskId) + + self._dialog.set_default_response(gtk.RESPONSE_OK) + for note in taskDetails["notes"].itervalues(): + noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox(note) + noteDeleteButton.connect("clicked", self._on_delete_existing, note["id"], noteBox) + + try: + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Edit Cancelled") + finally: + self._dialog.hide() + + for note in self._notes: + noteId = note[0] + noteTitle = note[2].get_text() + noteBody = note[4].get_buffer().get_text() + if noteId is None: + print "New note:", note + todoManager.add_note(taskId, noteTitle, noteBody) + else: + # @todo Provide way to only update on change + print "Updating note:", note + todoManager.update_note(noteId, noteTitle, noteBody) + + for deletedNoteId in self._notesToDelete: + print "Deleted note:", deletedNoteId + todoManager.delete_note(noteId) + + def _append_notebox(self, noteDetails = None): + if noteDetails is None: + noteDetails = {"id": None, "title": "", "body": ""} + + noteBox = gtk.VBox() + + titleBox = gtk.HBox() + titleEntry = gtk.Entry() + titleEntry.set_text(noteDetails["title"]) + titleBox.pack_start(titleEntry, True, True) + noteDeleteButton = gtk.Button(stock=gtk.STOCK_DELETE) + titleBox.pack_end(noteDeleteButton, False, False) + noteBox.pack_start(titleBox, False, True) + + noteEntryScroll = gtk.ScrolledWindow() + noteEntryScroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + noteEntry = gtk.TextView() + noteEntry.set_editable(True) + noteEntry.set_wrap_mode(gtk.WRAP_WORD) + noteEntry.get_buffer().set_text(noteDetails["body"]) + noteEntry.set_size_request(-1, 150) + noteEntryScroll.add(noteEntry) + noteBox.pack_start(noteEntryScroll, True, True) + + self._notesBox.pack_start(noteBox, True, True) + noteBox.show_all() + + note = noteDetails["id"], noteBox, titleEntry, noteDeleteButton, noteEntry + self._notes.append(note) + return note[1:] - def _on_date_select(self, *args): - self.__popupWindow.hide() - self.callback() + def _on_add_clicked(self, *args): + noteBox, titleEntry, noteDeleteButton, noteEntry = self._append_notebox() + noteDeleteButton.connect("clicked", self._on_delete_new, noteBox) + + def _on_save_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_cancel_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + def _on_delete_new(self, widget, noteBox): + self._notesBox.remove(noteBox) + self._notes = [note for note in self._notes if note[1] is not noteBox] - def callback(self): - pass + def _on_delete_existing(self, widget, noteId, noteBox): + self._notesBox.remove(noteBox) + self._notes = [note for note in self._notes if note[1] is not noteBox] + self._notesToDelete.append(noteId) class EditTaskDialog(object): @@ -221,32 +467,31 @@ class EditTaskDialog(object): self._taskName = widgetTree.get_widget("edit-taskNameEntry") self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton") self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo") - self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay") - self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties") + self._dueDateDisplay = widgetTree.get_widget("edit-dueDateCalendar") self._clearDueDate = widgetTree.get_widget("edit-clearDueDate") self._addButton = widgetTree.get_widget("edit-commitEditTaskButton") self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton") - self._popupCalendar = PopupCalendar(self._dialog) + self._onPasteTaskId = None + self._onClearDueDateId = None + self._onAddId = None + self._onCancelId = None def enable(self, todoManager): self._populate_projects(todoManager) - self._pasteTaskNameButton.connect("clicked", self._on_name_paste) - self._dueDateProperties.connect("clicked", self._on_choose_duedate) - self._clearDueDate.connect("clicked", self._on_clear_duedate) - self._addButton.connect("clicked", self._on_add_clicked) - self._cancelButton.connect("clicked", self._on_cancel_clicked) - - self._popupCalendar.callback = self._update_duedate + self._onPasteTaskId = self._pasteTaskNameButton.connect("clicked", self._on_name_paste) + self._onClearDueDateId = self._clearDueDate.connect("clicked", self._on_clear_duedate) + self._onAddId = self._addButton.connect("clicked", self._on_add_clicked) + self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked) def disable(self): - self._popupCalendar.callback = lambda: None + self._pasteTaskNameButton.disconnect(self._onPasteTaskId) + self._clearDueDate.disconnect(self._onClearDueDateId) + self._addButton.disconnect(self._onAddId) + self._cancelButton.disconnect(self._onCancelId) - self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste) - self._dueDateProperties.disconnect("clicked", self._on_choose_duedate) - self._clearDueDate.disconnect("clicked", self._on_clear_duedate) self._projectsList.clear() self._projectCombo.set_model(None) @@ -258,17 +503,24 @@ class EditTaskDialog(object): originalProjectId = taskDetails["projId"] originalProjectName = todoManager.get_project(originalProjectId)["name"] originalName = taskDetails["name"] - try: - originalPriority = int(taskDetails["priority"]) - except ValueError: - originalPriority = 0 - originalDue = taskDetails["due"] + originalPriority = str(taskDetails["priority"].get_nothrow(0)) + if taskDetails["dueDate"].is_good(): + originalDue = taskDetails["dueDate"].get() + else: + originalDue = None self._dialog.set_default_response(gtk.RESPONSE_OK) self._taskName.set_text(originalName) self._set_active_proj(originalProjectName) - self._priorityChoiceCombo.set_active(originalPriority) - self._dueDateDisplay.set_text(originalDue) + self._priorityChoiceCombo.set_active(int(originalPriority)) + if originalDue is not None: + # Months are 0 indexed + self._dueDateDisplay.select_month(originalDue.month - 1, originalDue.year) + self._dueDateDisplay.select_day(originalDue.day) + else: + now = datetime.datetime.now() + self._dueDateDisplay.select_month(now.month, now.year) + self._dueDateDisplay.select_day(0) try: response = self._dialog.run() @@ -280,7 +532,14 @@ class EditTaskDialog(object): newProjectName = self._get_project(todoManager) newName = self._taskName.get_text() newPriority = self._get_priority() - newDueDate = self._dueDateDisplay.get_text() + year, month, day = self._dueDateDisplay.get_date() + if day != 0: + # Months are 0 indexed + date = datetime.date(year, month + 1, day) + time = datetime.time() + newDueDate = datetime.datetime.combine(date, time) + else: + newDueDate = None isProjDifferent = newProjectName != originalProjectName isNameDifferent = newName != originalName @@ -295,10 +554,19 @@ class EditTaskDialog(object): todoManager.set_name(taskId, newName) print "NAME CHANGE" if isPriorityDifferent: - todoManager.set_priority(taskId, newPriority) + try: + priority = toolbox.Optional(int(newPriority)) + except ValueError: + priority = toolbox.Optional() + todoManager.set_priority(taskId, priority) print "PRIO CHANGE" if isDueDifferent: - todoManager.set_duedate(taskId, newDueDate) + if newDueDate: + due = toolbox.Optional(newDueDate) + else: + due = toolbox.Optional() + + todoManager.set_duedate(taskId, due) print "DUE CHANGE" return { @@ -338,26 +606,14 @@ class EditTaskDialog(object): else: return str(index) - def _get_date(self): - # @bug The date is not used in a consistent manner causing ... issues - dateParts = self._popupCalendar.get_date() - dateParts = (str(part) for part in dateParts) - return "-".join(dateParts) - - def _update_duedate(self): - self._dueDateDisplay.set_text(self._get_date()) - def _on_name_paste(self, *args): clipboard = gtk.clipboard_get() contents = clipboard.wait_for_text() if contents is not None: self._taskName.set_text(contents) - def _on_choose_duedate(self, *args): - self._popupCalendar.run() - def _on_clear_duedate(self, *args): - self._dueDateDisplay.set_text("") + self._dueDateDisplay.select_day(0) def _on_add_clicked(self, *args): self._dialog.response(gtk.RESPONSE_OK) @@ -365,3 +621,83 @@ class EditTaskDialog(object): def _on_cancel_clicked(self, *args): self._dialog.response(gtk.RESPONSE_CANCEL) + +class PreferencesDialog(object): + + def __init__(self, widgetTree): + self._backendList = gtk.ListStore(gobject.TYPE_STRING) + self._backendCell = gtk.CellRendererText() + + self._dialog = widgetTree.get_widget("preferencesDialog") + self._backendSelector = widgetTree.get_widget("prefsBackendSelector") + self._applyButton = widgetTree.get_widget("applyPrefsButton") + self._cancelButton = widgetTree.get_widget("cancelPrefsButton") + + self._onApplyId = None + self._onCancelId = None + + def enable(self): + self._dialog.set_default_size(800, 300) + self._onApplyId = self._applyButton.connect("clicked", self._on_apply_clicked) + self._onCancelId = self._cancelButton.connect("clicked", self._on_cancel_clicked) + + cell = self._backendCell + self._backendSelector.pack_start(cell, True) + self._backendSelector.add_attribute(cell, 'text', 0) + self._backendSelector.set_model(self._backendList) + + def disable(self): + self._applyButton.disconnect(self._onApplyId) + self._cancelButton.disconnect(self._onCancelId) + + self._backendList.clear() + self._backendSelector.set_model(None) + + def run(self, app, parentWindow = None): + if parentWindow is not None: + self._dialog.set_transient_for(parentWindow) + + self._backendList.clear() + activeIndex = 0 + for i, (uiName, ui) in enumerate(app.get_uis()): + self._backendList.append((uiName, )) + if uiName == app.get_default_ui(): + activeIndex = i + self._backendSelector.set_active(activeIndex) + + try: + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Edit Cancelled") + finally: + self._dialog.hide() + + backendName = self._backendSelector.get_active_text() + app.switch_ui(backendName) + + def _on_apply_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_cancel_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + +if __name__ == "__main__": + if True: + win = gtk.Window() + win.set_title("Tap'N'Hold") + eventBox = gtk.EventBox() + win.add(eventBox) + + context = ContextHandler(eventBox, coroutines.printer_sink()) + context.enable() + win.connect("destroy", lambda w: gtk.main_quit()) + + win.show_all() + + if False: + cal = PopupCalendar(None, datetime.datetime.now()) + cal._popupWindow.connect("destroy", lambda w: gtk.main_quit()) + cal.run() + + gtk.main()