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

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

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