From 2daa53025eb7cd0c6083b65a0149d5058ca7c412 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Fri, 10 Dec 2021 23:43:40 +0000 Subject: [PATCH] Create new Addon Manager dialog --- data/gramps.css | 16 + gramps/gen/config.py | 7 +- gramps/gen/plug/__init__.py | 2 +- gramps/gen/plug/_manager.py | 15 +- gramps/gen/plug/_pluginreg.py | 14 +- gramps/gen/plug/utils.py | 115 +-- gramps/gen/utils/requirements.py | 171 +++++ gramps/gui/configure.py | 121 +-- gramps/gui/displaystate.py | 8 +- gramps/gui/grampsgui.py | 6 + gramps/gui/plug/_windows.py | 723 +++++++++++++++++- gramps/gui/viewmanager.py | 21 +- ...ps-plugin-manager.png => gramps-addon.png} | Bin ...ps-plugin-manager.png => gramps-addon.png} | Bin ...ps-plugin-manager.png => gramps-addon.png} | Bin ...ps-plugin-manager.png => gramps-addon.png} | Bin ...ps-plugin-manager.svg => gramps-addon.svg} | 0 po/POTFILES.in | 1 + setup.py | 2 + 19 files changed, 1036 insertions(+), 186 deletions(-) create mode 100644 data/gramps.css create mode 100644 gramps/gen/utils/requirements.py rename images/hicolor/16x16/actions/{gramps-plugin-manager.png => gramps-addon.png} (100%) rename images/hicolor/22x22/actions/{gramps-plugin-manager.png => gramps-addon.png} (100%) rename images/hicolor/24x24/actions/{gramps-plugin-manager.png => gramps-addon.png} (100%) rename images/hicolor/48x48/actions/{gramps-plugin-manager.png => gramps-addon.png} (100%) rename images/hicolor/scalable/actions/{gramps-plugin-manager.svg => gramps-addon.svg} (100%) diff --git a/data/gramps.css b/data/gramps.css new file mode 100644 index 000000000..14b441c23 --- /dev/null +++ b/data/gramps.css @@ -0,0 +1,16 @@ +.lozenge { + font-size: small; + color: #ffffff; + background-color: #0d6efd; + padding: 3px; + border-radius: 6px; + box-shadow: 0px 0px 6px black; +} + +.addon-row { + border-width: 1px; + border-style: solid; + border-color: alpha(currentColor, .2); + border-radius: 10px; + margin: 1px; +} diff --git a/gramps/gen/config.py b/gramps/gen/config.py index b02c4e662..5ee96d440 100644 --- a/gramps/gen/config.py +++ b/gramps/gen/config.py @@ -157,7 +157,10 @@ register('behavior.translator-needed', True) register('behavior.use-tips', False) register('behavior.welcome', 100) register('behavior.web-search-url', 'http://google.com/#&q=%(text)s') -register('behavior.addons-url', "https://raw.githubusercontent.com/gramps-project/addons/master/gramps52") +register('behavior.addons-url', 'https://raw.githubusercontent.com/gramps-project/addons/master/gramps52') +register('behavior.addons-projects', + [['Gramps', 'https://raw.githubusercontent.com/gramps-project/addons/master/gramps52', True]]) +register('behavior.addons-allow-install', False) register('csv.dialect', 'excel') register('csv.delimiter', ',') @@ -225,7 +228,7 @@ register('interface.toolbar-on', True) register('interface.toolbar-text', False) register('interface.hide-lds', False) register('interface.toolbar-clipboard', True) -register('interface.toolbar-plugin', True) +register('interface.toolbar-addons', True) register('interface.toolbar-preference', True) register('interface.toolbar-reports', True) register('interface.toolbar-tools', True) diff --git a/gramps/gen/plug/__init__.py b/gramps/gen/plug/__init__.py index faeefa4cb..667fa447d 100644 --- a/gramps/gen/plug/__init__.py +++ b/gramps/gen/plug/__init__.py @@ -34,7 +34,7 @@ from ._pluginreg import (PluginData, PluginRegister, REPORT, TOOL, CATEGORY_QR_PLACE, CATEGORY_QR_REPOSITORY, CATEGORY_QR_NOTE, CATEGORY_QR_DATE, PTYPE_STR, CATEGORY_QR_MEDIA, CATEGORY_QR_CITATION, CATEGORY_QR_SOURCE_OR_CITATION, - START, END, make_environment, + START, END, make_environment, AUDIENCETEXT, STATUSTEXT, ) from ._import import ImportPlugin from ._export import ExportPlugin diff --git a/gramps/gen/plug/_manager.py b/gramps/gen/plug/_manager.py index 16474d5b6..6a724378a 100644 --- a/gramps/gen/plug/_manager.py +++ b/gramps/gen/plug/_manager.py @@ -104,6 +104,14 @@ class BasePluginManager: self.__loaded_plugins = {} self.__scanned_dirs = [] + def reg_plugin_dir(self, direct, dbstate=None, uistate=None, + load_on_reg=False, rescan=False): + """ + Register plugins in a given directory. + """ + self.__scanned_dirs.remove(direct) + self.reg_plugins(direct, dbstate, uistate, load_on_reg, rescan) + def reg_plugins(self, direct, dbstate=None, uistate=None, load_on_reg=False, rescan=False): """ @@ -128,8 +136,7 @@ class BasePluginManager: # " been_here=%s, pahte exists:%s", direct, load_on_reg, # direct in self.__scanned_dirs, os.path.isdir(direct)) - if os.path.isdir(direct) and direct not in self.__scanned_dirs: - self.__scanned_dirs.append(direct) + if os.path.isdir(direct): for (dirpath, dirnames, filenames) in os.walk(direct, topdown=True): for dirname in dirnames[:]: @@ -138,7 +145,9 @@ class BasePluginManager: "__pycache__"]: dirnames.remove(dirname) # LOG.warning("Plugin dir scanned: %s", dirpath) - self.__pgr.scan_dir(dirpath, filenames, uistate=uistate) + if dirpath not in self.__scanned_dirs: + self.__pgr.scan_dir(dirpath, filenames, uistate=uistate) + self.__scanned_dirs.append(dirpath) if load_on_reg: # Run plugins that request to be loaded on startup and diff --git a/gramps/gen/plug/_pluginreg.py b/gramps/gen/plug/_pluginreg.py index d47a6c279..f7ec9e2fd 100644 --- a/gramps/gen/plug/_pluginreg.py +++ b/gramps/gen/plug/_pluginreg.py @@ -40,6 +40,7 @@ import traceback # #------------------------------------------------------------------------- from ...version import VERSION as GRAMPSVERSION, VERSION_TUPLE +from ..utils.requirements import Requirements from ..const import IMAGE_DIR from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext @@ -1271,6 +1272,7 @@ class PluginRegister: self.stable_only = False self.__plugindata = [] self.__id_to_pdata = {} + self.__req = Requirements() def add_plugindata(self, plugindata): """ This is used to add an entry to the registration list. The way it @@ -1323,10 +1325,11 @@ class PluginRegister: exec (compile(stream, filename, 'exec'), make_environment(_=local_gettext), {'uistate': uistate}) for pdata in self.__plugindata[lenpd:]: - # should not be duplicate IDs in different plugins - assert pdata.id not in self.__id_to_pdata - # if pdata.id in self.__id_to_pdata: - # print("Error: %s is duplicated!" % pdata.id) + if pdata.id in self.__id_to_pdata: + # reloading + old = self.__id_to_pdata[pdata.id] + self.__plugindata.remove(old) + lenpd -= 1 self.__id_to_pdata[pdata.id] = pdata except ValueError as msg: print(_('ERROR: Failed reading plugin registration %(filename)s') % \ @@ -1358,6 +1361,9 @@ class PluginRegister: )) rmlist.append(ind) continue + if not self.__req.check_plugin(plugin): + rmlist.append(ind) + continue if not plugin.status == STABLE and self.stable_only: rmlist.append(ind) continue diff --git a/gramps/gen/plug/utils.py b/gramps/gen/plug/utils.py index a3fa3fa50..31ac135b0 100644 --- a/gramps/gen/plug/utils.py +++ b/gramps/gen/plug/utils.py @@ -31,6 +31,7 @@ import sys import os import datetime from io import StringIO, BytesIO +import json #------------------------------------------------------------------------- # @@ -192,8 +193,7 @@ def urlopen_maybe_no_check_cert(URL): fp = urlopen(URL, timeout=timeout) return fp -def available_updates(): - whattypes = config.get('behavior.check-for-addon-update-types') +def get_addons(project, url): LOG.debug("Checking for updated addons...") langs = glocale.get_language_list() @@ -201,15 +201,13 @@ def available_updates(): # now we have a list of languages to try: fp = None for lang in langs: - URL = ("%s/listings/addons-%s.txt" % - (config.get("behavior.addons-url"), lang)) + URL = ("%s/listings/addons-%s.json" % (url, lang)) LOG.debug(" trying: %s" % URL) try: fp = urlopen_maybe_no_check_cert(URL) except: try: - URL = ("%s/listings/addons-%s.txt" % - (config.get("behavior.addons-url"), lang[:2])) + URL = ("%s/listings/addons-%s.json" % (url, lang[:2])) fp = urlopen_maybe_no_check_cert(URL) except Exception as err: # some error LOG.warning("Failed to open addon metadata for {lang} {url}: {err}". @@ -218,60 +216,73 @@ def available_updates(): if fp and (fp.getcode() == 200 or fp.file): break - pmgr = BasePluginManager.get_instance() - addon_update_list = [] + addon_list = [] if fp and (fp.getcode() == 200 or fp.file): - lines = list(fp.readlines()) - count = 0 - for line in lines: - line = line.decode('utf-8') - try: - plugin_dict = safe_eval(line) - if type(plugin_dict) != type({}): - raise TypeError("Line with addon metadata is not " - "a dictionary") - except: - LOG.warning("Skipped a line in the addon listing: " + - str(line)) - continue + addon_list = json.load(fp) + for plugin_dict in addon_list: + if 'a' not in plugin_dict: + plugin_dict['a'] = 0 + if 's' not in plugin_dict: + plugin_dict['s'] = 0 + if 'h' not in plugin_dict: + plugin_dict['h'] = '' + plugin_dict['_p'] = project + plugin_dict['_u'] = url id = plugin_dict["i"] + pmgr = BasePluginManager.get_instance() plugin = pmgr.get_plugin(id) if plugin: - LOG.debug("Comparing %s > %s" % - (version_str_to_tup(plugin_dict["v"], 3), - version_str_to_tup(plugin.version, 3))) - if (version_str_to_tup(plugin_dict["v"], 3) > - version_str_to_tup(plugin.version, 3)): - LOG.debug(" Downloading '%s'..." % plugin_dict["z"]) - if "update" in whattypes: - if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or - plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): - addon_update_list.append((_("Updated"), - "%s/download/%s" % - (config.get("behavior.addons-url"), - plugin_dict["z"]), - plugin_dict)) - else: - LOG.debug(" '%s' is ok" % plugin_dict["n"]) - else: - LOG.debug(" '%s' is not installed" % plugin_dict["n"]) - if "new" in whattypes: - if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or - plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): - addon_update_list.append((_("New", "updates"), - "%s/download/%s" % - (config.get("behavior.addons-url"), - plugin_dict["z"]), - plugin_dict)) - config.set("behavior.last-check-for-addon-updates", - datetime.date.today().strftime("%Y/%m/%d")) - count += 1 - if fp: - fp.close() + plugin_dict['_v'] = plugin.version else: LOG.debug("Checking Addons Failed") LOG.debug("Done checking!") + return addon_list + +def get_all_addons(): + projects = config.get('behavior.addons-projects') + all_addons = [] + for project, url, enabled in projects: + if enabled: + addons_list = get_addons(project, url) + all_addons.extend(addons_list) + return all_addons + +def available_updates(): + + whattypes = config.get('behavior.check-for-addon-update-types') + addon_update_list = [] + for plugin_dict in get_all_addons(): + if '_v' in plugin_dict: + LOG.debug("Comparing %s > %s" % + (version_str_to_tup(plugin_dict["v"], 3), + version_str_to_tup(plugin_dict["_v"], 3))) + if (version_str_to_tup(plugin_dict["v"], 3) > + version_str_to_tup(plugin_dict["_v"], 3)): + LOG.debug(" Downloading '%s'..." % plugin_dict["z"]) + if "update" in whattypes: + if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or + plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): + addon_update_list.append((_("Updated"), + "%s/download/%s" % + (plugin_dict["_u"], + plugin_dict["z"]), + plugin_dict)) + else: + LOG.debug(" '%s' is ok" % plugin_dict["n"]) + else: + LOG.debug(" '%s' is not installed" % plugin_dict["n"]) + if "new" in whattypes: + if (not config.get('behavior.do-not-show-previously-seen-addon-updates') or + plugin_dict["i"] not in config.get('behavior.previously-seen-addon-updates')): + addon_update_list.append((_("New", "updates"), + "%s/download/%s" % + (plugin_dict["_u"], + plugin_dict["z"]), + plugin_dict)) + config.set("behavior.last-check-for-addon-updates", + datetime.date.today().strftime("%Y/%m/%d")) + return addon_update_list def load_addon_file(path, callback=None): diff --git a/gramps/gen/utils/requirements.py b/gramps/gen/utils/requirements.py new file mode 100644 index 000000000..3b01620a5 --- /dev/null +++ b/gramps/gen/utils/requirements.py @@ -0,0 +1,171 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2023 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +from importlib.util import find_spec + +#------------------------------------------------------------------------- +# +# GTK modules +# +#------------------------------------------------------------------------- +import gi + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from .file import search_for +from ..config import config +from ..const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext + +#------------------------------------------------------------------------- +# +# Requirements +# +#------------------------------------------------------------------------- +class Requirements(): + + def __init__(self): + self.mod_list = [] + self.gi_list = [] + self.exe_list = [] + + def check_mod(self, module): + ''' + Check to see if a given module is available. + ''' + if module in self.mod_list: + return True + if find_spec(module) is not None: + self.mod_list.append(module) + return True + else: + return False + + def check_gi(self, module_spec): + ''' + Check to see if a GObject introspection module is available. + ''' + if module_spec in self.gi_list: + return True + try: + gi.require_version(*module_spec) + self.gi_list.append(module_spec) + return True + except ValueError: + return False + + def check_exe(self, executable): + ''' + Check to see if a given executable is available. + ''' + if executable in self.exe_list: + return True + if search_for(executable): + self.exe_list.append(executable) + return True + else: + return False + + def check(self, gi_list, exe_list, mod_list, install=False): + ''' + Check lists of requirements. + ''' + for module_spec in gi_list: + if not self.check_gi(module_spec): + return False + for executable in exe_list: + if not self.check_exe(executable): + return False + if install and config.get('behavior.addons-allow-install'): + return True + for module in mod_list: + if not self.check_mod(module): + return False + return True + + def check_addon(self, addon, install=False): + ''' + Check the requirements of a given addon. + ''' + return self.check(addon.get("rg", []), + addon.get("re", []), + addon.get("rm", []), + install) + + def check_plugin(self, plugin): + ''' + Check the requirements of a given plugin. + ''' + return self.check(plugin.requires_gi, + plugin.requires_exe, + plugin.requires_mod) + + def install(self, addon): + ''' + Return a list of modules required to be installed. + ''' + install_list = [] + if "rm" in addon: + for module in addon.get("rm"): + if not self.check_mod(module): + install_list.append(module) + return install_list + + def info(self, addon): + ''' + Provide the requirements status of a given addon. + ''' + info = [] + if "rm" in addon: + info.append(_('Python modules')) + table = [] + for module in addon.get("rm"): + result = self.check_mod(module) + table.append([module, tick_cross(result)]) + info.append(table) + if "rg" in addon: + info.append(_('GObject introspection modules')) + table = [] + for module_spec in addon.get("rg"): + result = self.check_gi(module_spec) + table.append([' '.join(module_spec), tick_cross(result)]) + info.append(table) + if "re" in addon: + info.append(_('Executables')) + table = [] + for executable in addon.get("re"): + result = self.check_exe(executable) + table.append([executable, tick_cross(result)]) + info.append(table) + return info + +def tick_cross(value): + ''' + Return a tick for True or a cross for False + ''' + return '\u2714' if value else '\u2718' diff --git a/gramps/gui/configure.py b/gramps/gui/configure.py index f1e432fda..b1c50cdae 100644 --- a/gramps/gui/configure.py +++ b/gramps/gui/configure.py @@ -563,7 +563,6 @@ class GrampsPreferences(ConfigureDialog): page_funcs = ( self.add_data_panel, self.add_general_panel, - self.add_addons_panel, self.add_famtree_panel, self.add_import_panel, self.add_limits_panel, @@ -1543,29 +1542,12 @@ class GrampsPreferences(ConfigureDialog): self.pformat.set_model(model) self.pformat.set_active(0) - def check_for_type_changed(self, obj): - active = obj.get_active() - if active == 0: # update - config.set('behavior.check-for-addon-update-types', ["update"]) - elif active == 1: # update - config.set('behavior.check-for-addon-update-types', ["new"]) - elif active == 2: # update - config.set('behavior.check-for-addon-update-types', - ["update", "new"]) - def toggle_tag_on_import(self, obj): """ Update Entry sensitive for tag on import. """ self.tag_format_entry.set_sensitive(obj.get_active()) - def check_for_updates_changed(self, obj): - """ - Save "Check for addon updates" option. - """ - active = obj.get_active() - config.set('behavior.check-for-addon-updates', active) - def date_format_changed(self, obj): """ Save "Date format" option. @@ -1728,11 +1710,11 @@ class GrampsPreferences(ConfigureDialog): extra_callback=self.cb_toolbar_changed) row += 1 - # Show Plugins Icon: + # Show Addons Icon: self.add_checkbox( - grid, _("Show Plugins icon on toolbar"), - row, 'interface.toolbar-plugin', start=1, stop=3, - tooltip=_("Show or hide the Plugins icon on the toolbar."), + grid, _("Show Addons icon on toolbar"), + row, 'interface.toolbar-addons', start=1, stop=3, + tooltip=_("Show or hide the Addons icon on the toolbar."), extra_callback=self.cb_toolbar_changed) row += 1 @@ -1794,101 +1776,6 @@ class GrampsPreferences(ConfigureDialog): return _('General'), grid - def add_addons_panel(self, configdialog): - """ - Config tab with 'Addons' install settings. - """ - grid = self.create_grid() - - row = 1 - label = self.add_text( - grid, _('Configuration settings to have Gramps check for' - ' new or updated third party Addons and Plugins.' - ' The Plugin Manager has the complete list of installed' - ' Addons and Plugins and their activation status.\n'), row, - line_wrap=True, start=0, stop=9) - label.set_margin_top(10) - - row += 1 - # Check for addon updates: - obox = Gtk.ComboBoxText() - formats = [_("Never"), - _("Once a month"), - _("Once a week"), - _("Once a day"), - _("Always"), ] - list(map(obox.append_text, formats)) - active = config.get('behavior.check-for-addon-updates') - obox.set_active(active) - obox.connect('changed', self.check_for_updates_changed) - lwidget = BasicLabel(_("%s: ") % _('Check for addon updates')) - grid.attach(lwidget, 2, row, 1, 1) - grid.attach(obox, 3, row, 1, 1) - - row += 1 - self.whattype_box = Gtk.ComboBoxText() - formats = [_("Updated addons only"), - _("New addons only"), - _("New and updated addons")] - list(map(self.whattype_box.append_text, formats)) - whattype = config.get('behavior.check-for-addon-update-types') - if "new" in whattype and "update" in whattype: - self.whattype_box.set_active(2) - elif "new" in whattype: - self.whattype_box.set_active(1) - elif "update" in whattype: - self.whattype_box.set_active(0) - self.whattype_box.connect('changed', self.check_for_type_changed) - lwidget = BasicLabel(_("%s: ") % _('What to check')) - grid.attach(lwidget, 2, row, 1, 1) - grid.attach(self.whattype_box, 3, row, 1, 1) - - row += 1 - self.add_entry(grid, _('Where to check'), row, - 'behavior.addons-url', col_attach=2) - - row += 1 - self.add_checkbox( - grid, _('Do not ask about previously notified addons'), - row, 'behavior.do-not-show-previously-seen-addon-updates', - start=2, stop=9) - - row += 1 - button = Gtk.Button(label=_("Check for updated addons now")) - button.connect("clicked", self.check_for_updates) - button.set_hexpand(False) - button.set_halign(Gtk.Align.CENTER) - grid.attach(button, 1, row, 3, 1) - - return _('Addons'), grid - - def check_for_updates(self, button): - try: - addon_update_list = available_updates() - except: - OkDialog(_("Checking Addons Failed"), - _("The addon repository appears to be unavailable. " - "Please try again later."), - parent=self.window) - return - - if len(addon_update_list) > 0: - rescan = PluginWindows.UpdateAddons(self.uistate, self.track, - addon_update_list).rescan - self.uistate.viewmanager.do_reg_plugins(self.dbstate, self.uistate, - rescan=rescan) - else: - check_types = config.get('behavior.check-for-addon-update-types') - OkDialog( - _("There are no available addons of this type"), - _("Checked for '%s'") % - _("' and '").join([_(t) for t in check_types]), - parent=self.window) - - # List of translated strings used here - # Dead code for l10n - _('new'), _('update') - def database_backend_changed(self, obj): """ Update Database Backend. diff --git a/gramps/gui/displaystate.py b/gramps/gui/displaystate.py index a19d4dcaf..0ffb9d1ec 100644 --- a/gramps/gui/displaystate.py +++ b/gramps/gui/displaystate.py @@ -409,10 +409,10 @@ TOOLS = { 'win.Tools', _('Open the tools dialog'), _('Tools')), -'plugin': ('gramps-plugin-manager', - 'win.PluginStatus', - _('Open Plugin Manager'), - _('Plugins')), +'addons': ('gramps-addon', + 'win.AddonManager', + _('Open Addon Manager'), + _('Addons')), 'preference': ('gramps-preferences', 'app.preferences', _('Open Preferences'), diff --git a/gramps/gui/grampsgui.py b/gramps/gui/grampsgui.py index 5838643ca..cb30331a3 100644 --- a/gramps/gui/grampsgui.py +++ b/gramps/gui/grampsgui.py @@ -173,6 +173,12 @@ UIDEFAULT = ( win.Clipboard Clip_board + + win.AddonManager + ''' + '''_Addon Manager... + +
diff --git a/gramps/gui/plug/_windows.py b/gramps/gui/plug/_windows.py index 0e83b804e..d5aec9cc0 100644 --- a/gramps/gui/plug/_windows.py +++ b/gramps/gui/plug/_windows.py @@ -28,6 +28,10 @@ #------------------------------------------------------------------------- import traceback import os +from html import escape +import threading +import sys +import subprocess #------------------------------------------------------------------------- # @@ -44,6 +48,7 @@ LOG = logging.getLogger(".gui.plug") #------------------------------------------------------------------------- from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GLib from gi.repository import Pango from gi.repository import GObject @@ -57,7 +62,8 @@ _ = glocale.translation.gettext ngettext = glocale.translation.ngettext # else "nearby" comments are ignored from ..managedwindow import ManagedWindow from gramps.gen.errors import UnavailableError, WindowActiveError -from gramps.gen.plug import PluginRegister, PTYPE_STR, load_addon_file +from gramps.gen.plug import (PluginRegister, PTYPE_STR, load_addon_file, + AUDIENCETEXT, STATUSTEXT) from ..utils import open_file_with_default_application from ..pluginmanager import GuiPluginManager from . import tool @@ -66,11 +72,17 @@ from ..dialog import InfoDialog, OkDialog from ..editors import EditPerson from ..glade import Glade from ..listmodel import ListModel, NOSORT, TOGGLE -from gramps.gen.const import URL_WIKISTRING, USER_HOME, WIKI_EXTRAPLUGINS_RAWDATA +from gramps.gen.const import URL_WIKISTRING, USER_HOME, WIKI_EXTRAPLUGINS_RAWDATA, COLON from gramps.gen.config import config from ..widgets.progressdialog import (LongOpStatus, ProgressMonitor, GtkProgressDialog) +from gramps.gen.plug.utils import get_all_addons, available_updates +from ..display import display_help, display_url +from gramps.gui.widgets import BasicLabel, SimpleButton +from gramps.gen.utils.requirements import Requirements +from gramps.gen.const import USER_PLUGINS + def display_message(message): """ A default callback for displaying messages. @@ -79,6 +91,713 @@ def display_message(message): RELOAD = 777 # A custom Gtk response_type for the Reload button +#------------------------------------------------------------------------- +# +# GetAddons +# +#------------------------------------------------------------------------- +class GetAddons(threading.Thread): + """ + A class for retrieving a list of addons as a background task. + """ + def __init__(self, callback): + threading.Thread.__init__(self) + self.callback = callback + self.addon_list = [] + self.__pmgr = GuiPluginManager.get_instance() + + def emit_signal(self): + self.callback(self.addon_list) + + def run(self): + self.addon_list = self.__get_addon_list() + GLib.idle_add(self.emit_signal) + + def __get_addon_list(self): + return get_all_addons() + +#------------------------------------------------------------------------- +# +# ProjectRow +# +#------------------------------------------------------------------------- +class ProjectRow(Gtk.ListBoxRow): + """ + A class to display an external addons repository. + """ + def __init__(self, manager, project): + Gtk.ListBoxRow.__init__(self) + self.manager = manager + self.project = project + + hbox = Gtk.Box() + hbox.set_spacing(12) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_spacing(6) + + self.check = Gtk.CheckButton() + + hbox.pack_start(self.check, False, False, 0) + hbox.pack_start(vbox, True, True, 0) + + self.name = Gtk.Label() + self.name.set_use_markup(True) + self.name.set_halign(Gtk.Align.START) + self.url = Gtk.Label() + self.url.set_halign(Gtk.Align.START) + vbox.pack_start(self.name, False, False, 0) + vbox.pack_start(self.url, False, False, 0) + + self.add(hbox) + self.show_all() + + self.update() + self.check.connect('toggled', self.__check_toggled) + + def __check_toggled(self, check): + self.project[2] = check.get_active() + projects = [row.project for row in self.manager.project_list] + config.set('behavior.addons-projects', projects) + self.manager.refresh() + + def update(self): + """ + Update the row when the project data has been updated. + """ + text = self.project[0] + self.name.set_markup('%s' % text) + self.url.set_text(self.project[1]) + self.check.set_active(self.project[2]) + +#------------------------------------------------------------------------- +# +# AddonManager +# +#------------------------------------------------------------------------- +class AddonRow(Gtk.ListBoxRow): + """ + A class representing an addon in the Addon Manager. + """ + def __init__(self, manager, addon, req, window): + Gtk.ListBoxRow.__init__(self) + self.manager = manager + self.addon = addon + self.req = req + self.window = window + + context = self.get_style_context() + context.add_class('addon-row') + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_spacing(6) + + text = escape(addon['n']) + title = Gtk.Label() + title.set_text('%s' % text) + title.set_use_markup(True) + title.set_halign(Gtk.Align.CENTER) + vbox.pack_start(title, False, False, 0) + + hbox = Gtk.Box() + hbox.set_spacing(6) + lozenge = self.__create_lozenge(_('Project'), addon['_p']) + hbox.pack_start(lozenge, False, False, 0) + lozenge = self.__create_lozenge(_('Type'), addon['t']) + hbox.pack_start(lozenge, False, False, 0) + lozenge = self.__create_lozenge(_('Audience'), AUDIENCETEXT[addon['a']]) + hbox.pack_start(lozenge, False, False, 0) + lozenge = self.__create_lozenge(_('Status'), STATUSTEXT[addon['s']]) + hbox.pack_start(lozenge, False, False, 0) + lozenge = self.__create_lozenge(_('Version'), addon['v']) + hbox.pack_start(lozenge, False, False, 0) + if '_v' in addon: + lozenge = self.__create_lozenge(_('Installed version'), addon['_v']) + hbox.pack_end(lozenge, False, False, 0) + + vbox.pack_start(hbox, False, False, 0) + + text = addon['d'] + descr = Gtk.Label() + descr.set_text(text) + descr.set_halign(Gtk.Align.START) + descr.set_hexpand(False) + descr.set_line_wrap(True) + descr.set_line_wrap_mode(Pango.WrapMode.WORD) + vbox.pack_start(descr, False, False, 0) + + bb = Gtk.Box() + bb.set_spacing(6) + + if '_v' not in addon and req.check_addon(addon, install=True): + b1 = Gtk.Button(label=_("Install")) + b1.set_label(_("Install")) + b1.connect('clicked', self.__on_install_clicked, addon) + bb.pack_end(b1, False, False, 0) + + if addon['h']: + b2 = Gtk.Button(label=_("Wiki")) + b2.connect('clicked', self.__on_wiki_clicked, addon['h']) + bb.pack_start(b2, False, False, 0) + + if not req.check_addon(addon): + b3 = Gtk.Button(label=_("Requires")) + b3.connect('clicked', self.__on_requires_clicked, addon) + bb.pack_start(b3, False, False, 0) + + if '_v' in addon and addon['_v'] != addon['v']: + b4 = Gtk.Button(label=_("Upgrade")) + b4.connect('clicked', self.__on_upgrade_clicked, addon) + bb.pack_end(b4, False, False, 0) + + vbox.pack_start(bb, False, False, 0) + + self.add(vbox) + self.show_all() + + def __on_install_clicked(self, button, addon): + """ + Install the addon and possibly some required python modules. + """ + # Install required modules + for package in self.req.install(addon): + try: + subprocess.check_output( + [sys.executable, '-m', 'pip', 'install', package], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as err: + button.set_sensitive(False) + InfoDialog(_('Module installation failed'), + err.output.decode("utf-8"), + parent=self.window) + return + + # Install addon + path = addon['_u'] + '/download/' + addon['z'] + load_addon_file(path) + self.manager.install_addon(addon['i']) + self.manager.refresh() + + def __on_wiki_clicked(self, button, url): + """ + Display the wiki page for the addon. + """ + if url.startswith(('http://', 'https://')): + display_url(url) + else: + display_help(url) + + def __on_requires_clicked(self, button, addon): + """ + Display the requirements for the addon. + """ + InfoDialog(_('Requirements'), self.req.info(addon), parent=self.window) + + def __on_upgrade_clicked(self, button, addon): + """ + Upgrade the addon. + """ + path = addon['_u'] + '/download/' + addon['z'] + load_addon_file(path) + self.manager.upgrade_addon(addon['i']) + self.manager.refresh() + + def __create_lozenge(self, description, text): + """ + Create a lozenge shaped label to display addon information. + """ + label = Gtk.Label() + label.set_tooltip_text(description) + context = label.get_style_context() + context.add_class('lozenge') + label.set_text(text) + label.set_margin_start(6) + return label + +#------------------------------------------------------------------------- +# +# AddonManager +# +#------------------------------------------------------------------------- +class AddonManager(ManagedWindow): + """ + A class to allow the user to easily select addons to install. + """ + def __init__(self, dbstate, uistate, track): + self.dbstate = dbstate + self.title = _("Addon Manager") + ManagedWindow.__init__(self, uistate, [], self) + + self.__pmgr = GuiPluginManager.get_instance() + self.__preg = PluginRegister.get_instance() + dialog = Gtk.Dialog(title="", transient_for=uistate.window, + destroy_with_parent=True) + dialog.add_button(_('Refresh'), RELOAD) + dialog.add_button(_('_Close'), Gtk.ResponseType.CLOSE) + self.set_window(dialog, None, self.title) + + self.req = Requirements() + + self.setup_configs('interface.addonmanager', 750, 400) + self.window.connect('response', self.__on_dialog_button) + + book = Gtk.Notebook() + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_spacing(6) + vbox.set_margin_start(6) + vbox.set_margin_end(6) + vbox.set_margin_top(6) + vbox.set_margin_bottom(6) + + self.search = Gtk.Entry() + self.search.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, + 'system-search') + self.search.connect("changed", self.__combo_changed) + vbox.pack_start(self.search, False, False, 0) + + hbox = Gtk.Box() + hbox.set_spacing(6) + self.lb = Gtk.ListBox() + self.lb.set_activate_on_single_click(False) + + label = Gtk.Label(label=_('Filters') + COLON) + label.set_margin_end(12) + hbox.pack_start(label, False, False, 0) + + self.projects = config.get('behavior.addons-projects') + self.project_combo = Gtk.ComboBoxText() + self.project_combo.set_entry_text_column(0) + self.project_combo.connect("changed", self.__combo_changed) + self.project_combo.append_text(_('All')) + for project in self.projects: + self.project_combo.append_text(project[0]) + self.project_combo.set_active(0) + hbox.pack_start(self.project_combo, False, False, 0) + + self.type_combo = Gtk.ComboBoxText() + self.type_combo.set_entry_text_column(0) + self.type_combo.connect("changed", self.__combo_changed) + self.type_combo.append_text(_('All')) + for typestr in PTYPE_STR.values(): + self.type_combo.append_text(typestr) + self.type_combo.set_active(0) + hbox.pack_start(self.type_combo, False, False, 0) + + audience_store = Gtk.ListStore(int, str) + audience_store.append([-1, _('All')]) + for key, value in AUDIENCETEXT.items(): + audience_store.append([key, value]) + self.audience_combo = Gtk.ComboBox() + self.audience_combo.set_model(audience_store) + self.audience_combo.set_entry_text_column(1) + self.audience_combo.connect("changed", self.__combo_changed) + self.audience_combo.set_active(1) + renderer_text = Gtk.CellRendererText() + self.audience_combo.pack_start(renderer_text, True) + self.audience_combo.add_attribute(renderer_text, "text", 1) + hbox.pack_start(self.audience_combo, False, False, 0) + + status_store = Gtk.ListStore(int, str) + status_store.append([-1, _('All')]) + for key, value in STATUSTEXT.items(): + status_store.append([key, value]) + self.status_combo = Gtk.ComboBox() + self.status_combo.set_model(status_store) + self.status_combo.set_entry_text_column(1) + self.status_combo.connect("changed", self.__combo_changed) + self.status_combo.set_active(4) + renderer_text = Gtk.CellRendererText() + self.status_combo.pack_start(renderer_text, True) + self.status_combo.add_attribute(renderer_text, "text", 1) + hbox.pack_start(self.status_combo, False, False, 0) + + clear = Gtk.Button.new_from_icon_name('edit-clear', Gtk.IconSize.BUTTON) + clear.connect("clicked", self.__clear_filters) + hbox.pack_start(clear, False, False, 0) + + vbox.pack_start(hbox, False, False, 0) + + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.add(self.lb) + vbox.pack_start(sw, True, True, 0) + + book.append_page(vbox, Gtk.Label(_("Addons"))) + + grid = self.create_settings_panel() + book.append_page(grid, Gtk.Label(_("Settings"))) + + grid = self.create_projects_panel() + book.append_page(grid, Gtk.Label(_("Projects"))) + + for project in self.projects: + self.project_list.add(ProjectRow(self, project)) + + self.window.get_content_area().pack_start(book, True, True, 0) + + self.lb.set_sort_func(self.__sort_func) + self.lb.set_filter_func(self.__filter_func) + + self.show() + + self.refresh() + + def refresh(self): + """ + Refresh the addons list. + """ + for child in self.lb.get_children(): + self.lb.remove(child) + self.__placeholder(_('Loading...')) + + thread = GetAddons(self.load_addons) + thread.start() + + def upgrade_addon(self, addon_id): + """ + Upgrade the given addon. + """ + pdata = self.__preg.get_plugin(addon_id) + self.__pmgr.reg_plugin_dir(pdata.directory, self.dbstate, self.uistate, + load_on_reg=True) + pdata = self.__preg.get_plugin(addon_id) + self.__pmgr.load_plugin(pdata) + + def install_addon(self, addon_id): + """ + Install the given addon. + """ + self.__pmgr.reg_plugins(USER_PLUGINS, self.dbstate, self.uistate, + load_on_reg=True) + pdata = self.__preg.get_plugin(addon_id) + self.__pmgr.load_plugin(pdata) + + def build_menu_names(self, obj): + return (self.title, self.title) + + def __placeholder(self, text): + """ + A placeholder label if no addons are listed. + """ + label = Gtk.Label('%s' % text) + label.set_use_markup(True) + label.show() + self.lb.set_placeholder(label) + + def load_addons(self, addon_list): + """ + Populate the list box. + """ + for addon in addon_list: + self.lb.add(AddonRow(self, addon, self.req, self.window)) + self.__placeholder(_('No matching addons found.')) + + def __clear_filters(self, combo): + """ + Reset the filters back to their defaults. + """ + self.search.set_text('') + self.type_combo.set_active(0) + self.project_combo.set_active(0) + self.audience_combo.set_active(1) + self.status_combo.set_active(4) + + def __combo_changed(self, combo): + """ + Called when a filter is changed. + """ + self.lb.invalidate_filter() + + def __sort_func(self, row1, row2): + """ + Sort the addons by name. + """ + value1 = row1.addon['n'] + value2 = row2.addon['n'] + if value1 > value2: + return 1 + elif value1 < value2: + return -1 + else: + return 0 + + def __filter_func(self, row): + """ + Filter the addons list according to the user selection. + """ + search_text = self.search.get_text() + type_text = self.type_combo.get_active_text() + project_text = self.project_combo.get_active_text() + audience_iter = self.audience_combo.get_active_iter() + status_iter = self.status_combo.get_active_iter() + if type_text != _('All'): + if row.addon['t'] != type_text: + return False + if project_text != _('All'): + if row.addon['_p'] != project_text: + return False + model = self.audience_combo.get_model() + value = model.get_value(audience_iter, 0) + if value != -1 and row.addon['a'] != value: + return False + model = self.status_combo.get_model() + value = model.get_value(status_iter, 0) + if value != -1 and row.addon['s'] != value: + return False + if search_text and search_text not in row.addon['d']: + return False + return True + + def __on_dialog_button(self, dialog, response_id): + """ + Handle a main dialog button click. + """ + if response_id == Gtk.ResponseType.CLOSE: + self.close(dialog) + elif response_id == RELOAD: + self.refresh() + + def create_projects_panel(self): + """ + Configuration tab with addons projects. + """ + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_spacing(6) + vbox.set_margin_start(6) + vbox.set_margin_end(6) + vbox.set_margin_top(6) + vbox.set_margin_bottom(6) + + self.project_list = Gtk.ListBox() + self.project_list.set_activate_on_single_click(False) + self.project_list.connect('row-activated', self.__edit_project) + self.project_list.set_margin_start(6) + + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.add(self.project_list) + vbox.pack_start(sw, True, True, 0) + + hbox = Gtk.Box() + add_btn = SimpleButton('list-add', self.__add_project) + del_btn = SimpleButton('list-remove', self.__remove_project) + hbox.pack_start(add_btn, False, False, 0) + hbox.pack_start(del_btn, False, False, 0) + vbox.pack_start(hbox, False, False, 0) + + return vbox + + def create_settings_panel(self): + """ + Configuration tab with addons settings. + """ + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.set_spacing(6) + vbox.set_margin_start(6) + vbox.set_margin_end(6) + vbox.set_margin_top(6) + vbox.set_margin_bottom(6) + + heading1 = Gtk.Label() + text = _('General') + heading1.set_text('%s' % text) + heading1.set_use_markup(True) + heading1.set_halign(Gtk.Align.START) + vbox.pack_start(heading1, False, False, 0) + + grid = Gtk.Grid() + grid.set_row_spacing(6) + grid.set_margin_start(6) + grid.set_margin_bottom(12) + vbox.pack_start(grid, False, False, 0) + + row = 1 + install = Gtk.CheckButton() + install.set_label(_('Allow Gramps to install required python modules')) + install.connect("toggled", self.install_changed) + grid.attach(install, 1, row, 1, 1) + + heading2 = Gtk.Label() + text = _('Updates') + heading2.set_text('%s' % text) + heading2.set_use_markup(True) + heading2.set_halign(Gtk.Align.START) + vbox.pack_start(heading2, False, False, 0) + + grid = Gtk.Grid() + grid.set_row_spacing(6) + grid.set_margin_start(6) + vbox.pack_start(grid, False, False, 0) + + # Check for addon updates: + row = 1 + obox = Gtk.ComboBoxText() + formats = [_("Never"), + _("Once a month"), + _("Once a week"), + _("Once a day"), + _("Always"), ] + list(map(obox.append_text, formats)) + active = config.get('behavior.check-for-addon-updates') + obox.set_active(active) + obox.connect('changed', self.check_for_updates_changed) + lwidget = BasicLabel(_("%s: ") % _('Check for addon updates')) + grid.attach(lwidget, 1, row, 1, 1) + grid.attach(obox, 2, row, 1, 1) + + row += 1 + self.whattype_box = Gtk.ComboBoxText() + formats = [_("Updated addons only"), + _("New addons only"), + _("New and updated addons")] + list(map(self.whattype_box.append_text, formats)) + whattype = config.get('behavior.check-for-addon-update-types') + if "new" in whattype and "update" in whattype: + self.whattype_box.set_active(2) + elif "new" in whattype: + self.whattype_box.set_active(1) + elif "update" in whattype: + self.whattype_box.set_active(0) + self.whattype_box.connect('changed', self.check_for_type_changed) + lwidget = BasicLabel(_("%s: ") % _('What to check')) + grid.attach(lwidget, 1, row, 1, 1) + grid.attach(self.whattype_box, 2, row, 1, 1) + + row += 1 + previous = Gtk.CheckButton() + previous.set_label(_('Do not ask about previously notified addons')) + previous.connect("toggled", self.previous_changed) + grid.attach(previous, 1, row, 1, 1) + + row += 1 + button = Gtk.Button(label=_("Check for updated addons now")) + button.connect("clicked", self.check_for_updates) + button.set_hexpand(False) + button.set_halign(Gtk.Align.CENTER) + grid.attach(button, 1, row, 2, 1) + + return vbox + + def edit_project(self, row): + ''' + Add or edit a project + ''' + if row.project[0] == '': + title = _('New Project') + else: + title = _('Edit Project') + dialog = Gtk.Dialog(title=title, + transient_for=self.window) + dialog.set_border_width(12) + dialog.vbox.set_spacing(10) + hbox = Gtk.Box() + label = Gtk.Label(label=_("%s: ") % _('Project name')) + hbox.pack_start(label, True, True, 0) + name = Gtk.Entry() + name.set_text(row.project[0]) + name.set_activates_default(True) + hbox.pack_start(name, False, True, 0) + dialog.vbox.pack_start(hbox, False, True, 0) + hbox = Gtk.Box() + label = Gtk.Label(label=_("%s: ") % _('Url')) + hbox.pack_start(label, True, True, 0) + url = Gtk.Entry() + url.set_text(row.project[1]) + hbox.pack_start(url, False, True, 0) + dialog.vbox.pack_start(hbox, False, True, 0) + + dialog.add_buttons(_('_Cancel'), Gtk.ResponseType.CANCEL, + _('_OK'), Gtk.ResponseType.OK) + dialog.set_default_response(Gtk.ResponseType.OK) + dialog.vbox.show_all() + + if dialog.run() == Gtk.ResponseType.OK: + if row.project[0] == '': + self.project_list.add(row) + row.project[0] = name.get_text() + row.project[1] = url.get_text() + row.update() + projects = [row.project for row in self.project_list] + config.set('behavior.addons-projects', projects) + self.refresh() + dialog.destroy() + + def __add_project(self, button): + ''' + Add a project + ''' + self.edit_project(ProjectRow(self, ['', '', False])) + + def __remove_project(self, button): + ''' + Remove a project + ''' + row = self.project_list.get_selected_row() + if row: + self.project_list.remove(row) + projects = [p for p in config.get('behavior.addons-projects') + if p[0] != row.project[0]] + config.set('behavior.addons-projects', projects) + self.refresh() + + def __edit_project(self, listbox, row): + ''' + Edit a project + ''' + self.edit_project(row) + + def check_for_updates(self, button): + try: + addon_update_list = available_updates() + except: + OkDialog(_("Checking Addons Failed"), + _("The addon repository appears to be unavailable. " + "Please try again later."), + parent=self.window) + return + + if len(addon_update_list) > 0: + rescan = UpdateAddons(self.uistate, self.track, + addon_update_list).rescan + self.uistate.viewmanager.do_reg_plugins(self.dbstate, self.uistate, + rescan=rescan) + else: + check_types = config.get('behavior.check-for-addon-update-types') + OkDialog( + _("There are no available addons of this type"), + _("Checked for '%s'") % + _("' and '").join([_(t) for t in check_types]), + parent=self.window) + + # List of translated strings used here + # Dead code for l10n + _('new'), _('update') + + def check_for_type_changed(self, obj): + active = obj.get_active() + if active == 0: # update + config.set('behavior.check-for-addon-update-types', ["update"]) + elif active == 1: # update + config.set('behavior.check-for-addon-update-types', ["new"]) + elif active == 2: # update + config.set('behavior.check-for-addon-update-types', + ["update", "new"]) + + def check_for_updates_changed(self, obj): + """ + Save "Check for addon updates" option. + """ + active = obj.get_active() + config.set('behavior.check-for-addon-updates', active) + + def previous_changed(self, obj): + active = obj.get_active() + config.set('behavior.do-not-show-previously-seen-addon-updates', active) + + def install_changed(self, obj): + active = obj.get_active() + config.set('behavior.addons-allow-install', active) #------------------------------------------------------------------------- # diff --git a/gramps/gui/viewmanager.py b/gramps/gui/viewmanager.py index 560812fe9..a1463ea95 100644 --- a/gramps/gui/viewmanager.py +++ b/gramps/gui/viewmanager.py @@ -79,7 +79,7 @@ from gramps.gen.relationship import get_relationship_calculator from .displaystate import DisplayState, RecentDocsMenu from gramps.gen.const import (USER_DATA, ICON, URL_BUGTRACKER, URL_HOMEPAGE, URL_MAILINGLIST, URL_MANUAL_PAGE, URL_WIKISTRING, - WIKI_EXTRAPLUGINS, URL_BUGHOME) + WIKI_EXTRAPLUGINS, URL_BUGHOME, DATA_DIR) from gramps.gen.constfunc import is_quartz from gramps.gen.config import config from gramps.gen.errors import WindowActiveError @@ -267,6 +267,8 @@ class ViewManager(CLIManager): self.window.set_default_size(width, height) self.window.move(horiz_position, vert_position) + self.load_css() + self.provider = Gtk.CssProvider() self.change_font(font) @@ -382,6 +384,7 @@ class ViewManager(CLIManager): ('ExtraPlugins', extra_plugins_activate), #('about', self.display_about_box), ('PluginStatus', self.__plugin_status), + ('AddonManager', self.__addon_manager), ('FAQ', faq_activate), ('KeyBindings', key_bindings), ('UserManual', manual_activate, 'F1'), @@ -679,6 +682,13 @@ class ViewManager(CLIManager): except WindowActiveError: return + def load_css(self): + provider = Gtk.CssProvider() + provider.load_from_path(os.path.join(DATA_DIR, 'gramps.css')) + Gtk.StyleContext.add_provider_for_screen( + self.window.get_screen(), provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + def reset_font(self): """ Reset to the default application font. @@ -722,6 +732,15 @@ class ViewManager(CLIManager): except WindowActiveError: pass + def __addon_manager(self, obj=None, data=None): + """ + Display the addon manager dialog + """ + try: + PluginWindows.AddonManager(self.dbstate, self.uistate, []) + except WindowActiveError: + pass + def navigator_toggle(self, action, value): """ Set the sidebar based on the value of the toggle button. Save the diff --git a/images/hicolor/16x16/actions/gramps-plugin-manager.png b/images/hicolor/16x16/actions/gramps-addon.png similarity index 100% rename from images/hicolor/16x16/actions/gramps-plugin-manager.png rename to images/hicolor/16x16/actions/gramps-addon.png diff --git a/images/hicolor/22x22/actions/gramps-plugin-manager.png b/images/hicolor/22x22/actions/gramps-addon.png similarity index 100% rename from images/hicolor/22x22/actions/gramps-plugin-manager.png rename to images/hicolor/22x22/actions/gramps-addon.png diff --git a/images/hicolor/24x24/actions/gramps-plugin-manager.png b/images/hicolor/24x24/actions/gramps-addon.png similarity index 100% rename from images/hicolor/24x24/actions/gramps-plugin-manager.png rename to images/hicolor/24x24/actions/gramps-addon.png diff --git a/images/hicolor/48x48/actions/gramps-plugin-manager.png b/images/hicolor/48x48/actions/gramps-addon.png similarity index 100% rename from images/hicolor/48x48/actions/gramps-plugin-manager.png rename to images/hicolor/48x48/actions/gramps-addon.png diff --git a/images/hicolor/scalable/actions/gramps-plugin-manager.svg b/images/hicolor/scalable/actions/gramps-addon.svg similarity index 100% rename from images/hicolor/scalable/actions/gramps-plugin-manager.svg rename to images/hicolor/scalable/actions/gramps-addon.svg diff --git a/po/POTFILES.in b/po/POTFILES.in index bf85cefb0..a80a60c55 100755 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -375,6 +375,7 @@ gramps/gen/utils/image.py gramps/gen/utils/keyword.py gramps/gen/utils/lds.py gramps/gen/utils/place.py +gramps/gen/utils/requirements.py gramps/gen/utils/string.py gramps/gen/utils/symbols.py gramps/gen/utils/unknown.py diff --git a/setup.py b/setup.py index 9c0840228..98314201f 100755 --- a/setup.py +++ b/setup.py @@ -348,6 +348,7 @@ GRAMPS_FILES = glob.glob(os.path.join('example', 'gramps', '*.*')) IMAGE_WEB = glob.glob(os.path.join('images', 'webstuff', '*.png')) IMAGE_WEB.extend(glob.glob(os.path.join('images', 'webstuff','*.ico'))) IMAGE_WEB.extend(glob.glob(os.path.join('images', 'webstuff', '*.gif'))) +GRAMPS_CSS = glob.glob(os.path.join('data', '*.css')) CSS_FILES = glob.glob(os.path.join('data', 'css', '*.css')) SWANKY_PURSE = glob.glob(os.path.join('data', 'css', 'swanky-purse', '*.css')) SWANKY_IMG = glob.glob(os.path.join('data', 'css', 'swanky-purse', 'images', '*.png')) @@ -355,6 +356,7 @@ data_files_core.append(('share/doc/gramps', DOC_FILES)) data_files_core.append(('share/doc/gramps/example/gedcom', GEDCOM_FILES)) data_files_core.append(('share/doc/gramps/example/gramps', GRAMPS_FILES)) data_files_core.append(('share/gramps/images/webstuff', IMAGE_WEB)) +data_files_core.append(('share/gramps', GRAMPS_CSS)) data_files_core.append(('share/gramps/css', CSS_FILES)) data_files_core.append(('share/gramps/css/swanky-purse', SWANKY_PURSE)) data_files_core.append(('share/gramps/css/swanky-purse/images', SWANKY_IMG))