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

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

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