Package Gnumed :: Package timelinelib :: Package view :: Module drawingarea |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.view.drawingarea

      1  # Copyright (C) 2009, 2010, 2011  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  import webbrowser
     21
     22  from timelinelib.db.exceptions import TimelineIOError
     23  from timelinelib.db.objects import TimeOutOfRangeLeftError
     24  from timelinelib.db.objects import TimeOutOfRangeRightError
     25  from timelinelib.db.observer import STATE_CHANGE_ANY
     26  from timelinelib.db.observer import STATE_CHANGE_CATEGORY
     27  from timelinelib.drawing.viewproperties import ViewProperties
     28  from timelinelib.utils import ex_msg
     29  from timelinelib.view.move import MoveByDragInputHandler
     30  from timelinelib.view.noop import NoOpInputHandler
     31  from timelinelib.view.periodevent import CreatePeriodEventByDragInputHandler
     32  from timelinelib.view.resize import ResizeByDragInputHandler
     33  from timelinelib.view.scrolldrag import ScrollByDragInputHandler
     34  from timelinelib.view.zoom import ZoomByDragInputHandler
     35
     36
     37  # The width in pixels of the vertical scroll zones.
     38  # When the mouse reaches the any of the two scroll zone areas, scrolling
     39  # of the timeline will take place if there is an ongoing selection of the
     40  # timeline. The scroll zone areas are found at the beginning and at the
     41  # end of the timeline.
     42  SCROLL_ZONE_WIDTH = 20
     43
     44  LEFT_RIGHT_SCROLL_FACTOR = 1 / 200.0
     45  MOUSE_SCROLL_FACTOR = 1 / 10.0
     46
     47
     48 -class DrawingArea(object):
     49
     50 -    def __init__(self, view, status_bar_adapter, config, drawing_algorithm,
     51                   divider_line_slider, fn_handle_db_error):
     52          self.config = config
     53          self.view = view
     54          self.status_bar_adapter = status_bar_adapter
     55          self.drawing_algorithm = drawing_algorithm
     56          self.divider_line_slider = divider_line_slider
     57          self.fn_handle_db_error = fn_handle_db_error
     58          self._set_initial_values_to_member_variables()
     59          self._set_colors_and_styles()
     60          self.divider_line_slider.Bind(wx.EVT_SLIDER,       self._slider_on_slider)
     61          self.divider_line_slider.Bind(wx.EVT_CONTEXT_MENU, self._slider_on_context_menu)
     62          self.change_input_handler_to_no_op()
     63          self.fast_draw = False
     64
     65 -    def change_input_handler_to_zoom_by_drag(self, start_time):
     66          self.input_handler = ZoomByDragInputHandler(self, self.status_bar_adapter, start_time)
     67
     68 -    def change_input_handler_to_create_period_event_by_drag(self, initial_time):
     69          self.input_handler = CreatePeriodEventByDragInputHandler(self, self.view, initial_time)
     70
     71 -    def change_input_handler_to_resize_by_drag(self, event, direction):
     72          self.input_handler = ResizeByDragInputHandler(
     73              self, self.status_bar_adapter, event, direction)
     74
     75 -    def change_input_handler_to_move_by_drag(self, event, start_drag_time):
     76          self.input_handler = MoveByDragInputHandler(
     77              self, self.status_bar_adapter, event, start_drag_time)
     78
     79 -    def change_input_handler_to_scroll_by_drag(self, start_time):
     80          self.input_handler = ScrollByDragInputHandler(self, start_time)
     81
     82 -    def change_input_handler_to_no_op(self):
     83          self.input_handler = NoOpInputHandler(self, self.view)
     84
     85 -    def get_drawer(self):
     86          return self.drawing_algorithm
     87
     88 -    def get_timeline(self):
     89          return self.timeline
     90
     91 -    def get_view_properties(self):
     92          return self.view_properties
     93
     94 -    def set_timeline(self, timeline):
     95          """Inform what timeline to draw."""
     96          self._unregister_timeline(self.timeline)
     97          if timeline is None:
     98              self._set_null_timeline()
     99          else:
    100              self._set_non_null_timeline(timeline)
    101
    102 -    def use_fast_draw(self, value):
    103          self.fast_draw = value
    104
    105 -    def _set_null_timeline(self):
    106          self.timeline = None
    107          self.time_type = None
    108          self.view.Disable()
    109
    110 -    def _set_non_null_timeline(self, timeline):
    111          self.timeline = timeline
    112          self.time_type = timeline.get_time_type()
    113          self.timeline.register(self._timeline_changed)
    114          properties_loaded = self._load_view_properties()
    115          if properties_loaded:
    116              self._redraw_timeline()
    117              self.view.Enable()
    118              self.view.SetFocus()
    119
    120 -    def _load_view_properties(self):
    121          properties_loaded = True
    122          try:
    123              self.view_properties.clear_db_specific()
    124              self.timeline.load_view_properties(self.view_properties)
    125              if self.view_properties.displayed_period is None:
    126                  default_tp = self.time_type.get_default_time_period()
    127                  self.view_properties.displayed_period = default_tp
    128          except TimelineIOError, e:
    129              self.fn_handle_db_error(e)
    130              properties_loaded = False
    131          return properties_loaded
    132
    133 -    def _unregister_timeline(self, timeline):
    134          if timeline != None:
    135              timeline.unregister(self._timeline_changed)
    136
    137 -    def show_hide_legend(self, show):
    138          self.view_properties.show_legend = show
    139          if self.timeline:
    140              self._redraw_timeline()
    141
    142 -    def get_time_period(self):
    143          """Return currently displayed time period."""
    144          if self.timeline == None:
    145              raise Exception(_("No timeline set"))
    146          return self.view_properties.displayed_period
    147
    148 -    def navigate_timeline(self, navigation_fn):
    149          """
    150          Perform a navigation operation followed by a redraw.
    151
    152          The navigation_fn should take one argument which is the time period
    153          that should be manipulated in order to carry out the navigation
    154          operation.
    155
    156          Should the navigation operation fail (max zoom level reached, etc) a
    157          message will be displayed in the statusbar.
    158
    159          Note: The time period should never be modified directly. This method
    160          should always be used instead.
    161          """
    162          if self.timeline == None:
    163              raise Exception(_("No timeline set"))
    164          try:
    165              self.view_properties.displayed_period = navigation_fn(self.view_properties.displayed_period)
    166              self._redraw_timeline()
    167              self.status_bar_adapter.set_text("")
    168          except (TimeOutOfRangeLeftError), e:
    169              self.status_bar_adapter.set_text(_("Can't scroll more to the left"))
    170          except (TimeOutOfRangeRightError), e:
    171              self.status_bar_adapter.set_text(_("Can't scroll more to the right"))
    172          except (ValueError, OverflowError), e:
    173              self.status_bar_adapter.set_text(ex_msg(e))
    174
    175 -    def redraw_timeline(self):
    176          self._redraw_timeline()
    177
    178 -    def window_resized(self):
    179          self._redraw_timeline()
    180
    181 -    def left_mouse_down(self, x, y, ctrl_down, shift_down, alt_down=False):
    182          self.input_handler.left_mouse_down(x, y, ctrl_down, shift_down, alt_down)
    183
    184 -    def right_mouse_down(self, x, y, alt_down=False):
    185          """
    186          Event handler used when the right mouse button has been pressed.
    187
    188          If the mouse hits an event and the timeline is not readonly, the
    189          context menu for that event is displayed.
    190          """
    191          if self.timeline.is_read_only():
    192              return
    193          self.context_menu_event = self.drawing_algorithm.event_at(x, y, alt_down)
    194          if self.context_menu_event is None:
    195              return
    196          menu_definitions = [
    197              (_("Edit"), self._context_menu_on_edit_event),
    198              (_("Duplicate..."), self._context_menu_on_duplicate_event),
    199              (_("Delete"), self._context_menu_on_delete_event),
    200          ]
    201          if self.context_menu_event.has_data():
    202              menu_definitions.append((_("Sticky Balloon"), self._context_menu_on_sticky_balloon_event))
    203          hyperlink = self.context_menu_event.get_data("hyperlink")
    204          if hyperlink is not None:
    205              menu_definitions.append((_("Goto URL"), self._context_menu_on_goto_hyperlink_event))
    206          menu = wx.Menu()
    207          for menu_definition in menu_definitions:
    208              text, method = menu_definition
    209              menu_item = wx.MenuItem(menu, wx.NewId(), text)
    210              self.view.Bind(wx.EVT_MENU, method, id=menu_item.GetId())
    211              menu.AppendItem(menu_item)
    212          self.view.PopupMenu(menu)
    213          menu.Destroy()
    214
    215 -    def _one_and_only_one_event_selected(self):
    216          selected_event_ids = self.view_properties.get_selected_event_ids()
    217          nbr_of_selected_event_ids = len(selected_event_ids)
    218          return nbr_of_selected_event_ids == 1
    219
    220 -    def _get_first_selected_event(self):
    221          selected_event_ids = self.view_properties.get_selected_event_ids()
    222          if len(selected_event_ids) > 0:
    223              id = selected_event_ids[0]
    224              return self.timeline.find_event_with_id(id)
    225          return None
    226
    229
    232
    236
    240
    244
    245
    246
    247 -    def left_mouse_dclick(self, x, y, ctrl_down, alt_down=False):
    248          """
    249          Event handler used when the left mouse button has been double clicked.
    250
    251          If the timeline is readonly, no action is taken.
    252          If the mouse hits an event, a dialog opens for editing this event.
    253          Otherwise a dialog for creating a new event is opened.
    254          """
    255          if self.timeline.is_read_only():
    256              return
    257          # Since the event sequence is, 1. EVT_LEFT_DOWN  2. EVT_LEFT_UP
    258          # 3. EVT_LEFT_DCLICK we must compensate for the toggle_event_selection
    259          # that occurs in the handling of EVT_LEFT_DOWN, since we still want
    260          # the event(s) selected or deselected after a left doubleclick
    261          # It doesn't look too god but I havent found any other way to do it.
    262          self._toggle_event_selection(x, y, ctrl_down, alt_down)
    263          event = self.drawing_algorithm.event_at(x, y, alt_down)
    264          if event:
    265              self.view.open_event_editor_for(event)
    266          else:
    267              current_time = self.get_time(x)
    268              self.view.open_create_event_editor(current_time, current_time)
    269
    270 -    def get_time(self, x):
    271          return self.drawing_algorithm.get_time(x)
    272
    273 -    def event_with_rect_at(self, x, y, alt_down=False):
    274          return self.drawing_algorithm.event_with_rect_at(x, y, alt_down)
    275
    276 -    def event_at(self, x, y, alt_down=False):
    277          return self.drawing_algorithm.event_at(x, y, alt_down)
    278
    279 -    def is_selected(self, event):
    280          return self.view_properties.is_selected(event)
    281
    282 -    def event_is_period(self, event):
    283          return self.get_drawer().event_is_period(event.time_period)
    284
    285 -    def snap(self, time):
    286          return self.get_drawer().snap(time)
    287
    288 -    def get_selected_events(self):
    289          return [self.timeline.find_event_with_id(id_) for id_ in
    290                  self.view_properties.get_selected_event_ids()]
    291
    292 -    def middle_mouse_clicked(self, x):
    293          self.navigate_timeline(lambda tp: tp.center(self.get_time(x)))
    294
    295 -    def left_mouse_up(self):
    296          self.input_handler.left_mouse_up()
    297
    298 -    def mouse_enter(self, x, left_is_down):
    299          """
    300          Mouse event handler, when the mouse is entering the window.
    301
    302          If there is an ongoing selection-marking (dragscroll timer running)
    303          and the left mouse button is not down when we enter the window, we
    304          want to simulate a 'mouse left up'-event, so that the dialog for
    305          creating an event will be opened or sizing, moving stops.
    306          """
    307          if self.dragscroll_timer_running:
    308              if not left_is_down:
    309                  self.left_mouse_up()
    310
    311 -    def mouse_moved(self, x, y, alt_down=False):
    312          self.input_handler.mouse_moved(x, y, alt_down)
    313
    314 -    def mouse_wheel_moved(self, rotation, ctrl_down, shift_down, x):
    315          direction = _step_function(rotation)
    316          if ctrl_down:
    317              self._zoom_timeline(direction, x)
    318          elif shift_down:
    319              self.divider_line_slider.SetValue(self.divider_line_slider.GetValue() + direction)
    320              self._redraw_timeline()
    321          else:
    322              self._scroll_timeline_view(direction)
    323
    324 -    def key_down(self, keycode, alt_down):
    325          if keycode == wx.WXK_DELETE:
    326              self._delete_selected_events()
    327          elif alt_down:
    328              if keycode == wx.WXK_UP:
    329                  self._move_event_vertically(up=True)
    330              elif keycode == wx.WXK_DOWN:
    331                  self._move_event_vertically(up=False)
    332              elif keycode == wx.WXK_RIGHT:
    333                  self._scroll_timeline_view_by_factor(LEFT_RIGHT_SCROLL_FACTOR)
    334              elif keycode == wx.WXK_LEFT:
    335                  self._scroll_timeline_view_by_factor(-LEFT_RIGHT_SCROLL_FACTOR)
    336
    337 -    def _move_event_vertically(self, up=True):
    338          if self._one_and_only_one_event_selected():
    339              selected_event = self._get_first_selected_event()
    340              (overlapping_event, direction) = self.drawing_algorithm.get_closest_overlapping_event(selected_event,
    341                                                                                                    up=up)
    342              if overlapping_event is None:
    343                  return
    344              if direction > 0:
    345                  self.timeline.place_event_after_event(selected_event,
    346                                                        overlapping_event)
    347              else:
    348                  self.timeline.place_event_before_event(selected_event,
    349                                                         overlapping_event)
    350              self._redraw_timeline()
    351
    352 -    def key_up(self, keycode):
    353          if keycode == wx.WXK_CONTROL:
    354              self.view.set_default_cursor()
    355
    356 -    def _slider_on_slider(self, evt):
    357          self._redraw_timeline()
    358
    368
    373
    374 -    def _timeline_changed(self, state_change):
    375          if (state_change == STATE_CHANGE_ANY or
    376              state_change == STATE_CHANGE_CATEGORY):
    377              self._redraw_timeline()
    378
    379 -    def _set_initial_values_to_member_variables(self):
    380          self.timeline = None
    381          self.view_properties = ViewProperties()
    382          self.view_properties.show_legend = self.config.get_show_legend()
    383          self.view_properties.show_balloons_on_hover = self.config.get_balloon_on_hover()
    384          self.dragscroll_timer_running = False
    385
    386 -    def _set_colors_and_styles(self):
    387          """Define the look and feel of the drawing area."""
    388          self.view.SetBackgroundColour(wx.WHITE)
    389          self.view.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
    390          self.view.set_default_cursor()
    391          self.view.Disable()
    392
    393 -    def _redraw_timeline(self):
    394          def fn_draw(dc):
    395              try:
    396                  self.drawing_algorithm.use_fast_draw(self.fast_draw)
    397                  self.drawing_algorithm.draw(dc, self.timeline, self.view_properties, self.config)
    398              except TimelineIOError, e:
    399                  self.fn_handle_db_error(e)
    400              finally:
    401                  self.drawing_algorithm.use_fast_draw(False)
    402          if self.timeline:
    403              self.view_properties.divider_position = (self.divider_line_slider.GetValue())
    404              self.view_properties.divider_position = (float(self.divider_line_slider.GetValue()) / 100.0)
    405              self.view.redraw_surface(fn_draw)
    406              self.view.enable_disable_menus()
    407              self._display_hidden_event_count()
    408
    409 -    def _display_hidden_event_count(self):
    410          text = _("%s events hidden") % self.drawing_algorithm.get_hidden_event_count()
    411          self.status_bar_adapter.set_hidden_event_count_text(text)
    412
    413 -    def _toggle_event_selection(self, xpixelpos, ypixelpos, control_down, alt_down=False):
    414          event = self.drawing_algorithm.event_at(xpixelpos, ypixelpos, alt_down)
    415          if event:
    416              selected = not self.view_properties.is_selected(event)
    417              if not control_down:
    418                  self.view_properties.clear_selected()
    419              self.view_properties.set_selected(event, selected)
    420          else:
    421              self.view_properties.clear_selected()
    422          self._redraw_timeline()
    423          return event != None
    424
    425 -    def _display_eventinfo_in_statusbar(self, xpixelpos, ypixelpos, alt_down=False):
    426          event = self.drawing_algorithm.event_at(xpixelpos, ypixelpos, alt_down)
    427          if event != None:
    428              self.status_bar_adapter.set_text(event.get_label())
    429          else:
    430              self.status_bar_adapter.set_text("")
    431
    432 -    def balloon_show_timer_fired(self):
    433          self.input_handler.balloon_show_timer_fired()
    434
    435 -    def balloon_hide_timer_fired(self):
    436          self.input_handler.balloon_hide_timer_fired()
    437
    438 -    def _redraw_balloons(self, event):
    439          self.view_properties.hovered_event = event
    440          self._redraw_timeline()
    441
    442 -    def _in_scroll_zone(self, x):
    443          """
    444          Return True if x is within the left hand or right hand area
    445          where timed scrolling shall start/continue.
    446          """
    447          width, height = self.view.GetSizeTuple()
    448          if width - x < SCROLL_ZONE_WIDTH or x < SCROLL_ZONE_WIDTH:
    449              return True
    450          return False
    451
    452 -    def dragscroll_timer_fired(self):
    453          self.input_handler.dragscroll_timer_fired()
    454
    455 -    def _scroll_timeline_view(self, direction):
    456          factor = direction * MOUSE_SCROLL_FACTOR
    457          self._scroll_timeline_view_by_factor(factor)
    458
    459 -    def _scroll_timeline_view_by_factor(self, factor):
    460          time_period = self.view_properties.displayed_period
    461          delta = self.time_type.mult_timedelta(time_period.delta(), factor)
    462          self._scroll_timeline(delta)
    463
    464 -    def _scroll_timeline(self, delta):
    465          self.navigate_timeline(lambda tp: tp.move_delta(-delta))
    466
    467 -    def _zoom_timeline(self, direction, x):
    468          """ zoom time line at position x """
    469          width, height = self.view.GetSizeTuple()
    470          x_percent_of_width=float(x)/width
    471          self.navigate_timeline(lambda tp: tp.zoom(direction, x_percent_of_width))
    472
    473 -    def _delete_selected_events(self):
    474          """After acknowledge from the user, delete all selected events."""
    475          selected_event_ids = self.view_properties.get_selected_event_ids()
    476          nbr_of_selected_event_ids = len(selected_event_ids)
    477          if nbr_of_selected_event_ids > 1:
    478              text = _("Are you sure you want to delete %d events?" %
    479                       nbr_of_selected_event_ids)
    480          else:
    481              text = _("Are you sure you want to delete this event?")
    482          if self.view.ask_question(text) == wx.YES:
    483              try:
    484                  for event_id in selected_event_ids:
    485                      self.timeline.delete_event(event_id)
    486              except TimelineIOError, e:
    487                  self.fn_handle_db_error(e)
    488
    489 -    def balloon_visibility_changed(self, visible):
    490          self.view_properties.show_balloons_on_hover = visible
    491          # When display on hovering is disabled we have to make sure
    492          # that any visible balloon is removed.
    493          # TODO: Do we really need that?
    494          if not visible:
    495              self._redraw_timeline()
    496
    497
    498 -def _step_function(x_value):
    499      y_value = 0
    500      if x_value < 0:
    501          y_value = -1
    502      elif x_value > 0:
    503          y_value = 1
    504      return y_value
    505