Example #1
0
class WeekView(ViewBase, Gtk.VBox):
    __string_signal__ = (GObject.SignalFlags.RUN_FIRST, None, (str, ))
    __2string_signal__ = (GObject.SignalFlags.RUN_FIRST, None, (str, str,))
    __none_signal__ = (GObject.SignalFlags.RUN_FIRST, None, tuple())
    __gsignals__ = {'on_edit_task': __string_signal__,
                    'on_add_task': __2string_signal__,
                    'dates-changed': __none_signal__,
                    }

    def __init__(self, parent, requester, numdays=7):
        super(WeekView, self).__init__(parent, requester)
        super(Gtk.VBox, self).__init__()

        self.numdays = numdays
        self.min_day_width = 60
        self.grid = Grid(1, self.numdays)
        numweeks = int(self.numdays/7)
        self.week = WeekSpan(numweeks)

        # Header
        self.header = Header(self.numdays)
        self.header.set_size_request(-1, 35)
        self.pack_start(self.header, False, False, 0)

        # Scrolled Window
        self.scroll = Gtk.ScrolledWindow(None, None)
        self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        self.scroll.add_events(Gdk.EventMask.SCROLL_MASK)
        self.scroll.connect("scroll-event", self.on_scroll)
        self.pack_start(self.scroll, True, True, 0)
        self.vadjustment = self.scroll.get_vadjustment()
        self.vadjustment.connect('changed', self.on_vadjustment_changed)

        # AllDayTasks widget
        self.all_day_tasks = AllDayTasks(self, cols=self.numdays)
        self.scroll.add_with_viewport(self.all_day_tasks)

        # drag-and-drop support
        self.drag_offset = None
        self.drag_action = None
        self.is_dragging = False

        # handle the AllDayTasks DnD events
        self.all_day_tasks.connect("button-press-event", self.dnd_start)
        self.all_day_tasks.connect("motion-notify-event", self.motion_notify)
        self.all_day_tasks.connect("button-release-event", self.dnd_stop)

        self.connect("size-allocate", self.on_size_allocate)

    def on_scroll(self, widget, event):
        """
        Callback function to deal with scrolling the drawing area window.
        If scroll right or left, change the days displayed in the calendar
        view. If scroll up or down, propagates the signal to scroll window
        normally.
        """
        # scroll right
        if event.get_scroll_deltas()[1] > 0:
            self.next(days=1)
        # scroll left
        elif event.get_scroll_deltas()[1] < 0:
            self.previous(days=1)
        # scroll up or down
        else:
            return False  # propagates signal to scroll window normally
        return True

    def unselect_task(self):
        """ Unselects the task that was selected before. """
        self.selected_task = None
        self.all_day_tasks.selected_task = None

    def first_day(self):
        """ Returns the first day of the view being displayed """
        return self.week.start_date

    def last_day(self):
        """ Returns the last day of the view being displayed """
        return self.week.end_date

    def get_day_width(self):
        """ Returns the day/column width in pixels """
        return round(self.all_day_tasks.get_day_width(), 3)

    def show_today(self):
        """
        Shows the range of dates in the current view with the date
        corresponding to today among it.
        """
        self.week.week_containing_day(datetime.date.today())
        self.update()

    def on_size_allocate(self, widget=None, event=None):
        """ Calculates new day_width when window is resized """
        pass

    def on_vadjustment_changed(self, a):
        """ Verify if the scrollbar is needed, and notifies header of that """
        if (self.vadjustment.get_page_size() == self.vadjustment.get_upper()):
            self.header.set_sidebar_size(0)
        else:
            self.header.set_sidebar_size(15)

    def compute_size(self):
        """ Computes and requests the size needed to draw everything. """
        width = self.min_day_width * self.numdays
        height = TASK_HEIGHT * self.grid.num_rows
        self.all_day_tasks.set_size_request(width, height)

    def set_week_from(self, start):
        """
        Sets the week to be shown, starting on @start.

        @param start: must be a datetime object, first day to be shown.
        """
        self.week.set_week_starting_on(start)

    def update_header(self, format="%a %m/%d"):
        """
        Updates the header label of the days to be drawn given a specific
        strftime @format, and then redraws the header. If more than one line is
        wanted to display each labels, the format must separate the content
        inteded for each line by a space.

        @param format: string, must follow the strftime convention.
         Default: "%a %m/%d" - abbrev weekday in first line,
         month/day_of_month as decimal numbers in second line.
        """
        days = self.week.label(format)
        days = [d.split() for d in days]
        self.header.set_labels(days)
        self.header.queue_draw()
        self.emit('dates-changed')

    def set_task_drawing_position(self, dtask):
        """
        Calculates and sets the position of a @dtask.

        @param dtask: a DrawingTask object.
        """
        task = self.req.get_task(dtask.get_id())

        start = max(task.get_start_date().date(), self.first_day())
        end = min(task.get_due_date().date(), self.last_day())
        duration = (end - start).days + 1

        x = utils.date_to_col_coord(start, self.first_day())
        w = duration
        x, y, w, h = self.grid.add_to_grid(x, w, id=dtask.get_label()[4])

        dtask.set_position(x, y, w, h)
        dtask.set_overflowing_L(self.first_day())
        dtask.set_overflowing_R(self.last_day())

    def update_tasks(self):
        """ Updates and redraws everything related to the tasks """
        self.update_drawtasks()
        self.compute_size()
        self.all_day_tasks.queue_draw()

    def update_drawtasks(self, tasks=None):
        """
        Updates the drawtasks and calculates the position of where each one of
        them should be drawn.

        @param tasks: a Task list, containing the tasks to be drawn.
         If none is given, the tasks will be retrieved from the requester.
        """
        if not tasks:
            tasks = [self.req.get_task(t) for t in self.req.get_tasks_tree()]
        self.tasks = [DrawTask(t) for t in tasks if self.is_in_days_range(t)]

        self.grid.clear_rows()
        for t in self.tasks:
            self.set_task_drawing_position(t)
        self.all_day_tasks.set_tasks_to_draw(self.tasks)

        # clears selected_task if it is not being showed
        if self.selected_task:
            task = self.req.get_task(self.get_selected_task)
            if task and not self.is_in_days_range(task):
                self.unselect_task()
        self.all_day_tasks.selected_task = self.selected_task

    def highlight_today_cell(self):
        """ Highlights the cell equivalent to today."""
        row = 0
        col = utils.date_to_col_coord(datetime.date.today(), self.first_day())
        self.all_day_tasks.set_today_cell(row, col)
        self.header.set_highlight_cell(0, col)

    def update(self):
        """
        Updates the header, the content to be drawn (tasks), recalculates the
        size needed and then redraws everything.
        """
        self.update_drawtasks()
        self.compute_size()
        self.highlight_today_cell()
        self.update_header()
        self.all_day_tasks.queue_draw()

    def next(self, days=None):
        """
        Advances the dates being displayed by a given number of @days.
        If none is given, the default self.numdays will be used. In this case,
        if the actual first_day being shown is not at the beginning of a
        week, it will advance to the beginning of the next one instead
        of advancing @numdays.

        @param days: integer, the number of days to advance.
         If none is given, the default self.numdays will be used.
        """
        if not days:
            days = self.numdays - self.first_day().weekday()
        self.week.adjust(days)
        self.update()

    def previous(self, days=None):
        """
        Regresses the dates being displayed by a given number of @days.
        If none is given, the default self.numdays will be used. In this case,
        if the actual first_day being shown is not at the beginning of a
        week, it will go back to the beginning of it instead
        of going back @numdays.

        @param days: integer, the number of days to go back.
         If none is given, the default self.numdays will be used.
        """
        if not days:
            days = self.first_day().weekday() or self.numdays
        self.week.adjust(-days)
        self.update()

    def dnd_start(self, widget, event):
        """ User clicked the mouse button, starting drag and drop """
        # find which task was clicked, if any
        self.selected_task, self.drag_action, cursor = \
            self.all_day_tasks.identify_pointed_object(event, clicked=True)

        if self.selected_task:
            # double-click opens task to edit
            if event.type == Gdk.EventType._2BUTTON_PRESS:
                GObject.idle_add(self.emit, 'on_edit_task',
                                 self.selected_task)
                self.is_dragging = False
                self.drag_offset = None
                return
            widget.get_window().set_cursor(cursor)
            task = self.req.get_task(self.selected_task)
            start = (task.get_start_date().date() - self.first_day()).days
            end = (task.get_due_date().date() - self.first_day()).days + 1
            duration = end - start

            day_width = self.get_day_width()
            offset = (start * day_width) - event.x
            # offset_y = pos * TASK_HEIGHT - event.y
            if self.drag_action == "expand_right":
                offset += duration * day_width
            self.drag_offset = offset

            self.update_tasks()
        # if no task is selected, save mouse location in case the user wants
        # to create a new task using DnD
        else:
            self.drag_offset = event.x

    def motion_notify(self, widget, event):
        """ User moved mouse over widget """
        # dragging with no task selected: new task will be created
        if not self.selected_task and self.drag_offset:
            self.is_dragging = True
            day_width = self.get_day_width()
            curr_col = utils.convert_coordinates_to_col(event.x, day_width)
            start_col = utils.convert_coordinates_to_col(self.drag_offset,
                                                         day_width)
            if curr_col < start_col:
                temp = curr_col
                curr_col = start_col
                start_col = temp
            cells = []
            for i in range(curr_col - start_col + 1):
                row = 0
                col = start_col + i
                cells.append((row, col))
            self.all_day_tasks.cells = cells
            self.all_day_tasks.queue_draw()
            return

        if self.selected_task and self.drag_offset:  # a task was clicked
            self.is_dragging = True
            task = self.req.get_task(self.selected_task)
            start_date = task.get_start_date().date()
            end_date = task.get_due_date().date()
            duration = (end_date - start_date).days

            event_x = round(event.x + self.drag_offset, 3)
            # event_y = event.y

            day_width = self.get_day_width()
            weekday = utils.convert_coordinates_to_col(event_x, day_width)
            day = self.first_day() + datetime.timedelta(weekday)

            if self.drag_action == "expand_left":
                diff = start_date - day
                new_start_day = start_date - diff
                if new_start_day <= end_date:
                    task.set_start_date(new_start_day)
                pass

            elif self.drag_action == "expand_right":
                diff = end_date - day
                new_due_day = end_date - diff
                if new_due_day >= start_date:
                    task.set_due_date(new_due_day)
                pass

            else:
                new_start_day = self.first_day() + \
                    datetime.timedelta(days=weekday)
                new_due_day = new_start_day + datetime.timedelta(days=duration)
                task.set_start_date(new_start_day)
                task.set_due_date(new_due_day)

            self.update()

        else:  # mouse hover
            t_id, self.drag_action, cursor = \
                self.all_day_tasks.identify_pointed_object(event)
            widget.get_window().set_cursor(cursor)

    def dnd_stop(self, widget, event):
        """
        User released a button, stopping drag and drop.
        Selected task, if any, will still have the focus.
        """
        # dragging with no task selected: new task will be created
        if not self.selected_task and self.is_dragging:
            day_width = self.get_day_width()
            start = utils.convert_coordinates_to_col(self.drag_offset,
                                                     day_width)

            event_x = round(event.x, 3)
            end = utils.convert_coordinates_to_col(event_x, day_width)
            if start > end:
                temp = start
                start = end
                end = temp
            start_date = self.first_day() + datetime.timedelta(days=start)
            due_date = self.first_day() + datetime.timedelta(days=end)

            GObject.idle_add(self.emit, 'on_add_task', start_date, due_date)
            self.all_day_tasks.queue_draw()
            self.all_day_tasks.cells = []

        # user didn't click on a task - redraw to 'unselect' task
        elif not self.selected_task:
            self.unselect_task()
            self.all_day_tasks.queue_draw()

        # only changes selected task if any form of dragging ocurred
        elif self.is_dragging:
            event_x = round(event.x + self.drag_offset, 3)
            # event_y = event.y

            day_width = self.get_day_width()
            weekday = utils.convert_coordinates_to_col(event_x, day_width)

            task = self.req.get_task(self.selected_task)
            start = task.get_start_date().date()
            end = task.get_due_date().date()
            duration = (end - start).days

            new_start_day = self.first_day() + datetime.timedelta(days=weekday)
            if self.drag_action == "expand_right":
                new_start_day = task.get_start_date().date()
            new_due_day = new_start_day + datetime.timedelta(days=duration)

            if not self.drag_action == "expand_right" \
               and new_start_day <= end:
                task.set_start_date(new_start_day)
            if not self.drag_action == "expand_left" \
               and new_due_day >= start:
                task.set_due_date(new_due_day)
            self.unselect_task()
            self.update_tasks()

        widget.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
        self.drag_offset = None
        self.is_dragging = False
class MonthView(ViewBase, Gtk.VBox):
    __string_signal__ = (GObject.SignalFlags.RUN_FIRST, None, (str, ))
    __2string_signal__ = (GObject.SignalFlags.RUN_FIRST, None, (str, str,))
    __none_signal__ = (GObject.SignalFlags.RUN_FIRST, None, tuple())
    __gsignals__ = {'on_edit_task': __string_signal__,
                    'on_add_task': __2string_signal__,
                    'dates-changed': __none_signal__,
                    }

    def __init__(self, parent, requester, numdays=7):
        super(MonthView, self).__init__(parent, requester)
        super(Gtk.VBox, self).__init__()

        self.numdays = numdays
        self.min_day_width = 60
        self.min_week_height = 80
        self.font_size = 7
        self.fixed = None

        # Header
        self.header = Header(self.numdays)
        self.header.set_size_request(-1, 35)
        self.pack_start(self.header, False, False, 0)

        # Scrolled Window
        self.scroll = Gtk.ScrolledWindow(None, None)
        self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
        self.scroll.add_events(Gdk.EventMask.SCROLL_MASK)
        self.scroll.connect("scroll-event", self.on_scroll)
        self.pack_start(self.scroll, True, True, 0)

        # AllDayTasks widget
        self.all_day_tasks = AllDayTasks(self, cols=self.numdays)
        # self.pack_start(self.all_day_tasks, True, True, 0)
        self.scroll.add_with_viewport(self.all_day_tasks)

        # drag-and-drop support
        self.drag_offset = None
        self.drag_action = None
        self.is_dragging = False

        # handle the AllDayTasks DnD events
        self.all_day_tasks.connect("button-press-event", self.dnd_start)
        self.all_day_tasks.connect("motion-notify-event", self.motion_notify)
        self.all_day_tasks.connect("button-release-event", self.dnd_stop)

    def init_weeks(self, numweeks):
        """
        Initializates the structure needed to manage dates, tasks and task
        positions for each one of the @numweeks weeks of the month.
        Structure self.weeks is a list of size @numweeks, where each position
        manages the dates corresponding to a week, being actually a dictionary
        with entries:
            'grid': contains Grid object.
            'dates': contains WeekSpan object.
            'tasks': is an empty list, will keep track of list of DrawTask.

        @param numweeks: integer, the number of weeks
        """
        self.weeks = []
        for w in range(numweeks):
            week = {}
            week['grid'] = Grid(1, self.numdays)
            week['dates'] = WeekSpan()
            week['tasks'] = []
            self.weeks.append(week)
        self.all_day_tasks.set_num_rows(numweeks)

    def on_scroll(self, widget, event):
        """
        Callback function to deal with scrolling the drawing area window.
        If scroll right or left, change the days displayed in the calendar
        view.
        """
        # scroll right
        if event.get_scroll_deltas()[1] > 0:
            self.next(months=1)
        # scroll left
        elif event.get_scroll_deltas()[1] < 0:
            self.previous(months=1)
        return True

    def unselect_task(self):
        """ Unselects the task that was selected before. """
        self.selected_task = None
        self.all_day_tasks.selected_task = None

    def first_day(self):
        """ Returns the first day of the view being displayed """
        return self.weeks[0]['dates'].start_date

    def last_day(self):
        """ Returns the last day of the view being displayed """
        return self.weeks[-1]['dates'].end_date

    def get_day_width(self):
        """ Returns the day/column width in pixels """
        return self.all_day_tasks.get_day_width()

    def get_week_height(self):
        """ Returns the week/row height in pixels """
        return self.all_day_tasks.get_week_height()

    def get_task_height(self):
        return TASK_HEIGHT  # self.all_day_tasks.task_height

    def show_today(self):
        """
        Shows the range of dates in the current view with the date
        corresponding to today among it.
        """
        today = datetime.date.today()
        self.update_weeks(today.year, today.month)
        self.update()

    def total_rows_needed_in_calendar_cell(self, row, col):
        """
        Gets the total number of rows needed to display the content in a
        specific calendar cell given by @row and @col.

        @param row: integer, the row index of the cell, corresponding to the
                    week we are looking at.
        @param col: integer, the col index of the cell.
        @return: integer, the total number of rows needed in order to display
                 all the content in this specific cell.
        """
        grid = self.weeks[row]['grid']  # grid correspondent to this week/row
        last_row_index = grid.last_occupied_row_in_col(col)
        return last_row_index + 1

    def compute_size(self):
        """ Computes and requests the size needed to draw everything. """
        width = self.min_day_width * self.numdays
        height = self.min_week_height * self.numweeks
        self.all_day_tasks.set_size_request(width, height)

    def calculate_number_of_weeks(self, year, month):
        """
        Calculates the number of weeks the given @month of a @year has.

        @param year: integer, a valid year in the format YYYY.
        @param month: integer, a month (should be between 1 and 12)
        """
        num_days_in_month = calendar.monthrange(year, month)[1]
        first_day = datetime.date(year, month, 1)
        last_day = datetime.date(year, month, num_days_in_month)
        total_weeks = utils.date_to_row_coord(last_day, first_day) + 1
        return total_weeks

    def update_weeks(self, year, month):
        """
        Updates the dates of the weeks of a a specific @month of a @year.
        This will erase the whole self.weeks structure, and then fill the entry
        week['dates'] of each week with the right dates.

        @param year: integer, a valid year in the format YYYY.
        @param month: integer, a month (should be between 1 and 12)
        """
        self.year = year
        self.month = month
        self.numweeks = self.calculate_number_of_weeks(year, month)
        self.init_weeks(self.numweeks)
        first_day = datetime.date(year, month, 1)
        for i, week in enumerate(self.weeks):
            new_week = WeekSpan()
            day = first_day + datetime.timedelta(days=i*7)
            new_week.week_containing_day(day)
            week['dates'] = new_week

    def update_header(self, format="%A"):
        """
        Updates the header label of the days to be drawn given a specific
        strftime @format, and then redraws the header. If more than one line is
        wanted to display each labels, the format must separate the content
        inteded for each line by a space.

        @param format: string, must follow the strftime convention.
         Default: "%A" - weekday as locale's full name.
        """
        days = self.weeks[0]['dates'].label(format)
        days = [d.split() for d in days]
        self.header.set_labels(days)
        self.header.queue_draw()
        self.emit('dates-changed')

    def update_days_label(self, format="%d"):
        """
        Updates the label of the days of the month to be drawn given a specific
        strftime @format.

        @param format: string, must follow the strftime convention.
         Default: "%d" - day of month as a zero-padded decimal number.
        """
        days = []
        for week in self.weeks:
            days.append(week['dates'].label(format))
        self.all_day_tasks.set_labels(days)

    def set_task_drawing_position(self, dtask, week, grid, num_week=None):
        """
        Calculates and sets the position of a @dtask inside a specific @week
        using @grid as guidance.

        @param dtask: a DrawingTask object.
        @param week: a WeekSpan object.
        @param grid: a Grid object.
        """
        task = self.req.get_task(dtask.get_id())

        start = max(task.get_start_date().date(), week.start_date)
        end = min(task.get_due_date().date(), week.end_date)
        duration = (end - start).days + 1

        x = utils.date_to_col_coord(start, week.start_date)
        w = duration
        x, y, w, h = grid.add_to_grid(x, w, id=dtask.get_id())

        dtask.set_position(x, y, w, h)  # position inside this grid
        dtask.set_week_num(num_week)  # which week this task is in
        dtask.set_overflowing_L(week.start_date)
        dtask.set_overflowing_R(week.end_date)

    def get_current_year(self):
        """
        Gets the correspondent year of the days
        being displayed in the calendar view
        """
        date_this_month = datetime.date(self.year, self.month, 1)
        return date_this_month.strftime("%B / %Y")

    def update_tasks(self):
        """ Updates and redraws everything related to the tasks """
        self.update_drawtasks()
        self.compute_size()
        self.all_day_tasks.queue_draw()

    def is_in_week_range(self, task, week):
        """
        Returns true if the given @task have either the start or due days
        between the start and end day of @week.

        @param task: a Task object
        @param week: a WeekSpan object
        """
        return (task.get_due_date().date() >= week.start_date) and \
               (task.get_start_date().date() <= week.end_date)

    def get_maximum_tasks_per_week(self):
        tasks_available_area = (self.get_week_height() -
                                self.all_day_tasks.get_label_height())
        # FIXME: remove max(4)
        return max(int(tasks_available_area // self.get_task_height()), 4)

    def on_show_more_tasks(self, day):
        appears_in_day = lambda t: \
            (t.task.get_due_date().date() >= day) and \
            (t.task.get_start_date().date() <= day)

        row = utils.date_to_row_coord(day, datetime.date(self.year, self.month, 1))
        week = self.weeks[row]
        tasks = [t.task for t in week['tasks'] if
                 appears_in_day(t)]

        # FIXME: create popover also (check if GNOME >= 3.12)
        popup = DayCell(self.get_toplevel(), day, tasks)
        popup.run()
        popup.destroy()
        return True

    def create_label(self, row, col, count):
        label = '+%d more' % count
        self.overflow_links.append((label, row, col))

    def tasks_to_hide(self, row, col, visible_rows, needed_rows):
        grid = self.weeks[row]['grid']
        to_hide = []
        if needed_rows >= visible_rows:  # grid.num_rows:
            for i in range(visible_rows, needed_rows):
                cell = grid[i][col]
                if not cell.is_free():
                    to_hide.append(str(cell))
        return to_hide

    def update_drawtasks(self, tasks=None):
        """
        Updates the drawtasks and calculates the position of where each one of
        them should be drawn.

        @param tasks: a Task list, containing the tasks to be drawn.
         If none is given, the tasks will be retrieved from the requester.
        """
        def duration(task):
            return (task.get_due_date().date() - task.get_start_date().date()).days

        if not tasks:
            tasks = [self.req.get_task(t) for t in self.req.get_tasks_tree()]
            tasks.sort(key=lambda t: duration(t), reverse=True)
        self.tasks = [t for t in tasks if self.is_in_days_range(t)]

        dtasks = []
        for i, week in enumerate(self.weeks):
            week['tasks'] = [DrawTask(t) for t in self.tasks if
                             self.is_in_week_range(t, week['dates'])]
            dtasks += week['tasks']

            week['grid'].clear_rows()
            for t in week['tasks']:
                self.set_task_drawing_position(t, week['dates'],
                                               week['grid'], i)
        self.all_day_tasks.set_tasks_to_draw(dtasks)

        # deals with when we have more tasks than available lines in a same day
        self.overflow_links = []  # clear previous links, if any
        visible_rows = self.get_maximum_tasks_per_week()
        for row, week in enumerate(self.weeks):
            if week['grid'].num_rows > visible_rows:
                for col in range(self.numdays):
                    needed_rows = self.total_rows_needed_in_calendar_cell(
                        row, col)
                    # if can't fit, hide last tasks and create link to them
                    if needed_rows > visible_rows:
                        to_hide = self.tasks_to_hide(row, col, visible_rows,
                                                     needed_rows)
                        num_hidden_tasks = len(to_hide)

                        # hide overflowing tasks from cell
                        for dtask in week['tasks']:
                            if dtask.get_id() in to_hide:
                                dtask.set_position(-1, -1, -1, -1)

                        # create label to link to hidden tasks
                        self.create_label(row, col, num_hidden_tasks)
        self.all_day_tasks.overflow_links = self.overflow_links

        # clears selected_task if it is not being showed
        if self.selected_task:
            task = self.req.get_task(self.get_selected_task)
            if task and not self.is_in_days_range(task):
                self.unselect_task()
        self.all_day_tasks.selected_task = self.selected_task

    def fade_days_not_in_this_month(self):
        """
        Fade the days at beginnig and/or the end of the view that do not belong
        to the current month being displayed.
        """
        cells = []

        # cells to fade from days in previous month
        row = 0
        col = 0
        for day in self.weeks[0]['dates'].days:
            if day.month != self.month:
                cells.append((row, col))
                col += 1
            else:
                break

        # cells to fade from days in next month
        row = self.numweeks - 1
        col = self.numdays - 1
        for day in reversed(self.weeks[-1]['dates'].days):
            if day.month != self.month:
                cells.append((row, col))
                col -= 1
            else:
                break
        self.all_day_tasks.faded_cells = cells

    def highlight_today_cell(self):
        """ Highlights the cell equivalent to today."""
        if self.is_today_being_shown():
            today = datetime.date.today()
            row = utils.date_to_row_coord(
                today, datetime.date(self.year, self.month, 1))
            if row == -1:
                row = self.numweeks
            col = datetime.date.today().weekday()
        else:
            row = -1
            col = -1
        self.all_day_tasks.set_today_cell(row, col)
        # self.header.set_highlight_cell(0, col)

    def update(self):
        """
        Updates the header, the content to be drawn (tasks), recalculates the
        size needed and then redraws everything.
        """
        self.update_drawtasks()
        self.compute_size()
        self.highlight_today_cell()
        self.fade_days_not_in_this_month()
        self.update_header()
        self.update_days_label()
        self.all_day_tasks.queue_draw()

    def next(self, months=1):
        """
        Advances the dates being displayed by a given number of @months.

        @param days: integer, the number of months to advance. Default = 1.
        """
        day_in_next_month = self.last_day() + datetime.timedelta(days=1)
        self.update_weeks(day_in_next_month.year, day_in_next_month.month)
        self.update()

    def previous(self, months=1):
        """
        Regresses the dates being displayed by a given number of @months.

        @param months: integer, the number of months to go back. Default = 1.
        """
        day_in_prev_month = self.first_day() - datetime.timedelta(days=1)
        self.update_weeks(day_in_prev_month.year, day_in_prev_month.month)
        self.update()

    def total_days_between_cells(self, cell_a, cell_b):
        """
        Returns the total of days elapsed between two grid cells of a month
        calendar. If the dates are the same, it returns 0.

        @param cell_a: tuple (int, int), contains (row, col) of first cell.
        @param cell_b: tuple (int, int), contains (row, col) of sencond cell.
        @return total_days: integer, the number of days between the two cells,
        returning 0 if they are the same.
        """
        # get dates correspoding for each cell
        start = self.weeks[cell_a[0]]['dates'].days[cell_a[1]]
        end = self.weeks[cell_b[0]]['dates'].days[cell_b[1]]
        return (end - start).days

    def get_right_order(self, start_row, start_col, end_row, end_col):
        """
        Gets the right order two cells (@start_row, @start_col) and
        (@end_row, @end_col) should have, inverting them in case the @end cell
        starts before the @start. The result will always return the one most
        at the most top-left as @start, and the other as @end.

        @param start_row: integer, the row where the user started dragging
        @param start_col: integer, the col where the user started dragging
        @param end_row: integer, the row where the user finished dragging
        @param end_col: integer, the col where the user finished dragging

        @return start_row, start_col, end_row, end_col: a 4-tuple of integers,
                containing the row and col for the cell at the most top-left,
                and then the row and col of the other one.
        """
        if end_row < start_row:  # multiple rows
            end_row, start_row = start_row, end_row
            end_col, start_col = start_col, end_col
        elif end_row == start_row and end_col < start_col:  # single row
            end_col, start_col = start_col, end_col
        return start_row, start_col, end_row, end_col

    def calculate_offset(self, task_id, event):
        """
        Calculates the vertical and horizontal offsets, so a user can drag not
        only from the beggining of a task (in case it spans multiple rows
        and/or columns). The offsets will be calculated using the task
        represented by @task_id as reference.

        @param task_id: string, the id of the Task object we want to use as
                        reference.
        @param event: GdkEvent object, contains the pointer coordinates.
        @return offset_x: float, horizontal offset.
        @return offset_y: float, vertical offset.
        """
        task = self.req.get_task(task_id)

        # calculate vertical offset
        week_height = self.get_week_height()
        clicked_row = utils.convert_coordinates_to_row(event.y,
                                                       week_height)
        # start_row points to row where task starts, or to first row if
        # it starts in date previous to what is being shown at this view
        start_row = clicked_row
        while (start_row > 0 and task.get_start_date().date() <
               self.weeks[start_row]['dates'].start_date):
            start_row -= 1
        offset_y = (start_row - clicked_row) * week_height

        # calculate horizontal offset
        day_width = self.get_day_width()
        clicked_col = utils.convert_coordinates_to_col(event.x,
                                                       day_width)
        #start_col = task.get_start_date().date().weekday()
        start_col_in_clicked_row = max(task.get_start_date().date(),
            self.weeks[clicked_row]['dates'].start_date).weekday()
        col_diff = clicked_col - start_col_in_clicked_row

        offset_x = (start_col_in_clicked_row - clicked_col) * day_width
        if self.drag_action == "expand_right":
            offset_x += col_diff * day_width
            offset_y = 0
        elif self.drag_action == "move":
            offset_x = clicked_col * day_width
            offset_y = clicked_row * week_height

        return offset_x, offset_y

    def dnd_start(self, widget, event):
        """ User clicked the mouse button, starting drag and drop """
        # find which task was clicked, if any
        self.selected_task, self.drag_action, cursor = \
            self.all_day_tasks.identify_pointed_object(event, clicked=True)

        if self.selected_task:
            # double-click opens task to edit
            if event.type == Gdk.EventType._2BUTTON_PRESS:
                GObject.idle_add(self.emit, 'on_edit_task',
                                 self.selected_task)
                self.is_dragging = False
                self.drag_offset = None
                return

            self.drag_offset = self.calculate_offset(self.selected_task, event)
            self.update_tasks()
        # if no task is selected, save mouse location in case the user wants
        # to create a new task using DnD
        else:
            event_x = event.x
            event_y = event.y
            self.drag_offset = (event_x, event_y)

        widget.get_window().set_cursor(cursor)

    def motion_notify(self, widget, event):
        """ User moved mouse over widget """
        # dragging with no task selected: new task will be created
        if not self.selected_task and self.drag_offset:
            self.is_dragging = True
            day_width = self.get_day_width()
            week_height = self.get_week_height()
            curr_row, curr_col = utils.convert_coordinates_to_grid(
                event.x, event.y, day_width, week_height)
            start_row, start_col = utils.convert_coordinates_to_grid(
                self.drag_offset[0], self.drag_offset[1],
                day_width, week_height)

            # invert cols/rows in case user started dragging from the end date
            start_row, start_col, curr_row, curr_col = self.get_right_order(
                start_row, start_col, curr_row, curr_col)

            total_days = self.total_days_between_cells((start_row, start_col),
                                                       (curr_row, curr_col))+1

            # highlight cells while moving mouse
            cells = []
            for i in range((curr_row - start_row + 1) + 1):
                for col in range(start_col,
                                 min(start_col+total_days, self.numdays)):
                    cells.append((start_row + i, col))
                total_days -= (self.numdays - start_col)
                start_col = 0

            self.all_day_tasks.cells = cells
            self.all_day_tasks.queue_draw()
            return

        if self.selected_task and self.drag_offset:  # a task was clicked
            self.is_dragging = True
            task = self.req.get_task(self.selected_task)
            start_date = task.get_start_date().date()
            end_date = task.get_due_date().date()
            duration = (end_date - start_date).days

            # don't do any action beyond delimited area
            alloc = self.get_allocation()
            if (event.x < 0 or event.x > alloc.width or
                    event.y < 0 or event.y > alloc.height):
                return

            event_x = event.x
            event_y = event.y

            day_width = self.get_day_width()
            week_height = self.get_week_height()

            row = utils.convert_coordinates_to_row(event_y, week_height)
            col = utils.convert_coordinates_to_col(event_x, day_width)
            if row < 0 or row >= self.numweeks or col < 0 or col >= self.numdays:
                return

            if self.drag_action == "expand_left":
                new_start_day = self.weeks[row]['dates'].days[col]
                if new_start_day <= end_date:
                    task.set_start_date(new_start_day)

            elif self.drag_action == "expand_right":
                new_due_day = self.weeks[row]['dates'].days[col]
                if new_due_day >= start_date:
                    task.set_due_date(new_due_day)

            else:
                offset_x = self.drag_offset[0]
                offset_y = self.drag_offset[1]
                previous_row, previous_col = utils.convert_coordinates_to_grid(
                    offset_x, offset_y, day_width, week_height)
                diff = self.total_days_between_cells(
                    (previous_row, previous_col), (row, col))

                if diff != 0:  # new_start_day != start_date:
                    new_start_day = start_date + datetime.timedelta(days=diff)
                    new_due_day = new_start_day + datetime.timedelta(days=duration)
                    task.set_start_date(new_start_day)
                    task.set_due_date(new_due_day)
                    self.drag_offset = self.calculate_offset(self.selected_task, event)

            self.update()

        else:  # mouse hover
            t_id, self.drag_action, cursor = \
                self.all_day_tasks.identify_pointed_object(event)
            widget.get_window().set_cursor(cursor)

    def dnd_stop(self, widget, event):
        """
        User released a button, stopping drag and drop.
        Selected task, if any, will still have the focus.
        """
        # dragging with no task selected: new task will be created
        if not self.selected_task and self.is_dragging:
            day_width = self.get_day_width()
            week_height = self.get_week_height()
            start_row, start_col = utils.convert_coordinates_to_grid(
                self.drag_offset[0], self.drag_offset[1],
                day_width, week_height)

            event_x = event.x
            event_y = event.y
            end_row, end_col = utils.convert_coordinates_to_grid(
                event_x, event_y, day_width, week_height)

            # invert cols/rows in case user started dragging from the end date
            start_row, start_col, end_row, end_col = self.get_right_order(
                start_row, start_col, end_row, end_col)

            total_days = self.total_days_between_cells(
                (start_row, start_col), (end_row, end_col))
            start_date = self.weeks[start_row]['dates'].days[start_col]
            due_date = start_date + datetime.timedelta(days=total_days)

            GObject.idle_add(self.emit, 'on_add_task', start_date, due_date)
            self.all_day_tasks.queue_draw()
            self.all_day_tasks.cells = []

        # user didn't click on a task or just finished dragging task
        # in both cases, redraw to 'unselect' task
        elif not self.selected_task or self.is_dragging:
            self.unselect_task()
            self.all_day_tasks.queue_draw()

        # clicked on link to show hidden tasks
        if self.drag_action == 'click_link':
            row, col = utils.convert_coordinates_to_grid(
                event.x, event.y, self.get_day_width(), self.get_week_height())
            day = self.weeks[row]['dates'].days[col]
            self.on_show_more_tasks(day)
            self.drag_action = None

        widget.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
        self.drag_offset = None
        self.is_dragging = False
        self.drag_action = None