From da2fdec95ae88a0664956211fe1aaa7194a84cba Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Mon, 14 Jul 2025 23:02:30 +0200 Subject: [PATCH 1/6] Add a favorites sidebar and all persistant sidebar --- gramps/gui/navigator.py | 13 + gramps/plugins/sidebar/favoritessidebar.py | 340 +++++++++++++++++++++ gramps/plugins/sidebar/sidebar.gpr.py | 16 + 3 files changed, 369 insertions(+) create mode 100644 gramps/plugins/sidebar/favoritessidebar.py diff --git a/gramps/gui/navigator.py b/gramps/gui/navigator.py index ff652bf2b7f..6fcfdb357e0 100644 --- a/gramps/gui/navigator.py +++ b/gramps/gui/navigator.py @@ -36,6 +36,7 @@ # # ------------------------------------------------------------------------- from gramps.gen.plug import START, END +from gramps.gen.config import config from .pluginmanager import GuiPluginManager from .uimanager import ActionGroup @@ -91,6 +92,11 @@ def __init__(self, viewmanager): self.cat_view_group = None self.merge_ids = [] + self.config_name = "interface.favorite-menu" + if not config.is_set(self.config_name): + config.register(self.config_name, "") + self.conf_ft = True + self.top = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.top.show() @@ -197,6 +203,9 @@ def load_plugins(self, dbstate, uistate): sidebar_class = getattr(module, pdata.sidebarclass) sidebar_page = sidebar_class(dbstate, uistate, categories, views) self.add(pdata.menu_label, sidebar_page, pdata.order) + self.fav_menu = config.get(self.config_name) + if self.fav_menu != "": + self.stack.set_visible_child_name(self.fav_menu) def get_top(self): """ @@ -279,3 +288,7 @@ def cb_switch_page(self, stack, pspec): if self.active_view is not None: self.pages[title].view_changed(self.active_cat, self.active_view) self._active_page = title + if self.conf_ft: + self.conf_ft = False + else: + config.set(self.config_name, self.stack.get_visible_child_name()) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py new file mode 100644 index 00000000000..abb123e05ac --- /dev/null +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -0,0 +1,340 @@ +# +# 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 +# Copyright (C) 2011 Tim G L Lyons +# Copyright (C) 2024 Kari Kujansuu +# Copyright (C) 2025- Serge Noiraud +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# ------------------------------------------------------------------------- +# +# Python modules +# +# ------------------------------------------------------------------------- +import textwrap + +# ------------------------------------------------------------------------- +# +# GNOME modules +# +# ------------------------------------------------------------------------- +from gi.repository import Gtk + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from gramps.gen.const import GRAMPS_LOCALE +from gramps.gen.config import config +from gramps.gui.basesidebar import BaseSidebar +from gramps.gui.managedwindow import ManagedWindow + +_ = GRAMPS_LOCALE.translation.sgettext + +CLEAR = 1 +SETALL = 2 + + +# ------------------------------------------------------------------------- +# +# Favorites class +# +# ------------------------------------------------------------------------- +class FavoriteViews(ManagedWindow): + + def __init__(self, dialog, uistate, dbstate, views, categories, favorites): + self.track = [] + self.favorites = favorites + ManagedWindow.__init__(self, uistate, self.track, self._configure_favorite_view) + self.dialog = dialog + self.favorites = favorites + self.categories = categories + self.views = views + self.check_views = [] + self.list_views = [] + self.config_name = "interface.favorite-views" + self.dbstate = dbstate + self.uistate = uistate + self.set_window( + Gtk.Dialog(title=_("Choose your favorite views")), + None, + _("Choose your favorite views"), + None, + ) + self.setup_configs("interface.favorites", 400, 300) + self.window.add_button(_("Select All"), SETALL) + self.window.add_button(_("Clear All"), CLEAR) + self.window.add_button(_("_Close"), Gtk.ResponseType.CLOSE) + self.window.connect("response", self.on_response) + scroll = Gtk.ScrolledWindow() + scroll.set_min_content_width(-1) + box = self.window.get_content_area() + box.pack_start(scroll, True, True, 0) + vfav = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + scroll.add(vfav) + for cat_num, cat_name, cat_icon in categories: + for view_num, view_name, view_icon in views[cat_num]: + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=20) + vfav.pack_start(hbox, True, True, 0) + image = Gtk.Image() + image.set_from_icon_name(view_icon, Gtk.IconSize.DND) + image.show() + hbox.pack_start(image, False, False, 0) + button = Gtk.CheckButton() + self.check_views.append(button) + hbox.pack_start(button, False, False, 0) + if view_name in favorites: + button.set_active(True) + else: + button.set_active(False) + button.connect("clicked", self._configure_favorite_view, view_name) + title = Gtk.Label(label=view_name) + title.set_justify(Gtk.Justification.LEFT) + self.list_views.append(view_name) + hbox.pack_start(title, False, True, 0) + self.show() + + def _configure_favorite_view(self, button, viewname): + """define your favorite views""" + if button.get_active(): + if viewname not in self.favorites: + self.favorites.append(viewname) + elif viewname in self.favorites: + self.favorites.remove(viewname) + config.set(self.config_name, self.favorites) + FavoritesSidebar.show_views( + self.dialog, self.categories, self.views, self.favorites + ) + + def on_response(self, dialog, response): + if response == Gtk.ResponseType.CLOSE: + dialog.destroy() + elif response == SETALL: + # set all views active + self.set_views(setall=True) + elif response == CLEAR: + # set all views inactive + self.set_views(setall=False) + + def set_views(self, setall=False): + views = [] + for check in self.check_views: + if setall: + check.set_active(True) + views = self.list_views + else: + check.set_active(False) + config.set(self.config_name, views) + + +# ------------------------------------------------------------------------- +# +# Favorites Sidebar class +# +# ------------------------------------------------------------------------- +class FavoritesSidebar(BaseSidebar): + """ + A sidebar displaying a simple list of toggle buttons that allow the user to change the current view. + """ + + def __init__(self, dbstate, uistate, categories, views): + self.viewmanager = uistate.viewmanager + self.views = views + + self.buttons = [] + self.views = views + self.categories = categories + self.favorites = [] + self.config_name = "interface.favorite-views" + if not config.is_set(self.config_name): + config.register(self.config_name, []) + self.favorites = config.get(self.config_name) + self.button_handlers = [] + self.lookup = {} + self.uistate = uistate + self.dbstate = dbstate + + self.window = Gtk.ScrolledWindow() + self.window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + self.show_views(self.categories, self.views, self.favorites) + self.window.show() + + def show_views(self, categories, views, favorites): + children = self.window.get_children() + if children: + children[0].destroy() + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.window.add(vbox) + use_text = config.get("interface.sidebar-text") + button = self.__make_sidebar_button( + use_text, 99, 0, _("Choose your favorite views"), "gramps-preferences" + ) + button.set_margin_top(10) + vbox.pack_start(button, False, False, 0) + for cat_num, cat_name, cat_icon in categories: + catbox = Gtk.Box() + image = Gtk.Image() + image.set_from_icon_name(cat_icon, Gtk.IconSize.BUTTON) + catbox.pack_start(image, False, False, 4) + if use_text: + label = Gtk.Label(label=cat_name) + catbox.pack_start(label, False, True, 4) + catbox.set_tooltip_text(cat_name) + + viewbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + for view_num, view_name, view_icon in views[cat_num]: + if view_name in self.favorites: + # create the button and add it to the sidebar + button = self.__make_sidebar_button( + use_text, cat_num, view_num, view_name, view_icon + ) + if view_num == 0: + button.set_margin_top(10) + + viewbox.pack_start(button, False, False, 0) + vbox.pack_start(viewbox, False, False, 0) + + vbox.show_all() + + def get_top(self): + """ + Return the top container widget for the GUI. + """ + return self.window + + def view_changed(self, cat_num, view_num): + """ + Called when the active view is changed. + """ + # Expand category + # Set new button as selected + button_num = self.lookup.get((cat_num, view_num)) + self.__handlers_block() + for index, button in enumerate(self.buttons): + if index == button_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, button in enumerate(self.buttons): + button.handler_block(self.button_handlers[idx]) + + def __handlers_unblock(self): + """ + Unblock signals to the buttons. + """ + for idx, button in enumerate(self.buttons): + button.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.viewmanager.goto_page(cat_num, view_num) + + def __category_clicked(self, button, cat_num): + """ + Called when a category button is clicked. + """ + # Make the button active. If it was already active the category will + # not change. + button.set_active(True) + self.viewmanager.goto_page(cat_num, None) + + def __view_clicked(self, button, cat_num, view_num): + """ + Called when a view button is clicked. + """ + if cat_num == 99: + # We configure the favorite bar + self.favorites = config.get(self.config_name) + FavoriteViews( + self, + self.uistate, + self.dbstate, + self.views, + self.categories, + self.favorites, + ) + else: + self.viewmanager.goto_page(cat_num, view_num) + + def cb_menu_clicked(self, menuitem, cat_num, view_num): + """ + Called when a view is selected from a drop-down menu. + """ + self.viewmanager.goto_page(cat_num, view_num) + + def __make_sidebar_button(self, use_text, cat_num, view_num, view_name, view_icon): + """ + Create the sidebar button. The page_title is the text associated with + the button. + """ + top = Gtk.Box() + + # create the button + button = Gtk.ToggleButton() + button.set_relief(Gtk.ReliefStyle.NONE) + self.buttons.append(button) + self.lookup[(cat_num, view_num)] = len(self.buttons) - 1 + + # add the tooltip + button.set_tooltip_text(view_name) + + # connect the signal, along with the index as user data + handler_id = button.connect("clicked", self.__view_clicked, cat_num, view_num) + 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.Box() + hbox.show() + image = Gtk.Image() + if use_text: + image.set_from_icon_name(view_icon, Gtk.IconSize.BUTTON) + else: + image.set_from_icon_name(view_icon, Gtk.IconSize.DND) + image.show() + hbox.pack_start(image, False, False, 0) + hbox.set_spacing(4) + + # add text if requested + if use_text: + width = 20 + lines = textwrap.wrap(view_name, width, break_long_words=False) + labeltext = "\n".join(lines) + label = Gtk.Label(label=labeltext) + label.show() + hbox.pack_start(label, False, True, 0) + + button.add(hbox) + + top.pack_start(button, False, True, 0) + + return top diff --git a/gramps/plugins/sidebar/sidebar.gpr.py b/gramps/plugins/sidebar/sidebar.gpr.py index 802700822c5..74e38295b74 100644 --- a/gramps/plugins/sidebar/sidebar.gpr.py +++ b/gramps/plugins/sidebar/sidebar.gpr.py @@ -76,3 +76,19 @@ menu_label=_("Expander"), order=END, ) + +register( + SIDEBAR, + id="favoritessidebar", + name=_("Favorites Sidebar"), + description=_("Selection of views from a views selector"), + version="1.0", + gramps_target_version=MODULE_VERSION, + status=STABLE, + fname="favoritessidebar.py", + authors=["Serge Noiraud"], + authors_email=["serge.noiraud@free.fr"], + sidebarclass="FavoritesSidebar", + menu_label=_("Favorites"), + order=END, +) From fea7405fddfe139f52366ec2e629045d0523e18b Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Wed, 16 Jul 2025 10:11:31 +0200 Subject: [PATCH 2/6] Move the config button at the bottom. change icon. --- gramps/plugins/sidebar/favoritessidebar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py index abb123e05ac..af059eb5ee9 100644 --- a/gramps/plugins/sidebar/favoritessidebar.py +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -185,11 +185,6 @@ def show_views(self, categories, views, favorites): vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.window.add(vbox) use_text = config.get("interface.sidebar-text") - button = self.__make_sidebar_button( - use_text, 99, 0, _("Choose your favorite views"), "gramps-preferences" - ) - button.set_margin_top(10) - vbox.pack_start(button, False, False, 0) for cat_num, cat_name, cat_icon in categories: catbox = Gtk.Box() image = Gtk.Image() @@ -213,6 +208,11 @@ def show_views(self, categories, views, favorites): viewbox.pack_start(button, False, False, 0) vbox.pack_start(viewbox, False, False, 0) + button = self.__make_sidebar_button( + use_text, 99, 0, _("Choose your favorite views"), "gramps-config" + ) + button.set_margin_top(10) + vbox.pack_start(button, False, False, 0) vbox.show_all() def get_top(self): From 43d6bcab97d4a67af50d7a3ca9f003d9d096d0a0 Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Fri, 18 Jul 2025 12:20:50 +0200 Subject: [PATCH 3/6] Add the help button to the view selection dialog --- gramps/plugins/sidebar/favoritessidebar.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py index af059eb5ee9..7d949c4dbc4 100644 --- a/gramps/plugins/sidebar/favoritessidebar.py +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -43,13 +43,16 @@ # Gramps modules # # ------------------------------------------------------------------------- -from gramps.gen.const import GRAMPS_LOCALE +from gramps.gen.const import GRAMPS_LOCALE, URL_MANUAL_PAGE from gramps.gen.config import config from gramps.gui.basesidebar import BaseSidebar from gramps.gui.managedwindow import ManagedWindow +from gramps.gui.display import display_help _ = GRAMPS_LOCALE.translation.sgettext +WIKI_HELP_PAGE = URL_MANUAL_PAGE + "_-_Main_Window" +WIKI_HELP_SEC = _("Switching_Navigator_modes") CLEAR = 1 SETALL = 2 @@ -81,6 +84,10 @@ def __init__(self, dialog, uistate, dbstate, views, categories, favorites): None, ) self.setup_configs("interface.favorites", 400, 300) + help_btn = self.window.add_button(_("Help"), Gtk.ResponseType.HELP) + help_btn.connect( + "clicked", lambda x: display_help(WIKI_HELP_PAGE, WIKI_HELP_SEC) + ) self.window.add_button(_("Select All"), SETALL) self.window.add_button(_("Clear All"), CLEAR) self.window.add_button(_("_Close"), Gtk.ResponseType.CLOSE) @@ -128,6 +135,8 @@ def _configure_favorite_view(self, button, viewname): def on_response(self, dialog, response): if response == Gtk.ResponseType.CLOSE: dialog.destroy() + if response == Gtk.ResponseType.HELP: + display_help() elif response == SETALL: # set all views active self.set_views(setall=True) From 0266664b8e00d739d3c5a59c9d9d957818e3f36b Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Fri, 18 Jul 2025 17:15:55 +0200 Subject: [PATCH 4/6] Avoid having multiple view selection widgets --- gramps/plugins/sidebar/favoritessidebar.py | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py index 7d949c4dbc4..24f1aa856da 100644 --- a/gramps/plugins/sidebar/favoritessidebar.py +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -134,6 +134,7 @@ def _configure_favorite_view(self, button, viewname): def on_response(self, dialog, response): if response == Gtk.ResponseType.CLOSE: + self.dialog._dialog_count(0) dialog.destroy() if response == Gtk.ResponseType.HELP: display_help() @@ -181,6 +182,7 @@ def __init__(self, dbstate, uistate, categories, views): self.lookup = {} self.uistate = uistate self.dbstate = dbstate + self.dialog_count = 0 self.window = Gtk.ScrolledWindow() self.window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) @@ -281,15 +283,17 @@ def __view_clicked(self, button, cat_num, view_num): """ if cat_num == 99: # We configure the favorite bar - self.favorites = config.get(self.config_name) - FavoriteViews( - self, - self.uistate, - self.dbstate, - self.views, - self.categories, - self.favorites, - ) + self.dialog_count += 1 + if self.dialog_count < 2: + self.favorites = config.get(self.config_name) + FavoriteViews( + self, + self.uistate, + self.dbstate, + self.views, + self.categories, + self.favorites, + ) else: self.viewmanager.goto_page(cat_num, view_num) @@ -347,3 +351,8 @@ def __make_sidebar_button(self, use_text, cat_num, view_num, view_name, view_ico top.pack_start(button, False, True, 0) return top + + def _dialog_count(self, val): + if val == 0: + self.dialog_count = 0 + return self.dialog_count From a4cec5e2fceb260f869c6686f23f7ca6efd22796 Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Fri, 18 Jul 2025 18:00:32 +0200 Subject: [PATCH 5/6] Better solution to avoid multiple dialog. --- gramps/plugins/sidebar/favoritessidebar.py | 35 +++++++++------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py index 24f1aa856da..c3f1ea5f333 100644 --- a/gramps/plugins/sidebar/favoritessidebar.py +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -134,7 +134,7 @@ def _configure_favorite_view(self, button, viewname): def on_response(self, dialog, response): if response == Gtk.ResponseType.CLOSE: - self.dialog._dialog_count(0) + self.dialog.select.set_sensitive(True) dialog.destroy() if response == Gtk.ResponseType.HELP: display_help() @@ -182,7 +182,6 @@ def __init__(self, dbstate, uistate, categories, views): self.lookup = {} self.uistate = uistate self.dbstate = dbstate - self.dialog_count = 0 self.window = Gtk.ScrolledWindow() self.window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) @@ -219,11 +218,11 @@ def show_views(self, categories, views, favorites): viewbox.pack_start(button, False, False, 0) vbox.pack_start(viewbox, False, False, 0) - button = self.__make_sidebar_button( + self.select = self.__make_sidebar_button( use_text, 99, 0, _("Choose your favorite views"), "gramps-config" ) - button.set_margin_top(10) - vbox.pack_start(button, False, False, 0) + self.select.set_margin_top(10) + vbox.pack_start(self.select, False, False, 0) vbox.show_all() def get_top(self): @@ -283,17 +282,16 @@ def __view_clicked(self, button, cat_num, view_num): """ if cat_num == 99: # We configure the favorite bar - self.dialog_count += 1 - if self.dialog_count < 2: - self.favorites = config.get(self.config_name) - FavoriteViews( - self, - self.uistate, - self.dbstate, - self.views, - self.categories, - self.favorites, - ) + self.select.set_sensitive(False) + self.favorites = config.get(self.config_name) + FavoriteViews( + self, + self.uistate, + self.dbstate, + self.views, + self.categories, + self.favorites, + ) else: self.viewmanager.goto_page(cat_num, view_num) @@ -351,8 +349,3 @@ def __make_sidebar_button(self, use_text, cat_num, view_num, view_name, view_ico top.pack_start(button, False, True, 0) return top - - def _dialog_count(self, val): - if val == 0: - self.dialog_count = 0 - return self.dialog_count From d93d220e12ae755ba546e2eb5992e20c9688b8b2 Mon Sep 17 00:00:00 2001 From: SNoiraud Date: Sat, 19 Jul 2025 11:39:15 +0200 Subject: [PATCH 6/6] We have two WIKI pages. We only need one. --- gramps/plugins/sidebar/favoritessidebar.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/gramps/plugins/sidebar/favoritessidebar.py b/gramps/plugins/sidebar/favoritessidebar.py index c3f1ea5f333..3374e94992f 100644 --- a/gramps/plugins/sidebar/favoritessidebar.py +++ b/gramps/plugins/sidebar/favoritessidebar.py @@ -51,8 +51,6 @@ _ = GRAMPS_LOCALE.translation.sgettext -WIKI_HELP_PAGE = URL_MANUAL_PAGE + "_-_Main_Window" -WIKI_HELP_SEC = _("Switching_Navigator_modes") CLEAR = 1 SETALL = 2 @@ -84,10 +82,7 @@ def __init__(self, dialog, uistate, dbstate, views, categories, favorites): None, ) self.setup_configs("interface.favorites", 400, 300) - help_btn = self.window.add_button(_("Help"), Gtk.ResponseType.HELP) - help_btn.connect( - "clicked", lambda x: display_help(WIKI_HELP_PAGE, WIKI_HELP_SEC) - ) + self.window.add_button(_("Help"), Gtk.ResponseType.HELP) self.window.add_button(_("Select All"), SETALL) self.window.add_button(_("Clear All"), CLEAR) self.window.add_button(_("_Close"), Gtk.ResponseType.CLOSE) @@ -137,7 +132,9 @@ def on_response(self, dialog, response): self.dialog.select.set_sensitive(True) dialog.destroy() if response == Gtk.ResponseType.HELP: - display_help() + display_help( + URL_MANUAL_PAGE + "_-_Main_Window#" + _("Switching_Navigator_modes") + ) elif response == SETALL: # set all views active self.set_views(setall=True)