Gnumed.timelinelib.canvas.timelinecanvas

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
    304 -    def SpecialScrollVerticallyOnMouseWheel(self, evt):
    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