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