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
35
36 OUTER_PADDING = 5
37 INNER_PADDING = 3
38 PERIOD_THRESHOLD = 20
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
48
50 self.event_text_font = Font(8)
51 self._create_pens()
52 self._create_brushes()
53 self._fixed_ys = {}
54
56 self.event_box_drawer = event_box_drawer
57
59 self.background_drawer = background_drawer
60
62 self.event_text_font.increment(step)
63 self._adjust_outer_padding_to_font_size()
64
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
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
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
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
104
105 - def draw(self, dc, timeline, view_properties, appearance, fast_draw=False):
134
135 - def _create_scene(self, size, db, view_properties, get_text_extent_fn):
143
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
159
164
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
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
189
193
195 left_strip_time, _ = self._snap_region(time)
196 return left_strip_time
197
199 _, right_strip_time = self._snap_region(time)
200 return right_strip_time
201
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
210
211 - def event_at(self, x, y, alt_down=False):
224
228
234
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
256
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
266
272
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
284 if self.fast_draw:
285 self._draw_fast_bg()
286 else:
287 self._draw_normal_bg()
288
290 self._draw_minor_strips()
291 self._draw_divider_line()
292
294 self._draw_minor_strips()
295 self._draw_major_strips()
296 self._draw_divider_line()
297 self._draw_now_line()
298
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
308
318
331
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
353
360
364
368
383
387
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
401
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
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
440
447
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
462
470
474
477
488
493
498
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
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
525 """Draw ballons on selected events that has 'description' data."""
526 self.balloon_data = []
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
538 if top_event is not None:
539 self._draw_ballon(top_event, top_rect, False)
540
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
630
631
632
633 self.balloon_data.append((event, bounding_rect))
634
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
664 gc = wx.GraphicsContext.Create(self.dc)
665 path = gc.CreatePath()
666
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
674 (tipx, tipy) = tip_pos
675 p0 = wx.Point(tipx, tipy)
676 path.MoveToPoint(p0.x, p0.y)
677
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
682 p2 = wx.Point(p1.x - ARROW_OFFSET + R, p1.y)
683 path.AddLineToPoint(p2.x, p2.y)
684
685 p3 = wx.Point(p2.x, p2.y - R)
686 path.AddArc(p3.x, p3.y, R, math.radians(90), math.radians(180))
687
688 p4 = wx.Point(p3.x - R, p3.y - H + R)
689 left_x = p4.x
690 path.AddLineToPoint(p4.x, p4.y)
691
692 p5 = wx.Point(p4.x + R, p4.y)
693 path.AddArc(p5.x, p5.y, R, math.radians(180), math.radians(-90))
694
695 p6 = wx.Point(p5.x + W - R, p5.y - R)
696 top_y = p6.y
697 path.AddLineToPoint(p6.x, p6.y)
698
699 p7 = wx.Point(p6.x, p6.y + R)
700 path.AddArc(p7.x, p7.y, R, math.radians(-90), math.radians(0))
701
702 p8 = wx.Point(p7.x + R, p7.y + H - R)
703 path.AddLineToPoint(p8.x, p8.y)
704
705 p9 = wx.Point(p8.x - R, p8.y)
706 path.AddArc(p9.x, p9.y, R, math.radians(0), math.radians(90))
707
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
712
713 gc.Translate(0.5, 0.5)
714
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
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
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
741
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
757 else:
758 break_sentence(dc, lines, sentence, max_width_in_px)
759 return lines
760
761
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
770 if w > max_width_in_px:
771 lines.append("".join(line))
772 line = []
773 line.append(word + " ")
774
775 if word.endswith('-'):
776 lines.append("".join(line))
777 line = []
778 if len(line) > 0:
779 lines.append("".join(line))
780
781
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
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
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