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)
This commit is contained in:
Damien Elmes 2017-08-28 20:51:43 +10:00
parent b0a62838b5
commit 737a8d934e
5 changed files with 244 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import io
import json import json
import re import re
import zipfile import zipfile
import markdown
from send2trash import send2trash from send2trash import send2trash
from aqt.qt import * from aqt.qt import *
@ -26,8 +27,6 @@ class AddonManager:
f = self.mw.form f = self.mw.form
f.actionAdd_ons.triggered.connect(self.onAddonsDialog) f.actionAdd_ons.triggered.connect(self.onAddonsDialog)
sys.path.insert(0, self.addonsFolder()) sys.path.insert(0, self.addonsFolder())
if not self.mw.safeMode:
self.loadAddons()
def allAddons(self): def allAddons(self):
l = [] l = []
@ -105,7 +104,8 @@ When loading '%(name)s':
name = os.path.splitext(fname)[0] name = os.path.splitext(fname)[0]
# remove old version first # previously installed?
meta = self.addonMeta(sid)
base = self.addonsFolder(sid) base = self.addonsFolder(sid)
if os.path.exists(base): if os.path.exists(base):
self.deleteAddon(sid) self.deleteAddon(sid)
@ -119,9 +119,9 @@ When loading '%(name)s':
# write # write
z.extract(n, base) z.extract(n, base)
# write metadata # update metadata
meta = dict(name=name, meta['name'] = name
mod=intTime()) meta['mod'] = intTime()
self.writeAddonMeta(sid, meta) self.writeAddonMeta(sid, meta)
def deleteAddon(self, dir): def deleteAddon(self, dir):
@ -187,6 +187,43 @@ When loading '%(name)s':
updated.append(sid) updated.append(sid)
return updated 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 # Add-ons Dialog
###################################################################### ######################################################################
@ -206,6 +243,7 @@ class AddonsDialog(QDialog):
f.viewPage.clicked.connect(self.onViewPage) f.viewPage.clicked.connect(self.onViewPage)
f.viewFiles.clicked.connect(self.onViewFiles) f.viewFiles.clicked.connect(self.onViewFiles)
f.delete_2.clicked.connect(self.onDelete) f.delete_2.clicked.connect(self.onDelete)
f.config.clicked.connect(self.onConfig)
self.redrawAddons() self.redrawAddons()
self.show() self.show()
@ -293,6 +331,25 @@ class AddonsDialog(QDialog):
self.redrawAddons() 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 # Fetching Add-ons
###################################################################### ######################################################################
@ -332,3 +389,47 @@ class GetAddons(QDialog):
self.addonsDlg.redrawAddons() self.addonsDlg.redrawAddons()
QDialog.accept(self) 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()

View File

@ -597,6 +597,8 @@ title="%s" %s>%s</button>''' % (
def setupAddons(self): def setupAddons(self):
import aqt.addons import aqt.addons
self.addonManager = aqt.addons.AddonManager(self) self.addonManager = aqt.addons.AddonManager(self)
if not self.safeMode:
self.addonManager.loadAddons()
def setupThreads(self): def setupThreads(self):
self._mainThread = QThread.currentThread() self._mainThread = QThread.currentThread()

127
designer/addonconf.ui Normal file
View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>631</width>
<height>521</height>
</rect>
</property>
<property name="windowTitle">
<string>Configuration</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>607</width>
<height>112</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="editor">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>3</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -74,6 +74,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="config">
<property name="text">
<string>Config</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="viewFiles"> <widget class="QPushButton" name="viewFiles">
<property name="text"> <property name="text">

View File

@ -4,3 +4,4 @@ httplib2
pyaudio pyaudio
requests requests
decorator decorator
markdown