diff --git a/configure.in b/configure.in index 95cbd96cc..bbdc06a4d 100644 --- a/configure.in +++ b/configure.in @@ -140,6 +140,7 @@ src/plugins/lib/Makefile src/plugins/mapservices/Makefile src/plugins/quickview/Makefile src/plugins/rel/Makefile +src/plugins/sidebar/Makefile src/plugins/textreport/Makefile src/plugins/tool/Makefile src/plugins/view/Makefile diff --git a/src/gen/plug/_manager.py b/src/gen/plug/_manager.py index 7bd2174b1..bb92aa3b4 100644 --- a/src/gen/plug/_manager.py +++ b/src/gen/plug/_manager.py @@ -288,6 +288,11 @@ class BasePluginManager(object): """ return self.__pgr.gramplet_plugins() + def get_reg_sidebars(self): + """ Return list of registered sidebars. + """ + return self.__pgr.sidebar_plugins() + def get_external_opt_dict(self): """ Return the dictionary of external options. """ return self.__external_opt_dict diff --git a/src/gen/plug/_pluginreg.py b/src/gen/plug/_pluginreg.py index 31487ac15..2be679a12 100644 --- a/src/gen/plug/_pluginreg.py +++ b/src/gen/plug/_pluginreg.py @@ -68,8 +68,9 @@ MAPSERVICE = 7 VIEW = 8 RELCALC = 9 GRAMPLET = 10 -PTYPE = [ REPORT , QUICKREPORT, TOOL, IMPORT, - EXPORT, DOCGEN, GENERAL, MAPSERVICE, VIEW, RELCALC, GRAMPLET] +SIDEBAR = 11 +PTYPE = [REPORT , QUICKREPORT, TOOL, IMPORT, EXPORT, DOCGEN, GENERAL, + MAPSERVICE, VIEW, RELCALC, GRAMPLET, SIDEBAR] PTYPE_STR = { REPORT: _('Report') , QUICKREPORT: _('Quickreport'), @@ -82,6 +83,7 @@ PTYPE_STR = { VIEW: _('Gramps View'), RELCALC: _('Relationships'), GRAMPLET: _('Gramplet'), + SIDEBAR: _('Sidebar'), } #possible report categories @@ -301,6 +303,14 @@ class PluginData(object): the view is appended to the list of views. If START, then the view is prepended. Only set START if you want a view to be the first in the order of views + .. attribute:: stock_icon + The icon in the toolbar or sidebar used to select the view + + Attributes for SIDEBAR plugins + .. attribute:: sidebarclass + The class that defines the sidebar. + .. attribute:: menu_label + A label to use on the seltion menu. """ def __init__(self): @@ -364,6 +374,10 @@ class PluginData(object): #VIEW attr self._viewclass = None self._order = END + self._stock_icon = None + #SIDEBAR attr + self._sidebarclass = None + self._menu_label = '' def _set_id(self, id): self._id = id @@ -811,10 +825,38 @@ class PluginData(object): def _get_order(self): return self._order - viewclass = property(_get_viewclass, _set_viewclass) - order = property(_get_order, _set_order) - + def _set_stock_icon(self, stock_icon): + if not self._ptype == VIEW: + raise ValueError, 'stock_icon may only be set for VIEW plugins' + self._stock_icon = stock_icon + def _get_stock_icon(self): + return self._stock_icon + + viewclass = property(_get_viewclass, _set_viewclass) + order = property(_get_order, _set_order) + stock_icon = property(_get_stock_icon, _set_stock_icon) + + #SIDEBAR attributes + def _set_sidebarclass(self, sidebarclass): + if not self._ptype == SIDEBAR: + raise ValueError, 'sidebarclass may only be set for SIDEBAR plugins' + self._sidebarclass = sidebarclass + + def _get_sidebarclass(self): + return self._sidebarclass + + def _set_menu_label(self, menu_label): + if not self._ptype == SIDEBAR: + raise ValueError, 'menu_label may only be set for SIDEBAR plugins' + self._menu_label = menu_label + + def _get_menu_label(self): + return self._menu_label + + sidebarclass = property(_get_sidebarclass, _set_sidebarclass) + menu_label = property(_get_menu_label, _set_menu_label) + def newplugin(): """ Function to create a new plugindata object, add it to list of @@ -865,6 +907,7 @@ def make_environment(**kwargs): 'VIEW': VIEW, 'RELCALC': RELCALC, 'GRAMPLET': GRAMPLET, + 'SIDEBAR': SIDEBAR, 'CATEGORY_TEXT': CATEGORY_TEXT, 'CATEGORY_DRAW': CATEGORY_DRAW, 'CATEGORY_CODE': CATEGORY_CODE, @@ -1106,6 +1149,11 @@ class PluginRegister(object): """Return a list of PluginData that are of type GRAMPLET """ return self.type_plugins(GRAMPLET) + + def sidebar_plugins(self): + """Return a list of PluginData that are of type SIDEBAR + """ + return self.type_plugins(SIDEBAR) def filter_load_on_reg(self): """Return a list of PluginData that have load_on_reg == True diff --git a/src/gui/Makefile.am b/src/gui/Makefile.am index 764bf1ff1..0f30dad49 100644 --- a/src/gui/Makefile.am +++ b/src/gui/Makefile.am @@ -21,6 +21,7 @@ pkgdata_PYTHON = \ filtereditor.py \ grampsgui.py \ pluginmanager.py \ + sidebar.py \ utils.py \ viewmanager.py diff --git a/src/gui/grampsgui.py b/src/gui/grampsgui.py index 3eda845ac..8d466187e 100644 --- a/src/gui/grampsgui.py +++ b/src/gui/grampsgui.py @@ -62,8 +62,7 @@ from QuestionDialog import ErrorDialog import config import Utils from constfunc import win -from gui.pluginmanager import GuiPluginManager, base_reg_stock_icons -from gen.plug import (START, END) +from gui.pluginmanager import base_reg_stock_icons #------------------------------------------------------------------------- # @@ -71,7 +70,6 @@ from gen.plug import (START, END) # #------------------------------------------------------------------------- - def register_stock_icons (): """ Add the gramps names for its icons (eg gramps-person) to the GTK icon @@ -185,53 +183,6 @@ def _display_welcome_message(): config.set('behavior.autoload', False) # config.set('behavior.betawarn', True) config.set('behavior.betawarn', config.get('behavior.betawarn')) - -def construct_view_order(): - """ - Query the views and determine what views to show and in which order - - :Returns: a list of lists containing tuples (view_id, viewclass) - """ - pmgr = GuiPluginManager.get_instance() - view_list = pmgr.get_reg_views() - viewstoshow = {} - for pdata in view_list: - mod = pmgr.load_plugin(pdata) - if not mod or not hasattr(mod, pdata.viewclass): - #import of plugin failed - ErrorDialog( - _('Failed Loading View'), - _('The view %(name)s did not load. See Help Menu, Plugin Manager' - ' for more info.\nUse http://bugs.gramps-project.org to' - ' submit bugs of official views, contact the view ' - 'author (%(firstauthoremail)s) otherwise. ') % { - 'name': pdata.name, - 'firstauthoremail': pdata.authors_email[0] if - pdata.authors_email else '...'}) - continue - viewclass = getattr(mod, pdata.viewclass) - # pdata.category is (string, trans-string): - if pdata.category[0] in viewstoshow: - if pdata.order == START: - viewstoshow[pdata.category[0]].insert(0, ((pdata, viewclass))) - else: - viewstoshow[pdata.category[0]].append((pdata, viewclass)) - else: - viewstoshow[pdata.category[0]] = [(pdata, viewclass)] - - resultorder = [] - # First, get those in order defined, if exists: - for item in config.get("interface.view-categories"): - if item in viewstoshow: - resultorder.append(viewstoshow[item]) - # Next, get the rest in some order: - viewstoshow_names = viewstoshow.keys() - viewstoshow_names.sort() - for item in viewstoshow_names: - if viewstoshow[item] in resultorder: - continue - resultorder.append(viewstoshow[item]) - return resultorder #------------------------------------------------------------------------- # @@ -256,11 +207,7 @@ class Gramps(object): dbstate = DbState.DbState() self.vm = ViewManager(dbstate, config.get("interface.view-categories")) - - #now we determine which views are present, which to show, and we - #instruct the viewmanager to show them - vieworder = construct_view_order() - self.vm.init_interface(vieworder) + self.vm.init_interface() #act based on the given arguments ah = ArgHandler(dbstate, argparser, self.vm, self.argerrorfunc, @@ -289,7 +236,6 @@ class Gramps(object): def argerrorfunc(self, string): """ Show basic errors in argument handling in GUI fashion""" ErrorDialog(_("Error parsing arguments"), string) - #------------------------------------------------------------------------- # diff --git a/src/gui/pluginmanager.py b/src/gui/pluginmanager.py index 2cb8ee9e1..abfdefcb7 100644 --- a/src/gui/pluginmanager.py +++ b/src/gui/pluginmanager.py @@ -289,6 +289,12 @@ class GuiPluginManager(gen.utils.Callback): return [plg for plg in self.basemgr.get_reg_gramplets() if plg.id not in self.__hidden_plugins] + def get_reg_sidebars(self): + """ Return list of non hidden registered sidebars + """ + return [plg for plg in self.basemgr.get_reg_sidebars() + if plg.id not in self.__hidden_plugins] + def get_reg_importers(self): """ Return list of registered importers """ diff --git a/src/gui/sidebar.py b/src/gui/sidebar.py new file mode 100644 index 000000000..df712c5ff --- /dev/null +++ b/src/gui/sidebar.py @@ -0,0 +1,173 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 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$ + +""" +A module that provides pluggable sidebars. These provide an interface to +manage pages in the main Gramps window. +""" +#------------------------------------------------------------------------- +# +# GNOME modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# Sidebar class +# +#------------------------------------------------------------------------- +class Sidebar(object): + """ + A class which defines the graphical representation of the Gramps sidebar. + """ + def __init__(self, viewmanager): + + self.viewmanager = viewmanager + self.pages = [] + self.top = gtk.VBox() + + frame = gtk.Frame() + hbox = gtk.HBox() + frame.add(hbox) + + select_button = gtk.ToggleButton() + select_button.set_relief(gtk.RELIEF_NONE) + select_hbox = gtk.HBox() + self.title_label = gtk.Label('Category') + arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE) + select_hbox.pack_start(self.title_label, False) + select_hbox.pack_end(arrow, False) + select_button.add(select_hbox) + + select_button.connect('button_press_event', self.__menu_button_pressed) + + close_button = gtk.Button() + img = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + close_button.set_image(img) + close_button.set_relief(gtk.RELIEF_NONE) + close_button.connect('clicked', self.cb_close_clicked) + hbox.pack_start(select_button, False) + hbox.pack_end(close_button, False) + + self.top.pack_start(frame, False) + + self.menu = gtk.Menu() + self.menu.show() + self.menu.connect('deactivate', cb_menu_deactivate, select_button) + + self.notebook = gtk.Notebook() + self.notebook.show() + self.notebook.set_show_tabs(False) + self.notebook.set_show_border(False) + self.notebook.connect('switch_page', self.cb_switch_page) + self.top.pack_start(self.notebook, True) + + def get_top(self): + """ + Return the top container widget for the GUI. + """ + return self.top + + def add(self, title, sidebar): + """ + Add a page to the sidebar for a plugin. + """ + index = self.notebook.append_page(sidebar.get_top(), gtk.Label(title)) + self.pages.append((title, sidebar)) + + menu_item = gtk.MenuItem(title) + menu_item.connect('activate', self.cb_menu_activate, index) + menu_item.show() + self.menu.append(menu_item) + + def view_changed(self, page_num): + """ + Called when a Gramps view is changed. + """ + for page in self.pages: + page[1].view_changed(page_num) + + def handlers_block(self): + """ + Block signals to the buttons to prevent spurious events. + """ + for page in self.pages: + page[1].handlers_block() + + def handlers_unblock(self): + """ + Unblock signals to the buttons. + """ + for page in self.pages: + page[1].handlers_unblock() + + def __menu_button_pressed(self, button, event): + """ + Called when the button to select a sidebar page is pressed. + """ + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + button.grab_focus() + button.set_active(True) + + self.menu.popup(None, None, cb_menu_position, event.button, + event.time, button) + + def cb_menu_activate(self, menu, index): + """ + Called when an item in the popup menu is selected. + """ + self.notebook.set_current_page(index) + + def cb_switch_page(self, notebook, unused, index): + """ + Called when the user has switched to a new sidebar plugin page. + """ + if self.pages: + self.title_label.set_text(self.pages[index][0]) + + def cb_close_clicked(self, button): + """ + Called when the sidebar is closed. + """ + uimanager = self.viewmanager.uimanager + uimanager.get_action('/MenuBar/ViewMenu/Sidebar').activate() + +#------------------------------------------------------------------------- +# +# Functions +# +#------------------------------------------------------------------------- +def cb_menu_position(menu, button): + """ + Determine the position of the popup menu. + """ + x_pos, y_pos = button.window.get_origin() + x_pos += button.allocation.x + y_pos += button.allocation.y + button.allocation.height + + return (x_pos, y_pos, False) + +def cb_menu_deactivate(menu, button): + """ + Called when the popup menu disappears. + """ + button.set_active(False) diff --git a/src/gui/viewmanager.py b/src/gui/viewmanager.py index e7d8c1625..2cd339017 100644 --- a/src/gui/viewmanager.py +++ b/src/gui/viewmanager.py @@ -4,6 +4,7 @@ # Copyright (C) 2005-2007 Donald N. Allingham # Copyright (C) 2008 Brian G. Matherly # Copyright (C) 2009 Benny Malengier +# Copyright (C) 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 @@ -61,6 +62,7 @@ from PluginUtils import Tool, PluginWindows, \ ReportPluginDialog, ToolPluginDialog, gui_tool from gen.plug import REPORT from gui.pluginmanager import GuiPluginManager +from gen.plug import (START, END) import Relationship import ReportBase import DisplayState @@ -78,6 +80,7 @@ from gui.configure import GrampsPreferences from gen.db.backup import backup from gen.db.exceptions import DbException from GrampsAboutDialog import GrampsAboutDialog +from gui.sidebar import Sidebar #------------------------------------------------------------------------- # @@ -183,20 +186,6 @@ UIDEFAULT = ''' ''' -UICATEGORY = ''' - - - %s - - - - - %s - - - -''' - WIKI_HELP_PAGE_FAQ = '%s_-_FAQ' % const.URL_MANUAL_PAGE WIKI_HELP_PAGE_KEY = '%s_-_Keybindings' % const.URL_MANUAL_PAGE WIKI_HELP_PAGE_MAN = '%s' % const.URL_MANUAL_PAGE @@ -234,13 +223,14 @@ class ViewManager(CLIManager): The View Manager does not have to know the number of views, the type of views, or any other details about the views. It simply provides the - method of containing each view, and switching between the views. + method of containing each view, and has methods for creating, deleting and + switching between the views. """ def __init__(self, dbstate, view_category_order): """ - The viewmanager is initialiste with a dbstate on which GRAMPS is + The viewmanager is initialised with a dbstate on which GRAMPS is working, and a fixed view_category_order, which is the order in which the view categories are accessible in the sidebar. """ @@ -248,12 +238,8 @@ class ViewManager(CLIManager): self.view_category_order = view_category_order #set pluginmanager to GUI one self._pmgr = GuiPluginManager.get_instance() - self.page_is_changing = False self.active_page = None - self.views = [] self.pages = [] - self.button_handlers = [] - self.buttons = [] self.merge_ids = [] self.toolactions = None self.tool_menu_ui_id = None @@ -294,12 +280,13 @@ class ViewManager(CLIManager): vbox = gtk.VBox() self.window.add(vbox) - hbox = gtk.HBox() + hpane = gtk.HPaned() self.ebox = gtk.EventBox() - self.bbox = gtk.VBox() - self.ebox.add(self.bbox) - hbox.pack_start(self.ebox, False) - hbox.show_all() + + self.sidebar = Sidebar(self) + self.ebox.add(self.sidebar.get_top()) + hpane.add1(self.ebox) + hpane.show_all() self.notebook = gtk.Notebook() self.notebook.set_scrollable(True) @@ -309,12 +296,12 @@ class ViewManager(CLIManager): self.__init_lists() self.__build_ui_manager() - hbox.pack_start(self.notebook, True) + hpane.add2(self.notebook) self.menubar = self.uimanager.get_widget('/MenuBar') self.toolbar = self.uimanager.get_widget('/ToolBar') vbox.pack_start(self.menubar, False) vbox.pack_start(self.toolbar, False) - vbox.add(hbox) + vbox.add(hpane) vbox.pack_end(self.__setup_statusbar(), False) vbox.show() @@ -358,6 +345,21 @@ class ViewManager(CLIManager): # But we need to realize it here to have gtk.gdk.window handy self.window.realize() + def __load_sidebar_plugins(self): + """ + Load the sidebar plugins. + """ + for pdata in self._pmgr.get_reg_sidebars(): + module = self._pmgr.load_plugin(pdata) + if not module: + print "Error loading sidebar '%s': skipping content" \ + % pdata.name + continue + + sidebar_class = getattr(module, pdata.sidebarclass) + sidebar_page = sidebar_class(self.dbstate, self.uistate) + self.sidebar.add(pdata.menu_label, sidebar_page) + def __setup_statusbar(self): """ Create the statusbar that sits at the bottom of the window @@ -387,10 +389,8 @@ class ViewManager(CLIManager): """ if self.show_sidebar: self.ebox.show() - self.notebook.set_show_tabs(False) else: self.ebox.hide() - self.notebook.set_show_tabs(True) def __build_open_button(self): """ @@ -406,10 +406,17 @@ class ViewManager(CLIManager): def __connect_signals(self): """ - connects the signals needed + Connects the signals needed """ self.window.connect('delete-event', self.quit) - self.notebook.connect('switch-page', self.change_category) + self.notebook.connect('switch-page', self.view_changed) + + def view_changed(self, notebook, page, page_num): + """ + Called when the notebook page is changed. + """ + self.sidebar.view_changed(page_num) + self.__change_page(page_num) def __init_lists(self): """ @@ -547,11 +554,9 @@ class ViewManager(CLIManager): new_page = 0 else: new_page = current_page + 1 - if self.show_sidebar: - #cause a click signal - self.buttons[new_page].set_active(True) - else: - self.notebook.set_current_page(new_page) + self.sidebar.handlers_block() + self.notebook.set_current_page(new_page) + self.sidebar.handlers_unblock() def __prev_view(self, action): """ @@ -564,19 +569,16 @@ class ViewManager(CLIManager): new_page = len(self.pages)-1 else: new_page = current_page - 1 - if self.show_sidebar: - #cause a click signal - self.buttons[new_page].set_active(True) - else: - self.notebook.set_current_page(new_page) + self.sidebar.handlers_block() + self.notebook.set_current_page(new_page) + self.sidebar.handlers_unblock() - def init_interface(self, vieworder): + def init_interface(self): """ Initialize the interface, creating the pages as given in vieworder """ - self.views = vieworder self.__init_lists() - self.__create_pages() + self.__load_sidebar_plugins() if not self.file_loaded: self.actiongroup.set_visible(False) @@ -787,12 +789,10 @@ class ViewManager(CLIManager): """ if obj.get_active(): self.ebox.show() - self.notebook.set_show_tabs(False) config.set('interface.view', True) self.show_sidebar = True else: self.ebox.hide() - self.notebook.set_show_tabs(True) config.set('interface.view', False) self.show_sidebar = False config.save() @@ -823,267 +823,71 @@ class ViewManager(CLIManager): config.set('interface.fullscreen', False) config.save() - def view_toggle(self, radioaction, current, category_page): - """ - Go to the views in category_page, with in category: view_page - The view has id id_page - This is the only method that can call change of views in a category - """ - self.__vb_handlers_block() - if self.notebook.get_current_page() != category_page: - raise Error, 'Error changing view, category is not active' - cat_notebook = self.notebook_cat[category_page] - view_page = radioaction.get_current_value() - if self.notebook_cat[category_page].get_current_page() != view_page: - self.notebook_cat[category_page].set_current_page(view_page) - self.__change_view(category_page, view_page) - self.__vb_handlers_unblock() + def create_page(self, pdata, page_def): + try: + page = page_def(self.dbstate, self.uistate) + except: + import traceback + LOG.warn("View '%s' failed to load." % pdata.id) + traceback.print_exc() + return + # Category is (string, trans): + page.set_category(pdata.category) + page.set_ident(page.get_category() + '_' + pdata.id) + page_title = page.get_title() + page_category = page.get_category() + page_translated_category = page.get_translated_category() + page_stock = page.get_stock() + + page.define_actions() + try: + page_display = page.get_display() + except: + import traceback + print "ERROR: '%s' failed to create view" % pdata.name + traceback.print_exc() + return + page_display.show_all() + page.post() + self.pages.append(page) + + # create icon/label for workspace notebook + hbox = gtk.HBox() + image = gtk.Image() + image.set_from_stock(page_stock, gtk.ICON_SIZE_MENU) + hbox.pack_start(image, False) + hbox.add(gtk.Label(pdata.name)) + hbox.show_all() - def __switch_page_on_dnd(self, widget, context, xpos, ypos, time, page_no): - """ - Switches the page based on drag and drop - """ - self.__vb_handlers_block() - if self.notebook.get_current_page() != page_no: - self.notebook.set_current_page(page_no) - self.__vb_handlers_unblock() + page_num = self.notebook.append_page(page_display, hbox) + return page_num + + def goto_page(self, page_num): + self.sidebar.handlers_block() + self.notebook.set_current_page(page_num) + self.sidebar.handlers_unblock() + + self.__change_page(page_num) + + def __change_page(self, page_num): + self.__disconnect_previous_page() + + self.active_page = self.pages[page_num] + self.active_page.set_active() + self.__connect_active_page(page_num) + + self.uimanager.ensure_update() + while gtk.events_pending(): + gtk.main_iteration() + + self.active_page.change_page() def __delete_pages(self): """ Calls on_delete() for each view """ - for pages in self.pages: - for page in pages: - page.on_delete() - - def __create_pages(self): - """ - Create the Views - """ - self.pages = [] - self.ui_category = {} - self.view_toggle_actions = {} - self.cat_view_group = None - - use_text = config.get('interface.sidebar-text') - #obtain which views should be the active ones - current_cat, current_cat_view, default_cat_views = \ - self.__views_to_show(config.get('preferences.use-last-view')) - - for indexcat, cat_views in enumerate(self.views): - #for every category, we create a button in the sidebar and a main - #workspace in which to show the view - nr_views = len(cat_views) - uimenuitems = '' - uitoolitems = '' - self.view_toggle_actions[indexcat] = [] - self.pages.append([]) - nrpage = 0 - for pdata, page_def in cat_views: - try: - page = page_def(self.dbstate, self.uistate) - except: - import traceback - LOG.warn("View '%s' failed to load." % pdata.id) - traceback.print_exc() - continue - # Category is (string, trans): - page.set_category(pdata.category) - page.set_ident(page.get_category() + '_' + pdata.id) - page_title = page.get_title() - page_category = page.get_category() - page_translated_category = page.get_translated_category() - page_stock = page.get_stock() - - if nrpage == 0: - #the first page of this category, used to obtain - #category workspace notebook - notebook = gtk.Notebook() - notebook.set_scrollable(False) - notebook.set_show_tabs(False) - notebook.show() - self.notebook_cat.append(notebook) - # create icon/label for workspace notebook - hbox = gtk.HBox() - image = gtk.Image() - image.set_from_stock(page_stock, gtk.ICON_SIZE_MENU) - hbox.pack_start(image, False) - hbox.add(gtk.Label(page_translated_category)) - hbox.show_all() - page_cat = self.notebook.append_page(notebook, hbox) - # Enable view switching during DnD - hbox.drag_dest_set(0, [], 0) - hbox.connect('drag_motion', self.__switch_page_on_dnd, - page_cat) - - # create the button and add it to the sidebar - button = self.__make_sidebar_button(use_text, indexcat, - page_translated_category, - page_stock) - - self.bbox.pack_start(button, False) - self.buttons.append(button) - - # Enable view switching during DnD - button.drag_dest_set(0, [], 0) - button.connect('drag_motion', self.__switch_page_on_dnd, - page_cat) - - # create view page and add to category notebook - page.define_actions() - try: - page_display = page.get_display() - except: - import traceback - print "ERROR: '%s' failed to create view" % pdata.name - traceback.print_exc() - continue - page_display.show_all() - page.post() - page_no = self.notebook_cat[-1].append_page(page_display, - gtk.Label(page_title)) - self.pages[-1].append(page) - pageid = (pdata.id + '_%i' % nrpage) - uimenuitems += '\n' % pageid - uitoolitems += '\n' % pageid - # id, stock, button text, UI, tooltip, page - if nrpage < 9: - modifier = "%d" % ((nrpage % 9) + 1) - else: - modifier = "" - self.view_toggle_actions[indexcat].append((pageid, - page.get_viewtype_stock(), - pdata.name, modifier, page_title, nrpage)) - - nrpage += 1 - if nr_views > 1: - #allow for switching views in a category - self.ui_category[indexcat] = UICATEGORY % (uimenuitems, - uitoolitems) - #set view cat to last used in this category - self.notebook_cat[-1].set_current_page(default_cat_views[indexcat]) - - if self.views: - self.active_page = self.pages[current_cat][current_cat_view] - self.buttons[current_cat].set_active(True) - self.active_page.set_active() - self.notebook.set_current_page(current_cat) - self.notebook_cat[current_cat].set_current_page(current_cat_view) - else: - #not one single view loaded - WarningDialog( - _("No views loaded"), - _("No view plugins are loaded. Go to Help->Plugin " - "Manager, and make sure some plugins of type 'View' are " - "enabled. Then restart Gramps")) - - - def __views_to_show(self, use_last = True): - """ - Determine based on preference setting which views should be shown - """ - current_cat = 0 - current_cat_view = 0 - default_cat_views = [0] * len(self.views) - if use_last: - current_page_id = config.get('preferences.last-view') - default_page_ids = config.get('preferences.last-views') - found = False - for indexcat, cat_views in enumerate(self.views): - cat_view = 0 - for pdata, page_def in cat_views: - if not found: - if pdata.id == current_page_id: - current_cat = indexcat - current_cat_view = cat_view - default_cat_views[indexcat] = cat_view - found = True - break - if pdata.id in default_page_ids: - default_cat_views[indexcat] = cat_view - cat_view += 1 - if not found: - current_cat = 0 - current_cat_view = 0 - return current_cat, current_cat_view, default_cat_views - - def __make_sidebar_button(self, use_text, index, page_title, page_stock): - """ - Create the sidebar button. The page_title is the text associated with - the button. - """ - - # create the button - button = gtk.ToggleButton() - button.set_relief(gtk.RELIEF_NONE) - button.set_alignment(0, 0.5) - - # add the tooltip - button.set_tooltip_text(page_title) - #self.tips.set_tip(button, page_title) - - # connect the signal, along with the index as user data - handler_id = button.connect('clicked', self.__vb_clicked, index) - self.button_handlers.append(handler_id) - button.show() - - # add the image. If we are using text, use the BUTTON (larger) size. - # otherwise, use the smaller size - hbox = gtk.HBox() - hbox.show() - image = gtk.Image() - if use_text: - image.set_from_stock(page_stock, gtk.ICON_SIZE_BUTTON) - else: - image.set_from_stock(page_stock, gtk.ICON_SIZE_DND) - image.show() - hbox.pack_start(image, False, False) - hbox.set_spacing(4) - - # add text if requested - if use_text: - label = gtk.Label(page_title) - label.show() - hbox.pack_start(label, False, True) - - button.add(hbox) - return button - - def __vb_clicked(self, button, index): - """ - Called when the button causes a page change - """ - if config.get('interface.view'): - self.__vb_handlers_block() - self.notebook.set_current_page(index) - - # If the click is on the same view we're in, - # restore the button state to active - if not button.get_active(): - button.set_active(True) - self.__vb_handlers_unblock() - - def __vb_handlers_block(self): - """ - Block signals to the buttons to prevent spurious events - """ - for idx in range(len(self.buttons)): - self.buttons[idx].handler_block(self.button_handlers[idx]) - - def __vb_handlers_unblock(self): - """ - Unblock signals to the buttons - """ - for idx in range(len(self.buttons)): - self.buttons[idx].handler_unblock(self.button_handlers[idx]) - - def __set_active_button(self, num): - """ - Set the corresponding button active, while setting the others - inactive - """ - for idx in range(len(self.buttons)): - self.buttons[idx].set_active(idx==num) + for page in self.pages: + page.on_delete() def __disconnect_previous_page(self): """ @@ -1098,11 +902,8 @@ class ViewManager(CLIManager): for grp in groups: if grp in self.uimanager.get_action_groups(): self.uimanager.remove_action_group(grp) - if self.cat_view_group: - if self.cat_view_group in self.uimanager.get_action_groups(): - self.uimanager.remove_action_group(self.cat_view_group) - def __connect_active_page(self, category_page, view_page): + def __connect_active_page(self, page_num): """ Inserts the action groups associated with the current page into the UIManager @@ -1117,90 +918,12 @@ class ViewManager(CLIManager): mergeid = self.uimanager.add_ui_from_string(uidef) self.merge_ids.append(mergeid) - if category_page in self.ui_category: - #add entries for the different views in the category - self.cat_view_group = gtk.ActionGroup('categoryviews') - self.cat_view_group.add_radio_actions( - self.view_toggle_actions[category_page], value=view_page, - on_change=self.view_toggle, user_data=category_page) - self.cat_view_group.set_sensitive(True) - self.uimanager.insert_action_group(self.cat_view_group, 1) - mergeid = self.uimanager.add_ui_from_string(self.ui_category[ - category_page]) - self.merge_ids.append(mergeid) - configaction = self.actiongroup.get_action('ConfigView') if self.active_page.can_configure(): configaction.set_sensitive(True) else: configaction.set_sensitive(False) - def change_category(self, obj, page, num=-1): - """ - Wrapper for the __do_change_category, to prevent entering into the - routine while already in it. - """ - if not self.page_is_changing: - self.page_is_changing = True - self.__do_change_category(num) - self.page_is_changing = False - - def __do_change_category(self, num): - """ - Change the category to the new category - """ - if num == -1: - num = self.notebook.get_current_page() - - # set button of current page active - self.__set_active_button(num) - # now do view specific change - self.__change_view(num) - - def __change_view(self, category_page, view_page=-1): - """ - Change a view in a category. - - :Param category_page: the category number the view is in - :Type category_page: integer >= 0 - - :Param view_page: the view page number to switch to. If -1 is passed - the currently already active view in the category is switched to. - Use this when a category changes. - :Type view_page: integer >=0 to switch to a specific page, or -1 to - switch to the active view in the category - """ - if self.notebook_cat: - if view_page == -1: - #just show active one - view_page = self.notebook_cat[category_page].get_current_page() - if self.dbstate.open: - self.__disconnect_previous_page() - if len(self.pages) > 0: - self.active_page = self.pages[category_page][view_page] - self.active_page.set_active() - newcurpageid = self.views[category_page][view_page][0].id - config.set('preferences.last-view', newcurpageid) - olddefaults = config.get('preferences.last-views') - if len(olddefaults) != len(self.pages): - #number views changed, we cannot trust the old - olddefaults = [''] * len(self.pages) - olddefaults[category_page] = newcurpageid - config.set('preferences.last-views', olddefaults) - config.save() - - self.__connect_active_page(category_page, view_page) - - self.uimanager.ensure_update() - - while gtk.events_pending(): - gtk.main_iteration() - - self.active_page.change_page() - else: - #no views loaded - pass - def import_data(self, obj): """ Imports a file @@ -1267,7 +990,6 @@ class ViewManager(CLIManager): self.uistate.window.set_title(msg) self.actiongroup.set_sensitive(True) - self.change_category(None, None) self.actiongroup.set_visible(True) self.readonlygroup.set_visible(True) @@ -1597,3 +1319,79 @@ def make_plugin_callback(pdata, dbstate, uistate): Makes a callback for a report/tool menu item """ return lambda x: run_plugin(pdata, dbstate, uistate) + +def get_available_views(): + """ + Query the views and determine what views to show and in which order + + :Returns: a list of lists containing tuples (view_id, viewclass) + """ + pmgr = GuiPluginManager.get_instance() + view_list = pmgr.get_reg_views() + viewstoshow = {} + for pdata in view_list: + mod = pmgr.load_plugin(pdata) + if not mod or not hasattr(mod, pdata.viewclass): + #import of plugin failed + ErrorDialog( + _('Failed Loading View'), + _('The view %(name)s did not load. See Help Menu, Plugin Manager' + ' for more info.\nUse http://bugs.gramps-project.org to' + ' submit bugs of official views, contact the view ' + 'author (%(firstauthoremail)s) otherwise. ') % { + 'name': pdata.name, + 'firstauthoremail': pdata.authors_email[0] if + pdata.authors_email else '...'}) + continue + viewclass = getattr(mod, pdata.viewclass) + # pdata.category is (string, trans-string): + if pdata.category[0] in viewstoshow: + if pdata.order == START: + viewstoshow[pdata.category[0]].insert(0, ((pdata, viewclass))) + else: + viewstoshow[pdata.category[0]].append((pdata, viewclass)) + else: + viewstoshow[pdata.category[0]] = [(pdata, viewclass)] + + resultorder = [] + # First, get those in order defined, if exists: + for item in config.get("interface.view-categories"): + if item in viewstoshow: + resultorder.append(viewstoshow[item]) + # Next, get the rest in some order: + viewstoshow_names = viewstoshow.keys() + viewstoshow_names.sort() + for item in viewstoshow_names: + if viewstoshow[item] in resultorder: + continue + resultorder.append(viewstoshow[item]) + return resultorder + +def views_to_show(views, use_last=True): + """ + Determine based on preference setting which views should be shown + """ + current_cat = 0 + current_cat_view = 0 + default_cat_views = [0] * len(views) + if use_last: + current_page_id = config.get('preferences.last-view') + default_page_ids = config.get('preferences.last-views') + found = False + for indexcat, cat_views in enumerate(views): + cat_view = 0 + for pdata, page_def in cat_views: + if not found: + if pdata.id == current_page_id: + current_cat = indexcat + current_cat_view = cat_view + default_cat_views[indexcat] = cat_view + found = True + break + if pdata.id in default_page_ids: + default_cat_views[indexcat] = cat_view + cat_view += 1 + if not found: + current_cat = 0 + current_cat_view = 0 + return current_cat, current_cat_view, default_cat_views diff --git a/src/plugins/sidebar/Makefile.am b/src/plugins/sidebar/Makefile.am new file mode 100644 index 000000000..049ffca29 --- /dev/null +++ b/src/plugins/sidebar/Makefile.am @@ -0,0 +1,22 @@ +# This is the src/plugins/sidebar level Makefile for Gramps +# We could use GNU make's ':=' syntax for nice wildcard use, +# but that is not necessarily portable. +# If not using GNU make, then list all .py files individually + +pkgdatadir = $(datadir)/@PACKAGE@/plugins/sidebar + +pkgdata_PYTHON = \ + categorysidebar.py\ + sidebar.gpr.py + +pkgpyexecdir = @pkgpyexecdir@/plugins/sidebar +pkgpythondir = @pkgpythondir@/plugins/sidebar + +# Clean up all the byte-compiled files +MOSTLYCLEANFILES = *pyc *pyo + +GRAMPS_PY_MODPATH = "../../" + +pycheck: + (export PYTHONPATH=$(GRAMPS_PY_MODPATH); \ + pychecker $(pkgdata_PYTHON)); diff --git a/src/plugins/sidebar/categorysidebar.py b/src/plugins/sidebar/categorysidebar.py new file mode 100644 index 000000000..949e722fa --- /dev/null +++ b/src/plugins/sidebar/categorysidebar.py @@ -0,0 +1,301 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2005-2007 Donald N. Allingham +# Copyright (C) 2008 Brian G. Matherly +# Copyright (C) 2009 Benny Malengier +# Copyright (C) 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$ + +#------------------------------------------------------------------------- +# +# GNOME modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +import config +from gui.viewmanager import get_available_views, views_to_show + +#------------------------------------------------------------------------- +# +# Constants +# +#------------------------------------------------------------------------- +UICATEGORY = ''' + + + %s + + + + + %s + + + +''' + +CATEGORY_ICON = { + 'Gramplets': 'gramps-gramplet', + 'People': 'gramps-person', + 'Relationships': 'gramps-relation', + 'Families': 'gramps-family', + 'Events': 'gramps-event', + 'Ancestry': 'gramps-pedigree', + 'Places': 'gramps-place', + 'Geography': 'gramps-geo', + 'Sources': 'gramps-source', + 'Repositories': 'gramps-repository', + 'Media': 'gramps-media', + 'Notes': 'gramps-notes'} + +#------------------------------------------------------------------------- +# +# CategorySidebar class +# +#------------------------------------------------------------------------- +class CategorySidebar(object): + """ + A sidebar displaying a column of toggle buttons that allows the user to + change the current view. + """ + def __init__(self, dbstate, uistate): + + self.viewmanager = uistate.viewmanager + + self.buttons = [] + self.button_handlers = [] + + self.vbox = gtk.VBox() + + self.pages = {} + self.page_defs = {} + + self.ui_category = {} + self.view_toggle_actions = {} + self.cat_view_group = None + self.merge_ids = [] + + self.views = get_available_views() + defaults = views_to_show(self.views, + config.get('preferences.use-last-view')) + self.current_views = defaults[2] + + use_text = config.get('interface.sidebar-text') + for cat_num, cat_views in enumerate(self.views): + uimenuitems = '' + uitoolitems = '' + self.view_toggle_actions[cat_num] = [] + for view_num, page in enumerate(cat_views): + + if view_num == 0: + category = page[0].category[1] + cat_icon = CATEGORY_ICON.get(page[0].category[0]) + if cat_icon is None: + cat_icon = 'gramps-view' + + # create the button and add it to the sidebar + button = self.__make_sidebar_button(use_text, cat_num, + category, cat_icon) + self.vbox.pack_start(button, False) + + # Enable view switching during DnD + button.drag_dest_set(0, [], 0) + button.connect('drag_motion', self.cb_switch_page_on_dnd, + cat_num) + self.vbox.show_all() + + self.page_defs[(cat_num, view_num)] = page + + pageid = (page[0].id + '_%i' % view_num) + uimenuitems += '\n' % pageid + uitoolitems += '\n' % pageid + # id, stock, button text, UI, tooltip, page + if view_num < 9: + modifier = "%d" % ((view_num % 9) + 1) + else: + modifier = "" + + stock_icon = page[0].stock_icon + if stock_icon is None: + stock_icon = cat_icon + self.view_toggle_actions[cat_num].append((pageid, + stock_icon, + page[0].name, modifier, page[0].name, view_num)) + + if len(cat_views) > 1: + #allow for switching views in a category + self.ui_category[cat_num] = UICATEGORY % (uimenuitems, + uitoolitems) + # Open the default view + self.__category_clicked(self.buttons[defaults[0]], defaults[0]) + + def get_top(self): + """ + Return the top container widget for the GUI. + """ + return self.vbox + + def view_changed(self, page_num): + """ + Called when the active view is changed. + """ + cat_num = view_num = None + for key in self.pages: + if self.pages[key] == page_num: + cat_num, view_num = key + break + + # Save last view in configuration + view_id = self.views[cat_num][view_num][0].id + config.set('preferences.last-view', view_id) + last_views = config.get('preferences.last-views') + last_views[cat_num] = view_id + config.set('preferences.last-views', last_views) + config.save() + + # Add buttons to the toolbar for the different view in the category + uimanager = self.viewmanager.uimanager + if self.cat_view_group: + if self.cat_view_group in uimanager.get_action_groups(): + uimanager.remove_action_group(self.cat_view_group) + + map(uimanager.remove_ui, self.merge_ids) + + if cat_num in self.ui_category: + self.cat_view_group = gtk.ActionGroup('categoryviews') + self.cat_view_group.add_radio_actions( + self.view_toggle_actions[cat_num], value=view_num, + on_change=self.cb_view_clicked, user_data=cat_num) + self.cat_view_group.set_sensitive(True) + uimanager.insert_action_group(self.cat_view_group, 1) + mergeid = uimanager.add_ui_from_string(self.ui_category[cat_num]) + self.merge_ids.append(mergeid) + + # Set new button as selected + self.handlers_block() + for index, button in enumerate(self.buttons): + if index == cat_num: + button.set_active(True) + else: + button.set_active(False) + self.handlers_unblock() + + def handlers_block(self): + """ + Block signals to the buttons to prevent spurious events. + """ + for idx in range(len(self.buttons)): + self.buttons[idx].handler_block(self.button_handlers[idx]) + + def handlers_unblock(self): + """ + Unblock signals to the buttons. + """ + for idx in range(len(self.buttons)): + self.buttons[idx].handler_unblock(self.button_handlers[idx]) + + def cb_view_clicked(self, radioaction, current, cat_num): + """ + Called when a button causes a view change. + """ + view_num = radioaction.get_current_value() + self.__goto_page(cat_num, view_num) + + def __category_clicked(self, button, cat_num): + """ + Called when a button causes a category change. + """ + view_num = self.current_views[cat_num] + self.__goto_page(cat_num, view_num) + + # If the click is on the same view we're in, + # restore the button state to active + if not button.get_active(): + button.set_active(True) + + def __goto_page(self, cat_num, view_num): + """ + Create the page if it doesn't exist and make it the current page. + """ + self.current_views[cat_num] = view_num + + page_num = self.pages.get((cat_num, view_num)) + if page_num is None: + page = self.page_defs[(cat_num, view_num)] + page_num = self.viewmanager.create_page(page[0], page[1]) + self.pages[(cat_num, view_num)] = page_num + + self.current_views[cat_num] = view_num + self.viewmanager.goto_page(page_num) + + def __make_sidebar_button(self, use_text, index, page_title, page_stock): + """ + Create the sidebar button. The page_title is the text associated with + the button. + """ + # create the button + button = gtk.ToggleButton() + button.set_relief(gtk.RELIEF_NONE) + button.set_alignment(0, 0.5) + self.buttons.append(button) + + # add the tooltip + button.set_tooltip_text(page_title) + + # connect the signal, along with the index as user data + handler_id = button.connect('clicked', self.__category_clicked, index) + self.button_handlers.append(handler_id) + button.show() + + # add the image. If we are using text, use the BUTTON (larger) size. + # otherwise, use the smaller size + hbox = gtk.HBox() + hbox.show() + image = gtk.Image() + if use_text: + image.set_from_stock(page_stock, gtk.ICON_SIZE_BUTTON) + else: + image.set_from_stock(page_stock, gtk.ICON_SIZE_DND) + image.show() + hbox.pack_start(image, False, False) + hbox.set_spacing(4) + + # add text if requested + if use_text: + label = gtk.Label(page_title) + label.show() + hbox.pack_start(label, False, True) + + button.add(hbox) + return button + + def cb_switch_page_on_dnd(self, widget, context, xpos, ypos, time, page_no): + """ + Switches the page based on drag and drop. + """ + self.handlers_block() + if self.viewmanager.notebook.get_current_page() != page_no: + self.viewmanager.notebook.set_current_page(page_no) + self.handlers_unblock() diff --git a/src/plugins/sidebar/sidebar.gpr.py b/src/plugins/sidebar/sidebar.gpr.py new file mode 100644 index 000000000..60c2eb480 --- /dev/null +++ b/src/plugins/sidebar/sidebar.gpr.py @@ -0,0 +1,40 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 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$ + +#------------------------------------------------------------------------ +# +# Register default sidebars +# +#------------------------------------------------------------------------ + +register(SIDEBAR, +id = 'categorysidebar', +name = _("Category Sidebar"), +description = _("A sidebar to allow the selection of view categories"), +version = '1.0', +gramps_target_version = '3.3', +status = STABLE, +fname = 'categorysidebar.py', +authors = [u"Nick Hall"], +authors_email = ["nick__hall@hotmail.com"], +sidebarclass = 'CategorySidebar', +menu_label = 'Category' +) diff --git a/src/plugins/view/fanchartview.gpr.py b/src/plugins/view/fanchartview.gpr.py index 2ad2ab887..fe5e7a936 100644 --- a/src/plugins/view/fanchartview.gpr.py +++ b/src/plugins/view/fanchartview.gpr.py @@ -10,4 +10,5 @@ register(VIEW, authors = [u"Douglas S. Blank"], authors_email = ["doug.blank@gmail.com"], viewclass = 'FanChartView', + stock_icon = 'gramps-fanchart', ) diff --git a/src/plugins/view/placetreeview.gpr.py b/src/plugins/view/placetreeview.gpr.py index 6bab25367..f845a3aa4 100644 --- a/src/plugins/view/placetreeview.gpr.py +++ b/src/plugins/view/placetreeview.gpr.py @@ -10,4 +10,5 @@ register(VIEW, authors_email = [""], category = ("Places", _("Places")), viewclass = 'PlaceTreeView', + stock_icon = 'gramps-tree-group', ) diff --git a/src/plugins/view/view.gpr.py b/src/plugins/view/view.gpr.py index eaba02441..9c1945d33 100644 --- a/src/plugins/view/view.gpr.py +++ b/src/plugins/view/view.gpr.py @@ -130,6 +130,7 @@ authors_email = ["http://gramps-project.org"], category = ("Ancestry", _("Ancestry")), viewclass = 'PedigreeView', order = START, +stock_icon = 'gramps-pedigree', ) register(VIEW, @@ -145,6 +146,7 @@ authors_email = ["http://gramps-project.org"], category = ("People", _("People")), viewclass = 'PersonTreeView', order = START, +stock_icon = 'gramps-tree-group', ) register(VIEW, @@ -161,7 +163,9 @@ authors_email = ["http://gramps-project.org"], category = ("People", _("People")), viewclass = 'PersonListView', order = START, +stock_icon = 'gramps-tree-list', ) + register(VIEW, id = 'placelistview', name = _("Place View"), @@ -175,6 +179,7 @@ authors_email = ["http://gramps-project.org"], category = ("Places", _("Places")), viewclass = 'PlaceListView', order = START, +stock_icon = 'gramps-tree-list', ) register(VIEW,