def draw(self,
             dc,
             time_period,
             events,
             period_selection=None,
             legend=False,
             divider_line_slider=None):
        """
        Implement the drawing interface.

        The drawing is done in a number of steps: First positions of all events
        and strips are calculated and then they are drawn. Positions can also
        be used later to answer questions like what event is at position (x, y).
        """
        # Store data so we can use it in other functions
        self.dc = dc
        self.time_period = time_period
        self.metrics = Metrics(dc, time_period, divider_line_slider)
        # Data
        self.event_data = []  # List of tuples (event, rect)
        self.major_strip_data = []  # List of time_period
        self.minor_strip_data = []  # List of time_period
        # Calculate stuff later used for drawing
        self._calc_rects(events)
        self._calc_strips()
        # Perform the actual drawing
        if period_selection:
            self._draw_period_selection(period_selection)
        self._draw_bg()
        self._draw_events()
        if legend:
            self._draw_legend(self._extract_categories(events))
        self._draw_ballons()
        # Make sure to delete this one
        del self.dc
    def draw(self, dc, time_period, events, period_selection=None,
             legend=False,  divider_line_slider=None):
        """
        Implement the drawing interface.

        The drawing is done in a number of steps: First positions of all events
        and strips are calculated and then they are drawn. Positions can also
        be used later to answer questions like what event is at position (x, y).
        """
        # Store data so we can use it in other functions
        self.dc = dc
        self.time_period = time_period
        self.metrics = Metrics(dc, time_period, divider_line_slider)
        # Data
        self.event_data = []       # List of tuples (event, rect)
        self.major_strip_data = [] # List of time_period
        self.minor_strip_data = [] # List of time_period
        # Calculate stuff later used for drawing
        self._calc_rects(events)
        self._calc_strips()
        # Perform the actual drawing
        if period_selection:
            self._draw_period_selection(period_selection)
        self._draw_bg()
        self._draw_events()
        if legend:
            self._draw_legend(self._extract_categories(events))
        self._draw_ballons()
        # Make sure to delete this one
        del self.dc
class DefaultDrawingAlgorithm(DrawingAlgorithm):
    def __init__(self):
        # Fonts and pens we use when drawing
        self.header_font = drawing.get_default_font(12, True)
        self.small_text_font = drawing.get_default_font(8)
        self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.SOLID)
        self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.SOLID)
        self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.SOLID)
        self.black_dashed_pen = wx.Pen(wx.Colour(200, 200, 200), 1,
                                       wx.USER_DASH)
        self.black_dashed_pen.SetDashes([2, 2])
        self.black_dashed_pen.SetCap(wx.CAP_BUTT)
        self.grey_solid_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SOLID)
        self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.SOLID)
        self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.SOLID)
        self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230),
                                              wx.SOLID)
        self.DATA_ICON_WIDTH = 5

    def event_is_period(self, time_period):
        ew = self.metrics.calc_width(time_period)
        return ew > PERIOD_THRESHOLD

    def draw(self,
             dc,
             time_period,
             events,
             period_selection=None,
             legend=False,
             divider_line_slider=None):
        """
        Implement the drawing interface.

        The drawing is done in a number of steps: First positions of all events
        and strips are calculated and then they are drawn. Positions can also
        be used later to answer questions like what event is at position (x, y).
        """
        # Store data so we can use it in other functions
        self.dc = dc
        self.time_period = time_period
        self.metrics = Metrics(dc, time_period, divider_line_slider)
        # Data
        self.event_data = []  # List of tuples (event, rect)
        self.major_strip_data = []  # List of time_period
        self.minor_strip_data = []  # List of time_period
        # Calculate stuff later used for drawing
        self._calc_rects(events)
        self._calc_strips()
        # Perform the actual drawing
        if period_selection:
            self._draw_period_selection(period_selection)
        self._draw_bg()
        self._draw_events()
        if legend:
            self._draw_legend(self._extract_categories(events))
        self._draw_ballons()
        # Make sure to delete this one
        del self.dc

    def snap(self, time, snap_region=10):
        major_strip, minor_strip = self._choose_strip()
        time_x = self.metrics.calc_exact_x(time)
        left_strip_time = minor_strip.start(time)
        right_strip_time = minor_strip.increment(left_strip_time)
        left_diff = abs(time_x - self.metrics.calc_exact_x(left_strip_time))
        right_diff = abs(time_x - self.metrics.calc_exact_x(right_strip_time))
        if left_diff < snap_region:
            return left_strip_time
        elif right_diff < snap_region:
            return right_strip_time
        else:
            return time

    def snap_selection(self, period_selection):
        start, end = period_selection
        return (self.snap(start), self.snap(end))

    def event_at(self, x, y):
        for (event, rect) in self.event_data:
            if rect.Contains(wx.Point(x, y)):
                return event
        return None

    def event_with_rect_at(self, x, y):
        for (event, rect) in self.event_data:
            if rect.Contains(wx.Point(x, y)):
                return (event, rect)
        return None

    def event_rect(self, evt):
        for (event, rect) in self.event_data:
            if evt == event:
                return rect
        return None

    def get_selected_events(self):
        selected_events = []
        for (event, rect) in self.event_data:
            if event.selected:
                selected_events.append(event)
        return selected_events

    def _calc_rects(self, events):
        """
        Calculate rectangles for all events.

        The rectangles define the areas in which the events can draw
        themselves.

        During the calculations, the outer padding is part of the rectangles to
        make the calculations easier. Outer padding is removed in the end.
        """
        self.dc.SetFont(self.small_text_font)
        for event in events:
            tw, th = self.dc.GetTextExtent(event.text)
            ew = self.metrics.calc_width(event.time_period)
            if ew > PERIOD_THRESHOLD:
                # Treat as period (periods are placed below the baseline, with
                # indicates length of period)
                rw = ew + 2 * OUTER_PADDING
                rh = th + 2 * INNER_PADDING + 2 * OUTER_PADDING
                rx = (self.metrics.calc_x(event.time_period.start_time) -
                      OUTER_PADDING)
                ry = self.metrics.half_height + BASELINE_PADDING
                movedir = 1
            else:
                # Treat as event (events are placed above the baseline, with
                # indicates length of text)
                rw = tw + 2 * INNER_PADDING + 2 * OUTER_PADDING
                rh = th + 2 * INNER_PADDING + 2 * OUTER_PADDING
                if event.has_data():
                    rw += DATA_INDICATOR_SIZE / 3
                rx = self.metrics.calc_x(event.mean_time()) - rw / 2
                ry = self.metrics.half_height - rh - BASELINE_PADDING
                movedir = -1
            rect = wx.Rect(rx, ry, rw, rh)
            self._prevent_overlap(rect, movedir)
            self.event_data.append((event, rect))
        for (event, rect) in self.event_data:
            # Remove outer padding
            rect.Deflate(OUTER_PADDING, OUTER_PADDING)


#            # Make sure rectangle are not far outside the screen
#            if rect.X < -1:
#                move = -rect.X - 1
#                rect.X += move
#                rect.Width -= move
#            if rect.Width > self.metrics.width:
#                rect.Width = self.metrics.width + 2

    def _prevent_overlap(self, rect, movedir):
        """
        Prevent rect from overlapping with any rectangle by moving it.
        """
        while True:
            h = self._intersection_height(rect)
            if h > 0:
                rect.Y += movedir * h
            else:
                break

    def _intersection_height(self, rect):
        """
        Calculate height of first intersection with rectangle.
        """
        for (event, r) in self.event_data:
            if rect.Intersects(r):
                # Calculate height of intersection only if there is any
                r_copy = wx.Rect(*r)  # Because `Intersect` modifies rect
                intersection = r_copy.Intersect(rect)
                return intersection.Height
        return 0

    def _calc_strips(self):
        """Fill the two arrays `minor_strip_data` and `major_strip_data`."""
        def fill(list, strip):
            """Fill the given list with the given strip."""
            current_start = strip.start(self.time_period.start_time)
            while current_start < self.time_period.end_time:
                next_start = strip.increment(current_start)
                list.append(TimePeriod(current_start, next_start))
                current_start = next_start

        major_strip, minor_strip = self._choose_strip()
        fill(self.major_strip_data, major_strip)
        fill(self.minor_strip_data, minor_strip)

    def _choose_strip(self):
        """
        Return a tuple (major_strip, minor_strip) for current time period and
        window size.
        """
        today = datetime.now()
        tomorrow = today + timedelta(days=1)
        day_period = TimePeriod(today, tomorrow)
        one_day_width = self.metrics.calc_exact_width(day_period)
        if one_day_width > 600:
            return (StripDay(), StripHour())
        elif one_day_width > 45:
            return (StripWeek(), StripWeekday())
        elif one_day_width > 25:
            return (StripMonth(), StripDay())
        elif one_day_width > 1.5:
            return (StripYear(), StripMonth())
        elif one_day_width > 0.12:
            return (StripDecade(), StripYear())
        else:
            return (StripDecade(), StripDecade())

    def _draw_period_selection(self, period_selection):
        start, end = period_selection
        start_x = self.metrics.calc_x(start)
        end_x = self.metrics.calc_x(end)
        self.dc.SetBrush(self.lightgrey_solid_brush)
        self.dc.SetPen(wx.TRANSPARENT_PEN)
        self.dc.DrawRectangle(start_x, 0, end_x - start_x + 1,
                              self.metrics.height)

    def _draw_bg(self):
        """
        Draw major and minor strips, lines to all event boxes and baseline.

        Both major and minor strips have divider lines and labels.
        """
        major_strip, minor_strip = self._choose_strip()
        # Minor strips
        self.dc.SetFont(self.small_text_font)
        self.dc.SetPen(self.black_dashed_pen)
        for tp in self.minor_strip_data:
            # Divider line
            x = self.metrics.calc_x(tp.end_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)
            # Label
            label = minor_strip.label(tp.start_time)
            (tw, th) = self.dc.GetTextExtent(label)
            middle = self.metrics.calc_x(tp.mean_time())
            middley = self.metrics.half_height
            self.dc.DrawText(label, middle - tw / 2, middley - th)
        # Major strips
        self.dc.SetFont(self.header_font)
        self.dc.SetPen(self.grey_solid_pen)
        for tp in self.major_strip_data:
            # Divider line
            x = self.metrics.calc_x(tp.end_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)
            # Label
            label = major_strip.label(tp.start_time, True)
            (tw, th) = self.dc.GetTextExtent(label)
            x = self.metrics.calc_x(tp.mean_time()) - tw / 2
            # If the label is not visible when it is positioned in the middle
            # of the period, we move it so that as much of it as possible is
            # visible without crossing strip borders.
            if x - INNER_PADDING < 0:
                x = INNER_PADDING
                right = self.metrics.calc_x(tp.end_time)
                if x + tw + INNER_PADDING > right:
                    x = right - tw - INNER_PADDING
            elif x + tw + INNER_PADDING > self.metrics.width:
                x = self.metrics.width - tw - INNER_PADDING
                left = self.metrics.calc_x(tp.start_time)
                if x < left:
                    x = left + INNER_PADDING
            self.dc.DrawText(label, x, INNER_PADDING)
        # Main divider line
        self.dc.SetPen(self.black_solid_pen)
        self.dc.DrawLine(0, self.metrics.half_height, self.metrics.width,
                         self.metrics.half_height)
        # Lines to all events
        self.dc.SetBrush(self.black_solid_brush)
        for (event, rect) in self.event_data:
            if rect.Y < self.metrics.half_height:
                x = self.metrics.calc_x(event.mean_time())
                y = rect.Y + rect.Height / 2
                self.dc.DrawLine(x, y, x, self.metrics.half_height)
                self.dc.DrawCircle(x, self.metrics.half_height, 2)
        # Now line
        now_time = datetime.now()
        if self.time_period.inside(now_time):
            self.dc.SetPen(self.darkred_solid_pen)
            x = self.metrics.calc_x(now_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)

    def _extract_categories(self, events):
        categories = []
        for event in events:
            cat = event.category
            if cat and not cat in categories:
                categories.append(cat)
        return sort_categories(categories)

    def _draw_legend(self, categories):
        """
        Draw legend for the given categories.

        Box in lower left corner:

          +----------+
          | Name   O |
          | Name   O |
          +----------+
        """
        num_categories = len(categories)
        if num_categories == 0:
            return

        def calc_sizes(dc):
            """Return (width, height, item_height)."""
            width = 0
            height = INNER_PADDING
            item_heights = 0
            for cat in categories:
                tw, th = self.dc.GetTextExtent(cat.name)
                height = height + th + INNER_PADDING
                item_heights += th
                if tw > width:
                    width = tw
            item_height = item_heights / num_categories
            return (width + 4 * INNER_PADDING + item_height, height,
                    item_height)

        self.dc.SetFont(self.small_text_font)
        self.dc.SetTextForeground((0, 0, 0))
        width, height, item_height = calc_sizes(self.dc)
        # Draw big box
        self.dc.SetBrush(self.white_solid_brush)
        self.dc.SetPen(self.black_solid_pen)
        box_rect = (OUTER_PADDING,
                    self.metrics.height - height - OUTER_PADDING, width,
                    height)
        self.dc.DrawRectangleRect(box_rect)
        # Draw text and color boxes
        cur_y = self.metrics.height - height - OUTER_PADDING + INNER_PADDING
        for cat in categories:
            base_color = cat.color
            border_color = drawing.darken_color(base_color)
            self.dc.SetBrush(wx.Brush(base_color, wx.SOLID))
            self.dc.SetPen(wx.Pen(border_color, 1, wx.SOLID))
            color_box_rect = (OUTER_PADDING + width - item_height -
                              INNER_PADDING, cur_y, item_height, item_height)
            self.dc.DrawRectangleRect(color_box_rect)
            self.dc.DrawText(cat.name, OUTER_PADDING + INNER_PADDING, cur_y)
            cur_y = cur_y + item_height + INNER_PADDING

    def _draw_events(self):
        """Draw all event boxes and the text inside them."""
        self.dc.SetFont(self.small_text_font)
        self.dc.SetTextForeground((0, 0, 0))
        for (event, rect) in self.event_data:
            # Ensure that we can't draw outside rectangle
            self.dc.DestroyClippingRegion()
            self.dc.SetClippingRect(rect)
            # Draw the box
            self.dc.SetBrush(self._get_box_brush(event))
            self.dc.SetPen(self._get_box_pen(event))
            self.dc.DrawRectangleRect(rect)
            # Ensure that we can't draw content outside inner rectangle
            self.dc.DestroyClippingRegion()
            rect_copy = wx.Rect(*rect)
            rect_copy.Deflate(INNER_PADDING, INNER_PADDING)
            self.dc.SetClippingRect(rect_copy)
            if rect_copy.Width > 0:
                # Draw the text (if there is room for it)
                text_x = rect.X + INNER_PADDING
                text_y = rect.Y + INNER_PADDING
                if text_x < INNER_PADDING:
                    text_x = INNER_PADDING
                self.dc.DrawText(event.text, text_x, text_y)
            # Draw data contents indicator
            self.dc.DestroyClippingRegion()
            self.dc.SetClippingRect(rect)
            if event.has_data():
                self._draw_contents_indicator(event, rect)
            # Draw selection and handles
            if event.selected:
                small_rect = wx.Rect(*rect)
                small_rect.Deflate(1, 1)
                border_color = self._get_border_color(event)
                border_color = drawing.darken_color(border_color)
                pen = wx.Pen(border_color, 1, wx.SOLID)
                self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
                self.dc.SetPen(pen)
                self.dc.DrawRectangleRect(small_rect)
                self._draw_handles(rect)
        # Reset this when we are done
        self.dc.DestroyClippingRegion()

    def _draw_handles(self, rect):
        SIZE = 4
        big_rect = wx.Rect(rect.X - SIZE, rect.Y - SIZE, rect.Width + 2 * SIZE,
                           rect.Height + 2 * SIZE)
        self.dc.DestroyClippingRegion()
        self.dc.SetClippingRect(big_rect)
        y = rect.Y + rect.Height / 2 - SIZE / 2
        x = rect.X - SIZE / 2
        west_rect = wx.Rect(x + 1, y, SIZE, SIZE)
        center_rect = wx.Rect(x + rect.Width / 2, y, SIZE, SIZE)
        east_rect = wx.Rect(x + rect.Width - 1, y, SIZE, SIZE)
        self.dc.SetBrush(wx.Brush("BLACK", wx.SOLID))
        self.dc.SetPen(wx.Pen("BLACK", 1, wx.SOLID))
        self.dc.DrawRectangleRect(east_rect)
        self.dc.DrawRectangleRect(west_rect)
        self.dc.DrawRectangleRect(center_rect)

    def _draw_contents_indicator(self, event, rect):
        """
        The data contents indicator is a small icon added to the end of
        the event rectangle.
        The icon is a rectangle with LIGHT_GRAY background and a RED
        triangle.
        """
        corner_x = rect.X + rect.Width
        if corner_x > self.metrics.width:
            corner_x = self.metrics.width
        points = (
            wx.Point(corner_x - DATA_INDICATOR_SIZE, rect.Y),
            wx.Point(corner_x, rect.Y),
            wx.Point(corner_x, rect.Y + DATA_INDICATOR_SIZE),
        )
        self.dc.SetBrush(self._get_box_indicator_brush(event))
        self.dc.SetPen(wx.TRANSPARENT_PEN)
        self.dc.DrawPolygon(points)

    def _get_base_color(self, event):
        if event.category:
            base_color = event.category.color
        else:
            base_color = (200, 200, 200)
        return base_color

    def _get_border_color(self, event):
        base_color = self._get_base_color(event)
        border_color = drawing.darken_color(base_color)
        return border_color

    def _get_box_pen(self, event):
        border_color = self._get_border_color(event)
        pen = wx.Pen(border_color, 1, wx.SOLID)
        return pen

    def _get_box_brush(self, event):
        base_color = self._get_base_color(event)
        brush = wx.Brush(base_color, wx.SOLID)
        return brush

    def _get_box_indicator_brush(self, event):
        base_color = self._get_base_color(event)
        darker_color = drawing.darken_color(base_color, 0.6)
        brush = wx.Brush(darker_color, wx.SOLID)
        return brush

    def _get_selected_box_brush(self, event):
        border_color = self._get_border_color(event)
        brush = wx.Brush(border_color, wx.BDIAGONAL_HATCH)
        return brush

    def _draw_ballons(self):
        """Draw ballons on selected events that has 'description' data."""
        for (event, rect) in self.event_data:
            if (event.get_data("description") != None
                    or event.get_data("icon") != None):
                if event.draw_ballon:
                    self._draw_ballon(event, rect)

    def _draw_ballon(self, event, event_rect):
        """Draw one ballon on a selected event that has 'description' data."""
        # Constants
        MAX_TEXT_WIDTH = 200
        MIN_WIDTH = 100
        inner_rect_w = 0
        inner_rect_h = 0
        # Icon
        (iw, ih) = (0, 0)
        icon = event.get_data("icon")
        if icon != None:
            (iw, ih) = icon.Size
            inner_rect_w = iw
            inner_rect_h = ih
        # Text
        self.dc.SetFont(drawing.get_default_font(8))
        font_h = self.dc.GetCharHeight()
        (tw, th) = (0, 0)
        description = event.get_data("description")
        lines = None
        if description != None:
            lines = break_text(description, self.dc, MAX_TEXT_WIDTH)
            th = len(lines) * self.dc.GetCharHeight()
            for line in lines:
                (lw, lh) = self.dc.GetTextExtent(line)
                tw = max(lw, tw)
            if icon != None:
                inner_rect_w += BALLOON_RADIUS
            inner_rect_w += min(tw, MAX_TEXT_WIDTH)
            inner_rect_h = max(inner_rect_h, th)
        inner_rect_w = max(MIN_WIDTH, inner_rect_w)
        x, y = self._draw_balloon_bg(
            self.dc, (inner_rect_w, inner_rect_h),
            (event_rect.X + event_rect.Width / 2, event_rect.Y), True)
        if icon != None:
            self.dc.DrawBitmap(icon, x, y, False)
            x += iw + BALLOON_RADIUS
        if lines != None:
            ty = y
            for line in lines:
                self.dc.DrawText(line, x, ty)
                ty += font_h
            x += tw

    def _draw_balloon_bg(self, dc, inner_size, tip_pos, above):
        """
        Draw the balloon background leaving inner_size for content.

        tip_pos determines where the tip of the ballon should be.

        above determines if the balloon should be above the tip (True) or below
        (False). This is not currently implemented.

                    W
           |----------------|
             ______________           _
            /              \          |             R = Corner Radius
           |                |         |            AA = Left Arrow-leg angle
           |  W_ARROW       |         |  H     MARGIN = Text margin
           |     |--|       |         |             * = Starting point
            \____    ______/          _
                /  /                  |
               /_/                    |  H_ARROW
              *                       -
           |----|
           ARROW_OFFSET
    
        Calculation of points starts at the tip of the arrow and continues
        clockwise around the ballon.

        Return (x, y) which is at top of inner region.
        """
        # Prepare path object
        gc = wx.GraphicsContext.Create(self.dc)
        path = gc.CreatePath()
        # Calculate path
        R = BALLOON_RADIUS
        W = 1 * R + inner_size[0]
        H = 1 * R + inner_size[1]
        H_ARROW = 14
        W_ARROW = 15
        W_ARROW_OFFSET = R + 25
        AA = 20
        # Starting point at the tip of the arrow
        (tipx, tipy) = tip_pos
        p0 = wx.Point(tipx, tipy)
        path.MoveToPoint(p0.x, p0.y)
        # Next point is the left base of the arrow
        p1 = wx.Point(p0.x + H_ARROW * math.tan(math.radians(AA)),
                      p0.y - H_ARROW)
        path.AddLineToPoint(p1.x, p1.y)
        # Start of lower left rounded corner
        p2 = wx.Point(p1.x - W_ARROW_OFFSET + R, p1.y)
        bottom_y = p2.y
        path.AddLineToPoint(p2.x, p2.y)
        # The lower left rounded corner. p3 is the center of the arc
        p3 = wx.Point(p2.x, p2.y - R)
        path.AddArc(p3.x, p3.y, R, math.radians(90), math.radians(180))
        # The left side
        p4 = wx.Point(p3.x - R, p3.y - H + R)
        left_x = p4.x
        path.AddLineToPoint(p4.x, p4.y)
        # The upper left rounded corner. p5 is the center of the arc
        p5 = wx.Point(p4.x + R, p4.y)
        path.AddArc(p5.x, p5.y, R, math.radians(180), math.radians(-90))
        # The upper side
        p6 = wx.Point(p5.x + W - R, p5.y - R)
        top_y = p6.y
        path.AddLineToPoint(p6.x, p6.y)
        # The upper right rounded corner. p7 is the center of the arc
        p7 = wx.Point(p6.x, p6.y + R)
        path.AddArc(p7.x, p7.y, R, math.radians(-90), math.radians(0))
        # The right side
        p8 = wx.Point(p7.x + R, p7.y + H - R)
        right_x = p8.x
        path.AddLineToPoint(p8.x, p8.y)
        # The lower right rounded corner. p9 is the center of the arc
        p9 = wx.Point(p8.x - R, p8.y)
        path.AddArc(p9.x, p9.y, R, math.radians(0), math.radians(90))
        # The lower side
        p10 = wx.Point(p9.x - W + W_ARROW + W_ARROW_OFFSET, p9.y + R)
        path.AddLineToPoint(p10.x, p10.y)
        path.CloseSubpath()
        # Draw sharp lines on GTK which uses Cairo
        # See: http://www.cairographics.org/FAQ/#sharp_lines
        gc.Translate(0.5, 0.5)
        # Draw the ballon
        BORDER_COLOR = wx.Colour(127, 127, 127)
        BG_COLOR = wx.Colour(255, 255, 231)
        PEN = wx.Pen(BORDER_COLOR, 1, wx.SOLID)
        BRUSH = wx.Brush(BG_COLOR, wx.SOLID)
        gc.SetPen(PEN)
        gc.SetBrush(BRUSH)
        gc.DrawPath(path)
        # Return
        return (left_x + BALLOON_RADIUS, top_y + BALLOON_RADIUS)

    def notify_events(self, notification, data):
        """
        Send notification to all visible events
        """
        for (event, rect) in self.event_data:
            event.notify(notification, data)
class DefaultDrawingAlgorithm(DrawingAlgorithm):

    def __init__(self):
        # Fonts and pens we use when drawing
        self.header_font = drawing.get_default_font(12, True)
        self.small_text_font = drawing.get_default_font(8)
        self.red_solid_pen = wx.Pen(wx.Colour(255,0, 0), 1, wx.SOLID)
        self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.SOLID)
        self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.SOLID)
        self.black_dashed_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.USER_DASH)
        self.black_dashed_pen.SetDashes([2, 2])
        self.black_dashed_pen.SetCap(wx.CAP_BUTT)
        self.grey_solid_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SOLID)
        self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.SOLID)
        self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.SOLID)
        self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230), wx.SOLID)
        self.DATA_ICON_WIDTH = 5

    def event_is_period(self, time_period):
        ew = self.metrics.calc_width(time_period)
        return ew > PERIOD_THRESHOLD
    
    def draw(self, dc, time_period, events, period_selection=None,
             legend=False,  divider_line_slider=None):
        """
        Implement the drawing interface.

        The drawing is done in a number of steps: First positions of all events
        and strips are calculated and then they are drawn. Positions can also
        be used later to answer questions like what event is at position (x, y).
        """
        # Store data so we can use it in other functions
        self.dc = dc
        self.time_period = time_period
        self.metrics = Metrics(dc, time_period, divider_line_slider)
        # Data
        self.event_data = []       # List of tuples (event, rect)
        self.major_strip_data = [] # List of time_period
        self.minor_strip_data = [] # List of time_period
        # Calculate stuff later used for drawing
        self._calc_rects(events)
        self._calc_strips()
        # Perform the actual drawing
        if period_selection:
            self._draw_period_selection(period_selection)
        self._draw_bg()
        self._draw_events()
        if legend:
            self._draw_legend(self._extract_categories(events))
        self._draw_ballons()
        # Make sure to delete this one
        del self.dc

    def snap(self, time, snap_region=10):
        major_strip, minor_strip = self._choose_strip()
        time_x = self.metrics.calc_exact_x(time)
        left_strip_time = minor_strip.start(time)
        right_strip_time = minor_strip.increment(left_strip_time)
        left_diff = abs(time_x - self.metrics.calc_exact_x(left_strip_time))
        right_diff = abs(time_x - self.metrics.calc_exact_x(right_strip_time))
        if left_diff < snap_region:
            return left_strip_time
        elif right_diff < snap_region:
            return right_strip_time
        else:
            return time

    def snap_selection(self, period_selection):
        start, end = period_selection
        return (self.snap(start), self.snap(end))

    def event_at(self, x, y):
        for (event, rect) in self.event_data:
            if rect.Contains(wx.Point(x, y)):
                return event
        return None

    def event_with_rect_at(self, x, y):
        for (event, rect) in self.event_data:
            if rect.Contains(wx.Point(x, y)):
                return (event, rect)
        return None

    def event_rect(self, evt):
        for (event, rect) in self.event_data:
            if evt == event:
                return rect
        return None

    def get_selected_events(self):
        selected_events = []
        for (event, rect) in self.event_data:
            if event.selected:
                selected_events.append(event)
        return selected_events
 
    def _calc_rects(self, events):
        """
        Calculate rectangles for all events.

        The rectangles define the areas in which the events can draw
        themselves.

        During the calculations, the outer padding is part of the rectangles to
        make the calculations easier. Outer padding is removed in the end.
        """
        self.dc.SetFont(self.small_text_font)
        for event in events:
            tw, th = self.dc.GetTextExtent(event.text)
            ew = self.metrics.calc_width(event.time_period)
            if ew > PERIOD_THRESHOLD:
                # Treat as period (periods are placed below the baseline, with
                # indicates length of period)
                rw = ew + 2 * OUTER_PADDING
                rh = th + 2 * INNER_PADDING + 2 * OUTER_PADDING
                rx = (self.metrics.calc_x(event.time_period.start_time) -
                      OUTER_PADDING)
                ry = self.metrics.half_height + BASELINE_PADDING
                movedir = 1
            else:
                # Treat as event (events are placed above the baseline, with
                # indicates length of text)
                rw = tw + 2 * INNER_PADDING + 2 * OUTER_PADDING
                rh = th + 2 * INNER_PADDING + 2 * OUTER_PADDING
                if event.has_data():
                    rw += DATA_INDICATOR_SIZE / 3
                rx = self.metrics.calc_x(event.mean_time()) - rw / 2
                ry = self.metrics.half_height - rh - BASELINE_PADDING
                movedir = -1
            rect = wx.Rect(rx, ry, rw, rh)
            self._prevent_overlap(rect, movedir)
            self.event_data.append((event, rect))
        for (event, rect) in self.event_data:
            # Remove outer padding
            rect.Deflate(OUTER_PADDING, OUTER_PADDING)
#            # Make sure rectangle are not far outside the screen
#            if rect.X < -1:
#                move = -rect.X - 1
#                rect.X += move
#                rect.Width -= move
#            if rect.Width > self.metrics.width:
#                rect.Width = self.metrics.width + 2

    def _prevent_overlap(self, rect, movedir):
        """
        Prevent rect from overlapping with any rectangle by moving it.
        """
        while True:
            h = self._intersection_height(rect)
            if h > 0:
                rect.Y += movedir * h
            else:
                break

    def _intersection_height(self, rect):
        """
        Calculate height of first intersection with rectangle.
        """
        for (event, r) in self.event_data:
            if rect.Intersects(r):
                # Calculate height of intersection only if there is any
                r_copy = wx.Rect(*r) # Because `Intersect` modifies rect
                intersection = r_copy.Intersect(rect)
                return intersection.Height
        return 0

    def _calc_strips(self):
        """Fill the two arrays `minor_strip_data` and `major_strip_data`."""
        def fill(list, strip):
            """Fill the given list with the given strip."""
            current_start = strip.start(self.time_period.start_time)
            while current_start < self.time_period.end_time:
                next_start = strip.increment(current_start)
                list.append(TimePeriod(current_start, next_start))
                current_start = next_start
        major_strip, minor_strip = self._choose_strip()
        fill(self.major_strip_data, major_strip)
        fill(self.minor_strip_data, minor_strip)

    def _choose_strip(self):
        """
        Return a tuple (major_strip, minor_strip) for current time period and
        window size.
        """
        today = datetime.now()
        tomorrow = today + timedelta(days=1)
        day_period = TimePeriod(today, tomorrow)
        one_day_width = self.metrics.calc_exact_width(day_period)
        if one_day_width > 600:
            return (StripDay(), StripHour())
        elif one_day_width > 45:
            return (StripWeek(), StripWeekday())
        elif one_day_width > 25:
            return (StripMonth(), StripDay())
        elif one_day_width > 1.5:
            return (StripYear(), StripMonth())
        elif one_day_width > 0.12:
            return (StripDecade(), StripYear())
        else:
            return (StripDecade(), StripDecade())

    def _draw_period_selection(self, period_selection):
        start, end = period_selection
        start_x = self.metrics.calc_x(start)
        end_x = self.metrics.calc_x(end)
        self.dc.SetBrush(self.lightgrey_solid_brush)
        self.dc.SetPen(wx.TRANSPARENT_PEN)
        self.dc.DrawRectangle(start_x, 0,
                              end_x - start_x + 1, self.metrics.height)

    def _draw_bg(self):
        """
        Draw major and minor strips, lines to all event boxes and baseline.

        Both major and minor strips have divider lines and labels.
        """
        major_strip, minor_strip = self._choose_strip()
        # Minor strips
        self.dc.SetFont(self.small_text_font)
        self.dc.SetPen(self.black_dashed_pen)
        for tp in self.minor_strip_data:
            # Divider line
            x = self.metrics.calc_x(tp.end_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)
            # Label
            label = minor_strip.label(tp.start_time)
            (tw, th) = self.dc.GetTextExtent(label)
            middle = self.metrics.calc_x(tp.mean_time())
            middley = self.metrics.half_height
            self.dc.DrawText(label, middle - tw / 2, middley - th)
        # Major strips
        self.dc.SetFont(self.header_font)
        self.dc.SetPen(self.grey_solid_pen)
        for tp in self.major_strip_data:
            # Divider line
            x = self.metrics.calc_x(tp.end_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)
            # Label
            label = major_strip.label(tp.start_time, True)
            (tw, th) = self.dc.GetTextExtent(label)
            x = self.metrics.calc_x(tp.mean_time()) - tw / 2
            # If the label is not visible when it is positioned in the middle
            # of the period, we move it so that as much of it as possible is
            # visible without crossing strip borders.
            if x - INNER_PADDING < 0:
                x = INNER_PADDING
                right = self.metrics.calc_x(tp.end_time)
                if x + tw + INNER_PADDING > right:
                    x = right - tw - INNER_PADDING
            elif x + tw + INNER_PADDING > self.metrics.width:
                x = self.metrics.width - tw - INNER_PADDING
                left = self.metrics.calc_x(tp.start_time)
                if x < left:
                    x = left + INNER_PADDING
            self.dc.DrawText(label, x, INNER_PADDING)
        # Main divider line
        self.dc.SetPen(self.black_solid_pen)
        self.dc.DrawLine(0, self.metrics.half_height, self.metrics.width,
                         self.metrics.half_height)
        # Lines to all events
        self.dc.SetBrush(self.black_solid_brush)
        for (event, rect) in self.event_data:
            if rect.Y < self.metrics.half_height:
                x = self.metrics.calc_x(event.mean_time())
                y = rect.Y + rect.Height / 2
                self.dc.DrawLine(x, y, x, self.metrics.half_height)
                self.dc.DrawCircle(x, self.metrics.half_height, 2)
        # Now line
        now_time = datetime.now()
        if self.time_period.inside(now_time):
            self.dc.SetPen(self.darkred_solid_pen)
            x = self.metrics.calc_x(now_time)
            self.dc.DrawLine(x, 0, x, self.metrics.height)

    def _extract_categories(self, events):
        categories = []
        for event in events:
            cat = event.category
            if cat and not cat in categories:
                categories.append(cat)
        return sort_categories(categories)

    def _draw_legend(self, categories):
        """
        Draw legend for the given categories.

        Box in lower left corner:

          +----------+
          | Name   O |
          | Name   O |
          +----------+
        """
        num_categories = len(categories)
        if num_categories == 0:
            return
        def calc_sizes(dc):
            """Return (width, height, item_height)."""
            width = 0
            height = INNER_PADDING
            item_heights = 0
            for cat in categories:
                tw, th = self.dc.GetTextExtent(cat.name)
                height = height + th + INNER_PADDING
                item_heights += th
                if tw > width:
                    width = tw
            item_height = item_heights / num_categories
            return (width + 4 * INNER_PADDING + item_height, height,
                    item_height)
        self.dc.SetFont(self.small_text_font)
        self.dc.SetTextForeground((0, 0, 0))
        width, height, item_height = calc_sizes(self.dc)
        # Draw big box
        self.dc.SetBrush(self.white_solid_brush)
        self.dc.SetPen(self.black_solid_pen)
        box_rect = (OUTER_PADDING,
                    self.metrics.height - height - OUTER_PADDING,
                    width, height)
        self.dc.DrawRectangleRect(box_rect)
        # Draw text and color boxes
        cur_y = self.metrics.height - height - OUTER_PADDING + INNER_PADDING
        for cat in categories:
            base_color = cat.color
            border_color = drawing.darken_color(base_color)
            self.dc.SetBrush(wx.Brush(base_color, wx.SOLID))
            self.dc.SetPen(wx.Pen(border_color, 1, wx.SOLID))
            color_box_rect = (OUTER_PADDING + width - item_height -
                              INNER_PADDING,
                              cur_y, item_height, item_height)
            self.dc.DrawRectangleRect(color_box_rect)
            self.dc.DrawText(cat.name, OUTER_PADDING + INNER_PADDING, cur_y)
            cur_y = cur_y + item_height + INNER_PADDING

    def _draw_events(self):
        """Draw all event boxes and the text inside them."""
        self.dc.SetFont(self.small_text_font)
        self.dc.SetTextForeground((0, 0, 0))
        for (event, rect) in self.event_data:
            # Ensure that we can't draw outside rectangle
            self.dc.DestroyClippingRegion()
            self.dc.SetClippingRect(rect)
            # Draw the box
            self.dc.SetBrush(self._get_box_brush(event))
            self.dc.SetPen(self._get_box_pen(event))
            self.dc.DrawRectangleRect(rect)
            # Ensure that we can't draw content outside inner rectangle
            self.dc.DestroyClippingRegion()
            rect_copy = wx.Rect(*rect)
            rect_copy.Deflate(INNER_PADDING, INNER_PADDING)
            self.dc.SetClippingRect(rect_copy)
            if rect_copy.Width > 0:
                # Draw the text (if there is room for it)
                text_x = rect.X + INNER_PADDING
                text_y = rect.Y + INNER_PADDING
                if text_x < INNER_PADDING:
                    text_x = INNER_PADDING
                self.dc.DrawText(event.text, text_x, text_y)
            # Draw data contents indicator
            self.dc.DestroyClippingRegion()
            self.dc.SetClippingRect(rect)
            if event.has_data():
                self._draw_contents_indicator(event, rect)
            # Draw selection and handles
            if event.selected:
                small_rect = wx.Rect(*rect)
                small_rect.Deflate(1, 1)
                border_color = self._get_border_color(event)
                border_color = drawing.darken_color(border_color)
                pen = wx.Pen(border_color, 1, wx.SOLID)
                self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
                self.dc.SetPen(pen)
                self.dc.DrawRectangleRect(small_rect)
                self._draw_handles(rect)
        # Reset this when we are done
        self.dc.DestroyClippingRegion()

    def _draw_handles(self, rect):
        SIZE = 4
        big_rect = wx.Rect(rect.X - SIZE, rect.Y - SIZE, rect.Width + 2 * SIZE, rect.Height + 2 * SIZE)
        self.dc.DestroyClippingRegion()
        self.dc.SetClippingRect(big_rect)
        y = rect.Y + rect.Height/2 - SIZE/2
        x = rect.X - SIZE / 2
        west_rect   = wx.Rect(x + 1             , y, SIZE, SIZE)
        center_rect = wx.Rect(x + rect.Width / 2, y, SIZE, SIZE)
        east_rect   = wx.Rect(x + rect.Width - 1, y, SIZE, SIZE)
        self.dc.SetBrush(wx.Brush("BLACK", wx.SOLID))
        self.dc.SetPen(wx.Pen("BLACK", 1, wx.SOLID))
        self.dc.DrawRectangleRect(east_rect)
        self.dc.DrawRectangleRect(west_rect)
        self.dc.DrawRectangleRect(center_rect)
        
    def _draw_contents_indicator(self, event, rect):
        """
        The data contents indicator is a small icon added to the end of
        the event rectangle.
        The icon is a rectangle with LIGHT_GRAY background and a RED
        triangle.
        """
        corner_x = rect.X + rect.Width
        if corner_x > self.metrics.width:
            corner_x = self.metrics.width
        points = (
            wx.Point(corner_x - DATA_INDICATOR_SIZE, rect.Y),
            wx.Point(corner_x, rect.Y),
            wx.Point(corner_x, rect.Y + DATA_INDICATOR_SIZE),
        )
        self.dc.SetBrush(self._get_box_indicator_brush(event))
        self.dc.SetPen(wx.TRANSPARENT_PEN)
        self.dc.DrawPolygon(points)

    def _get_base_color(self, event):
        if event.category:
            base_color = event.category.color
        else:
            base_color = (200, 200, 200)
        return base_color

    def _get_border_color(self, event):
        base_color = self._get_base_color(event)
        border_color = drawing.darken_color(base_color)
        return border_color

    def _get_box_pen(self, event):
        border_color = self._get_border_color(event)
        pen = wx.Pen(border_color, 1, wx.SOLID)
        return pen

    def _get_box_brush(self, event):
        base_color = self._get_base_color(event)
        brush = wx.Brush(base_color, wx.SOLID)
        return brush

    def _get_box_indicator_brush(self, event):
        base_color = self._get_base_color(event)
        darker_color = drawing.darken_color(base_color, 0.6)
        brush = wx.Brush(darker_color, wx.SOLID)
        return brush

    def _get_selected_box_brush(self, event):
        border_color = self._get_border_color(event)
        brush = wx.Brush(border_color, wx.BDIAGONAL_HATCH)
        return brush

    def _draw_ballons(self):
        """Draw ballons on selected events that has 'description' data."""
        for (event, rect) in self.event_data:
            if (event.get_data("description") != None or
                event.get_data("icon") != None):
                if event.draw_ballon:
                    self._draw_ballon(event, rect)

    def _draw_ballon(self, event, event_rect):
        """Draw one ballon on a selected event that has 'description' data."""
        # Constants
        MAX_TEXT_WIDTH = 200
        MIN_WIDTH = 100
        inner_rect_w = 0
        inner_rect_h = 0
        # Icon
        (iw, ih) = (0, 0)
        icon = event.get_data("icon")
        if icon != None:
            (iw, ih) = icon.Size
            inner_rect_w = iw
            inner_rect_h = ih
        # Text
        self.dc.SetFont(drawing.get_default_font(8))
        font_h = self.dc.GetCharHeight()
        (tw, th) = (0, 0)
        description = event.get_data("description")
        lines = None
        if description != None:
            lines = break_text(description, self.dc, MAX_TEXT_WIDTH)
            th = len(lines) * self.dc.GetCharHeight()
            for line in lines:
                (lw, lh) = self.dc.GetTextExtent(line)
                tw = max(lw, tw)
            if icon != None:
                inner_rect_w += BALLOON_RADIUS
            inner_rect_w += min(tw, MAX_TEXT_WIDTH)
            inner_rect_h = max(inner_rect_h, th)
        inner_rect_w = max(MIN_WIDTH, inner_rect_w)
        x, y = self._draw_balloon_bg(self.dc, (inner_rect_w, inner_rect_h),
                              (event_rect.X + event_rect.Width / 2,
                               event_rect.Y),
                              True)
        if icon != None:
            self.dc.DrawBitmap(icon, x, y, False)
            x += iw + BALLOON_RADIUS
        if lines != None:
            ty = y
            for line in lines:
                self.dc.DrawText(line, x, ty)
                ty += font_h
            x += tw

    def _draw_balloon_bg(self, dc, inner_size, tip_pos, above):
        """
        Draw the balloon background leaving inner_size for content.

        tip_pos determines where the tip of the ballon should be.

        above determines if the balloon should be above the tip (True) or below
        (False). This is not currently implemented.

                    W
           |----------------|
             ______________           _
            /              \          |             R = Corner Radius
           |                |         |            AA = Left Arrow-leg angle
           |  W_ARROW       |         |  H     MARGIN = Text margin
           |     |--|       |         |             * = Starting point
            \____    ______/          _
                /  /                  |
               /_/                    |  H_ARROW
              *                       -
           |----|
           ARROW_OFFSET
    
        Calculation of points starts at the tip of the arrow and continues
        clockwise around the ballon.

        Return (x, y) which is at top of inner region.
        """
        # Prepare path object
        gc = wx.GraphicsContext.Create(self.dc)
        path = gc.CreatePath()
        # Calculate path
        R = BALLOON_RADIUS
        W = 1 * R + inner_size[0]
        H = 1 * R + inner_size[1]
        H_ARROW = 14
        W_ARROW = 15
        W_ARROW_OFFSET = R + 25
        AA = 20
        # Starting point at the tip of the arrow
        (tipx, tipy) = tip_pos
        p0 = wx.Point(tipx, tipy)
        path.MoveToPoint(p0.x, p0.y)
        # Next point is the left base of the arrow
        p1 = wx.Point(p0.x + H_ARROW * math.tan(math.radians(AA)),
                      p0.y - H_ARROW)
        path.AddLineToPoint(p1.x, p1.y)
        # Start of lower left rounded corner
        p2 = wx.Point(p1.x - W_ARROW_OFFSET + R, p1.y)
        bottom_y = p2.y
        path.AddLineToPoint(p2.x, p2.y)
        # The lower left rounded corner. p3 is the center of the arc
        p3 = wx.Point(p2.x, p2.y - R)
        path.AddArc(p3.x, p3.y, R, math.radians(90), math.radians(180))
        # The left side
        p4 = wx.Point(p3.x - R, p3.y - H + R)
        left_x = p4.x
        path.AddLineToPoint(p4.x, p4.y)
        # The upper left rounded corner. p5 is the center of the arc
        p5 = wx.Point(p4.x + R, p4.y)
        path.AddArc(p5.x, p5.y, R, math.radians(180), math.radians(-90))
        # The upper side
        p6 = wx.Point(p5.x + W - R, p5.y - R)
        top_y = p6.y
        path.AddLineToPoint(p6.x, p6.y)
        # The upper right rounded corner. p7 is the center of the arc
        p7 = wx.Point(p6.x, p6.y + R)
        path.AddArc(p7.x, p7.y, R, math.radians(-90), math.radians(0))
        # The right side
        p8 = wx.Point(p7.x + R , p7.y + H - R)
        right_x = p8.x
        path.AddLineToPoint(p8.x, p8.y)
        # The lower right rounded corner. p9 is the center of the arc
        p9 = wx.Point(p8.x - R, p8.y)
        path.AddArc(p9.x, p9.y, R, math.radians(0), math.radians(90))
        # The lower side
        p10 = wx.Point(p9.x - W + W_ARROW +  W_ARROW_OFFSET, p9.y + R)
        path.AddLineToPoint(p10.x, p10.y)
        path.CloseSubpath()
        # Draw sharp lines on GTK which uses Cairo
        # See: http://www.cairographics.org/FAQ/#sharp_lines
        gc.Translate(0.5, 0.5)
        # Draw the ballon
        BORDER_COLOR = wx.Colour(127, 127, 127)
        BG_COLOR = wx.Colour(255, 255, 231)
        PEN = wx.Pen(BORDER_COLOR, 1, wx.SOLID)
        BRUSH = wx.Brush(BG_COLOR, wx.SOLID)
        gc.SetPen(PEN)
        gc.SetBrush(BRUSH)
        gc.DrawPath(path)
        # Return
        return (left_x + BALLOON_RADIUS, top_y + BALLOON_RADIUS)

    def notify_events(self, notification, data):
        """
        Send notification to all visible events
        """
        for (event, rect) in self.event_data:
            event.notify(notification, data)