Gnumed.timelinelib.drawing.drawers.default

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

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

      1  # Copyright (C) 2009, 2010, 2011  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.config.paths import ICONS_DIR
     25  from timelinelib.db.objects.category import sort_categories
     26  from timelinelib.drawing.interface import Drawer
     27  from timelinelib.drawing.scene import TimelineScene
     28  from timelinelib.drawing.utils import darken_color
     29  from timelinelib.drawing.utils import get_default_font
     30
     31
     32  OUTER_PADDING = 5 # Space between event boxes (pixels)
     33  INNER_PADDING = 3 # Space inside event box to text (pixels)
     34  PERIOD_THRESHOLD = 20  # Periods smaller than this are drawn as events (pixels)
     35  BALLOON_RADIUS = 12
     36  DATA_INDICATOR_SIZE = 10
     37  CONTRAST_RATIO_THREASHOLD = 2250
     38  WHITE = (255, 255, 255)
     39  BLACK = (0, 0, 0)
     40
     41
     42 -class DefaultDrawingAlgorithm(Drawer):
     43
     44 -    def __init__(self):
     45          self._create_fonts()
     46          self._create_pens()
     47          self._create_brushes()
     48          self.fast_draw = False
     49
     50 -    def _create_fonts(self):
     51          self.header_font = get_default_font(12, True)
     52          self.small_text_font = get_default_font(8)
     53          self.small_text_font_bold = get_default_font(8, True)
     54
     55 -    def _create_pens(self):
     56          self.red_solid_pen = wx.Pen(wx.Color(255,0, 0), 1, wx.SOLID)
     57          self.black_solid_pen = wx.Pen(wx.Color(0, 0, 0), 1, wx.SOLID)
     58          self.darkred_solid_pen = wx.Pen(wx.Color(200, 0, 0), 1, wx.SOLID)
     59          self.black_dashed_pen = wx.Pen(wx.Color(200, 200, 200), 1, wx.USER_DASH)
     60          self.black_dashed_pen.SetDashes([2, 2])
     61          self.black_dashed_pen.SetCap(wx.CAP_BUTT)
     62          self.grey_solid_pen = wx.Pen(wx.Color(200, 200, 200), 1, wx.SOLID)
     63          self.red_solid_pen = wx.Pen(wx.Color(255, 0, 0), 1, wx.SOLID)
     64
     65 -    def _create_brushes(self):
     66          self.white_solid_brush = wx.Brush(wx.Color(255, 255, 255), wx.SOLID)
     67          self.black_solid_brush = wx.Brush(wx.Color(0, 0, 0), wx.SOLID)
     68          self.red_solid_brush = wx.Brush(wx.Color(255, 0, 0), wx.SOLID)
     69          self.lightgrey_solid_brush = wx.Brush(wx.Color(230, 230, 230), wx.SOLID)
     70
     71 -    def event_is_period(self, time_period):
     72          period_width_in_pixels = self.scene.width_of_period(time_period)
     73          return period_width_in_pixels > PERIOD_THRESHOLD
     74
     75 -    def _get_text_extent(self, text):
     76          self.dc.SetFont(self.small_text_font)
     77          tw, th = self.dc.GetTextExtent(text)
     78          return (tw, th)
     79
     80 -    def get_closest_overlapping_event(self, event_to_move, up=True):
     81          return self.scene.get_closest_overlapping_event(event_to_move, up=up)
     82
     83 -    def draw(self, dc, timeline, view_properties, config):
     84          self.config = config
     85          self.dc = dc
     86          self.time_type = timeline.get_time_type()
     87          self.scene = self._create_scene(
     88              dc.GetSizeTuple(), timeline, view_properties, self._get_text_extent)
     89          self._perform_drawing(view_properties)
     90          del self.dc # Program crashes if we don't delete the dc reference.
     91
     92 -    def _create_scene(self, size, db, view_properties, get_text_extent_fn):
     93          scene = TimelineScene(size, db, view_properties, get_text_extent_fn, self.config)
     94          scene.set_outer_padding(OUTER_PADDING)
     95          scene.set_inner_padding(INNER_PADDING)
     96          scene.set_period_threshold(PERIOD_THRESHOLD)
     97          scene.set_data_indicator_size(DATA_INDICATOR_SIZE)
     98          scene.create()
     99          return scene
    100
    101 -    def _perform_drawing(self, view_properties):
    102          if self.fast_draw:
    103              self._perform_fast_drawing(view_properties)
    104          else:
    105              self._perform_normal_drawing(view_properties)
    106
    107 -    def _perform_fast_drawing(self, view_properties):
    108          self._draw_bg(view_properties)
    109          self._draw_events(view_properties)
    110
    111 -    def _perform_normal_drawing(self, view_properties):
    112          self._draw_period_selection(view_properties)
    113          self._draw_bg(view_properties)
    114          self._draw_events(view_properties)
    115          self._draw_legend(view_properties, self._extract_categories())
    116          self._draw_ballons(view_properties)
    117
    118 -    def snap(self, time, snap_region=10):
    119          if self._distance_to_left_border(time) < snap_region:
    120              return self._get_time_at_left_border(time)
    121          elif self._distance_to_right_border(time)  < snap_region:
    122              return self._get_time_at_right_border(time)
    123          else:
    124              return time
    125
    126 -    def _distance_to_left_border(self, time):
    127          left_strip_time, right_strip_time = self._snap_region(time)
    128          return self.scene.distance_between_times(time, left_strip_time)
    129
    130 -    def _distance_to_right_border(self, time):
    131          left_strip_time, right_strip_time = self._snap_region(time)
    132          return self.scene.distance_between_times(time, right_strip_time)
    133
    134 -    def _get_time_at_left_border(self, time):
    135          left_strip_time, right_strip_time = self._snap_region(time)
    136          return left_strip_time
    137
    138 -    def _get_time_at_right_border(self, time):
    139          left_strip_time, right_strip_time = self._snap_region(time)
    140          return right_strip_time
    141
    142 -    def _snap_region(self, time):
    143          left_strip_time = self.scene.minor_strip.start(time)
    144          right_strip_time = self.scene.minor_strip.increment(left_strip_time)
    145          return (left_strip_time, right_strip_time)
    146
    147 -    def snap_selection(self, period_selection):
    148          start, end = period_selection
    149          return (self.snap(start), self.snap(end))
    150
    151 -    def event_at(self, x, y, alt_down=False):
    152          container_event = None
    153          for (event, rect) in self.scene.event_data:
    154              if rect.Contains(wx.Point(x, y)):
    155                  if event.is_container():
    156                      if alt_down:
    157                          return event
    158                      container_event = event
    159                  else:
    160                      return event
    161          return container_event
    162
    163 -    def event_with_rect_at(self, x, y, alt_down=False):
    164          container_event = None
    165          container_rect = None
    166          for (event, rect) in self.scene.event_data:
    167              if rect.Contains(wx.Point(x, y)):
    168                  if event.is_container():
    169                      if alt_down:
    170                          return event, rect
    171                      container_event = event
    172                      container_rect = rect
    173                  else:
    174                      return event, rect
    175          if container_event == None:
    176              return None
    177          return container_event, container_rect
    178
    179 -    def event_rect(self, evt):
    180          for (event, rect) in self.scene.event_data:
    181              if evt == event:
    182                  return rect
    183          return None
    184
    185 -    def balloon_at(self, x, y):
    186          event = None
    187          for (event_in_list, rect) in self.balloon_data:
    188              if rect.Contains(wx.Point(x, y)):
    189                  event = event_in_list
    190          return event
    191
    192 -    def get_time(self, x):
    193          return self.scene.get_time(x)
    194
    195 -    def get_hidden_event_count(self):
    196          return self.scene.get_hidden_event_count()
    197
    198 -    def _draw_period_selection(self, view_properties):
    199          if not view_properties.period_selection:
    200              return
    201          start, end = view_properties.period_selection
    202          start_x = self.scene.x_pos_for_time(start)
    203          end_x = self.scene.x_pos_for_time(end)
    204          self.dc.SetBrush(self.lightgrey_solid_brush)
    205          self.dc.SetPen(wx.TRANSPARENT_PEN)
    206          self.dc.DrawRectangle(start_x, 0,
    207                                end_x - start_x + 1, self.scene.height)
    208
    209 -    def _draw_bg(self, view_properties):
    210          if self.fast_draw:
    211              self._draw_fast_bg()
    212          else:
    213              self._draw_normal_bg(view_properties)
    214
    215 -    def _draw_fast_bg(self):
    216          self._draw_minor_strips()
    217          self._draw_divider_line()
    218
    219 -    def _draw_normal_bg(self, view_properties):
    220          self._draw_minor_strips()
    221          self._draw_major_strips()
    222          self._draw_divider_line()
    223          self._draw_now_line()
    224
    225 -    def _draw_minor_strips(self):
    226          for strip_period in self.scene.minor_strip_data:
    227              self._draw_minor_strip_divider_line_at(strip_period.end_time)
    228              self._draw_minor_strip_label(strip_period)
    229
    230 -    def _draw_minor_strip_divider_line_at(self, time):
    231          x = self.scene.x_pos_for_time(time)
    232          self.dc.SetPen(self.black_dashed_pen)
    233          self.dc.DrawLine(x, 0, x, self.scene.height)
    234
    235 -    def _draw_minor_strip_label(self, strip_period):
    236          label = self.scene.minor_strip.label(strip_period.start_time)
    237          self.dc.SetFont(self.scene.minor_strip.get_font(strip_period))
    238          (tw, th) = self.dc.GetTextExtent(label)
    239          middle = self.scene.x_pos_for_time(strip_period.mean_time())
    240          middley = self.scene.divider_y
    241          self.dc.DrawText(label, middle - tw / 2, middley - th)
    242
    243 -    def _draw_major_strips(self):
    244          self.dc.SetFont(self.header_font)
    245          self.dc.SetPen(self.grey_solid_pen)
    246          for time_period in self.scene.major_strip_data:
    247              self._draw_major_strip_end_line(time_period)
    248              self._draw_major_strip_label(time_period)
    249
    250 -    def _draw_major_strip_end_line(self, time_period):
    251          end_time = self.time_type.adjust_for_bc_years(time_period.end_time)
    252          x = self.scene.x_pos_for_time(end_time)
    253          self.dc.DrawLine(x, 0, x, self.scene.height)
    254
    255 -    def _draw_major_strip_label(self, time_period):
    256          label = self.scene.major_strip.label(time_period.start_time, True)
    257          x = self._calculate_major_strip_label_x(time_period, label)
    258          self.dc.DrawText(label, x, INNER_PADDING)
    259
    260 -    def _calculate_major_strip_label_x(self, time_period, label):
    261          (tw, th) = self.dc.GetTextExtent(label)
    262          x = self.scene.x_pos_for_time(time_period.mean_time()) - tw / 2
    263          if x - INNER_PADDING < 0:
    264              x = INNER_PADDING
    265              right = self.scene.x_pos_for_time(time_period.end_time)
    266              if x + tw + INNER_PADDING > right:
    267                  x = right - tw - INNER_PADDING
    268          elif x + tw + INNER_PADDING > self.scene.width:
    269              x = self.scene.width - tw - INNER_PADDING
    270              left = self.scene.x_pos_for_time(time_period.start_time)
    271              if x < left + INNER_PADDING:
    272                  x = left + INNER_PADDING
    273          return x
    274
    275 -    def _draw_divider_line(self):
    276          self.dc.SetPen(self.black_solid_pen)
    277          self.dc.DrawLine(0, self.scene.divider_y, self.scene.width,
    278                           self.scene.divider_y)
    279
    280 -    def _draw_lines_to_non_period_events(self, view_properties):
    281          for (event, rect) in self.scene.event_data:
    282              if self._invisible_container_subevent(event, rect):
    283                  continue
    284              if self._event_displayed_as_point_event(rect):
    285                  self._draw_line(view_properties, event, rect)
    286
    287 -    def _invisible_container_subevent(self, event, rect):
    288          return (self._subevent_displayed_as_point_event(event, rect) and
    289                  event.is_period())
    290
    291 -    def _event_displayed_as_point_event(self, rect):
    292          return self.scene.divider_y > rect.Y
    293
    294 -    def _draw_line(self, view_properties, event, rect):
    295          x = self.scene.x_pos_for_time(event.mean_time())
    296          y = rect.Y + rect.Height
    297          y2 = self._get_end_of_line(event)
    298          self._set_line_color(view_properties, event)
    299          self.dc.DrawLine(x, y, x, y2)
    300          self.dc.DrawCircle(x, y2, 2)
    301
    302 -    def _get_end_of_line(self, event):
    303          if self._point_subevent(event):
    304              y = self._get_container_y(event.container_id)
    305          else:
    306              y = self.scene.divider_y
    307          return y
    308
    309 -    def _point_subevent(self, event):
    310          return event.is_subevent() and not event.is_period()
    311
    312 -    def _get_container_y(self, id):
    313          for (event, rect) in self.scene.event_data:
    314              if event.is_container():
    315                  if event.container_id == id:
    316                      return rect.y - 1
    317          return self.scene.divider_y
    318
    319 -    def _set_line_color(self, view_properties, event):
    320          if view_properties.is_selected(event):
    321              self.dc.SetPen(self.red_solid_pen)
    322              self.dc.SetBrush(self.red_solid_brush)
    323          else:
    324              self.dc.SetBrush(self.black_solid_brush)
    325              self.dc.SetPen(self.black_solid_pen)
    326
    327 -    def _draw_now_line(self):
    328          now_time = self.time_type.now()
    329          x = self.scene.x_pos_for_time(now_time)
    330          if x > 0 and x < self.scene.width:
    331              self.dc.SetPen(self.darkred_solid_pen)
    332              self.dc.DrawLine(x, 0, x, self.scene.height)
    333
    334 -    def _extract_categories(self):
    335          categories = []
    336          for (event, rect) in self.scene.event_data:
    337              cat = event.category
    338              if cat and not cat in categories:
    339                  categories.append(cat)
    340          return sort_categories(categories)
    341
    342 -    def _draw_legend(self, view_properties, categories):
    343          if self._legend_should_be_drawn(view_properties, categories):
    344              self.dc.SetFont(self.small_text_font)
    345              rect = self._calculate_legend_rect(categories)
    346              self._draw_legend_box(rect)
    347              self._draw_legend_items(rect, categories)
    348
    349 -    def _legend_should_be_drawn(self, view_properties, categories):
    350          return view_properties.show_legend and len(categories) > 0
    351
    352 -    def _calculate_legend_rect(self, categories):
    353          max_width = 0
    354          height = INNER_PADDING
    355          for cat in categories:
    356              tw, th = self.dc.GetTextExtent(cat.name)
    357              height = height + th + INNER_PADDING
    358              if tw > max_width:
    359                  max_width = tw
    360          item_height = self._text_height_with_current_font()
    361          width = max_width + 4 * INNER_PADDING + item_height
    362          return wx.Rect(OUTER_PADDING,
    363                         self.scene.height - height - OUTER_PADDING,
    364                         width,
    365                         height)
    366
    367 -    def _draw_legend_box(self, rect):
    368          self.dc.SetBrush(self.white_solid_brush)
    369          self.dc.SetPen(self.black_solid_pen)
    370          self.dc.DrawRectangleRect(rect)
    371
    372 -    def _text_height_with_current_font(self):
    373          STRING_WITH_MIXED_CAPITALIZATION = "jJ"
    374          tw, th = self.dc.GetTextExtent(STRING_WITH_MIXED_CAPITALIZATION)
    375          return th
    376
    377 -    def _draw_legend_items(self, rect, categories):
    378          item_height = self._text_height_with_current_font()
    379          cur_y = rect.Y + INNER_PADDING
    380          for cat in categories:
    381              base_color = cat.color
    382              border_color = darken_color(base_color)
    383              self.dc.SetBrush(wx.Brush(base_color, wx.SOLID))
    384              self.dc.SetPen(wx.Pen(border_color, 1, wx.SOLID))
    385              color_box_rect = (OUTER_PADDING + rect.Width - item_height -
    386                                INNER_PADDING,
    387                                cur_y, item_height, item_height)
    388              self.dc.DrawRectangleRect(color_box_rect)
    389              self.dc.SetTextForeground((0, 0, 0))
    390              self.dc.DrawText(cat.name, OUTER_PADDING + INNER_PADDING, cur_y)
    391              cur_y = cur_y + item_height + INNER_PADDING
    392
    393 -    def _draw_events(self, view_properties):
    394          """Draw all event boxes and the text inside them."""
    395          self.dc.SetFont(self.small_text_font)
    396          self.dc.DestroyClippingRegion()
    397          self._draw_lines_to_non_period_events(view_properties)
    398          for (event, rect) in self.scene.event_data:
    399              if event.is_container():
    400                  self._draw_container(event, rect, view_properties)
    401              else:
    402                  self._draw_event(event, rect, view_properties)
    403
    404 -    def _draw_container(self, event, rect, view_properties):
    405          box_rect = wx.Rect(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4)
    406          self._draw_box(box_rect, event)
    407          if self._event_displayed_as_point_event(rect):
    408              self._draw_text(rect, event)
    409          if view_properties.is_selected(event):
    410              self._draw_selection_and_handles(rect, event)
    411
    412 -    def _draw_event(self, event, rect, view_properties):
    413          if self._subevent_displayed_as_point_event(event, rect):
    414              if event.is_period():
    415                  return
    416          self._draw_box(rect, event)
    417          self._draw_text(rect, event)
    418          if event.has_data():
    419              self._draw_contents_indicator(event, rect)
    420          if view_properties.is_selected(event):
    421              self._draw_selection_and_handles(rect, event)
    422
    423 -    def _subevent_displayed_as_point_event(self, event, rect):
    424          return (event.is_subevent() and
    425                  self._event_displayed_as_point_event(rect))
    426
    427 -    def _draw_box(self, rect, event):
    428          self.dc.SetClippingRect(rect)
    429          self.dc.SetBrush(self._get_box_brush(event))
    430          self.dc.SetPen(self._get_box_pen(event))
    431          self.dc.DrawRectangleRect(rect)
    432          if event.fuzzy:
    433              self._draw_fuzzy_edges(rect, event)
    434          if event.locked:
    435              self._draw_locked_edges(rect, event)
    436          self.dc.DestroyClippingRegion()
    437
    438 -    def _draw_fuzzy_edges(self, rect, event):
    439          self._draw_fuzzy_start(rect, event)
    440          self._draw_fuzzy_end(rect, event)
    441
    442 -    def _draw_fuzzy_start(self, rect, event):
    443          """
    444            p1     /p2 ----------
    445                  /
    446            p3  <
    447                  \
    448            p4     \p5 ----------
    449          """
    450          x1 = rect.x
    451          x2 = rect.x + rect.height / 2
    452          y1 = rect.y
    453          y2 = rect.y + rect.height / 2
    454          y3 = rect.y + rect.height
    455          p1 = wx.Point(x1, y1)
    456          p2 = wx.Point(x2, y1)
    457          p3 = wx.Point(x1, y2)
    458          p4 = wx.Point(x1, y3)
    459          p5 = wx.Point(x2, y3)
    460          self.draw_fuzzy(event, p1, p2, p3, p4, p5)
    461
    462 -    def _draw_fuzzy_end(self, rect, event):
    463          """
    464            ---- P2\    p1
    465                    \
    466                     >  p3
    467                    /
    468            ---- p4/    p4
    469          """
    470          x1 = rect.x + rect.width - rect.height / 2
    471          x2 = rect.x + rect.width
    472          y1 = rect.y
    473          y2 = rect.y + rect.height / 2
    474          y3 = rect.y + rect.height
    475          p1 = wx.Point(x2, y1)
    476          p2 = wx.Point(x1, y1)
    477          p3 = wx.Point(x2, y2)
    478          p4 = wx.Point(x2, y3)
    479          p5 = wx.Point(x1, y3)
    480          self.draw_fuzzy(event, p1, p2, p3, p4, p5)
    481
    482 -    def draw_fuzzy(self, event, p1, p2, p3, p4, p5):
    483          self._draw_fuzzy_polygon(p1, p2 ,p3)
    484          self._draw_fuzzy_polygon(p3, p4 ,p5)
    485          self._draw_fuzzy_border(event, p2, p3, p5)
    486
    487 -    def _draw_fuzzy_polygon(self, p1, p2 ,p3):
    488          self.dc.SetBrush(wx.WHITE_BRUSH)
    489          self.dc.SetPen(wx.WHITE_PEN)
    490          self.dc.DrawPolygon((p1, p2, p3))
    491
    492 -    def _draw_fuzzy_border(self, event, p1, p2, p3):
    493          gc = wx.GraphicsContext.Create(self.dc)
    494          path = gc.CreatePath()
    495          path.MoveToPoint(p1.x, p1.y)
    496          path.AddLineToPoint(p2.x, p2.y)
    497          path.AddLineToPoint(p3.x, p3.y)
    498          gc.SetPen(self._get_box_pen(event))
    499          gc.StrokePath(path)
    500
    501 -    def _draw_locked_edges(self, rect, event):
    502          self._draw_locked_start(event, rect)
    503          self._draw_locked_end(event, rect)
    504
    505 -    def _draw_locked_start(self, event, rect):
    506          x = rect.x
    507          if event.fuzzy:
    508              start_angle = -math.pi / 4
    509              end_angle = math.pi / 4
    510          else:
    511              start_angle = -math.pi
    512              end_angle = math.pi
    513          self._draw_locked(event, rect, x, start_angle, end_angle)
    514
    515 -    def _draw_locked_end(self, event, rect):
    516          x = rect.x + rect.width
    517          if event.fuzzy:
    518              start_angle = 3 * math.pi / 4
    519              end_angle = 5 * math.pi / 4
    520          else:
    521              start_angle = math.pi / 2
    522              end_angle = 3 * math.pi / 2
    523          self._draw_locked(event, rect, x, start_angle, end_angle)
    524
    525 -    def _draw_locked(self, event, rect, x, start_angle, end_angle):
    526          y = rect.y + rect.height / 2
    527          r = rect.height / 2.5
    528          self.dc.SetBrush(wx.WHITE_BRUSH)
    529          self.dc.SetPen(wx.WHITE_PEN)
    530          self.dc.DrawCircle(x, y, r)
    531          self.dc.SetPen(self._get_box_pen(event))
    532          self.draw_segment(event, x, y, r, start_angle, end_angle)
    533
    534 -    def draw_segment(self, event, x0, y0, r, start_angle, end_angle):
    535          gc = wx.GraphicsContext.Create(self.dc)
    536          path = gc.CreatePath()
    537          segment_length = 2.0 * (end_angle - start_angle) * r
    538          delta = (end_angle - start_angle) / segment_length
    539          angle = start_angle
    540          x1 = r * math.cos(angle) + x0
    541          y1 = r * math.sin(angle) + y0
    542          path.MoveToPoint(x1, y1)
    543          while angle < end_angle:
    544              angle += delta
    545              if angle > end_angle:
    546                  angle = end_angle
    547              x2 = r * math.cos(angle) + x0
    548              y2 = r * math.sin(angle) + y0
    549              path.AddLineToPoint(x2, y2)
    550              x1 = x2
    551              y1 = y2
    552          gc.SetPen(self._get_box_pen(event))
    553          gc.StrokePath(path)
    554
    555 -    def _draw_text(self, rect, event):
    556          # Ensure that we can't draw content outside inner rectangle
    557          rect_copy = wx.Rect(*rect)
    558          rect_copy.Deflate(INNER_PADDING, INNER_PADDING)
    559          if rect_copy.Width > 0:
    560              # Draw the text (if there is room for it)
    561              self.dc.SetClippingRect(rect_copy)
    562              text_x = rect.X + INNER_PADDING
    563              if event.fuzzy or event.locked:
    564                  text_x += rect.Height / 2
    565              text_y = rect.Y + INNER_PADDING
    566              if text_x < INNER_PADDING:
    567                  text_x = INNER_PADDING
    568              self._set_text_foreground_color(event)
    569              self.dc.DrawText(event.text, text_x, text_y)
    570              self.dc.DestroyClippingRegion()
    571
    572 -    def _set_text_foreground_color(self, event):
    573          if event.category is None:
    574              fg_color = BLACK
    575          elif event.category.font_color is None:
    576              fg_color = BLACK
    577          else:
    578              font_color = event.category.font_color
    579              fg_color = wx.Color(font_color[0], font_color[1], font_color[2])
    580          self.dc.SetTextForeground(fg_color)
    581
    582 -    def _draw_contents_indicator(self, event, rect):
    583          """
    584          The data contents indicator is a small triangle drawn in the upper
    585          right corner of the event rectangle.
    586          """
    587          self.dc.SetClippingRect(rect)
    588          corner_x = rect.X + rect.Width
    589          if corner_x > self.scene.width:
    590              corner_x = self.scene.width
    591          points = (
    592              wx.Point(corner_x - DATA_INDICATOR_SIZE, rect.Y),
    593              wx.Point(corner_x, rect.Y),
    594              wx.Point(corner_x, rect.Y + DATA_INDICATOR_SIZE),
    595          )
    596          self.dc.SetBrush(self._get_box_indicator_brush(event))
    597          self.dc.SetPen(wx.TRANSPARENT_PEN)
    598          self.dc.DrawPolygon(points)
    599          self.dc.DestroyClippingRegion()
    600
    601 -    def _draw_selection_and_handles(self, rect, event):
    602          self.dc.SetClippingRect(rect)
    603          small_rect = wx.Rect(*rect)
    604          small_rect.Deflate(1, 1)
    605          border_color = self._get_border_color(event)
    606          border_color = darken_color(border_color)
    607          pen = wx.Pen(border_color, 1, wx.SOLID)
    608          self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
    609          self.dc.SetPen(pen)
    610          self.dc.DrawRectangleRect(small_rect)
    611          self._draw_handles(rect, event)
    612          self.dc.DestroyClippingRegion()
    613
    614 -    def _draw_handles(self, rect, event):
    615          SIZE = 4
    616          big_rect = wx.Rect(rect.X - SIZE, rect.Y - SIZE, rect.Width + 2 * SIZE, rect.Height + 2 * SIZE)
    617          self.dc.DestroyClippingRegion()
    618          self.dc.SetClippingRect(big_rect)
    619          y = rect.Y + rect.Height/2 - SIZE/2
    620          x = rect.X - SIZE / 2
    621          west_rect   = wx.Rect(x + 1             , y, SIZE, SIZE)
    622          center_rect = wx.Rect(x + rect.Width / 2, y, SIZE, SIZE)
    623          east_rect   = wx.Rect(x + rect.Width - 1, y, SIZE, SIZE)
    624          self.dc.SetBrush(wx.Brush("BLACK", wx.SOLID))
    625          self.dc.SetPen(wx.Pen("BLACK", 1, wx.SOLID))
    626          if not event.locked:
    627              self.dc.DrawRectangleRect(east_rect)
    628              self.dc.DrawRectangleRect(west_rect)
    629          if not event.locked and not event.ends_today:
    630              self.dc.DrawRectangleRect(center_rect)
    631
    632 -    def _get_base_color(self, event):
    633          if event.category:
    634              base_color = event.category.color
    635          else:
    636              base_color = (200, 200, 200)
    637          return base_color
    638
    639 -    def _get_border_color(self, event):
    640          base_color = self._get_base_color(event)
    641          border_color = darken_color(base_color)
    642          return border_color
    643
    644 -    def _get_box_pen(self, event):
    645          border_color = self._get_border_color(event)
    646          pen = wx.Pen(border_color, 1, wx.SOLID)
    647          return pen
    648
    649 -    def _get_box_brush(self, event):
    650          base_color = self._get_base_color(event)
    651          brush = wx.Brush(base_color, wx.SOLID)
    652          return brush
    653
    654 -    def _get_box_indicator_brush(self, event):
    655          base_color = self._get_base_color(event)
    656          darker_color = darken_color(base_color, 0.6)
    657          brush = wx.Brush(darker_color, wx.SOLID)
    658          return brush
    659
    660 -    def _get_selected_box_brush(self, event):
    661          border_color = self._get_border_color(event)
    662          brush = wx.Brush(border_color, wx.BDIAGONAL_HATCH)
    663          return brush
    664
    665 -    def _draw_ballons(self, view_properties):
    666          """Draw ballons on selected events that has 'description' data."""
    667          self.balloon_data = []     # List of (event, rect)
    668          top_event = None
    669          top_rect = None
    670          self.dc.SetTextForeground(BLACK)
    671          for (event, rect) in self.scene.event_data:
    672              if (event.get_data("description") != None or
    673                  event.get_data("icon") != None):
    674                  sticky = view_properties.event_has_sticky_balloon(event)
    675                  if (view_properties.event_is_hovered(event) or sticky):
    676                      if not sticky:
    677                          top_event, top_rect = event, rect
    678                      self._draw_ballon(event, rect, sticky)
    679          # Make the unsticky balloon appear on top
    680          if top_event is not None:
    681              self._draw_ballon(top_event, top_rect, False)
    682
    683 -    def _draw_ballon(self, event, event_rect, sticky):
    684          """Draw one ballon on a selected event that has 'description' data."""
    685          # Constants
    686          MIN_TEXT_WIDTH = 200
    687          MIN_WIDTH = 100
    688          SLIDER_WIDTH = 20
    689
    690          inner_rect_w = 0
    691          inner_rect_h = 0
    692          # Icon
    693          (iw, ih) = (0, 0)
    694          icon = event.get_data("icon")
    695          if icon != None:
    696              (iw, ih) = icon.Size
    697              inner_rect_w = iw
    698              inner_rect_h = ih
    699          max_text_width = max(MIN_TEXT_WIDTH, (self.scene.width - SLIDER_WIDTH - event_rect.X - iw))
    700          # Text
    701          self.dc.SetFont(get_default_font(8))
    702          font_h = self.dc.GetCharHeight()
    703          (tw, th) = (0, 0)
    704          description = event.get_data("description")
    705          lines = None
    706          if description != None:
    707              lines = break_text(description, self.dc, max_text_width)
    708              th = len(lines) * self.dc.GetCharHeight()
    709              for line in lines:
    710                  (lw, lh) = self.dc.GetTextExtent(line)
    711                  tw = max(lw, tw)
    712              if icon != None:
    713                  inner_rect_w += BALLOON_RADIUS
    714              inner_rect_w += min(tw, max_text_width)
    715              inner_rect_h = max(inner_rect_h, th)
    716          inner_rect_w = max(MIN_WIDTH, inner_rect_w)
    717          bounding_rect, x, y = self._draw_balloon_bg(
    718              self.dc, (inner_rect_w, inner_rect_h),
    719              (event_rect.X + event_rect.Width / 2,
    720              event_rect.Y),
    721              True, sticky)
    722          if icon != None:
    723              self.dc.DrawBitmap(icon, x, y, False)
    724              x += iw + BALLOON_RADIUS
    725          if lines != None:
    726              ty = y
    727              for line in lines:
    728                  self.dc.DrawText(line, x, ty)
    729                  ty += font_h
    730              x += tw
    731          # Write data so we know where the balloon was drawn
    732          # Following two lines can be used when debugging the rectangle
    733          #self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
    734          #self.dc.DrawRectangleRect(bounding_rect)
    735          self.balloon_data.append((event, bounding_rect))
    736
    737 -    def _draw_balloon_bg(self, dc, inner_size, tip_pos, above, sticky):
    738          """
    739          Draw the balloon background leaving inner_size for content.
    740
    741          tip_pos determines where the tip of the ballon should be.
    742
    743          above determines if the balloon should be above the tip (True) or below
    744          (False). This is not currently implemented.
    745
    746                      W
    747             |----------------|
    748               ______________           _
    749              /              \          |             R = Corner Radius
    750             |                |         |            AA = Left Arrow-leg angle
    751             |  W_ARROW       |         |  H     MARGIN = Text margin
    752             |     |--|       |         |             * = Starting point
    753              \____    ______/          _
    754                  /  /                  |
    755                 /_/                    |  H_ARROW
    756                *                       -
    757             |----|
    758             ARROW_OFFSET
    759
    760          Calculation of points starts at the tip of the arrow and continues
    761          clockwise around the ballon.
    762
    763          Return (bounding_rect, x, y) where x and y is at top of inner region.
    764          """
    765          # Prepare path object
    766          gc = wx.GraphicsContext.Create(self.dc)
    767          path = gc.CreatePath()
    768          # Calculate path
    769          R = BALLOON_RADIUS
    770          W = 1 * R + inner_size[0]
    771          H = 1 * R + inner_size[1]
    772          H_ARROW = 14
    773          W_ARROW = 15
    774          W_ARROW_OFFSET = R + 25
    775          AA = 20
    776          # Starting point at the tip of the arrow
    777          (tipx, tipy) = tip_pos
    778          p0 = wx.Point(tipx, tipy)
    779          path.MoveToPoint(p0.x, p0.y)
    780          # Next point is the left base of the arrow
    781          p1 = wx.Point(p0.x + H_ARROW * math.tan(math.radians(AA)),
    782                        p0.y - H_ARROW)
    783          path.AddLineToPoint(p1.x, p1.y)
    784          # Start of lower left rounded corner
    785          p2 = wx.Point(p1.x - W_ARROW_OFFSET + R, p1.y)
    786          path.AddLineToPoint(p2.x, p2.y)
    787          # The lower left rounded corner. p3 is the center of the arc
    788          p3 = wx.Point(p2.x, p2.y - R)
    789          path.AddArc(p3.x, p3.y, R, math.radians(90), math.radians(180))
    790          # The left side
    791          p4 = wx.Point(p3.x - R, p3.y - H + R)
    792          left_x = p4.x
    793          path.AddLineToPoint(p4.x, p4.y)
    794          # The upper left rounded corner. p5 is the center of the arc
    795          p5 = wx.Point(p4.x + R, p4.y)
    796          path.AddArc(p5.x, p5.y, R, math.radians(180), math.radians(-90))
    797          # The upper side
    798          p6 = wx.Point(p5.x + W - R, p5.y - R)
    799          top_y = p6.y
    800          path.AddLineToPoint(p6.x, p6.y)
    801          # The upper right rounded corner. p7 is the center of the arc
    802          p7 = wx.Point(p6.x, p6.y + R)
    803          path.AddArc(p7.x, p7.y, R, math.radians(-90), math.radians(0))
    804          # The right side
    805          p8 = wx.Point(p7.x + R , p7.y + H - R)
    806          path.AddLineToPoint(p8.x, p8.y)
    807          # The lower right rounded corner. p9 is the center of the arc
    808          p9 = wx.Point(p8.x - R, p8.y)
    809          path.AddArc(p9.x, p9.y, R, math.radians(0), math.radians(90))
    810          # The lower side
    811          p10 = wx.Point(p9.x - W + W_ARROW +  W_ARROW_OFFSET, p9.y + R)
    812          path.AddLineToPoint(p10.x, p10.y)
    813          path.CloseSubpath()
    814          # Draw sharp lines on GTK which uses Cairo
    815          # See: http://www.cairographics.org/FAQ/#sharp_lines
    816          gc.Translate(0.5, 0.5)
    817          # Draw the ballon
    818          BORDER_COLOR = wx.Color(127, 127, 127)
    819          BG_COLOR = wx.Color(255, 255, 231)
    820          PEN = wx.Pen(BORDER_COLOR, 1, wx.SOLID)
    821          BRUSH = wx.Brush(BG_COLOR, wx.SOLID)
    822          gc.SetPen(PEN)
    823          gc.SetBrush(BRUSH)
    824          gc.DrawPath(path)
    825          # Draw the pin
    826          if sticky:
    827              pin = wx.Bitmap(os.path.join(ICONS_DIR, "stickypin.png"))
    828          else:
    829              pin = wx.Bitmap(os.path.join(ICONS_DIR, "unstickypin.png"))
    830          self.dc.DrawBitmap(pin, p7.x -5, p6.y + 5, True)
    831
    832          # Return
    833          bx = left_x
    834          by = top_y
    835          bw = W + R + 1
    836          bh = H + R + H_ARROW + 1
    837          bounding_rect = wx.Rect(bx, by, bw, bh)
    838          return (bounding_rect, left_x + BALLOON_RADIUS, top_y + BALLOON_RADIUS)
    839
    840
    841 -def break_text(text, dc, max_width_in_px):
    842      """ Break the text into lines so that they fits within the given width."""
    843      sentences = text.split("\n")
    844      lines = []
    845      for sentence in sentences:
    846          w, h = dc.GetTextExtent(sentence)
    847          if w <= max_width_in_px:
    848              lines.append(sentence)
    849          # The sentence is too long. Break it.
    850          else:
    851              break_sentence(dc, lines, sentence, max_width_in_px);
    852      return lines
    853
    854
    855 -def break_sentence(dc, lines, sentence, max_width_in_px):
    856      """Break a sentence into lines."""
    857      line = []
    858      max_word_len_in_ch = get_max_word_length(dc, max_width_in_px)
    859      words = break_line(dc, sentence, max_word_len_in_ch)
    860      for word in words:
    861          w, h = dc.GetTextExtent("".join(line) + word + " ")
    862          # Max line length reached. Start a new line
    863          if w > max_width_in_px:
    864              lines.append("".join(line))
    865              line = []
    866          line.append(word + " ")
    867          # Word edning with '-' is a broken word. Start a new line
    868          if word.endswith('-'):
    869              lines.append("".join(line))
    870              line = []
    871      if len(line) > 0:
    872          lines.append("".join(line))
    873
    874
    875 -def break_line(dc, sentence, max_word_len_in_ch):
    876      """Break a sentence into words."""
    877      words = sentence.split(" ")
    878      new_words = []
    879      for word in words:
    880          broken_words = break_word(dc, word, max_word_len_in_ch)
    881          for broken_word in broken_words:
    882              new_words.append(broken_word)
    883      return new_words
    884
    885
    886 -def break_word(dc, word, max_word_len_in_ch):
    887      """
    888      Break words if they are too long.
    889
    890      If a single word is too long to fit we have to break it.
    891      If not we just return the word given.
    892      """
    893      words = []
    894      while len(word) > max_word_len_in_ch:
    895          word1 = word[0:max_word_len_in_ch] + "-"
    896          word =  word[max_word_len_in_ch:]
    897          words.append(word1)
    898      words.append(word)
    899      return words
    900
    901
    902 -def get_max_word_length(dc, max_width_in_px):
    903      TEMPLATE_CHAR = 'K'
    904      word = [TEMPLATE_CHAR]
    905      w, h = dc.GetTextExtent("".join(word))
    906      while w < max_width_in_px:
    907          word.append(TEMPLATE_CHAR)
    908          w, h = dc.GetTextExtent("".join(word))
    909      return len(word) - 1
    910