Lua: Cleanup InfoBar entries

Also changes InfoBarDisplay::update to completely disconnect the resulting
widgets when an entry is removed.

Change-Id: Ic32ee8a1c9ee8dcd026e4a0cb7521b07323ca892
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Marcus Tillmanns
2025-01-28 15:49:11 +01:00
parent aafded8d33
commit 49fe642721
5 changed files with 214 additions and 166 deletions

View File

@@ -264,10 +264,20 @@ void InfoBarDisplay::infoBarDestroyed()
// will delete the widgets itself) or setInfoBar() being called explicitly.
}
static void disconnectRecursively(QObject *obj)
{
obj->disconnect();
for (QObject *child : obj->children())
disconnectRecursively(child);
}
void InfoBarDisplay::update()
{
for (QWidget *widget : std::as_const(m_infoWidgets)) {
widget->disconnect(this); // We want no destroyed() signal now
// Make sure that we are no longer connect to anything (especially lambdas).
// Otherwise a lambda might live longer than the owner of the lambda.
disconnectRecursively(widget);
widget->hide(); // Late deletion can cause duplicate infos. Hide immediately to prevent it.
widget->deleteLater();
}

View File

@@ -4,6 +4,8 @@
#include "../luaengine.h"
#include "../luatr.h"
#include "utils.h"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
@@ -135,188 +137,199 @@ void setupFetchModule()
std::shared_ptr<Module> module = std::make_shared<Module>();
registerProvider("Fetch", [mod = std::move(module)](sol::state_view lua) -> sol::object {
const ScriptPluginSpec *pluginSpec = lua.get<ScriptPluginSpec *>("PluginSpec");
registerProvider(
"Fetch",
[mod = std::move(module),
infoBarCleaner = InfoBarCleaner()](sol::state_view lua) mutable -> sol::object {
const ScriptPluginSpec *pluginSpec = lua.get<ScriptPluginSpec *>("PluginSpec");
sol::table async = lua.script("return require('async')", "_fetch_").get<sol::table>();
sol::function wrap = async["wrap"];
sol::table async = lua.script("return require('async')", "_fetch_").get<sol::table>();
sol::function wrap = async["wrap"];
sol::table fetch = lua.create_table();
sol::table fetch = lua.create_table();
auto networkReplyType = lua.new_usertype<QNetworkReply>(
"QNetworkReply",
"error",
sol::property([](QNetworkReply *self) -> int { return self->error(); }),
"readAll",
[](QNetworkReply *r) { return r->readAll().toStdString(); },
"__tostring",
[](QNetworkReply *r) {
return QString("QNetworkReply(%1 \"%2\") => %3")
.arg(opToString(r->operation()))
.arg(r->url().toString())
.arg(r->error());
});
auto networkReplyType = lua.new_usertype<QNetworkReply>(
"QNetworkReply",
"error",
sol::property([](QNetworkReply *self) -> int { return self->error(); }),
"readAll",
[](QNetworkReply *r) { return r->readAll().toStdString(); },
"__tostring",
[](QNetworkReply *r) {
return QString("QNetworkReply(%1 \"%2\") => %3")
.arg(opToString(r->operation()))
.arg(r->url().toString())
.arg(r->error());
});
auto checkPermission = [mod,
pluginName = pluginSpec->name,
guard = pluginSpec->connectionGuard.get()](
QString url,
std::function<void()> fetch,
std::function<void()> notAllowed) {
auto isAllowed = mod->isAllowedToFetch(pluginName);
if (isAllowed == Module::IsAllowed::Yes) {
fetch();
return;
}
if (isAllowed == Module::IsAllowed::No) {
notAllowed();
return;
}
if (QApplication::activeModalWidget()) {
// We are already showing a modal dialog,
// so we have to use a QMessageBox instead of the info bar
auto msgBox = new QMessageBox(
QMessageBox::Question,
Tr::tr("Allow Internet Access"),
Tr::tr("Allow the extension \"%1\" to fetch from the following URL:\n%2")
.arg(pluginName)
.arg(url),
QMessageBox::Yes | QMessageBox::No,
ICore::dialogParent());
msgBox->setCheckBox(new QCheckBox(Tr::tr("Remember choice")));
QObject::connect(
msgBox, &QMessageBox::accepted, guard, [mod, fetch, pluginName, msgBox]() {
if (msgBox->checkBox()->isChecked())
mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes);
fetch();
});
QObject::connect(
msgBox, &QMessageBox::rejected, guard, [mod, notAllowed, pluginName, msgBox]() {
if (msgBox->checkBox()->isChecked())
mod->setAllowedToFetch(pluginName, Module::IsAllowed::No);
notAllowed();
});
msgBox->show();
return;
}
InfoBarEntry entry{
Id("Fetch").withSuffix(pluginName),
Tr::tr("Allow the extension \"%1\" to fetch data from the internet?")
.arg(pluginName)};
entry.setDetailsWidgetCreator([pluginName, url] {
const QString markdown = Tr::tr("Allow the extension \"%1\" to fetch data"
"from the following URL:\n\n")
.arg("**" + pluginName + "**")
+ QString("* [%1](%1)").arg(url);
QLabel *list = new QLabel();
list->setTextFormat(Qt::TextFormat::MarkdownText);
list->setText(markdown);
list->setMargin(StyleHelper::SpacingTokens::ExPaddingGapS);
return list;
});
entry.addCustomButton(Tr::tr("Always Allow"), [mod, pluginName, fetch]() {
mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes);
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
fetch();
});
entry.addCustomButton(Tr::tr("Allow Once"), [pluginName, fetch]() {
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
fetch();
});
entry.setCancelButtonInfo(Tr::tr("Deny"), [mod, notAllowed, pluginName]() {
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
mod->setAllowedToFetch(pluginName, Module::IsAllowed::No);
notAllowed();
});
ICore::infoBar()->addInfo(entry);
};
fetch["fetch_cb"] = [checkPermission,
pluginName = pluginSpec->name,
guard = pluginSpec->connectionGuard.get(),
mod](
const sol::table &options,
const sol::function &callback,
const sol::this_state &thisState) {
auto url = options.get<QString>("url");
auto actualFetch = [guard, url, options, callback, thisState]() {
auto method = (options.get_or<QString>("method", "GET")).toLower();
auto headers = options.get_or<sol::table>("headers", {});
auto data = options.get_or<QString>("body", {});
bool convertToTable
= options.get<std::optional<bool>>("convertToTable").value_or(false);
QNetworkRequest request((QUrl(url)));
if (headers && !headers.empty()) {
for (const auto &[k, v] : headers)
request.setRawHeader(k.as<QString>().toUtf8(), v.as<QString>().toUtf8());
auto checkPermission = [mod,
&infoBarCleaner,
pluginName = pluginSpec->name,
guard = pluginSpec->connectionGuard.get()](
QString url,
std::function<void()> fetch,
std::function<void()> notAllowed) {
auto isAllowed = mod->isAllowedToFetch(pluginName);
if (isAllowed == Module::IsAllowed::Yes) {
fetch();
return;
}
QNetworkReply *reply = nullptr;
if (method == "get")
reply = NetworkAccessManager::instance()->get(request);
else if (method == "post")
reply = NetworkAccessManager::instance()->post(request, data.toUtf8());
else
throw std::runtime_error("Unknown method: " + method.toStdString());
if (isAllowed == Module::IsAllowed::No) {
notAllowed();
return;
}
if (QApplication::activeModalWidget()) {
// We are already showing a modal dialog,
// so we have to use a QMessageBox instead of the info bar
auto msgBox = new QMessageBox(
QMessageBox::Question,
Tr::tr("Allow Internet Access"),
Tr::tr("Allow the extension \"%1\" to fetch from the following URL:\n%2")
.arg(pluginName)
.arg(url),
QMessageBox::Yes | QMessageBox::No,
ICore::dialogParent());
msgBox->setCheckBox(new QCheckBox(Tr::tr("Remember choice")));
if (convertToTable) {
QObject::connect(
reply, &QNetworkReply::finished, guard, [reply, thisState, callback]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
callback(QString("%1 (%2):\n%3")
.arg(reply->errorString())
.arg(QString::fromLatin1(
QMetaEnum::fromType<QNetworkReply::NetworkError>()
.valueToKey(reply->error())))
.arg(QString::fromUtf8(reply->readAll())));
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
callback(error.errorString());
return;
}
callback(toTable(thisState, doc));
msgBox, &QMessageBox::accepted, guard, [mod, fetch, pluginName, msgBox]() {
if (msgBox->checkBox()->isChecked())
mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes);
fetch();
});
} else {
QObject::connect(
reply, &QNetworkReply::finished, guard, [reply, callback]() {
msgBox,
&QMessageBox::rejected,
guard,
[mod, notAllowed, pluginName, msgBox]() {
if (msgBox->checkBox()->isChecked())
mod->setAllowedToFetch(pluginName, Module::IsAllowed::No);
notAllowed();
});
msgBox->show();
return;
}
const Id infoBarId = Id("Fetch").withSuffix(pluginName);
infoBarCleaner.infoBarEntryAdded(infoBarId);
InfoBarEntry entry{
infoBarId,
Tr::tr("Allow the extension \"%1\" to fetch data from the internet?")
.arg(pluginName)};
entry.setDetailsWidgetCreator([pluginName, url] {
const QString markdown = Tr::tr("Allow the extension \"%1\" to fetch data"
"from the following URL:\n\n")
.arg("**" + pluginName + "**")
+ QString("* [%1](%1)").arg(url);
QLabel *list = new QLabel();
list->setTextFormat(Qt::TextFormat::MarkdownText);
list->setText(markdown);
list->setMargin(StyleHelper::SpacingTokens::ExPaddingGapS);
return list;
});
entry.addCustomButton(Tr::tr("Always Allow"), [mod, pluginName, fetch]() {
mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes);
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
fetch();
});
entry.addCustomButton(Tr::tr("Allow Once"), [pluginName, fetch]() {
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
fetch();
});
entry.setCancelButtonInfo(Tr::tr("Deny"), [mod, notAllowed, pluginName]() {
ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName));
mod->setAllowedToFetch(pluginName, Module::IsAllowed::No);
notAllowed();
});
ICore::infoBar()->addInfo(entry);
};
fetch["fetch_cb"] = [checkPermission,
pluginName = pluginSpec->name,
guard = pluginSpec->connectionGuard.get(),
mod](
const sol::main_table &options,
const sol::main_function &callback,
const sol::this_state &thisState) {
auto url = options.get<QString>("url");
auto actualFetch = [guard, url, options, callback, thisState]() {
auto method = (options.get_or<QString>("method", "GET")).toLower();
auto headers = options.get_or<sol::table>("headers", {});
auto data = options.get_or<QString>("body", {});
bool convertToTable
= options.get<std::optional<bool>>("convertToTable").value_or(false);
QNetworkRequest request((QUrl(url)));
if (headers && !headers.empty()) {
for (const auto &[k, v] : headers)
request.setRawHeader(k.as<QString>().toUtf8(), v.as<QString>().toUtf8());
}
QNetworkReply *reply = nullptr;
if (method == "get")
reply = NetworkAccessManager::instance()->get(request);
else if (method == "post")
reply = NetworkAccessManager::instance()->post(request, data.toUtf8());
else
throw std::runtime_error("Unknown method: " + method.toStdString());
if (convertToTable) {
QObject::connect(
reply, &QNetworkReply::finished, guard, [reply, thisState, callback]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
callback(
QString("%1 (%2):\n%3")
.arg(reply->errorString())
.arg(QString::fromLatin1(
QMetaEnum::fromType<QNetworkReply::NetworkError>()
.valueToKey(reply->error())))
.arg(QString::fromUtf8(reply->readAll())));
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
callback(error.errorString());
return;
}
callback(toTable(thisState, doc));
});
} else {
QObject::connect(reply, &QNetworkReply::finished, guard, [reply, callback]() {
// We don't want the network reply to be deleted by the manager, but
// by the Lua GC
reply->setParent(nullptr);
callback(std::unique_ptr<QNetworkReply>(reply));
});
}
}
};
checkPermission(url, actualFetch, [callback, pluginName]() {
callback(
Tr::tr("Fetching is not allowed for the extension \"%1\". (You can edit "
"permissions in Preferences > Lua.)")
.arg(pluginName));
});
};
checkPermission(url, actualFetch, [callback, pluginName]() {
callback(Tr::tr("Fetching is not allowed for the extension \"%1\". (You can edit "
"permissions in Preferences > Lua.)")
.arg(pluginName));
});
};
fetch["fetch"] = wrap(fetch["fetch_cb"]);
fetch["fetch"] = wrap(fetch["fetch_cb"]);
return fetch;
});
return fetch;
});
}
} // namespace Lua::Internal

View File

@@ -4,6 +4,8 @@
#include "../luaengine.h"
#include "../luatr.h"
#include "utils.h"
#include <coreplugin/icore.h>
#include <coreplugin/progressmanager/progressmanager.h>
#include <coreplugin/progressmanager/taskprogress.h>
@@ -255,7 +257,9 @@ void setupInstallModule()
};
registerProvider(
"Install", [state = State()](sol::state_view lua) mutable -> sol::object {
"Install",
[state = State(),
infoBarCleaner = InfoBarCleaner()](sol::state_view lua) mutable -> sol::object {
sol::table async
= lua.script("return require('async')", "_install_async_").get<sol::table>();
sol::function wrap = async["wrap"];
@@ -281,7 +285,7 @@ void setupInstallModule()
};
install["install_cb"] =
[pluginSpec, &state](
[pluginSpec, &state, &infoBarCleaner](
const QString &msg,
const sol::table &installOptions,
const sol::function &callback) {
@@ -354,6 +358,8 @@ void setupInstallModule()
.withSuffix(pluginSpec->name)
.withSuffix(QString::number(qHash(installOptionsList)));
infoBarCleaner.infoBarEntryAdded(infoBarId);
InfoBarEntry entry(infoBarId, msg, InfoBarEntry::GlobalSuppression::Enabled);
entry.addCustomButton(Tr::tr("Install"), [install, infoBarId]() {

View File

@@ -4,6 +4,8 @@
#include "../luaengine.h"
#include "../luaqttypes.h"
#include <coreplugin/icore.h>
#include "utils.h"
#include <utils/async.h>
@@ -12,6 +14,7 @@
#include <utils/hostosinfo.h>
#include <utils/icon.h>
#include <utils/id.h>
#include <utils/infobar.h>
#include <utils/processinterface.h>
#include <QDesktopServices>
@@ -318,4 +321,10 @@ void setupUtilsModule()
});
}
InfoBarCleaner::~InfoBarCleaner()
{
for (const auto &id : openInfoBars)
Core::ICore::infoBar()->removeInfo(id);
}
} // namespace Lua::Internal

View File

@@ -7,6 +7,7 @@
#include <utils/filepath.h>
#include <utils/icon.h>
#include <utils/id.h>
#include <QMetaEnum>
@@ -61,4 +62,13 @@ inline QFlags<E> tableToFlags(const sol::table &table) noexcept {
return flags;
}
class InfoBarCleaner
{
QList<Utils::Id> openInfoBars;
public:
~InfoBarCleaner();
void infoBarEntryAdded(const Utils::Id &id) { openInfoBars.append(id); }
};
} // namespace Lua::Internal