Source code for timelinelib.canvas.drawing.drawers.default

# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Timeline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Timeline.  If not, see <http://www.gnu.org/licenses/>.


import math
import os.path

import wx

from timelinelib.canvas.data import sort_categories
from timelinelib.canvas.data.timeperiod import TimePeriod
from timelinelib.canvas.drawing.drawers.dividerline import DividerLine
from timelinelib.canvas.drawing.drawers.legenddrawer import LegendDrawer
from timelinelib.canvas.drawing.drawers.minorstrip import MinorStripDrawer
from timelinelib.canvas.drawing.drawers.nowline import NowLine
from timelinelib.canvas.drawing.interface import Drawer
from timelinelib.canvas.drawing.scene import TimelineScene
from timelinelib.config.paths import ICONS_DIR
from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
from timelinelib.utils import unique_based_on_eq
from timelinelib.wxgui.components.font import Font
from wx import BRUSHSTYLE_TRANSPARENT
import timelinelib.wxgui.components.font as font
from timelinelib.canvas.drawing.drawers.ballondrawer import BallonDrawer


OUTER_PADDING = 5  # Space between event boxes (pixels)
INNER_PADDING = 3  # Space inside event box to text (pixels)
PERIOD_THRESHOLD = 20  # Periods smaller than this are drawn as events (pixels)
BALLOON_RADIUS = 12
ARROW_OFFSET = BALLOON_RADIUS + 25
DATA_INDICATOR_SIZE = 10
CONTRAST_RATIO_THREASHOLD = 2250
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)


[docs]class DefaultDrawingAlgorithm(Drawer):
[docs] def __init__(self): self._event_text_font = Font(8) self._create_pens() self._create_brushes() self._fixed_ys = {} self._do_draw_top_scale = False self._do_draw_bottom_scale = True self._do_draw_divider_line = False self._event_box_drawer = None self._background_drawer = None
[docs] def set_event_font(self, new_font): from timelinelib.wxgui.components.font import deserialize_font self._event_text_font = deserialize_font(new_font)
[docs] def set_event_box_drawer(self, event_box_drawer): self._event_box_drawer = event_box_drawer
[docs] def set_background_drawer(self, background_drawer): self._background_drawer = background_drawer
[docs] def increment_font_size(self, step=2): self._event_text_font.increment(step) self._adjust_outer_padding_to_font_size() return self._event_text_font.serialize()
[docs] def decrement_font_size(self, step=2): if self._event_text_font.GetPointSize() > step: self._event_text_font.decrement(step) self._adjust_outer_padding_to_font_size() return self._event_text_font.serialize()
def _adjust_outer_padding_to_font_size(self): if self._event_text_font.GetPointSize() < 8: self.outer_padding = OUTER_PADDING * self._event_text_font.GetPointSize() // 8 else: self.outer_padding = OUTER_PADDING def _create_pens(self): self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID) self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_SOLID) self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID) self.minor_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_USER_DASH) self.minor_strip_pen.SetDashes([2, 2]) self.minor_strip_pen.SetCap(wx.CAP_BUTT) self.major_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_SOLID) self.now_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID) self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID) def _create_brushes(self): self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.BRUSHSTYLE_SOLID) self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.BRUSHSTYLE_SOLID) self.red_solid_brush = wx.Brush(wx.Colour(255, 0, 0), wx.BRUSHSTYLE_SOLID) self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230), wx.BRUSHSTYLE_SOLID)
[docs] def event_is_period(self, time_period): period_width_in_pixels = self.scene.width_of_period(time_period) return period_width_in_pixels > PERIOD_THRESHOLD
def _get_text_extent(self, text): self.dc.SetFont(self._event_text_font) tw, th = self.dc.GetTextExtent(text) return tw, th
[docs] def get_closest_overlapping_event(self, event_to_move, up=True): return self.scene.get_closest_overlapping_event(event_to_move, up=up)
[docs] def draw(self, dc, timeline, view_properties, appearance, fast_draw=False): self.fast_draw = fast_draw view_properties.hide_events_done = appearance.get_hide_events_done() view_properties._legend_pos = appearance.get_legend_pos() view_properties._time_scale_pos = appearance.get_time_scale_pos() view_properties.set_fuzzy_icon(appearance.get_fuzzy_icon()) view_properties.set_locked_icon(appearance.get_locked_icon()) view_properties.set_hyperlink_icon(appearance.get_hyperlink_icon()) view_properties.set_skip_s_in_decade_text(appearance.get_skip_s_in_decade_text()) view_properties.set_display_checkmark_on_events_done(appearance.get_display_checkmark_on_events_done()) self.minor_strip_pen.SetColour(appearance.get_minor_strip_divider_line_colour()) self.major_strip_pen.SetColour(appearance.get_major_strip_divider_line_colour()) self.now_pen.SetColour(appearance.get_now_line_colour()) self.weekend_color = appearance.get_weekend_colour() self.bg_color = appearance.get_bg_colour() self.colorize_weekends = appearance.get_colorize_weekends() self.outer_padding = OUTER_PADDING self.outer_padding = appearance.get_vertical_space_between_events() if EXTENDED_CONTAINER_HEIGHT.enabled(): self.outer_padding += EXTENDED_CONTAINER_HEIGHT.get_extra_outer_padding_to_avoid_vertical_overlapping() self.appearance = appearance self.dc = dc self.time_type = timeline.get_time_type() self.scene = self._create_scene(dc.GetSize(), timeline, view_properties, self._get_text_extent) if view_properties.use_fixed_event_vertical_pos(): self._calc_fixed_event_rect_y(dc.GetSize(), timeline, view_properties, self._get_text_extent) else: self._fixed_ys = {} self._perform_drawing(timeline, view_properties) del self.dc # Program crashes if we don't delete the dc reference.
def _create_scene(self, size, db, view_properties, get_text_extent_fn): scene = TimelineScene(size, db, view_properties, get_text_extent_fn, self.appearance) scene.set_outer_padding(self.outer_padding) scene.set_inner_padding(INNER_PADDING) scene.set_period_threshold(PERIOD_THRESHOLD) scene.set_data_indicator_size(DATA_INDICATOR_SIZE) scene.create() return scene def _calc_fixed_event_rect_y(self, size, db, view_properties, get_text_extent_fn): periods = view_properties.periods view_properties.set_displayed_period(TimePeriod(periods[0].start_time, periods[-1].end_time), False) large_size = (size[0] * len(periods), size[1]) scene = self._create_scene(large_size, db, view_properties, get_text_extent_fn) for (evt, rect) in scene.event_data: self._fixed_ys[evt.id] = rect.GetY() def _perform_drawing(self, timeline, view_properties): self._background_drawer.draw( self, self.dc, self.scene, timeline, self.colorize_weekends, self.weekend_color, self.bg_color) if self.fast_draw: self._perform_fast_drawing(view_properties) else: self._perform_normal_drawing(view_properties) def _perform_fast_drawing(self, view_properties): self._draw_bg() self._draw_events(view_properties) self._draw_selection_rect(view_properties) def _draw_selection_rect(self, view_properties): if view_properties._selection_rect: self.dc.SetPen(wx.BLACK_PEN) self.dc.SetBrush(wx.Brush(wx.WHITE, style=BRUSHSTYLE_TRANSPARENT)) self.dc.DrawRectangle(*view_properties._selection_rect) def _perform_normal_drawing(self, view_properties): self._draw_period_selection(view_properties) self._draw_bg() self._draw_events(view_properties) self._draw_legend(view_properties, self._extract_categories()) self._draw_ballons(view_properties)
[docs] def snap(self, time, snap_region=10): if self._distance_to_left_border(time) < snap_region: return self._get_time_at_left_border(time) elif self._distance_to_right_border(time) < snap_region: return self._get_time_at_right_border(time) else: return time
def _distance_to_left_border(self, time): left_strip_time, _ = self._snap_region(time) return self.scene.distance_between_times(time, left_strip_time) def _distance_to_right_border(self, time): _, right_strip_time = self._snap_region(time) return self.scene.distance_between_times(time, right_strip_time) def _get_time_at_left_border(self, time): left_strip_time, _ = self._snap_region(time) return left_strip_time def _get_time_at_right_border(self, time): _, right_strip_time = self._snap_region(time) return right_strip_time def _snap_region(self, time): left_strip_time = self.scene.minor_strip.start(time) right_strip_time = self.scene.minor_strip.increment(left_strip_time) return left_strip_time, right_strip_time
[docs] def snap_selection(self, period_selection): start, end = period_selection return self.snap(start), self.snap(end)
[docs] def event_at(self, x, y, alt_down=False): container_event = None for (event, rect) in self.scene.event_data: if event.is_container(): rect = self._adjust_container_rect_for_hittest(rect) if rect.Contains(wx.Point(x, y)): self.set_horizontal_mouse_position_factor(event, x) if event.is_container(): if self.scene.view_properties.is_selected(event): return event container_event = event else: return event return container_event
[docs] def set_horizontal_mouse_position_factor(self, event, x): try: event_period_duration = self.scene.distance_between_times(event.time_period.end_time, event.time_period.start_time) mouse_duration = self.scene.distance_between_times(self.scene.get_time(x), event.time_period.start_time) event.horizontal_mouse_position_factor = mouse_duration / event_period_duration except ZeroDivisionError: event.horizontal_mouse_position_factor = None
[docs] def get_events_in_rect(self, rect): wx_rect = wx.Rect(*rect) return [event for (event, rect) in self.scene.event_data if rect.Intersects(wx_rect)]
def _adjust_container_rect_for_hittest(self, rect): if EXTENDED_CONTAINER_HEIGHT.enabled(): return EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect) else: return rect
[docs] def event_with_rect_at(self, x, y, view_properties=None): for (event, rect) in self.scene.event_data: if view_properties.is_selected(event): if rect.Contains(wx.Point(x, y)): if event.is_container(): return event, rect else: return event, rect return None
[docs] def event_rect(self, evt): for (event, rect) in self.scene.event_data: if evt == event: return rect return None
[docs] def balloon_at(self, x, y): event = None for (event_in_list, rect) in self.balloon_data: if rect.Contains(wx.Point(x, y)): event = event_in_list return event
[docs] def get_time(self, x): return self.scene.get_time(x)
[docs] def get_hidden_event_count(self): try: return self.scene.get_hidden_event_count() except AttributeError: return 0
def _draw_period_selection(self, view_properties): if not view_properties.period_selection: return start, end = view_properties.period_selection start_x = self.scene.x_pos_for_time(start) end_x = self.scene.x_pos_for_time(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.scene.height) def _draw_bg(self): if self.fast_draw: self._draw_fast_bg() else: self._draw_normal_bg() def _draw_fast_bg(self): self._draw_minor_strips() self._draw_divider_line() def _draw_normal_bg(self): self._draw_major_strips() self._draw_minor_strips() self._draw_divider_line() self._draw_now_line() def _draw_minor_strips(self): drawer = MinorStripDrawer(self) for strip_period in self.scene.minor_strip_data: label = self.scene.minor_strip.label(strip_period.start_time) drawer.draw(label, strip_period.start_time, strip_period.end_time) def _draw_major_strips(self): font.set_major_strip_text_font(self.appearance.get_major_strip_font(), self.dc) self.dc.SetPen(self.major_strip_pen) self._calculate_use_major_strip_vertical_label() for time_period in self.scene.major_strip_data: self._draw_major_strip_end_line(time_period) self._draw_major_strip_label(time_period) def _calculate_use_major_strip_vertical_label(self): if len(self.scene.major_strip_data) > 0: strip_period = self.scene.major_strip_data[0] label = self.scene.major_strip.label(strip_period.start_time, True) strip_width = self.scene.width_of_period(strip_period) tw, _ = self.dc.GetTextExtent(label) self.use_major_strip_vertical_label = strip_width < (tw + 5) else: self.use_major_strip_vertical_label = False def _draw_major_strip_end_line(self, time_period): x = self.scene.x_pos_for_time(time_period.end_time) self.dc.DrawLine(x, 0, x, self.scene.height) def _draw_major_strip_label(self, time_period): label = self.scene.major_strip.label(time_period.start_time, True) if self.use_major_strip_vertical_label: self._draw_major_strip_vertical_label(time_period, label) else: self._draw_major_strip_horizontal_label(time_period, label) def _draw_major_strip_vertical_label(self, time_period, label): x = self._calculate_major_strip_vertical_label_x(time_period, label) self.dc.DrawRotatedText(label, x, INNER_PADDING, -90) def _draw_major_strip_horizontal_label(self, time_period, label): x = self._calculate_major_strip_horizontal_label_x(time_period, label) self.dc.DrawText(label, x, INNER_PADDING) def _calculate_major_strip_horizontal_label_x(self, time_period, label): tw, _ = self.dc.GetTextExtent(label) x = self.scene.x_pos_for_time(time_period.mean_time()) - tw // 2 if x - INNER_PADDING < 0: x = INNER_PADDING right = self.scene.x_pos_for_time(time_period.end_time) if x + tw + INNER_PADDING > right: x = right - tw - INNER_PADDING elif x + tw + INNER_PADDING > self.scene.width: x = self.scene.width - tw - INNER_PADDING left = self.scene.x_pos_for_time(time_period.start_time) if x < left + INNER_PADDING: x = left + INNER_PADDING return x def _calculate_major_strip_vertical_label_x(self, time_period, label): _, th = self.dc.GetTextExtent(label) return self.scene.x_pos_for_time(time_period.mean_time()) + th // 2 def _draw_divider_line(self): DividerLine(self).draw() def _draw_lines_to_non_period_events(self, view_properties): for (event, rect) in self.scene.event_data: if event.is_milestone(): continue if not event.is_period(): self._draw_line(view_properties, event, rect) elif not self.scene.never_show_period_events_as_point_events() and self._event_displayed_as_point_event(rect): self._draw_line(view_properties, event, rect) def _event_displayed_as_point_event(self, rect): return self.scene.divider_y > rect.Y def _draw_line(self, view_properties, event, rect): if self.appearance.get_draw_period_events_to_right(): x = rect.X else: x = self.scene.x_pos_for_time(event.mean_time()) y = rect.Y + rect.Height y2 = self._get_end_of_line(event) self._set_line_color(view_properties, event) if event.is_period(): if self.appearance.get_draw_period_events_to_right(): x += 1 self.dc.DrawLine(x - 1, y, x - 1, y2) self.dc.DrawLine(x + 1, y, x + 1, y2) self.dc.DrawLine(x, y, x, y2) self._draw_endpoint(event, x, y2) def _draw_endpoint(self, event, x, y): if event.get_milestone(): size = 8 self.dc.SetBrush(wx.BLUE_BRUSH) self.dc.DrawPolygon([wx.Point(-size), wx.Point(0, -size), wx.Point(size, 0), wx.Point(0, size)], x, y) else: self.dc.DrawCircle(x, y, 2) def _get_end_of_line(self, event): # Lines are only drawn for events shown as point events and the line length # is only dependent on the fact that an event is a subevent or not if event.is_subevent(): y = self._get_container_y(event) else: y = self.scene.divider_y return y def _get_container_y(self, subevent): for (event, rect) in self.scene.event_data: if event.is_container(): if event is subevent.container: return rect.y - 1 return self.scene.divider_y def _set_line_color(self, view_properties, event): if view_properties.is_selected(event): self.dc.SetPen(self.red_solid_pen) self.dc.SetBrush(self.red_solid_brush) else: self.dc.SetBrush(self.black_solid_brush) self.dc.SetPen(self.black_solid_pen) def _draw_now_line(self): NowLine(self).draw() def _extract_categories(self): return sort_categories(unique_based_on_eq( event.category for (event, _) in self.scene.event_data if event.category )) def _draw_legend(self, view_properties, categories): if self._legend_should_be_drawn(categories): LegendDrawer(self.dc, self.scene, categories).draw() def _legend_should_be_drawn(self, categories): return self.appearance.get_legend_visible() and len(categories) > 0 def _scroll_events_vertically(self, view_properties): collection = [] amount = view_properties.hscroll_amount if amount != 0: for (event, rect) in self.scene.event_data: if rect.Y < self.scene.divider_y: self._scroll_point_events(amount, event, rect, collection) else: self._scroll_period_events(amount, event, rect, collection) self.scene.event_data = collection def _scroll_point_events(self, amount, event, rect, collection): rect.Y += amount if rect.Y < self.scene.divider_y - rect.height: collection.append((event, rect)) def _scroll_period_events(self, amount, event, rect, collection): rect.Y -= amount if rect.Y > self.scene.divider_y + rect.height: collection.append((event, rect)) def _draw_events(self, view_properties): """Draw all event boxes and the text inside them.""" self._scroll_events_vertically(view_properties) self.dc.DestroyClippingRegion() self._draw_lines_to_non_period_events(view_properties) for (event, rect) in self.scene.event_data: self.dc.SetFont(self._event_text_font) if view_properties.use_fixed_event_vertical_pos(): rect.SetY(self._fixed_ys[event.id]) if event.is_container(): self._draw_container(event, rect, view_properties) else: self._draw_box(rect, event, view_properties) def _draw_container(self, event, rect, view_properties): box_rect = wx.Rect(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4) if EXTENDED_CONTAINER_HEIGHT.enabled(): box_rect = EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect) self._draw_box(box_rect, event, view_properties) def _draw_box(self, rect, event, view_properties): self.dc.SetClippingRegion(rect) self._event_box_drawer.draw(self.dc, self.scene, rect, event, view_properties) self.dc.DestroyClippingRegion() def _draw_ballons(self, view_properties): """Draw ballons on selected events that has 'description' data.""" self.balloon_data = [] # List of (event, rect) top_event = None top_rect = None self.dc.SetTextForeground(BLACK) for (event, rect) in self.scene.event_data: if event.get_data("description") is not None or event.get_data("icon") is not None: sticky = view_properties.event_has_sticky_balloon(event) if view_properties.event_is_hovered(event) or sticky: if not sticky: top_event, top_rect = event, rect self._draw_ballon(event, rect, sticky) # Make the unsticky balloon appear on top if top_event is not None: self._draw_ballon(top_event, top_rect, False) def _draw_ballon(self, event, event_rect, sticky): """Draw one ballon on a selected event that has 'description' data.""" ballon_drawer = BallonDrawer(self.dc, self.scene, self.appearance, event) self.balloon_data.append(ballon_drawer.draw(event_rect, sticky))
[docs] def get_period_xpos(self, time_period): w, _ = self.dc.GetSize() return (max(0, self.scene.x_pos_for_time(time_period.start_time)), min(w, self.scene.x_pos_for_time(time_period.end_time)))
[docs] def period_is_visible(self, time_period): w, _ = self.dc.GetSize() return (self.scene.x_pos_for_time(time_period.start_time) < w and self.scene.x_pos_for_time(time_period.end_time) > 0)