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