From 737a8d934e8d3c20f4a24c268c94e421377893b2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 28 Aug 2017 20:51:43 +1000 Subject: [PATCH] persistent add-on configuration - add-ons can ship default config in a config.json file - users can edit the config in the add-ons dialog, easily syntax-check the json, and restore it to the defaults - an optional config.md contains instructions to the user in markdown format - config will be preserved when add-on is updated, instead of being overwritten as is the case when users are required to edit the source files A simple example: in config.json: {"myvar": 5} In your add-on's code: from aqt import mw config = mw.addonManager.getConfig(__name__) print("var is", config['myvar']) Add-ons that manage options in their own GUI can have that GUI displayed when the config button is clicked: mw.addonManager.setConfigAction(__name__, myOptionsFunc) --- aqt/addons.py | 113 +++++++++++++++++++++++++++++++++++-- aqt/main.py | 2 + designer/addonconf.ui | 127 ++++++++++++++++++++++++++++++++++++++++++ designer/addons.ui | 7 +++ requirements.txt | 1 + 5 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 designer/addonconf.ui diff --git a/aqt/addons.py b/aqt/addons.py index 40457dc45..324e76fd5 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -5,6 +5,7 @@ import io import json import re import zipfile +import markdown from send2trash import send2trash from aqt.qt import * @@ -26,8 +27,6 @@ class AddonManager: f = self.mw.form f.actionAdd_ons.triggered.connect(self.onAddonsDialog) sys.path.insert(0, self.addonsFolder()) - if not self.mw.safeMode: - self.loadAddons() def allAddons(self): l = [] @@ -105,7 +104,8 @@ When loading '%(name)s': name = os.path.splitext(fname)[0] - # remove old version first + # previously installed? + meta = self.addonMeta(sid) base = self.addonsFolder(sid) if os.path.exists(base): self.deleteAddon(sid) @@ -119,9 +119,9 @@ When loading '%(name)s': # write z.extract(n, base) - # write metadata - meta = dict(name=name, - mod=intTime()) + # update metadata + meta['name'] = name + meta['mod'] = intTime() self.writeAddonMeta(sid, meta) def deleteAddon(self, dir): @@ -187,6 +187,43 @@ When loading '%(name)s': updated.append(sid) return updated + # Add-on Config + ###################################################################### + + _configButtonActions = {} + + def addonConfigDefaults(self, dir): + path = os.path.join(self.addonsFolder(dir), "config.json") + try: + return json.load(open(path, encoding="utf8")) + except: + return None + + def addonConfigHelp(self, dir): + path = os.path.join(self.addonsFolder(dir), "config.md") + if os.path.exists(path): + return markdown.markdown(open(path).read()) + else: + return "" + + def getConfig(self, module): + addon = module.split(".")[0] + meta = self.addonMeta(addon) + if meta.get("config"): + return meta["config"] + return self.addonConfigDefaults(addon) + + def configAction(self, addon): + return self._configButtonActions.get(addon) + + def setConfigAction(self, addon, fn): + self._configButtonActions[addon] = fn + + def writeConfig(self, addon, conf): + meta = self.addonMeta(addon) + meta['config'] = conf + self.writeAddonMeta(addon, meta) + # Add-ons Dialog ###################################################################### @@ -206,6 +243,7 @@ class AddonsDialog(QDialog): f.viewPage.clicked.connect(self.onViewPage) f.viewFiles.clicked.connect(self.onViewFiles) f.delete_2.clicked.connect(self.onDelete) + f.config.clicked.connect(self.onConfig) self.redrawAddons() self.show() @@ -293,6 +331,25 @@ class AddonsDialog(QDialog): self.redrawAddons() + def onConfig(self): + addon = self.onlyOneSelected() + if not addon: + return + + # does add-on manage its own config? + act = self.mgr.configAction(addon) + if act: + act() + return + + conf = self.mgr.getConfig(addon) + if conf is None: + showInfo(_("Add-on has no configuration.")) + return + + ConfigEditor(self, addon, conf) + + # Fetching Add-ons ###################################################################### @@ -332,3 +389,47 @@ class GetAddons(QDialog): self.addonsDlg.redrawAddons() QDialog.accept(self) + +# Editing config +###################################################################### + +class ConfigEditor(QDialog): + + def __init__(self, dlg, addon, conf): + super().__init__(dlg) + self.addon = addon + self.conf = conf + self.mgr = dlg.mgr + self.form = aqt.forms.addonconf.Ui_Dialog() + self.form.setupUi(self) + restore = self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults) + restore.clicked.connect(self.onRestoreDefaults) + self.updateHelp() + self.updateText() + self.show() + + def onRestoreDefaults(self): + self.conf = self.mgr.addonConfigDefaults(self.addon) + self.updateText() + + def updateHelp(self): + txt = self.mgr.addonConfigHelp(self.addon) + if txt: + self.form.label.setText(txt) + else: + self.form.scrollArea.setVisible(False) + + def updateText(self): + self.form.editor.setPlainText( + json.dumps(self.conf,sort_keys=True,indent=4, separators=(',', ': '))) + + def accept(self): + txt = self.form.editor.toPlainText() + try: + self.conf = json.loads(txt) + except Exception as e: + showInfo(_("Invalid configuration: ") + repr(e)) + return + + self.mgr.writeConfig(self.addon, self.conf) + super().accept() diff --git a/aqt/main.py b/aqt/main.py index 1b1cd3eeb..26a814afe 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -597,6 +597,8 @@ title="%s" %s>%s''' % ( def setupAddons(self): import aqt.addons self.addonManager = aqt.addons.AddonManager(self) + if not self.safeMode: + self.addonManager.loadAddons() def setupThreads(self): self._mainThread = QThread.currentThread() diff --git a/designer/addonconf.ui b/designer/addonconf.ui new file mode 100644 index 000000000..e2a688d63 --- /dev/null +++ b/designer/addonconf.ui @@ -0,0 +1,127 @@ + + + Dialog + + + Qt::ApplicationModal + + + + 0 + 0 + 631 + 521 + + + + Configuration + + + + + + + 0 + 1 + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 607 + 112 + + + + + 0 + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + + + + + + + + 0 + 3 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/designer/addons.ui b/designer/addons.ui index b962c7bb9..eb7144608 100644 --- a/designer/addons.ui +++ b/designer/addons.ui @@ -74,6 +74,13 @@ + + + + Config + + + diff --git a/requirements.txt b/requirements.txt index ba519212a..dd6379d2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ httplib2 pyaudio requests decorator +markdown