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.use-tips', False)
register('behavior.welcome', 100) register('behavior.welcome', 100)
register('behavior.web-search-url', 'http://google.com/#&q=%(text)s') 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.dialect', 'excel')
register('csv.delimiter', ',') register('csv.delimiter', ',')
@ -225,7 +228,7 @@ register('interface.toolbar-on', True)
register('interface.toolbar-text', False) register('interface.toolbar-text', False)
register('interface.hide-lds', False) register('interface.hide-lds', False)
register('interface.toolbar-clipboard', True) register('interface.toolbar-clipboard', True)
register('interface.toolbar-plugin', True) register('interface.toolbar-addons', True)
register('interface.toolbar-preference', True) register('interface.toolbar-preference', True)
register('interface.toolbar-reports', True) register('interface.toolbar-reports', True)
register('interface.toolbar-tools', 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_PLACE, CATEGORY_QR_REPOSITORY, CATEGORY_QR_NOTE,
CATEGORY_QR_DATE, PTYPE_STR, CATEGORY_QR_MEDIA, CATEGORY_QR_DATE, PTYPE_STR, CATEGORY_QR_MEDIA,
CATEGORY_QR_CITATION, CATEGORY_QR_SOURCE_OR_CITATION, CATEGORY_QR_CITATION, CATEGORY_QR_SOURCE_OR_CITATION,
START, END, make_environment, START, END, make_environment, AUDIENCETEXT, STATUSTEXT,
) )
from ._import import ImportPlugin from ._import import ImportPlugin
from ._export import ExportPlugin from ._export import ExportPlugin

View File

@ -104,6 +104,14 @@ class BasePluginManager:
self.__loaded_plugins = {} self.__loaded_plugins = {}
self.__scanned_dirs = [] 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, def reg_plugins(self, direct, dbstate=None, uistate=None,
load_on_reg=False, rescan=False): load_on_reg=False, rescan=False):
""" """
@ -128,8 +136,7 @@ class BasePluginManager:
# " been_here=%s, pahte exists:%s", direct, load_on_reg, # " been_here=%s, pahte exists:%s", direct, load_on_reg,
# direct in self.__scanned_dirs, os.path.isdir(direct)) # direct in self.__scanned_dirs, os.path.isdir(direct))
if os.path.isdir(direct) and direct not in self.__scanned_dirs: if os.path.isdir(direct):
self.__scanned_dirs.append(direct)
for (dirpath, dirnames, filenames) in os.walk(direct, for (dirpath, dirnames, filenames) in os.walk(direct,
topdown=True): topdown=True):
for dirname in dirnames[:]: for dirname in dirnames[:]:
@ -138,7 +145,9 @@ class BasePluginManager:
"__pycache__"]: "__pycache__"]:
dirnames.remove(dirname) dirnames.remove(dirname)
# LOG.warning("Plugin dir scanned: %s", dirpath) # 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: if load_on_reg:
# Run plugins that request to be loaded on startup and # 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 ...version import VERSION as GRAMPSVERSION, VERSION_TUPLE
from ..utils.requirements import Requirements
from ..const import IMAGE_DIR from ..const import IMAGE_DIR
from ..const import GRAMPS_LOCALE as glocale from ..const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext _ = glocale.translation.gettext
@ -1271,6 +1272,7 @@ class PluginRegister:
self.stable_only = False self.stable_only = False
self.__plugindata = [] self.__plugindata = []
self.__id_to_pdata = {} self.__id_to_pdata = {}
self.__req = Requirements()
def add_plugindata(self, plugindata): def add_plugindata(self, plugindata):
""" This is used to add an entry to the registration list. The way it """ 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'), exec (compile(stream, filename, 'exec'),
make_environment(_=local_gettext), {'uistate': uistate}) make_environment(_=local_gettext), {'uistate': uistate})
for pdata in self.__plugindata[lenpd:]: for pdata in self.__plugindata[lenpd:]:
# should not be duplicate IDs in different plugins if pdata.id in self.__id_to_pdata:
assert pdata.id not in self.__id_to_pdata # reloading
# if pdata.id in self.__id_to_pdata: old = self.__id_to_pdata[pdata.id]
# print("Error: %s is duplicated!" % pdata.id) self.__plugindata.remove(old)
lenpd -= 1
self.__id_to_pdata[pdata.id] = pdata self.__id_to_pdata[pdata.id] = pdata
except ValueError as msg: except ValueError as msg:
print(_('ERROR: Failed reading plugin registration %(filename)s') % \ print(_('ERROR: Failed reading plugin registration %(filename)s') % \
@ -1358,6 +1361,9 @@ class PluginRegister:
)) ))
rmlist.append(ind) rmlist.append(ind)
continue continue
if not self.__req.check_plugin(plugin):
rmlist.append(ind)
continue
if not plugin.status == STABLE and self.stable_only: if not plugin.status == STABLE and self.stable_only:
rmlist.append(ind) rmlist.append(ind)
continue continue

View File

@ -31,6 +31,7 @@ import sys
import os import os
import datetime import datetime
from io import StringIO, BytesIO from io import StringIO, BytesIO
import json
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -192,8 +193,7 @@ def urlopen_maybe_no_check_cert(URL):
fp = urlopen(URL, timeout=timeout) fp = urlopen(URL, timeout=timeout)
return fp return fp
def available_updates(): def get_addons(project, url):
whattypes = config.get('behavior.check-for-addon-update-types')
LOG.debug("Checking for updated addons...") LOG.debug("Checking for updated addons...")
langs = glocale.get_language_list() langs = glocale.get_language_list()
@ -201,15 +201,13 @@ def available_updates():
# now we have a list of languages to try: # now we have a list of languages to try:
fp = None fp = None
for lang in langs: for lang in langs:
URL = ("%s/listings/addons-%s.txt" % URL = ("%s/listings/addons-%s.json" % (url, lang))
(config.get("behavior.addons-url"), lang))
LOG.debug(" trying: %s" % URL) LOG.debug(" trying: %s" % URL)
try: try:
fp = urlopen_maybe_no_check_cert(URL) fp = urlopen_maybe_no_check_cert(URL)
except: except:
try: try:
URL = ("%s/listings/addons-%s.txt" % URL = ("%s/listings/addons-%s.json" % (url, lang[:2]))
(config.get("behavior.addons-url"), lang[:2]))
fp = urlopen_maybe_no_check_cert(URL) fp = urlopen_maybe_no_check_cert(URL)
except Exception as err: # some error except Exception as err: # some error
LOG.warning("Failed to open addon metadata for {lang} {url}: {err}". 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): if fp and (fp.getcode() == 200 or fp.file):
break break
pmgr = BasePluginManager.get_instance() addon_list = []
addon_update_list = []
if fp and (fp.getcode() == 200 or fp.file): if fp and (fp.getcode() == 200 or fp.file):
lines = list(fp.readlines()) addon_list = json.load(fp)
count = 0 for plugin_dict in addon_list:
for line in lines: if 'a' not in plugin_dict:
line = line.decode('utf-8') plugin_dict['a'] = 0
try: if 's' not in plugin_dict:
plugin_dict = safe_eval(line) plugin_dict['s'] = 0
if type(plugin_dict) != type({}): if 'h' not in plugin_dict:
raise TypeError("Line with addon metadata is not " plugin_dict['h'] = ''
"a dictionary") plugin_dict['_p'] = project
except: plugin_dict['_u'] = url
LOG.warning("Skipped a line in the addon listing: " +
str(line))
continue
id = plugin_dict["i"] id = plugin_dict["i"]
pmgr = BasePluginManager.get_instance()
plugin = pmgr.get_plugin(id) plugin = pmgr.get_plugin(id)
if plugin: if plugin:
LOG.debug("Comparing %s > %s" % plugin_dict['_v'] = plugin.version
(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()
else: else:
LOG.debug("Checking Addons Failed") LOG.debug("Checking Addons Failed")
LOG.debug("Done checking!") 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 return addon_update_list
def load_addon_file(path, callback=None): 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 = ( page_funcs = (
self.add_data_panel, self.add_data_panel,
self.add_general_panel, self.add_general_panel,
self.add_addons_panel,
self.add_famtree_panel, self.add_famtree_panel,
self.add_import_panel, self.add_import_panel,
self.add_limits_panel, self.add_limits_panel,
@ -1543,29 +1542,12 @@ class GrampsPreferences(ConfigureDialog):
self.pformat.set_model(model) self.pformat.set_model(model)
self.pformat.set_active(0) 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): def toggle_tag_on_import(self, obj):
""" """
Update Entry sensitive for tag on import. Update Entry sensitive for tag on import.
""" """
self.tag_format_entry.set_sensitive(obj.get_active()) 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): def date_format_changed(self, obj):
""" """
Save "Date format" option. Save "Date format" option.
@ -1728,11 +1710,11 @@ class GrampsPreferences(ConfigureDialog):
extra_callback=self.cb_toolbar_changed) extra_callback=self.cb_toolbar_changed)
row += 1 row += 1
# Show Plugins Icon: # Show Addons Icon:
self.add_checkbox( self.add_checkbox(
grid, _("Show Plugins icon on toolbar"), grid, _("Show Addons icon on toolbar"),
row, 'interface.toolbar-plugin', start=1, stop=3, row, 'interface.toolbar-addons', start=1, stop=3,
tooltip=_("Show or hide the Plugins icon on the toolbar."), tooltip=_("Show or hide the Addons icon on the toolbar."),
extra_callback=self.cb_toolbar_changed) extra_callback=self.cb_toolbar_changed)
row += 1 row += 1
@ -1794,101 +1776,6 @@ class GrampsPreferences(ConfigureDialog):
return _('General'), grid 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): def database_backend_changed(self, obj):
""" """
Update Database Backend. Update Database Backend.

View File

@ -409,10 +409,10 @@ TOOLS = {
'win.Tools', 'win.Tools',
_('Open the tools dialog'), _('Open the tools dialog'),
_('Tools')), _('Tools')),
'plugin': ('gramps-plugin-manager', 'addons': ('gramps-addon',
'win.PluginStatus', 'win.AddonManager',
_('Open Plugin Manager'), _('Open Addon Manager'),
_('Plugins')), _('Addons')),
'preference': ('gramps-preferences', 'preference': ('gramps-preferences',
'app.preferences', 'app.preferences',
_('Open Preferences'), _('Open Preferences'),

View File

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

View File

@ -28,6 +28,10 @@
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
import traceback import traceback
import os 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 Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Pango from gi.repository import Pango
from gi.repository import GObject from gi.repository import GObject
@ -57,7 +62,8 @@ _ = glocale.translation.gettext
ngettext = glocale.translation.ngettext # else "nearby" comments are ignored ngettext = glocale.translation.ngettext # else "nearby" comments are ignored
from ..managedwindow import ManagedWindow from ..managedwindow import ManagedWindow
from gramps.gen.errors import UnavailableError, WindowActiveError 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 ..utils import open_file_with_default_application
from ..pluginmanager import GuiPluginManager from ..pluginmanager import GuiPluginManager
from . import tool from . import tool
@ -66,11 +72,17 @@ from ..dialog import InfoDialog, OkDialog
from ..editors import EditPerson from ..editors import EditPerson
from ..glade import Glade from ..glade import Glade
from ..listmodel import ListModel, NOSORT, TOGGLE 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 gramps.gen.config import config
from ..widgets.progressdialog import (LongOpStatus, ProgressMonitor, from ..widgets.progressdialog import (LongOpStatus, ProgressMonitor,
GtkProgressDialog) 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): def display_message(message):
""" """
A default callback for displaying messages. 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 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):
'''
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)
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #

View File

@ -79,7 +79,7 @@ from gramps.gen.relationship import get_relationship_calculator
from .displaystate import DisplayState, RecentDocsMenu from .displaystate import DisplayState, RecentDocsMenu
from gramps.gen.const import (USER_DATA, ICON, URL_BUGTRACKER, URL_HOMEPAGE, from gramps.gen.const import (USER_DATA, ICON, URL_BUGTRACKER, URL_HOMEPAGE,
URL_MAILINGLIST, URL_MANUAL_PAGE, URL_WIKISTRING, 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.constfunc import is_quartz
from gramps.gen.config import config from gramps.gen.config import config
from gramps.gen.errors import WindowActiveError from gramps.gen.errors import WindowActiveError
@ -267,6 +267,8 @@ class ViewManager(CLIManager):
self.window.set_default_size(width, height) self.window.set_default_size(width, height)
self.window.move(horiz_position, vert_position) self.window.move(horiz_position, vert_position)
self.load_css()
self.provider = Gtk.CssProvider() self.provider = Gtk.CssProvider()
self.change_font(font) self.change_font(font)
@ -382,6 +384,7 @@ class ViewManager(CLIManager):
('ExtraPlugins', extra_plugins_activate), ('ExtraPlugins', extra_plugins_activate),
#('about', self.display_about_box), #('about', self.display_about_box),
('PluginStatus', self.__plugin_status), ('PluginStatus', self.__plugin_status),
('AddonManager', self.__addon_manager),
('FAQ', faq_activate), ('FAQ', faq_activate),
('KeyBindings', key_bindings), ('KeyBindings', key_bindings),
('UserManual', manual_activate, 'F1'), ('UserManual', manual_activate, 'F1'),
@ -679,6 +682,13 @@ class ViewManager(CLIManager):
except WindowActiveError: except WindowActiveError:
return 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): def reset_font(self):
""" """
Reset to the default application font. Reset to the default application font.
@ -722,6 +732,15 @@ class ViewManager(CLIManager):
except WindowActiveError: except WindowActiveError:
pass 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): def navigator_toggle(self, action, value):
""" """
Set the sidebar based on the value of the toggle button. Save the Set the sidebar based on the value of the toggle button. Save the

View File

Before

Width:  |  Height:  |  Size: 831 B

After

Width:  |  Height:  |  Size: 831 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -375,6 +375,7 @@ gramps/gen/utils/image.py
gramps/gen/utils/keyword.py gramps/gen/utils/keyword.py
gramps/gen/utils/lds.py gramps/gen/utils/lds.py
gramps/gen/utils/place.py gramps/gen/utils/place.py
gramps/gen/utils/requirements.py
gramps/gen/utils/string.py gramps/gen/utils/string.py
gramps/gen/utils/symbols.py gramps/gen/utils/symbols.py
gramps/gen/utils/unknown.py gramps/gen/utils/unknown.py

View File

@ -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 = 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','*.ico')))
IMAGE_WEB.extend(glob.glob(os.path.join('images', 'webstuff', '*.gif'))) 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')) CSS_FILES = glob.glob(os.path.join('data', 'css', '*.css'))
SWANKY_PURSE = glob.glob(os.path.join('data', 'css', 'swanky-purse', '*.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')) 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/gedcom', GEDCOM_FILES))
data_files_core.append(('share/doc/gramps/example/gramps', GRAMPS_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/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', CSS_FILES))
data_files_core.append(('share/gramps/css/swanky-purse', SWANKY_PURSE)) data_files_core.append(('share/gramps/css/swanky-purse', SWANKY_PURSE))
data_files_core.append(('share/gramps/css/swanky-purse/images', SWANKY_IMG)) data_files_core.append(('share/gramps/css/swanky-purse/images', SWANKY_IMG))