Create new Addon Manager dialog

This commit is contained in:
Nick Hall 2021-12-10 23:43:40 +00:00
parent f026cfb720
commit 2daa53025e
19 changed files with 1036 additions and 186 deletions

16
data/gramps.css Normal file
View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -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.

View File

@ -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'),

View File

@ -173,6 +173,12 @@ UIDEFAULT = (
<attribute name="action">win.Clipboard</attribute>
<attribute name="label" translatable="yes">Clip_board</attribute>
</item>
<item>
<attribute name="action">win.AddonManager</attribute>
<attribute name="label" translatable="yes">'''
'''_Addon Manager...</attribute>
</item>
</section>
<section>
<placeholder groups='OSX' id='osxpref'>

View File

@ -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('<span weight="bold">%s</span>' % 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('<span size="larger" weight="bold">%s</span>' % 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('<span size="larger" weight="bold">%s</span>' % 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('<span weight="bold">%s</span>' % 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('<span weight="bold">%s</span>' % 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):
'''
<