Package Gnumed :: Package timelinelib :: Package wxgui :: Package components :: Module timelinepanel
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.wxgui.components.timelinepanel

  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 os 
 20  import webbrowser 
 21   
 22  import wx 
 23   
 24  from timelinelib.canvas import EVT_DIVIDER_POSITION_CHANGED 
 25  from timelinelib.canvas import EVT_TIMELINE_REDRAWN 
 26  from timelinelib.db.utils import safe_locking 
 27  from timelinelib.general.observer import Listener 
 28  from timelinelib.wxgui.components.maincanvas.createperiodeventbydrag import CreatePeriodEventByDragInputHandler 
 29  from timelinelib.wxgui.components.maincanvas.maincanvas import MainCanvas 
 30  from timelinelib.wxgui.components.maincanvas.movebydrag import MoveByDragInputHandler 
 31  from timelinelib.wxgui.components.maincanvas.noop import NoOpInputHandler 
 32  from timelinelib.wxgui.components.maincanvas.resizebydrag import ResizeByDragInputHandler 
 33  from timelinelib.wxgui.components.maincanvas.scrollbydrag import ScrollByDragInputHandler 
 34  from timelinelib.wxgui.components.maincanvas.zoombydrag import ZoomByDragInputHandler 
 35  from timelinelib.wxgui.components.maincanvas.selectevents import SelectEventsInputHandler 
 36  from timelinelib.wxgui.components.messagebar import MessageBar 
 37  from timelinelib.wxgui.components.sidebar import Sidebar 
 38  from timelinelib.wxgui.dialogs.duplicateevent.view import open_duplicate_event_dialog_for_event 
 39  from timelinelib.wxgui.dialogs.editevent.view import open_create_event_editor 
 40  from timelinelib.wxgui.dialogs.editevent.view import open_event_editor_for 
 41  from timelinelib.wxgui.dialogs.milestone.view import open_milestone_editor_for 
 42  from timelinelib.wxgui.frames.mainframe.toolbar import ToolbarCreator 
 43  from timelinelib.wxgui.utils import _ask_question 
 44  from timelinelib.config.paths import ICONS_DIR 
 45  from timelinelib.wxgui.cursor import Cursor 
 46   
 47   
 48  LEFT_RIGHT_SCROLL_FACTOR = 1 / 200.0 
 49   
 50   
51 -class TimelinePanelGuiCreator(wx.Panel):
52
53 - def __init__(self, parent):
54 self.sidebar_width = self.config.sidebar_width 55 wx.Panel.__init__(self, parent) 56 self._create_gui()
57
58 - def BitmapFromIcon(self, icon):
59 return wx.Bitmap(os.path.join(ICONS_DIR, icon))
60
61 - def CreateToolbar(self):
62 return wx.ToolBar(self, wx.ID_ANY)
63
64 - def _create_gui(self):
65 self._create_toolbar() 66 self._create_warning_bar() 67 self._create_divider_line_slider() 68 self._create_splitter() 69 self._layout_components()
70
71 - def _create_toolbar(self):
72 self.tool_bar = ToolbarCreator(self, self.config).create()
73
74 - def _create_warning_bar(self):
75 self.message_bar = MessageBar(self)
76
78 79 def on_slider(evt): 80 self.config.divider_line_slider_pos = evt.GetPosition()
81 82 style = wx.SL_LEFT | wx.SL_VERTICAL 83 self.divider_line_slider = wx.Slider(self, style=style) 84 self.Bind(wx.EVT_SCROLL, on_slider, self.divider_line_slider) 85 86 self.divider_line_slider.Bind(wx.EVT_SLIDER, self._slider_on_slider) 87 self.divider_line_slider.Bind(wx.EVT_CONTEXT_MENU, self._slider_on_context_menu)
88
89 - def _slider_on_slider(self, evt):
90 self.timeline_canvas.SetDividerPosition(self.divider_line_slider.GetValue())
91
92 - def _slider_on_context_menu(self, evt):
93 menu = wx.Menu() 94 menu_item = wx.MenuItem(menu, wx.NewId(), _("Center")) 95 self.Bind(wx.EVT_MENU, self._context_menu_on_menu_center, id=menu_item.GetId()) 96 menu.Append(menu_item) 97 self.PopupMenu(menu) 98 menu.Destroy()
99
100 - def _context_menu_on_menu_center(self, evt):
101 self.timeline_canvas.SetDividerPosition(50)
102
103 - def _create_splitter(self):
104 self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) 105 self.splitter.SetMinimumPaneSize(50) 106 self.Bind( 107 wx.EVT_SPLITTER_SASH_POS_CHANGED, 108 self._splitter_on_splitter_sash_pos_changed, self.splitter) 109 self._create_sidebar() 110 self._create_timeline_canvas() 111 self.splitter.Initialize(self.timeline_canvas)
112
113 - def _splitter_on_splitter_sash_pos_changed(self, event):
114 if self.IsShown(): 115 self.sidebar_width = self.splitter.GetSashPosition()
116
117 - def _create_sidebar(self):
118 self.sidebar = Sidebar(self._edit_controller, self.splitter)
119
120 - def _create_timeline_canvas(self):
121 self.timeline_canvas = MainCanvas( 122 self.splitter, self._edit_controller, self.status_bar_adapter) 123 self.timeline_canvas.Bind( 124 wx.EVT_LEFT_DCLICK, 125 self._timeline_canvas_on_double_clicked 126 ) 127 self.timeline_canvas.Bind( 128 wx.EVT_RIGHT_DOWN, 129 self._timeline_canvas_on_right_down 130 ) 131 self.timeline_canvas.Bind( 132 wx.EVT_KEY_DOWN, 133 self._timeline_canvas_on_key_down 134 ) 135 self.timeline_canvas.Bind( 136 EVT_DIVIDER_POSITION_CHANGED, 137 self._timeline_canvas_on_divider_position_changed 138 ) 139 self.timeline_canvas.Bind( 140 EVT_TIMELINE_REDRAWN, 141 self._timeline_canvas_on_timeline_redrawn 142 ) 143 self.timeline_canvas.SetDividerPosition(self.config.divider_line_slider_pos) 144 self.timeline_canvas.SetEventBoxDrawer(self._get_saved_event_box_drawer()) 145 self.timeline_canvas.SetInputHandler(NoOpInputHandler( 146 InputHandlerState( 147 self.timeline_canvas, self.status_bar_adapter, 148 self._edit_controller, self.config), 149 self.timeline_canvas)) 150 151 def update_appearance(): 152 appearance = self.timeline_canvas.GetAppearance() 153 appearance.set_legend_visible(self.config.show_legend) 154 appearance.set_balloons_visible(self.config.balloon_on_hover) 155 appearance.set_hide_events_done(self.config.hide_events_done) 156 appearance.set_minor_strip_divider_line_colour(self.config.minor_strip_divider_line_colour) 157 appearance.set_major_strip_divider_line_colour(self.config.major_strip_divider_line_colour) 158 appearance.set_now_line_colour(self.config.now_line_colour) 159 appearance.set_weekend_colour(self.config.weekend_colour) 160 appearance.set_bg_colour(self.config.bg_colour) 161 appearance.set_colorize_weekends(self.config.colorize_weekends) 162 appearance.set_draw_period_events_to_right(self.config.draw_point_events_to_right) 163 appearance.set_text_below_icon(self.config.text_below_icon) 164 appearance.set_minor_strip_font(self.config.minor_strip_font) 165 appearance.set_major_strip_font(self.config.major_strip_font) 166 appearance.set_balloon_font(self.config.balloon_font) 167 appearance.set_legend_font(self.config.legend_font) 168 appearance.set_center_event_texts(self.config.center_event_texts) 169 appearance.set_never_show_period_events_as_point_events(self.config.never_show_period_events_as_point_events) 170 appearance.set_week_start(self.config.get_week_start()) 171 appearance.set_use_inertial_scrolling(self.config.use_inertial_scrolling) 172 appearance.set_fuzzy_icon(self.config.fuzzy_icon) 173 appearance.set_locked_icon(self.config.locked_icon) 174 appearance.set_hyperlink_icon(self.config.hyperlink_icon) 175 appearance.set_vertical_space_between_events(self.config.vertical_space_between_events) 176 appearance.set_skip_s_in_decade_text(self.config.skip_s_in_decade_text) 177 appearance.set_display_checkmark_on_events_done(self.config.display_checkmark_on_events_done) 178 appearance.set_never_use_time(self.config.never_use_time) 179 appearance.set_legend_pos(self.config.legend_pos) 180 appearance.set_time_scale_pos(self.config.time_scale_pos) 181 appearance.set_use_bold_nowline(self.config.use_bold_nowline)
182 self.config.listen_for_any(update_appearance) 183 update_appearance() 184
185 - def _get_saved_event_box_drawer(self):
186 from timelinelib.plugin import factory 187 from timelinelib.plugin.factory import EVENTBOX_DRAWER 188 from timelinelib.plugin.plugins.eventboxdrawers.defaulteventboxdrawer import DefaultEventBoxDrawer 189 plugin = factory.get_plugin(EVENTBOX_DRAWER, self.config.get_selected_event_box_drawer()) or DefaultEventBoxDrawer() 190 return plugin.run()
191
192 - def _timeline_canvas_on_double_clicked(self, event):
193 if self.timeline_canvas.GetDb().is_read_only(): 194 return 195 cursor = Cursor(event.GetX(), event.GetY()) 196 timeline_event = self.timeline_canvas.GetEventAt(cursor) 197 time = self.timeline_canvas.GetTimeAt(cursor.x) 198 if timeline_event is not None: 199 if timeline_event.is_milestone(): 200 self.open_milestone_editor(timeline_event) 201 else: 202 self.open_event_editor(timeline_event) 203 else: 204 open_create_event_editor( 205 self._edit_controller, 206 self, 207 self.config, 208 self.timeline_canvas.GetDb(), 209 time, 210 time) 211 event.Skip()
212
213 - def _timeline_canvas_on_right_down(self, event):
214 cursor = Cursor(event.GetX(), event.GetY()) 215 timeline_event = self.timeline_canvas.GetEventAt(cursor) 216 if timeline_event is not None and not self.timeline_canvas.GetDb().is_read_only(): 217 self.timeline_canvas.SetEventSelected(timeline_event, True) 218 self._display_event_context_menu() 219 event.Skip()
220
221 - def _timeline_canvas_on_key_down(self, event):
222 if event.GetKeyCode() == wx.WXK_DELETE: 223 self._delete_selected_events() 224 elif event.GetKeyCode() == wx.WXK_UP: 225 self.move_selected_event_up() 226 elif event.GetKeyCode() == wx.WXK_DOWN: 227 self.move_selected_event_down() 228 elif event.AltDown() and event.GetKeyCode() in (wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT): 229 self.timeline_canvas.Scroll(LEFT_RIGHT_SCROLL_FACTOR) 230 elif event.AltDown() and event.GetKeyCode() in (wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT): 231 self.timeline_canvas.Scroll(-LEFT_RIGHT_SCROLL_FACTOR) 232 else: 233 event.Skip()
234
235 - def move_selected_event_up(self):
236 self._try_move_event_vertically(True)
237
238 - def move_selected_event_down(self):
239 self._try_move_event_vertically(False)
240
241 - def _try_move_event_vertically(self, up=True):
242 event = self.timeline_canvas.GetSelectedEvent() 243 if event is not None: 244 self._move_event_vertically(event, up)
245
246 - def _move_event_vertically(self, event, up=True):
247 def edit_function(): 248 ( 249 overlapping_event, 250 direction 251 ) = self.timeline_canvas.GetClosestOverlappingEvent(event, up=up) 252 if overlapping_event is None: 253 pass 254 elif direction > 0: 255 self.timeline_canvas.GetDb().place_event_after_event( 256 event, 257 overlapping_event 258 ) 259 else: 260 self.timeline_canvas.GetDb().place_event_before_event( 261 event, 262 overlapping_event 263 ) 264 # GetClosestOverlappingEvent inflates rectangles, so subsequent 265 # calls do calculations on incorrect rectangles. A redraw 266 # recalculates all rectangles. 267 self.redraw_timeline()
268 safe_locking(self._edit_controller, edit_function) 269
270 - def _display_event_context_menu(self):
271 menu_definitions = [ 272 (_("Delete"), self._context_menu_on_delete_event, None), 273 ] 274 nbr_of_selected_events = len(self.timeline_canvas.GetSelectedEvents()) 275 if nbr_of_selected_events == 1: 276 menu_definitions.insert(0, (_("Edit"), self._context_menu_on_edit_event, None)) 277 menu_definitions.insert(1, (_("Duplicate..."), self._context_menu_on_duplicate_event, None)) 278 menu_definitions.append((_("Mark Event as Done"), self._context_menu_on_done_event, None)) 279 menu_definitions.append((_("Select Category..."), self._context_menu_on_select_category, None)) 280 if nbr_of_selected_events == 1 and self.timeline_canvas.GetSelectedEvent().has_data(): 281 menu_definitions.append((_("Sticky Balloon"), self._context_menu_on_sticky_balloon_event, None)) 282 if nbr_of_selected_events == 1: 283 hyperlinks = self.timeline_canvas.GetSelectedEvent().get_data("hyperlink") 284 if hyperlinks is not None: 285 imp = wx.Menu() 286 menuid = 0 287 for hyperlink in hyperlinks.split(";"): 288 imp.Append(menuid, hyperlink) 289 menuid += 1 290 menu_definitions.append((_("Goto URL"), self._context_menu_on_goto_hyperlink_event, imp)) 291 menu = wx.Menu() 292 mid = 0 293 for menu_definition in menu_definitions: 294 text, method, imp = menu_definition 295 menu_item = wx.MenuItem(menu, mid, text) 296 if imp is not None: 297 for menu_item in imp.GetMenuItems(): 298 self.Bind(wx.EVT_MENU, method, id=menu_item.GetId()) 299 menu.AppendMenu(wx.ID_ANY, text, imp) 300 else: 301 self.Bind(wx.EVT_MENU, method, id=menu_item.GetId()) 302 menu.Append(menu_item) 303 mid += 1 304 self.PopupMenu(menu) 305 menu.Destroy()
306
307 - def _context_menu_on_delete_event(self, evt):
308 self._delete_selected_events()
309
310 - def _delete_selected_events(self):
311 selected_events = self.timeline_canvas.GetSelectedEvents() 312 number_of_selected_events = len(selected_events) 313 314 def edit_function(): 315 if user_ack(): 316 with self.timeline_canvas.GetDb().transaction("Delete events"): 317 for event in selected_events: 318 event.delete() 319 self.timeline_canvas.ClearSelectedEvents()
320 321 def user_ack(): 322 if number_of_selected_events > 1: 323 text = _("Are you sure you want to delete %d events?" % 324 number_of_selected_events) 325 else: 326 text = _("Are you sure you want to delete this event?") 327 return _ask_question(text) == wx.YES 328 safe_locking(self._edit_controller, edit_function) 329
330 - def _context_menu_on_edit_event(self, evt):
331 self.open_event_editor(self.timeline_canvas.GetSelectedEvent())
332
333 - def _context_menu_on_duplicate_event(self, evt):
334 open_duplicate_event_dialog_for_event( 335 self._edit_controller, 336 self, 337 self.timeline_canvas.GetDb(), 338 self.timeline_canvas.GetSelectedEvent())
339 340 # @experimental_feature(EVENT_DONE)
341 - def _context_menu_on_done_event(self, evt):
342 def edit_function(): 343 with self.timeline_canvas.GetDb().transaction("Mark events done"): 344 for event in self.timeline_canvas.GetSelectedEvents(): 345 if not event.is_container(): 346 event.set_progress(100) 347 event.save() 348 self.timeline_canvas.ClearSelectedEvents()
349 safe_locking(self._edit_controller, edit_function) 350
351 - def _context_menu_on_select_category(self, evt):
352 # TODO: Disable the context menu if the main_frame don't have 353 # a set_category_on_selected() method 354 set_category_on_selected = getattr(self.main_frame, 'set_category_on_selected', None) 355 if callable(set_category_on_selected): 356 set_category_on_selected()
357
358 - def _context_menu_on_sticky_balloon_event(self, evt):
359 self.timeline_canvas.SetEventStickyBalloon(self.timeline_canvas.GetSelectedEvent(), True)
360 365
366 - def _timeline_canvas_on_divider_position_changed(self, event):
367 self.divider_line_slider.SetValue(self.timeline_canvas.GetDividerPosition()) 368 self.config.divider_line_slider_pos = self.timeline_canvas.GetDividerPosition()
369
370 - def _timeline_canvas_on_timeline_redrawn(self, event):
371 text = _("%s events hidden") % self.timeline_canvas.GetHiddenEventCount() 372 self.status_bar_adapter.set_hidden_event_count_text(text) 373 enable_disable_menus = getattr(self.main_frame, 'enable_disable_menus') 374 if callable(enable_disable_menus): 375 enable_disable_menus()
376
377 - def _layout_components(self):
378 hsizer = wx.BoxSizer(wx.HORIZONTAL) 379 hsizer.Add(self.splitter, proportion=1, flag=wx.EXPAND) 380 hsizer.Add(self.divider_line_slider, proportion=0, flag=wx.EXPAND) 381 vsizer = wx.BoxSizer(wx.VERTICAL) 382 vsizer.Add(self.tool_bar, proportion=0, flag=wx.EXPAND) 383 vsizer.Add(self.message_bar, proportion=0, flag=wx.EXPAND) 384 vsizer.Add(hsizer, proportion=1, flag=wx.EXPAND) 385 self.SetSizer(vsizer)
386 387
388 -class TimelinePanel(TimelinePanelGuiCreator):
389
390 - def __init__(self, parent, config, status_bar_adapter, main_frame):
391 self.config = config 392 self.status_bar_adapter = status_bar_adapter 393 self.main_frame = main_frame 394 self._edit_controller = main_frame 395 TimelinePanelGuiCreator.__init__(self, parent) 396 self._db_listener = Listener(self._on_db_changed)
397
398 - def SetDb(self, db):
399 self.timeline_canvas.SetDb(db) 400 self._db_listener.set_observable(db)
401
402 - def get_timeline_canvas(self):
403 return self.timeline_canvas
404
405 - def get_time_period(self):
406 return self.timeline_canvas.GetTimePeriod()
407
408 - def open_event_editor(self, event):
409 if event.is_milestone(): 410 self.open_milestone_editor(event) 411 else: 412 open_event_editor_for( 413 self._edit_controller, 414 self, 415 self.config, 416 self.timeline_canvas.GetDb(), 417 event)
418
419 - def open_milestone_editor(self, event):
420 open_milestone_editor_for( 421 self._edit_controller, 422 self, 423 self.config, 424 self.timeline_canvas.GetDb(), 425 event)
426
427 - def redraw_timeline(self):
428 self.timeline_canvas.Redraw()
429
430 - def Navigate(self, navigation_fn):
431 return self.timeline_canvas.Navigate(navigation_fn)
432
433 - def get_view_properties(self):
434 return self.timeline_canvas.GetViewProperties()
435
436 - def get_sidebar_width(self):
437 return self.sidebar_width
438
439 - def show_sidebar(self):
440 self.splitter.SplitVertically(self.sidebar, self.timeline_canvas, self.sidebar_width) 441 self.splitter.SetSashPosition(self.sidebar_width) 442 self.splitter.SetMinimumPaneSize(self.sidebar.GetBestSize()[0])
443
444 - def hide_sidebar(self):
445 self.splitter.Unsplit(self.sidebar)
446
447 - def activated(self):
448 if self.config.show_sidebar: 449 self.show_sidebar()
450
451 - def _on_db_changed(self, db):
452 if db.is_read_only(): 453 header = _("This timeline is read-only.") 454 body = _("To edit this timeline, save it to a new file: File -> Save As.") 455 self.message_bar.ShowInformationMessage("%s\n%s" % (header, body)) 456 elif not db.is_saved(): 457 header = _("This timeline is not being saved.") 458 body = _("To save this timeline, save it to a new file: File -> Save As.") 459 self.message_bar.ShowWarningMessage("%s\n%s" % (header, body)) 460 else: 461 self.message_bar.ShowNoMessage()
462 463
464 -class InputHandlerState(object):
465
466 - def __init__(self, canvas, status_bar, edit_controller, config):
467 self._timeline_canvas = canvas 468 self._status_bar = status_bar 469 self._edit_controller = edit_controller 470 self._config = config
471
472 - def ok_to_edit(self):
473 return self._edit_controller.ok_to_edit()
474
475 - def edit_ends(self):
476 return self._edit_controller.edit_ends()
477
478 - def display_status(self, message):
479 return self._edit_controller.DisplayStatus(message)
480
481 - def change_to_no_op(self):
482 self._timeline_canvas.SetInputHandler( 483 NoOpInputHandler(self, self._timeline_canvas))
484
485 - def change_to_move_by_drag(self, event, start_drag_time):
486 self._timeline_canvas.SetInputHandler( 487 MoveByDragInputHandler(self, self._timeline_canvas, event, start_drag_time))
488
489 - def change_to_zoom_by_drag(self, cursor):
490 start_time = self._timeline_canvas.GetTimeAt(cursor.x) 491 self._timeline_canvas.SetInputHandler( 492 ZoomByDragInputHandler(self, self._timeline_canvas, start_time))
493
494 - def change_to_select(self, cursor):
495 self._timeline_canvas.SetInputHandler( 496 SelectEventsInputHandler(self, self._timeline_canvas, cursor))
497
498 - def change_to_resize_by_drag(self, event, direction):
499 self._timeline_canvas.SetInputHandler( 500 ResizeByDragInputHandler(self, self._timeline_canvas, event, direction))
501
502 - def change_to_scroll_by_drag(self, cursor):
503 start_time = self._timeline_canvas.GetTimeAt(cursor.x) 504 y = cursor.y 505 self._timeline_canvas.SetInputHandler( 506 ScrollByDragInputHandler(self, self._timeline_canvas, start_time, y))
507
509 time_at_x = self._timeline_canvas.GetTimeAt(cursor.x) 510 self._timeline_canvas.SetInputHandler( 511 CreatePeriodEventByDragInputHandler(self, self._timeline_canvas, self._config, time_at_x))
512