#include "addons.h"
#include <QDir>
#include <QSettings>
#include <QPushButton>
#include <QAction>
#include <QProcess>
#include <QtDebug>
#include <QRegularExpression>
#define MKSTR(x) static const QString s_ ## x(#x)
MKSTR(EVENT);
MKSTR(ADDRESS);
MKSTR(ISLOCAL);
MKSTR(CHECKED);
MKSTR(ENABLED);
MKSTR(VISIBLE);
MKSTR(connected);
MKSTR(disconnected);
MKSTR(init);
MKSTR(clicked);
MKSTR(true);
MKSTR(false);
static const QSize ICON_SIZE(20, 20);
QList<Addon*> AddonManager::_addons;
class Addon
{
public:
Addon() : button(nullptr), menu(nullptr), wantConnectInfo(false) {}
QPushButton *button;
QAction *menu;
QProcess process;
bool wantConnectInfo;
bool wantInit;
bool runAsync;
QList<QPair<QString, QString>> envir;
};
static inline bool toBool(const QString &string)
{
return string.toLower() == s_true;
}
static void executeAddon(Addon *addon);
static void setAddonVisible(Addon *addon, bool visible);
static void handleAddonOutput(Addon *addon, QRegularExpressionMatchIterator &matches);
void AddonManager::loadFromPath(const QString &path, QList<QPushButton*> &buttons, QList<QAction*> &menuEntries)
{
QDir configDir(path);
QFileInfoList fileInfoList = configDir.entryInfoList(QDir::Files, QDir::Name);
QRegularExpression paramRegex("^([A-Z]+)=(.*)$", QRegularExpression::MultilineOption);
for (QFileInfo fileInfo : fileInfoList) {
QString filePath = fileInfo.absoluteFilePath();
QSettings setting(filePath, QSettings::IniFormat);
setting.setIniCodec("UTF-8");
QString caption = setting.value("caption").toString(); // TODO: i18n
QString exec = setting.value("exec").toString();
QString type = setting.value("type").toString();
QString tooltip = setting.value("tooltip").toString();
QIcon icon(setting.value("icon").toString());
bool checkable = setting.value("checkable").toBool();
if (exec.isEmpty() || (caption.isEmpty() && icon.isNull())) {
qDebug() << "Ignoring" << filePath << "caption+icon or exec empty";
continue;
}
if (!QFileInfo(exec).isExecutable() || !QFileInfo(exec).isFile()) {
qDebug() << "Ignoring" << filePath << "since target" << exec << "doesn't exist or isn't an executable file";
continue;
}
// Alloc addon
Addon *addon = new Addon();
// Toggle/click callback
auto toggleFun = [=](bool value) {
addon->envir.append(qMakePair(s_EVENT, s_clicked));
addon->envir.append(qMakePair(s_CHECKED, (value ? s_true : s_false)));
executeAddon(addon);
};
if (type == "menu") {
addon->menu = new QAction(caption);
if (!icon.isNull()) {
addon->menu->setIcon(icon);
}
addon->menu->setCheckable(checkable);
addon->menu->setToolTip(tooltip);
menuEntries.append(addon->menu);
addon->menu->connect(addon->menu, &QAction::triggered, toggleFun);
} else if (type == "button") {
addon->button = new QPushButton(caption);
if (!icon.isNull()) {
addon->button->setIcon(icon);
addon->button->setIconSize(ICON_SIZE);
}
addon->button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
addon->button->setCheckable(checkable);
addon->button->setToolTip(tooltip);
buttons.append(addon->button);
addon->button->connect(addon->button, &QPushButton::clicked, toggleFun);
} else {
qDebug() << "Ignoring unknown addon type" << type;
delete addon;
continue;
}
_addons.append(addon);
addon->wantConnectInfo = setting.value("connection-events").toBool();
addon->wantInit = setting.value("init").toBool();
addon->runAsync = setting.value("async").toBool();
// Setup process
addon->process.setProgram(exec);
// Stdin for status updates
addon->process.connect(&addon->process, &QProcess::readyReadStandardOutput, [=]() {
auto lines = addon->process.readAllStandardOutput();
auto matches = paramRegex.globalMatch(lines);
handleAddonOutput(addon, matches);
});
// Stderr just for debugging
addon->process.connect(&addon->process, &QProcess::readyReadStandardError, [=]() {
qDebug() << exec << "stderr:" << QString::fromLocal8Bit(addon->process.readAllStandardError());
});
}
}
void AddonManager::initControls()
{
// Call all init functions
for (auto addon : _addons) {
if (addon->wantInit) {
addon->envir.append(qMakePair(s_EVENT, s_init));
executeAddon(addon);
} else {
setAddonVisible(addon, true);
}
}
}
void AddonManager::connectEvent(bool isLocal, const QString &address)
{
for (auto addon : _addons) {
if (!addon->wantConnectInfo)
continue;
addon->envir.append(qMakePair(s_EVENT, s_connected));
addon->envir.append(qMakePair(s_ADDRESS, address));
addon->envir.append(qMakePair(s_ISLOCAL, isLocal ? s_true : s_false));
executeAddon(addon);
}
}
void AddonManager::disconnectEvent()
{
for (auto addon : _addons) {
if (!addon->wantConnectInfo)
continue;
addon->envir.append(qMakePair(s_EVENT, s_disconnected));
executeAddon(addon);
}
}
static void executeAddon(Addon *addon)
{
// Set up environment
auto env = QProcessEnvironment::systemEnvironment();
for (auto e : addon->envir) {
env.insert(e.first, e.second);
}
addon->envir.clear();
if (!addon->runAsync) {
// Kill remains
if (addon->process.state() != QProcess::NotRunning) {
addon->process.waitForFinished(500);
}
if (addon->process.state() != QProcess::NotRunning) {
addon->process.close();
}
}
addon->process.setProcessEnvironment(env);
// Run
if (addon->runAsync) {
addon->process.startDetached(addon->process.program(), QStringList());
} else {
addon->process.start();
addon->process.closeWriteChannel();
}
}
static void setAddonVisible(Addon *addon, bool newValue)
{
if (addon->button != nullptr) {
QWidget *p = addon->button->parentWidget();
bool wasVisible = p != nullptr && addon->button->isVisibleTo(p);
addon->button->setVisible(newValue);
if (p != nullptr && wasVisible != newValue) {
// Visibility changed -- adjust size of toolbar
int size = (addon->button->width() + 2) * (newValue ? 1 : -1);
p->setFixedWidth(p->width() + size);
}
} else {
addon->menu->setVisible(newValue);
}
}
static void handleAddonOutput(Addon* addon, QRegularExpressionMatchIterator &matches)
{
while (matches.hasNext()) {
auto m = matches.next();
auto key = m.captured(1);
auto val = m.captured(2);
bool newValue = toBool(val);
if (key == s_VISIBLE) {
setAddonVisible(addon, newValue);
} else if (key == s_CHECKED) {
if (addon->button != nullptr) {
addon->button->setChecked(newValue);
} else {
addon->menu->setChecked(newValue);
}
} else if (key == s_ENABLED) {
if (addon->button != nullptr) {
addon->button->setEnabled(newValue);
} else {
addon->menu->setEnabled(newValue);
}
}
}
}