1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2001-2007  Donald N. Allingham
5# Copyright (C) 2009-2010  Nick Hall
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22"""
23Provide the base classes for GRAMPS' DataView classes
24"""
25
26#----------------------------------------------------------------
27#
28# python modules
29#
30#----------------------------------------------------------------
31from abc import abstractmethod
32import html
33import logging
34
35_LOG = logging.getLogger('.navigationview')
36
37#----------------------------------------------------------------
38#
39# gtk
40#
41#----------------------------------------------------------------
42from gi.repository import Gdk
43from gi.repository import Gtk
44
45#----------------------------------------------------------------
46#
47# Gramps
48#
49#----------------------------------------------------------------
50from gramps.gen.const import GRAMPS_LOCALE as glocale
51_ = glocale.translation.sgettext
52from .pageview import PageView
53from ..uimanager import ActionGroup
54from gramps.gen.utils.db import navigation_label
55from gramps.gen.constfunc import mod_key
56from ..utils import match_primary_mask
57
58DISABLED = -1
59MRU_SIZE = 10
60
61MRU_TOP = '<section id="CommonHistory">'
62MRU_BTM = '</section>'
63
64#------------------------------------------------------------------------------
65#
66# NavigationView
67#
68#------------------------------------------------------------------------------
69class NavigationView(PageView):
70    """
71    The NavigationView class is the base class for all Data Views that require
72    navigation functionalilty. Views that need bookmarks and forward/backward
73    should derive from this class.
74    """
75
76    def __init__(self, title, pdata, state, uistate, bm_type, nav_group):
77        PageView.__init__(self, title, pdata, state, uistate)
78        self.bookmarks = bm_type(self.dbstate, self.uistate, self.change_active)
79
80        self.fwd_action = None
81        self.back_action = None
82        self.book_action = None
83        self.other_action = None
84        self.active_signal = None
85        self.mru_signal = None
86        self.nav_group = nav_group
87        self.mru_active = DISABLED
88        self.uimanager = uistate.uimanager
89
90        self.uistate.register(state, self.navigation_type(), self.nav_group)
91
92
93    def navigation_type(self):
94        """
95        Indictates the navigation type. Navigation type can be the string
96        name of any of the primary Objects. A History object will be
97        created for it, see DisplayState.History
98        """
99        return None
100
101    def define_actions(self):
102        """
103        Define menu actions.
104        """
105        PageView.define_actions(self)
106        self.bookmark_actions()
107        self.navigation_actions()
108
109    def disable_action_group(self):
110        """
111        Normally, this would not be overridden from the base class. However,
112        in this case, we have additional action groups that need to be
113        handled correctly.
114        """
115        PageView.disable_action_group(self)
116
117        self.uimanager.set_actions_visible(self.fwd_action, False)
118        self.uimanager.set_actions_visible(self.back_action, False)
119
120    def enable_action_group(self, obj):
121        """
122        Normally, this would not be overridden from the base class. However,
123        in this case, we have additional action groups that need to be
124        handled correctly.
125        """
126        PageView.enable_action_group(self, obj)
127
128        self.uimanager.set_actions_visible(self.fwd_action, True)
129        self.uimanager.set_actions_visible(self.back_action, True)
130        hobj = self.get_history()
131        self.uimanager.set_actions_sensitive(self.fwd_action,
132                                             not hobj.at_end())
133        self.uimanager.set_actions_sensitive(self.back_action,
134                                             not hobj.at_front())
135
136    def change_page(self):
137        """
138        Called when the page changes.
139        """
140        hobj = self.get_history()
141        self.uimanager.set_actions_sensitive(self.fwd_action,
142                                             not hobj.at_end())
143        self.uimanager.set_actions_sensitive(self.back_action,
144                                             not hobj.at_front())
145        self.uimanager.set_actions_sensitive(self.other_action,
146                                             not self.dbstate.db.readonly)
147        self.uistate.modify_statusbar(self.dbstate)
148
149    def set_active(self):
150        """
151        Called when the page becomes active (displayed).
152        """
153        PageView.set_active(self)
154        self.bookmarks.display()
155
156        hobj = self.get_history()
157        self.active_signal = hobj.connect('active-changed', self.goto_active)
158        self.mru_signal = hobj.connect('mru-changed', self.update_mru_menu)
159        self.update_mru_menu(hobj.mru, update_menu=False)
160
161        self.goto_active(None)
162
163    def set_inactive(self):
164        """
165        Called when the page becomes inactive (not displayed).
166        """
167        if self.active:
168            PageView.set_inactive(self)
169            self.bookmarks.undisplay()
170            hobj = self.get_history()
171            hobj.disconnect(self.active_signal)
172            hobj.disconnect(self.mru_signal)
173            self.mru_disable()
174
175    def navigation_group(self):
176        """
177        Return the navigation group.
178        """
179        return self.nav_group
180
181    def get_history(self):
182        """
183        Return the history object.
184        """
185        return self.uistate.get_history(self.navigation_type(),
186                                        self.navigation_group())
187
188    def goto_active(self, active_handle):
189        """
190        Callback (and usable function) that selects the active person
191        in the display tree.
192        """
193        active_handle = self.uistate.get_active(self.navigation_type(),
194                                                self.navigation_group())
195        if active_handle:
196            self.goto_handle(active_handle)
197
198        hobj = self.get_history()
199        self.uimanager.set_actions_sensitive(self.fwd_action,
200                                             not hobj.at_end())
201        self.uimanager.set_actions_sensitive(self.back_action,
202                                             not hobj.at_front())
203
204    def get_active(self):
205        """
206        Return the handle of the active object.
207        """
208        hobj = self.uistate.get_history(self.navigation_type(),
209                                        self.navigation_group())
210        return hobj.present()
211
212    def change_active(self, handle):
213        """
214        Changes the active object.
215        """
216        hobj = self.get_history()
217        if handle and not hobj.lock and not (handle == hobj.present()):
218            hobj.push(handle)
219
220    @abstractmethod
221    def goto_handle(self, handle):
222        """
223        Needs to be implemented by classes derived from this.
224        Used to move to the given handle.
225        """
226
227    def selected_handles(self):
228        """
229        Return the active person's handle in a list. Used for
230        compatibility with those list views that can return multiply
231        selected items.
232        """
233        active_handle = self.uistate.get_active(self.navigation_type(),
234                                                self.navigation_group())
235        return [active_handle] if active_handle else []
236
237    ####################################################################
238    # BOOKMARKS
239    ####################################################################
240    def add_bookmark(self, *obj):
241        """
242        Add a bookmark to the list.
243        """
244        from gramps.gen.display.name import displayer as name_displayer
245
246        active_handle = self.uistate.get_active('Person')
247        active_person = self.dbstate.db.get_person_from_handle(active_handle)
248        if active_person:
249            self.bookmarks.add(active_handle)
250            name = name_displayer.display(active_person)
251            self.uistate.push_message(self.dbstate,
252                                      _("%s has been bookmarked") % name)
253        else:
254            from ..dialog import WarningDialog
255            WarningDialog(
256                _("Could Not Set a Bookmark"),
257                _("A bookmark could not be set because "
258                  "no one was selected."),
259                parent=self.uistate.window)
260
261    def edit_bookmarks(self, *obj):
262        """
263        Call the bookmark editor.
264        """
265        self.bookmarks.edit()
266
267    def bookmark_actions(self):
268        """
269        Define the bookmark menu actions.
270        """
271        self.book_action = ActionGroup(name=self.title + '/Bookmark')
272        self.book_action.add_actions([
273            ('AddBook', self.add_bookmark, '<PRIMARY>d'),
274            ('EditBook', self.edit_bookmarks, '<shift><PRIMARY>D'),
275            ])
276
277        self._add_action_group(self.book_action)
278
279    ####################################################################
280    # NAVIGATION
281    ####################################################################
282    def navigation_actions(self):
283        """
284        Define the navigation menu actions.
285        """
286        # add the Forward action group to handle the Forward button
287        self.fwd_action = ActionGroup(name=self.title + '/Forward')
288        self.fwd_action.add_actions([('Forward', self.fwd_clicked,
289                                      "%sRight" % mod_key())])
290
291        # add the Backward action group to handle the Forward button
292        self.back_action = ActionGroup(name=self.title + '/Backward')
293        self.back_action.add_actions([('Back', self.back_clicked,
294                                       "%sLeft" % mod_key())])
295
296        self._add_action('HomePerson', self.home, "%sHome" % mod_key())
297
298        self.other_action = ActionGroup(name=self.title + '/PersonOther')
299        self.other_action.add_actions([
300            ('SetActive', self.set_default_person)])
301
302        self._add_action_group(self.back_action)
303        self._add_action_group(self.fwd_action)
304        self._add_action_group(self.other_action)
305
306    def set_default_person(self, *obj):
307        """
308        Set the default person.
309        """
310        active = self.uistate.get_active('Person')
311        if active:
312            self.dbstate.db.set_default_person_handle(active)
313
314    def home(self, *obj):
315        """
316        Move to the default person.
317        """
318        defperson = self.dbstate.db.get_default_person()
319        if defperson:
320            self.change_active(defperson.get_handle())
321        else:
322            from ..dialog import WarningDialog
323            WarningDialog(_("No Home Person"),
324                _("You need to set a 'Home Person' to go to. "
325                  "Select the People View, select the person you want as "
326                  "'Home Person', then confirm your choice "
327                  "via the menu Edit -> Set Home Person."),
328                parent=self.uistate.window)
329
330    def jump(self, *obj):
331        """
332        A dialog to move to a Gramps ID entered by the user.
333        """
334        dialog = Gtk.Dialog(_('Jump to by Gramps ID'), self.uistate.window)
335        dialog.set_border_width(12)
336        label = Gtk.Label(label='<span weight="bold" size="larger">%s</span>' %
337                          _('Jump to by Gramps ID'))
338        label.set_use_markup(True)
339        dialog.vbox.add(label)
340        dialog.vbox.set_spacing(10)
341        dialog.vbox.set_border_width(12)
342        hbox = Gtk.Box()
343        hbox.pack_start(Gtk.Label(label=_("%s: ") % _('ID')), True, True, 0)
344        text = Gtk.Entry()
345        text.set_activates_default(True)
346        hbox.pack_start(text, False, True, 0)
347        dialog.vbox.pack_start(hbox, False, True, 0)
348        dialog.add_buttons(_('_Cancel'), Gtk.ResponseType.CANCEL,
349                           _('_Jump to'), Gtk.ResponseType.OK)
350        dialog.set_default_response(Gtk.ResponseType.OK)
351        dialog.vbox.show_all()
352
353        if dialog.run() == Gtk.ResponseType.OK:
354            gid = text.get_text()
355            handle = self.get_handle_from_gramps_id(gid)
356            if handle is not None:
357                self.change_active(handle)
358            else:
359                self.uistate.push_message(
360                    self.dbstate,
361                    _("Error: %s is not a valid Gramps ID") % gid)
362        dialog.destroy()
363
364    def get_handle_from_gramps_id(self, gid):
365        """
366        Get an object handle from its Gramps ID.
367        Needs to be implemented by the inheriting class.
368        """
369        pass
370
371    def fwd_clicked(self, *obj):
372        """
373        Move forward one object in the history.
374        """
375        hobj = self.get_history()
376        hobj.lock = True
377        if not hobj.at_end():
378            hobj.forward()
379            self.uistate.modify_statusbar(self.dbstate)
380        self.uimanager.set_actions_sensitive(self.fwd_action,
381                                             not hobj.at_end())
382        self.uimanager.set_actions_sensitive(self.back_action, True)
383        hobj.lock = False
384
385    def back_clicked(self, *obj):
386        """
387        Move backward one object in the history.
388        """
389        hobj = self.get_history()
390        hobj.lock = True
391        if not hobj.at_front():
392            hobj.back()
393            self.uistate.modify_statusbar(self.dbstate)
394        self.uimanager.set_actions_sensitive(self.back_action,
395                                             not hobj.at_front())
396        self.uimanager.set_actions_sensitive(self.fwd_action, True)
397        hobj.lock = False
398
399    ####################################################################
400    # MRU functions
401    ####################################################################
402
403    def mru_disable(self):
404        """
405        Remove the UI and action groups for the MRU list.
406        """
407        if self.mru_active != DISABLED:
408            self.uimanager.remove_ui(self.mru_active)
409            self.uimanager.remove_action_group(self.mru_action)
410            self.mru_active = DISABLED
411
412    def mru_enable(self, update_menu=False):
413        """
414        Enables the UI and action groups for the MRU list.
415        """
416        if self.mru_active == DISABLED:
417            self.uimanager.insert_action_group(self.mru_action)
418            self.mru_active = self.uimanager.add_ui_from_string(self.mru_ui)
419            if update_menu:
420                self.uimanager.update_menu()
421
422    def update_mru_menu(self, items, update_menu=True):
423        """
424        Builds the UI and action group for the MRU list.
425        """
426        menuitem = '''        <item>
427              <attribute name="action">win.%s%02d</attribute>
428              <attribute name="label">%s</attribute>
429            </item>
430            '''
431        menus = ''
432        self.mru_disable()
433        nav_type = self.navigation_type()
434        hobj = self.get_history()
435        menu_len = min(len(items) - 1, MRU_SIZE)
436
437        data = []
438        for index in range(menu_len - 1, -1, -1):
439            name, _obj = navigation_label(self.dbstate.db, nav_type,
440                                          items[index])
441            menus += menuitem % (nav_type, index, html.escape(name))
442            data.append(('%s%02d' % (nav_type, index),
443                         make_callback(hobj.push, items[index]),
444                         "%s%d" % (mod_key(), menu_len - 1 - index)))
445        self.mru_ui = [MRU_TOP + menus + MRU_BTM]
446
447        self.mru_action = ActionGroup(name=self.title + '/MRU')
448        self.mru_action.add_actions(data)
449        self.mru_enable(update_menu)
450
451    ####################################################################
452    # Template functions
453    ####################################################################
454    @abstractmethod
455    def build_tree(self):
456        """
457        Rebuilds the current display. This must be overridden by the derived
458        class.
459        """
460
461    @abstractmethod
462    def build_widget(self):
463        """
464        Builds the container widget for the interface. Must be overridden by the
465        the base class. Returns a gtk container widget.
466        """
467
468    def key_press_handler(self, widget, event):
469        """
470        Handle the control+c (copy) and control+v (paste), or pass it on.
471        """
472        if self.active:
473            if event.type == Gdk.EventType.KEY_PRESS:
474                if (event.keyval == Gdk.KEY_c and
475                    match_primary_mask(event.get_state())):
476                    self.call_copy()
477                    return True
478        return super(NavigationView, self).key_press_handler(widget, event)
479
480    def call_copy(self):
481        """
482        Navigation specific copy (control+c) hander. If the
483        copy can be handled, it returns true, otherwise false.
484
485        The code brings up the Clipboard (if already exists) or
486        creates it. The copy is handled through the drag and drop
487        system.
488        """
489        nav_type = self.navigation_type()
490        handles = self.selected_handles()
491        return self.copy_to_clipboard(nav_type, handles)
492
493def make_callback(func, handle):
494    """
495    Generates a callback function based off the passed arguments
496    """
497    return lambda x, y: func(handle)
498