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  from timelinelib.canvas.drawing.drawers.dividerline import DividerLine 
 35  from timelinelib.canvas.drawing.drawers.minorstrip import MinorStripDrawer 
 36  from timelinelib.canvas.drawing.drawers.nowline import NowLine 
 37   
 38   
 39  OUTER_PADDING = 5  # Space between event boxes (pixels) 
 40  INNER_PADDING = 3  # Space inside event box to text (pixels) 
 41  PERIOD_THRESHOLD = 20  # Periods smaller than this are drawn as events (pixels) 
 42  BALLOON_RADIUS = 12 
 43  ARROW_OFFSET = BALLOON_RADIUS + 25 
 44  DATA_INDICATOR_SIZE = 10 
 45  CONTRAST_RATIO_THREASHOLD = 2250 
 46  WHITE = (255, 255, 255) 
 47  BLACK = (0, 0, 0) 
 48   
 49   
50 -class DefaultDrawingAlgorithm(Drawer):
51
52 - def __init__(self):
53 self.event_text_font = Font(8) 54 self._create_pens() 55 self._create_brushes() 56 self._fixed_ys = {} 57 self._do_draw_top_scale = False 58 self._do_draw_bottom_scale = True 59 self._do_draw_divider_line = False
60
61 - def set_event_box_drawer(self, event_box_drawer):
62 self.event_box_drawer = event_box_drawer
63
64 - def set_background_drawer(self, background_drawer):
65 self.background_drawer = background_drawer
66
67 - def increment_font_size(self, step=2):
68 self.event_text_font.increment(step) 69 self._adjust_outer_padding_to_font_size()
70
71 - def decrement_font_size(self, step=2):
72 if self.event_text_font.PointSize > step: 73 self.event_text_font.decrement(step) 74 self._adjust_outer_padding_to_font_size()
75
77 if self.event_text_font.PointSize < 8: 78 self.outer_padding = OUTER_PADDING * self.event_text_font.PointSize / 8 79 else: 80 self.outer_padding = OUTER_PADDING
81
82 - def _create_pens(self):
83 self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID) 84 self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_SOLID) 85 self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID) 86 self.minor_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_USER_DASH) 87 self.minor_strip_pen.SetDashes([2, 2]) 88 self.minor_strip_pen.SetCap(wx.CAP_BUTT) 89 self.major_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_SOLID) 90 self.now_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID) 91 self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID)
92
93 - def _create_brushes(self):
94 self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.BRUSHSTYLE_SOLID) 95 self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.BRUSHSTYLE_SOLID) 96 self.red_solid_brush = wx.Brush(wx.Colour(255, 0, 0), wx.BRUSHSTYLE_SOLID) 97 self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230), wx.BRUSHSTYLE_SOLID)
98
99 - def event_is_period(self, time_period):
100 period_width_in_pixels = self.scene.width_of_period(time_period) 101 return period_width_in_pixels > PERIOD_THRESHOLD
102
103 - def _get_text_extent(self, text):
104 self.dc.SetFont(self.event_text_font) 105 tw, th = self.dc.GetTextExtent(text) 106 return (tw, th)
107
108 - def get_closest_overlapping_event(self, event_to_move, up=True):
109 return self.scene.get_closest_overlapping_event(event_to_move, up=up)
110
111 - def draw(self, dc, timeline, view_properties, appearance, fast_draw=False):
112 self.fast_draw = fast_draw 113 view_properties.hide_events_done = appearance.get_hide_events_done() 114 view_properties._legend_pos = appearance.get_legend_pos() 115 view_properties._time_scale_pos = appearance.get_time_scale_pos() 116 view_properties.set_fuzzy_icon(appearance.get_fuzzy_icon()) 117 view_properties.set_locked_icon(appearance.get_locked_icon()) 118 view_properties.set_hyperlink_icon(appearance.get_hyperlink_icon()) 119 view_properties.set_skip_s_in_decade_text(appearance.get_skip_s_in_decade_text()) 120 view_properties.set_display_checkmark_on_events_done(appearance.get_display_checkmark_on_events_done()) 121 self.minor_strip_pen.SetColour(appearance.get_minor_strip_divider_line_colour()) 122 self.major_strip_pen.SetColour(appearance.get_major_strip_divider_line_colour()) 123 self.now_pen.SetColour(appearance.get_now_line_colour()) 124 self.weekend_color = appearance.get_weekend_colour() 125 self.bg_color = appearance.get_bg_colour() 126 self.colorize_weekends = appearance.get_colorize_weekends() 127 self.outer_padding = OUTER_PADDING 128 self.outer_padding = appearance.get_vertical_space_between_events() 129 if EXTENDED_CONTAINER_HEIGHT.enabled(): 130 self.outer_padding += EXTENDED_CONTAINER_HEIGHT.get_extra_outer_padding_to_avoid_vertical_overlapping() 131 self.appearance = appearance 132 self.dc = dc 133 self.time_type = timeline.get_time_type() 134 self.scene = self._create_scene(dc.GetSize(), timeline, view_properties, self._get_text_extent) 135 if view_properties.use_fixed_event_vertical_pos(): 136 self._calc_fixed_event_rect_y(dc.GetSize(), timeline, view_properties, self._get_text_extent) 137 else: 138 self._fixed_ys = {} 139 self._perform_drawing(timeline, view_properties) 140 del self.dc # Program crashes if we don't delete the dc reference.
141
142 - def _create_scene(self, size, db, view_properties, get_text_extent_fn):
143 scene = TimelineScene(size, db, view_properties, get_text_extent_fn, self.appearance) 144 scene.set_outer_padding(self.outer_padding) 145 scene.set_inner_padding(INNER_PADDING) 146 scene.set_period_threshold(PERIOD_THRESHOLD) 147 scene.set_data_indicator_size(DATA_INDICATOR_SIZE) 148 scene.create() 149 return scene
150
151 - def _calc_fixed_event_rect_y(self, size, db, view_properties, get_text_extent_fn):
152 periods = view_properties.periods 153 view_properties.set_displayed_period(TimePeriod(periods[0].start_time, periods[-1].end_time), False) 154 large_size = (size[0] * len(periods), size[1]) 155 scene = self._create_scene(large_size, db, view_properties, get_text_extent_fn) 156 for (evt, rect) in scene.event_data: 157 self._fixed_ys[evt.id] = rect.GetY()
158
159 - def _perform_drawing(self, timeline, view_properties):
160 self.background_drawer.draw( 161 self, self.dc, self.scene, timeline, self.colorize_weekends, self.weekend_color, self.bg_color) 162 if self.fast_draw: 163 self._perform_fast_drawing(view_properties) 164 else: 165 self._perform_normal_drawing(view_properties)
166
167 - def _perform_fast_drawing(self, view_properties):
168 self._draw_bg() 169 self._draw_events(view_properties) 170 self._draw_selection_rect(view_properties)
171
172 - def _draw_selection_rect(self, view_properties):
173 if view_properties._selection_rect: 174 self.dc.SetPen(wx.BLACK_PEN) 175 self.dc.SetBrush(wx.Brush(wx.WHITE, style=BRUSHSTYLE_TRANSPARENT)) 176 self.dc.DrawRectangle(*view_properties._selection_rect)
177
178 - def _perform_normal_drawing(self, view_properties):
179 self._draw_period_selection(view_properties) 180 self._draw_bg() 181 self._draw_events(view_properties) 182 self._draw_legend(view_properties, self._extract_categories()) 183 self._draw_ballons(view_properties)
184
185 - def snap(self, time, snap_region=10):
186 if self._distance_to_left_border(time) < snap_region: 187 return self._get_time_at_left_border(time) 188 elif self._distance_to_right_border(time) < snap_region: 189 return self._get_time_at_right_border(time) 190 else: 191 return time
192
193 - def _distance_to_left_border(self, time):
194 left_strip_time, _ = self._snap_region(time) 195 return self.scene.distance_between_times(time, left_strip_time)
196
197 - def _distance_to_right_border(self, time):
198 _, right_strip_time = self._snap_region(time) 199 return self.scene.distance_between_times(time, right_strip_time)
200
201 - def _get_time_at_left_border(self, time):
202 left_strip_time, _ = self._snap_region(time) 203 return left_strip_time
204
205 - def _get_time_at_right_border(self, time):
206 _, right_strip_time = self._snap_region(time) 207 return right_strip_time
208
209 - def _snap_region(self, time):
210 left_strip_time = self.scene.minor_strip.start(time) 211 right_strip_time = self.scene.minor_strip.increment(left_strip_time) 212 return (left_strip_time, right_strip_time)
213
214 - def snap_selection(self, period_selection):
215 start, end = period_selection 216 return (self.snap(start), self.snap(end))
217
218 - def event_at(self, x, y, alt_down=False):
219 container_event = None 220 for (event, rect) in self.scene.event_data: 221 if event.is_container(): 222 rect = self._adjust_container_rect_for_hittest(rect) 223 if rect.Contains(wx.Point(x, y)): 224 if event.is_container(): 225 if alt_down: 226 return event 227 container_event = event 228 else: 229 return event 230 return container_event
231
232 - def get_events_in_rect(self, rect):
233 wx_rect = wx.Rect(*rect) 234 return [event for (event, rect) in self.scene.event_data if rect.Intersects(wx_rect)]
235 241
242 - def event_with_rect_at(self, x, y, alt_down=False):
243 container_event = None 244 container_rect = None 245 for (event, rect) in self.scene.event_data: 246 if rect.Contains(wx.Point(x, y)): 247 if event.is_container(): 248 if alt_down: 249 return event, rect 250 container_event = event 251 container_rect = rect 252 else: 253 return event, rect 254 if container_event is None: 255 return None 256 return container_event, container_rect
257
258 - def event_rect(self, evt):
259 for (event, rect) in self.scene.event_data: 260 if evt == event: 261 return rect 262 return None
263
264 - def balloon_at(self, x, y):
265 event = None 266 for (event_in_list, rect) in self.balloon_data: 267 if rect.Contains(wx.Point(x, y)): 268 event = event_in_list 269 return event
270
271 - def get_time(self, x):
272 return self.scene.get_time(x)
273
274 - def get_hidden_event_count(self):
275 try: 276 return self.scene.get_hidden_event_count() 277 except AttributeError: 278 return 0
279
280 - def _draw_period_selection(self, view_properties):
281 if not view_properties.period_selection: 282 return 283 start, end = view_properties.period_selection 284 start_x = self.scene.x_pos_for_time(start) 285 end_x = self.scene.x_pos_for_time(end) 286 self.dc.SetBrush(self.lightgrey_solid_brush) 287 self.dc.SetPen(wx.TRANSPARENT_PEN) 288 self.dc.DrawRectangle(start_x, 0, end_x - start_x + 1, self.scene.height)
289
290 - def _draw_bg(self):
291 if self.fast_draw: 292 self._draw_fast_bg() 293 else: 294 self._draw_normal_bg()
295
296 - def _draw_fast_bg(self):
297 self._draw_minor_strips() 298 self._draw_divider_line()
299
300 - def _draw_normal_bg(self):
301 self._draw_major_strips() 302 self._draw_minor_strips() 303 self._draw_divider_line() 304 self._draw_now_line()
305
306 - def _draw_minor_strips(self):
307 drawer = MinorStripDrawer(self) 308 for strip_period in self.scene.minor_strip_data: 309 label = self.scene.minor_strip.label(strip_period.start_time) 310 drawer.draw(label, strip_period.start_time, strip_period.end_time)
311 #self._draw_minor_strip_divider_line_at(strip_period.end_time) 312 #self._draw_minor_strip_label(strip_period) 313 314 # def _draw_minor_strip_divider_line_at(self, time): 315 # x = self.scene.x_pos_for_time(time) 316 # self.dc.SetPen(self.minor_strip_pen) 317 # self.dc.DrawLine(x, 0, x, self.scene.height) 318 # 319 # def _draw_minor_strip_label(self, strip_period): 320 # label = self.scene.minor_strip.label(strip_period.start_time) 321 # self._set_minor_strip_font(strip_period) 322 # (tw, th) = self.dc.GetTextExtent(label) 323 # start_x = self.scene.x_pos_for_time(strip_period.get_start_time()) 324 # end_x = self.scene.x_pos_for_time(strip_period.get_end_time()) 325 # middle = (start_x + end_x) / 2 326 # if self._do_draw_divider_line: 327 # middley = self.scene.divider_y 328 # self.dc.DrawText(label, middle - tw / 2, middley - th) 329 # if self._do_draw_bottom_scale: 330 # middley = self.scene.height 331 # self.dc.DrawText(label, middle - tw / 2, middley - th) 332 # 333 # def _set_minor_strip_font(self, strip_period): 334 # if self.scene.minor_strip_is_day(): 335 # bold = False 336 # italic = False 337 # if self.time_type.is_weekend_day(strip_period.start_time): 338 # bold = True 339 # if self.time_type.is_special_day(strip_period.start_time): 340 # italic = True 341 # font.set_minor_strip_text_font(self.appearance.get_minor_strip_font(), self.dc, 342 # force_bold=bold, force_normal=not bold, force_italic=italic, force_upright=not italic) 343 # else: 344 # font.set_minor_strip_text_font(self.appearance.get_minor_strip_font(), self.dc) 345
346 - def _draw_major_strips(self):
347 font.set_major_strip_text_font(self.appearance.get_major_strip_font(), self.dc) 348 self.dc.SetPen(self.major_strip_pen) 349 self._calculate_use_major_strip_vertical_label() 350 for time_period in self.scene.major_strip_data: 351 self._draw_major_strip_end_line(time_period) 352 self._draw_major_strip_label(time_period)
353
355 if len(self.scene.major_strip_data) > 0: 356 strip_period = self.scene.major_strip_data[0] 357 label = self.scene.major_strip.label(strip_period.start_time, True) 358 strip_width = self.scene.width_of_period(strip_period) 359 tw, _ = self.dc.GetTextExtent(label) 360 self.use_major_strip_vertical_label = strip_width < (tw + 5) 361 else: 362 self.use_major_strip_vertical_label = False
363
364 - def _draw_major_strip_end_line(self, time_period):
365 x = self.scene.x_pos_for_time(time_period.end_time) 366 self.dc.DrawLine(x, 0, x, self.scene.height)
367
368 - def _draw_major_strip_label(self, time_period):
369 label = self.scene.major_strip.label(time_period.start_time, True) 370 if self.use_major_strip_vertical_label: 371 self._draw_major_strip_vertical_label(time_period, label) 372 else: 373 self._draw_major_strip_horizontal_label(time_period, label)
374
375 - def _draw_major_strip_vertical_label(self, time_period, label):
376 x = self._calculate_major_strip_vertical_label_x(time_period, label) 377 self.dc.DrawRotatedText(label, x, INNER_PADDING, -90)
378
379 - def _draw_major_strip_horizontal_label(self, time_period, label):
380 x = self._calculate_major_strip_horizontal_label_x(time_period, label) 381 self.dc.DrawText(label, x, INNER_PADDING)
382
383 - def _calculate_major_strip_horizontal_label_x(self, time_period, label):
384 tw, _ = self.dc.GetTextExtent(label) 385 x = self.scene.x_pos_for_time(time_period.mean_time()) - tw / 2 386 if x - INNER_PADDING < 0: 387 x = INNER_PADDING 388 right = self.scene.x_pos_for_time(time_period.end_time) 389 if x + tw + INNER_PADDING > right: 390 x = right - tw - INNER_PADDING 391 elif x + tw + INNER_PADDING > self.scene.width: 392 x = self.scene.width - tw - INNER_PADDING 393 left = self.scene.x_pos_for_time(time_period.start_time) 394 if x < left + INNER_PADDING: 395 x = left + INNER_PADDING 396 return x
397
398 - def _calculate_major_strip_vertical_label_x(self, time_period, label):
399 _, th = self.dc.GetTextExtent(label) 400 return self.scene.x_pos_for_time(time_period.mean_time()) + th / 2
401
402 - def _draw_divider_line(self):
403 DividerLine(self).draw()
404
405 - def _draw_lines_to_non_period_events(self, view_properties):
406 for (event, rect) in self.scene.event_data: 407 if event.is_milestone(): 408 continue 409 if not event.is_period(): 410 self._draw_line(view_properties, event, rect) 411 elif not self.scene.never_show_period_events_as_point_events() and self._event_displayed_as_point_event(rect): 412 self._draw_line(view_properties, event, rect)
413
414 - def _event_displayed_as_point_event(self, rect):
415 return self.scene.divider_y > rect.Y
416
417 - def _draw_line(self, view_properties, event, rect):
418 if self.appearance.get_draw_period_events_to_right(): 419 x = rect.X 420 else: 421 x = self.scene.x_pos_for_time(event.mean_time()) 422 y = rect.Y + rect.Height 423 y2 = self._get_end_of_line(event) 424 self._set_line_color(view_properties, event) 425 if event.is_period(): 426 if self.appearance.get_draw_period_events_to_right(): 427 x += 1 428 self.dc.DrawLine(x - 1, y, x - 1, y2) 429 self.dc.DrawLine(x + 1, y, x + 1, y2) 430 self.dc.DrawLine(x, y, x, y2) 431 self._draw_endpoint(event, x, y2)
432
433 - def _draw_endpoint(self, event, x, y):
434 if event.get_milestone(): 435 size = 8 436 self.dc.SetBrush(wx.BLUE_BRUSH) 437 self.dc.DrawPolygon([wx.Point(-size), 438 wx.Point(0, -size), 439 wx.Point(size, 0), 440 wx.Point(0, size)], x, y) 441 else: 442 self.dc.DrawCircle(x, y, 2)
443
444 - def _get_end_of_line(self, event):
445 # Lines are only drawn for events shown as point events and the line length 446 # is only dependent on the fact that an event is a subevent or not 447 if event.is_subevent(): 448 y = self._get_container_y(event) 449 else: 450 y = self.scene.divider_y 451 return y
452
453 - def _get_container_y(self, subevent):
454 for (event, rect) in self.scene.event_data: 455 if event.is_container(): 456 if event is subevent.container: 457 return rect.y - 1 458 return self.scene.divider_y
459
460 - def _set_line_color(self, view_properties, event):
461 if view_properties.is_selected(event): 462 self.dc.SetPen(self.red_solid_pen) 463 self.dc.SetBrush(self.red_solid_brush) 464 else: 465 self.dc.SetBrush(self.black_solid_brush) 466 self.dc.SetPen(self.black_solid_pen)
467
468 - def _draw_now_line(self):
469 NowLine(self).draw()
470
471 - def _extract_categories(self):
472 categories = [] 473 for (event, _) in self.scene.event_data: 474 cat = event.get_category() 475 if cat and cat not in categories: 476 categories.append(cat) 477 return sort_categories(categories)
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