Package Gnumed :: Package timelinelib :: Package canvas :: Module timelinecanvas
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.canvas.timelinecanvas

  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 wx 
 20   
 21  from timelinelib.canvas.events import create_divider_position_changed_event 
 22  from timelinelib.canvas.timelinecanvascontroller import TimelineCanvasController 
 23  from timelinelib.wxgui.keyboard import Keyboard 
 24  from timelinelib.wxgui.cursor import Cursor 
 25  from timelinelib.canvas.data import TimePeriod 
 26  from timelinelib.canvas.highlighttimer import HighlightTimer 
 27  import timelinelib.wxgui.utils as guiutils 
 28   
 29   
 30  MOVE_HANDLE = 0 
 31  LEFT_RESIZE_HANDLE = 1 
 32  RIGHT_RESIZE_HANDLE = 2 
 33  # Used by Sizer and Mover classes to detect when to go into action 
 34  HIT_REGION_PX_WITH = 5 
 35  HSCROLL_STEP = 25 
36 37 38 -class TimelineCanvas(wx.Panel):
39 40 """ 41 This is the surface on which a timeline is drawn. It is also the object that handles user 42 input events such as mouse and keyboard actions. 43 """ 44 45 HORIZONTAL = 8 46 VERTICAL = 16 47 BOTH = 32 48 49 START = 0 50 DRAG = 1 51 STOP = 2 52
53 - def __init__(self, parent):
54 wx.Panel.__init__(self, parent, style=wx.NO_BORDER | wx.WANTS_CHARS) 55 self._controller = TimelineCanvasController(self) 56 self._surface_bitmap = None 57 self._create_gui() 58 self.SetDividerPosition(50) 59 self._highlight_timer = HighlightTimer(self._highlight_timer_tick) 60 self._last_balloon_event = None 61 self._waiting = False
62
63 - def GetAppearance(self):
64 return self._controller.get_appearance()
65
66 - def SetAppearance(self, appearance):
67 self._controller.set_appearance(appearance)
68
69 - def GetDividerPosition(self):
70 return self._divider_position
71
72 - def SetDividerPosition(self, position):
73 self._divider_position = int(min(100, max(0, position))) 74 self.PostEvent(create_divider_position_changed_event()) 75 self._controller.redraw_timeline()
76
77 - def GetHiddenEventCount(self):
78 return self._controller.get_hidden_event_count()
79
80 - def Scroll(self, factor):
81 self.Navigate(lambda tp: tp.move_delta(-tp.delta() * factor))
82
83 - def DrawSelectionRect(self, cursor):
84 self._controller.set_selection_rect(cursor)
85
86 - def RemoveSelectionRect(self):
87 self._controller.remove_selection_rect()
88
89 - def UseFastDraw(self, use):
90 self._controller.use_fast_draw(use) 91 self.Redraw()
92
93 - def GetHScrollAmount(self):
94 return self._controller.get_hscroll_amount()
95
96 - def SetHScrollAmount(self, amount):
97 self._controller.set_hscroll_amount(amount)
98
99 - def IncrementEventTextFont(self):
100 self._controller.increment_font_size()
101
102 - def DecrementEventTextFont(self):
103 self._controller.decrement_font_size()
104
105 - def SetPeriodSelection(self, period):
106 self._controller.set_period_selection(period)
107
108 - def Snap(self, time):
109 return self._controller.snap(time)
110
111 - def PostEvent(self, event):
112 wx.PostEvent(self, event)
113
114 - def SetEventBoxDrawer(self, event_box_drawer):
115 self._controller.set_event_box_drawer(event_box_drawer) 116 self.Redraw()
117
118 - def SetEventSelected(self, event, is_selected):
119 self._controller.set_selected(event, is_selected)
120
121 - def ClearSelectedEvents(self):
122 self._controller.clear_selected()
123
124 - def SelectAllEvents(self):
125 self._controller.select_all_events()
126
127 - def IsEventSelected(self, event):
128 return self._controller.is_selected(event)
129
130 - def SetHoveredEvent(self, event):
131 self._controller.set_hovered_event(event)
132
133 - def GetHoveredEvent(self):
134 return self._controller.get_hovered_event
135
136 - def GetSelectedEvent(self):
137 selected_events = self.GetSelectedEvents() 138 if len(selected_events) == 1: 139 return selected_events[0] 140 return None
141
142 - def GetSelectedEvents(self):
143 return self._controller.get_selected_events()
144
145 - def GetClosestOverlappingEvent(self, event, up):
146 return self._controller.get_closest_overlapping_event(event, up=up)
147
148 - def GetTimeType(self):
149 return self.GetDb().get_time_type()
150
151 - def GetDb(self):
152 return self._controller.get_timeline()
153
154 - def IsReadOnly(self):
155 return self.GetDb().is_read_only()
156
157 - def GetEventAtCursor(self, prefer_container=False):
158 cursor = Cursor(*self.ScreenToClient(wx.GetMousePosition())) 159 return self.GetEventAt(cursor, prefer_container)
160
161 - def GetEventAt(self, cursor, prefer_container=False):
162 return self._controller.event_at(cursor.x, cursor.y, prefer_container)
163
164 - def SelectEventsInRect(self, rect):
165 self._controller.select_events_in_rect(rect)
166
167 - def GetEventWithHitInfoAt(self, cursor, keyboard=Keyboard()):
168 x, y = cursor.pos 169 prefer_container = keyboard 170 event_and_rect = self._controller.event_with_rect_at(x, y, prefer_container.alt) 171 if event_and_rect is not None: 172 event, rect = event_and_rect 173 center = rect.X + rect.Width // 2 174 if abs(x - center) <= HIT_REGION_PX_WITH: 175 return (event, MOVE_HANDLE) 176 elif abs(x - rect.X) < HIT_REGION_PX_WITH: 177 return (event, LEFT_RESIZE_HANDLE) 178 elif abs(rect.X + rect.Width - x) < HIT_REGION_PX_WITH: 179 return (event, RIGHT_RESIZE_HANDLE) 180 return None
181
182 - def GetBalloonAtCursor(self):
183 cursor = Cursor(*self.ScreenToClient(wx.GetMousePosition())) 184 return self._controller.balloon_at(cursor)
185
186 - def GetBalloonAt(self, cursor):
187 return self._controller.balloon_at(cursor)
188
189 - def EventHasStickyBalloon(self, event):
190 return self._controller.event_has_sticky_balloon(event)
191
192 - def SetEventStickyBalloon(self, event, is_sticky):
193 self._controller.set_event_sticky_balloon(event, is_sticky)
194
195 - def GetTimeAt(self, x):
196 return self._controller.get_time(x)
197
198 - def SetTimeline(self, timeline):
199 self._controller.set_timeline(timeline)
200
201 - def GetViewProperties(self):
202 return self._controller.get_view_properties()
203
204 - def SaveAsPng(self, path):
205 self._surface_bitmap.ConvertToImage().SaveFile(path, wx.BITMAP_TYPE_PNG)
206
207 - def SaveAsSvg(self, path):
208 from timelinelib.canvas.svg import export 209 export(path, self._controller.get_timeline(), self._controller.scene, 210 self._controller.get_view_properties(), self.GetAppearance())
211
212 - def GetFilteredEvents(self, search_target, search_period):
213 events = self.GetDb().search(search_target) 214 return self._controller.filter_events(events, search_period)
215
216 - def GetTimePeriod(self):
217 return self._controller.get_time_period()
218
219 - def Navigate(self, navigation_fn):
220 self._controller.navigate(navigation_fn)
221
222 - def Redraw(self):
223 self._controller.redraw_timeline()
224
225 - def EventIsPeriod(self, event):
226 return self._controller.event_is_period(event)
227
228 - def RedrawSurface(self, fn_draw):
229 width, height = self.GetSize() 230 self._surface_bitmap = wx.Bitmap(width, height) 231 memdc = wx.MemoryDC() 232 memdc.SelectObject(self._surface_bitmap) 233 memdc.SetBackground(wx.Brush(wx.WHITE, wx.BRUSHSTYLE_SOLID)) 234 memdc.Clear() 235 fn_draw(memdc) 236 del memdc 237 self.Refresh() 238 self.Update()
239
240 - def set_size_cursor(self):
241 self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE))
242
243 - def set_move_cursor(self):
244 self.SetCursor(wx.Cursor(wx.CURSOR_SIZING))
245
246 - def set_default_cursor(self):
247 guiutils.set_default_cursor(self)
248
249 - def zoom_in(self):
250 self.Zoom(1, self._get_half_width())
251
252 - def zoom_out(self):
253 self.Zoom(-1, self._get_half_width())
254
255 - def Zoom(self, direction, x):
256 """ zoom time line at position x """ 257 width, _ = self.GetSize() 258 x_percent_of_width = x / width 259 self.Navigate(lambda tp: tp.zoom(direction, x_percent_of_width))
260
261 - def vertical_zoom_in(self):
262 self.ZoomVertically(1)
263
264 - def vertical_zoom_out(self):
265 self.ZoomVertically(-1)
266
267 - def ZoomVertically(self, direction):
268 if direction > 0: 269 self.IncrementEventTextFont() 270 else: 271 self.DecrementEventTextFont()
272
273 - def Scrollvertically(self, direction):
274 if direction > 0: 275 self._scroll_up() 276 else: 277 self._scroll_down() 278 self.Redraw()
279 280 # ----(Helper functions simplifying usage of timeline component)-------- 281
282 - def SetStartTime(self, evt):
283 self._start_time = self.GetTimeAt(evt.GetX())
284
285 - def _direction(self, evt):
286 rotation = evt.GetWheelRotation() 287 return 1 if rotation > 0 else -1 if rotation < 0 else 0
288
289 - def ZoomHorizontallyOnMouseWheel(self, evt):
290 self.Zoom(self._direction(evt), evt.GetX())
291
292 - def ZoomVerticallyOnMouseWheel(self, evt):
293 if self._direction(evt) > 0: 294 self.IncrementEventTextFont() 295 else: 296 self.DecrementEventTextFont()
297
298 - def ScrollHorizontallyOnMouseWheel(self, evt):
299 self.Scroll(evt.GetWheelRotation() / 1200.0)
300
301 - def ScrollVerticallyOnMouseWheel(self, evt):
302 self.SetDividerPosition(self.GetDividerPosition() + self._direction(evt))
303
305 self.Scrollvertically(self._direction(evt))
306
307 - def DisplayBalloons(self, evt):
308 309 def cursor_has_left_event(): 310 # TODO: Can't figure out why self.GetEventAtCursor() returns None 311 # in this situation. The LeftDown check saves us for the moment. 312 if wx.GetMouseState().LeftIsDown(): 313 return False 314 else: 315 return self.GetEventAtCursor() != self._last_balloon_event
316 317 def no_balloon_at_cursor(): 318 return not self.GetBalloonAtCursor()
319 320 def update_last_seen_event(): 321 if self._last_balloon_event is None: 322 self._last_balloon_event = self.GetEventAtCursor() 323 elif cursor_has_left_event() and no_balloon_at_cursor(): 324 self._last_balloon_event = None 325 return self._last_balloon_event 326 327 def delayed_call(): 328 if self.GetAppearance().get_balloons_visible(): 329 self.SetHoveredEvent(self._last_balloon_event) 330 self._waiting = False 331 332 # Same delay as when we used timers 333 # Don't issue call when in wait state, to avoid flicker 334 if not self._waiting: 335 update_last_seen_event() 336 self._wating = True 337 wx.CallLater(500, delayed_call) 338
339 - def GetTimelineInfoText(self, evt):
340 341 def format_current_pos_time_string(x): 342 tm = self.GetTimeAt(x) 343 return self.GetTimeType().format_period(TimePeriod(tm, tm))
344 345 event = self.GetEventAtCursor() 346 if event: 347 return event.get_label(self.GetTimeType()) 348 else: 349 return format_current_pos_time_string(evt.GetX()) 350
351 - def SetCursorShape(self, evt):
352 353 def get_cursor(): 354 return Cursor(evt.GetX(), evt.GetY())
355 356 def get_keyboard(): 357 return Keyboard(evt.ControlDown(), evt.ShiftDown(), evt.AltDown()) 358 359 def hit_resize_handle(): 360 try: 361 event, hit_info = self.GetEventWithHitInfoAt(get_cursor(), get_keyboard()) 362 if event.get_locked(): 363 return None 364 if event.is_milestone(): 365 return None 366 if not self.IsEventSelected(event): 367 return None 368 if hit_info == LEFT_RESIZE_HANDLE: 369 return wx.LEFT 370 if hit_info == RIGHT_RESIZE_HANDLE: 371 return wx.RIGHT 372 return None 373 except: 374 return None 375 376 def hit_move_handle(): 377 event_and_hit_info = self.GetEventWithHitInfoAt(get_cursor(), get_keyboard()) 378 if event_and_hit_info is None: 379 return False 380 (event, hit_info) = event_and_hit_info 381 if event.get_locked(): 382 return False 383 if not self.IsEventSelected(event): 384 return False 385 if event.get_ends_today(): 386 return False 387 return hit_info == MOVE_HANDLE 388 389 def over_resize_handle(): 390 return hit_resize_handle() is not None 391 392 def over_move_handle(): 393 return hit_move_handle() 394 395 if over_resize_handle(): 396 self.set_size_cursor() 397 elif over_move_handle(): 398 self.set_move_cursor() 399 else: 400 self.set_default_cursor() 401
402 - def CenterAtCursor(self, evt):
403 _time_at_cursor = self.GetTimeAt(evt.GetX()) 404 self.Navigate(lambda tp: tp.center(_time_at_cursor))
405
406 - def ToggleEventSelection(self, evt):
407 408 def get_cursor(): 409 return Cursor(evt.GetX(), evt.GetY())
410 411 event = self.GetEventAt(get_cursor(), evt.AltDown()) 412 if event: 413 self.SetEventSelected(event, not self.IsEventSelected(event)) 414
415 - def InitDragScroll(self, direction=wx.HORIZONTAL):
416 self._scrolling = False 417 self._scrolling_direction = direction
418
419 - def StartDragScroll(self, evt):
420 self._scrolling = True 421 self._drag_scroll_start_time = self.GetTimeAt(evt.GetX()) 422 self._start_mouse_pos = evt.GetY() 423 self._start_divider_pos = self.GetDividerPosition()
424
425 - def DragScroll(self, evt):
426 if self._scrolling: 427 if self._scrolling_direction in (wx.HORIZONTAL, wx.BOTH): 428 delta = self._drag_scroll_start_time - self.GetTimeAt(evt.GetX()) 429 self.Navigate(lambda tp: tp.move_delta(delta)) 430 if self._scrolling_direction in (wx.VERTICAL, wx.BOTH): 431 percentage_distance = 100 * float(evt.GetY() - self._start_mouse_pos) / float(self.GetSize()[1]) 432 new_pos = self._start_divider_pos + percentage_distance 433 self.SetDividerPosition(new_pos)
434
435 - def StopDragScroll(self):
436 self._scrolling = False
437
438 - def InitDragEventSelect(self):
439 self._selecting = False
440
441 - def StartDragEventSelect(self, evt):
442 self._selecting = True 443 self._cursor = self.GetCursor(evt)
444
445 - def DragEventSelect(self, evt):
446 if self._selecting: 447 cursor = self.GetCursor(evt) 448 self._cursor.move(*cursor.pos) 449 self.DrawSelectionRect(self._cursor)
450
451 - def GetCursor(self, evt):
452 return Cursor(evt.GetX(), evt.GetY())
453
454 - def StopDragEventSelect(self):
455 if self._selecting: 456 self.SelectEventsInRect(self._cursor.rect) 457 self.RemoveSelectionRect() 458 self._selecting = False
459
460 - def InitZoomSelect(self):
461 self._zooming = False
462
463 - def StartZoomSelect(self, evt):
464 self._zooming = True 465 self._start_time = self.GetTimeAt(evt.GetX()) 466 self._end_time = self.GetTimeAt(evt.GetX())
467
468 - def DragZoom(self, evt):
469 if self._zooming: 470 self._end_time = self.GetTimeAt(evt.GetX()) 471 self.SetPeriodSelection(TimePeriod(self._start_time, self._end_time))
472
473 - def StopDragZoom(self):
474 self._zooming = False 475 self.SetPeriodSelection(None) 476 self.Navigate(lambda tp: tp.update(self._start_time, self._end_time))
477
478 - def InitDragPeriodSelect(self):
479 self._period_select = False
480
481 - def StartDragPeriodSelect(self, evt):
482 self._period_select = True 483 self._start_time = self.GetTimeAt(evt.GetX()) 484 self._end_time = self.GetTimeAt(evt.GetX())
485
486 - def DragPeriodSelect(self, evt):
487 if self._period_select: 488 self._end_time = self.GetTimeAt(evt.GetX()) 489 self.SetPeriodSelection(TimePeriod(self._start_time, self._end_time))
490
491 - def StopDragPeriodSelect(self):
492 self._period_select = False 493 self.SetPeriodSelection(None) 494 return self._start_time, self._end_time
495
496 - def InitDrag(self, scroll=None, zoom=None, period_select=None, event_select=None):
497 498 def init_scroll(): 499 if self.BOTH & scroll: 500 self.InitDragScroll(direction=wx.BOTH) 501 self._drag_scroll = scroll - self.BOTH 502 elif self.HORIZONTAL & scroll: 503 self.InitDragScroll(direction=wx.HORIZONTAL) 504 self._drag_scroll = scroll - self.HORIZONTAL 505 elif self.VERTICAL & scroll: 506 self.InitDragScroll(direction=wx.VERTICAL) 507 self._drag_scroll = scroll - self.VERTICAL 508 else: 509 self._drag_scroll = None 510 if self._drag_scroll is not None: 511 self._methods[self._drag_scroll] = (self.StartDragScroll, 512 self.DragScroll, 513 self.StopDragScroll)
514 515 def init_zoom(): 516 if zoom not in self._methods: 517 self.InitZoomSelect() 518 self._methods[zoom] = (self.StartZoomSelect, 519 self.DragZoom, 520 self.StopDragZoom) 521 522 def init_period_select(): 523 if not period_select in self._methods: 524 self.InitDragPeriodSelect() 525 self._methods[period_select] = (self.StartDragPeriodSelect, 526 self.DragPeriodSelect, 527 self.StopDragPeriodSelect) 528 529 def init_event_select(): 530 if not event_select in self._methods: 531 self.InitDragEventSelect() 532 self._methods[event_select] = (self.StartDragEventSelect, 533 self.DragEventSelect, 534 self.StopDragEventSelect) 535 536 self._drag_scroll = scroll 537 self._drag_zoom = zoom 538 self._drag_period_select = period_select 539 self._drag_event_select = event_select 540 self._methods = {} 541 542 if scroll: 543 init_scroll() 544 if zoom: 545 init_zoom() 546 if period_select: 547 init_period_select() 548 if event_select: 549 init_event_select() 550
551 - def CallDragMethod(self, index, evt):
552 553 def calc_cotrol_keys_value(evt): 554 combo = 0 555 if evt.ControlDown(): 556 combo += Keyboard.CTRL 557 if evt.ShiftDown(): 558 combo += Keyboard.SHIFT 559 if evt.AltDown(): 560 combo += Keyboard.ALT 561 return combo
562 563 combo = calc_cotrol_keys_value(evt) 564 if combo in self._methods: 565 if index == self.STOP: 566 self._methods[combo][index]() 567 else: 568 self._methods[combo][index](evt) 569
570 - def GetPeriodChoices(self):
571 return self._controller.get_period_choices()
572 573 @property
574 - def view_properties(self):
575 return self._controller.view_properties
576 577 # ------------ 578
579 - def _scroll_up(self):
580 self.SetHScrollAmount(max(0, self.GetHScrollAmount() - HSCROLL_STEP))
581
582 - def _scroll_down(self):
583 self.SetHScrollAmount(self.GetHScrollAmount() + HSCROLL_STEP)
584
585 - def _get_half_width(self):
586 return self.GetSize()[0] // 2
587
588 - def _create_gui(self):
589 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background) 590 self.Bind(wx.EVT_PAINT, self._on_paint) 591 self.Bind(wx.EVT_SIZE, self._on_size)
592
593 - def _on_erase_background(self, event):
594 # For double buffering 595 pass
596
597 - def _on_paint(self, event):
598 dc = wx.AutoBufferedPaintDC(self) 599 if self._surface_bitmap: 600 dc.DrawBitmap(self._surface_bitmap, 0, 0, True) 601 else: 602 pass # TODO: Fill with white?
603
604 - def _on_size(self, evt):
605 self._controller.window_resized()
606
607 - def HighligtEvent(self, event, clear=False):
608 self._controller.add_highlight(event, clear) 609 self._highlight_timer.start_highlighting()
610
611 - def _highlight_timer_tick(self):
612 self.Redraw() 613 self._controller.tick_highlights() 614 if not self._controller.has_higlights(): 615 self._highlight_timer.Stop()
616