# # 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 = [ '' '' '' '' ] MRU_BTM = [ '' '' '' '' ] #------------------------------------------------------------------------------ # # 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)