Gnumed.timelinelib.canvas.drawing.drawers.default

Package Gnumed :: Package timelinelib :: Package canvas :: Package drawing :: Package drawers :: Module default |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.canvas.drawing.drawers.default

      1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  import math
     20  import os.path
     21
     22  import wx
     23
     24  from timelinelib.canvas.data import sort_categories
     25  from timelinelib.canvas.data.timeperiod import TimePeriod
     26  from timelinelib.canvas.drawing.drawers.dividerline import DividerLine
     27  from timelinelib.canvas.drawing.drawers.legenddrawer import LegendDrawer
     28  from timelinelib.canvas.drawing.drawers.minorstrip import MinorStripDrawer
     29  from timelinelib.canvas.drawing.drawers.nowline import NowLine
     30  from timelinelib.canvas.drawing.interface import Drawer
     31  from timelinelib.canvas.drawing.scene import TimelineScene
     32  from timelinelib.config.paths import ICONS_DIR
     33  from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
     34  from timelinelib.utils import unique_based_on_eq
     35  from timelinelib.wxgui.components.font import Font
     36  from wx import BRUSHSTYLE_TRANSPARENT
     37  import timelinelib.wxgui.components.font as font
     38
     39
     40  OUTER_PADDING = 5  # Space between event boxes (pixels)
     41  INNER_PADDING = 3  # Space inside event box to text (pixels)
     42  PERIOD_THRESHOLD = 20  # Periods smaller than this are drawn as events (pixels)
     43  BALLOON_RADIUS = 12
     44  ARROW_OFFSET = BALLOON_RADIUS + 25
     45  DATA_INDICATOR_SIZE = 10
     46  CONTRAST_RATIO_THREASHOLD = 2250
     47  WHITE = (255, 255, 255)
     48  BLACK = (0, 0, 0)
     49
     50
     51 -class DefaultDrawingAlgorithm(Drawer):
     52
     53 -    def __init__(self):
     54          self.event_text_font = Font(8)
     55          self._create_pens()
     56          self._create_brushes()
     57          self._fixed_ys = {}
     58          self._do_draw_top_scale = False
     59          self._do_draw_bottom_scale = True
     60          self._do_draw_divider_line = False
     61
     62 -    def set_event_box_drawer(self, event_box_drawer):
     63          self.event_box_drawer = event_box_drawer
     64
     65 -    def set_background_drawer(self, background_drawer):
     66          self.background_drawer = background_drawer
     67
     68 -    def increment_font_size(self, step=2):
     69          self.event_text_font.increment(step)
     70          self._adjust_outer_padding_to_font_size()
     71
     72 -    def decrement_font_size(self, step=2):
     73          if self.event_text_font.PointSize > step:
     74              self.event_text_font.decrement(step)
     75              self._adjust_outer_padding_to_font_size()
     76
     77 -    def _adjust_outer_padding_to_font_size(self):
     78          if self.event_text_font.PointSize < 8:
     79              self.outer_padding = OUTER_PADDING * self.event_text_font.PointSize // 8
     80          else:
     81              self.outer_padding = OUTER_PADDING
     82
     83 -    def _create_pens(self):
     84          self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID)
     85          self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_SOLID)
     86          self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID)
     87          self.minor_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_USER_DASH)
     88          self.minor_strip_pen.SetDashes([2, 2])
     89          self.minor_strip_pen.SetCap(wx.CAP_BUTT)
     90          self.major_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_SOLID)
     91          self.now_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID)
     92          self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID)
     93
     94 -    def _create_brushes(self):
     95          self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.BRUSHSTYLE_SOLID)
     96          self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.BRUSHSTYLE_SOLID)
     97          self.red_solid_brush = wx.Brush(wx.Colour(255, 0, 0), wx.BRUSHSTYLE_SOLID)
     98          self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230), wx.BRUSHSTYLE_SOLID)
     99
    100 -    def event_is_period(self, time_period):
    101          period_width_in_pixels = self.scene.width_of_period(time_period)
    102          return period_width_in_pixels > PERIOD_THRESHOLD
    103
    104 -    def _get_text_extent(self, text):
    105          self.dc.SetFont(self.event_text_font)
    106          tw, th = self.dc.GetTextExtent(text)
    107          return (tw, th)
    108
    109 -    def get_closest_overlapping_event(self, event_to_move, up=True):
    110          return self.scene.get_closest_overlapping_event(event_to_move, up=up)
    111
    112 -    def draw(self, dc, timeline, view_properties, appearance, fast_draw=False):
    113          self.fast_draw = fast_draw
    114          view_properties.hide_events_done = appearance.get_hide_events_done()
    115          view_properties._legend_pos = appearance.get_legend_pos()
    116          view_properties._time_scale_pos = appearance.get_time_scale_pos()
    117          view_properties.set_fuzzy_icon(appearance.get_fuzzy_icon())
    118          view_properties.set_locked_icon(appearance.get_locked_icon())
    119          view_properties.set_hyperlink_icon(appearance.get_hyperlink_icon())
    120          view_properties.set_skip_s_in_decade_text(appearance.get_skip_s_in_decade_text())
    121          view_properties.set_display_checkmark_on_events_done(appearance.get_display_checkmark_on_events_done())
    122          self.minor_strip_pen.SetColour(appearance.get_minor_strip_divider_line_colour())
    123          self.major_strip_pen.SetColour(appearance.get_major_strip_divider_line_colour())
    124          self.now_pen.SetColour(appearance.get_now_line_colour())
    125          self.weekend_color = appearance.get_weekend_colour()
    126          self.bg_color = appearance.get_bg_colour()
    127          self.colorize_weekends = appearance.get_colorize_weekends()
    128          self.outer_padding = OUTER_PADDING
    129          self.outer_padding = appearance.get_vertical_space_between_events()
    130          if EXTENDED_CONTAINER_HEIGHT.enabled():
    131              self.outer_padding += EXTENDED_CONTAINER_HEIGHT.get_extra_outer_padding_to_avoid_vertical_overlapping()
    132          self.appearance = appearance
    133          self.dc = dc
    134          self.time_type = timeline.get_time_type()
    135          self.scene = self._create_scene(dc.GetSize(), timeline, view_properties, self._get_text_extent)
    136          if view_properties.use_fixed_event_vertical_pos():
    137              self._calc_fixed_event_rect_y(dc.GetSize(), timeline, view_properties, self._get_text_extent)
    138          else:
    139              self._fixed_ys = {}
    140          self._perform_drawing(timeline, view_properties)
    141          del self.dc  # Program crashes if we don't delete the dc reference.
    142
    143 -    def _create_scene(self, size, db, view_properties, get_text_extent_fn):
    144          scene = TimelineScene(size, db, view_properties, get_text_extent_fn, self.appearance)
    145          scene.set_outer_padding(self.outer_padding)
    146          scene.set_inner_padding(INNER_PADDING)
    147          scene.set_period_threshold(PERIOD_THRESHOLD)
    148          scene.set_data_indicator_size(DATA_INDICATOR_SIZE)
    149          scene.create()
    150          return scene
    151
    152 -    def _calc_fixed_event_rect_y(self, size, db, view_properties, get_text_extent_fn):
    153          periods = view_properties.periods
    154          view_properties.set_displayed_period(TimePeriod(periods[0].start_time, periods[-1].end_time), False)
    155          large_size = (size[0] * len(periods), size[1])
    156          scene = self._create_scene(large_size, db, view_properties, get_text_extent_fn)
    157          for (evt, rect) in scene.event_data:
    158              self._fixed_ys[evt.id] = rect.GetY()
    159
    160 -    def _perform_drawing(self, timeline, view_properties):
    161          self.background_drawer.draw(
    162              self, self.dc, self.scene, timeline, self.colorize_weekends, self.weekend_color, self.bg_color)
    163          if self.fast_draw:
    164              self._perform_fast_drawing(view_properties)
    165          else:
    166              self._perform_normal_drawing(view_properties)
    167
    168 -    def _perform_fast_drawing(self, view_properties):
    169          self._draw_bg()
    170          self._draw_events(view_properties)
    171          self._draw_selection_rect(view_properties)
    172
    173 -    def _draw_selection_rect(self, view_properties):
    174          if view_properties._selection_rect:
    175              self.dc.SetPen(wx.BLACK_PEN)
    176              self.dc.SetBrush(wx.Brush(wx.WHITE, style=BRUSHSTYLE_TRANSPARENT))
    177              self.dc.DrawRectangle(*view_properties._selection_rect)
    178
    179 -    def _perform_normal_drawing(self, view_properties):
    180          self._draw_period_selection(view_properties)
    181          self._draw_bg()
    182          self._draw_events(view_properties)
    183          self._draw_legend(view_properties, self._extract_categories())
    184          self._draw_ballons(view_properties)
    185
    186 -    def snap(self, time, snap_region=10):
    187          if self._distance_to_left_border(time) < snap_region:
    188              return self._get_time_at_left_border(time)
    189          elif self._distance_to_right_border(time) < snap_region:
    190              return self._get_time_at_right_border(time)
    191          else:
    192              return time
    193
    194 -    def _distance_to_left_border(self, time):
    195          left_strip_time, _ = self._snap_region(time)
    196          return self.scene.distance_between_times(time, left_strip_time)
    197
    198 -    def _distance_to_right_border(self, time):
    199          _, right_strip_time = self._snap_region(time)
    200          return self.scene.distance_between_times(time, right_strip_time)
    201
    202 -    def _get_time_at_left_border(self, time):
    203          left_strip_time, _ = self._snap_region(time)
    204          return left_strip_time
    205
    206 -    def _get_time_at_right_border(self, time):
    207          _, right_strip_time = self._snap_region(time)
    208          return right_strip_time
    209
    210 -    def _snap_region(self, time):
    211          left_strip_time = self.scene.minor_strip.start(time)
    212          right_strip_time = self.scene.minor_strip.increment(left_strip_time)
    213          return (left_strip_time, right_strip_time)
    214
    215 -    def snap_selection(self, period_selection):
    216          start, end = period_selection
    217          return (self.snap(start), self.snap(end))
    218
    219 -    def event_at(self, x, y, alt_down=False):
    220          container_event = None
    221          for (event, rect) in self.scene.event_data:
    222              if event.is_container():
    223                  rect = self._adjust_container_rect_for_hittest(rect)
    224              if rect.Contains(wx.Point(x, y)):
    225                  if event.is_container():
    226                      if alt_down:
    227                          return event
    228                      container_event = event
    229                  else:
    230                      return event
    231          return container_event
    232
    233 -    def get_events_in_rect(self, rect):
    234          wx_rect = wx.Rect(*rect)
    235          return [event for (event, rect) in self.scene.event_data if rect.Intersects(wx_rect)]
    236
    237 -    def _adjust_container_rect_for_hittest(self, rect):
    238          if EXTENDED_CONTAINER_HEIGHT.enabled():
    239              return EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect)
    240          else:
    241              return rect
    242
    243 -    def event_with_rect_at(self, x, y, alt_down=False):
    244          container_event = None
    245          container_rect = None
    246          for (event, rect) in self.scene.event_data:
    247              if rect.Contains(wx.Point(x, y)):
    248                  if event.is_container():
    249                      if alt_down:
    250                          return event, rect
    251                      container_event = event
    252                      container_rect = rect
    253                  else:
    254                      return event, rect
    255          if container_event is None:
    256              return None
    257          return container_event, container_rect
    258
    259 -    def event_rect(self, evt):
    260          for (event, rect) in self.scene.event_data:
    261              if evt == event:
    262                  return rect
    263          return None
    264
    265 -    def balloon_at(self, x, y):
    266          event = None
    267          for (event_in_list, rect) in self.balloon_data:
    268              if rect.Contains(wx.Point(x, y)):
    269                  event = event_in_list
    270          return event
    271
    272 -    def get_time(self, x):
    273          return self.scene.get_time(x)
    274
    275 -    def get_hidden_event_count(self):
    276          try:
    277              return self.scene.get_hidden_event_count()
    278          except AttributeError:
    279              return 0
    280
    281 -    def _draw_period_selection(self, view_properties):
    282          if not view_properties.period_selection:
    283              return
    284          start, end = view_properties.period_selection
    285          start_x = self.scene.x_pos_for_time(start)
    286          end_x = self.scene.x_pos_for_time(end)
    287          self.dc.SetBrush(self.lightgrey_solid_brush)
    288          self.dc.SetPen(wx.TRANSPARENT_PEN)
    289          self.dc.DrawRectangle(start_x, 0, end_x - start_x + 1, self.scene.height)
    290
    291 -    def _draw_bg(self):
    292          if self.fast_draw:
    293              self._draw_fast_bg()
    294          else:
    295              self._draw_normal_bg()
    296
    297 -    def _draw_fast_bg(self):
    298          self._draw_minor_strips()
    299          self._draw_divider_line()
    300
    301 -    def _draw_normal_bg(self):
    302          self._draw_major_strips()
    303          self._draw_minor_strips()
    304          self._draw_divider_line()
    305          self._draw_now_line()
    306
    307 -    def _draw_minor_strips(self):
    308          drawer = MinorStripDrawer(self)
    309          for strip_period in self.scene.minor_strip_data:
    310              label = self.scene.minor_strip.label(strip_period.start_time)
    311              drawer.draw(label, strip_period.start_time, strip_period.end_time)
    312              #self._draw_minor_strip_divider_line_at(strip_period.end_time)
    313              #self._draw_minor_strip_label(strip_period)
    314
    315  #     def _draw_minor_strip_divider_line_at(self, time):
    316  #         x = self.scene.x_pos_for_time(time)
    317  #         self.dc.SetPen(self.minor_strip_pen)
    318  #         self.dc.DrawLine(x, 0, x, self.scene.height)
    319  #
    320  #     def _draw_minor_strip_label(self, strip_period):
    321  #         label = self.scene.minor_strip.label(strip_period.start_time)
    322  #         self._set_minor_strip_font(strip_period)
    323  #         (tw, th) = self.dc.GetTextExtent(label)
    324  #         start_x = self.scene.x_pos_for_time(strip_period.get_start_time())
    325  #         end_x = self.scene.x_pos_for_time(strip_period.get_end_time())
    326  #         middle = (start_x + end_x) / 2
    327  #         if self._do_draw_divider_line:
    328  #             middley = self.scene.divider_y
    329  #             self.dc.DrawText(label, middle - tw / 2, middley - th)
    330  #         if self._do_draw_bottom_scale:
    331  #             middley = self.scene.height
    332  #             self.dc.DrawText(label, middle - tw / 2, middley - th)
    333  #
    334  #     def _set_minor_strip_font(self, strip_period):
    335  #         if self.scene.minor_strip_is_day():
    336  #             bold = False
    337  #             italic = False
    338  #             if self.time_type.is_weekend_day(strip_period.start_time):
    339  #                 bold = True
    340  #             if self.time_type.is_special_day(strip_period.start_time):
    341  #                 italic = True
    342  #             font.set_minor_strip_text_font(self.appearance.get_minor_strip_font(), self.dc,
    343  #                                            force_bold=bold, force_normal=not bold, force_italic=italic, force_upright=not italic)
    344  #         else:
    345  #             font.set_minor_strip_text_font(self.appearance.get_minor_strip_font(), self.dc)
    346
    347 -    def _draw_major_strips(self):
    348          font.set_major_strip_text_font(self.appearance.get_major_strip_font(), self.dc)
    349          self.dc.SetPen(self.major_strip_pen)
    350          self._calculate_use_major_strip_vertical_label()
    351          for time_period in self.scene.major_strip_data:
    352              self._draw_major_strip_end_line(time_period)
    353              self._draw_major_strip_label(time_period)
    354
    355 -    def _calculate_use_major_strip_vertical_label(self):
    356          if len(self.scene.major_strip_data) > 0:
    357              strip_period = self.scene.major_strip_data[0]
    358              label = self.scene.major_strip.label(strip_period.start_time, True)
    359              strip_width = self.scene.width_of_period(strip_period)
    360              tw, _ = self.dc.GetTextExtent(label)
    361              self.use_major_strip_vertical_label = strip_width < (tw + 5)
    362          else:
    363              self.use_major_strip_vertical_label = False
    364
    365 -    def _draw_major_strip_end_line(self, time_period):
    366          x = self.scene.x_pos_for_time(time_period.end_time)
    367          self.dc.DrawLine(x, 0, x, self.scene.height)
    368
    369 -    def _draw_major_strip_label(self, time_period):
    370          label = self.scene.major_strip.label(time_period.start_time, True)
    371          if self.use_major_strip_vertical_label:
    372              self._draw_major_strip_vertical_label(time_period, label)
    373          else:
    374              self._draw_major_strip_horizontal_label(time_period, label)
    375
    376 -    def _draw_major_strip_vertical_label(self, time_period, label):
    377          x = self._calculate_major_strip_vertical_label_x(time_period, label)
    378          self.dc.DrawRotatedText(label, x, INNER_PADDING, -90)
    379
    380 -    def _draw_major_strip_horizontal_label(self, time_period, label):
    381          x = self._calculate_major_strip_horizontal_label_x(time_period, label)
    382          self.dc.DrawText(label, x, INNER_PADDING)
    383
    384 -    def _calculate_major_strip_horizontal_label_x(self, time_period, label):
    385          tw, _ = self.dc.GetTextExtent(label)
    386          x = self.scene.x_pos_for_time(time_period.mean_time()) - tw // 2
    387          if x - INNER_PADDING < 0:
    388              x = INNER_PADDING
    389              right = self.scene.x_pos_for_time(time_period.end_time)
    390              if x + tw + INNER_PADDING > right:
    391                  x = right - tw - INNER_PADDING
    392          elif x + tw + INNER_PADDING > self.scene.width:
    393              x = self.scene.width - tw - INNER_PADDING
    394              left = self.scene.x_pos_for_time(time_period.start_time)
    395              if x < left + INNER_PADDING:
    396                  x = left + INNER_PADDING
    397          return x
    398
    399 -    def _calculate_major_strip_vertical_label_x(self, time_period, label):
    400          _, th = self.dc.GetTextExtent(label)
    401          return self.scene.x_pos_for_time(time_period.mean_time()) + th // 2
    402
    403 -    def _draw_divider_line(self):
    404          DividerLine(self).draw()
    405
    406 -    def _draw_lines_to_non_period_events(self, view_properties):
    407          for (event, rect) in self.scene.event_data:
    408              if event.is_milestone():
    409                  continue
    410              if not event.is_period():
    411                  self._draw_line(view_properties, event, rect)
    412              elif not self.scene.never_show_period_events_as_point_events() and self._event_displayed_as_point_event(rect):
    413                  self._draw_line(view_properties, event, rect)
    414
    415 -    def _event_displayed_as_point_event(self, rect):
    416          return self.scene.divider_y > rect.Y
    417
    418 -    def _draw_line(self, view_properties, event, rect):
    419          if self.appearance.get_draw_period_events_to_right():
    420              x = rect.X
    421          else:
    422              x = self.scene.x_pos_for_time(event.mean_time())
    423          y = rect.Y + rect.Height
    424          y2 = self._get_end_of_line(event)
    425          self._set_line_color(view_properties, event)
    426          if event.is_period():
    427              if self.appearance.get_draw_period_events_to_right():
    428                  x += 1
    429              self.dc.DrawLine(x - 1, y, x - 1, y2)
    430              self.dc.DrawLine(x + 1, y, x + 1, y2)
    431          self.dc.DrawLine(x, y, x, y2)
    432          self._draw_endpoint(event, x, y2)
    433
    434 -    def _draw_endpoint(self, event, x, y):
    435          if event.get_milestone():
    436              size = 8
    437              self.dc.SetBrush(wx.BLUE_BRUSH)
    438              self.dc.DrawPolygon([wx.Point(-size),
    439                                   wx.Point(0, -size),
    440                                   wx.Point(size, 0),
    441                                   wx.Point(0, size)], x, y)
    442          else:
    443              self.dc.DrawCircle(x, y, 2)
    444
    445 -    def _get_end_of_line(self, event):
    446          # Lines are only drawn for events shown as point events and the line length
    447          # is only dependent on the fact that an event is a subevent or not
    448          if event.is_subevent():
    449              y = self._get_container_y(event)
    450          else:
    451              y = self.scene.divider_y
    452          return y
    453
    454 -    def _get_container_y(self, subevent):
    455          for (event, rect) in self.scene.event_data:
    456              if event.is_container():
    457                  if event is subevent.container:
    458                      return rect.y - 1
    459          return self.scene.divider_y
    460
    461 -    def _set_line_color(self, view_properties, event):
    462          if view_properties.is_selected(event):
    463              self.dc.SetPen(self.red_solid_pen)
    464              self.dc.SetBrush(self.red_solid_brush)
    465          else:
    466              self.dc.SetBrush(self.black_solid_brush)
    467              self.dc.SetPen(self.black_solid_pen)
    468
    469 -    def _draw_now_line(self):
    470          NowLine(self).draw()
    471
    472 -    def _extract_categories(self):
    473          return sort_categories(unique_based_on_eq(
    474              event.category
    475              for (event, _) in self.scene.event_data
    476              if event.category
    477          ))
    478
    479 -    def _draw_legend(self, view_properties, categories):
    480          if self._legend_should_be_drawn(categories):
    481              LegendDrawer(self.dc, self.scene, categories).draw()
    482
    483 -    def _legend_should_be_drawn(self, categories):
    484          return self.appearance.get_legend_visible() and len(categories) > 0
    485
    486 -    def _scroll_events_vertically(self, view_properties):
    487          collection = []
    488          amount = view_properties.hscroll_amount
    489          if amount != 0:
    490              for (event, rect) in self.scene.event_data:
    491                  if rect.Y < self.scene.divider_y:
    492                      self._scroll_point_events(amount, event, rect, collection)
    493                  else:
    494                      self._scroll_period_events(amount, event, rect, collection)
    495              self.scene.event_data = collection
    496
    497 -    def _scroll_point_events(self, amount, event, rect, collection):
    498          rect.Y += amount
    499          if rect.Y < self.scene.divider_y - rect.height:
    500              collection.append((event, rect))
    501
    502 -    def _scroll_period_events(self, amount, event, rect, collection):
    503          rect.Y -= amount
    504          if rect.Y > self.scene.divider_y + rect.height:
    505              collection.append((event, rect))
    506
    507 -    def _draw_events(self, view_properties):
    508          """Draw all event boxes and the text inside them."""
    509          self._scroll_events_vertically(view_properties)
    510          self.dc.DestroyClippingRegion()
    511          self._draw_lines_to_non_period_events(view_properties)
    512          for (event, rect) in self.scene.event_data:
    513              self.dc.SetFont(self.event_text_font)
    514              if view_properties.use_fixed_event_vertical_pos():
    515                  rect.SetY(self._fixed_ys[event.id])
    516              if event.is_container():
    517                  self._draw_container(event, rect, view_properties)
    518              else:
    519                  self._draw_box(rect, event, view_properties)
    520
    521 -    def _draw_container(self, event, rect, view_properties):
    522          box_rect = wx.Rect(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4)
    523          if EXTENDED_CONTAINER_HEIGHT.enabled():
    524              box_rect = EXTENDED_CONTAINER_HEIGHT.get_vertical_larger_box_rect(rect)
    525          self._draw_box(box_rect, event, view_properties)
    526
    527 -    def _draw_box(self, rect, event, view_properties):
    528          self.dc.SetClippingRegion(rect)
    529          self.event_box_drawer.draw(self.dc, self.scene, rect, event, view_properties)
    530          self.dc.DestroyClippingRegion()
    531
    532 -    def _draw_ballons(self, view_properties):
    533          """Draw ballons on selected events that has 'description' data."""
    534          self.balloon_data = []  # List of (event, rect)
    535          top_event = None
    536          top_rect = None
    537          self.dc.SetTextForeground(BLACK)
    538          for (event, rect) in self.scene.event_data:
    539              if (event.get_data("description") is not None or event.get_data("icon") is not None):
    540                  sticky = view_properties.event_has_sticky_balloon(event)
    541                  if (view_properties.event_is_hovered(event) or sticky):
    542                      if not sticky:
    543                          top_event, top_rect = event, rect
    544                      self._draw_ballon(event, rect, sticky)
    545          # Make the unsticky balloon appear on top
    546          if top_event is not None:
    547              self._draw_ballon(top_event, top_rect, False)
    548
    549 -    def _draw_ballon(self, event, event_rect, sticky):
    550          """Draw one ballon on a selected event that has 'description' data."""
    551
    552          def max_text_width(icon_width):
    553              MIN_TEXT_WIDTH = 200
    554              SLIDER_WIDTH = 20
    555              padding = 2 * BALLOON_RADIUS
    556              if icon_width > 0:
    557                  padding += BALLOON_RADIUS
    558              else:
    559                  icon_width = 0
    560              padding += icon_width
    561              visble_background = self.scene.width - SLIDER_WIDTH
    562              balloon_width = visble_background - event_rect.X - event_rect.width // 2 + ARROW_OFFSET
    563              max_text_width = balloon_width - padding
    564              return max(MIN_TEXT_WIDTH, max_text_width)
    565
    566          def get_icon_size():
    567              (iw, ih) = (0, 0)
    568              icon = event.get_data("icon")
    569              if icon is not None:
    570                  (iw, ih) = icon.Size
    571              return (iw, ih)
    572
    573          def draw_lines(lines, x, y):
    574              font_h = self.dc.GetCharHeight()
    575              ty = y
    576              for line in lines:
    577                  self.dc.DrawText(line, x, ty)
    578                  ty += font_h
    579
    580          def adjust_text_x_pos_when_icon_is_present(x):
    581              icon = event.get_data("icon")
    582              (iw, _) = get_icon_size()
    583              if icon is not None:
    584                  return x + iw + BALLOON_RADIUS
    585              else:
    586                  return x
    587
    588          def draw_icon(x, y):
    589              icon = event.get_data("icon")
    590              if icon is not None:
    591                  self.dc.DrawBitmap(icon, x, y, False)
    592
    593          def draw_description(lines, x, y):
    594              if self.appearance.get_text_below_icon():
    595                  iw, ih = get_icon_size()
    596                  if ih > 0:
    597                      ih += BALLOON_RADIUS // 2
    598                  x -= iw
    599                  y += ih
    600              if lines is not None:
    601                  x = adjust_text_x_pos_when_icon_is_present(x)
    602                  draw_lines(lines, x, y)
    603
    604          def get_description_lines(max_text_width, iw):
    605              description = event.get_data("description")
    606              if description is not None:
    607                  return break_text(description, self.dc, max_text_width)
    608
    609          def calc_inner_rect(w, h, max_text_width):
    610              th = len(lines) * self.dc.GetCharHeight()
    611              tw = 0
    612              for line in lines:
    613                  (lw, _) = self.dc.GetTextExtent(line)
    614                  tw = max(lw, tw)
    615              if event.get_data("icon") is not None:
    616                  w += BALLOON_RADIUS
    617              w += min(tw, max_text_width)
    618              h = max(h, th)
    619              if self.appearance.get_text_below_icon():
    620                  iw, ih = get_icon_size()
    621                  w -= iw
    622                  h = ih + th
    623              return w, h
    624
    625          (inner_rect_w, inner_rect_h) = (iw, _) = get_icon_size()
    626          font.set_balloon_text_font(self.appearance.get_balloon_font(), self.dc)
    627          max_text_width = max_text_width(iw)
    628          lines = get_description_lines(max_text_width, iw)
    629          if lines is not None:
    630              inner_rect_w, inner_rect_h = calc_inner_rect(inner_rect_w, inner_rect_h, max_text_width)
    631          MIN_WIDTH = 100
    632          inner_rect_w = max(MIN_WIDTH, inner_rect_w)
    633          bounding_rect, x, y = self._draw_balloon_bg(self.dc, (inner_rect_w, inner_rect_h),
    634                                                      (event_rect.X + event_rect.Width // 2, event_rect.Y), True, sticky)
    635          draw_icon(x, y)
    636          draw_description(lines, x, y)
    637          # Write data so we know where the balloon was drawn
    638          # Following two lines can be used when debugging the rectangle
    639          # self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
    640          # self.dc.DrawRectangle(bounding_rect)
    641          self.balloon_data.append((event, bounding_rect))
    642
    643 -    def _draw_balloon_bg(self, dc, inner_size, tip_pos, above, sticky):
    644          """
    645          Draw the balloon background leaving inner_size for content.
    646
    647          tip_pos determines where the tip of the ballon should be.
    648
    649          above determines if the balloon should be above the tip (True) or below
    650          (False). This is not currently implemented.
    651
    652                      W
    653             |----------------|
    654               ______________           _
    655              /              \          |             R = Corner Radius
    656             |                |         |            AA = Left Arrow-leg angle
    657             |  W_ARROW       |         |  H     MARGIN = Text margin
    658             |     |--|       |         |             * = Starting point
    659              \____    ______/          _
    660                  /  /                  |
    661                 /_/                    |  H_ARROW
    662                *                       -
    663             |----|
    664             ARROW_OFFSET
    665
    666          Calculation of points starts at the tip of the arrow and continues
    667          clockwise around the ballon.
    668
    669          Return (bounding_rect, x, y) where x and y is at top of inner region.
    670          """
    671          # Prepare path object
    672          gc = wx.GraphicsContext.Create(self.dc)
    673          path = gc.CreatePath()
    674          # Calculate path
    675          R = BALLOON_RADIUS
    676          W = 1 * R + inner_size[0]
    677          H = 1 * R + inner_size[1]
    678          H_ARROW = 14
    679          W_ARROW = 15
    680          AA = 20
    681          # Starting point at the tip of the arrow
    682          (tipx, tipy) = tip_pos
    683          p0 = wx.Point(tipx, tipy)
    684          path.MoveToPoint(p0.x, p0.y)
    685          # Next point is the left base of the arrow
    686          p1 = wx.Point(p0.x + H_ARROW * math.tan(math.radians(AA)),
    687                        p0.y - H_ARROW)
    688          path.AddLineToPoint(p1.x, p1.y)
    689          # Start of lower left rounded corner
    690          p2 = wx.Point(p1.x - ARROW_OFFSET + R, p1.y)
    691          path.AddLineToPoint(p2.x, p2.y)
    692          # The lower left rounded corner. p3 is the center of the arc
    693          p3 = wx.Point(p2.x, p2.y - R)
    694          path.AddArc(p3.x, p3.y, R, math.radians(90), math.radians(180), True)
    695          # The left side
    696          p4 = wx.Point(p3.x - R, p3.y - H + R)
    697          left_x = p4.x
    698          path.AddLineToPoint(p4.x, p4.y)
    699          # The upper left rounded corner. p5 is the center of the arc
    700          p5 = wx.Point(p4.x + R, p4.y)
    701          path.AddArc(p5.x, p5.y, R, math.radians(180), math.radians(-90), True)
    702          # The upper side
    703          p6 = wx.Point(p5.x + W - R, p5.y - R)
    704          top_y = p6.y
    705          path.AddLineToPoint(p6.x, p6.y)
    706          # The upper right rounded corner. p7 is the center of the arc
    707          p7 = wx.Point(p6.x, p6.y + R)
    708          path.AddArc(p7.x, p7.y, R, math.radians(-90), math.radians(0), True)
    709          # The right side
    710          p8 = wx.Point(p7.x + R, p7.y + H - R)
    711          path.AddLineToPoint(p8.x, p8.y)
    712          # The lower right rounded corner. p9 is the center of the arc
    713          p9 = wx.Point(p8.x - R, p8.y)
    714          path.AddArc(p9.x, p9.y, R, math.radians(0), math.radians(90), True)
    715          # The lower side
    716          p10 = wx.Point(p9.x - W + W_ARROW + ARROW_OFFSET, p9.y + R)
    717          path.AddLineToPoint(p10.x, p10.y)
    718          path.CloseSubpath()
    719          # Draw sharp lines on GTK which uses Cairo
    720          # See: http://www.cairographics.org/FAQ/#sharp_lines
    721          gc.Translate(0.5, 0.5)
    722          # Draw the ballon
    723          BORDER_COLOR = wx.Colour(127, 127, 127)
    724          BG_COLOR = wx.Colour(255, 255, 231)
    725          PEN = wx.Pen(BORDER_COLOR, 1, wx.PENSTYLE_SOLID)
    726          BRUSH = wx.Brush(BG_COLOR, wx.BRUSHSTYLE_SOLID)
    727          gc.SetPen(PEN)
    728          gc.SetBrush(BRUSH)
    729          gc.DrawPath(path)
    730          # Draw the pin
    731          if sticky:
    732              pin = wx.Bitmap(os.path.join(ICONS_DIR, "stickypin.png"))
    733          else:
    734              pin = wx.Bitmap(os.path.join(ICONS_DIR, "unstickypin.png"))
    735          self.dc.DrawBitmap(pin, p7.x - 5, p6.y + 5, True)
    736
    737          # Return
    738          bx = left_x
    739          by = top_y
    740          bw = W + R + 1
    741          bh = H + R + H_ARROW + 1
    742          bounding_rect = wx.Rect(bx, by, bw, bh)
    743          return (bounding_rect, left_x + BALLOON_RADIUS, top_y + BALLOON_RADIUS)
    744
    745 -    def get_period_xpos(self, time_period):
    746          w, _ = self.dc.GetSize()
    747          return (max(0, self.scene.x_pos_for_time(time_period.start_time)),
    748                  min(w, self.scene.x_pos_for_time(time_period.end_time)))
    749
    750 -    def period_is_visible(self, time_period):
    751          w, _ = self.dc.GetSize()
    752          return (self.scene.x_pos_for_time(time_period.start_time) < w and
    753                  self.scene.x_pos_for_time(time_period.end_time) > 0)
    754
    755
    756 -def break_text(text, dc, max_width_in_px):
    757      """ Break the text into lines so that they fits within the given width."""
    758      sentences = text.split("\n")
    759      lines = []
    760      for sentence in sentences:
    761          w, _ = dc.GetTextExtent(sentence)
    762          if w <= max_width_in_px:
    763              lines.append(sentence)
    764          # The sentence is too long. Break it.
    765          else:
    766              break_sentence(dc, lines, sentence, max_width_in_px)
    767      return lines
    768
    769
    770 -def break_sentence(dc, lines, sentence, max_width_in_px):
    771      """Break a sentence into lines."""
    772      line = []
    773      max_word_len_in_ch = get_max_word_length(dc, max_width_in_px)
    774      words = break_line(dc, sentence, max_word_len_in_ch)
    775      for word in words:
    776          w, _ = dc.GetTextExtent("".join(line) + word + " ")
    777          # Max line length reached. Start a new line
    778          if w > max_width_in_px:
    779              lines.append("".join(line))
    780              line = []
    781          line.append(word + " ")
    782          # Word edning with '-' is a broken word. Start a new line
    783          if word.endswith('-'):
    784              lines.append("".join(line))
    785              line = []
    786      if len(line) > 0:
    787          lines.append("".join(line))
    788
    789
    790 -def break_line(dc, sentence, max_word_len_in_ch):
    791      """Break a sentence into words."""
    792      words = sentence.split(" ")
    793      new_words = []
    794      for word in words:
    795          broken_words = break_word(dc, word, max_word_len_in_ch)
    796          for broken_word in broken_words:
    797              new_words.append(broken_word)
    798      return new_words
    799
    800
    801 -def break_word(dc, word, max_word_len_in_ch):
    802      """
    803      Break words if they are too long.
    804
    805      If a single word is too long to fit we have to break it.
    806      If not we just return the word given.
    807      """
    808      words = []
    809      while len(word) > max_word_len_in_ch:
    810          word1 = word[0:max_word_len_in_ch] + "-"
    811          word = word[max_word_len_in_ch:]
    812          words.append(word1)
    813      words.append(word)
    814      return words
    815
    816
    817 -def get_max_word_length(dc, max_width_in_px):
    818      TEMPLATE_CHAR = 'K'
    819      word = [TEMPLATE_CHAR]
    820      w, _ = dc.GetTextExtent("".join(word))
    821      while w < max_width_in_px:
    822          word.append(TEMPLATE_CHAR)
    823          w, _ = dc.GetTextExtent("".join(word))
    824      return len(word) - 1
    825