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
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
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
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
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
227 - def _context_menu_on_edit_event(self, evt):
228 self.view.open_event_editor_for(self.context_menu_event)
229
230 - def _context_menu_on_duplicate_event(self, evt):
231 self.view.open_duplicate_event_dialog_for_event(self.context_menu_event)
232
233 - def _context_menu_on_delete_event(self, evt):
234 self.context_menu_event.selected = True 235 self._delete_selected_events()
236
238 self.view_properties.set_event_has_sticky_balloon(self.context_menu_event, has_sticky=True) 239 self._redraw_timeline()
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
359 - def _slider_on_context_menu(self, evt):
360 """A right click has occured in the divider-line slider.""" 361 menu = wx.Menu() 362 menu_item = wx.MenuItem(menu, wx.NewId(), _("Center")) 363 self.view.Bind(wx.EVT_MENU, self._context_menu_on_menu_center, 364 id=menu_item.GetId()) 365 menu.AppendItem(menu_item) 366 self.view.PopupMenu(menu) 367 menu.Destroy()
368
369 - def _context_menu_on_menu_center(self, evt):
370 """The 'Center' context menu has been selected.""" 371 self.divider_line_slider.SetValue(50) 372 self._redraw_timeline()
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
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