1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """
19 A base class for the mainframe window, responsible for creating the GUI.
20 """
21
22
23 import collections
24 import wx
25
26 from timelinelib.db.utils import safe_locking
27 from timelinelib.meta.about import display_about_dialog
28 from timelinelib.plugin.factory import EVENTBOX_DRAWER
29 from timelinelib.plugin.factory import EXPORTER
30 from timelinelib.plugin import factory
31 from timelinelib.proxies.drawingarea import DrawingAreaProxy
32 from timelinelib.wxgui.components.mainpanel import MainPanel
33 from timelinelib.wxgui.components.statusbaradapter import StatusBarAdapter
34 from timelinelib.wxgui.dialogs.duplicateevent.view import open_duplicate_event_dialog_for_event
35 from timelinelib.wxgui.dialogs.editevent.view import open_create_event_editor
36 from timelinelib.wxgui.dialogs.feedback.view import show_feedback_dialog
37 from timelinelib.wxgui.dialogs.filenew.view import FileNewDialog
38 from timelinelib.wxgui.dialogs.importevents.view import ImportEventsDialog
39 from timelinelib.wxgui.dialogs.milestone.view import open_milestone_editor_for
40 from timelinelib.wxgui.dialogs.preferences.view import PreferencesDialog
41 from timelinelib.wxgui.dialogs.shortcutseditor.view import ShortcutsEditorDialog
42 from timelinelib.wxgui.dialogs.systeminfo.view import show_system_info_dialog
43
44
45 NONE = 0
46 CHECKBOX = 1
47 CHECKED_RB = 2
48 UNCHECKED_RB = 3
49 ID_SIDEBAR = wx.NewId()
50 ID_LEGEND = wx.NewId()
51 ID_BALLOONS = wx.NewId()
52 ID_ZOOMIN = wx.NewId()
53 ID_ZOOMOUT = wx.NewId()
54 ID_VERT_ZOOMIN = wx.NewId()
55 ID_VERT_ZOOMOUT = wx.NewId()
56 ID_CREATE_EVENT = wx.NewId()
57 ID_CREATE_MILESTONE = wx.NewId()
58 ID_PT_EVENT_TO_RIGHT = wx.NewId()
59 ID_EDIT_EVENT = wx.NewId()
60 ID_DUPLICATE_EVENT = wx.NewId()
61 ID_SET_CATEGORY_ON_SELECTED = wx.NewId()
62 ID_MEASURE_DISTANCE = wx.NewId()
63 ID_COMPRESS = wx.NewId()
64 ID_SET_CATEGORY_ON_WITHOUT = wx.NewId()
65 ID_EDIT_ERAS = wx.NewId()
66 ID_SET_READONLY = wx.NewId()
67 ID_FIND_FIRST = wx.NewId()
68 ID_FIND_LAST = wx.NewId()
69 ID_FIT_ALL = wx.NewId()
70 ID_EDIT_SHORTCUTS = wx.NewId()
71 ID_TUTORIAL = wx.NewId()
72 ID_NUMTUTORIAL = wx.NewId()
73 ID_FEEDBACK = wx.NewId()
74 ID_CONTACT = wx.NewId()
75 ID_SYSTEM_INFO = wx.NewId()
76 ID_IMPORT = wx.NewId()
77 ID_EXPORT = wx.NewId()
78 ID_EXPORT_ALL = wx.NewId()
79 ID_EXPORT_SVG = wx.NewId()
80 ID_FIND_CATEGORIES = wx.NewId()
81 ID_SELECT_ALL = wx.NewId()
82 ID_RESTORE_TIME_PERIOD = wx.NewId()
83 ID_NEW = wx.ID_NEW
84 ID_FIND = wx.ID_FIND
85 ID_UNDO = wx.NewId()
86 ID_REDO = wx.NewId()
87 ID_PREFERENCES = wx.ID_PREFERENCES
88 ID_HELP = wx.ID_HELP
89 ID_ABOUT = wx.ID_ABOUT
90 ID_SAVEAS = wx.ID_SAVEAS
91 ID_EXIT = wx.ID_EXIT
92 ID_MOVE_EVENT_UP = wx.NewId()
93 ID_MOVE_EVENT_DOWN = wx.NewId()
94 ID_PRESENTATION = wx.NewId()
95 ID_HIDE_DONE = wx.NewId()
96 ID_NAVIGATE = wx.NewId() + 100
97
98
100
102 self.shortcut_items = {}
103 self._create_status_bar()
104 self._create_main_panel()
105 self._create_main_menu_bar()
106 self._bind_frame_events()
107
109 self.CreateStatusBar()
110 self.status_bar_adapter = StatusBarAdapter(self.GetStatusBar())
111
113 self.main_panel = MainPanel(self, self.config, self)
114
116 main_menu_bar = wx.MenuBar()
117 main_menu_bar.Append(self._create_file_menu(), _("&File"))
118 main_menu_bar.Append(self._create_edit_menu(), _("&Edit"))
119 main_menu_bar.Append(self._create_view_menu(), _("&View"))
120 main_menu_bar.Append(self._create_timeline_menu(), _("&Timeline"))
121 main_menu_bar.Append(self._create_navigate_menu(), _("&Navigate"))
122 main_menu_bar.Append(self._create_help_menu(), _("&Help"))
123 self._set_shortcuts()
124 self.SetMenuBar(main_menu_bar)
125 self.update_navigation_menu_items()
126 self.enable_disable_menus()
127
132
134 self.Bind(wx.EVT_CLOSE, self._window_on_close)
135
137 file_menu = wx.Menu()
138 self._create_file_new_menu_item(file_menu)
139 self._create_file_open_menu_item(file_menu)
140 self._create_file_open_recent_menu(file_menu)
141 file_menu.AppendSeparator()
142 self._create_file_save_as_menu(file_menu)
143 file_menu.AppendSeparator()
144 self._create_import_menu_item(file_menu)
145 file_menu.AppendSeparator()
146 self._create_export_menues(file_menu)
147 file_menu.AppendSeparator()
148 self._create_file_exit_menu_item(file_menu)
149 self._file_menu = file_menu
150 return file_menu
151
153
154 def create_click_handler(plugin, main_frame):
155 def event_handler(evt):
156 plugin.run(main_frame)
157 return event_handler
158
159 submenu = wx.Menu()
160 file_menu.Append(wx.ID_ANY, _("Export"), submenu)
161 for plugin in factory.get_plugins(EXPORTER):
162 mnu = submenu.Append(wx.ID_ANY, plugin.display_name(), plugin.display_name())
163 self.menu_controller.add_menu_requiring_timeline(mnu)
164 handler = create_click_handler(plugin, self)
165 self.Bind(wx.EVT_MENU, handler, mnu)
166 method = getattr(plugin, "wxid", None)
167 if callable(method):
168 self.shortcut_items[method()] = mnu
169
171 accel = wx.GetStockLabel(wx.ID_NEW, wx.STOCK_WITH_ACCELERATOR | wx.STOCK_WITH_MNEMONIC)
172 accel = accel.split("\t", 1)[1]
173 file_menu.Append(
174 wx.ID_NEW, _("New...") + "\t" + accel, _("Create a new timeline"))
175 self.shortcut_items[wx.ID_NEW] = file_menu.FindItemById(wx.ID_NEW)
176 self.Bind(wx.EVT_MENU, self._mnu_file_new_on_click, id=wx.ID_NEW)
177
179 file_menu.Append(
180 wx.ID_OPEN, self._add_ellipses_to_menuitem(wx.ID_OPEN),
181 _("Open an existing timeline"))
182 self.Bind(wx.EVT_MENU, self._mnu_file_open_on_click, id=wx.ID_OPEN)
183
185 self.mnu_file_open_recent_submenu = wx.Menu()
186 file_menu.Append(wx.ID_ANY, _("Open &Recent"), self.mnu_file_open_recent_submenu)
187 self.update_open_recent_submenu()
188
193
195 mnu_file_import = file_menu.Append(
196 ID_IMPORT, _("Import events..."), _("Import events..."))
197 self.shortcut_items[ID_IMPORT] = mnu_file_import
198 self.Bind(wx.EVT_MENU, self._mnu_file_import_on_click, mnu_file_import)
199 self.menu_controller.add_menu_requiring_writable_timeline(mnu_file_import)
200
202 file_menu.Append(wx.ID_EXIT, "", _("Exit the program"))
203 self.shortcut_items[wx.ID_EXIT] = file_menu.FindItemById(wx.ID_EXIT)
204 self.Bind(wx.EVT_MENU, self._mnu_file_exit_on_click, id=wx.ID_EXIT)
205
211
212 def find(evt):
213 self.main_panel.show_searchbar(True)
214
215 def find_categories(evt):
216 dialog = create_category_find_dialog()
217 dialog.ShowModal()
218 dialog.Destroy()
219
220 def select_all(evt):
221 self.controller.select_all()
222
223 def preferences(evt):
224 def edit_function():
225 dialog = PreferencesDialog(self, self.config)
226 dialog.ShowModal()
227 dialog.Destroy()
228 safe_locking(self, edit_function)
229
230 def edit_shortcuts(evt):
231
232 def edit_function():
233 dialog = ShortcutsEditorDialog(self, self.shortcut_controller)
234 dialog.ShowModal()
235 dialog.Destroy()
236 safe_locking(self, edit_function)
237 cbx = NONE
238 items_spec = ((wx.ID_FIND, find, None, cbx),
239 (ID_FIND_CATEGORIES, find_categories, _("Find Categories..."), cbx),
240 None,
241 (ID_SELECT_ALL, select_all, _("Select All Events"), cbx),
242 None,
243 (wx.ID_PREFERENCES, preferences, None, cbx),
244 (ID_EDIT_SHORTCUTS, edit_shortcuts, _("Shortcuts..."), cbx))
245 self._edit_menu = self._create_menu(items_spec)
246 self._add_edit_menu_items_to_controller(self._edit_menu)
247 return self._edit_menu
248
254
263
264 def legend(evt):
265 self.config.show_legend = evt.IsChecked()
266
267 def balloons(evt):
268 self.config.balloon_on_hover = evt.IsChecked()
269
270 def zoomin(evt):
271 DrawingAreaProxy(self).zoom_in()
272
273 def zoomout(evt):
274 DrawingAreaProxy(self).zoom_out()
275
276 def vert_zoomin(evt):
277 DrawingAreaProxy(self).vertical_zoom_in()
278
279 def vert_zoomout(evt):
280 DrawingAreaProxy(self).vertical_zoom_out()
281
282 def start_slide_show(evt):
283 canvas = self.main_panel.get_timeline_canvas()
284 self.controller.start_slide_show(canvas)
285
286 def hide_events_done(evt):
287 self.config.hide_events_done = evt.IsChecked()
288
289 items_spec = [self._create_view_toolbar_menu_item,
290 (ID_SIDEBAR, sidebar, _("&Sidebar") + "\tCtrl+I", CHECKBOX),
291 (ID_LEGEND, legend, _("&Legend"), CHECKBOX),
292 None,
293 (ID_BALLOONS, balloons, _("&Balloons on hover"), CHECKBOX),
294 None,
295 (ID_ZOOMIN, zoomin, _("Zoom &In") + "\tCtrl++", NONE),
296 (ID_ZOOMOUT, zoomout, _("Zoom &Out") + "\tCtrl+-", NONE),
297 (ID_VERT_ZOOMIN, vert_zoomin, _("Vertical Zoom &In") + "\tAlt++", NONE),
298 (ID_VERT_ZOOMOUT, vert_zoomout, _("Vertical Zoom &Out") + "\tAlt+-", NONE),
299 None,
300 self._create_view_point_event_alignment_menu,
301 None,
302 self._create_event_box_drawers_menu,
303 None,
304 (ID_PRESENTATION, start_slide_show, _("Start slide show") + "...", NONE),
305 None,
306 (ID_HIDE_DONE, hide_events_done, _("&Hide Events done"), CHECKBOX),
307 ]
308 self._view_menu = self._create_menu(items_spec)
309 self._check_view_menu_items(self._view_menu)
310 self._add_view_menu_items_to_controller(self._view_menu)
311 return self._view_menu
312
318
319 def check_item_corresponding_to_config():
320 item.Check(self.config.show_toolbar)
321
322 self.Bind(wx.EVT_MENU, on_click, item)
323 self.config.listen_for_any(check_item_corresponding_to_config)
324 check_item_corresponding_to_config()
325
332 return event_handler
333
334 items = []
335 for plugin in factory.get_plugins(EVENTBOX_DRAWER):
336 if plugin.display_name() == self.config.get_selected_event_box_drawer():
337 items.append((wx.ID_ANY, create_click_handler(plugin), plugin.display_name(), CHECKED_RB))
338 else:
339 items.append((wx.ID_ANY, create_click_handler(plugin), plugin.display_name(), UNCHECKED_RB))
340 sub_menu = self._create_menu(items)
341 view_menu.Append(wx.ID_ANY, _("Event appearance"), sub_menu)
342
344 sub_menu = wx.Menu()
345 left_item = sub_menu.Append(wx.ID_ANY, _("Left"), kind=wx.ITEM_RADIO)
346 center_item = sub_menu.Append(wx.ID_ANY, _("Center"), kind=wx.ITEM_RADIO)
347 view_menu.Append(wx.ID_ANY, _("Point event alignment"), sub_menu)
348
349 def on_first_tool_click(event):
350 self.config.draw_point_events_to_right = True
351
352 def on_second_tool_click(event):
353 self.config.draw_point_events_to_right = False
354
355 def check_item_corresponding_to_config():
356 if self.config.draw_point_events_to_right:
357 left_item.Check()
358 else:
359 center_item.Check()
360
361 self.Bind(wx.EVT_MENU, on_first_tool_click, left_item)
362 self.Bind(wx.EVT_MENU, on_second_tool_click, center_item)
363 self.config.listen_for_any(check_item_corresponding_to_config)
364 check_item_corresponding_to_config()
365
367
368 def item(item_id):
369 return view_menu.FindItemById(item_id)
370
371 item(ID_SIDEBAR).Check(self.config.show_sidebar)
372 item(ID_LEGEND).Check(self.config.show_legend)
373 item(ID_BALLOONS).Check(self.config.balloon_on_hover)
374 item(ID_HIDE_DONE).Check(self.config.hide_events_done)
375
387
389
390 def edit_function():
391 self._set_category_to_selected_events()
392
393 safe_locking(self, edit_function)
394
399
400 def edit_event(evt):
401 try:
402 event_id = self.main_panel.get_id_of_first_selected_event()
403 event = self.timeline.find_event_with_id(event_id)
404 except IndexError:
405
406 return
407 self.main_panel.open_event_editor(event)
408
409 def duplicate_event(evt):
410 try:
411 event_id = self.main_panel.get_id_of_first_selected_event()
412 event = self.timeline.find_event_with_id(event_id)
413 except IndexError:
414
415 return
416 open_duplicate_event_dialog_for_event(self, self, self.timeline, event)
417
418 def create_milestone(evt):
419 open_milestone_editor_for(self, self, self.config, self.timeline)
420
421 def set_categoryon_selected(evt):
422
423 def edit_function():
424 self._set_category_to_selected_events()
425 safe_locking(self, edit_function)
426
427 def measure_distance(evt):
428 self._measure_distance_between_events()
429
430 def set_category_on_without(evt):
431 def edit_function():
432 self._set_category()
433 safe_locking(self, edit_function)
434
435 def edit_eras(evt):
436 def edit_function():
437 self._edit_eras()
438 safe_locking(self, edit_function)
439
440 def set_readonly(evt):
441 self.controller.set_timeline_in_readonly_mode()
442
443 def undo(evt):
444 safe_locking(self, self.timeline.undo)
445
446 def redo(evt):
447 safe_locking(self, self.timeline.redo)
448
449 def compress(evt):
450 safe_locking(self, self.timeline.compress)
451
452 def move_up_handler(event):
453 self.main_panel.timeline_panel.move_selected_event_up()
454
455 def move_down_handler(event):
456 self.main_panel.timeline_panel.move_selected_event_down()
457
458 cbx = NONE
459 items_spec = ((ID_CREATE_EVENT, create_event, _("Create &Event..."), cbx),
460 (ID_EDIT_EVENT, edit_event, _("&Edit Selected Event..."), cbx),
461 (ID_DUPLICATE_EVENT, duplicate_event, _("&Duplicate Selected Event..."), cbx),
462 (ID_SET_CATEGORY_ON_SELECTED, set_categoryon_selected, _("Set Category on Selected Events..."), cbx),
463 (ID_MOVE_EVENT_UP, move_up_handler, _("Move event up") + "\tAlt+Up", cbx),
464 (ID_MOVE_EVENT_DOWN, move_down_handler, _("Move event down") + "\tAlt+Down", cbx),
465 None,
466 (ID_CREATE_MILESTONE, create_milestone, _("Create &Milestone..."), cbx),
467 None,
468 (ID_COMPRESS, compress, _("&Compress timeline Events"), cbx),
469 None,
470 (ID_MEASURE_DISTANCE, measure_distance, _("&Measure Distance between two Events..."), cbx),
471 None,
472 (ID_SET_CATEGORY_ON_WITHOUT, set_category_on_without,
473 _("Set Category on events &without category..."), cbx),
474 None,
475 (ID_EDIT_ERAS, edit_eras, _("Edit Era's..."), cbx),
476 None,
477 (ID_SET_READONLY, set_readonly, _("&Read Only"), cbx),
478 None,
479 (ID_UNDO, undo, _("&Undo") + "\tCtrl+Z", cbx),
480 (ID_REDO, redo, _("&Redo") + "\tAlt+Z", cbx))
481 self._timeline_menu = self._create_menu(items_spec)
482 self._add_timeline_menu_items_to_controller(self._timeline_menu)
483 return self._timeline_menu
484
486 self._add_to_controller_requiring_writeable_timeline(menu, ID_CREATE_EVENT)
487 self._add_to_controller_requiring_writeable_timeline(menu, ID_EDIT_EVENT)
488 self._add_to_controller_requiring_writeable_timeline(menu, ID_CREATE_MILESTONE)
489 self._add_to_controller_requiring_writeable_timeline(menu, ID_DUPLICATE_EVENT)
490 self._add_to_controller_requiring_writeable_timeline(menu, ID_SET_CATEGORY_ON_SELECTED)
491 self._add_to_controller_requiring_writeable_timeline(menu, ID_MEASURE_DISTANCE)
492 self._add_to_controller_requiring_writeable_timeline(menu, ID_SET_CATEGORY_ON_WITHOUT)
493 self._add_to_controller_requiring_writeable_timeline(menu, ID_SET_READONLY)
494 self._add_to_controller_requiring_writeable_timeline(menu, ID_EDIT_ERAS)
495 self._add_to_controller_requiring_writeable_timeline(menu, ID_COMPRESS)
496
500
511
512 def find_last(evt):
513 event = self.timeline.get_last_event()
514 if event:
515 end = event.get_end_time()
516 delta = self.main_panel.get_displayed_period_delta()
517 try:
518 start = end - delta
519 except ValueError:
520 start = self.timeline.get_time_type().get_min_time()
521 margin_delta = delta / 24
522 self.main_panel.Navigate(lambda tp: tp.update(start, end, end_delta=margin_delta))
523
524 def restore_time_period(evt):
525 if self.prev_time_period:
526 self.main_panel.Navigate(lambda tp: self.prev_time_period)
527
528 def fit_all(evt):
529 self._fit_all_events()
530
531 cbx = NONE
532 items_spec = (None,
533 (ID_FIND_FIRST, find_first, _("Find &First Event"), cbx),
534 (ID_FIND_LAST, find_last, _("Find &Last Event"), cbx),
535 (ID_FIT_ALL, fit_all, _("Fit &All Events"), cbx),
536 None,
537 (ID_RESTORE_TIME_PERIOD, restore_time_period, _("Go to previous time period"), cbx),)
538 self._navigation_menu_items = []
539 self._navigation_functions_by_menu_item_id = {}
540 self.update_navigation_menu_items()
541 self._navigate_menu = self._create_menu(items_spec)
542 self._add_navigate_menu_items_to_controller(self._navigate_menu)
543 return self._navigate_menu
544
546 self._add_to_controller_requiring_timeline(menu, ID_FIND_FIRST)
547 self._add_to_controller_requiring_timeline(menu, ID_FIND_LAST)
548 self._add_to_controller_requiring_timeline(menu, ID_FIT_ALL)
549
553
558
559 cbx = NONE
560 items_spec = [(wx.ID_HELP, self.help_browser.show_contents_page, _("&Contents") + "\tF1", cbx),
561 None,
562 (ID_TUTORIAL, self.controller.open_gregorian_tutorial_timeline, _("Getting started &tutorial"), cbx),
563 (ID_NUMTUTORIAL, self.controller.open_numeric_tutorial_timeline, _("Getting started numeric &tutorial"), cbx),
564 None,
565 (ID_FEEDBACK, feedback, _("Give &Feedback..."), cbx),
566 (ID_CONTACT, self.help_browser.show_contact_page, _("Co&ntact"), cbx),
567 None,
568 (ID_SYSTEM_INFO, show_system_info_dialog, _("System information"), cbx),
569 None,
570 (wx.ID_ABOUT, display_about_dialog, None, cbx)]
571 self._help_menu = self._create_menu(items_spec)
572 return self._help_menu
573
575 menu = wx.Menu()
576 for item in items_spec:
577 if item is not None:
578 self._create_menu_item(menu, item)
579 else:
580 menu.AppendSeparator()
581 return menu
582
584 if isinstance(item_spec, collections.Callable):
585 item_spec(menu)
586 else:
587 item_id, handler, label, checkbox = item_spec
588 if label is not None:
589 if checkbox == CHECKBOX:
590 item = menu.Append(item_id, label, kind=wx.ITEM_CHECK)
591 elif checkbox == CHECKED_RB:
592 item = menu.Append(item_id, label, kind=wx.ITEM_RADIO)
593 item.Check(True)
594 elif checkbox == UNCHECKED_RB:
595 item = menu.Append(item_id, label, kind=wx.ITEM_RADIO)
596 else:
597 if label is not None:
598 item = menu.Append(item_id, label)
599 else:
600 item = menu.Append(item_id)
601 else:
602 item = menu.Append(item_id)
603 self.shortcut_items[item_id] = menu.FindItemById(item_id)
604 self.Bind(wx.EVT_MENU, handler, item)
605
607 items = [
608 {
609 "text": _("Gregorian"),
610 "description": _("This creates a timeline using the standard calendar."),
611 "create_fn": self._create_new_timeline,
612 },
613 {
614 "text": _("Numeric"),
615 "description": _("This creates a timeline that has numbers on the x-axis instead of dates."),
616 "create_fn": self._create_new_numeric_timeline,
617 },
618 {
619 "text": _("Directory"),
620 "description": _("This creates a timeline where the modification date of files in a directory are shown as events."),
621 "create_fn": self._create_new_dir_timeline,
622 },
623 {
624 "text": _("Bosparanian"),
625 "description": _("This creates a timeline using the fictuous Bosparanian calendar from the German pen-and-paper RPG \"The Dark Eye\" (\"Das schwarze Auge\", DSA)."),
626 "create_fn": self._create_new_bosparanian_timeline,
627 },
628 {
629 "text": _("Pharaonic"),
630 "description": _("This creates a timeline using the ancient egypt pharaonic calendar"),
631 "create_fn": self._create_new_pharaonic_timeline,
632 },
633 {
634 "text": _("Coptic"),
635 "description": _("This creates a timeline using the coptic calendar"),
636 "create_fn": self._create_new_coptic_timeline,
637 },
638 ]
639 dialog = FileNewDialog(self, items)
640 if dialog.ShowModal() == wx.ID_OK:
641 dialog.GetSelection()["create_fn"]()
642 dialog.Destroy()
643
645 self._open_existing_timeline()
646
648 if self.timeline is not None:
649 self._save_as()
650
652 def open_import_dialog():
653 dialog = ImportEventsDialog(self.timeline, self)
654 dialog.ShowModal()
655 dialog.Destroy()
656 safe_locking(self, open_import_dialog)
657
660
662 plain = wx.GetStockLabel(wx_id, wx.STOCK_WITH_ACCELERATOR | wx.STOCK_WITH_MNEMONIC)
663
664 tab_index = plain.find("\t")
665 if tab_index != -1:
666 return plain[:tab_index] + "..." + plain[tab_index:]
667 return plain + "..."
668