1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2005-2007 Donald N. Allingham 5# Copyright (C) 2008 Brian G. Matherly 6# Copyright (C) 2009 Benny Malengier 7# Copyright (C) 2010 Nick Hall 8# Copyright (C) 2010 Jakim Friant 9# Copyright (C) 2012 Gary Burton 10# Copyright (C) 2012 Doug Blank <doug.blank@gmail.com> 11# 12# This program is free software; you can redistribute it and/or modify 13# it under the terms of the GNU General Public License as published by 14# the Free Software Foundation; either version 2 of the License, or 15# (at your option) any later version. 16# 17# This program is distributed in the hope that it will be useful, 18# but WITHOUT ANY WARRANTY; without even the implied warranty of 19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20# GNU General Public License for more details. 21# 22# You should have received a copy of the GNU General Public License 23# along with this program; if not, write to the Free Software 24# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 25# 26 27""" 28Manages the main window and the pluggable views 29""" 30 31#------------------------------------------------------------------------- 32# 33# Standard python modules 34# 35#------------------------------------------------------------------------- 36from collections import defaultdict 37import os 38import time 39import datetime 40from io import StringIO 41import gc 42import html 43 44#------------------------------------------------------------------------- 45# 46# set up logging 47# 48#------------------------------------------------------------------------- 49import logging 50LOG = logging.getLogger(".") 51 52#------------------------------------------------------------------------- 53# 54# GNOME modules 55# 56#------------------------------------------------------------------------- 57from gi.repository import Gtk 58from gi.repository import Gdk 59from gi.repository import GLib 60 61#------------------------------------------------------------------------- 62# 63# Gramps modules 64# 65#------------------------------------------------------------------------- 66from gramps.gen.const import GRAMPS_LOCALE as glocale 67_ = glocale.translation.sgettext 68from gramps.cli.grampscli import CLIManager 69from .user import User 70from .plug import tool 71from gramps.gen.plug import START 72from gramps.gen.plug import REPORT 73from gramps.gen.plug.report._constants import standalone_categories 74from .plug import (PluginWindows, ReportPluginDialog, ToolPluginDialog) 75from .plug.report import report, BookSelector 76from .utils import AvailableUpdates 77from .pluginmanager import GuiPluginManager 78from gramps.gen.relationship import get_relationship_calculator 79from .displaystate import DisplayState, RecentDocsMenu 80from gramps.gen.const import (HOME_DIR, ICON, URL_BUGTRACKER, URL_HOMEPAGE, 81 URL_MAILINGLIST, URL_MANUAL_PAGE, URL_WIKISTRING, 82 WIKI_EXTRAPLUGINS, URL_BUGHOME) 83from gramps.gen.constfunc import is_quartz 84from gramps.gen.config import config 85from gramps.gen.errors import WindowActiveError 86from .dialog import ErrorDialog, WarningDialog, QuestionDialog2, InfoDialog 87from .widgets import Statusbar 88from .undohistory import UndoHistory 89from gramps.gen.utils.file import media_path_full 90from .dbloader import DbLoader 91from .display import display_help, display_url 92from .configure import GrampsPreferences 93from .aboutdialog import GrampsAboutDialog 94from .navigator import Navigator 95from .views.tags import Tags 96from .uimanager import ActionGroup, valid_action_name 97from gramps.gen.lib import (Person, Surname, Family, Media, Note, Place, 98 Source, Repository, Citation, Event, EventType, 99 ChildRef) 100from gramps.gui.editors import (EditPerson, EditFamily, EditMedia, EditNote, 101 EditPlace, EditSource, EditRepository, 102 EditCitation, EditEvent) 103from gramps.gen.db.exceptions import DbWriteFailure 104from gramps.gen.filters import reload_custom_filters 105from .managedwindow import ManagedWindow 106 107#------------------------------------------------------------------------- 108# 109# Constants 110# 111#------------------------------------------------------------------------- 112 113_UNSUPPORTED = ("Unsupported", _("Unsupported")) 114 115WIKI_HELP_PAGE_FAQ = '%s_-_FAQ' % URL_MANUAL_PAGE 116WIKI_HELP_PAGE_KEY = '%s_-_Keybindings' % URL_MANUAL_PAGE 117WIKI_HELP_PAGE_MAN = '%s' % URL_MANUAL_PAGE 118 119CSS_FONT = """ 120#view { 121 font-family: %s; 122 } 123""" 124#------------------------------------------------------------------------- 125# 126# ViewManager 127# 128#------------------------------------------------------------------------- 129class ViewManager(CLIManager): 130 """ 131 **Overview** 132 133 The ViewManager is the session manager of the program. 134 Specifically, it manages the main window of the program. It is closely tied 135 into the Gtk.UIManager to control all menus and actions. 136 137 The ViewManager controls the various Views within the Gramps programs. 138 Views are organised in categories. The categories can be accessed via 139 a sidebar. Within a category, the different views are accesible via the 140 toolbar of view menu. 141 142 A View is a particular way of looking a information in the Gramps main 143 window. Each view is separate from the others, and has no knowledge of 144 the others. 145 146 Examples of current views include: 147 148 - Person View 149 - Relationship View 150 - Family View 151 - Source View 152 153 The View Manager does not have to know the number of views, the type of 154 views, or any other details about the views. It simply provides the 155 method of containing each view, and has methods for creating, deleting and 156 switching between the views. 157 158 """ 159 160 def __init__(self, app, dbstate, view_category_order, user=None): 161 """ 162 The viewmanager is initialised with a dbstate on which Gramps is 163 working, and a fixed view_category_order, which is the order in which 164 the view categories are accessible in the sidebar. 165 """ 166 CLIManager.__init__(self, dbstate, setloader=False, user=user) 167 168 self.view_category_order = view_category_order 169 self.app = app 170 171 #set pluginmanager to GUI one 172 self._pmgr = GuiPluginManager.get_instance() 173 self.merge_ids = [] 174 self.toolactions = None 175 self.tool_menu_ui_id = None 176 self.reportactions = None 177 self.report_menu_ui_id = None 178 179 self.active_page = None 180 self.pages = [] 181 self.page_lookup = {} 182 self.views = None 183 self.current_views = [] # The current view in each category 184 self.view_changing = False 185 186 self.show_navigator = config.get('interface.view') 187 self.show_toolbar = config.get('interface.toolbar-on') 188 self.fullscreen = config.get('interface.fullscreen') 189 190 self.__build_main_window() # sets self.uistate 191 if self.user is None: 192 self.user = User(error=ErrorDialog, 193 parent=self.window, 194 callback=self.uistate.pulse_progressbar, 195 uistate=self.uistate, 196 dbstate=self.dbstate) 197 self.__connect_signals() 198 199 self.do_reg_plugins(self.dbstate, self.uistate) 200 reload_custom_filters() 201 #plugins loaded now set relationship class 202 self.rel_class = get_relationship_calculator() 203 self.uistate.set_relationship_class() 204 # Need to call after plugins have been registered 205 self.uistate.connect('update-available', self.process_updates) 206 self.check_for_updates() 207 # Set autobackup 208 self.uistate.connect('autobackup', self.autobackup) 209 self.uistate.set_backup_timer() 210 211 def check_for_updates(self): 212 """ 213 Check for add-on updates. 214 """ 215 howoften = config.get("behavior.check-for-addon-updates") 216 update = False 217 if howoften != 0: # update never if zero 218 year, mon, day = list(map( 219 int, config.get("behavior.last-check-for-addon-updates").split("/"))) 220 days = (datetime.date.today() - datetime.date(year, mon, day)).days 221 if howoften == 1 and days >= 30: # once a month 222 update = True 223 elif howoften == 2 and days >= 7: # once a week 224 update = True 225 elif howoften == 3 and days >= 1: # once a day 226 update = True 227 elif howoften == 4: # always 228 update = True 229 230 if update: 231 AvailableUpdates(self.uistate).start() 232 233 def process_updates(self, addon_update_list): 234 """ 235 Called when add-on updates are available. 236 """ 237 rescan = PluginWindows.UpdateAddons(self.uistate, [], 238 addon_update_list).rescan 239 self.do_reg_plugins(self.dbstate, self.uistate, rescan=rescan) 240 241 def _errordialog(self, title, errormessage): 242 """ 243 Show the error. 244 In the GUI, the error is shown, and a return happens 245 """ 246 ErrorDialog(title, errormessage, 247 parent=self.uistate.window) 248 return 1 249 250 def __build_main_window(self): 251 """ 252 Builds the GTK interface 253 """ 254 width = config.get('interface.main-window-width') 255 height = config.get('interface.main-window-height') 256 horiz_position = config.get('interface.main-window-horiz-position') 257 vert_position = config.get('interface.main-window-vert-position') 258 font = config.get('utf8.selected-font') 259 260 self.window = Gtk.ApplicationWindow(application=self.app) 261 self.app.window = self.window 262 self.window.set_icon_from_file(ICON) 263 self.window.set_default_size(width, height) 264 self.window.move(horiz_position, vert_position) 265 266 self.provider = Gtk.CssProvider() 267 self.change_font(font) 268 269 #Set the mnemonic modifier on Macs to alt-ctrl so that it 270 #doesn't interfere with the extended keyboard, see 271 #https://gramps-project.org/bugs/view.php?id=6943 272 if is_quartz(): 273 self.window.set_mnemonic_modifier( 274 Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK) 275 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 276 self.window.add(vbox) 277 self.hpane = Gtk.Paned() 278 self.ebox = Gtk.EventBox() 279 280 self.navigator = Navigator(self) 281 self.ebox.add(self.navigator.get_top()) 282 self.hpane.pack1(self.ebox, False, False) 283 self.hpane.show() 284 285 self.notebook = Gtk.Notebook() 286 self.notebook.set_scrollable(True) 287 self.notebook.set_show_tabs(False) 288 self.notebook.show() 289 self.__init_lists() 290 self.__build_ui_manager() 291 292 self.hpane.add2(self.notebook) 293 toolbar = self.uimanager.get_widget('ToolBar') 294 self.statusbar = Statusbar() 295 self.statusbar.show() 296 vbox.pack_end(self.statusbar, False, True, 0) 297 vbox.pack_start(toolbar, False, True, 0) 298 vbox.pack_end(self.hpane, True, True, 0) 299 vbox.show_all() 300 301 self.uistate = DisplayState(self.window, self.statusbar, 302 self.uimanager, self) 303 304 # Create history objects 305 for nav_type in ('Person', 'Family', 'Event', 'Place', 'Source', 306 'Citation', 'Repository', 'Note', 'Media'): 307 self.uistate.register(self.dbstate, nav_type, 0) 308 309 self.dbstate.connect('database-changed', self.uistate.db_changed) 310 311 self.tags = Tags(self.uistate, self.dbstate) 312 313 # handle OPEN Recent Menu, insert it into the toolbar. 314 self.recent_manager = RecentDocsMenu( 315 self.uistate, self.dbstate, self._read_recent_file) 316 self.recent_manager.build(update_menu=False) 317 318 self.db_loader = DbLoader(self.dbstate, self.uistate) 319 320 self.__setup_navigator() 321 322 # need to get toolbar again, because it is a new object now. 323 toolbar = self.uimanager.get_widget('ToolBar') 324 if self.show_toolbar: 325 toolbar.show() 326 else: 327 toolbar.hide() 328 329 if self.fullscreen: 330 self.window.fullscreen() 331 332 self.window.set_title("%s - Gramps" % _('No Family Tree')) 333 self.window.show() 334 335 def __setup_navigator(self): 336 """ 337 If we have enabled te sidebar, show it, and turn off the tabs. If 338 disabled, hide the sidebar and turn on the tabs. 339 """ 340 if self.show_navigator: 341 self.ebox.show() 342 else: 343 self.ebox.hide() 344 345 def __connect_signals(self): 346 """ 347 Connects the signals needed 348 """ 349 self.del_event = self.window.connect('delete-event', self.quit) 350 self.notebook.connect('switch-page', self.view_changed) 351 352 def __init_lists(self): 353 """ 354 Initialize the actions lists for the UIManager 355 """ 356 self._app_actionlist = [ 357 ('quit', self.quit, None if is_quartz() else "<PRIMARY>q"), 358 ('preferences', self.preferences_activate), 359 ('about', self.display_about_box), ] 360 361 self._file_action_list = [ 362 #('FileMenu', None, _('_Family Trees')), 363 ('Open', self.__open_activate, "<PRIMARY>o"), 364 #('OpenRecent'_("Open an existing database")), 365 #('quit', self.quit, "<PRIMARY>q"), 366 #('ViewMenu', None, _('_View')), 367 ('Navigator', self.navigator_toggle, "<PRIMARY>m", 368 self.show_navigator), 369 ('Toolbar', self.toolbar_toggle, '', self.show_toolbar), 370 ('Fullscreen', self.fullscreen_toggle, "F11", self.fullscreen), 371 #('EditMenu', None, _('_Edit')), 372 #('preferences', self.preferences_activate), 373 #('HelpMenu', None, _('_Help')), 374 ('HomePage', home_page_activate), 375 ('MailingLists', mailing_lists_activate), 376 ('ReportBug', report_bug_activate), 377 ('ExtraPlugins', extra_plugins_activate), 378 #('about', self.display_about_box), 379 ('PluginStatus', self.__plugin_status), 380 ('FAQ', faq_activate), 381 ('KeyBindings', key_bindings), 382 ('UserManual', manual_activate, 'F1'), 383 ('TipOfDay', self.tip_of_day_activate), ] 384 385 self._readonly_action_list = [ 386 ('Close', self.close_database, "<control>w"), 387 ('Export', self.export_data, "<PRIMARY>e"), 388 ('Backup', self.quick_backup), 389 ('Abandon', self.abort), 390 ('Reports', self.reports_clicked), 391 #('GoMenu', None, _('_Go')), 392 #('ReportsMenu', None, _('_Reports')), 393 ('Books', self.run_book), 394 #('WindowsMenu', None, _('_Windows')), 395 #('F2', self.__keypress, 'F2'), #pedigreeview 396 #('F3', self.__keypress, 'F3'), # timelinepedigreeview 397 #('F4', self.__keypress, 'F4'), # timelinepedigreeview 398 #('F5', self.__keypress, 'F5'), # timelinepedigreeview 399 #('F6', self.__keypress, 'F6'), # timelinepedigreeview 400 #('F7', self.__keypress, 'F7'), 401 #('F8', self.__keypress, 'F8'), 402 #('F9', self.__keypress, 'F9'), 403 #('F11', self.__keypress, 'F11'), # used to go full screen 404 #('F12', self.__keypress, 'F12'), 405 #('<PRIMARY>BackSpace', self.__keypress, '<PRIMARY>BackSpace'), 406 #('<PRIMARY>Delete', self.__keypress, '<PRIMARY>Delete'), 407 #('<PRIMARY>Insert', self.__keypress, '<PRIMARY>Insert'), 408 #('<PRIMARY>J', self.__keypress, '<PRIMARY>J'), 409 ('PRIMARY-1', self.__gocat, '<PRIMARY>1'), 410 ('PRIMARY-2', self.__gocat, '<PRIMARY>2'), 411 ('PRIMARY-3', self.__gocat, '<PRIMARY>3'), 412 ('PRIMARY-4', self.__gocat, '<PRIMARY>4'), 413 ('PRIMARY-5', self.__gocat, '<PRIMARY>5'), 414 ('PRIMARY-6', self.__gocat, '<PRIMARY>6'), 415 ('PRIMARY-7', self.__gocat, '<PRIMARY>7'), 416 ('PRIMARY-8', self.__gocat, '<PRIMARY>8'), 417 ('PRIMARY-9', self.__gocat, '<PRIMARY>9'), 418 ('PRIMARY-0', self.__gocat, '<PRIMARY>0'), 419 # NOTE: CTRL+ALT+NUMBER is set in gramps.gui.navigator 420 ('PRIMARY-N', self.__next_view, '<PRIMARY>N'), 421 # the following conflicts with PrintView!!! 422 ('PRIMARY-P', self.__prev_view, '<PRIMARY>P'), ] 423 424 self._action_action_list = [ 425 ('Clipboard', self.clipboard, "<PRIMARY>b"), 426 #('AddMenu', None, _('_Add')), 427 #('AddNewMenu', None, _('New')), 428 ('PersonAdd', self.add_new_person, "<shift><Alt>p"), 429 ('FamilyAdd', self.add_new_family, "<shift><Alt>f"), 430 ('EventAdd', self.add_new_event, "<shift><Alt>e"), 431 ('PlaceAdd', self.add_new_place, "<shift><Alt>l"), 432 ('SourceAdd', self.add_new_source, "<shift><Alt>s"), 433 ('CitationAdd', self.add_new_citation, "<shift><Alt>c"), 434 ('RepositoryAdd', self.add_new_repository, "<shift><Alt>r"), 435 ('MediaAdd', self.add_new_media, "<shift><Alt>m"), 436 ('NoteAdd', self.add_new_note, "<shift><Alt>n"), 437 ('UndoHistory', self.undo_history, "<PRIMARY>H"), 438 #-------------------------------------- 439 ('Import', self.import_data, "<PRIMARY>i"), 440 ('Tools', self.tools_clicked), 441 #('BookMenu', None, _('_Bookmarks')), 442 #('ToolsMenu', None, _('_Tools')), 443 ('ConfigView', self.config_view, '<shift><PRIMARY>c'), ] 444 445 self._undo_action_list = [ 446 ('Undo', self.undo, '<PRIMARY>z'), ] 447 448 self._redo_action_list = [ 449 ('Redo', self.redo, '<shift><PRIMARY>z'), ] 450 451 def run_book(self, *action): 452 """ 453 Run a book. 454 """ 455 try: 456 BookSelector(self.dbstate, self.uistate) 457 except WindowActiveError: 458 return 459 460 def __gocat(self, action, value): 461 """ 462 Callback that is called on ctrl+number press. It moves to the 463 requested category like __next_view/__prev_view. 0 is 10 464 """ 465 cat = int(action.get_name()[-1]) 466 if cat == 0: 467 cat = 10 468 cat -= 1 469 if cat >= len(self.current_views): 470 #this view is not present 471 return False 472 self.goto_page(cat, None) 473 474 def __next_view(self, action, value): 475 """ 476 Callback that is called when the next category action is selected. It 477 selects the next category as the active category. If we reach the end, 478 we wrap around to the first. 479 """ 480 curpage = self.notebook.get_current_page() 481 #find cat and view of the current page 482 for key in self.page_lookup: 483 if self.page_lookup[key] == curpage: 484 cat_num, view_num = key 485 break 486 #now go to next category 487 if cat_num >= len(self.current_views)-1: 488 self.goto_page(0, None) 489 else: 490 self.goto_page(cat_num+1, None) 491 492 def __prev_view(self, action, value): 493 """ 494 Callback that is called when the previous category action is selected. 495 It selects the previous category as the active category. If we reach 496 the beginning of the list, we wrap around to the last. 497 """ 498 curpage = self.notebook.get_current_page() 499 #find cat and view of the current page 500 for key in self.page_lookup: 501 if self.page_lookup[key] == curpage: 502 cat_num, view_num = key 503 break 504 #now go to next category 505 if cat_num > 0: 506 self.goto_page(cat_num-1, None) 507 else: 508 self.goto_page(len(self.current_views)-1, None) 509 510 def init_interface(self): 511 """ 512 Initialize the interface. 513 """ 514 self.views = self.get_available_views() 515 defaults = views_to_show(self.views, 516 config.get('preferences.use-last-view')) 517 self.current_views = defaults[2] 518 519 self.navigator.load_plugins(self.dbstate, self.uistate) 520 521 self.goto_page(defaults[0], defaults[1]) 522 523 self.uimanager.set_actions_sensitive(self.fileactions, False) 524 self.__build_tools_menu(self._pmgr.get_reg_tools()) 525 self.__build_report_menu(self._pmgr.get_reg_reports()) 526 self._pmgr.connect('plugins-reloaded', 527 self.__rebuild_report_and_tool_menus) 528 self.uimanager.set_actions_sensitive(self.fileactions, True) 529 if not self.file_loaded: 530 self.uimanager.set_actions_visible(self.actiongroup, False) 531 self.uimanager.set_actions_visible(self.readonlygroup, False) 532 self.uimanager.set_actions_visible(self.undoactions, False) 533 self.uimanager.set_actions_visible(self.redoactions, False) 534 self.uimanager.update_menu() 535 config.connect("interface.statusbar", self.__statusbar_key_update) 536 537 def __statusbar_key_update(self, client, cnxn_id, entry, data): 538 """ 539 Callback function for statusbar key update 540 """ 541 self.uistate.modify_statusbar(self.dbstate) 542 543 def post_init_interface(self, show_manager=True): 544 """ 545 Showing the main window is deferred so that 546 ArgHandler can work without it always shown 547 """ 548 self.window.show() 549 if not self.dbstate.is_open() and show_manager: 550 self.__open_activate(None, None) 551 552 def do_reg_plugins(self, dbstate, uistate, rescan=False): 553 """ 554 Register the plugins at initialization time. The plugin status window 555 is opened on an error if the user has requested. 556 """ 557 # registering plugins 558 self.uistate.status_text(_('Registering plugins...')) 559 error = CLIManager.do_reg_plugins(self, dbstate, uistate, 560 rescan=rescan) 561 562 # get to see if we need to open the plugin status window 563 if error and config.get('behavior.pop-plugin-status'): 564 self.__plugin_status() 565 566 self.uistate.push_message(self.dbstate, _('Ready')) 567 568 def close_database(self, action=None, make_backup=True): 569 """ 570 Close the database 571 """ 572 self.dbstate.no_database() 573 self.post_close_db() 574 575 def no_del_event(self, *obj): 576 """ Routine to prevent window destroy with default handler if user 577 hits 'x' multiple times. """ 578 return True 579 580 def quit(self, *obj): 581 """ 582 Closes out the program, backing up data 583 """ 584 # mark interface insenstitive to prevent unexpected events 585 self.uistate.set_sensitive(False) 586 # the following prevents reentering quit if user hits 'x' again 587 self.window.disconnect(self.del_event) 588 # the following prevents premature closing of main window if user 589 # hits 'x' multiple times. 590 self.window.connect('delete-event', self.no_del_event) 591 592 # backup data 593 if config.get('database.backup-on-exit'): 594 self.autobackup() 595 596 # close the database 597 if self.dbstate.is_open(): 598 self.dbstate.db.close(user=self.user) 599 600 # have each page save anything, if they need to: 601 self.__delete_pages() 602 603 # save the current window size 604 (width, height) = self.window.get_size() 605 config.set('interface.main-window-width', width) 606 config.set('interface.main-window-height', height) 607 # save the current window position 608 (horiz_position, vert_position) = self.window.get_position() 609 config.set('interface.main-window-horiz-position', horiz_position) 610 config.set('interface.main-window-vert-position', vert_position) 611 config.save() 612 self.app.quit() 613 614 def abort(self, *obj): 615 """ 616 Abandon changes and quit. 617 """ 618 if self.dbstate.db.abort_possible: 619 620 dialog = QuestionDialog2( 621 _("Abort changes?"), 622 _("Aborting changes will return the database to the state " 623 "it was before you started this editing session."), 624 _("Abort changes"), 625 _("Cancel"), 626 parent=self.uistate.window) 627 628 if dialog.run(): 629 self.dbstate.db.disable_signals() 630 while self.dbstate.db.undo(): 631 pass 632 self.quit() 633 else: 634 WarningDialog( 635 _("Cannot abandon session's changes"), 636 _('Changes cannot be completely abandoned because the ' 637 'number of changes made in the session exceeded the ' 638 'limit.'), parent=self.uistate.window) 639 640 def __init_action_group(self, name, actions, sensitive=True, toggles=None): 641 """ 642 Initialize an action group for the UIManager 643 """ 644 new_group = ActionGroup(name, actions) 645 self.uimanager.insert_action_group(new_group) 646 self.uimanager.set_actions_sensitive(new_group, sensitive) 647 return new_group 648 649 def __build_ui_manager(self): 650 """ 651 Builds the action groups 652 """ 653 self.uimanager = self.app.uimanager 654 655 self.actiongroup = self.__init_action_group( 656 'RW', self._action_action_list) 657 self.readonlygroup = self.__init_action_group( 658 'RO', self._readonly_action_list) 659 self.fileactions = self.__init_action_group( 660 'FileWindow', self._file_action_list) 661 self.undoactions = self.__init_action_group( 662 'Undo', self._undo_action_list, sensitive=False) 663 self.redoactions = self.__init_action_group( 664 'Redo', self._redo_action_list, sensitive=False) 665 self.appactions = ActionGroup('AppActions', self._app_actionlist, 'app') 666 self.uimanager.insert_action_group(self.appactions, gio_group=self.app) 667 668 def preferences_activate(self, *obj): 669 """ 670 Open the preferences dialog. 671 """ 672 try: 673 GrampsPreferences(self.uistate, self.dbstate) 674 except WindowActiveError: 675 return 676 677 def reset_font(self): 678 """ 679 Reset to the default application font. 680 """ 681 Gtk.StyleContext.remove_provider_for_screen(self.window.get_screen(), 682 self.provider) 683 684 def change_font(self, font): 685 """ 686 Change the default application font. 687 Only in the case we use symbols. 688 """ 689 if config.get('utf8.in-use') and font != "": 690 css_font = CSS_FONT % font 691 try: 692 self.provider.load_from_data(css_font.encode('UTF-8')) 693 Gtk.StyleContext.add_provider_for_screen( 694 self.window.get_screen(), self.provider, 695 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 696 return True 697 except: 698 # Force gramps to use the standard font. 699 print("I can't set the new font :", font) 700 config.set('utf8.in-use', False) 701 config.set('utf8.selected-font', "") 702 return False 703 704 def tip_of_day_activate(self, *obj): 705 """ 706 Display Tip of the day 707 """ 708 from .tipofday import TipOfDay 709 TipOfDay(self.uistate) 710 711 def __plugin_status(self, obj=None, data=None): 712 """ 713 Display plugin status dialog 714 """ 715 try: 716 PluginWindows.PluginStatus(self.dbstate, self.uistate, []) 717 except WindowActiveError: 718 pass 719 720 def navigator_toggle(self, action, value): 721 """ 722 Set the sidebar based on the value of the toggle button. Save the 723 results in the configuration settings 724 """ 725 action.set_state(value) 726 if value.get_boolean(): 727 self.ebox.show() 728 config.set('interface.view', True) 729 self.show_navigator = True 730 else: 731 self.ebox.hide() 732 config.set('interface.view', False) 733 self.show_navigator = False 734 config.save() 735 736 def toolbar_toggle(self, action, value): 737 """ 738 Set the toolbar based on the value of the toggle button. Save the 739 results in the configuration settings 740 """ 741 action.set_state(value) 742 toolbar = self.uimanager.get_widget('ToolBar') 743 if value.get_boolean(): 744 toolbar.show_all() 745 config.set('interface.toolbar-on', True) 746 else: 747 toolbar.hide() 748 config.set('interface.toolbar-on', False) 749 config.save() 750 751 def fullscreen_toggle(self, action, value): 752 """ 753 Set the main Gramps window fullscreen based on the value of the 754 toggle button. Save the setting in the config file. 755 """ 756 action.set_state(value) 757 if value.get_boolean(): 758 self.window.fullscreen() 759 config.set('interface.fullscreen', True) 760 else: 761 self.window.unfullscreen() 762 config.set('interface.fullscreen', False) 763 config.save() 764 765 def get_views(self): 766 """ 767 Return the view definitions. 768 """ 769 return self.views 770 771 def goto_page(self, cat_num, view_num): 772 """ 773 Create the page if it doesn't exist and make it the current page. 774 """ 775 if view_num is None: 776 view_num = self.current_views[cat_num] 777 else: 778 self.current_views[cat_num] = view_num 779 780 page_num = self.page_lookup.get((cat_num, view_num)) 781 if page_num is None: 782 page_def = self.views[cat_num][view_num] 783 page_num = self.notebook.get_n_pages() 784 self.page_lookup[(cat_num, view_num)] = page_num 785 self.__create_page(page_def[0], page_def[1]) 786 787 self.notebook.set_current_page(page_num) 788 return self.pages[page_num] 789 790 def get_category(self, cat_name): 791 """ 792 Return the category number from the given category name. 793 """ 794 for cat_num, cat_views in enumerate(self.views): 795 if cat_name == cat_views[0][0].category[1]: 796 return cat_num 797 return None 798 799 def __create_dummy_page(self, pdata, error): 800 """ Create a dummy page """ 801 from .views.pageview import DummyPage 802 return DummyPage(pdata.name, pdata, self.dbstate, self.uistate, 803 _("View failed to load. Check error output."), error) 804 805 def __create_page(self, pdata, page_def): 806 """ 807 Create a new page and set it as the current page. 808 """ 809 try: 810 page = page_def(pdata, self.dbstate, self.uistate) 811 except: 812 import traceback 813 LOG.warning("View '%s' failed to load.", pdata.id) 814 traceback.print_exc() 815 page = self.__create_dummy_page(pdata, traceback.format_exc()) 816 817 try: 818 page_display = page.get_display() 819 except: 820 import traceback 821 print("ERROR: '%s' failed to create view" % pdata.name) 822 traceback.print_exc() 823 page = self.__create_dummy_page(pdata, traceback.format_exc()) 824 page_display = page.get_display() 825 826 page.define_actions() 827 page.post() 828 829 self.pages.append(page) 830 831 # create icon/label for notebook tab (useful for debugging) 832 hbox = Gtk.Box() 833 image = Gtk.Image() 834 image.set_from_icon_name(page.get_stock(), Gtk.IconSize.MENU) 835 hbox.pack_start(image, False, True, 0) 836 hbox.add(Gtk.Label(label=pdata.name)) 837 hbox.show_all() 838 page_num = self.notebook.append_page(page.get_display(), hbox) 839 self.active_page.post_create() 840 if not self.file_loaded: 841 self.uimanager.set_actions_visible(self.actiongroup, False) 842 self.uimanager.set_actions_visible(self.readonlygroup, False) 843 self.uimanager.set_actions_visible(self.undoactions, False) 844 self.uimanager.set_actions_visible(self.redoactions, False) 845 return page 846 847 def view_changed(self, notebook, page, page_num): 848 """ 849 Called when the notebook page is changed. 850 """ 851 if self.view_changing: 852 return 853 self.view_changing = True 854 855 cat_num = view_num = None 856 for key in self.page_lookup: 857 if self.page_lookup[key] == page_num: 858 cat_num, view_num = key 859 break 860 861 # Save last view in configuration 862 view_id = self.views[cat_num][view_num][0].id 863 config.set('preferences.last-view', view_id) 864 last_views = config.get('preferences.last-views') 865 if len(last_views) != len(self.views): 866 # If the number of categories has changed then reset the defaults 867 last_views = [''] * len(self.views) 868 last_views[cat_num] = view_id 869 config.set('preferences.last-views', last_views) 870 config.save() 871 872 self.navigator.view_changed(cat_num, view_num) 873 self.__change_page(page_num) 874 self.view_changing = False 875 876 def __change_page(self, page_num): 877 """ 878 Perform necessary actions when a page is changed. 879 """ 880 self.__disconnect_previous_page() 881 882 self.active_page = self.pages[page_num] 883 self.__connect_active_page(page_num) 884 self.active_page.set_active() 885 while Gtk.events_pending(): 886 Gtk.main_iteration() 887 888 # bug 12048 this avoids crash if part of toolbar in view is not shown 889 # because of a small screen when changing views. Part of the Gtk code 890 # was deleting a toolbar object too soon; and another part of Gtk still 891 # had a reference. 892 def page_changer(self): 893 self.uimanager.update_menu() 894 self.active_page.change_page() 895 return False 896 897 GLib.idle_add(page_changer, self, 898 priority=GLib.PRIORITY_DEFAULT_IDLE - 10) 899 900 def __delete_pages(self): 901 """ 902 Calls on_delete() for each view 903 """ 904 for page in self.pages: 905 page.on_delete() 906 907 def __disconnect_previous_page(self): 908 """ 909 Disconnects the previous page, removing the old action groups 910 and removes the old UI components. 911 """ 912 list(map(self.uimanager.remove_ui, self.merge_ids)) 913 914 if self.active_page is not None: 915 self.active_page.set_inactive() 916 groups = self.active_page.get_actions() 917 for grp in groups: 918 if grp in self.uimanager.get_action_groups(): 919 self.uimanager.remove_action_group(grp) 920 self.active_page = None 921 922 def __connect_active_page(self, page_num): 923 """ 924 Inserts the action groups associated with the current page 925 into the UIManager 926 """ 927 for grp in self.active_page.get_actions(): 928 self.uimanager.insert_action_group(grp) 929 930 uidef = self.active_page.ui_definition() 931 self.merge_ids = [self.uimanager.add_ui_from_string(uidef)] 932 933 for uidef in self.active_page.additional_ui_definitions(): 934 mergeid = self.uimanager.add_ui_from_string(uidef) 935 self.merge_ids.append(mergeid) 936 937 configaction = self.uimanager.get_action(self.actiongroup, 938 'ConfigView') 939 if self.active_page.can_configure(): 940 configaction.set_enabled(True) 941 else: 942 configaction.set_enabled(False) 943 944 def import_data(self, *obj): 945 """ 946 Imports a file 947 """ 948 if self.dbstate.is_open(): 949 self.db_loader.import_file() 950 infotxt = self.db_loader.import_info_text() 951 if infotxt: 952 InfoDialog(_('Import Statistics'), infotxt, 953 parent=self.window) 954 self.__post_load() 955 956 def __open_activate(self, obj, value): 957 """ 958 Called when the Open button is clicked, opens the DbManager 959 """ 960 from .dbman import DbManager 961 dialog = DbManager(self.uistate, self.dbstate, self, self.window) 962 value = dialog.run() 963 if value: 964 if self.dbstate.is_open(): 965 self.dbstate.db.close(user=self.user) 966 (filename, title) = value 967 self.db_loader.read_file(filename) 968 self._post_load_newdb(filename, 'x-directory/normal', title) 969 else: 970 if dialog.after_change != "": 971 # We change the title of the main window. 972 old_title = self.uistate.window.get_title() 973 if old_title: 974 delim = old_title.find(' - ') 975 tit1 = old_title[:delim] 976 tit2 = old_title[delim:] 977 new_title = dialog.after_change 978 if '<=' in tit2: 979 ## delim2 = tit2.find('<=') + 3 980 ## tit3 = tit2[delim2:-1] 981 new_title += tit2.replace(']', '') + ' => ' + tit1 + ']' 982 else: 983 new_title += tit2 + ' <= [' + tit1 + ']' 984 self.uistate.window.set_title(new_title) 985 986 def __post_load(self): 987 """ 988 This method is for the common UI post_load, both new files 989 and added data like imports. 990 """ 991 self.dbstate.db.undo_callback = self.__change_undo_label 992 self.dbstate.db.redo_callback = self.__change_redo_label 993 self.__change_undo_label(None, update_menu=False) 994 self.__change_redo_label(None, update_menu=False) 995 self.dbstate.db.undo_history_callback = self.undo_history_update 996 self.undo_history_close() 997 998 def _post_load_newdb(self, filename, filetype, title=None): 999 """ 1000 The method called after load of a new database. 1001 Inherit CLI method to add GUI part 1002 """ 1003 if self.dbstate.db.is_open(): 1004 self._post_load_newdb_nongui(filename, title) 1005 self._post_load_newdb_gui(filename, filetype, title) 1006 1007 def _post_load_newdb_gui(self, filename, filetype, title=None): 1008 """ 1009 Called after a new database is loaded to do GUI stuff 1010 """ 1011 # GUI related post load db stuff 1012 # Update window title 1013 if filename[-1] == os.path.sep: 1014 filename = filename[:-1] 1015 name = os.path.basename(filename) 1016 if title: 1017 name = title 1018 1019 isopen = self.dbstate.is_open() 1020 if not isopen: 1021 rw = False 1022 msg = "Gramps" 1023 else: 1024 rw = not self.dbstate.db.readonly 1025 if rw: 1026 msg = "%s - Gramps" % name 1027 else: 1028 msg = "%s (%s) - Gramps" % (name, _('Read Only')) 1029 self.uistate.window.set_title(msg) 1030 1031 if(bool(config.get('behavior.runcheck')) and QuestionDialog2( 1032 _("Gramps had a problem the last time it was run."), 1033 _("Would you like to run the Check and Repair tool?"), 1034 _("Yes"), _("No"), parent=self.uistate.window).run()): 1035 pdata = self._pmgr.get_plugin('check') 1036 mod = self._pmgr.load_plugin(pdata) 1037 tool.gui_tool(dbstate=self.dbstate, user=self.user, 1038 tool_class=getattr(mod, pdata.toolclass), 1039 options_class=getattr(mod, pdata.optionclass), 1040 translated_name=pdata.name, 1041 name=pdata.id, 1042 category=pdata.category, 1043 callback=self.dbstate.db.request_rebuild) 1044 config.set('behavior.runcheck', False) 1045 self.__change_page(self.notebook.get_current_page()) 1046 self.uimanager.set_actions_visible(self.actiongroup, rw) 1047 self.uimanager.set_actions_visible(self.readonlygroup, isopen) 1048 self.uimanager.set_actions_visible(self.undoactions, rw) 1049 self.uimanager.set_actions_visible(self.redoactions, rw) 1050 1051 self.recent_manager.build() 1052 1053 # Call common __post_load method for GUI update after a change 1054 self.__post_load() 1055 1056 def post_close_db(self): 1057 """ 1058 Called after a database is closed to do GUI stuff. 1059 """ 1060 self.undo_history_close() 1061 self.uistate.window.set_title("%s - Gramps" % _('No Family Tree')) 1062 self.uistate.clear_filter_results() 1063 self.__disconnect_previous_page() 1064 self.uimanager.set_actions_visible(self.actiongroup, False) 1065 self.uimanager.set_actions_visible(self.readonlygroup, False) 1066 self.uimanager.set_actions_visible(self.undoactions, False) 1067 self.uimanager.set_actions_visible(self.redoactions, False) 1068 self.uimanager.update_menu() 1069 config.set('paths.recent-file', '') 1070 config.save() 1071 1072 def __change_undo_label(self, label, update_menu=True): 1073 """ 1074 Change the UNDO label 1075 """ 1076 _menu = '''<placeholder id="undo"> 1077 <item> 1078 <attribute name="action">win.Undo</attribute> 1079 <attribute name="label">%s</attribute> 1080 </item> 1081 </placeholder> 1082 ''' 1083 if not label: 1084 label = _('_Undo') 1085 self.uimanager.set_actions_sensitive(self.undoactions, False) 1086 else: 1087 self.uimanager.set_actions_sensitive(self.undoactions, True) 1088 self.uimanager.add_ui_from_string([_menu % html.escape(label)]) 1089 if update_menu: 1090 self.uimanager.update_menu() 1091 1092 def __change_redo_label(self, label, update_menu=True): 1093 """ 1094 Change the REDO label 1095 """ 1096 _menu = '''<placeholder id="redo"> 1097 <item> 1098 <attribute name="action">win.Redo</attribute> 1099 <attribute name="label">%s</attribute> 1100 </item> 1101 </placeholder> 1102 ''' 1103 if not label: 1104 label = _('_Redo') 1105 self.uimanager.set_actions_sensitive(self.redoactions, False) 1106 else: 1107 self.uimanager.set_actions_sensitive(self.redoactions, True) 1108 self.uimanager.add_ui_from_string([_menu % html.escape(label)]) 1109 if update_menu: 1110 self.uimanager.update_menu() 1111 1112 def undo_history_update(self): 1113 """ 1114 This function is called to update both the state of 1115 the Undo History menu item (enable/disable) and 1116 the contents of the Undo History window. 1117 """ 1118 try: 1119 # Try updating undo history window if it exists 1120 self.undo_history_window.update() 1121 except AttributeError: 1122 # Let it go: history window does not exist 1123 return 1124 1125 def undo_history_close(self): 1126 """ 1127 Closes the undo history 1128 """ 1129 try: 1130 # Try closing undo history window if it exists 1131 if self.undo_history_window.opened: 1132 self.undo_history_window.close() 1133 except AttributeError: 1134 # Let it go: history window does not exist 1135 return 1136 1137 def quick_backup(self, *obj): 1138 """ 1139 Make a quick XML back with or without media. 1140 """ 1141 try: 1142 QuickBackup(self.dbstate, self.uistate, self.user) 1143 except WindowActiveError: 1144 return 1145 1146 def autobackup(self): 1147 """ 1148 Backup the current family tree. 1149 """ 1150 if self.dbstate.db.is_open() and self.dbstate.db.has_changed: 1151 self.uistate.set_busy_cursor(True) 1152 self.uistate.progress.show() 1153 self.uistate.push_message(self.dbstate, _("Autobackup...")) 1154 try: 1155 self.__backup() 1156 except DbWriteFailure as msg: 1157 self.uistate.push_message(self.dbstate, 1158 _("Error saving backup data")) 1159 self.uistate.set_busy_cursor(False) 1160 self.uistate.progress.hide() 1161 1162 def __backup(self): 1163 """ 1164 Backup database to a Gramps XML file. 1165 """ 1166 from gramps.plugins.export.exportxml import XmlWriter 1167 backup_path = config.get('database.backup-path') 1168 compress = config.get('database.compress-backup') 1169 writer = XmlWriter(self.dbstate.db, self.user, strip_photos=0, 1170 compress=compress) 1171 timestamp = '{0:%Y-%m-%d-%H-%M-%S}'.format(datetime.datetime.now()) 1172 backup_name = "%s-%s.gramps" % (self.dbstate.db.get_dbname(), 1173 timestamp) 1174 filename = os.path.join(backup_path, backup_name) 1175 writer.write(filename) 1176 1177 def reports_clicked(self, *obj): 1178 """ 1179 Displays the Reports dialog 1180 """ 1181 try: 1182 ReportPluginDialog(self.dbstate, self.uistate, []) 1183 except WindowActiveError: 1184 return 1185 1186 def tools_clicked(self, *obj): 1187 """ 1188 Displays the Tools dialog 1189 """ 1190 try: 1191 ToolPluginDialog(self.dbstate, self.uistate, []) 1192 except WindowActiveError: 1193 return 1194 1195 def clipboard(self, *obj): 1196 """ 1197 Displays the Clipboard 1198 """ 1199 from .clipboard import ClipboardWindow 1200 try: 1201 ClipboardWindow(self.dbstate, self.uistate) 1202 except WindowActiveError: 1203 return 1204 1205 # ---------------Add new xxx -------------------------------- 1206 def add_new_person(self, *obj): 1207 """ 1208 Add a new person to the database. (Global keybinding) 1209 """ 1210 person = Person() 1211 #the editor requires a surname 1212 person.primary_name.add_surname(Surname()) 1213 person.primary_name.set_primary_surname(0) 1214 1215 try: 1216 EditPerson(self.dbstate, self.uistate, [], person) 1217 except WindowActiveError: 1218 pass 1219 1220 def add_new_family(self, *obj): 1221 """ 1222 Add a new family to the database. (Global keybinding) 1223 """ 1224 family = Family() 1225 try: 1226 EditFamily(self.dbstate, self.uistate, [], family) 1227 except WindowActiveError: 1228 pass 1229 1230 def add_new_event(self, *obj): 1231 """ 1232 Add a new custom/unknown event (Note you type first letter of event) 1233 """ 1234 try: 1235 event = Event() 1236 event.set_type(EventType.UNKNOWN) 1237 EditEvent(self.dbstate, self.uistate, [], event) 1238 except WindowActiveError: 1239 pass 1240 1241 def add_new_place(self, *obj): 1242 """Add a new place to the place list""" 1243 try: 1244 EditPlace(self.dbstate, self.uistate, [], Place()) 1245 except WindowActiveError: 1246 pass 1247 1248 def add_new_source(self, *obj): 1249 """Add a new source to the source list""" 1250 try: 1251 EditSource(self.dbstate, self.uistate, [], Source()) 1252 except WindowActiveError: 1253 pass 1254 1255 def add_new_repository(self, *obj): 1256 """Add a new repository to the repository list""" 1257 try: 1258 EditRepository(self.dbstate, self.uistate, [], Repository()) 1259 except WindowActiveError: 1260 pass 1261 1262 def add_new_citation(self, *obj): 1263 """ 1264 Add a new citation 1265 """ 1266 try: 1267 EditCitation(self.dbstate, self.uistate, [], Citation()) 1268 except WindowActiveError: 1269 pass 1270 1271 def add_new_media(self, *obj): 1272 """Add a new media object to the media list""" 1273 try: 1274 EditMedia(self.dbstate, self.uistate, [], Media()) 1275 except WindowActiveError: 1276 pass 1277 1278 def add_new_note(self, *obj): 1279 """Add a new note to the note list""" 1280 try: 1281 EditNote(self.dbstate, self.uistate, [], Note()) 1282 except WindowActiveError: 1283 pass 1284 # ------------------------------------------------------------------------ 1285 1286 def config_view(self, *obj): 1287 """ 1288 Displays the configuration dialog for the active view 1289 """ 1290 self.active_page.configure() 1291 1292 def undo(self, *obj): 1293 """ 1294 Calls the undo function on the database 1295 """ 1296 self.uistate.set_busy_cursor(True) 1297 self.dbstate.db.undo() 1298 self.uistate.set_busy_cursor(False) 1299 1300 def redo(self, *obj): 1301 """ 1302 Calls the redo function on the database 1303 """ 1304 self.uistate.set_busy_cursor(True) 1305 self.dbstate.db.redo() 1306 self.uistate.set_busy_cursor(False) 1307 1308 def undo_history(self, *obj): 1309 """ 1310 Displays the Undo history window 1311 """ 1312 try: 1313 self.undo_history_window = UndoHistory(self.dbstate, self.uistate) 1314 except WindowActiveError: 1315 return 1316 1317 def export_data(self, *obj): 1318 """ 1319 Calls the ExportAssistant to export data 1320 """ 1321 if self.dbstate.is_open(): 1322 from .plug.export import ExportAssistant 1323 try: 1324 ExportAssistant(self.dbstate, self.uistate) 1325 except WindowActiveError: 1326 return 1327 1328 def __rebuild_report_and_tool_menus(self): 1329 """ 1330 Callback that rebuilds the tools and reports menu 1331 """ 1332 self.__build_tools_menu(self._pmgr.get_reg_tools()) 1333 self.__build_report_menu(self._pmgr.get_reg_reports()) 1334 self.uistate.set_relationship_class() 1335 1336 def __build_tools_menu(self, tool_menu_list): 1337 """ 1338 Builds a new tools menu 1339 """ 1340 if self.toolactions: 1341 self.uistate.uimanager.remove_action_group(self.toolactions) 1342 self.uistate.uimanager.remove_ui(self.tool_menu_ui_id) 1343 self.toolactions = ActionGroup(name='ToolWindow') 1344 (uidef, actions) = self.build_plugin_menu( 1345 'ToolsMenu', tool_menu_list, tool.tool_categories, 1346 make_plugin_callback) 1347 self.toolactions.add_actions(actions) 1348 self.tool_menu_ui_id = self.uistate.uimanager.add_ui_from_string(uidef) 1349 self.uimanager.insert_action_group(self.toolactions) 1350 1351 def __build_report_menu(self, report_menu_list): 1352 """ 1353 Builds a new reports menu 1354 """ 1355 if self.reportactions: 1356 self.uistate.uimanager.remove_action_group(self.reportactions) 1357 self.uistate.uimanager.remove_ui(self.report_menu_ui_id) 1358 self.reportactions = ActionGroup(name='ReportWindow') 1359 (udef, actions) = self.build_plugin_menu( 1360 'ReportsMenu', report_menu_list, standalone_categories, 1361 make_plugin_callback) 1362 self.reportactions.add_actions(actions) 1363 self.report_menu_ui_id = self.uistate.uimanager.add_ui_from_string(udef) 1364 self.uimanager.insert_action_group(self.reportactions) 1365 1366 def build_plugin_menu(self, text, item_list, categories, func): 1367 """ 1368 Builds a new XML description for a menu based on the list of plugindata 1369 """ 1370 menuitem = ('<item>\n' 1371 '<attribute name="action">win.%s</attribute>\n' 1372 '<attribute name="label">%s...</attribute>\n' 1373 '</item>\n') 1374 1375 actions = [] 1376 ofile = StringIO() 1377 ofile.write('<section id="%s">' % ('P_' + text)) 1378 1379 hash_data = defaultdict(list) 1380 for pdata in item_list: 1381 if not pdata.supported: 1382 category = _UNSUPPORTED 1383 else: 1384 category = categories[pdata.category] 1385 hash_data[category].append(pdata) 1386 1387 # Sort categories, skipping the unsupported 1388 catlist = sorted(item for item in hash_data if item != _UNSUPPORTED) 1389 1390 for key in catlist: 1391 ofile.write('<submenu>\n<attribute name="label"' 1392 '>%s</attribute>\n' % key[1]) 1393 pdatas = hash_data[key] 1394 pdatas.sort(key=lambda x: x.name) 1395 for pdata in pdatas: 1396 new_key = valid_action_name(pdata.id) 1397 ofile.write(menuitem % (new_key, pdata.name)) 1398 actions.append((new_key, func(pdata, self.dbstate, 1399 self.uistate))) 1400 ofile.write('</submenu>\n') 1401 1402 # If there are any unsupported items we add separator 1403 # and the unsupported category at the end of the menu 1404 if _UNSUPPORTED in hash_data: 1405 ofile.write('<submenu>\n<attribute name="label"' 1406 '>%s</attribute>\n' % 1407 _UNSUPPORTED[1]) 1408 pdatas = hash_data[_UNSUPPORTED] 1409 pdatas.sort(key=lambda x: x.name) 1410 for pdata in pdatas: 1411 new_key = pdata.id.replace(' ', '-') 1412 ofile.write(menuitem % (new_key, pdata.name)) 1413 actions.append((new_key, func(pdata, self.dbstate, 1414 self.uistate))) 1415 ofile.write('</submenu>\n') 1416 1417 ofile.write('</section>\n') 1418 return ([ofile.getvalue()], actions) 1419 1420 def display_about_box(self, *obj): 1421 """Display the About box.""" 1422 about = GrampsAboutDialog(self.uistate.window) 1423 about.run() 1424 about.destroy() 1425 1426 def get_available_views(self): 1427 """ 1428 Query the views and determine what views to show and in which order 1429 1430 :Returns: a list of lists containing tuples (view_id, viewclass) 1431 """ 1432 pmgr = GuiPluginManager.get_instance() 1433 view_list = pmgr.get_reg_views() 1434 viewstoshow = defaultdict(list) 1435 for pdata in view_list: 1436 mod = pmgr.load_plugin(pdata) 1437 if not mod or not hasattr(mod, pdata.viewclass): 1438 #import of plugin failed 1439 try: 1440 lasterror = pmgr.get_fail_list()[-1][1][1] 1441 except: 1442 lasterror = '*** No error found, ' 1443 lasterror += 'probably error in gpr.py file ***' 1444 ErrorDialog( 1445 _('Failed Loading View'), 1446 _('The view %(name)s did not load and reported an error.' 1447 '\n\n%(error_msg)s\n\n' 1448 'If you are unable to fix the fault yourself then you ' 1449 'can submit a bug at %(gramps_bugtracker_url)s ' 1450 'or contact the view author (%(firstauthoremail)s).\n\n' 1451 'If you do not want Gramps to try and load this view ' 1452 'again, you can hide it by using the Plugin Manager ' 1453 'on the Help menu.' 1454 ) % {'name': pdata.name, 1455 'gramps_bugtracker_url': URL_BUGHOME, 1456 'firstauthoremail': pdata.authors_email[0] 1457 if pdata.authors_email else '...', 1458 'error_msg': lasterror}, 1459 parent=self.uistate.window) 1460 continue 1461 viewclass = getattr(mod, pdata.viewclass) 1462 1463 # pdata.category is (string, trans-string): 1464 if pdata.order == START: 1465 viewstoshow[pdata.category[0]].insert(0, (pdata, viewclass)) 1466 else: 1467 viewstoshow[pdata.category[0]].append((pdata, viewclass)) 1468 1469 # First, get those in order defined, if exists: 1470 resultorder = [viewstoshow[cat] 1471 for cat in config.get("interface.view-categories") 1472 if cat in viewstoshow] 1473 1474 # Next, get the rest in some order: 1475 resultorder.extend(viewstoshow[cat] 1476 for cat in sorted(viewstoshow.keys()) 1477 if viewstoshow[cat] not in resultorder) 1478 return resultorder 1479 1480def key_bindings(*obj): 1481 """ 1482 Display key bindings 1483 """ 1484 display_help(webpage=WIKI_HELP_PAGE_KEY) 1485 1486def manual_activate(*obj): 1487 """ 1488 Display the Gramps manual 1489 """ 1490 display_help(webpage=WIKI_HELP_PAGE_MAN) 1491 1492def report_bug_activate(*obj): 1493 """ 1494 Display the bug tracker web site 1495 """ 1496 display_url(URL_BUGTRACKER) 1497 1498def home_page_activate(*obj): 1499 """ 1500 Display the Gramps home page 1501 """ 1502 display_url(URL_HOMEPAGE) 1503 1504def mailing_lists_activate(*obj): 1505 """ 1506 Display the mailing list web page 1507 """ 1508 display_url(URL_MAILINGLIST) 1509 1510def extra_plugins_activate(*obj): 1511 """ 1512 Display the wiki page with extra plugins 1513 """ 1514 display_url(URL_WIKISTRING+WIKI_EXTRAPLUGINS) 1515 1516def faq_activate(*obj): 1517 """ 1518 Display FAQ 1519 """ 1520 display_help(webpage=WIKI_HELP_PAGE_FAQ) 1521 1522def run_plugin(pdata, dbstate, uistate): 1523 """ 1524 run a plugin based on it's PluginData: 1525 1/ load plugin. 1526 2/ the report is run 1527 """ 1528 pmgr = GuiPluginManager.get_instance() 1529 mod = pmgr.load_plugin(pdata) 1530 if not mod: 1531 #import of plugin failed 1532 failed = pmgr.get_fail_list() 1533 if failed: 1534 error_msg = failed[-1][1][1] 1535 else: 1536 error_msg = "(no error message)" 1537 ErrorDialog( 1538 _('Failed Loading Plugin'), 1539 _('The plugin %(name)s did not load and reported an error.\n\n' 1540 '%(error_msg)s\n\n' 1541 'If you are unable to fix the fault yourself then you can ' 1542 'submit a bug at %(gramps_bugtracker_url)s or contact ' 1543 'the plugin author (%(firstauthoremail)s).\n\n' 1544 'If you do not want Gramps to try and load this plugin again, ' 1545 'you can hide it by using the Plugin Manager on the ' 1546 'Help menu.') % {'name' : pdata.name, 1547 'gramps_bugtracker_url' : URL_BUGHOME, 1548 'firstauthoremail' : pdata.authors_email[0] 1549 if pdata.authors_email 1550 else '...', 1551 'error_msg' : error_msg}, 1552 parent=uistate.window) 1553 return 1554 1555 if pdata.ptype == REPORT: 1556 report(dbstate, uistate, uistate.get_active('Person'), 1557 getattr(mod, pdata.reportclass), 1558 getattr(mod, pdata.optionclass), 1559 pdata.name, pdata.id, 1560 pdata.category, pdata.require_active) 1561 else: 1562 tool.gui_tool(dbstate=dbstate, user=User(uistate=uistate), 1563 tool_class=getattr(mod, pdata.toolclass), 1564 options_class=getattr(mod, pdata.optionclass), 1565 translated_name=pdata.name, 1566 name=pdata.id, 1567 category=pdata.category, 1568 callback=dbstate.db.request_rebuild) 1569 gc.collect(2) 1570 1571def make_plugin_callback(pdata, dbstate, uistate): 1572 """ 1573 Makes a callback for a report/tool menu item 1574 """ 1575 return lambda x, y: run_plugin(pdata, dbstate, uistate) 1576 1577def views_to_show(views, use_last=True): 1578 """ 1579 Determine based on preference setting which views should be shown 1580 """ 1581 current_cat = 0 1582 current_cat_view = 0 1583 default_cat_views = [0] * len(views) 1584 if use_last: 1585 current_page_id = config.get('preferences.last-view') 1586 default_page_ids = config.get('preferences.last-views') 1587 found = False 1588 for indexcat, cat_views in enumerate(views): 1589 cat_view = 0 1590 for pdata, page_def in cat_views: 1591 if not found: 1592 if pdata.id == current_page_id: 1593 current_cat = indexcat 1594 current_cat_view = cat_view 1595 default_cat_views[indexcat] = cat_view 1596 found = True 1597 break 1598 if pdata.id in default_page_ids: 1599 default_cat_views[indexcat] = cat_view 1600 cat_view += 1 1601 if not found: 1602 current_cat = 0 1603 current_cat_view = 0 1604 return current_cat, current_cat_view, default_cat_views 1605 1606class QuickBackup(ManagedWindow): # TODO move this class into its own module 1607 1608 def __init__(self, dbstate, uistate, user): 1609 """ 1610 Make a quick XML back with or without media. 1611 """ 1612 self.dbstate = dbstate 1613 self.user = user 1614 1615 ManagedWindow.__init__(self, uistate, [], self.__class__) 1616 window = Gtk.Dialog('', 1617 self.uistate.window, 1618 Gtk.DialogFlags.DESTROY_WITH_PARENT, None) 1619 self.set_window(window, None, _("Gramps XML Backup")) 1620 self.setup_configs('interface.quick-backup', 500, 150) 1621 close_button = window.add_button(_('_Close'), 1622 Gtk.ResponseType.CLOSE) 1623 ok_button = window.add_button(_('_OK'), 1624 Gtk.ResponseType.APPLY) 1625 vbox = window.get_content_area() 1626 hbox = Gtk.Box() 1627 label = Gtk.Label(label=_("Path:")) 1628 label.set_justify(Gtk.Justification.LEFT) 1629 label.set_size_request(90, -1) 1630 label.set_halign(Gtk.Align.START) 1631 hbox.pack_start(label, False, True, 0) 1632 path_entry = Gtk.Entry() 1633 dirtext = config.get('paths.quick-backup-directory') 1634 path_entry.set_text(dirtext) 1635 hbox.pack_start(path_entry, True, True, 0) 1636 file_entry = Gtk.Entry() 1637 button = Gtk.Button() 1638 button.connect("clicked", 1639 lambda widget: 1640 self.select_backup_path(widget, path_entry)) 1641 image = Gtk.Image() 1642 image.set_from_icon_name('document-open', Gtk.IconSize.BUTTON) 1643 image.show() 1644 button.add(image) 1645 hbox.pack_end(button, False, True, 0) 1646 vbox.pack_start(hbox, False, True, 0) 1647 hbox = Gtk.Box() 1648 label = Gtk.Label(label=_("File:")) 1649 label.set_justify(Gtk.Justification.LEFT) 1650 label.set_size_request(90, -1) 1651 label.set_halign(Gtk.Align.START) 1652 hbox.pack_start(label, False, True, 0) 1653 struct_time = time.localtime() 1654 file_entry.set_text( 1655 config.get('paths.quick-backup-filename' 1656 ) % {"filename": self.dbstate.db.get_dbname(), 1657 "year": struct_time.tm_year, 1658 "month": struct_time.tm_mon, 1659 "day": struct_time.tm_mday, 1660 "hour": struct_time.tm_hour, 1661 "minutes": struct_time.tm_min, 1662 "seconds": struct_time.tm_sec, 1663 "extension": "gpkg"}) 1664 hbox.pack_end(file_entry, True, True, 0) 1665 vbox.pack_start(hbox, False, True, 0) 1666 hbox = Gtk.Box() 1667 fbytes = 0 1668 mbytes = "0" 1669 for media in self.dbstate.db.iter_media(): 1670 fullname = media_path_full(self.dbstate.db, media.get_path()) 1671 try: 1672 fbytes += os.path.getsize(fullname) 1673 length = len(str(fbytes)) 1674 if fbytes <= 999999: 1675 mbytes = "< 1" 1676 else: 1677 mbytes = str(fbytes)[:(length-6)] 1678 except OSError: 1679 pass 1680 label = Gtk.Label(label=_("Media:")) 1681 label.set_justify(Gtk.Justification.LEFT) 1682 label.set_size_request(90, -1) 1683 label.set_halign(Gtk.Align.START) 1684 hbox.pack_start(label, False, True, 0) 1685 include = Gtk.RadioButton.new_with_mnemonic_from_widget( 1686 None, "%s (%s %s)" % (_("Include"), 1687 mbytes, _("Megabyte|MB"))) 1688 exclude = Gtk.RadioButton.new_with_mnemonic_from_widget(include, 1689 _("Exclude")) 1690 include.connect("toggled", lambda widget: self.media_toggle(widget, 1691 file_entry)) 1692 include_mode = config.get('preferences.quick-backup-include-mode') 1693 if include_mode: 1694 include.set_active(True) 1695 else: 1696 exclude.set_active(True) 1697 hbox.pack_start(include, False, True, 0) 1698 hbox.pack_end(exclude, False, True, 0) 1699 vbox.pack_start(hbox, False, True, 0) 1700 self.show() 1701 dbackup = window.run() 1702 if dbackup == Gtk.ResponseType.APPLY: 1703 # if file exists, ask if overwrite; else abort 1704 basefile = file_entry.get_text() 1705 basefile = basefile.replace("/", r"-") 1706 filename = os.path.join(path_entry.get_text(), basefile) 1707 if os.path.exists(filename): 1708 question = QuestionDialog2( 1709 _("Backup file already exists! Overwrite?"), 1710 _("The file '%s' exists.") % filename, 1711 _("Proceed and overwrite"), 1712 _("Cancel the backup"), 1713 parent=self.window) 1714 yes_no = question.run() 1715 if not yes_no: 1716 current_dir = path_entry.get_text() 1717 if current_dir != dirtext: 1718 config.set('paths.quick-backup-directory', current_dir) 1719 self.close() 1720 return 1721 position = self.window.get_position() # crock 1722 window.hide() 1723 self.window.move(position[0], position[1]) 1724 self.uistate.set_busy_cursor(True) 1725 self.uistate.pulse_progressbar(0) 1726 self.uistate.progress.show() 1727 self.uistate.push_message(self.dbstate, _("Making backup...")) 1728 if include.get_active(): 1729 from gramps.plugins.export.exportpkg import PackageWriter 1730 writer = PackageWriter(self.dbstate.db, filename, self.user) 1731 writer.export() 1732 else: 1733 from gramps.plugins.export.exportxml import XmlWriter 1734 writer = XmlWriter(self.dbstate.db, self.user, 1735 strip_photos=0, compress=1) 1736 writer.write(filename) 1737 self.uistate.set_busy_cursor(False) 1738 self.uistate.progress.hide() 1739 self.uistate.push_message(self.dbstate, 1740 _("Backup saved to '%s'") % filename) 1741 config.set('paths.quick-backup-directory', path_entry.get_text()) 1742 else: 1743 self.uistate.push_message(self.dbstate, _("Backup aborted")) 1744 if dbackup != Gtk.ResponseType.DELETE_EVENT: 1745 self.close() 1746 1747 def select_backup_path(self, widget, path_entry): 1748 """ 1749 Choose a backup folder. Make sure there is one highlighted in 1750 right pane, otherwise FileChooserDialog will hang. 1751 """ 1752 fdialog = Gtk.FileChooserDialog( 1753 title=_("Select backup directory"), 1754 parent=self.window, 1755 action=Gtk.FileChooserAction.SELECT_FOLDER, 1756 buttons=(_('_Cancel'), 1757 Gtk.ResponseType.CANCEL, 1758 _('_Apply'), 1759 Gtk.ResponseType.OK)) 1760 mpath = path_entry.get_text() 1761 if not mpath: 1762 mpath = HOME_DIR 1763 fdialog.set_current_folder(os.path.dirname(mpath)) 1764 fdialog.set_filename(os.path.join(mpath, ".")) 1765 status = fdialog.run() 1766 if status == Gtk.ResponseType.OK: 1767 filename = fdialog.get_filename() 1768 if filename: 1769 path_entry.set_text(filename) 1770 fdialog.destroy() 1771 return True 1772 1773 def media_toggle(self, widget, file_entry): 1774 """ 1775 Toggles media include values in the quick backup dialog. 1776 """ 1777 include = widget.get_active() 1778 config.set('preferences.quick-backup-include-mode', include) 1779 extension = "gpkg" if include else "gramps" 1780 filename = file_entry.get_text() 1781 if "." in filename: 1782 base, ext = filename.rsplit(".", 1) 1783 file_entry.set_text("%s.%s" % (base, extension)) 1784 else: 1785 file_entry.set_text("%s.%s" % (filename, extension)) 1786