1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import math
20 import os.path
21
22 import wx
23
24 from timelinelib.canvas.drawing.interface import Drawer
25 from timelinelib.canvas.drawing.scene import TimelineScene
26 from timelinelib.config.paths import ICONS_DIR
27 from timelinelib.canvas.data import sort_categories
28 from timelinelib.canvas.data.timeperiod import TimePeriod
29 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
30 from timelinelib.wxgui.components.font import Font
31 import timelinelib.wxgui.components.font as font
32 from timelinelib.canvas.drawing.drawers.legenddrawer import LegendDrawer
33 from wx import BRUSHSTYLE_TRANSPARENT
34 from timelinelib.canvas.drawing.drawers.dividerline import DividerLine
35 from timelinelib.canvas.drawing.drawers.minorstrip import MinorStripDrawer
36 from timelinelib.canvas.drawing.drawers.nowline import NowLine
37
38
39 OUTER_PADDING = 5
40 INNER_PADDING = 3
41 PERIOD_THRESHOLD = 20
42 BALLOON_RADIUS = 12
43 ARROW_OFFSET = BALLOON_RADIUS + 25
44 DATA_INDICATOR_SIZE = 10
45 CONTRAST_RATIO_THREASHOLD = 2250
46 WHITE = (255, 255, 255)
47 BLACK = (0, 0, 0)
48
49
51
53 self.event_text_font = Font(8)
54 self._create_pens()
55 self._create_brushes()
56 self._fixed_ys = {}
57 self._do_draw_top_scale = False
58 self._do_draw_bottom_scale = True
59 self._do_draw_divider_line = False
60
62 self.event_box_drawer = event_box_drawer
63
65 self.background_drawer = background_drawer
66
68 self.event_text_font.increment(step)
69 self._adjust_outer_padding_to_font_size()
70
72 if self.event_text_font.PointSize > step:
73 self.event_text_font.decrement(step)
74 self._adjust_outer_padding_to_font_size()
75
77 if self.event_text_font.PointSize < 8:
78 self.outer_padding = OUTER_PADDING * self.event_text_font.PointSize / 8
79 else:
80 self.outer_padding = OUTER_PADDING
81
83 self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID)
84 self.black_solid_pen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.PENSTYLE_SOLID)
85 self.darkred_solid_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID)
86 self.minor_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_USER_DASH)
87 self.minor_strip_pen.SetDashes([2, 2])
88 self.minor_strip_pen.SetCap(wx.CAP_BUTT)
89 self.major_strip_pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.PENSTYLE_SOLID)
90 self.now_pen = wx.Pen(wx.Colour(200, 0, 0), 1, wx.PENSTYLE_SOLID)
91 self.red_solid_pen = wx.Pen(wx.Colour(255, 0, 0), 1, wx.PENSTYLE_SOLID)
92
94 self.white_solid_brush = wx.Brush(wx.Colour(255, 255, 255), wx.BRUSHSTYLE_SOLID)
95 self.black_solid_brush = wx.Brush(wx.Colour(0, 0, 0), wx.BRUSHSTYLE_SOLID)
96 self.red_solid_brush = wx.Brush(wx.Colour(255, 0, 0), wx.BRUSHSTYLE_SOLID)
97 self.lightgrey_solid_brush = wx.Brush(wx.Colour(230, 230, 230), wx.BRUSHSTYLE_SOLID)
98
102
103 - def _get_text_extent(self, text):
104 self.dc.SetFont(self.event_text_font)
105 tw, th = self.dc.GetTextExtent(text)
106 return (tw, th)
107
110
111 - def draw(self, dc, timeline, view_properties, appearance, fast_draw=False):
141
142 - def _create_scene(self, size, db, view_properties, get_text_extent_fn):
150
152 periods = view_properties.periods
153 view_properties.set_displayed_period(TimePeriod(periods[0].start_time, periods[-1].end_time), False)
154 large_size = (size[0] * len(periods), size[1])
155 scene = self._create_scene(large_size, db, view_properties, get_text_extent_fn)
156 for (evt, rect) in scene.event_data:
157 self._fixed_ys[evt.id] = rect.GetY()
158
166
171
173 if view_properties._selection_rect:
174 self.dc.SetPen(wx.BLACK_PEN)
175 self.dc.SetBrush(wx.Brush(wx.WHITE, style=BRUSHSTYLE_TRANSPARENT))
176 self.dc.DrawRectangle(*view_properties._selection_rect)
177
184
185 - def snap(self, time, snap_region=10):
186 if self._distance_to_left_border(time) < snap_region:
187 return self._get_time_at_left_border(time)
188 elif self._distance_to_right_border(time) < snap_region:
189 return self._get_time_at_right_border(time)
190 else:
191 return time
192
196
200
202 left_strip_time, _ = self._snap_region(time)
203 return left_strip_time
204
206 _, right_strip_time = self._snap_region(time)
207 return right_strip_time
208
210 left_strip_time = self.scene.minor_strip.start(time)
211 right_strip_time = self.scene.minor_strip.increment(left_strip_time)
212 return (left_strip_time, right_strip_time)
213
217
218 - def event_at(self, x, y, alt_down=False):
231
235
241
243 container_event = None
244 container_rect = None
245 for (event, rect) in self.scene.event_data:
246 if rect.Contains(wx.Point(x, y)):
247 if event.is_container():
248 if alt_down:
249 return event, rect
250 container_event = event
251 container_rect = rect
252 else:
253 return event, rect
254 if container_event is None:
255 return None
256 return container_event, container_rect
257
263
265 event = None
266 for (event_in_list, rect) in self.balloon_data:
267 if rect.Contains(wx.Point(x, y)):
268 event = event_in_list
269 return event
270
273
279
281 if not view_properties.period_selection:
282 return
283 start, end = view_properties.period_selection
284 start_x = self.scene.x_pos_for_time(start)
285 end_x = self.scene.x_pos_for_time(end)
286 self.dc.SetBrush(self.lightgrey_solid_brush)
287 self.dc.SetPen(wx.TRANSPARENT_PEN)
288 self.dc.DrawRectangle(start_x, 0, end_x - start_x + 1, self.scene.height)
289
291 if self.fast_draw:
292 self._draw_fast_bg()
293 else:
294 self._draw_normal_bg()
295
297 self._draw_minor_strips()
298 self._draw_divider_line()
299
301 self._draw_major_strips()
302 self._draw_minor_strips()
303 self._draw_divider_line()
304 self._draw_now_line()
305
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
353
355 if len(self.scene.major_strip_data) > 0:
356 strip_period = self.scene.major_strip_data[0]
357 label = self.scene.major_strip.label(strip_period.start_time, True)
358 strip_width = self.scene.width_of_period(strip_period)
359 tw, _ = self.dc.GetTextExtent(label)
360 self.use_major_strip_vertical_label = strip_width < (tw + 5)
361 else:
362 self.use_major_strip_vertical_label = False
363
367
374
378
382
397
401
404
413
416
417 - def _draw_line(self, view_properties, event, rect):
418 if self.appearance.get_draw_period_events_to_right():
419 x = rect.X
420 else:
421 x = self.scene.x_pos_for_time(event.mean_time())
422 y = rect.Y + rect.Height
423 y2 = self._get_end_of_line(event)
424 self._set_line_color(view_properties, event)
425 if event.is_period():
426 if self.appearance.get_draw_period_events_to_right():
427 x += 1
428 self.dc.DrawLine(x - 1, y, x - 1, y2)
429 self.dc.DrawLine(x + 1, y, x + 1, y2)
430 self.dc.DrawLine(x, y, x, y2)
431 self._draw_endpoint(event, x, y2)
432
434 if event.get_milestone():
435 size = 8
436 self.dc.SetBrush(wx.BLUE_BRUSH)
437 self.dc.DrawPolygon([wx.Point(-size),
438 wx.Point(0, -size),
439 wx.Point(size, 0),
440 wx.Point(0, size)], x, y)
441 else:
442 self.dc.DrawCircle(x, y, 2)
443
452
459
461 if view_properties.is_selected(event):
462 self.dc.SetPen(self.red_solid_pen)
463 self.dc.SetBrush(self.red_solid_brush)
464 else:
465 self.dc.SetBrush(self.black_solid_brush)
466 self.dc.SetPen(self.black_solid_pen)
467
470
478
482
485
496
501
506
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
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
548
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
638
639
640
641 self.balloon_data.append((event, bounding_rect))
642
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
672 gc = wx.GraphicsContext.Create(self.dc)
673 path = gc.CreatePath()
674
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
682 (tipx, tipy) = tip_pos
683 p0 = wx.Point(tipx, tipy)
684 path.MoveToPoint(p0.x, p0.y)
685
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
690 p2 = wx.Point(p1.x - ARROW_OFFSET + R, p1.y)
691 path.AddLineToPoint(p2.x, p2.y)
692
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
696 p4 = wx.Point(p3.x - R, p3.y - H + R)
697 left_x = p4.x
698 path.AddLineToPoint(p4.x, p4.y)
699
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
703 p6 = wx.Point(p5.x + W - R, p5.y - R)
704 top_y = p6.y
705 path.AddLineToPoint(p6.x, p6.y)
706
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
710 p8 = wx.Point(p7.x + R, p7.y + H - R)
711 path.AddLineToPoint(p8.x, p8.y)
712
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
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
720
721 gc.Translate(0.5, 0.5)
722
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
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
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
749
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
765 else:
766 break_sentence(dc, lines, sentence, max_width_in_px)
767 return lines
768
769
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
778 if w > max_width_in_px:
779 lines.append("".join(line))
780 line = []
781 line.append(word + " ")
782
783 if word.endswith('-'):
784 lines.append("".join(line))
785 line = []
786 if len(line) > 0:
787 lines.append("".join(line))
788
789
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
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
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