Example #1
0
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()
Example #2
0
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
Example #3
0
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
Example #4
0
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
Example #5
0
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()
Example #6
0
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