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

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

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