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