class TaskEditor(object): def __init__(self, requester, vmanager, task, taskconfig=None, thisisnew=False, clipboard=None): ''' req is the requester vmanager is the view manager taskconfig is a ConfigParser to save infos about tasks thisisnew is True when a new task is created and opened ''' self.req = requester self.browser_config = self.req.get_config('browser') self.vmanager = vmanager self.config = taskconfig self.time = None self.clipboard = clipboard self.builder = Gtk.Builder() self.builder.add_from_file(GnomeConfig.EDITOR_UI_FILE) self.donebutton = self.builder.get_object("mark_as_done_editor") self.dismissbutton = self.builder.get_object("dismiss_editor") self.deletebutton = self.builder.get_object("delete_editor") self.deletebutton.set_tooltip_text(GnomeConfig.DELETE_TOOLTIP) self.subtask_button = self.builder.get_object("insert_subtask") self.subtask_button.set_tooltip_text(GnomeConfig.SUBTASK_TOOLTIP) self.inserttag_button = self.builder.get_object("inserttag") self.inserttag_button.set_tooltip_text(GnomeConfig.TAG_TOOLTIP) self.open_parents_button = self.builder.get_object("open_parents") self.open_parents_button.set_tooltip_text( GnomeConfig.OPEN_PARENT_TOOLTIP) # Create our dictionary and connect it dic = { "mark_as_done_clicked": self.change_status, "on_dismiss": self.dismiss, "delete_clicked": self.delete_task, "on_duedate_pressed": lambda w: self.on_date_pressed( w, GTGCalendar.DATE_KIND_DUE), "on_startdate_pressed": lambda w: self.on_date_pressed( w, GTGCalendar.DATE_KIND_START), "on_closeddate_pressed": lambda w: self.on_date_pressed( w, GTGCalendar.DATE_KIND_CLOSED), "close_clicked": self.close, "duedate_changed": lambda w: self.date_changed( w, GTGCalendar.DATE_KIND_DUE), "duedate_focus_out": lambda w, e: self.date_focus_out( w, e, GTGCalendar.DATE_KIND_DUE), "startingdate_changed": lambda w: self.date_changed( w, GTGCalendar.DATE_KIND_START), "startdate_focus_out": lambda w, e: self.date_focus_out( w, e, GTGCalendar.DATE_KIND_START), "closeddate_changed": lambda w: self.date_changed( w, GTGCalendar.DATE_KIND_CLOSED), "closeddate_focus_out": lambda w, e: self.date_focus_out( w, e, GTGCalendar.DATE_KIND_CLOSED), "on_insert_subtask_clicked": self.insert_subtask, "on_inserttag_clicked": self.inserttag_clicked, "on_open_parent_clicked": self.open_parent_clicked, "on_move": self.on_move, } self.builder.connect_signals(dic) self.window = self.builder.get_object("TaskEditor") # Removing the Normal textview to replace it by our own # So don't try to change anything with glade, this is a home-made # widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req, self.clipboard) self.textview.show() self.textview.set_subtask_callback(self.new_subtask) self.textview.open_task_callback(self.vmanager.open_task) self.textview.set_left_margin(7) self.textview.set_right_margin(5) scrolled.add(self.textview) conf_font_value = self.browser_config.get("font_name") if conf_font_value != "": self.textview.override_font(Pango.FontDescription(conf_font_value)) # Voila! it's done self.calendar = GTGCalendar() self.calendar.set_transient_for(self.window) self.calendar.set_decorated(False) self.duedate_widget = self.builder.get_object("duedate_entry") self.startdate_widget = self.builder.get_object("startdate_entry") self.closeddate_widget = self.builder.get_object("closeddate_entry") self.dayleft_label = self.builder.get_object("dayleft") self.tasksidebar = self.builder.get_object("tasksidebar") # Define accelerator keys self.init_accelerators() self.task = task tags = task.get_tags() self.textview.subtasks_callback(task.get_children) self.textview.removesubtask_callback(task.remove_child) self.textview.set_get_tagslist_callback(task.get_tags_name) self.textview.set_add_tag_callback(task.add_tag) self.textview.set_remove_tag_callback(task.remove_tag) self.textview.save_task_callback(self.light_save) texte = self.task.get_text() title = self.task.get_title() # the first line is the title self.textview.set_text("%s\n" % title) # we insert the rest of the task if texte: self.textview.insert("%s" % texte) else: # If not text, we insert tags if tags: for t in tags: self.textview.insert_text("%s, " % t.get_name()) self.textview.insert_text("\n") # If we don't have text, we still need to insert subtasks if any subtasks = task.get_children() if subtasks: self.textview.insert_subtasks(subtasks) # We select the title if it's a new task if thisisnew: self.textview.select_title() else: self.task.set_to_keep() self.textview.modified(full=True) self.window.connect("destroy", self.destruction) self.calendar.connect("date-changed", self.on_date_changed) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.vmanager, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) # Putting the refresh callback at the end make the start a lot faster self.textview.refresh_callback(self.refresh_editor) self.refresh_editor() self.textview.grab_focus() # restoring size and position, spatial tasks if self.config is not None: tid = self.task.get_id() if self.config.has_section(tid): if self.config.has_option(tid, "position"): pos_x, pos_y = self.config.get(tid, "position") self.move(int(pos_x), int(pos_y)) if self.config.has_option(tid, "size"): width, height = self.config.get(tid, "size") self.window.resize(int(width), int(height)) self.textview.set_editable(True) self.window.show() # Define accelerator-keys for this dialog # TODO: undo/redo def init_accelerators(self): agr = Gtk.AccelGroup() self.window.add_accel_group(agr) # Escape and Ctrl-W close the dialog. It's faster to call close # directly, rather than use the close button widget key, modifier = Gtk.accelerator_parse('Escape') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.close) key, modifier = Gtk.accelerator_parse('<Control>w') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.close) # F1 shows help add_help_shortcut(self.window, "editor") # Ctrl-N creates a new task key, modifier = Gtk.accelerator_parse('<Control>n') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.new_task) # Ctrl-Shift-N creates a new subtask insert_subtask = self.builder.get_object("insert_subtask") key, mod = Gtk.accelerator_parse("<Control><Shift>n") insert_subtask.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-D marks task as done mark_as_done_editor = self.builder.get_object('mark_as_done_editor') key, mod = Gtk.accelerator_parse('<Control>d') mark_as_done_editor.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-I marks task as dismissed dismiss_editor = self.builder.get_object('dismiss_editor') key, mod = Gtk.accelerator_parse('<Control>i') dismiss_editor.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-Q quits GTG key, modifier = Gtk.accelerator_parse('<Control>q') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.quit) # Can be called at any time to reflect the status of the Task # Refresh should never interfere with the TaskView. # If a title is passed as a parameter, it will become # the new window title. If not, we will look for the task title. # Refreshtext is whether or not we should refresh the TaskView # (doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window is None: return to_save = False # title of the window if title: self.window.set_title(title) to_save = True else: self.window.set_title(self.task.get_title()) status = self.task.get_status() dismiss_tooltip = GnomeConfig.MARK_DISMISS_TOOLTIP undismiss_tooltip = GnomeConfig.MARK_UNDISMISS_TOOLTIP if status == Task.STA_DISMISSED: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_UNDISMISS) self.dismissbutton.set_tooltip_text(undismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-undismiss") elif status == Task.STA_DONE: self.donebutton.set_label(GnomeConfig.MARK_UNDONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_UNDONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-undone") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(dismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-dismiss") else: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(dismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-dismiss") self.donebutton.show() self.tasksidebar.show() # Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("label2").hide() self.builder.get_object("box1").hide() self.builder.get_object("label4").show() self.builder.get_object("box4").show() else: self.builder.get_object("label4").hide() self.builder.get_object("box4").hide() self.builder.get_object("label2").show() self.builder.get_object("box1").show() # refreshing the start date field startdate = self.task.get_start_date() try: prevdate = Date.parse(self.startdate_widget.get_text()) update_date = startdate != prevdate except ValueError: update_date = True if update_date: self.startdate_widget.set_text(str(startdate)) # refreshing the due date field duedate = self.task.get_due_date() try: prevdate = Date.parse(self.duedate_widget.get_text()) update_date = duedate != prevdate except ValueError: update_date = True if update_date: self.duedate_widget.set_text(str(duedate)) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = Date.parse(self.closeddate_widget.get_text()) if closeddate != prevcldate: self.closeddate_widget.set_text(str(closeddate)) # refreshing the day left label # If the task is marked as done, we display the delay between the # due date and the actual closing date. If the task isn't marked # as done, we display the number of days left. if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % \ {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % \ {'days': abs_delay} else: due_date = self.task.get_due_date() result = due_date.days_left() if due_date.is_fuzzy(): txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result) \ % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % {'days': abs_result} style_context = self.window.get_style_context() color = style_context.get_color(Gtk.StateFlags.INSENSITIVE).to_color() self.dayleft_label.set_markup( "<span color='%s'>%s</span>" % (color.to_string(), txt)) # Refreshing the tag list in the insert tag button taglist = self.req.get_used_tags() menu = Gtk.Menu() tag_count = 0 for tagname in taglist: tag_object = self.req.get_tag(tagname) if not tag_object.is_special() and \ not self.task.has_tags(tag_list=[tagname]): tag_count += 1 mi = Gtk.MenuItem(label=tagname, use_underline=False) mi.connect("activate", self.inserttag, tagname) mi.show() menu.append(mi) if tag_count > 0: self.inserttag_button.set_menu(menu) # Refreshing the parent list in open_parent_button menu = Gtk.Menu() parents = self.task.get_parents() if len(parents) > 0: for parent in self.task.get_parents(): task = self.req.get_task(parent) mi = Gtk.MenuItem(label=task.get_title(), use_underline=False) mi.connect("activate", self.open_parent, parent) mi.show() menu.append(mi) self.open_parents_button.set_menu(menu) else: self.open_parents_button.set_sensitive(False) if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def date_changed(self, widget, data): try: Date.parse(widget.get_text()) valid = True except ValueError: valid = False if valid: # If the date is valid, we write with default color in the widget # "none" will set the default color. widget.override_color(Gtk.StateType.NORMAL, None) widget.override_background_color(Gtk.StateType.NORMAL, None) else: # We should write in red in the entry if the date is not valid text_color = Gdk.RGBA() text_color.parse("#F00") widget.override_color(Gtk.StateType.NORMAL, text_color) bg_color = Gdk.RGBA() bg_color.parse("#F88") widget.override_background_color(Gtk.StateType.NORMAL, bg_color) def date_focus_out(self, widget, event, date_kind): try: datetoset = Date.parse(widget.get_text()) except ValueError: datetoset = None if datetoset is not None: if date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(datetoset) elif date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(datetoset) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(datetoset) self.refresh_editor() def on_date_pressed(self, widget, date_kind): """Called when a date-changing button is clicked.""" if date_kind == GTGCalendar.DATE_KIND_DUE: if not self.task.get_due_date(): date = self.task.get_start_date() else: date = self.task.get_due_date() elif date_kind == GTGCalendar.DATE_KIND_START: date = self.task.get_start_date() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: date = self.task.get_closed_date() self.calendar.set_date(date, date_kind) # we show the calendar at the right position rect = widget.get_allocation() result, x, y = widget.get_window().get_origin() self.calendar.show_at_position(x + rect.x + rect.width, y + rect.y) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def close_all_subtasks(self): all_subtasks = [] def trace_subtasks(root): for i in root.get_subtasks(): if i not in all_subtasks: all_subtasks.append(i) trace_subtasks(i) trace_subtasks(self.task) for task in all_subtasks: self.vmanager.close_task(task.get_id()) def dismiss(self, widget): stat = self.task.get_status() if stat == Task.STA_DISMISSED: self.vmanager.ask_set_task_status(self.task, Task.STA_ACTIVE) self.refresh_editor() else: self.vmanager.ask_set_task_status(self.task, Task.STA_DISMISSED) self.close_all_subtasks() self.close(None) def change_status(self, widget): stat = self.task.get_status() if stat == Task.STA_DONE: self.vmanager.ask_set_task_status(self.task, Task.STA_ACTIVE) self.refresh_editor() else: self.vmanager.ask_set_task_status(self.task, Task.STA_DONE) self.close_all_subtasks() self.close(None) def delete_task(self, widget): # this triggers the closing of the window in the view manager if self.task.is_new(): # self.req.delete_task(self.task.get_id()) self.vmanager.close_task(self.task.get_id()) else: self.vmanager.ask_delete_tasks([self.task.get_id()]) # Take the title as argument and return the subtask ID def new_subtask(self, title=None, tid=None): if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid # Create a new task def new_task(self, *args): task = self.req.new_task(newtask=True) task_id = task.get_id() self.vmanager.open_task(task_id) def insert_subtask(self, widget): self.textview.insert_newtask() self.textview.grab_focus() def inserttag_clicked(self, widget): itera = self.textview.get_insert() if itera.starts_line(): self.textview.insert_text("@", itera) else: self.textview.insert_text(" @", itera) self.textview.grab_focus() def inserttag(self, widget, tag): self.textview.insert_tags([tag]) self.textview.grab_focus() def open_parent_clicked(self, widget): self.vmanager.open_task(self.task.get_parents()[0]) # On click handler for open_parent_button's menu items def open_parent(self, widget, tid): self.vmanager.open_task(tid) def save(self): self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config is not None: self.config.save() self.time = time.time() # light_save save the task without refreshing every 30seconds # We will reduce the time when the get_text will be in another thread def light_save(self): # if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: # we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() # This will bring the Task Editor to front def present(self): self.window.present() def move(self, x, y): try: xx = int(x) yy = int(y) self.window.move(xx, yy) except: pass def get_position(self): return self.window.get_position() def on_move(self, widget, event): # saving the position if self.config is not None: tid = self.task.get_id() if not self.config.has_section(tid): self.config.add_section(tid) self.config.set(tid, "position", self.get_position()) self.config.set(tid, "size", self.window.get_size()) # We define dummy variable for when close is called from a callback def close(self, window=None, a=None, b=None, c=None): # We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None # The destroy signal is linked to the "close" button. So if we call # destroy in the close function, this will cause the close to be called # twice # To solve that, close will just call "destroy" and the destroy signal # Will be linked to this destruction method that will save the task def destruction(self, a=None): # Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() for i in self.task.get_subtasks(): if i: i.set_to_keep() self.vmanager.close_task(tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window def quit(self, accel_group=None, acceleratable=None, keyval=None, modifier=None): """Handles the accelerator for quitting GTG.""" self.vmanager.quit()
class TaskEditor(): EDITOR_UI_FILE = os.path.join(UI_DIR, "task_editor.ui") def __init__(self, requester, app, task, thisisnew=False, clipboard=None): """ req is the requester app is the view manager thisisnew is True when a new task is created and opened """ self.req = requester self.app = app self.browser_config = self.req.get_config('browser') self.config = self.req.get_task_config(task.get_id()) self.time = None self.clipboard = clipboard self.builder = Gtk.Builder() self.builder.add_from_file(self.EDITOR_UI_FILE) self.donebutton = self.builder.get_object("mark_as_done") self.undonebutton = self.builder.get_object("mark_as_undone") self.dismissbutton = self.builder.get_object("dismiss") self.undismissbutton = self.builder.get_object("undismiss") self.add_subtask = self.builder.get_object("add_subtask") self.tag_store = self.builder.get_object("tag_store") self.parent_button = self.builder.get_object("parent") # Closed date self.closed_popover = self.builder.get_object("closed_popover") self.closed_entry = self.builder.get_object("closeddate_entry") self.closed_calendar = self.builder.get_object("calendar_closed") # Start date self.start_popover = self.builder.get_object("start_popover") self.start_entry = self.builder.get_object("startdate_entry") self.start_calendar = self.builder.get_object("calendar_start") # Due date self.due_popover = self.builder.get_object("due_popover") self.due_entry = self.builder.get_object("duedate_entry") self.due_calendar = self.builder.get_object("calendar_due") # Recurrence self.recurring_menu = RecurringMenu(self.req, task.tid, self.builder) # Create our dictionary and connect it dic = { "on_tags_popover": self.open_tags_popover, "on_tag_toggled": self.on_tag_toggled, "on_move": self.on_move, "set_recurring_term_every_day": self.set_recurring_term_every_day, "set_recurring_term_every_otherday": self.set_recurring_term_every_otherday, "set_recurring_term_every_week": self.set_recurring_term_every_week, "set_recurring_term_every_month": self.set_recurring_term_every_month, "set_recurring_term_every_year": self.set_recurring_term_every_year, "set_recurring_term_week_day": self.set_recurring_term_week_day, "set_recurring_term_calender_month": self.set_recurring_term_month, "set_recurring_term_calender_year": self.set_recurring_term_year, "toggle_recurring_status": self.toggle_recurring_status, "on_repeat_icon_toggled": self.on_repeat_icon_toggled, "show_popover_start": self.show_popover_start, "startingdate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_START), "startdate_cleared": lambda w: self.on_date_cleared(w, GTGCalendar.DATE_KIND_START), "startdate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_START ), "show_popover_due": self.show_popover_due, "duedate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_DUE), "duedate_now_selected": lambda w: self.on_duedate_fuzzy(w, Date.now()), "duedate_soon_selected": lambda w: self.on_duedate_fuzzy(w, Date.soon()), "duedate_someday_selected": lambda w: self.on_duedate_fuzzy(w, Date.someday()), "duedate_cleared": lambda w: self.on_date_cleared(w, GTGCalendar.DATE_KIND_DUE), "duedate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_DUE), "show_popover_closed": self.show_popover_closed, "closeddate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_CLOSED), "closeddate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_CLOSED ), } self.window = self.builder.get_object("TaskEditor") self.builder.connect_signals(dic) self.window.set_application(app) if task.has_parent(): self.parent_button.set_label(_('Open Parent')) else: self.parent_button.set_label(_('Add Parent')) # Connect signals for the calendar self.start_handle = self.start_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_START)) self.due_handle = self.due_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_DUE)) self.closed_handle = self.closed_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_CLOSED)) # Removing the Normal textview to replace it by our own # So don't try to change anything with glade, this is a home-made # widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req, self.clipboard) self.textview.show() scrolled.add(self.textview) conf_font_value = self.browser_config.get("font_name") if conf_font_value != "": self.textview.override_font(Pango.FontDescription(conf_font_value)) self.textview.browse_tag_cb = app.select_tag self.textview.new_subtask_cb = self.new_subtask self.textview.get_subtasks_cb = task.get_children self.textview.delete_subtask_cb = self.remove_subtask self.textview.rename_subtask_cb = self.rename_subtask self.textview.open_subtask_cb = self.open_subtask self.textview.save_cb = self.light_save self.textview.add_tasktag_cb = task.tag_added self.textview.remove_tasktag_cb = task.remove_tag self.textview.refresh_cb = self.refresh_editor self.textview.get_tagslist_cb = task.get_tags_name self.textview.tid = task.tid # Voila! it's done self.textview.connect('focus-in-event', self.on_textview_focus_in) self.textview.connect('focus-out-event', self.on_textview_focus_out) """ TODO(jakubbrindza): Once all the functionality in editor is back and working, bring back also the accelerators! Dayleft_label needs to be brought back, however its position is unsure. """ # self.dayleft_label = self.builder.get_object("dayleft") self.task = task tags = task.get_tags() text = self.task.get_text() title = self.task.get_title() self.textview.buffer.set_text(f"{title}\n") if text: self.textview.insert(text) # Insert any remaining tags if tags: tag_names = [t.get_name() for t in tags] self.textview.insert_tags(tag_names) else: # If not text, we insert tags if tags: tag_names = [t.get_name() for t in tags] self.textview.insert_tags(tag_names) start = self.textview.buffer.get_end_iter() self.textview.buffer.insert(start, '\n') # Insert subtasks if they weren't inserted in the text subtasks = task.get_children() for sub in subtasks: if sub not in self.textview.subtasks['tags']: self.textview.insert_existing_subtask(sub) if thisisnew: self.textview.select_title() else: self.task.set_to_keep() self.window.connect("destroy", self.destruction) # Connect search field to tags popup self.tags_entry = self.builder.get_object("tags_entry") self.tags_tree = self.builder.get_object("tags_tree") self.tags_tree.set_search_entry(self.tags_entry) self.tags_tree.set_search_equal_func(self.search_function, None) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.app, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) # Putting the refresh callback at the end make the start a lot faster self.refresh_editor() self.textview.grab_focus() self.init_dimensions() self.window.insert_action_group('app', app) self.window.insert_action_group('win', app.browser) self.textview.set_editable(True) self.window.set_transient_for(self.app.browser) self.window.show() def show_popover_start(self, widget, event): """Open the start date calendar popup.""" start_date = self.task.get_start_date() or Date.today() with signal_handler_block(self.start_calendar, self.start_handle): self.start_calendar.select_day(start_date.day) self.start_calendar.select_month(start_date.month - 1, start_date.year) self.start_popover.popup() def show_popover_due(self, widget, popover): """Open the due date calendar popup.""" due_date = self.task.get_due_date() if not due_date or due_date.is_fuzzy(): due_date = Date.today() with signal_handler_block(self.due_calendar, self.due_handle): self.due_calendar.select_day(due_date.day) self.due_calendar.select_month(due_date.month - 1, due_date.year) self.due_popover.popup() def show_popover_closed(self, widget, popover): """Open the closed date calendar popup.""" closed_date = self.task.get_closed_date() with signal_handler_block(self.closed_calendar, self.closed_handle): self.closed_calendar.select_day(closed_date.day) self.closed_calendar.select_month(closed_date.month - 1, closed_date.year) self.closed_popover.popup() def open_tags_popover(self): self.tag_store.clear() tags = self.req.get_tag_tree().get_all_nodes() used_tags = self.task.get_tags() for tagname in tags: tag = self.req.get_tag(tagname) if tag_filter(tag): is_used = tag in used_tags self.tag_store.append([is_used, tagname]) """ TODO(jakubbrindza): add sorting of the tags based on True | False and within each sub-group arrange them alphabetically """ def on_tag_toggled(self, widget, path, column): """We toggle by tag_row variable. tag_row is meant to be a tuple (is_used, tagname)""" tag_row = self.tag_store[path] tag_row[0] = not tag_row[0] if tag_row[0]: self.textview.insert_tags([tag_row[1]]) """ TODO(jakubbrindza): Add else case that will remove tag. """ def on_repeat_icon_toggled(self, widget): """ Reset popup stack to the first page every time you open it """ if widget.get_active(): self.recurring_menu.reset_stack() def toggle_recurring_status(self, widget): self.recurring_menu.update_repeat_checkbox() self.refresh_editor() def set_recurring_term_every_day(self, widget): self.recurring_menu.set_selected_term('day') self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_every_otherday(self, widget): self.recurring_menu.set_selected_term('other-day') self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_every_week(self, widget): self.recurring_menu.set_selected_term('week') self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_every_month(self, widget): self.recurring_menu.set_selected_term('month') self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_every_year(self, widget): self.recurring_menu.set_selected_term('year') self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_week_day(self, widget): self.recurring_menu.set_selected_term(widget.get_property("name")) self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_month(self, widget): self.recurring_menu.set_selected_term(str(widget.get_date()[2])) self.recurring_menu.update_term() self.refresh_editor() def set_recurring_term_year(self, widget): month = str(widget.get_date()[1] + 1) day = str(widget.get_date()[2]) if len(month) < 2: month = "0" + month if len(day) < 2: day = "0" + day self.recurring_menu.set_selected_term(month + day) self.recurring_menu.update_term() self.refresh_editor() def search_function(self, model, column, key, iter, *search_data): """Callback when searching in the tags popup.""" if not key.startswith('@'): key = f'@{key}' # The return value is reversed. False if it matches, True # otherwise. return not model.get(iter, column)[0].startswith(key) def get_monitor_dimensions(self) -> Gdk.Rectangle: """Get dimensions for the first monitor.""" monitor = Gdk.Display.get_default().get_monitor(0) return monitor.get_geometry() def init_dimensions(self): """ Restores position and size of task if possible """ position = self.config.get('position') size = self.config.get('size') screen_size = self.get_monitor_dimensions() if size and len(size) == 2: try: self.window.resize(int(size[0]), int(size[1])) except ValueError: log.warning('Invalid size configuration for task %s: %s', self.task.get_id(), size) can_move = True if position and len(position) == 2: try: x = max(0, int(position[0])) y = max(0, int(position[1])) can_move = True except ValueError: can_move = False log.warning('Invalid position configuration for task %s:%s', self.task.get_id(), position) else: gdk_window = self.window.get_window() if gdk_window is None: log.debug("Using default display to position editor window") display = Gdk.Display.get_default() else: # TODO: AFAIK never happens because window is not realized at # this point, but maybe we should just in case the display # is actually different. display = gdk_window.get_display() seat = display.get_default_seat() pointer = seat.get_pointer() if pointer is None: can_move = False log.debug( "Didn't receiver pointer info, can't move editor window") else: screen, x, y = pointer.get_position() assert isinstance(x, int) assert isinstance(y, int) if can_move: width, height = self.window.get_size() # Clamp positions to current screen size x = min(x, screen_size.width - width) y = min(y, screen_size.height - height) self.window.move(x, y) # Can be called at any time to reflect the status of the Task # Refresh should never interfere with the TaskView. # If a title is passed as a parameter, it will become # the new window title. If not, we will look for the task title. # Refreshtext is whether or not we should refresh the TaskView # (doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window is None: return to_save = False # title of the window if title: self.window.set_title(title) to_save = True else: self.window.set_title(self.task.get_title()) status = self.task.get_status() if status == Task.STA_DISMISSED: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.hide() self.undismissbutton.show() elif status == Task.STA_DONE: self.donebutton.hide() self.undonebutton.show() self.dismissbutton.show() self.undismissbutton.hide else: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.show() self.undismissbutton.hide() # Refreshing the parent button if self.task.has_parent(): # Translators: Button label to open the parent task self.parent_button.set_label(_('Open Parent')) else: # Translators: Button label to add an new parent task self.parent_button.set_label(_('Add Parent')) # Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("start_box").hide() self.builder.get_object("closed_box").show() else: self.builder.get_object("closed_box").hide() self.builder.get_object("start_box").show() # refreshing the start date field startdate = self.task.get_start_date() try: prevdate = Date.parse(self.start_entry.get_text()) update_date = startdate != prevdate except ValueError: update_date = True if update_date: self.start_entry.set_text(str(startdate)) # refreshing the due date field duedate = self.task.get_due_date() try: prevdate = Date.parse(self.due_entry.get_text()) update_date = duedate != prevdate except ValueError: update_date = True if update_date: self.due_entry.set_text(str(duedate)) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = Date.parse(self.closed_entry.get_text()) if closeddate != prevcldate: self.closed_entry.set_text(str(closeddate)) # refreshing the day left label """ TODO(jakubbrindza): re-enable refreshing the day left. We need to come up how and where this information is viewed in the editor window. """ # self.refresh_day_left() if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def refresh_day_left(self): # If the task is marked as done, we display the delay between the # due date and the actual closing date. If the task isn't marked # as done, we display the number of days left. status = self.task.get_status() if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % \ {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % \ {'days': abs_delay} else: due_date = self.task.get_due_date() result = due_date.days_left() if due_date.is_fuzzy(): txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result)\ % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % { 'days': abs_result } style_context = self.window.get_style_context() color = style_context.get_color(Gtk.StateFlags.INSENSITIVE).to_color() self.dayleft_label.set_markup( f"<span color='{color.to_string()}'>{txt}</span>") def reload_editor(self): task = self.task textview = self.textview task_text = task.get_text() task_title = task.get_title() textview.set_text(f"{task_title}\n") if task_text: textview.insert(f"{task_text}") task.set_title(task_title) textview.modified(full=True) def date_changed(self, widget, data): try: Date.parse(widget.get_text()) valid = True except ValueError: valid = False if valid: # If the date is valid, we write with default color in the widget # "none" will set the default color. widget.override_color(Gtk.StateType.NORMAL, None) widget.override_background_color(Gtk.StateType.NORMAL, None) else: # We should write in red in the entry if the date is not valid text_color = Gdk.RGBA() text_color.parse("#F00") widget.override_color(Gtk.StateType.NORMAL, text_color) bg_color = Gdk.RGBA() bg_color.parse("#F88") widget.override_background_color(Gtk.StateType.NORMAL, bg_color) def date_focus_out(self, widget, event, date_kind): try: datetoset = Date.parse(widget.get_text()) except ValueError: datetoset = None if datetoset is not None: if date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(datetoset) self.start_popover.popdown() elif date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(datetoset) self.due_popover.popdown() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(datetoset) self.closed_popover.popdown() self.refresh_editor() def calendar_to_datetime(self, calendar): """ Gtk.Calendar uses a 0-based convention for counting months. The rest of the world, including the datetime module, starts from 1. This is a converter between the two. GTG follows the datetime convention. """ year, month, day = calendar.get_date() return datetime.date(year, month + 1, day) def on_duedate_fuzzy(self, widget, date): """ Callback when a fuzzy date is selected through the popup. """ self.task.set_due_date(date) self.due_entry.set_text(str(date)) def on_date_cleared(self, widget, kind): """ Callback when a date is cleared through the popups. """ if kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(Date.no_date()) self.start_entry.set_text('') elif kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(Date.no_date()) self.due_entry.set_text('') def on_date_selected(self, calendar, kind): """ Callback when a day is selected in the calendars.""" date = self.calendar_to_datetime(calendar) if kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(Date(date)) self.start_entry.set_text(str(Date(date))) elif kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(Date(date)) self.due_entry.set_text(str(Date(date))) elif kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(Date(date)) self.closed_entry.set_text(str(Date(date))) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def close_all_subtasks(self): all_subtasks = [] def trace_subtasks(root): for i in root.get_subtasks(): if i not in all_subtasks: all_subtasks.append(i) trace_subtasks(i) trace_subtasks(self.task) for task in all_subtasks: self.app.close_task(task.get_id()) def dismiss(self): stat = self.task.get_status() if stat == Task.STA_DISMISSED: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DISMISSED) self.close_all_subtasks() self.close(None) def change_status(self): stat = self.task.get_status() if stat == Task.STA_DONE: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DONE) self.close_all_subtasks() self.close(None) def open_subtask(self, tid): """Open subtask (closing parent task).""" task = self.req.get_task(tid) self.app.open_task(tid) self.app.close_task(task.parents[0]) # Take the title as argument and return the subtask ID def new_subtask(self, title=None, tid=None): if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid def remove_subtask(self, tid): """Remove a subtask of this task.""" self.task.remove_child(tid) def rename_subtask(self, tid, new_title): """Rename a subtask of this task.""" try: self.req.get_task(tid).set_title(new_title) except AttributeError: # There's no task at that tid pass def insert_subtask(self, action=None, param=None): self.textview.insert_new_subtask() self.textview.grab_focus() def inserttag_clicked(self, widget): itera = self.textview.get_insert() if itera.starts_line(): self.textview.insert_text("@", itera) else: self.textview.insert_text(" @", itera) def open_parent(self): """ Open (or create) the parent task, then close the child to avoid various window management issues and to prevent visible content divergence when the child title changes. """ parents = self.task.get_parents() if not parents: tags = [t.get_name() for t in self.task.get_tags()] parent = self.req.new_task(tags=tags, newtask=True) parent_id = parent.get_id() self.task.set_parent(parent_id) self.app.open_task(parent_id) # Prevent WM issues and risks of conflicting content changes: self.close() elif len(parents) == 1: self.app.open_task(parents[0]) # Prevent WM issues and risks of conflicting content changes: self.close() elif len(parents) > 1: self.show_multiple_parent_popover(parents) def show_multiple_parent_popover(self, parent_ids): parent_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) for parent in parent_ids: parent_name = self.req.get_task(parent).get_title() button = Gtk.ToolButton.new(None, parent_name) button.connect("clicked", self.on_parent_item_clicked, parent) parent_box.add(button) self.parent_popover = Gtk.Popover.new(self.parent_button) self.parent_popover.add(parent_box) self.parent_popover.set_property("border-width", 0) self.parent_popover.set_position(Gtk.PositionType.BOTTOM) self.parent_popover.set_transitions_enabled(True) self.parent_popover.show_all() # On click handler for open_parent menu items in the case of multiple parents def on_parent_item_clicked(self, widget, parent_id): self.app.open_task(parent_id) if self.parent_popover.get_visible(): self.parent_popover.hide() # Prevent WM issues and risks of conflicting content changes: self.close() def save(self): self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config is not None: self.config.save() self.time = time.time() # light_save save the task without refreshing every 30seconds # We will reduce the time when the get_text will be in another thread def light_save(self): # if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: # we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() def present(self): # This tries to bring the Task Editor to the front. # If TaskEditor is a "utility" window type, this doesn't work on X11, # it only works on GNOME's Wayland session, unless the child is closed. # This is partly why we use self.close() in various places elsewhere. self.window.present() def get_position(self): return self.window.get_position() def on_move(self, widget, event): """ Save position and size of window """ self.config.set('position', list(self.window.get_position())) self.config.set('size', list(self.window.get_size())) def on_textview_focus_in(self, widget, event): self.app.browser.toggle_delete_accel(False) def on_textview_focus_out(self, widget, event): self.app.browser.toggle_delete_accel(True) # We define dummy variable for when close is called from a callback def close(self, action=None, param=None): # We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None def destruction(self, _=None): """Callback when destroying the window.""" # Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() [sub.set_to_keep() for sub in self.task.get_subtasks() if sub] try: del self.app.open_tasks[tid] except KeyError: log.debug('Task %s was already removed from the open list', tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window
class TaskEditor: def __init__(self, requester, vmanager, task, taskconfig=None, thisisnew=False, clipboard=None): ''' req is the requester vmanager is the view manager taskconfig is a ConfigObj dic to save infos about tasks thisisnew is True when a new task is created and opened ''' self.req = requester self.browser_config = self.req.get_config('browser') self.vmanager = vmanager self.config = taskconfig self.time = None self.clipboard = clipboard self.builder = gtk.Builder() self.builder.add_from_file(GnomeConfig.GLADE_FILE) self.donebutton = self.builder.get_object("mark_as_done_editor") self.dismissbutton = self.builder.get_object("dismiss_editor") self.deletebutton = self.builder.get_object("delete_editor") self.deletebutton.set_tooltip_text(GnomeConfig.DELETE_TOOLTIP) self.subtask_button = self.builder.get_object("insert_subtask") self.subtask_button.set_tooltip_text(GnomeConfig.SUBTASK_TOOLTIP) self.inserttag_button = self.builder.get_object("inserttag") self.inserttag_button.set_tooltip_text(GnomeConfig.TAG_TOOLTIP) # Create our dictionary and connect it dic = { "mark_as_done_clicked": self.change_status, "on_dismiss": self.dismiss, "delete_clicked": self.delete_task, "on_duedate_pressed": (self.on_date_pressed, GTGCalendar.DATE_KIND_DUE), "on_startdate_pressed": (self.on_date_pressed, GTGCalendar.DATE_KIND_START), "on_closeddate_pressed": (self.on_date_pressed, GTGCalendar.DATE_KIND_CLOSED), "close_clicked": self.close, "duedate_changed": (self.date_changed, GTGCalendar.DATE_KIND_DUE), "duedate_focus_out": (self.date_focus_out, GTGCalendar.DATE_KIND_DUE), "startingdate_changed": (self.date_changed, GTGCalendar.DATE_KIND_START), "startdate_focus_out": (self.date_focus_out, GTGCalendar.DATE_KIND_START), "closeddate_changed": (self.date_changed, GTGCalendar.DATE_KIND_CLOSED), "closeddate_focus_out": (self.date_focus_out, GTGCalendar.DATE_KIND_CLOSED), "on_insert_subtask_clicked": self.insert_subtask, "on_inserttag_clicked": self.inserttag_clicked, "on_move": self.on_move, } self.builder.connect_signals(dic) self.window = self.builder.get_object("TaskEditor") # Removing the Normal textview to replace it by our own # So don't try to change anything with glade, this is a home-made # widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req, self.clipboard) self.textview.show() self.textview.set_subtask_callback(self.new_subtask) self.textview.open_task_callback(self.vmanager.open_task) self.textview.set_left_margin(7) self.textview.set_right_margin(5) scrolled.add(self.textview) conf_font_value = self.browser_config.get("font_name") if conf_font_value != "": self.textview.modify_font(pango.FontDescription(conf_font_value)) # Voila! it's done self.calendar = GTGCalendar(self.builder) self.duedate_widget = self.builder.get_object("duedate_entry") self.startdate_widget = self.builder.get_object("startdate_entry") self.closeddate_widget = self.builder.get_object("closeddate_entry") self.dayleft_label = self.builder.get_object("dayleft") self.tasksidebar = self.builder.get_object("tasksidebar") # Define accelerator keys self.init_accelerators() self.task = task tags = task.get_tags() self.textview.subtasks_callback(task.get_children) self.textview.removesubtask_callback(task.remove_child) self.textview.set_get_tagslist_callback(task.get_tags_name) self.textview.set_add_tag_callback(task.add_tag) self.textview.set_remove_tag_callback(task.remove_tag) self.textview.save_task_callback(self.light_save) texte = self.task.get_text() title = self.task.get_title() # the first line is the title self.textview.set_text("%s\n" % title) # we insert the rest of the task if texte: self.textview.insert("%s" % texte) else: # If not text, we insert tags if tags: for t in tags: self.textview.insert_text("%s, " % t.get_name()) self.textview.insert_text("\n") # If we don't have text, we still need to insert subtasks if any subtasks = task.get_children() if subtasks: self.textview.insert_subtasks(subtasks) # We select the title if it's a new task if thisisnew: self.textview.select_title() else: self.task.set_to_keep() self.textview.modified(full=True) self.window.connect("destroy", self.destruction) self.calendar.connect("date-changed", self.on_date_changed) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.vmanager, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) # Putting the refresh callback at the end make the start a lot faster self.textview.refresh_callback(self.refresh_editor) self.refresh_editor() self.textview.grab_focus() # restoring size and position, spatial tasks if self.config: tid = self.task.get_id() if tid in self.config: if "position" in self.config[tid]: pos_x, pos_y = self.config[tid]["position"] self.move(int(pos_x), int(pos_y)) if "size" in self.config[tid]: width, height = self.config[tid]["size"] self.window.resize(int(width), int(height)) self.textview.set_editable(True) self.window.show() # Define accelerator-keys for this dialog # TODO: undo/redo def init_accelerators(self): agr = gtk.AccelGroup() self.window.add_accel_group(agr) # Escape and Ctrl-W close the dialog. It's faster to call close # directly, rather than use the close button widget key, modifier = gtk.accelerator_parse('Escape') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close) key, modifier = gtk.accelerator_parse('<Control>w') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close) # Ctrl-N creates a new task key, modifier = gtk.accelerator_parse('<Control>n') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.new_task) # Ctrl-Shift-N creates a new subtask insert_subtask = self.builder.get_object("insert_subtask") key, mod = gtk.accelerator_parse("<Control><Shift>n") insert_subtask.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) # Ctrl-D marks task as done mark_as_done_editor = self.builder.get_object('mark_as_done_editor') key, mod = gtk.accelerator_parse('<Control>d') mark_as_done_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) # Ctrl-I marks task as dismissed dismiss_editor = self.builder.get_object('dismiss_editor') key, mod = gtk.accelerator_parse('<Control>i') dismiss_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) # Can be called at any time to reflect the status of the Task # Refresh should never interfere with the TaskView. # If a title is passed as a parameter, it will become # the new window title. If not, we will look for the task title. # Refreshtext is whether or not we should refresh the TaskView #(doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window is None: return to_save = False # title of the window if title: self.window.set_title(title) to_save = True else: self.window.set_title(self.task.get_title()) status = self.task.get_status() dismiss_tooltip = GnomeConfig.MARK_UNDISMISS_TOOLTIP if status == Task.STA_DISMISSED: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_UNDISMISS) self.dismissbutton.set_tooltip_text(dismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-undismiss") elif status == Task.STA_DONE: self.donebutton.set_label(GnomeConfig.MARK_UNDONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_UNDONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-undone") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(dismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-dismiss") else: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(dismiss_tooltip) self.dismissbutton.set_icon_name("gtg-task-dismiss") self.donebutton.show() self.tasksidebar.show() # Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("label2").hide() self.builder.get_object("hbox1").hide() self.builder.get_object("label4").show() self.builder.get_object("hbox4").show() else: self.builder.get_object("label4").hide() self.builder.get_object("hbox4").hide() self.builder.get_object("label2").show() self.builder.get_object("hbox1").show() # refreshing the start date field startdate = self.task.get_start_date() try: prevdate = Date.parse(self.startdate_widget.get_text()) update_date = startdate != prevdate except ValueError: update_date = True if update_date: self.startdate_widget.set_text(str(startdate)) # refreshing the due date field duedate = self.task.get_due_date() try: prevdate = Date.parse(self.duedate_widget.get_text()) update_date = duedate != prevdate except ValueError: update_date = True if update_date: self.duedate_widget.set_text(str(duedate)) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = Date.parse(self.closeddate_widget.get_text()) if closeddate != prevcldate: self.closeddate_widget.set_text(str(closeddate)) # refreshing the day left label # If the task is marked as done, we display the delay between the # due date and the actual closing date. If the task isn't marked # as done, we display the number of days left. if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % \ {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % \ {'days': abs_delay} else: due_date = self.task.get_due_date() result = due_date.days_left() if due_date.is_fuzzy(): txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result) \ % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % { 'days': abs_result } window_style = self.window.get_style() color = str(window_style.text[gtk.STATE_INSENSITIVE]) self.dayleft_label.set_markup("<span color='" + color + "'>" + txt + "</span>") # Refreshing the tag list in the insert tag button taglist = self.req.get_used_tags() menu = gtk.Menu() tag_count = 0 for tagname in taglist: tag_object = self.req.get_tag(tagname) if not tag_object.is_special() and \ not self.task.has_tags(tag_list=[tagname]): tag_count += 1 mi = gtk.MenuItem(label=tagname, use_underline=False) mi.connect("activate", self.inserttag, tagname) mi.show() menu.append(mi) if tag_count > 0: self.inserttag_button.set_menu(menu) if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def date_changed(self, widget, data): try: Date.parse(widget.get_text()) valid = True except ValueError: valid = False if valid: # If the date is valid, we write with default color in the widget # "none" will set the default color. widget.modify_text(gtk.STATE_NORMAL, None) widget.modify_base(gtk.STATE_NORMAL, None) else: # We should write in red in the entry if the date is not valid widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F00")) widget.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F88")) def date_focus_out(self, widget, event, data): try: datetoset = Date.parse(widget.get_text()) except ValueError: datetoset = None if datetoset is not None: if data == "start": self.task.set_start_date(datetoset) elif data == "due": self.task.set_due_date(datetoset) elif data == "closed": self.task.set_closed_date(datetoset) self.refresh_editor() def on_date_pressed(self, widget, date_kind): """Called when a date-changing button is clicked.""" if date_kind == GTGCalendar.DATE_KIND_DUE: if not self.task.get_due_date(): date = self.task.get_start_date() else: date = self.task.get_due_date() elif date_kind == GTGCalendar.DATE_KIND_START: date = self.task.get_start_date() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: date = self.task.get_closed_date() self.calendar.set_date(date, date_kind) # we show the calendar at the right position rect = widget.get_allocation() x, y = widget.window.get_origin() self.calendar.show_at_position(x + rect.x + rect.width, y + rect.y) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def close_all_subtasks(self): all_subtasks = [] def trace_subtasks(root): for i in root.get_subtasks(): if i not in all_subtasks: all_subtasks.append(i) trace_subtasks(i) trace_subtasks(self.task) for task in all_subtasks: self.vmanager.close_task(task.get_id()) def dismiss(self, widget): stat = self.task.get_status() if stat == "Dismiss": self.task.set_status("Active") self.refresh_editor() else: self.task.set_status("Dismiss") self.close_all_subtasks() self.close(None) def change_status(self, widget): stat = self.task.get_status() if stat == "Done": self.task.set_status("Active") self.refresh_editor() else: self.task.set_status("Done") self.close_all_subtasks() self.close(None) def delete_task(self, widget): # this triggers the closing of the window in the view manager if self.task.is_new(): # self.req.delete_task(self.task.get_id()) self.vmanager.close_task(self.task.get_id()) else: self.vmanager.ask_delete_tasks([self.task.get_id()]) # Take the title as argument and return the subtask ID def new_subtask(self, title=None, tid=None): if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid # Create a new task def new_task(self, *args): task = self.req.new_task(newtask=True) task_id = task.get_id() self.vmanager.open_task(task_id) def insert_subtask(self, widget): self.textview.insert_newtask() self.textview.grab_focus() def inserttag_clicked(self, widget): itera = self.textview.get_insert() if itera.starts_line(): self.textview.insert_text("@", itera) else: self.textview.insert_text(" @", itera) self.textview.grab_focus() def inserttag(self, widget, tag): self.textview.insert_tags([tag]) self.textview.grab_focus() def save(self): self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config is not None: self.config.write() self.time = time.time() # light_save save the task without refreshing every 30seconds # We will reduce the time when the get_text will be in another thread def light_save(self): # if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: # we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() # This will bring the Task Editor to front def present(self): self.window.present() def move(self, x, y): try: xx = int(x) yy = int(y) self.window.move(xx, yy) except: pass def get_position(self): return self.window.get_position() def on_move(self, widget, event): # saving the position if self.config is not None: tid = self.task.get_id() if not tid in self.config: self.config[tid] = dict() self.config[tid]["position"] = self.get_position() self.config[tid]["size"] = self.window.get_size() # We define dummy variable for when close is called from a callback def close(self, window=None, a=None, b=None, c=None): # We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None # The destroy signal is linked to the "close" button. So if we call # destroy in the close function, this will cause the close to be called # twice # To solve that, close will just call "destroy" and the destroy signal # Will be linked to this destruction method that will save the task def destruction(self, a=None): # Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() for i in self.task.get_subtasks(): if i: i.set_to_keep() self.vmanager.close_task(tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window
class TaskEditor(): EDITOR_UI_FILE = os.path.join(UI_DIR, "task_editor.ui") def __init__(self, requester, app, task, thisisnew=False, clipboard=None): """ req is the requester app is the view manager thisisnew is True when a new task is created and opened """ self.req = requester self.app = app self.browser_config = self.req.get_config('browser') self.config = self.req.get_task_config(task.get_id()) self.time = None self.clipboard = clipboard self.builder = Gtk.Builder() self.builder.add_from_file(self.EDITOR_UI_FILE) self.donebutton = self.builder.get_object("mark_as_done") self.undonebutton = self.builder.get_object("mark_as_undone") self.dismissbutton = self.builder.get_object("dismiss") self.undismissbutton = self.builder.get_object("undismiss") self.add_subtask = self.builder.get_object("add_subtask") self.tag_store = self.builder.get_object("tag_store") self.parent_button = self.builder.get_object("parent") # Closed date self.closed_popover = self.builder.get_object("closed_popover") self.closed_entry = self.builder.get_object("closeddate_entry") self.closed_calendar = self.builder.get_object("calendar_closed") # Start date self.start_popover = self.builder.get_object("start_popover") self.start_entry = self.builder.get_object("startdate_entry") self.start_calendar = self.builder.get_object("calendar_start") # Due date self.due_popover = self.builder.get_object("due_popover") self.due_entry = self.builder.get_object("duedate_entry") self.due_calendar = self.builder.get_object("calendar_due") # Create our dictionary and connect it dic = { "on_tags_popover": self.open_tags_popover, "on_tag_toggled": self.on_tag_toggled, "on_move": self.on_move, "show_popover_start": self.show_popover_start, "startingdate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_START), "startdate_cleared": lambda w: self.on_date_cleared(w, GTGCalendar.DATE_KIND_START), "startdate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_START ), "show_popover_due": self.show_popover_due, "duedate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_DUE), "duedate_now_selected": lambda w: self.on_duedate_fuzzy(w, Date.now()), "duedate_soon_selected": lambda w: self.on_duedate_fuzzy(w, Date.soon()), "duedate_someday_selected": lambda w: self.on_duedate_fuzzy(w, Date.someday()), "duedate_cleared": lambda w: self.on_date_cleared(w, GTGCalendar.DATE_KIND_DUE), "duedate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_DUE), "show_popover_closed": self.show_popover_closed, "closeddate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_CLOSED), "closeddate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_CLOSED ), } self.builder.connect_signals(dic) self.window = self.builder.get_object("TaskEditor") self.window.set_application(app) # Connect signals for the calendar self.start_handle = self.start_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_START)) self.due_handle = self.due_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_DUE)) self.closed_handle = self.closed_calendar.connect( 'day-selected', lambda c: self.on_date_selected(c, GTGCalendar.DATE_KIND_CLOSED)) # Removing the Normal textview to replace it by our own # So don't try to change anything with glade, this is a home-made # widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req, self.clipboard) self.textview.show() self.textview.set_subtask_callback(self.new_subtask) self.textview.open_task_callback(self.app.open_task) self.textview.set_left_margin(20) self.textview.set_right_margin(20) scrolled.add(self.textview) conf_font_value = self.browser_config.get("font_name") if conf_font_value != "": self.textview.override_font(Pango.FontDescription(conf_font_value)) # Voila! it's done """ TODO(jakubbrindza): Once all the functionality in editor is back and working, bring back also the accelerators! Dayleft_label needs to be brought back, however its position is unsure. """ # self.dayleft_label = self.builder.get_object("dayleft") self.task = task tags = task.get_tags() self.textview.subtasks_callback(task.get_children) self.textview.removesubtask_callback(task.remove_child) self.textview.set_get_tagslist_callback(task.get_tags_name) self.textview.set_add_tag_callback(task.add_tag) self.textview.set_remove_tag_callback(task.remove_tag) self.textview.save_task_callback(self.light_save) texte = self.task.get_text() title = self.task.get_title() # the first line is the title self.textview.set_text(f"{title}\n") # we insert the rest of the task if texte: self.textview.insert(f"{texte}") else: # If not text, we insert tags if tags: for t in tags: self.textview.insert_text("%s, " % t.get_name()) self.textview.insert_text("\n") # If we don't have text, we still need to insert subtasks if any subtasks = task.get_children() if subtasks: self.textview.insert_subtasks(subtasks) # We select the title if it's a new task if thisisnew: self.textview.select_title() else: self.task.set_to_keep() self.textview.modified(full=True) self.window.connect("destroy", self.destruction) # Connect search field to tags popup self.tags_entry = self.builder.get_object("tags_entry") self.tags_tree = self.builder.get_object("tags_tree") self.tags_tree.set_search_entry(self.tags_entry) self.tags_tree.set_search_equal_func(self.search_function, None) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.app, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) # Putting the refresh callback at the end make the start a lot faster self.textview.refresh_callback(self.refresh_editor) self.refresh_editor() self.textview.grab_focus() self.init_dimensions() self.window.insert_action_group('app', app) self.window.insert_action_group('win', app.browser) self._set_actions() self.textview.set_editable(True) self.window.set_transient_for(self.app.browser) self.window.show() def _set_actions(self): """Setup actions.""" action_entries = [ ('editor.close', self.close, ('app.editor.close', ['Escape', '<ctrl>w'])), ('editor.show_parent', self.on_parent_select, None), ('editor.delete', self.delete_task, None), ('editor.open_tags_popup', self.open_tags_popover, None), ] for action, callback, accel in action_entries: simple_action = Gio.SimpleAction.new(action, None) simple_action.connect('activate', callback) simple_action.set_enabled(True) self.app.add_action(simple_action) if accel is not None: self.app.set_accels_for_action(*accel) def show_popover_start(self, widget, event): """Open the start date calendar popup.""" start_date = self.task.get_start_date() or Date.today() with signal_handler_block(self.start_calendar, self.start_handle): self.start_calendar.select_day(start_date.day) self.start_calendar.select_month(start_date.month - 1, start_date.year) self.start_popover.popup() def show_popover_due(self, widget, popover): """Open the due date calendar popup.""" due_date = self.task.get_due_date() if not due_date or due_date.is_fuzzy(): due_date = Date.today() with signal_handler_block(self.due_calendar, self.due_handle): self.due_calendar.select_day(due_date.day) self.due_calendar.select_month(due_date.month - 1, due_date.year) self.due_popover.popup() def show_popover_closed(self, widget, popover): """Open the closed date calendar popup.""" closed_date = self.task.get_closed_date() with signal_handler_block(self.closed_calendar, self.closed_handle): self.closed_calendar.select_day(closed_date.day) self.closed_calendar.select_month(closed_date.month - 1, closed_date.year) self.closed_popover.popup() def open_tags_popover(self, action, param): self.tag_store.clear() tags = self.req.get_tag_tree().get_all_nodes() used_tags = self.task.get_tags() for tagname in tags: tag = self.req.get_tag(tagname) if tag_filter(tag): is_used = tag in used_tags self.tag_store.append([is_used, tagname]) """ TODO(jakubbrindza): add sorting of the tags based on True | False and within each sub-group arrange them alphabetically """ def on_tag_toggled(self, widget, path, column): """We toggle by tag_row variable. tag_row is meant to be a tuple (is_used, tagname)""" tag_row = self.tag_store[path] tag_row[0] = not tag_row[0] if tag_row[0]: self.textview.insert_tags([tag_row[1]]) """ TODO(jakubbrindza): Add else case that will remove tag. """ def search_function(self, model, column, key, iter, *search_data): """Callback when searching in the tags popup.""" if not key.startswith('@'): key = f'@{key}' # The return value is reversed. False if it matches, True # otherwise. return not model.get(iter, column)[0].startswith(key) def init_dimensions(self): """ Restores position and size of task if possible """ position = self.config.get('position') if position and len(position) == 2: try: self.window.move(int(position[0]), int(position[1])) except ValueError: log.warning('Invalid position configuration for task %s: %s', self.task.get_id(), position) size = self.config.get('size') if size and len(size) == 2: try: self.window.resize(int(size[0]), int(size[1])) except ValueError: log.warning('Invalid size configuration for task %s: %s', self.task.get_id(), size) # Can be called at any time to reflect the status of the Task # Refresh should never interfere with the TaskView. # If a title is passed as a parameter, it will become # the new window title. If not, we will look for the task title. # Refreshtext is whether or not we should refresh the TaskView # (doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window is None: return to_save = False # title of the window if title: self.window.set_title(title) to_save = True else: self.window.set_title(self.task.get_title()) status = self.task.get_status() if status == Task.STA_DISMISSED: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.hide() self.undismissbutton.show() elif status == Task.STA_DONE: self.donebutton.hide() self.undonebutton.show() self.dismissbutton.show() self.undismissbutton.hide else: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.show() self.undismissbutton.hide() # Refreshing the the parent button has_parents = len(self.task.get_parents()) > 0 self.parent_button.set_sensitive(has_parents) # Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("start_box").hide() self.builder.get_object("closed_box").show() else: self.builder.get_object("closed_box").hide() self.builder.get_object("start_box").show() # refreshing the start date field startdate = self.task.get_start_date() try: prevdate = Date.parse(self.start_entry.get_text()) update_date = startdate != prevdate except ValueError: update_date = True if update_date: self.start_entry.set_text(str(startdate)) # refreshing the due date field duedate = self.task.get_due_date() try: prevdate = Date.parse(self.due_entry.get_text()) update_date = duedate != prevdate except ValueError: update_date = True if update_date: self.due_entry.set_text(str(duedate)) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = Date.parse(self.closed_entry.get_text()) if closeddate != prevcldate: self.closed_entry.set_text(str(closeddate)) # refreshing the day left label """ TODO(jakubbrindza): re-enable refreshing the day left. We need to come up how and where this information is viewed in the editor window. """ # self.refresh_day_left() if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def refresh_day_left(self): # If the task is marked as done, we display the delay between the # due date and the actual closing date. If the task isn't marked # as done, we display the number of days left. status = self.task.get_status() if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % \ {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % \ {'days': abs_delay} else: due_date = self.task.get_due_date() result = due_date.days_left() if due_date.is_fuzzy(): txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result)\ % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % { 'days': abs_result } style_context = self.window.get_style_context() color = style_context.get_color(Gtk.StateFlags.INSENSITIVE).to_color() self.dayleft_label.set_markup( f"<span color='{color.to_string()}'>{txt}</span>") def reload_editor(self): task = self.task textview = self.textview task_text = task.get_text() task_title = task.get_title() textview.set_text(f"{task_title}\n") if task_text: textview.insert(f"{task_text}") task.set_title(task_title) textview.modified(full=True) def date_changed(self, widget, data): try: Date.parse(widget.get_text()) valid = True except ValueError: valid = False if valid: # If the date is valid, we write with default color in the widget # "none" will set the default color. widget.override_color(Gtk.StateType.NORMAL, None) widget.override_background_color(Gtk.StateType.NORMAL, None) else: # We should write in red in the entry if the date is not valid text_color = Gdk.RGBA() text_color.parse("#F00") widget.override_color(Gtk.StateType.NORMAL, text_color) bg_color = Gdk.RGBA() bg_color.parse("#F88") widget.override_background_color(Gtk.StateType.NORMAL, bg_color) def date_focus_out(self, widget, event, date_kind): try: datetoset = Date.parse(widget.get_text()) except ValueError: datetoset = None if datetoset is not None: if date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(datetoset) self.start_popover.popdown() elif date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(datetoset) self.due_popover.popdown() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(datetoset) self.closed_popover.popdown() self.refresh_editor() def calendar_to_datetime(self, calendar): """ Gtk.Calendar uses a 0-based convention for counting months. The rest of the world, including the datetime module, starts from 1. This is a converter between the two. GTG follows the datetime convention. """ year, month, day = calendar.get_date() return datetime.date(year, month + 1, day) def on_duedate_fuzzy(self, widget, date): """ Callback when a fuzzy date is selected through the popup. """ self.task.set_due_date(date) self.due_entry.set_text(str(date)) def on_date_cleared(self, widget, kind): """ Callback when a date is cleared through the popups. """ if kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(Date.no_date()) self.start_entry.set_text('') elif kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(Date.no_date()) self.due_entry.set_text('') def on_date_selected(self, calendar, kind): """ Callback when a day is selected in the calendars.""" date = self.calendar_to_datetime(calendar) if kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(Date(date)) self.start_entry.set_text(str(Date(date))) elif kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(Date(date)) self.due_entry.set_text(str(Date(date))) elif kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(Date(date)) self.closed_entry.set_text(str(Date(date))) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def close_all_subtasks(self): all_subtasks = [] def trace_subtasks(root): for i in root.get_subtasks(): if i not in all_subtasks: all_subtasks.append(i) trace_subtasks(i) trace_subtasks(self.task) for task in all_subtasks: self.app.close_task(task.get_id()) def dismiss(self): stat = self.task.get_status() if stat == Task.STA_DISMISSED: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DISMISSED) self.close_all_subtasks() self.close(None) def change_status(self): stat = self.task.get_status() if stat == Task.STA_DONE: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DONE) self.close_all_subtasks() self.close(None) def delete_task(self, action, param): # this triggers the closing of the window in the view manager if self.task.is_new(): self.app.close_task(self.task.get_id(), self.window) else: self.app.ask_delete_tasks([self.task.get_id()], self.window) # Take the title as argument and return the subtask ID def new_subtask(self, title=None, tid=None): if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid def insert_subtask(self, action=None, param=None): self.textview.insert_newtask() self.textview.grab_focus() def inserttag_clicked(self, widget): itera = self.textview.get_insert() if itera.starts_line(): self.textview.insert_text("@", itera) else: self.textview.insert_text(" @", itera) def on_parent_select(self, action, param): parents = self.task.get_parents() if not parents: tags = [t.get_name() for t in self.task.get_tags()] parent = self.req.new_task(tags=tags, newtask=True) parent_id = parent.get_id() self.task.set_parent(parent_id) self.app.open_task(parent_id) elif len(parents) == 1: self.app.open_task(parents[0]) elif len(parents) > 1: self.show_multiple_parent_popover(parents) def show_multiple_parent_popover(self, parent_ids): parent_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) for parent in parent_ids: parent_name = self.req.get_task(parent).get_title() button = Gtk.ToolButton.new(None, parent_name) button.connect("clicked", self.on_parent_item_clicked, parent) parent_box.add(button) self.parent_popover = Gtk.Popover.new(self.parent_button) self.parent_popover.add(parent_box) self.parent_popover.set_property("border-width", 0) self.parent_popover.set_position(Gtk.PositionType.BOTTOM) self.parent_popover.set_transitions_enabled(True) self.parent_popover.show_all() # On click handler for open_parent_button's menu items def on_parent_item_clicked(self, widget, parent_id): self.app.open_task(parent_id) if self.parent_popover.get_visible(): self.parent_popover.hide() def save(self): self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config is not None: self.config.save() self.time = time.time() # light_save save the task without refreshing every 30seconds # We will reduce the time when the get_text will be in another thread def light_save(self): # if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: # we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() # This will bring the Task Editor to front def present(self): self.window.present() def get_position(self): return self.window.get_position() def on_move(self, widget, event): """ Save position and size of window """ self.config.set('position', list(self.window.get_position())) self.config.set('size', list(self.window.get_size())) # We define dummy variable for when close is called from a callback def close(self, action=None, param=None): # We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None # The destroy signal is linked to the "close" button. So if we call # destroy in the close function, this will cause the close to be called # twice # To solve that, close will just call "destroy" and the destroy signal # Will be linked to this destruction method that will save the task def destruction(self, a=None): # Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() for i in self.task.get_subtasks(): if i: i.set_to_keep() self.app.close_task(tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window
class TaskEditor(object): EDITOR_UI_FILE = os.path.join(UI_DIR, "taskeditor.ui") def __init__(self, requester, vmanager, task, thisisnew=False, clipboard=None): ''' req is the requester vmanager is the view manager thisisnew is True when a new task is created and opened ''' self.req = requester self.vmanager = vmanager self.browser_config = self.req.get_config('browser') self.config = self.req.get_task_config(task.get_id()) self.time = None self.clipboard = clipboard self.builder = Gtk.Builder() self.builder.add_from_file(self.EDITOR_UI_FILE) self.donebutton = self.builder.get_object("mark_as_done") self.undonebutton = self.builder.get_object("mark_as_undone") self.dismissbutton = self.builder.get_object("dismiss") self.undismissbutton = self.builder.get_object("undismiss") self.add_subtask = self.builder.get_object("add_subtask") self.tag_store = self.builder.get_object("tag_store") self.parent_button = self.builder.get_object("parent") # Create our dictionary and connect it dic = { "on_mark_as_done": self.change_status, "on_dismiss": self.dismiss, "delete_clicked": self.delete_task, "on_duedate_pressed": lambda w: self.on_date_pressed(w, GTGCalendar.DATE_KIND_DUE), "on_tags_popover": self.open_tags_popover, "on_startdate_pressed": lambda w: self.on_date_pressed(w, GTGCalendar.DATE_KIND_START), "on_closeddate_pressed": lambda w: self.on_date_pressed(w, GTGCalendar.DATE_KIND_CLOSED), "duedate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_DUE), "duedate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_DUE), "startingdate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_START), "startdate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_START ), "closeddate_changed": lambda w: self.date_changed(w, GTGCalendar.DATE_KIND_CLOSED), "closeddate_focus_out": lambda w, e: self.date_focus_out(w, e, GTGCalendar.DATE_KIND_CLOSED ), "on_insert_subtask_clicked": self.insert_subtask, "on_inserttag_clicked": self.inserttag_clicked, "on_parent_select": self.on_parent_select, "on_move": self.on_move, "show_popover_start": self.show_popover_start, "show_popover_due": self.show_popover_due, "show_popover_closed": self.show_popover_closed, "on_tag_toggled": self.on_tag_toggled, } self.builder.connect_signals(dic) self.window = self.builder.get_object("TaskEditor") # Removing the Normal textview to replace it by our own # So don't try to change anything with glade, this is a home-made # widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req, self.clipboard) self.textview.show() self.textview.set_subtask_callback(self.new_subtask) self.textview.open_task_callback(self.vmanager.open_task) self.textview.set_left_margin(7) self.textview.set_right_margin(5) scrolled.add(self.textview) conf_font_value = self.browser_config.get("font_name") if conf_font_value != "": self.textview.override_font(Pango.FontDescription(conf_font_value)) # Voila! it's done self.calendar = GTGCalendar() self.calendar.set_transient_for(self.window) self.calendar.set_decorated(False) self.duedate_widget = self.builder.get_object("duedate_entry") self.startdate_widget = self.builder.get_object("startdate_entry") self.closeddate_widget = self.builder.get_object("closeddate_entry") ''' TODO(jakubbrindza): Once all the functionality in editor is back and working, bring back also the accelerators! Dayleft_label needs to be brought back, however its position is unsure. ''' # self.dayleft_label = self.builder.get_object("dayleft") # Define accelerator keys self.init_accelerators() self.task = task tags = task.get_tags() self.textview.subtasks_callback(task.get_children) self.textview.removesubtask_callback(task.remove_child) self.textview.set_get_tagslist_callback(task.get_tags_name) self.textview.set_add_tag_callback(task.add_tag) self.textview.set_remove_tag_callback(task.remove_tag) self.textview.save_task_callback(self.light_save) texte = self.task.get_text() title = self.task.get_title() # the first line is the title self.textview.set_text("%s\n" % title) # we insert the rest of the task if texte: self.textview.insert("%s" % texte) else: # If not text, we insert tags if tags: for t in tags: self.textview.insert_text("%s, " % t.get_name()) self.textview.insert_text("\n") # If we don't have text, we still need to insert subtasks if any subtasks = task.get_children() if subtasks: self.textview.insert_subtasks(subtasks) # We select the title if it's a new task if thisisnew: self.textview.select_title() else: self.task.set_to_keep() self.textview.modified(full=True) self.window.connect("destroy", self.destruction) ''' TODO(jakubbrindza): make on_date_changed work alongside the new popover calendar ''' # self.calendar.connect("date-changed", self.on_date_changed) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.vmanager, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) # Putting the refresh callback at the end make the start a lot faster self.textview.refresh_callback(self.refresh_editor) self.refresh_editor() self.textview.grab_focus() self.init_dimensions() self.textview.set_editable(True) self.window.show() # Define accelerator-keys for this dialog ''' TODO: undo/redo + RE-enable all the features so that they work properly. + new shortcuts for bold and italic once implemented. ''' def init_accelerators(self): agr = Gtk.AccelGroup() self.window.add_accel_group(agr) # Escape and Ctrl-W close the dialog. It's faster to call close # directly, rather than use the close button widget key, modifier = Gtk.accelerator_parse('Escape') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.close) key, modifier = Gtk.accelerator_parse('<Control>w') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.close) # F1 shows help add_help_shortcut(self.window, "editor") # Ctrl-N creates a new task key, modifier = Gtk.accelerator_parse('<Control>n') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.new_task) # Ctrl-Shift-N creates a new subtask key, mod = Gtk.accelerator_parse("<Control><Shift>n") self.add_subtask.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-D marks task as done key, mod = Gtk.accelerator_parse('<Control>d') self.donebutton.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-I marks task as dismissed key, mod = Gtk.accelerator_parse('<Control>i') self.dismissbutton.add_accelerator('clicked', agr, key, mod, Gtk.AccelFlags.VISIBLE) # Ctrl-Q quits GTG key, modifier = Gtk.accelerator_parse('<Control>q') agr.connect(key, modifier, Gtk.AccelFlags.VISIBLE, self.quit) ''' TODO(jakubbrindza): Add the functionality to the existing calendar widgets. This will require ammending and re-factoring the entire calendar.py. ''' def show_popover_start(self, widget, event): popover = self.builder.get_object("date_popover") popover.set_relative_to(self.startdate_widget) popover.set_modal(False) popover.show_all() def show_popover_due(self, widget, popover): popover = self.builder.get_object("date_popover") popover.set_relative_to(self.duedate_widget) popover.set_modal(False) popover.show_all() def show_popover_closed(self, widget, popover): closed_popover = self.builder.get_object("closed_popover") closed_popover.set_relative_to(self.closeddate_widget) closed_popover.set_modal(False) closed_popover.show_all() def open_tags_popover(self, widget): self.tag_store.clear() tags = self.req.get_tag_tree().get_all_nodes() used_tags = self.task.get_tags() for tagname in tags: tag = self.req.get_tag(tagname) if tag_filter(tag): is_used = tag in used_tags self.tag_store.append([is_used, tagname]) ''' TODO(jakubbrindza): add sorting of the tags based on True | False and within each sub-group arrange them alphabetically ''' def on_tag_toggled(self, widget, path, column): """We toggle by tag_row variable. tag_row is meant to be a tuple (is_used, tagname)""" tag_row = self.tag_store[path] tag_row[0] = not tag_row[0] if tag_row[0]: self.textview.insert_tags([tag_row[1]]) ''' TODO(jakubbrindza): Add else case that will remove tag. ''' def init_dimensions(self): """ Restores position and size of task if possible """ position = self.config.get('position') if position and len(position) == 2: try: self.window.move(int(position[0]), int(position[1])) except ValueError: Log.warning('Invalid position configuration for task %s: %s', self.task.get_id(), position) size = self.config.get('size') if size and len(size) == 2: try: self.window.resize(int(size[0]), int(size[1])) except ValueError: Log.warning('Invalid size configuration for task %s: %s', self.task.get_id(), size) # Can be called at any time to reflect the status of the Task # Refresh should never interfere with the TaskView. # If a title is passed as a parameter, it will become # the new window title. If not, we will look for the task title. # Refreshtext is whether or not we should refresh the TaskView # (doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window is None: return to_save = False # title of the window if title: self.window.set_title(title) to_save = True else: self.window.set_title(self.task.get_title()) status = self.task.get_status() if status == Task.STA_DISMISSED: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.hide() self.undismissbutton.show() elif status == Task.STA_DONE: self.donebutton.hide() self.undonebutton.show() self.dismissbutton.show() self.undismissbutton.hide else: self.donebutton.show() self.undonebutton.hide() self.dismissbutton.show() self.undismissbutton.hide() # Refreshing the the parent button has_parents = len(self.task.get_parents()) > 0 self.parent_button.set_sensitive(has_parents) # Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("start_box").hide() self.builder.get_object("closed_box").show() else: self.builder.get_object("closed_box").hide() self.builder.get_object("start_box").show() # refreshing the start date field startdate = self.task.get_start_date() try: prevdate = Date.parse(self.startdate_widget.get_text()) update_date = startdate != prevdate except ValueError: update_date = True if update_date: self.startdate_widget.set_text(str(startdate)) # refreshing the due date field duedate = self.task.get_due_date() try: prevdate = Date.parse(self.duedate_widget.get_text()) update_date = duedate != prevdate except ValueError: update_date = True if update_date: self.duedate_widget.set_text(str(duedate)) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = Date.parse(self.closeddate_widget.get_text()) if closeddate != prevcldate: self.closeddate_widget.set_text(str(closeddate)) # refreshing the day left label ''' TODO(jakubbrindza): re-enable refreshing the day left. We need to come up how and where this information is viewed in the editor window. ''' # self.refresh_day_left() if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def refresh_day_left(self): # If the task is marked as done, we display the delay between the # due date and the actual closing date. If the task isn't marked # as done, we display the number of days left. status = self.task.get_status() if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % \ {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % \ {'days': abs_delay} else: due_date = self.task.get_due_date() result = due_date.days_left() if due_date.is_fuzzy(): txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result)\ % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % { 'days': abs_result } style_context = self.window.get_style_context() color = style_context.get_color(Gtk.StateFlags.INSENSITIVE).to_color() self.dayleft_label.set_markup("<span color='%s'>%s</span>" % (color.to_string(), txt)) def date_changed(self, widget, data): try: Date.parse(widget.get_text()) valid = True except ValueError: valid = False if valid: # If the date is valid, we write with default color in the widget # "none" will set the default color. widget.override_color(Gtk.StateType.NORMAL, None) widget.override_background_color(Gtk.StateType.NORMAL, None) else: # We should write in red in the entry if the date is not valid text_color = Gdk.RGBA() text_color.parse("#F00") widget.override_color(Gtk.StateType.NORMAL, text_color) bg_color = Gdk.RGBA() bg_color.parse("#F88") widget.override_background_color(Gtk.StateType.NORMAL, bg_color) def date_focus_out(self, widget, event, date_kind): try: datetoset = Date.parse(widget.get_text()) except ValueError: datetoset = None if datetoset is not None: if date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(datetoset) elif date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(datetoset) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(datetoset) self.refresh_editor() def on_date_pressed(self, widget, date_kind): """Called when a date-changing button is clicked.""" if date_kind == GTGCalendar.DATE_KIND_DUE: if not self.task.get_due_date(): date = self.task.get_start_date() else: date = self.task.get_due_date() elif date_kind == GTGCalendar.DATE_KIND_START: date = self.task.get_start_date() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: date = self.task.get_closed_date() self.calendar.set_date(date, date_kind) # we show the calendar at the right position rect = widget.get_allocation() result, x, y = widget.get_window().get_origin() self.calendar.show_at_position(x + rect.x + rect.width, y + rect.y) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def close_all_subtasks(self): all_subtasks = [] def trace_subtasks(root): for i in root.get_subtasks(): if i not in all_subtasks: all_subtasks.append(i) trace_subtasks(i) trace_subtasks(self.task) for task in all_subtasks: self.vmanager.close_task(task.get_id()) def dismiss(self, widget): stat = self.task.get_status() if stat == Task.STA_DISMISSED: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DISMISSED) self.close_all_subtasks() self.close(None) def change_status(self, widget): stat = self.task.get_status() if stat == Task.STA_DONE: self.task.set_status(Task.STA_ACTIVE) self.refresh_editor() else: self.task.set_status(Task.STA_DONE) self.close_all_subtasks() self.close(None) def delete_task(self, widget): # this triggers the closing of the window in the view manager if self.task.is_new(): self.vmanager.close_task(self.task.get_id()) else: self.vmanager.ask_delete_tasks([self.task.get_id()]) # Take the title as argument and return the subtask ID def new_subtask(self, title=None, tid=None): if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid # Create a new task def new_task(self, *args): task = self.req.new_task(newtask=True) task_id = task.get_id() self.vmanager.open_task(task_id) def insert_subtask(self, widget): self.textview.insert_newtask() self.textview.grab_focus() def inserttag_clicked(self, widget): itera = self.textview.get_insert() if itera.starts_line(): self.textview.insert_text("@", itera) else: self.textview.insert_text(" @", itera) def on_parent_select(self, widget): parents = self.task.get_parents() if len(parents) == 1: self.vmanager.open_task(parents[0]) elif len(parents) > 1: self.show_multiple_parent_popover(parents) def show_multiple_parent_popover(self, parent_ids): parent_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) for parent in parent_ids: parent_name = self.req.get_task(parent).get_title() button = Gtk.ToolButton.new(None, parent_name) button.connect("clicked", self.on_parent_item_clicked, parent) parent_box.add(button) self.parent_popover = Gtk.Popover.new(self.parent_button) self.parent_popover.add(parent_box) self.parent_popover.set_property("border-width", 0) self.parent_popover.set_position(Gtk.PositionType.BOTTOM) self.parent_popover.set_transitions_enabled(True) self.parent_popover.show_all() # On click handler for open_parent_button's menu items def on_parent_item_clicked(self, widget, parent_id): self.vmanager.open_task(parent_id) if self.parent_popover.get_visible(): self.parent_popover.hide() def save(self): self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config is not None: self.config.save() self.time = time.time() # light_save save the task without refreshing every 30seconds # We will reduce the time when the get_text will be in another thread def light_save(self): # if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: # we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() # This will bring the Task Editor to front def present(self): self.window.present() def get_position(self): return self.window.get_position() def on_move(self, widget, event): """ Save position and size of window """ self.config.set('position', self.window.get_position()) self.config.set('size', self.window.get_size()) # We define dummy variable for when close is called from a callback def close(self, window=None, a=None, b=None, c=None): # We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None # The destroy signal is linked to the "close" button. So if we call # destroy in the close function, this will cause the close to be called # twice # To solve that, close will just call "destroy" and the destroy signal # Will be linked to this destruction method that will save the task def destruction(self, a=None): # Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() for i in self.task.get_subtasks(): if i: i.set_to_keep() self.vmanager.close_task(tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window def quit(self, accel_group=None, acceleratable=None, keyval=None, modifier=None): """Handles the accelerator for quitting GTG.""" self.vmanager.quit()
class TaskEditor: def __init__(self, requester, vmanager, task, taskconfig = None, thisisnew = False, clipboard = None) : ''' req is the requester vmanager is the view manager taskconfig is a ConfigObj dic to save infos about tasks thisisnew is True when a new task is created and opened ''' self.req = requester self.vmanager = vmanager self.config = taskconfig self.time = None self.clipboard = clipboard self.builder = gtk.Builder() self.builder.add_from_file(GnomeConfig.GLADE_FILE) self.donebutton = self.builder.get_object("mark_as_done_editor") self.dismissbutton = self.builder.get_object("dismiss_editor") self.deletebutton = self.builder.get_object("delete_editor") self.deletebutton.set_tooltip_text(GnomeConfig.DELETE_TOOLTIP) self.subtask_button = self.builder.get_object("insert_subtask") self.subtask_button.set_tooltip_text(GnomeConfig.SUBTASK_TOOLTIP) self.inserttag_button = self.builder.get_object("inserttag") self.inserttag_button.set_tooltip_text(GnomeConfig.TAG_TOOLTIP) #Create our dictionary and connect it dic = { "mark_as_done_clicked" : self.change_status, "on_dismiss" : self.dismiss, "delete_clicked" : self.delete_task, "on_duedate_pressed" : (self.on_date_pressed, GTGCalendar.DATE_KIND_DUE), "on_startdate_pressed" : (self.on_date_pressed, GTGCalendar.DATE_KIND_START), "on_closeddate_pressed" : (self.on_date_pressed, GTGCalendar.DATE_KIND_CLOSED), "close_clicked" : self.close, "duedate_changed" : (self.date_changed, GTGCalendar.DATE_KIND_DUE), "startingdate_changed" : (self.date_changed, GTGCalendar.DATE_KIND_START), "closeddate_changed" : (self.date_changed, GTGCalendar.DATE_KIND_CLOSED), "on_insert_subtask_clicked" : self.insert_subtask, "on_inserttag_clicked" : self.inserttag_clicked, "on_move" : self.on_move, } self.builder.connect_signals(dic) self.window = self.builder.get_object("TaskEditor") #Removing the Normal textview to replace it by our own #So don't try to change anything with glade, this is a home-made widget textview = self.builder.get_object("textview") scrolled = self.builder.get_object("scrolledtask") scrolled.remove(textview) self.textview = TaskView(self.req,self.clipboard) self.textview.show() self.textview.set_subtask_callback(self.new_subtask) self.textview.open_task_callback(self.vmanager.open_task) self.textview.set_left_margin(7) self.textview.set_right_margin(5) scrolled.add(self.textview) #Voila! it's done self.calendar = GTGCalendar(self.builder) self.duedate_widget = self.builder.get_object("duedate_entry") self.startdate_widget = self.builder.get_object("startdate_entry") self.closeddate_widget = self.builder.get_object("closeddate_entry") self.dayleft_label = self.builder.get_object("dayleft") self.tasksidebar = self.builder.get_object("tasksidebar") # Define accelerator keys self.init_accelerators() self.task = task tags = task.get_tags() self.textview.subtasks_callback(task.get_children) self.textview.removesubtask_callback(task.remove_child) self.textview.set_get_tagslist_callback(task.get_tags_name) self.textview.set_add_tag_callback(task.add_tag) self.textview.set_remove_tag_callback(task.remove_tag) self.textview.save_task_callback(self.light_save) texte = self.task.get_text() title = self.task.get_title() #the first line is the title self.textview.set_text("%s\n"%title) #we insert the rest of the task if texte : self.textview.insert("%s"%texte) else : #If not text, we insert tags if tags : for t in tags : self.textview.insert_text("%s, "%t.get_name()) self.textview.insert_text("\n") #If we don't have text, we still need to insert subtasks if any subtasks = task.get_children() if subtasks : self.textview.insert_subtasks(subtasks) #We select the title if it's a new task if thisisnew : self.textview.select_title() else : self.task.set_to_keep() self.textview.modified(full=True) self.window.connect("destroy", self.destruction) self.calendar.connect("date-changed", self.on_date_changed) # plugins self.pengine = PluginEngine() self.plugin_api = PluginAPI(self.req, self.vmanager, self) self.pengine.register_api(self.plugin_api) self.pengine.onTaskLoad(self.plugin_api) #Putting the refresh callback at the end make the start a lot faster self.textview.refresh_callback(self.refresh_editor) self.refresh_editor() self.textview.grab_focus() #restoring size and position, spatial tasks if self.config : tid = self.task.get_id() if tid in self.config: if "position" in self.config[tid]: pos = self.config[tid]["position"] self.move(pos[0],pos[1]) #print "restoring position %s %s" %(pos[0],pos[1]) if "size" in self.config[tid]: size = self.config[tid]["size"] #print "size %s - %s" %(str(size[0]),str(size[1])) #this eval(str()) is a ugly (!) hack to accept both int and str #FIXME: Fix this! self.window.resize(eval(str(size[0])),eval(str(size[1]))) self.textview.set_editable(True) #Connection for the update self.req.connect('task-modified',self.task_modified) self.window.show() #FIXME: avoid to update to many time when we modify from the editor itself def task_modified(self,sender,tid): self.refresh_editor(refreshtext=True) # Define accelerator-keys for this dialog # TODO: undo/redo def init_accelerators(self): agr = gtk.AccelGroup() self.window.add_accel_group(agr) # Escape and Ctrl-W close the dialog. It's faster to call close # directly, rather than use the close button widget key, modifier = gtk.accelerator_parse('Escape') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close) key, modifier = gtk.accelerator_parse('<Control>w') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close) # Ctrl-N creates a new task key, modifier = gtk.accelerator_parse('<Control>n') agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.new_task) # Ctrl-Shift-N creates a new subtask insert_subtask = self.builder.get_object("insert_subtask") key, mod = gtk.accelerator_parse("<Control><Shift>n") insert_subtask.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) # Ctrl-D marks task as done mark_as_done_editor = self.builder.get_object('mark_as_done_editor') key, mod = gtk.accelerator_parse('<Control>d') mark_as_done_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) # Ctrl-I marks task as dismissed dismiss_editor = self.builder.get_object('dismiss_editor') key, mod = gtk.accelerator_parse('<Control>i') dismiss_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE) #Can be called at any time to reflect the status of the Task #Refresh should never interfere with the TaskView. #If a title is passed as a parameter, it will become #the new window title. If not, we will look for the task title. #Refreshtext is whether or not we should refresh the TaskView #(doing it all the time is dangerous if the task is empty) def refresh_editor(self, title=None, refreshtext=False): if self.window == None: return to_save = False #title of the window if title : self.window.set_title(title) to_save = True else : self.window.set_title(self.task.get_title()) status = self.task.get_status() if status == Task.STA_DISMISSED: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_UNDISMISS) self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_UNDISMISS_TOOLTIP) self.dismissbutton.set_icon_name("gtg-task-undismiss") elif status == Task.STA_DONE: self.donebutton.set_label(GnomeConfig.MARK_UNDONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_UNDONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-undone") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_DISMISS_TOOLTIP) self.dismissbutton.set_icon_name("gtg-task-dismiss") else: self.donebutton.set_label(GnomeConfig.MARK_DONE) self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP) self.donebutton.set_icon_name("gtg-task-done") self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS) self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_DISMISS_TOOLTIP) self.dismissbutton.set_icon_name("gtg-task-dismiss") self.donebutton.show() self.tasksidebar.show() #Refreshing the status bar labels and date boxes if status in [Task.STA_DISMISSED, Task.STA_DONE]: self.builder.get_object("label2").hide() self.builder.get_object("hbox1").hide() self.builder.get_object("label4").show() self.builder.get_object("hbox4").show() else: self.builder.get_object("label4").hide() self.builder.get_object("hbox4").hide() self.builder.get_object("label2").show() self.builder.get_object("hbox1").show() #refreshing the due date field duedate = self.task.get_due_date() prevdate = dates.strtodate(self.duedate_widget.get_text()) if duedate != prevdate or type(duedate) is not type(prevdate): zedate = str(duedate).replace("-", date_separator) self.duedate_widget.set_text(zedate) # refreshing the closed date field closeddate = self.task.get_closed_date() prevcldate = dates.strtodate(self.closeddate_widget.get_text()) if closeddate != prevcldate or type(closeddate) is not type(prevcldate): zecldate = str(closeddate).replace("-", date_separator) self.closeddate_widget.set_text(zecldate) #refreshing the day left label #If the task is marked as done, we display the delay between the #due date and the actual closing date. If the task isn't marked #as done, we display the number of days left. if status in [Task.STA_DISMISSED, Task.STA_DONE]: delay = self.task.get_days_late() if delay is None: txt = "" elif delay == 0: txt = "Completed on time" elif delay >= 1: txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % {'days': delay} elif delay <= -1: abs_delay = abs(delay) txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % {'days': abs_delay} else: result = self.task.get_days_left() if result is None: txt = "" elif result > 0: txt = ngettext("Due tomorrow!", "%(days)d days left", result) % {'days': result} elif result == 0: txt = _("Due today!") elif result < 0: abs_result = abs(result) txt = ngettext("Due yesterday!", "Was %(days)d days ago", abs_result) % {'days': abs_result} window_style = self.window.get_style() color = str(window_style.text[gtk.STATE_INSENSITIVE]) self.dayleft_label.set_markup("<span color='"+color+"'>"+txt+"</span>") startdate = self.task.get_start_date() prevdate = dates.strtodate(self.startdate_widget.get_text()) if startdate != prevdate or type(startdate) is not type(prevdate): zedate = str(startdate).replace("-",date_separator) self.startdate_widget.set_text(zedate) #Refreshing the tag list in the insert tag button taglist = self.req.get_used_tags() menu = gtk.Menu() tag_count = 0 for tagname in taglist: tag_object = self.req.get_tag(tagname) if not tag_object.is_special() and \ not self.task.has_tags(tag_list=[tagname]): tag_count += 1 mi = gtk.MenuItem(label = tagname, use_underline=False) mi.connect("activate", self.inserttag, tagname) mi.show() menu.append(mi) if tag_count > 0 : self.inserttag_button.set_menu(menu) if refreshtext: self.textview.modified(refresheditor=False) if to_save: self.light_save() def date_changed(self,widget,data): text = widget.get_text() validdate = False if not text : validdate = True datetoset = dates.no_date else : datetoset = dates.strtodate(text) if datetoset : validdate = True if validdate : #If the date is valid, we write with default color in the widget # "none" will set the default color. widget.modify_text(gtk.STATE_NORMAL, None) widget.modify_base(gtk.STATE_NORMAL, None) if data == "start" : self.task.set_start_date(datetoset) elif data == "due" : self.task.set_due_date(datetoset) elif data == "closed" : self.task.set_closed_date(datetoset) #Set the due date to be equal to the start date # when it happens that the start date is later than the due date start_date = self.task.get_start_date() due_date = self.task.get_due_date() if start_date and (start_date > due_date): self.task.set_due_date(self.task.get_start_date()) else : #We should write in red in the entry if the date is not valid widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F00")) widget.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F88")) def on_date_pressed(self, widget, date_kind): """Called when a date-changing button is clicked.""" if date_kind == GTGCalendar.DATE_KIND_DUE: #we display a calendar open on a day: # the due date, the start date (if due is not set), or today # (which is the default of the GTGCalendar class) date = self.task.get_due_date() if not date or self.task.get_start_date() > date: date = self.task.get_start_date() elif date_kind == GTGCalendar.DATE_KIND_START: date = self.task.get_start_date() elif date_kind == GTGCalendar.DATE_KIND_CLOSED: date = self.task.get_closed_date() self.calendar.set_date(date, date_kind) #we show the calendar at the right position rect = widget.get_allocation() x, y = widget.window.get_origin() self.calendar.show_at_position(x + rect.x + rect.width, y + rect.y) def on_date_changed(self, calendar): date, date_kind = calendar.get_selected_date() if date_kind == GTGCalendar.DATE_KIND_DUE: self.task.set_due_date(date) elif date_kind == GTGCalendar.DATE_KIND_START: self.task.set_start_date(date) elif date_kind == GTGCalendar.DATE_KIND_CLOSED: self.task.set_closed_date(date) self.refresh_editor() def dismiss(self,widget) : #pylint: disable-msg=W0613 stat = self.task.get_status() if stat == "Dismiss": self.task.set_status("Active") self.refresh_editor() else: self.task.set_status("Dismiss") self.close(None) def change_status(self,widget) : #pylint: disable-msg=W0613 stat = self.task.get_status() if stat == "Done": self.task.set_status("Active") self.refresh_editor() else: self.task.set_status("Done") self.close(None) def delete_task(self, widget) : #this triggers the closing of the window in the view manager self.vmanager.ask_delete_tasks([self.task.get_id()]) #Take the title as argument and return the subtask ID def new_subtask(self,title=None,tid=None) : if tid: self.task.add_child(tid) elif title: subt = self.task.new_subtask() subt.set_title(title) tid = subt.get_id() return tid # Create a new task def new_task(self, *args): task = self.req.new_task(newtask=True) task_id = task.get_id() self.vmanager.open_task(task_id) def insert_subtask(self,widget) : #pylint: disable-msg=W0613 self.textview.insert_newtask() self.textview.grab_focus() def inserttag_clicked(self,widget) : #pylint: disable-msg=W0613 itera = self.textview.get_insert() if itera.starts_line() : self.textview.insert_text("@",itera) else : self.textview.insert_text(" @",itera) self.textview.grab_focus() def inserttag(self,widget,tag) : #pylint: disable-msg=W0613 self.textview.insert_tags([tag]) self.textview.grab_focus() def save(self) : self.task.set_title(self.textview.get_title()) self.task.set_text(self.textview.get_text()) self.task.sync() if self.config != None: self.config.write() self.time = time.time() #light_save save the task without refreshing every 30seconds #We will reduce the time when the get_text will be in another thread def light_save(self) : #if self.time is none, we never called any save if self.time: diff = time.time() - self.time tosave = diff > GnomeConfig.SAVETIME else: #we don't want to save a task while opening it tosave = self.textview.get_editable() diff = None if tosave: self.save() #This will bring the Task Editor to front def present(self): self.window.present() def move(self,x,y): try: xx=int(x) yy=int(y) self.window.move(xx,yy) except: pass def get_position(self): return self.window.get_position() def on_move(self,widget,event): #saving the position if self.config != None: tid = self.task.get_id() if not tid in self.config : self.config[tid] = dict() #print "saving task position %s" %str(self.get_position()) self.config[tid]["position"] = self.get_position() self.config[tid]["size"] = self.window.get_size() #We define dummy variable for when close is called from a callback def close(self,window=None,a=None,b=None,c=None): #pylint: disable-msg=W0613 #We should also destroy the whole taskeditor object. if self.window: self.window.destroy() self.window = None #The destroy signal is linked to the "close" button. So if we call #destroy in the close function, this will cause the close to be called twice #To solve that, close will just call "destroy" and the destroy signal #Will be linked to this destruction method that will save the task def destruction(self,a=None): #Save should be also called when buffer is modified self.pengine.onTaskClose(self.plugin_api) self.pengine.remove_api(self.plugin_api) tid = self.task.get_id() if self.task.is_new(): self.req.delete_task(tid) else: self.save() for i in self.task.get_subtasks(): if i: i.set_to_keep() self.vmanager.close_task(tid) def get_builder(self): return self.builder def get_task(self): return self.task def get_textview(self): return self.textview def get_window(self): return self.window