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))