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