#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2001-2007 Donald N. Allingham
# Copyright (C) 2009-2010 Nick Hall
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
"""
Provide the base classes for GRAMPS' DataView classes
"""
#----------------------------------------------------------------
#
# python modules
#
#----------------------------------------------------------------
import logging
_LOG = logging.getLogger('.navigationview')
#----------------------------------------------------------------
#
# gtk
#
#----------------------------------------------------------------
import gtk
#----------------------------------------------------------------
#
# GRAMPS
#
#----------------------------------------------------------------
from gui.views.pageview import PageView
from gen.ggettext import sgettext as _
from Utils import navigation_label
DISABLED = -1
MRU_SIZE = 10
MRU_TOP = [
''
''
''
''
''
]
#------------------------------------------------------------------------------
#
# NavigationView
#
#------------------------------------------------------------------------------
class NavigationView(PageView):
"""
The NavigationView class is the base class for all Data Views that require
navigation functionalilty. Views that need bookmarks and forward/backward
should derive from this class.
"""
def __init__(self, title, state, uistate, bookmarks, bm_type, nav_group):
PageView.__init__(self, title, state, uistate)
self.bookmarks = bm_type(self.dbstate, self.uistate, bookmarks,
self.goto_handle)
self.fwd_action = None
self.back_action = None
self.book_action = None
self.other_action = None
self.active_signal = None
self.mru_signal = None
self.nav_group = nav_group
self.mru_active = DISABLED
self.uistate.register(state, self.navigation_type(), self.nav_group)
def navigation_type(self):
"""
Indictates the navigation type. Navigation type can be the string
name of any of the primary Objects. A History object will be
created for it, see DisplayState.History
"""
return None
def define_actions(self):
"""
Define menu actions.
"""
self.bookmark_actions()
self.navigation_actions()
def disable_action_group(self):
"""
Normally, this would not be overridden from the base class. However,
in this case, we have additional action groups that need to be
handled correctly.
"""
PageView.disable_action_group(self)
self.fwd_action.set_visible(False)
self.back_action.set_visible(False)
def enable_action_group(self, obj):
"""
Normally, this would not be overridden from the base class. However,
in this case, we have additional action groups that need to be
handled correctly.
"""
PageView.enable_action_group(self, obj)
self.fwd_action.set_visible(True)
self.back_action.set_visible(True)
hobj = self.get_history()
self.fwd_action.set_sensitive(not hobj.at_end())
self.back_action.set_sensitive(not hobj.at_front())
def change_page(self):
"""
Called when the page changes.
"""
hobj = self.get_history()
self.fwd_action.set_sensitive(not hobj.at_end())
self.back_action.set_sensitive(not hobj.at_front())
self.other_action.set_sensitive(not self.dbstate.db.readonly)
self.uistate.modify_statusbar(self.dbstate)
def set_active(self):
"""
Called when the page becomes active (displayed).
"""
PageView.set_active(self)
self.bookmarks.display()
hobj = self.get_history()
self.active_signal = hobj.connect('active-changed', self.goto_active)
self.mru_signal = hobj.connect('mru-changed', self.update_mru_menu)
self.update_mru_menu(hobj.mru)
self.goto_active(None)
def set_inactive(self):
"""
Called when the page becomes inactive (not displayed).
"""
if self.active:
PageView.set_inactive(self)
self.bookmarks.undisplay()
hobj = self.get_history()
hobj.disconnect(self.active_signal)
hobj.disconnect(self.mru_signal)
self.mru_disable()
def navigation_group(self):
"""
Return the navigation group.
"""
return self.nav_group
def get_history(self):
"""
Return the history object.
"""
return self.uistate.get_history(self.navigation_type(),
self.navigation_group())
def goto_active(self, active_handle):
"""
Callback (and usable function) that selects the active person
in the display tree.
"""
active_handle = self.uistate.get_active(self.navigation_type(),
self.navigation_group())
if active_handle:
self.goto_handle(active_handle)
hobj = self.get_history()
self.fwd_action.set_sensitive(not hobj.at_end())
self.back_action.set_sensitive(not hobj.at_front())
def get_active(self):
"""
Return the handle of the active object.
"""
hobj = self.uistate.get_history(self.navigation_type(),
self.navigation_group())
return hobj.present()
def change_active(self, handle):
"""
Changes the active object.
"""
hobj = self.get_history()
if handle and not hobj.lock and not (handle == hobj.present()):
hobj.push(handle)
def goto_handle(self, handle):
"""
Needs to be implemented by classes derived from this.
Used to move to the given handle.
"""
raise NotImplementedError
####################################################################
# BOOKMARKS
####################################################################
def add_bookmark(self, obj):
"""
Add a bookmark to the list.
"""
from gen.display.name import displayer as name_displayer
active_handle = self.uistate.get_active('Person')
active_person = self.dbstate.db.get_person_from_handle(active_handle)
if active_person:
self.bookmarks.add(active_handle)
name = name_displayer.display(active_person)
self.uistate.push_message(self.dbstate,
_("%s has been bookmarked") % name)
else:
from QuestionDialog import WarningDialog
WarningDialog(
_("Could Not Set a Bookmark"),
_("A bookmark could not be set because "
"no one was selected."))
def edit_bookmarks(self, obj):
"""
Call the bookmark editor.
"""
self.bookmarks.edit()
def bookmark_actions(self):
"""
Define the bookmark menu actions.
"""
self.book_action = gtk.ActionGroup(self.title + '/Bookmark')
self.book_action.add_actions([
('AddBook', 'gramps-bookmark-new', _('_Add Bookmark'),
'd', None, self.add_bookmark),
('EditBook', 'gramps-bookmark-edit',
_("%(title)s...") % {'title': _("Organize Bookmarks")},
'b', None,
self.edit_bookmarks),
])
self._add_action_group(self.book_action)
####################################################################
# NAVIGATION
####################################################################
def navigation_actions(self):
"""
Define the navigation menu actions.
"""
# add the Forward action group to handle the Forward button
self.fwd_action = gtk.ActionGroup(self.title + '/Forward')
self.fwd_action.add_actions([
('Forward', gtk.STOCK_GO_FORWARD, _("_Forward"),
"Right", _("Go to the next person in the history"),
self.fwd_clicked)
])
# add the Backward action group to handle the Forward button
self.back_action = gtk.ActionGroup(self.title + '/Backward')
self.back_action.add_actions([
('Back', gtk.STOCK_GO_BACK, _("_Back"),
"Left", _("Go to the previous person in the history"),
self.back_clicked)
])
self._add_action('HomePerson', gtk.STOCK_HOME, _("_Home"),
accel="Home",
tip=_("Go to the default person"), callback=self.home)
self.other_action = gtk.ActionGroup(self.title + '/PersonOther')
self.other_action.add_actions([
('SetActive', gtk.STOCK_HOME, _("Set _Home Person"), None,
None, self.set_default_person),
])
self._add_action_group(self.back_action)
self._add_action_group(self.fwd_action)
self._add_action_group(self.other_action)
def set_default_person(self, obj):
"""
Set the default person.
"""
active = self.uistate.get_active('Person')
if active:
self.dbstate.db.set_default_person_handle(active)
def home(self, obj):
"""
Move to the default person.
"""
defperson = self.dbstate.db.get_default_person()
if defperson:
self.change_active(defperson.get_handle())
def jump(self):
"""
A dialog to move to a Gramps ID entered by the user.
"""
dialog = gtk.Dialog(_('Jump to by Gramps ID'), None,
gtk.DIALOG_NO_SEPARATOR)
dialog.set_border_width(12)
label = gtk.Label('%s' %
_('Jump to by Gramps ID'))
label.set_use_markup(True)
dialog.vbox.add(label)
dialog.vbox.set_spacing(10)
dialog.vbox.set_border_width(12)
hbox = gtk.HBox()
hbox.pack_start(gtk.Label("%s: " % _('ID')), False)
text = gtk.Entry()
text.set_activates_default(True)
hbox.pack_start(text, False)
dialog.vbox.pack_start(hbox, False)
dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_JUMP_TO, gtk.RESPONSE_OK)
dialog.set_default_response(gtk.RESPONSE_OK)
dialog.vbox.show_all()
if dialog.run() == gtk.RESPONSE_OK:
gid = text.get_text()
handle = self.get_handle_from_gramps_id(gid)
if handle is not None:
self.change_active(handle)
self.goto_handle(handle)
else:
self.uistate.push_message(
self.dbstate,
_("Error: %s is not a valid Gramps ID") % gid)
dialog.destroy()
def get_handle_from_gramps_id(self, gid):
"""
Get an object handle from its Gramps ID.
Needs to be implemented by the inheriting class.
"""
pass
def fwd_clicked(self, obj):
"""
Move forward one object in the history.
"""
hobj = self.get_history()
hobj.lock = True
if not hobj.at_end():
hobj.forward()
self.uistate.modify_statusbar(self.dbstate)
self.fwd_action.set_sensitive(not hobj.at_end())
self.back_action.set_sensitive(True)
hobj.lock = False
def back_clicked(self, obj):
"""
Move backward one object in the history.
"""
hobj = self.get_history()
hobj.lock = True
if not hobj.at_front():
hobj.back()
self.uistate.modify_statusbar(self.dbstate)
self.back_action.set_sensitive(not hobj.at_front())
self.fwd_action.set_sensitive(True)
hobj.lock = False
####################################################################
# MRU functions
####################################################################
def mru_disable(self):
"""
Remove the UI and action groups for the MRU list.
"""
if self.mru_active != DISABLED:
self.uistate.uimanager.remove_ui(self.mru_active)
self.uistate.uimanager.remove_action_group(self.mru_action)
self.mru_active = DISABLED
def mru_enable(self):
"""
Enables the UI and action groups for the MRU list.
"""
if self.mru_active == DISABLED:
self.uistate.uimanager.insert_action_group(self.mru_action, 1)
self.mru_active = self.uistate.uimanager.add_ui_from_string(self.mru_ui)
self.uistate.uimanager.ensure_update()
def update_mru_menu(self, items):
"""
Builds the UI and action group for the MRU list.
"""
self.mru_disable()
nav_type = self.navigation_type()
hobj = self.get_history()
menu_len = min(len(items) - 1, MRU_SIZE)
entry = ''
data = [entry % (nav_type, index) for index in range(0, menu_len)]
self.mru_ui = "".join(MRU_TOP) + "".join(data) + "".join(MRU_BTM)
mitems = items[-MRU_SIZE - 1:-1] # Ignore current handle
mitems.reverse()
data = []
for index, handle in enumerate(mitems):
name, obj = navigation_label(self.dbstate.db, nav_type, handle)
data.append(('%s%02d'%(nav_type, index), None, name,
"%d" % index, None,
make_callback(hobj.push, handle)))
self.mru_action = gtk.ActionGroup(nav_type)
self.mru_action.add_actions(data)
self.mru_enable()
####################################################################
# Template functions
####################################################################
def get_bookmarks(self):
"""
Template function to get bookmarks.
We could implement this here based on navigation_type()
"""
raise NotImplementedError
def edit(self, obj):
"""
Template function to allow the editing of the selected object
"""
raise NotImplementedError
def remove(self, handle):
"""
Template function to allow the removal of an object by its handle
"""
raise NotImplementedError
def add(self, obj):
"""
Template function to allow the adding of a new object
"""
raise NotImplementedError
def remove_object_from_handle(self, handle):
"""
Template function to allow the removal of an object by its handle
"""
raise NotImplementedError
def build_tree(self):
"""
Rebuilds the current display. This must be overridden by the derived
class.
"""
raise NotImplementedError
def build_widget(self):
"""
Builds the container widget for the interface. Must be overridden by the
the base class. Returns a gtk container widget.
"""
raise NotImplementedError
def key_press_handler(self, widget, event):
"""
Handle the control+c (copy) and control+v (paste), or pass it on.
"""
if self.active:
if event.type == gtk.gdk.KEY_PRESS:
if (event.keyval == gtk.keysyms.c and
event.state == gtk.gdk.CONTROL_MASK | gtk.gdk.MOD2_MASK):
self.call_copy()
return True
elif (event.keyval == gtk.keysyms.v and
event.state == gtk.gdk.CONTROL_MASK | gtk.gdk.MOD2_MASK):
self.call_paste()
return True
return False
def call_copy(self):
"""
This code is called on Control+C in a navigation view. If the
copy can be handled, it returns true, otherwise false.
The code brings up the Clipboard (if already exists) or
creates it. The copy is handled through the drag and drop
system.
"""
import cPickle as pickle
from ScratchPad import ScratchPadWindow, obj2target
nav_type, nav_group = self.navigation_type(), self.navigation_group()
active_handle = self.uistate.get_active(nav_type, nav_group)
handled = False
for active_handle in self.selected_handles():
clipboard = None
for widget in self.uistate.gwm.window_tree:
if isinstance(widget, ScratchPadWindow):
clipboard = widget
if clipboard is None:
clipboard = ScratchPadWindow(self.dbstate, self.uistate)
# Construct a drop:
drag_type = obj2target(nav_type)
if drag_type:
class Selection(object):
def __init__(self, data):
self.data = data
class Context(object):
targets = [drag_type]
action = 1
# eg: ('person-link', 23767, '27365123671', 0)
data = (drag_type, id(self), active_handle, 0)
clipboard.object_list.object_drag_data_received(
clipboard.object_list._widget, # widget
Context(), # drag type and action
0, 0, # x, y
Selection(pickle.dumps(data)), # pickled data
None, # info (not used)
-1) # time
handled = True
return handled
def call_paste(self):
"""
This code is called on Control+V in a navigation view. If the
copy can be handled, it returns true, otherwise false.
The code creates the Clipboard if it does not already exist.
"""
from ScratchPad import ScratchPadWindow, obj2target
clipboard = None
for widget in self.uistate.gwm.window_tree:
if isinstance(widget, ScratchPadWindow):
clipboard = widget
if clipboard is None:
clipboard = ScratchPadWindow(self.dbstate, self.uistate)
return True
return False
def make_callback(func, handle):
"""
Generates a callback function based off the passed arguments
"""
return lambda x: func(handle)