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.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
41 INNER_PADDING = 3
42 PERIOD_THRESHOLD = 20
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
52
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
63 self.event_box_drawer = event_box_drawer
64
66 self.background_drawer = background_drawer
67
69 self.event_text_font.increment(step)
70 self._adjust_outer_padding_to_font_size()
71
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
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
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
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
111
112 - def draw(self, dc, timeline, view_properties, appearance, fast_draw=False):
142
143 - def _create_scene(self, size, db, view_properties, get_text_extent_fn):
151
159
167
172
178
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
197
201
203 left_strip_time, _ = self._snap_region(time)
204 return left_strip_time
205
207 _, right_strip_time = self._snap_region(time)
208 return right_strip_time
209
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
218
219 - def event_at(self, x, y, alt_down=False):
232
236
242
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
264
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
274
280
290
292 if self.fast_draw:
293 self._draw_fast_bg()
294 else:
295 self._draw_normal_bg()
296
298 self._draw_minor_strips()
299 self._draw_divider_line()
300
302 self._draw_major_strips()
303 self._draw_minor_strips()
304 self._draw_divider_line()
305 self._draw_now_line()
306
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
346
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
368
375
379
383
398
402
405
414
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
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
453
460
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
471
478
482
485
496
501
506
520
526
527 - def _draw_box(self, rect, event, view_properties):
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