1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
38
39
40
41
42 SCROLL_ZONE_WIDTH = 20
43
44 LEFT_RIGHT_SCROLL_FACTOR = 1 / 200.0
45 MOUSE_SCROLL_FACTOR = 1 / 10.0
46
47
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
67
70
74
78
81
84
86 return self.drawing_algorithm
87
90
92 return self.view_properties
93
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
103 self.fast_draw = value
104
106 self.timeline = None
107 self.time_type = None
108 self.view.Disable()
109
119
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
136
141
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
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
176 self._redraw_timeline()
177
179 self._redraw_timeline()
180
182 self.input_handler.left_mouse_down(x, y, ctrl_down, shift_down, alt_down)
183
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
226
228 self.view.open_event_editor_for(self.context_menu_event)
229
232
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
242 hyperlink = self.context_menu_event.get_data("hyperlink")
243 webbrowser.open(hyperlink)
244
245
246
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
258
259
260
261
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
271 return self.drawing_algorithm.get_time(x)
272
275
276 - def event_at(self, x, y, alt_down=False):
277 return self.drawing_algorithm.event_at(x, y, alt_down)
278
281
284
285 - def snap(self, time):
287
291
294
297
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
313
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
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
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
355
357 self._redraw_timeline()
358
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
370 """The 'Center' context menu has been selected."""
371 self.divider_line_slider.SetValue(50)
372 self._redraw_timeline()
373
378
385
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
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
412
424
431
434
437
439 self.view_properties.hovered_event = event
440 self._redraw_timeline()
441
451
454
458
463
466
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
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
490 self.view_properties.show_balloons_on_hover = visible
491
492
493
494 if not visible:
495 self._redraw_timeline()
496
497
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