1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2010 Nick Hall
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19#
20# $Id: navigator.py 20492 2012-10-02 21:08:19Z nick-h $
21
22"""
23A module that provides pluggable sidebars.  These provide an interface to
24manage pages in the main Gramps window.
25"""
26#-------------------------------------------------------------------------
27#
28# GNOME modules
29#
30#-------------------------------------------------------------------------
31from gi.repository import Gtk
32from gi.repository import Gdk
33
34#-------------------------------------------------------------------------
35#
36# Gramps modules
37#
38#-------------------------------------------------------------------------
39from gramps.gen.plug import (START, END)
40from .pluginmanager import GuiPluginManager
41from .uimanager import ActionGroup
42
43#-------------------------------------------------------------------------
44#
45# Constants
46#
47#-------------------------------------------------------------------------
48UICATEGORY = '''      <section id="ViewsInCatagory">
49        %s
50      </section>
51    '''
52UICATAGORYBAR = '''    <placeholder id='ViewsInCategoryBar'>
53        %s
54      </placeholder>
55    '''
56
57CATEGORY_ICON = {
58    'Dashboard': 'gramps-gramplet',
59    'People': 'gramps-person',
60    'Relationships': 'gramps-relation',
61    'Families': 'gramps-family',
62    'Events': 'gramps-event',
63    'Ancestry': 'gramps-pedigree',
64    'Places': 'gramps-place',
65    'Geography': 'gramps-geo',
66    'Sources': 'gramps-source',
67    'Repositories': 'gramps-repository',
68    'Media': 'gramps-media',
69    'Notes': 'gramps-notes',
70    'Citations': 'gramps-citation',
71}
72
73#-------------------------------------------------------------------------
74#
75# Navigator class
76#
77#-------------------------------------------------------------------------
78class Navigator:
79    """
80    A class which defines the graphical representation of the Gramps navigator.
81    """
82    def __init__(self, viewmanager):
83
84        self.viewmanager = viewmanager
85        self.pages = []
86        self.active_cat = None
87        self.active_view = None
88
89        self.ui_category = {}
90        self.cat_view_group = None
91        self.merge_ids = []
92
93        self.top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
94
95        frame = Gtk.Frame()
96        hbox = Gtk.Box()
97        hbox.show()
98        frame.add(hbox)
99        frame.show()
100
101        self.select_button = Gtk.ToggleButton()
102        self.select_button.set_relief(Gtk.ReliefStyle.NONE)
103        select_hbox = Gtk.Box()
104        self.title_label = Gtk.Label(label='')
105        arrow = Gtk.Arrow(arrow_type=Gtk.ArrowType.DOWN,
106                                    shadow_type=Gtk.ShadowType.NONE)
107        select_hbox.pack_start(self.title_label, False, True, 0)
108        select_hbox.pack_end(arrow, False, True, 0)
109        self.select_button.add(select_hbox)
110
111        self.select_button.connect('button_press_event',
112                                   self.__menu_button_pressed)
113
114        #close_button = Gtk.Button()
115        #img = Gtk.Image.new_from_icon_name('window-close', Gtk.IconSize.MENU)
116        #close_button.set_image(img)
117        #close_button.set_relief(Gtk.ReliefStyle.NONE)
118        #close_button.connect('clicked', self.cb_close_clicked)
119        hbox.pack_start(self.select_button, False, True, 0)
120        #hbox.pack_end(close_button, False, True, 0)
121
122        self.top.pack_end(frame, False, True, 0)
123
124        self.menu = Gtk.Menu()
125        self.menu.show()
126        self.menu.connect('deactivate', cb_menu_deactivate, self.select_button)
127
128        self.notebook = Gtk.Notebook()
129        self.notebook.show()
130        self.notebook.set_show_tabs(False)
131        self.notebook.set_show_border(False)
132        self.notebook.connect('switch_page', self.cb_switch_page)
133        self.top.show()
134        self.top.pack_start(self.notebook, True, True, 0)
135
136    def load_plugins(self, dbstate, uistate):
137        """
138        Load the sidebar plugins.
139        """
140        menuitem = '''
141            <item>
142              <attribute name="action">win.ViewInCatagory</attribute>
143              <attribute name="label" translatable="yes">%s</attribute>
144              <attribute name="target">%d %d</attribute>
145            </item>
146            '''
147        baritem = '''
148            <child>
149              <object class="GtkToggleToolButton" id="bar%d">
150                <property name="action-name">win.ViewInCatagory</property>
151                <property name="action-target">'%d %d'</property>
152                <property name="icon-name">%s</property>
153                <property name="tooltip_text" translatable="yes">%s</property>
154                <property name="label" translatable="yes">%s</property>
155              </object>
156              <packing>
157                <property name="homogeneous">False</property>
158              </packing>
159            </child>
160            '''
161
162        plugman = GuiPluginManager.get_instance()
163
164        categories = []
165        views = {}
166        for cat_num, cat_views in enumerate(self.viewmanager.get_views()):
167            uimenuitems = ''
168            uibaritems = ''
169            for view_num, page in enumerate(cat_views):
170
171                if view_num == 0:
172                    views[cat_num] = []
173                    cat_name = page[0].category[1]
174                    cat_icon = CATEGORY_ICON.get(page[0].category[0])
175                    if cat_icon is None:
176                        cat_icon = 'gramps-view'
177                    categories.append([cat_num, cat_name, cat_icon])
178
179                if view_num < 9:
180                    accel = "<PRIMARY><ALT>%d" % ((view_num % 9) + 1)
181                    self.viewmanager.uimanager.app.set_accels_for_action(
182                        "win.ViewInCatagory('%d %d')" % (cat_num, view_num),
183                        [accel])
184                uimenuitems += menuitem % (page[0].name, cat_num, view_num)
185
186                stock_icon = page[0].stock_icon
187                if stock_icon is None:
188                    stock_icon = cat_icon
189                uibaritems += baritem % (view_num, cat_num, view_num,
190                                         stock_icon, page[0].name,
191                                         page[0].name)
192
193                views[cat_num].append((view_num, page[0].name, stock_icon))
194
195            if len(cat_views) > 1:
196                #allow for switching views in a category
197                self.ui_category[cat_num] = [UICATEGORY % uimenuitems,
198                                             UICATAGORYBAR % uibaritems]
199
200        for pdata in plugman.get_reg_sidebars():
201            module = plugman.load_plugin(pdata)
202            if not module:
203                print("Error loading sidebar '%s': skipping content"
204                      % pdata.name)
205                continue
206
207            sidebar_class = getattr(module, pdata.sidebarclass)
208            sidebar_page = sidebar_class(dbstate, uistate, categories, views)
209            self.add(pdata.menu_label, sidebar_page, pdata.order)
210
211    def get_top(self):
212        """
213        Return the top container widget for the GUI.
214        """
215        return self.top
216
217    def add(self, title, sidebar, order):
218        """
219        Add a page to the sidebar for a plugin.
220        """
221        self.pages.append((title, sidebar))
222        page = sidebar.get_top()
223        # hide for now so notebook is only size of active page
224        page.get_child().hide()
225        index = self.notebook.append_page(page, Gtk.Label(label=title))
226
227        menu_item = Gtk.MenuItem(label=title)
228        if order == START:
229            self.menu.prepend(menu_item)
230            self.notebook.set_current_page(index)
231        else:
232            self.menu.append(menu_item)
233        menu_item.connect('activate', self.cb_menu_activate, index)
234        menu_item.show()
235
236        if self.notebook.get_n_pages() == 2:
237            self.select_button.show_all()
238
239    def view_changed(self, cat_num, view_num):
240        """
241        Called when a Gramps view is changed.
242        """
243        self.active_cat = cat_num
244        self.active_view = view_num
245
246        # Add buttons to the menu for the different view in the category
247        uimanager = self.viewmanager.uimanager
248        if self.cat_view_group:
249            if self.cat_view_group in uimanager.get_action_groups():
250                uimanager.remove_action_group(self.cat_view_group)
251
252            list(map(uimanager.remove_ui, self.merge_ids))
253
254        if cat_num in self.ui_category:
255            action = ('ViewInCatagory', self.cb_view_clicked, '',
256                      str(cat_num) + ' ' + str(view_num))
257            self.cat_view_group = ActionGroup('viewmenu', [action])
258            uimanager.insert_action_group(self.cat_view_group)
259            mergeid = uimanager.add_ui_from_string(self.ui_category[cat_num])
260            self.merge_ids.append(mergeid)
261
262        # Call the view_changed method for the active sidebar
263        try:
264            sidebar = self.pages[self.notebook.get_current_page()][1]
265        except IndexError:
266            return
267        sidebar.view_changed(cat_num, view_num)
268
269    def cb_view_clicked(self, radioaction, value):
270        """
271        Called when a view is selected from the menu.
272        """
273        cat_num, view_num = value.get_string().split()
274        self.viewmanager.goto_page(int(cat_num), int(view_num))
275
276    def __menu_button_pressed(self, button, event):
277        """
278        Called when the button to select a sidebar page is pressed.
279        """
280        if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
281            button.grab_focus()
282            button.set_active(True)
283
284            self.menu.popup(None, None, cb_menu_position, button, event.button,
285                            event.time)
286
287    def cb_menu_activate(self, menu, index):
288        """
289        Called when an item in the popup menu is selected.
290        """
291        self.notebook.set_current_page(index)
292
293    def cb_switch_page(self, notebook, unused, index):
294        """
295        Called when the user has switched to a new sidebar plugin page.
296        """
297        old_page = notebook.get_current_page()
298        if old_page != -1:
299            self.pages[old_page][1].inactive()
300            # hide so notebook is only size of active page
301            notebook.get_nth_page(old_page).get_child().hide()
302
303        self.pages[index][1].active(self.active_cat, self.active_view)
304        notebook.get_nth_page(index).get_child().show()
305        notebook.queue_resize()
306        if self.active_view is not None:
307            self.pages[index][1].view_changed(self.active_cat,
308                                              self.active_view)
309        self.title_label.set_text(self.pages[index][0])
310
311    def cb_close_clicked(self, button):
312        """
313        Called when the sidebar is closed.
314        """
315        uimanager = self.viewmanager.uimanager
316        uimanager.get_action('/MenuBar/ViewMenu/Navigator').activate()
317
318#-------------------------------------------------------------------------
319#
320# Functions
321#
322#-------------------------------------------------------------------------
323def cb_menu_position(*args):
324    """
325    Determine the position of the popup menu.
326    """
327    # takes two argument: menu, button
328    if len(args) == 2:
329        menu = args[0]
330        button = args[1]
331    # broken introspection can't handle MenuPositionFunc annotations corectly
332    else:
333        menu = args[0]
334        button = args[3]
335    ret_val, x_pos, y_pos = button.get_window().get_origin()
336    x_pos += button.get_allocation().x
337    y_pos += button.get_allocation().y + button.get_allocation().height
338
339    return (x_pos, y_pos, False)
340
341def cb_menu_deactivate(menu, button):
342    """
343    Called when the popup menu disappears.
344    """
345    button.set_active(False)
346