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):
     66          self.controller.set_appearance(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):
     77          return self.controller.get_hidden_event_count()
     78
     79 -    def Scroll(self, factor):
     80          self.Navigate(lambda tp: tp.move_delta(-tp.delta() * factor))
     81
     82 -    def DrawSelectionRect(self, cursor):
     83          self.controller.set_selection_rect(cursor)
     84
     85 -    def RemoveSelectionRect(self):
     86          self.controller.remove_selection_rect()
     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):
     99          self.controller.increment_font_size()
    100
    101 -    def DecrementEventTextFont(self):
    102          self.controller.decrement_font_size()
    103
    104 -    def SetPeriodSelection(self, period):
    105          self.controller.set_period_selection(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):
    118          self.controller.set_selected(event, is_selected)
    119
    120 -    def ClearSelectedEvents(self):
    121          self.controller.clear_selected()
    122
    123 -    def SelectAllEvents(self):
    124          self.controller.select_all_events()
    125
    126 -    def IsEventSelected(self, event):
    127          return self.controller.is_selected(event)
    128
    129 -    def SetHoveredEvent(self, event):
    130          self.controller.set_hovered_event(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):
    145          return self.controller.get_closest_overlapping_event(event, up=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):
    164          self.controller.select_events_in_rect(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):
    189          return self.controller.event_has_sticky_balloon(event)
    190
    191 -    def SetEventStickyBalloon(self, event, is_sticky):
    192          self.controller.set_event_sticky_balloon(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):
    207          from timelinelib.canvas.svg import export
    208          export(path, self.controller.get_timeline(), self.controller.scene,
    209                 self.controller.get_view_properties(), self.GetAppearance())
    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):
    222          self.controller.redraw_timeline()
    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
    308 -    def SpecialScrollVerticallyOnMouseWheel(self, evt):
    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