diff options
| author | saturneric <[email protected]> | 2026-09-23 23:07:07 +0200 |
|---|---|---|
| committer | saturneric <[email protected]> | 2026-09-23 23:07:07 +0200 |
| commit | d06e0d6f50afbb8254fa228e03befcaac4511ad2 (patch) | |
| tree | 0f9a1a8139b9f7c8fec5552484dcc69a99c0e760 | |
| parent | feat(gpg-info): expose GnuPG details through scripted UI (diff) | |
| download | modules-d06e0d6f50afbb8254fa228e03befcaac4511ad2.tar.gz modules-d06e0d6f50afbb8254fa228e03befcaac4511ad2.zip | |
feat(ui): migrate modules to command mounts
- replace legacy menu and settings events with typed commands
- add Lua scripts for module UI placement and native widget mounts
- migrate key server, inspector, and update dialogs to host-owned views
- route module settings through storage capabilities
- update lifecycle hooks, event manifests, and widget interfaces
- fix key server task cleanup and theme color resolution
21 files changed, 488 insertions, 691 deletions
diff --git a/src/m_key_server_sync/CMakeLists.txt b/src/m_key_server_sync/CMakeLists.txt index fdaef92..df12fc1 100644 --- a/src/m_key_server_sync/CMakeLists.txt +++ b/src/m_key_server_sync/CMakeLists.txt @@ -27,4 +27,5 @@ # capabilities and translation context -- lives in module.json beside this file. gf_add_module( NAME key_server_sync - QT Core Widgets Network) + QT Core Widgets Network + LUA_SCRIPTS ui/main.lua) diff --git a/src/m_key_server_sync/KeyServerList.cpp b/src/m_key_server_sync/KeyServerList.cpp index b4267d7..160d57c 100644 --- a/src/m_key_server_sync/KeyServerList.cpp +++ b/src/m_key_server_sync/KeyServerList.cpp @@ -32,28 +32,32 @@ #include <QJsonDocument> #include <QMutex> #include <QMutexLocker> -#include <QSettings> #include "GFModule.h" #include "GFSDKUI.h" namespace { -constexpr auto kSchemaVersionKey = "key_server_sync/schema_version"; -constexpr auto kServersKey = "key_server_sync/servers"; -constexpr auto kDefaultServerKey = "key_server_sync/default_server"; +// Relative: the Host places them in this module's own settings group, which +// is where they always were ("key_server_sync/..."), so nothing moves. +constexpr auto kSchemaVersionKey = "schema_version"; +constexpr auto kServersKey = "servers"; +constexpr auto kDefaultServerKey = "default_server"; constexpr auto kSchemaVersion = 1; constexpr auto kOpenPGPServer = "https://keys.openpgp.org"; -/// The settings object is shared with the host and with the network task -/// runner, and QSettings is only reentrant, so every read-modify-write of our -/// keys has to be serialized. +/// A read-modify-write of the list is two calls; the mutex keeps two of this +/// module's own threads from interleaving them. Every call reaches the Host's +/// settings through the storage capability -- no settings object is shared. Q_GLOBAL_STATIC(QMutex, settings_mutex) -auto GlobalSettings() -> QSettings* { - return qobject_cast<QSettings*>( - static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); +auto Get(const char* key) -> QVariant { + return gf::sdk::Setting(GFModuleSdkContext(), GF_SETTING_MODULE, key); +} + +void Put(const char* key, const QVariant& value) { + gf::sdk::SetSetting(GFModuleSdkContext(), GF_SETTING_MODULE, key, value); } /** @@ -92,10 +96,7 @@ auto ResolveDefaultUrl(const QList<KeyServerEntry>& entries) -> QString { QString stored; { QMutexLocker locker(settings_mutex()); - auto* settings = GlobalSettings(); - if (settings != nullptr) { - stored = settings->value(kDefaultServerKey).toString(); - } + stored = Get(kDefaultServerKey).toString(); } const auto listed = std::any_of( @@ -155,18 +156,8 @@ namespace KeyServerList { auto Load() -> QList<KeyServerEntry> { QMutexLocker locker(settings_mutex()); - auto* settings = GlobalSettings(); - if (settings == nullptr) { - LOG_ERROR("global settings unavailable, falling back to seed key servers"); - return SeedEntries(); - } - - // The host writes through its own short-lived QSettings objects, and this one - // outlives them all; without a sync it would keep serving whatever it read at - // startup. - settings->sync(); - - if (!settings->contains(kServersKey)) { + const auto stored = Get(kServersKey); + if (!stored.isValid()) { locker.unlock(); auto seed = SeedEntries(); Store(seed, kOpenPGPServer); @@ -174,7 +165,7 @@ auto Load() -> QList<KeyServerEntry> { } const auto document = - QJsonDocument::fromJson(settings->value(kServersKey).toString().toUtf8()); + QJsonDocument::fromJson(stored.toString().toUtf8()); QList<KeyServerEntry> entries; if (document.isArray()) { @@ -202,12 +193,6 @@ auto Load() -> QList<KeyServerEntry> { void Store(const QList<KeyServerEntry>& entries, const QString& default_url) { QMutexLocker locker(settings_mutex()); - auto* settings = GlobalSettings(); - if (settings == nullptr) { - LOG_ERROR("global settings unavailable, key server list not stored"); - return; - } - QJsonArray array; for (const auto& entry : entries) { if (entry.url.isEmpty()) continue; @@ -225,15 +210,10 @@ void Store(const QList<KeyServerEntry>& entries, const QString& default_url) { resolved_default = entries.isEmpty() ? QString() : entries.first().url; } - settings->setValue(kSchemaVersionKey, kSchemaVersion); - settings->setValue( - kServersKey, - QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact))); - settings->setValue(kDefaultServerKey, resolved_default); - - // Our QSettings is a different object from the host's, so without this the - // host would keep reading the previous values. - settings->sync(); + Put(kSchemaVersionKey, kSchemaVersion); + Put(kServersKey, QString::fromUtf8( + QJsonDocument(array).toJson(QJsonDocument::Compact))); + Put(kDefaultServerKey, resolved_default); } auto DefaultUrl() -> QString { return ResolveDefaultUrl(Load()); } diff --git a/src/m_key_server_sync/KeyServerSettingsPage.cpp b/src/m_key_server_sync/KeyServerSettingsPage.cpp index 6ffe576..48c6494 100644 --- a/src/m_key_server_sync/KeyServerSettingsPage.cpp +++ b/src/m_key_server_sync/KeyServerSettingsPage.cpp @@ -87,17 +87,18 @@ KeyServerSettingsPage::KeyServerSettingsPage(QWidget* parent) &KeyServerSettingsPage::slot_test_selected); switch_ui_busy(false); - SetSettings(); + LoadSettings(); } -void KeyServerSettingsPage::SetSettings() { +void KeyServerSettingsPage::LoadSettings() { entries_ = KeyServerList::Load(); default_url_ = KeyServerList::DefaultUrl(); refresh_table(); } -void KeyServerSettingsPage::ApplySettings() { +auto KeyServerSettingsPage::ApplySettings() -> bool { KeyServerList::Store(entries_, default_url_); + return true; } void KeyServerSettingsPage::refresh_table() { @@ -300,7 +301,3 @@ void KeyServerSettingsPage::switch_ui_busy(bool busy) { ui_->testButton->setDisabled(busy); } -auto KeyServerSettingsPageFactory(void* data) -> void* { - Q_UNUSED(data); - return new KeyServerSettingsPage(); -} diff --git a/src/m_key_server_sync/KeyServerSettingsPage.h b/src/m_key_server_sync/KeyServerSettingsPage.h index 8c387b8..365c94b 100644 --- a/src/m_key_server_sync/KeyServerSettingsPage.h +++ b/src/m_key_server_sync/KeyServerSettingsPage.h @@ -28,6 +28,8 @@ #pragma once +#include <GFModule.h> + #include <QWidget> #include <memory> @@ -39,30 +41,27 @@ class Ui_KeyServerSettingsPage; /** * @brief The Key Servers page of the application's Settings dialog. * - * Owned by this module, but hosted by the application: the dialog finds - * SetSettings() and ApplySettings() by name, the same contract its built-in - * pages follow, so edits here are staged and only written when the user - * accepts. + * Owned by this module, in a page the application's Settings dialog owns: + * LoadSettings() and ApplySettings() are the typed SettingsWidget interface, + * so edits here are staged and only written when the user accepts. */ -class KeyServerSettingsPage : public QWidget { +class KeyServerSettingsPage : public QWidget, public gf::ui::SettingsWidget { Q_OBJECT public: explicit KeyServerSettingsPage(QWidget* parent = nullptr); - public slots: + public: /** * @brief Load the stored servers, discarding anything staged. * - * Invoked by name from the host, both when the page is built and when the - * user cancels out of a restart confirmation. + * Called by the Host both when the page is built and when the user + * cancels out of a restart confirmation. */ - void SetSettings(); + void LoadSettings() override; - /** - * @brief Persist the staged servers. Invoked by name from the host on OK. - */ - void ApplySettings(); + /// Persist the staged servers. Called by the Host on OK. + auto ApplySettings() -> bool override; private slots: void slot_add(); @@ -84,19 +83,9 @@ class KeyServerSettingsPage : public QWidget { std::shared_ptr<Ui_KeyServerSettingsPage> ui_; /// Staged, not stored: the dialog's Cancel has to be able to throw all of - /// this away, which it does by calling SetSettings() again. + /// this away, which it does by calling LoadSettings() again. QList<KeyServerEntry> entries_; QString default_url_; int pending_probes_{0}; }; -/** - * @brief Build a page for the host's Settings dialog. - * - * Matches QObjectFactory. A fresh page every call: the dialog is rebuilt each - * time it is opened and takes ownership of what it is given. - * - * @param data unused - * @return void* a new KeyServerSettingsPage - */ -auto KeyServerSettingsPageFactory(void* data) -> void*; diff --git a/src/m_key_server_sync/KeyServerSyncModule.cpp b/src/m_key_server_sync/KeyServerSyncModule.cpp index dcc5827..50d3776 100644 --- a/src/m_key_server_sync/KeyServerSyncModule.cpp +++ b/src/m_key_server_sync/KeyServerSyncModule.cpp @@ -36,6 +36,7 @@ #include "GFModule.h" #include "GFModuleIdentity.h" +#include "GFSDKHostCommands.hpp" #include "GFSDKUI.h" #include "KeyServerList.h" #include "KeyServerSettingsPage.h" @@ -44,8 +45,31 @@ #include "VKSInterface.h" namespace { -constexpr auto kSettingsPageId = - "com.bktus.gpgfrontend.module.key_server_sync.settings"; + +using Severity = gf::cmd::host::AppMessage::Severity; + +/// A result for the user, shown by the Host from its own window: the module +/// has no business holding one of the Host's windows to parent a box to. +void Tell(Severity severity, const QString& title, const QString& text) { + Commands().Invoke<gf::cmd::host::AppMessage>({severity, title, text}); +} + +/// A fingerprint to open the search dialog with, handed from whoever asked +/// to the widget factory the Host calls on the GUI thread. +auto SearchPreset() -> QString& { + static QString preset; + return preset; +} +QMutex g_search_preset_mutex; + +void OpenSearch(const QString& fingerprint) { + { + QMutexLocker locker(&g_search_preset_mutex); + SearchPreset() = fingerprint; + } + Commands().Invoke<gf::cmd::host::ViewOpen>( + {gf::cmd::ViewRef{QStringLiteral(GF_MODULE_ID ".search")}}); +} using KeyCallback = std::function<void(const QString&)>; using ErrorCallback = std::function<void(const QString&, const QString&)>; @@ -121,10 +145,10 @@ void FetchKey(const KeyServerList::Route& route, const QString& handle, * * @return bool false when the user declined */ -auto ConfirmHkpPublish(QWidget* parent, const QString& url) -> bool { +auto ConfirmHkpPublish(const QString& url) -> bool { const auto host = QUrl(url).host(); return QMessageBox::warning( - parent, + nullptr, QCoreApplication::translate("GTrC", "Publish Without Verification?"), QCoreApplication::translate( @@ -144,30 +168,41 @@ auto ConfirmHkpPublish(QWidget* parent, const QString& url) -> bool { auto OnActivate() -> GFResult { LOG_INFO("key server sync module registering"); - // Registered untranslated: the module translators are not installed yet, so - // anything translated here would be stuck at the source text for the rest of - // the session. The host translates these when it builds the dialog. - const auto keywords = - QStringList{GC_TR("keyserver"), GC_TR("key server"), GC_TR("hkp"), - GC_TR("vks"), GC_TR("publish"), GC_TR("search")} - .join('\n'); - gf::sdk::RegisterSettingsPage(GFModuleSdkContext(), kSettingsPageId, - "keys_engines", GC_TR("Key Servers"), - keywords.toUtf8().constData(), - KeyServerSettingsPageFactory, nullptr); + // Presentation is registered untranslated: the module translators are not + // installed yet, so anything translated here would be stuck at the source + // text for the rest of the session. The Host translates it when shown. + const bool search = gf::ui::RegisterNativeWidget<SearchKeyDialog>( + "search", {GC_TR("Key Server"), "", "", "", "", 0, 0}, + [](const QCborMap& /*args*/) { + auto* dialog = new SearchKeyDialog(); + QString preset; + { + QMutexLocker locker(&g_search_preset_mutex); + std::swap(preset, SearchPreset()); + } + // An empty preset opens a blank search; only a fingerprint that was + // actually supplied (the verify-failure flow) seeds the field. + if (!preset.isEmpty()) dialog->SetPresetFingerprint(preset); + return dialog; + }); + const bool settings = gf::ui::RegisterNativeWidget<KeyServerSettingsPage>( + "settings", + {GC_TR("Key Servers"), + GC_TR("keyserver,key server,hkp,vks,publish,search"), "", "", "", 0, 0}, + [](const QCborMap& /*args*/) { return new KeyServerSettingsPage(); }); - return GFResult::Ok(); + return search && settings + ? GFResult::Ok() + : GFResult::Fail("the key server widgets were not registered"); } namespace { -auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) - -> int { +auto UploadKeyToServer(int channel, const QString& key_id) -> int { const auto exported = gf::sdk::ExportKey(GFModuleSdkContext(), channel, key_id, true); if (exported.isEmpty()) { - QMessageBox::critical( - parent, QCoreApplication::translate("GTrC", "Key Upload Failed"), + Tell(Severity::kError, QCoreApplication::translate("GTrC", "Key Upload Failed"), QCoreApplication::translate( "GTrC", "Failed to export the public key before uploading.\n" @@ -182,18 +217,16 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) const auto server = route.url; if (!route.vks) { - if (!ConfirmHkpPublish(parent, server)) return 0; + if (!ConfirmHkpPublish(server)) return 0; auto* pks = new PKSInterface(); QObject::connect( pks, &PKSInterface::SignalKeyServerKeyUploadResult, QThread::currentThread(), - [parent, server, key_id](QNetworkReply::NetworkError error, + [server, key_id](QNetworkReply::NetworkError error, const QString& error_string) { if (error != QNetworkReply::NoError) { - QMessageBox::critical( - parent, - QCoreApplication::translate("GTrC", "Key Upload Failed"), + Tell(Severity::kError, QCoreApplication::translate("GTrC", "Key Upload Failed"), QCoreApplication::translate( "GTrC", "Failed to upload public key to the server.\n" @@ -204,9 +237,7 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) } // No verification mail follows an HKP upload, so do not promise one. - QMessageBox::information( - parent, - QCoreApplication::translate("GTrC", + Tell(Severity::kInfo, QCoreApplication::translate("GTrC", "Public Key Upload Successful"), QCoreApplication::translate( "GTrC", @@ -225,7 +256,7 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) auto* vks = new VKSInterface(server); QObject::connect( vks, &VKSInterface::SignalKeyUploaded, QThread::currentThread(), - [parent, server](const QString& fpr, const QJsonObject& status, + [server](const QString& fpr, const QJsonObject& status, const QString& token) { // Handle successful response QString status_message = QCoreApplication::translate( @@ -247,9 +278,7 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) const auto host = QUrl(server).host(); // Notify user of successful upload and status details - QMessageBox::information( - parent, - QCoreApplication::translate("GTrC", "Public Key Upload Successful"), + Tell(Severity::kInfo, QCoreApplication::translate("GTrC", "Public Key Upload Successful"), QCoreApplication::translate( "GTrC", "The public key was successfully uploaded to the " @@ -263,9 +292,8 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) QObject::connect( vks, &VKSInterface::SignalErrorOccurred, QThread::currentThread(), - [parent, key_id](const QString& error, const QString& data) { - QMessageBox::critical( - parent, QCoreApplication::translate("GTrC", "Key Upload Failed"), + [key_id](const QString& error, const QString& data) { + Tell(Severity::kError, QCoreApplication::translate("GTrC", "Key Upload Failed"), QCoreApplication::translate( "GTrC", "Failed to upload public key to the server.\n" @@ -284,23 +312,21 @@ auto UploadKeyToServer(QWidget* parent, int channel, const QString& key_id) return 0; } -auto UpdateKeyFromKeyServer(QWidget* parent, int channel, const QString& fpr) - -> int { +auto UpdateKeyFromKeyServer(int channel, const QString& fpr) -> int { const auto route = KeyServerList::SyncRoute(); const auto host = QUrl(route.url).host(); FetchKey( route, fpr, true, - [parent, channel](const QString& key_data) { - gf::sdk::ImportKeys(GFModuleSdkContext(), channel, parent, + [channel](const QString& key_data) { + gf::sdk::ImportKeys(GFModuleSdkContext(), channel, nullptr, key_data.toUtf8()); }, - [parent, fpr, host](const QString& error, const QString& data) { + [fpr, host](const QString& error, const QString& data) { Q_UNUSED(data); // Name the server: it is the user's choice now, and a failure they // cannot attribute to a host is one they cannot fix. - QMessageBox::critical( - parent, QCoreApplication::translate("GTrC", "Key Update Failed"), + Tell(Severity::kError, QCoreApplication::translate("GTrC", "Key Update Failed"), QCoreApplication::translate( "GTrC", "Failed to retrieve public key from %3.\n" @@ -313,120 +339,72 @@ auto UpdateKeyFromKeyServer(QWidget* parent, int channel, const QString& fpr) } // namespace -auto OnMainwindowMenuMounted(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("main window menu mounted event: processing"); - - if (!event.Has("main_window")) { - LOG_DEBUG("main window menu mounted event: no main_window found"); - return GFEventResult::Bad("no main_window found"); - } +namespace { - auto* main_window = GFUIObject<QMainWindow>(event.Str("main_window")); - if (!main_window) { - LOG_ERROR( - "main window menu mounted: main_window handle invalid or not " - "QMainWindow"); - return GFEventResult::Bad("main_window handle invalid or not QMainWindow"); +/// Both key commands act on one key, named by the Host's context. +struct KeyArgs { + gf::cmd::KeyRef key; + static constexpr auto Fields() { + return std::make_tuple(gf::cmd::F("key", &KeyArgs::key)); } +}; - if (!event.Has("import_key_menu")) { - LOG_DEBUG("main window menu mounted event: no import_key_menu found"); - return GFEventResult::Bad("no import_key_menu found"); - } +struct PublishKey { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".publish_key", GC_TR("Publish Public Key to Key Server"), + GC_TR("Upload the public key to the key server used for syncing"), + GC_TR("Key Server Operations"), 0, gf::cmd::kNeedsGuiThread}; + using Args = KeyArgs; + using Result = gf::cmd::Unit; +}; - auto* import_key_menu = GFUIObject<QMenu>(event.Str("import_key_menu")); - if (!import_key_menu) { - LOG_ERROR( - "main window menu mounted: import_key_menu handle invalid or not " - "QMenu"); - return GFEventResult::Bad("import_key_menu handle invalid or not QMenu"); - } +struct RefreshKey { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".refresh_key", GC_TR("Refresh Public Key From Key Server"), + GC_TR("Import the latest copy of the public key from the key server"), + GC_TR("Key Server Operations"), 0, gf::cmd::kNeedsGuiThread}; + using Args = KeyArgs; + using Result = gf::cmd::Unit; +}; - LOG_DEBUG("adding key server sync actions to import key menu"); +struct SearchKey { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".search_key", GC_TR("Key Server"), + GC_TR("Import public keys from a trusted key server."), "", 0, + gf::cmd::kNeedsGuiThread}; + struct Args { + std::optional<QString> fingerprint; + static constexpr auto Fields() { + return std::make_tuple(gf::cmd::F("fingerprint", &Args::fingerprint)); + } + }; + using Result = gf::cmd::Unit; +}; - QMetaObject::invokeMethod( - QApplication::instance(), - [&]() -> void { - QWidget* parent = - qobject_cast<QWidget*>(static_cast<QObject*>(main_window)); - auto* action = new QAction( - QCoreApplication::translate("GTrC", "Key Server"), parent); - action->setToolTip(QCoreApplication::translate( - "GTrC", "Import public keys from a trusted key server.")); - action->setIcon(QIcon(":/icons/import_key_from_server.png")); - QObject::connect(action, &QAction::triggered, parent, [=]() { - auto* dialog = new SearchKeyDialog(parent); - dialog->show(); - }); - import_key_menu->addAction(action); - }, - Qt::BlockingQueuedConnection); - return GFEventResult::Ok(); +auto DoPublishKey(const gf::cmd::CommandContext& /*ctx*/, const KeyArgs& a) + -> gf::cmd::Outcome<gf::cmd::Unit> { + // Any key with a public part can be published, mirroring the Key + // Management "Publish Key to Keyserver" action. + UploadKeyToServer(static_cast<int>(a.key.channel), a.key.key_id); + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } -auto OnKeyPairOperaMenuCreated(const GFEvent& event) -> GFEventResult { - auto* tab = GFUIObject<QWidget>(event.Str("tab")); - if (!tab) { - LOG_ERROR( - "key pair opera menu created: tab handle " - "invalid or not KeyPairOperaTab"); - return GFEventResult::Bad("tab handle invalid or not KeyPairOperaTab"); - } - - auto* layout = GFUIObject<QVBoxLayout>(event.Str("opera_layout")); - if (!layout) { - LOG_ERROR( - "key pair opera menu created: opera_menu handle " - "invalid or not QMenu"); - return GFEventResult::Bad("opera_menu handle invalid or not QMenu"); - } - - auto is_private_key = event.Str("is_private_key").toInt() != 0; - auto has_master_key = event.Str("has_master_key").toInt() != 0; - - auto channel = event.Str("channel").toInt(); - auto key_id = event.Str("key_id"); - auto fpr = event.Str("fpr"); - - FLOG_DEBUG( - "adding key server sync actions: key id: %1, channel: %2, is " - "private key: %3, has master key: %4", - key_id, channel, static_cast<int>(is_private_key), - static_cast<int>(has_master_key)); - - QMetaObject::invokeMethod(QApplication::instance(), [=]() -> void { - auto* menu = new QMenu(tab); - - auto* key_server_opera_button = new QPushButton( - QCoreApplication::translate("GTrC", "Key Server Operations")); - key_server_opera_button->setStyleSheet("text-align:center;"); - key_server_opera_button->setMenu(menu); - - // add upload / update key actions - auto* upload_key_pair = new QAction(QCoreApplication::translate( - "GTrC", "Publish Public Key to Key Server")); - QObject::connect(upload_key_pair, &QAction::triggered, tab, - [=]() { UploadKeyToServer(tab, channel, key_id); }); - // Any key with a public part can be published, mirroring the Key - // Management "Publish Key to Keyserver" action. - - auto* update_key_pair = new QAction(QCoreApplication::translate( - "GTrC", "Refresh Public Key From Key Server")); - QObject::connect(update_key_pair, &QAction::triggered, tab, - [=]() { UpdateKeyFromKeyServer(tab, channel, fpr); }); - - // Refresh re-imports the latest public key from the server; it is - // valid for any key, including your own. - - menu->addAction(upload_key_pair); - menu->addAction(update_key_pair); - - layout->addWidget(key_server_opera_button); - }); +auto DoRefreshKey(const gf::cmd::CommandContext& /*ctx*/, const KeyArgs& a) + -> gf::cmd::Outcome<gf::cmd::Unit> { + // Refresh re-imports the latest public key from the server; it is valid + // for any key, including your own. + UpdateKeyFromKeyServer(static_cast<int>(a.key.channel), a.key.fingerprint); + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); +} - return GFEventResult::Ok(); +auto DoSearchKey(const gf::cmd::CommandContext& /*ctx*/, + const SearchKey::Args& a) -> gf::cmd::Outcome<gf::cmd::Unit> { + OpenSearch(a.fingerprint.value_or(QString())); + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } +} // namespace + auto OnRequestGetPublicKeyByFingerprint(const GFEvent& event) -> GFEventResult { if (event.Str("fingerprint").isEmpty()) return GFEventResult::Bad("fingerprint is empty"); @@ -527,7 +505,11 @@ auto OnRequestUploadPublicKey(const GFEvent& event) -> GFEventResult { {"key_server", server}, {"protocol", "vks"}}); }); - QObject::connect(vks, &VKSInterface::SignalKeyRetrieved, vks, + // Released on the signals an upload actually sends: it never retrieves a + // key, so connecting to that one left every upload task behind. + QObject::connect(vks, &VKSInterface::SignalKeyUploaded, vks, + &VKSInterface::deleteLater); + QObject::connect(vks, &VKSInterface::SignalErrorOccurred, vks, &VKSInterface::deleteLater); vks->UploadKey(key_text); return GFEventResult::Deferred(); @@ -535,46 +517,13 @@ auto OnRequestUploadPublicKey(const GFEvent& event) -> GFEventResult { auto OnRequestSearchPublicKeyByFingerprint(const GFEvent& event) -> GFEventResult { - auto fingerprint = event.Str("fingerprint").trimmed(); - - QWidget* parent = nullptr; - - if (event.Has("parent")) { - parent = GFUIObject<QWidget>(event.Str("parent")); - } - - if (parent == nullptr) { - parent = QApplication::activeWindow(); - } - + const auto fingerprint = event.Str("fingerprint").trimmed(); FLOG_DEBUG("open key server search dialog with fingerprint: %1", fingerprint); - - QMetaObject::invokeMethod( - QApplication::instance(), - [parent, fingerprint]() { - auto* dialog = new SearchKeyDialog(parent); - // An empty preset opens a blank search dialog; only seed the field - // when a fingerprint was actually supplied (verify-failure flow). - if (!fingerprint.isEmpty()) { - dialog->SetPresetFingerprint(fingerprint); - } - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->show(); - dialog->raise(); - dialog->activateWindow(); - }, - Qt::QueuedConnection); - + // The module's own dialog, opened by the Host in a frame of its own. + OpenSearch(fingerprint); return GFEventResult::Ok(); } -auto OnDeactivate() -> GFResult { - // The registry holds a function pointer into this shared object; leaving it - // behind would crash the next time the Settings dialog is built. - GFUIUnregisterSettingsPage(GFModuleSdkContext(), kSettingsPageId); - return GFResult::Ok(); -} - auto OnUnload() -> void { // Said "paper key module" until now, copied from a module that no longer // exists -- so the one line naming who was shutting down named the wrong one. @@ -582,27 +531,33 @@ auto OnUnload() -> void { } // The module's whole framework surface. -constexpr GFEventBinding kEvents[] = { - {"KEY_PAIR_OPERA_MENU_CREATED", &OnKeyPairOperaMenuCreated}, - {"MAINWINDOW_MENU_MOUNTED", &OnMainwindowMenuMounted}, +constexpr std::array<GFEventBinding, 4> kEvents = {{ {"REQUEST_GET_PUBLIC_KEY_BY_FINGERPRINT", &OnRequestGetPublicKeyByFingerprint}, {"REQUEST_GET_PUBLIC_KEY_BY_KEY_ID", &OnRequestGetPublicKeyByKeyId}, {"REQUEST_SEARCH_PUBLIC_KEY_BY_FINGERPRINT", &OnRequestSearchPublicKeyByFingerprint}, {"REQUEST_UPLOAD_PUBLIC_KEY", &OnRequestUploadPublicKey}, +}}; + +const std::array<gf::cmd::Binding, 3> kCommands = { + gf::cmd::Bind<PublishKey, &DoPublishKey>(), + gf::cmd::Bind<RefreshKey, &DoRefreshKey>(), + gf::cmd::Bind<SearchKey, &DoSearchKey>(), }; -constexpr GFModuleHooks kHooks = { +const GFModuleHooks kHooks = { sizeof(GFModuleHooks), GF_MODULE_ID, GF_MODULE_VERSION, GF_MODULE_TRANSLATION_CONTEXT, &OnActivate, - &OnDeactivate, + nullptr, // the Host withdraws the commands, widgets and script itself &OnUnload, - kEvents, - std::size(kEvents), + kEvents.data(), + kEvents.size(), + kCommands.data(), + kCommands.size(), }; extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi) diff --git a/src/m_key_server_sync/SearchKeyDialog.cpp b/src/m_key_server_sync/SearchKeyDialog.cpp index 082963f..579edba 100644 --- a/src/m_key_server_sync/SearchKeyDialog.cpp +++ b/src/m_key_server_sync/SearchKeyDialog.cpp @@ -375,5 +375,5 @@ void SearchKeyDialog::slot_lookup_finished_pks( return; } - gf::sdk::ImportKeys(GFModuleSdkContext(), channel, this, key_data); + gf::sdk::ImportKeys(GFModuleSdkContext(), channel, nullptr, key_data); } diff --git a/src/m_key_server_sync/SearchKeyDialog.h b/src/m_key_server_sync/SearchKeyDialog.h index 30b8879..c7cd1c7 100644 --- a/src/m_key_server_sync/SearchKeyDialog.h +++ b/src/m_key_server_sync/SearchKeyDialog.h @@ -28,13 +28,15 @@ #pragma once +#include <GFModule.h> + #include <QDialog> #include "PKSInterface.h" class Ui_SearchKeyDialog; -class SearchKeyDialog : public QDialog { +class SearchKeyDialog : public QDialog, public gf::ui::DialogWidget { Q_OBJECT public: explicit SearchKeyDialog(QWidget* parent = nullptr); diff --git a/src/m_key_server_sync/module.json b/src/m_key_server_sync/module.json index 0f1b85c..c02aff7 100644 --- a/src/m_key_server_sync/module.json +++ b/src/m_key_server_sync/module.json @@ -13,12 +13,15 @@ "ui.custom" ], "events": [ - "KEY_PAIR_OPERA_MENU_CREATED", - "MAINWINDOW_MENU_MOUNTED", "REQUEST_GET_PUBLIC_KEY_BY_FINGERPRINT", "REQUEST_GET_PUBLIC_KEY_BY_KEY_ID", "REQUEST_SEARCH_PUBLIC_KEY_BY_FINGERPRINT", "REQUEST_UPLOAD_PUBLIC_KEY" ], + "commands": [ + "com.bktus.gpgfrontend.module.key_server_sync.publish_key", + "com.bktus.gpgfrontend.module.key_server_sync.refresh_key", + "com.bktus.gpgfrontend.module.key_server_sync.search_key" + ], "translation_context": "ModuleKeyServerSync" } diff --git a/src/m_key_server_sync/ui/main.lua b/src/m_key_server_sync/ui/main.lua new file mode 100644 index 0000000..2ad2a01 --- /dev/null +++ b/src/m_key_server_sync/ui/main.lua @@ -0,0 +1,35 @@ +-- Key server sync: where it is offered. +-- +-- The commands do the work and carry the titles; the search dialog and the +-- settings page are the module's own widgets, in containers the Host owns. + +local search = commands.get("com.bktus.gpgfrontend.module.key_server_sync.search_key") +local publish = commands.get("com.bktus.gpgfrontend.module.key_server_sync.publish_key") +local refresh = commands.get("com.bktus.gpgfrontend.module.key_server_sync.refresh_key") + +ui.mount { id = "search", anchor = ui.anchor.dialog {}, widget = native.widget("search") } + +ui.mount { + id = "settings", + anchor = ui.anchor.settings { section = "keys_engines" }, + widget = native.widget("settings"), +} + +ui.action { + id = "search", + anchor = ui.anchor("main.menu.import_key"), + command = search, + icon = ":/icons/import_key_from_server.png", +} + +-- Both act on the key the details dialog is showing. They share a category, +-- so the Host groups them under one "Key Server Operations" button. +local function for_this_key(ctx) + if not ctx.key then return { visible = false } end + return { args = { key = ctx.key } } +end + +ui.action { id = "publish", anchor = ui.anchor("key.details.actions"), + command = publish, update = for_this_key } +ui.action { id = "refresh", anchor = ui.anchor("key.details.actions"), + command = refresh, update = for_this_key } diff --git a/src/m_pgp_inspect/CMakeLists.txt b/src/m_pgp_inspect/CMakeLists.txt index 705ac23..e052bf4 100644 --- a/src/m_pgp_inspect/CMakeLists.txt +++ b/src/m_pgp_inspect/CMakeLists.txt @@ -34,7 +34,8 @@ gf_add_module( QT Core Widgets SOURCES PGPInspectModule.cpp PGPInspectModel.cpp - PGPInspectDialog.cpp) + PGPInspectDialog.cpp + LUA_SCRIPTS ui/main.lua) if(GPGFRONTEND_MODULES_BUILD_TESTS) add_subdirectory(test) diff --git a/src/m_pgp_inspect/PGPInspectDialog.cpp b/src/m_pgp_inspect/PGPInspectDialog.cpp index e3bde03..9dcab4e 100644 --- a/src/m_pgp_inspect/PGPInspectDialog.cpp +++ b/src/m_pgp_inspect/PGPInspectDialog.cpp @@ -170,12 +170,10 @@ void PGPInspectDialog::render_document() { if (block.has_armor && block.armor.crc24 == QLatin1String("mismatch")) { item->setForeground( - kLabelColumn, QBrush(QColor(GFUIThemeColor( - GFModuleSdkContext(), GF_UI_COLOR_DANGER, this)))); + kLabelColumn, QBrush(QColor(GFUIThemeColorForRole(GFModuleSdkContext(), GF_UI_COLOR_DANGER)))); } else if (!block.error.isEmpty()) { item->setForeground( - kLabelColumn, QBrush(QColor(GFUIThemeColor( - GFModuleSdkContext(), GF_UI_COLOR_WARNING, this)))); + kLabelColumn, QBrush(QColor(GFUIThemeColorForRole(GFModuleSdkContext(), GF_UI_COLOR_WARNING)))); } add_packets(item, block.packets); @@ -205,8 +203,7 @@ void PGPInspectDialog::add_packets(QTreeWidgetItem* parent, if (packet.Malformed()) { item->setForeground( - kLabelColumn, QBrush(QColor(GFUIThemeColor( - GFModuleSdkContext(), GF_UI_COLOR_WARNING, this)))); + kLabelColumn, QBrush(QColor(GFUIThemeColorForRole(GFModuleSdkContext(), GF_UI_COLOR_WARNING)))); } auto fields = packet.fields; @@ -228,8 +225,7 @@ void PGPInspectDialog::add_fields(QTreeWidgetItem* parent, row->setText(kValueColumn, field.value); row->setForeground( kLabelColumn, - QBrush(QColor(GFUIThemeColor(GFModuleSdkContext(), - GF_UI_COLOR_MUTED_TEXT, this)))); + QBrush(QColor(GFUIThemeColorForRole(GFModuleSdkContext(), GF_UI_COLOR_MUTED_TEXT)))); } } diff --git a/src/m_pgp_inspect/PGPInspectDialog.h b/src/m_pgp_inspect/PGPInspectDialog.h index d84b7c9..93e4224 100644 --- a/src/m_pgp_inspect/PGPInspectDialog.h +++ b/src/m_pgp_inspect/PGPInspectDialog.h @@ -29,6 +29,8 @@ #pragma once #include <QByteArray> +#include <GFModule.h> + #include <QDialog> #include "PGPInspectModel.h" @@ -56,7 +58,7 @@ auto PGPInspectBytes(const QByteArray& data) -> PGPInspectDocument; * Every rule about what the bytes mean lives in PGPInspectModel.h and, below * that, in the Rust packet walk. */ -class PGPInspectDialog : public QDialog { +class PGPInspectDialog : public QDialog, public gf::ui::DialogWidget { Q_OBJECT public: diff --git a/src/m_pgp_inspect/PGPInspectModule.cpp b/src/m_pgp_inspect/PGPInspectModule.cpp index 2130cda..5753b9a 100644 --- a/src/m_pgp_inspect/PGPInspectModule.cpp +++ b/src/m_pgp_inspect/PGPInspectModule.cpp @@ -27,12 +27,7 @@ */ #include <GFModule.h> - -#include <QAction> -#include <QApplication> -#include <QMainWindow> -#include <QMenu> -#include <QPointer> +#include <GFSDKHostCommands.hpp> #include "GFModuleIdentity.h" #include "PGPInspectDialog.h" @@ -52,77 +47,73 @@ auto InspectCurrentTab() -> PGPInspectDocument { return PGPInspectBytes(bytes); } -void RaiseInspectDialog(QWidget *parent) { - const auto bytes = PGPInspectCurrentTabBytes(); - auto *dialog = - new PGPInspectDialog(PGPInspectBytes(bytes), bytes.size(), parent); - dialog->setModal(false); - dialog->show(); -} +/** + * @brief The module's one action: show the current tab's packet structure. + * + * Enabled only when the tab holds something to show -- decided when the + * menu opens, because what the current tab holds changes constantly and an + * entry enabled over plain text promises a window with nothing in it. + */ +struct OpenInspector { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".open_inspector", + GC_TR("Open OpenPGP Structure Inspector"), + GC_TR("Show the packet structure of the current tab"), "", 0, + gf::cmd::kNeedsGuiThread}; + using Args = gf::cmd::Unit; + using Result = gf::cmd::Unit; -} // namespace + static auto State(const gf::cmd::CommandContext& /*ctx*/) -> uint32_t { + return PGPInspectHasStructure(InspectCurrentTab()) + ? GF_CMD_STATE_ENABLED | GF_CMD_STATE_VISIBLE + : GF_CMD_STATE_VISIBLE; + } +}; -auto OnActivate() -> GFResult { - // Nothing to register with the host: the module owns no settings page, no - // tab view and no metatype. Its whole surface is one menu action, added - // when the menu is mounted. - return GFResult::Ok(); +auto DoOpenInspector(const gf::cmd::CommandContext& /*ctx*/, + const gf::cmd::Unit& /*args*/) + -> gf::cmd::Outcome<gf::cmd::Unit> { + // The dialog is the module's mount; the Host owns its frame and opens it. + Commands().Invoke<gf::cmd::host::ViewOpen>( + {gf::cmd::ViewRef{QStringLiteral(GF_MODULE_ID ".inspector")}}); + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } -auto OnMainWindowMenuMounted(const GFEvent &event) -> GFEventResult { - QMainWindow *main_window = nullptr; - if (auto r = event.RequireGui("main_window", main_window); !r.ok) return r; - - QMenu *advance_menu = nullptr; - if (auto r = event.RequireGui("advance_menu", advance_menu); !r.ok) return r; - - LOG_DEBUG("adding openpgp structure inspector to the advanced menu"); - - QMetaObject::invokeMethod( - QApplication::instance(), - [=]() -> void { - auto *action = - new QAction(QCoreApplication::translate( - "GTrC", "Open OpenPGP Structure Inspector"), - nullptr); - action->setToolTip(QCoreApplication::translate( - "GTrC", "Show the packet structure of the current tab")); - QObject::connect(action, &QAction::triggered, main_window, - [=]() { RaiseInspectDialog(main_window); }); - advance_menu->addAction(action); - - // Decided when the menu opens rather than once at mount time: what - // the current tab holds changes constantly, and an entry that is - // enabled over plain text promises a window that would have nothing - // in it. - QPointer<QAction> guarded(action); - QObject::connect(advance_menu, &QMenu::aboutToShow, action, [guarded] { - if (guarded.isNull()) return; - guarded->setEnabled(PGPInspectHasStructure(InspectCurrentTab())); - }); - }, - Qt::BlockingQueuedConnection); +} // namespace - return GFEventResult::Ok(); +auto OnActivate() -> GFResult { + // The inspector, built from the current tab each time it is opened. Where + // it is offered is ui/main.lua's business. + const bool ok = gf::ui::RegisterNativeWidget<PGPInspectDialog>( + "inspector", + {GC_TR("OpenPGP Structure"), "", "", "", ":/icons/help.png", 820, 620}, + [](const QCborMap& /*args*/) { + const auto bytes = PGPInspectCurrentTabBytes(); + return new PGPInspectDialog(PGPInspectBytes(bytes), bytes.size()); + }); + return ok ? GFResult::Ok() + : GFResult::Fail("the inspector widget was not registered"); } -// The module's whole framework surface: which events it handles, what runs at -// each lifecycle point, and one forwarder to the runtime that implements all -// of it. Everything above is business logic. -constexpr GFEventBinding kEvents[] = { - {"MAINWINDOW_MENU_MOUNTED", &OnMainWindowMenuMounted}, +// The module's whole framework surface: what it provides, what runs at each +// lifecycle point, and one forwarder to the runtime that implements all of +// it. Everything above is business logic. +const std::array<gf::cmd::Binding, 1> kCommands = { + gf::cmd::Bind<OpenInspector, &DoOpenInspector>(), }; -constexpr GFModuleHooks kHooks = { +const GFModuleHooks kHooks = { sizeof(GFModuleHooks), GF_MODULE_ID, GF_MODULE_VERSION, GF_MODULE_TRANSLATION_CONTEXT, &OnActivate, - nullptr, // nothing registered with the host, so nothing to undo + nullptr, // the Host withdraws the command, widget and script itself nullptr, - kEvents, - std::size(kEvents), + nullptr, // no events + 0, + kCommands.data(), + kCommands.size(), }; extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi) diff --git a/src/m_pgp_inspect/module.json b/src/m_pgp_inspect/module.json index 8a52688..9cb6754 100644 --- a/src/m_pgp_inspect/module.json +++ b/src/m_pgp_inspect/module.json @@ -11,8 +11,9 @@ "ui", "ui.custom" ], - "events": [ - "MAINWINDOW_MENU_MOUNTED" + "events": [], + "commands": [ + "com.bktus.gpgfrontend.module.pgp_inspect.open_inspector" ], "translation_context": "ModulePGPInspect" } diff --git a/src/m_pgp_inspect/ui/main.lua b/src/m_pgp_inspect/ui/main.lua new file mode 100644 index 0000000..e85975d --- /dev/null +++ b/src/m_pgp_inspect/ui/main.lua @@ -0,0 +1,16 @@ +-- OpenPGP structure inspector: where it is offered. +-- +-- What the entry does, what it is called and when it is enabled all belong +-- to the module's command; this file only places it. The dialog is the +-- module's own widget, in a frame the Host owns. + +local open = commands.get("com.bktus.gpgfrontend.module.pgp_inspect.open_inspector") + +ui.mount { + id = "inspector", + anchor = ui.anchor.dialog {}, + widget = native.widget("inspector"), +} + +ui.action { id = "menu", anchor = ui.anchor("main.menu.advanced"), command = open } +ui.action { id = "context", anchor = ui.anchor("editor.context"), command = open } diff --git a/src/m_ver_check/CMakeLists.txt b/src/m_ver_check/CMakeLists.txt index b760e6e..fa3369d 100644 --- a/src/m_ver_check/CMakeLists.txt +++ b/src/m_ver_check/CMakeLists.txt @@ -28,4 +28,5 @@ gf_add_module( NAME ver_check QT Core Network Widgets Xml - UI_DIR ui) + UI_DIR ui + LUA_SCRIPTS ui/main.lua) diff --git a/src/m_ver_check/UpdateTab.cpp b/src/m_ver_check/UpdateTab.cpp index 02727d0..575ff7b 100644 --- a/src/m_ver_check/UpdateTab.cpp +++ b/src/m_ver_check/UpdateTab.cpp @@ -205,15 +205,14 @@ void UpdateTab::slot_show_version_status() { } } -auto UpdateTabFactory(void*) -> void* { return new UpdateTab(); } - void UpdateTab::slot_check_version_update() { check_update_btn_->setEnabled(false); pb_->show(); - auto api = - gf::sdk::StateText(GFModuleSdkContext(), "ui", - "settings.network.update_checking_api", "github"); + // The one place the choice is stored; the settings page writes it. + auto api = gf::sdk::Setting(GFModuleSdkContext(), GF_SETTING_MODULE, + "update_checking_api", "github") + .toString(); if (api == "bktus") { auto* task = new BKTUSVersionCheckTask(); diff --git a/src/m_ver_check/UpdateTab.h b/src/m_ver_check/UpdateTab.h index 9c969de..55ba33d 100644 --- a/src/m_ver_check/UpdateTab.h +++ b/src/m_ver_check/UpdateTab.h @@ -86,5 +86,3 @@ class UpdateTab : public QWidget { */ void SignalReplyFromUpdateServer(QByteArray data); }; - -auto UpdateTabFactory(void* id) -> void*;
\ No newline at end of file diff --git a/src/m_ver_check/VersionCheckingModule.cpp b/src/m_ver_check/VersionCheckingModule.cpp index 078df10..bb259a2 100644 --- a/src/m_ver_check/VersionCheckingModule.cpp +++ b/src/m_ver_check/VersionCheckingModule.cpp @@ -29,10 +29,18 @@ #include "VersionCheckingModule.h" #include <GFSDKBuildInfo.h> +#include <GFSDKHostCommands.hpp> #include <GFSDKLog.h> #include <GFSDKUI.h> +#include <QButtonGroup> +#include <QCheckBox> +#include <QGroupBox> +#include <QHBoxLayout> +#include <QLabel> #include <QMetaType> +#include <QRadioButton> +#include <QVBoxLayout> #include <QtNetwork> #include "BKTUSVersionCheckTask.h" @@ -43,163 +51,171 @@ #include "UpdateTab.h" #include "Utils.h" -auto OnActivate() -> GFResult { - LOG_INFO("version checking module activating"); - return GFResult::Ok(); +namespace { + +/// Whether to check at startup. The Host's own setting: the setup wizard +/// asks it, and this module's settings page shows the same switch rather +/// than a second one that could disagree. +constexpr auto kProhibitKey = "network/prohibit_update_check"; + +/// Which service to ask. The module's own setting. +constexpr auto kApiKey = "update_checking_api"; + +auto Api() -> QString { + return gf::sdk::Setting(GFModuleSdkContext(), GF_SETTING_MODULE, kApiKey, + "github") + .toString(); } -namespace { +void StoreResult(const SoftwareVersion& sv) { + gf::sdk::SetCacheText(GFModuleSdkContext(), GF_STORE_DURABLE, + "update_checking_cache", + (QJsonDocument(sv.ToJson()).toJson()).constData()); +} -auto CheckUpdate(const GFEvent& event) -> GFEventResult { - if (event.Str("api") == "bktus") { +/// Ask the service; cache what it says for the next start. The task always +/// reports, success or not, so it is always released. +void CheckUpdate() { + if (Api() == "bktus") { MLogInfo("checking updating using api of bktus.com"); auto* task = new BKTUSVersionCheckTask(); - QObject::connect( - task, &BKTUSVersionCheckTask::SignalUpgradeVersion, - QThread::currentThread(), [event](const SoftwareVersion& sv) { - gf::sdk::SetCacheText( - GFModuleSdkContext(), GF_STORE_DURABLE, "update_checking_cache", - (QJsonDocument(sv.ToJson()).toJson()).constData()); - return GFEventResult::Ok(); - }); + QObject::connect(task, &BKTUSVersionCheckTask::SignalUpgradeVersion, + QCoreApplication::instance(), &StoreResult); QObject::connect(task, &BKTUSVersionCheckTask::SignalUpgradeVersion, task, &QObject::deleteLater); task->Run(); } else { MLogInfo("checking updating using api of github.com"); auto* task = new GitHubVersionCheckTask(); - QObject::connect( - task, &GitHubVersionCheckTask::SignalUpgradeVersion, - QCoreApplication::instance(), [event](const SoftwareVersion& sv) { - gf::sdk::SetCacheText( - GFModuleSdkContext(), GF_STORE_DURABLE, "update_checking_cache", - (QJsonDocument(sv.ToJson()).toJson()).constData()); - return GFEventResult::Ok(); - }); + QObject::connect(task, &GitHubVersionCheckTask::SignalUpgradeVersion, + QCoreApplication::instance(), &StoreResult); QObject::connect(task, &GitHubVersionCheckTask::SignalUpgradeVersion, task, &QObject::deleteLater); task->Run(); } - // The check runs asynchronously and its callback answers, so nothing is - // answered here. - return GFEventResult::Deferred(); } -auto RaiseUpdateDialog(QWidget* parent) -> QDialog* { - auto* dialog = new QDialog(parent); - dialog->setWindowTitle( - QCoreApplication::translate("GTrC", "Check for Updates")); - dialog->setModal(true); - dialog->setAttribute(Qt::WA_DeleteOnClose); - auto* layout = new QVBoxLayout(); - auto* update_tab = new UpdateTab(dialog); - layout->addWidget(update_tab); - dialog->setLayout(layout); - dialog->resize(500, 600); - dialog->show(); - return dialog; +void OpenUpdateDialog() { + Commands().Invoke<gf::cmd::host::ViewOpen>( + {gf::cmd::ViewRef{QStringLiteral(GF_MODULE_ID ".update")}}); } -} // namespace - -auto OnMainwindowMenuMounted(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("main window menu mounted event: processing"); - - if (!event.Has("main_window")) { - LOG_DEBUG("main window menu mounted event: no main_window found"); - return GFEventResult::Bad("no main_window found"); +/// The update dialog, in a frame the Host owns. +class UpdateWidget : public QWidget, public gf::ui::DialogWidget { + public: + UpdateWidget() { + auto* layout = new QVBoxLayout(this); + layout->addWidget(new UpdateTab(this)); } +}; - auto* main_window = GFUIObject<QMainWindow>(event.Str("main_window")); - if (!main_window) { - LOG_ERROR( - "main window menu mounted: main_window handle invalid or not " - "QMainWindow"); - return GFEventResult::Bad("main_window handle invalid or not QMainWindow"); - } +/// Settings > Updates: whether to check at startup, and where. +class UpdateSettingsWidget : public QWidget, public gf::ui::SettingsWidget { + public: + UpdateSettingsWidget() { + auto* box = new QGroupBox( + QCoreApplication::translate("GTrC", "Update Checking"), this); + auto* column = new QVBoxLayout(box); - auto p_mw = QPointer<QMainWindow>(main_window); + check_ = new QCheckBox( + QCoreApplication::translate( + "GTrC", "Checking for version updates when the application " + "starts."), + box); + column->addWidget(check_); - if (!event.Has("help_menu")) { - LOG_DEBUG("main window menu mounted event: no help_menu found"); - return GFEventResult::Bad("no help_menu found"); + auto* row = new QHBoxLayout(); + row->addWidget(new QLabel( + QCoreApplication::translate("GTrC", "Update Checking API:"), box)); + github_ = new QRadioButton(QCoreApplication::translate("GTrC", "GitHub"), + box); + bktus_ = new QRadioButton(QCoreApplication::translate("GTrC", "BKTUS.com"), + box); + auto* group = new QButtonGroup(this); + group->addButton(github_); + group->addButton(bktus_); + row->addWidget(github_); + row->addWidget(bktus_); + row->addStretch(); + column->addLayout(row); + + auto* layout = new QVBoxLayout(this); + layout->addWidget(box); + layout->addStretch(); } - auto* help_menu = GFUIObject<QMenu>(event.Str("help_menu")); - if (!help_menu) { - LOG_ERROR( - "main window menu mounted: help_menu handle invalid or not " - "QMenu"); - return GFEventResult::Bad("help_menu handle invalid or not QMenu"); + void LoadSettings() override { + check_->setChecked(!gf::sdk::Setting(GFModuleSdkContext(), GF_SETTING_HOST, + kProhibitKey, true) + .toBool()); + const bool bktus = Api() == "bktus"; + bktus_->setChecked(bktus); + github_->setChecked(!bktus); } - auto p_help_menu = QPointer<QMenu>(help_menu); + auto ApplySettings() -> bool override { + const bool host_ok = + gf::sdk::SetSetting(GFModuleSdkContext(), GF_SETTING_HOST, + kProhibitKey, !check_->isChecked()); + const bool module_ok = gf::sdk::SetSetting( + GFModuleSdkContext(), GF_SETTING_MODULE, kApiKey, + bktus_->isChecked() ? QStringLiteral("bktus") + : QStringLiteral("github")); + return host_ok && module_ok; + } - LOG_DEBUG("adding check update action to help menu"); + private: + QCheckBox* check_; + QRadioButton* github_; + QRadioButton* bktus_; +}; - QMetaObject::invokeMethod( - QApplication::instance(), - [&]() -> void { - if (!p_mw || !p_help_menu) { - LOG_ERROR( - "main window menu mounted: main_window or help_menu was deleted " - "before invoking the check update action"); - return; - } +/// Help > Check for Updates. +struct CheckForUpdates { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".check_for_updates", GC_TR("Check for Updates"), + GC_TR("See whether a newer GpgFrontend is available"), "", 0, + gf::cmd::kNeedsGuiThread}; + using Args = gf::cmd::Unit; + using Result = gf::cmd::Unit; +}; - QWidget* parent = - qobject_cast<QWidget*>(static_cast<QObject*>(main_window)); - auto* action = new QAction( - QCoreApplication::translate("GTrC", "Check for Updates"), nullptr); - action->setToolTip(QCoreApplication::translate( - "GTrC", "Check for updates from the Internet.")); - action->setIcon(QIcon(":/icons/update.png")); - QObject::connect(action, &QAction::triggered, parent, - [=]() -> void { RaiseUpdateDialog(parent); }); - help_menu->addAction(action); - }, - Qt::BlockingQueuedConnection); - return GFEventResult::Ok(); +auto DoCheckForUpdates(const gf::cmd::CommandContext& /*ctx*/, + const gf::cmd::Unit& /*args*/) + -> gf::cmd::Outcome<gf::cmd::Unit> { + OpenUpdateDialog(); + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } -auto OnApplicationLoaded(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("application starting completed event: processing"); +} // namespace - auto* parent = GFUIObject<QWidget>( - event.Has("main_window") ? event.Str("main_window") : ""); - if (!parent) { - LOG_ERROR("application loaded: main_window handle invalid or not QWidget"); - return GFEventResult::Bad("main_window handle invalid or not QWidget"); - } +auto OnActivate() -> GFResult { + LOG_INFO("version checking module activating"); + const bool dialog = gf::ui::RegisterNativeWidget<UpdateWidget>( + "update", {GC_TR("Check for Updates"), "", "", "", "", 500, 600}, + [](const QCborMap& /*args*/) { return new UpdateWidget(); }); + const bool settings = gf::ui::RegisterNativeWidget<UpdateSettingsWidget>( + "settings", + {GC_TR("Updates"), GC_TR("update,version,check,github,bktus"), "", "", + "", 0, 0}, + [](const QCborMap& /*args*/) { return new UpdateSettingsWidget(); }); + return dialog && settings + ? GFResult::Ok() + : GFResult::Fail("the update widgets were not registered"); +} - // check version information - auto settings = qobject_cast<QSettings*>( - static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); - if (!settings) { - LOG_ERROR("application loaded: global settings handle invalid"); - return GFEventResult::Bad("global settings handle invalid"); - } +auto OnApplicationLoaded(const GFEvent& /*event*/) -> GFEventResult { + LOG_DEBUG("application starting completed event: processing"); - // ensure that it will not perform at the first startup before wizard is done - if (!settings->contains("network/prohibit_update_checking")) { - LOG_DEBUG( - "application loaded: prohibit_update_checking setting " - "not found"); + // Absent until the setup wizard has run: no check before the user has been + // asked whether to check at all. + const auto prohibited = gf::sdk::Setting(GFModuleSdkContext(), + GF_SETTING_HOST, kProhibitKey); + if (!prohibited.isValid()) { + LOG_DEBUG("application loaded: the setup wizard has not asked yet"); return GFEventResult::Ok(); } - - auto update_checking_api = - settings->value("network/update_checking_api", "github").toString(); - FLOG_DEBUG("application loaded: update checking api: %1", - update_checking_api); - - // we only check for update if the user did set the option to allow it - auto prohibit_update_checking = - settings->value("network/prohibit_update_checking", true).toBool(); - FLOG_DEBUG("application loaded: prohibit update checking: %1", - prohibit_update_checking); - - if (prohibit_update_checking) { + if (prohibited.toBool()) { LOG_DEBUG("application loaded: update checking is prohibited"); return GFEventResult::Ok(); } @@ -209,10 +225,9 @@ auto OnApplicationLoaded(const GFEvent& event) -> GFEventResult { auto json = QJsonDocument::fromJson(cache.toUtf8()); if (json.isEmpty() || !json.isObject()) { - LOG_DEBUG( - "application loaded: no valid cached version info found, " - "checking update"); - return CheckUpdate(event); + LOG_DEBUG("application loaded: nothing cached, checking now"); + CheckUpdate(); + return GFEventResult::Ok(); } SoftwareVersion sv; @@ -220,7 +235,8 @@ auto OnApplicationLoaded(const GFEvent& event) -> GFEventResult { FLOG_DEBUG("got software version meta data: %1", json.toJson()); if (sv.timestamp.addDays(1) < QDateTime::currentDateTime()) { - return CheckUpdate(event); + CheckUpdate(); + return GFEventResult::Ok(); } FillGrtWithVersionInfo(sv); @@ -229,235 +245,36 @@ auto OnApplicationLoaded(const GFEvent& event) -> GFEventResult { !sv.current_commit_hash_publish_in_remote) { LOG_INFO( "software version is outdated or not fully released, notifying user"); - - QMetaObject::invokeMethod( - QApplication::instance(), - [=]() { - auto* dialog = RaiseUpdateDialog(parent); - Q_UNUSED(dialog); - }, - Qt::QueuedConnection); + OpenUpdateDialog(); } - return GFEventResult::Ok(); -} - -auto OnNetworkSettingsTabUiCreated(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("network settings tab ui created event: processing"); - - auto* tab = GFUIObject<QWidget>(event.Has("network_settings_tab") - ? event.Str("network_settings_tab") - : ""); - if (!tab) { - LOG_ERROR( - "network settings tab ui created: network_settings_tab handle " - "invalid or not QWidget"); - return GFEventResult::Bad( - "network_settings_tab handle invalid or not QWidget"); - } - - auto* capability_group_box = GFUIObject<QGroupBox>( - event.Has("capability_group_box") ? event.Str("capability_group_box") - : ""); - - if (!capability_group_box) { - LOG_ERROR( - "network settings tab ui created: capability_group_box handle " - "invalid or not QGroupBox"); - return GFEventResult::Bad( - "capability_group_box handle invalid or not QGroupBox"); - } - - QMetaObject::invokeMethod( - QApplication::instance(), - [=]() { - auto* update_checking_check_box = new QCheckBox( - QCoreApplication::translate("GTrC", - "Checking for version updates when the " - "application starts."), - capability_group_box); - update_checking_check_box->setObjectName("update_checking_check_box"); - - capability_group_box->layout()->addWidget(update_checking_check_box); - - auto* github_radio_button = - new QRadioButton(QCoreApplication::translate("GTrC", "GitHub"), - capability_group_box); - auto* bktus_radio_button = - new QRadioButton(QCoreApplication::translate("GTrC", "BKTUS.com"), - capability_group_box); - - auto layout = new QHBoxLayout(); - layout->addWidget(new QLabel( - QCoreApplication::translate("GTrC", "Update Checking API:"), - capability_group_box)); - layout->addWidget(github_radio_button); - layout->addWidget(bktus_radio_button); - - capability_group_box->layout()->addItem(layout); - - auto* update_api_group = new QButtonGroup(tab); - update_api_group->setObjectName("update_api_group"); - update_api_group->addButton(github_radio_button); - update_api_group->addButton(bktus_radio_button); - }, - Qt::QueuedConnection); - - return GFEventResult::Ok(); -} - -auto OnNetworkSettingsTabApplySettings(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("network settings tab apply settings event: processing"); - auto* settings = qobject_cast<QSettings*>( - static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); - if (!settings) { - LOG_ERROR( - "network settings tab apply settings: global settings handle " - "invalid"); - return GFEventResult::Bad("global settings handle invalid"); - } - auto* tab = GFUIObject<QWidget>(event.Has("network_settings_tab") - ? event.Str("network_settings_tab") - : ""); - if (!tab) { - LOG_ERROR( - "network settings tab apply settings: network_settings_tab " - "handle invalid or not QWidget"); - return GFEventResult::Bad( - "network_settings_tab handle invalid or not QWidget"); - } - - // we need to apply the settings in the main thread to avoid some - // potential racing conditions - auto p_tab = QPointer<QWidget>(tab); - - QMetaObject::invokeMethod( - QApplication::instance(), - [=]() { - if (!p_tab) { - LOG_ERROR( - "network settings tab apply settings: network_settings_tab " - "already deleted when applying settings"); - return; - } - auto* update_checking_check_box = - p_tab->findChild<QCheckBox*>("update_checking_check_box"); - if (update_checking_check_box) { - settings->setValue( - "network/version_checking/" - "check_for_updates_on_startup", - update_checking_check_box->isChecked()); - } - - auto* update_api_group = - tab->findChild<QButtonGroup*>("update_api_group"); - if (update_api_group) { - QString api = "github"; - if (update_api_group->buttons().at(1)->isChecked()) { - api = "bktus"; - } - settings->setValue("network/version_checking/update_checking_api", - api); - } - - FLOG_DEBUG( - "network settings tab apply settings: version checking " - "settings applied, " - "check for updates on startup: %1, update checking api: %2", - update_checking_check_box - ? QString::number(update_checking_check_box->isChecked()) - : "null", - update_api_group - ? (update_api_group->buttons().at(1)->isChecked() ? "bktus" - : "github") - : "null"); - }, - Qt::QueuedConnection); - - return GFEventResult::Ok(); -} - -auto OnNetworkSettingsTabLoadSettings(const GFEvent& event) -> GFEventResult { - LOG_DEBUG("network settings tab load settings event: processing"); - - auto* settings = qobject_cast<QSettings*>( - static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); - - if (!settings) { - LOG_ERROR( - "network settings tab load settings: global settings handle " - "invalid"); - return GFEventResult::Bad("global settings handle invalid"); - } - - auto* tab = GFUIObject<QWidget>(event.Has("network_settings_tab") - ? event.Str("network_settings_tab") - : ""); - if (!tab) { - LOG_ERROR( - "network settings tab load settings: network_settings_tab " - "handle invalid or not QWidget"); - return GFEventResult::Bad( - "network_settings_tab handle invalid or not QWidget"); - } - - QMetaObject::invokeMethod( - QApplication::instance(), - [=]() { - auto* update_checking_check_box = - tab->findChild<QCheckBox*>("update_checking_check_box"); - if (update_checking_check_box) { - auto check_for_updates_on_startup = - settings - ->value( - "network/version_checking/" - "check_for_updates_on_startup", - false) - .toBool(); - update_checking_check_box->setChecked(check_for_updates_on_startup); - } - - auto* update_api_group = - tab->findChild<QButtonGroup*>("update_api_group"); - if (update_api_group) { - auto update_checking_api = - settings - ->value("network/version_checking/update_checking_api", - "github") - .toString(); - if (update_checking_api == "github") { - update_api_group->buttons().at(0)->setChecked(true); - } else { - update_api_group->buttons().at(1)->setChecked(true); - } - } - }, - Qt::BlockingQueuedConnection); - + // An observation, answered at once: the check runs on its own. return GFEventResult::Ok(); } auto OnUnload() -> void { LOG_INFO("version checking module unregistering"); } // The module's whole framework surface. -constexpr GFEventBinding kEvents[] = { +constexpr std::array<GFEventBinding, 1> kEvents = {{ {"APPLICATION_LOADED", &OnApplicationLoaded}, - {"MAINWINDOW_MENU_MOUNTED", &OnMainwindowMenuMounted}, - {"NETWORK_SETTINGS_TAB_APPLY_SETTINGS", &OnNetworkSettingsTabApplySettings}, - {"NETWORK_SETTINGS_TAB_LOAD_SETTINGS", &OnNetworkSettingsTabLoadSettings}, - {"NETWORK_SETTINGS_TAB_UI_CREATED", &OnNetworkSettingsTabUiCreated}, +}}; + +const std::array<gf::cmd::Binding, 1> kCommands = { + gf::cmd::Bind<CheckForUpdates, &DoCheckForUpdates>(), }; -constexpr GFModuleHooks kHooks = { +const GFModuleHooks kHooks = { sizeof(GFModuleHooks), GF_MODULE_ID, GF_MODULE_VERSION, GF_MODULE_TRANSLATION_CONTEXT, &OnActivate, - nullptr, // the settings tab is built through events, so nothing to undo + nullptr, // the Host withdraws the command, widgets and script itself &OnUnload, - kEvents, - std::size(kEvents), + kEvents.data(), + kEvents.size(), + kCommands.data(), + kCommands.size(), }; extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi) diff --git a/src/m_ver_check/module.json b/src/m_ver_check/module.json index 77f3980..7c37190 100644 --- a/src/m_ver_check/module.json +++ b/src/m_ver_check/module.json @@ -12,11 +12,10 @@ "ui.custom" ], "events": [ - "APPLICATION_LOADED", - "MAINWINDOW_MENU_MOUNTED", - "NETWORK_SETTINGS_TAB_APPLY_SETTINGS", - "NETWORK_SETTINGS_TAB_LOAD_SETTINGS", - "NETWORK_SETTINGS_TAB_UI_CREATED" + "APPLICATION_LOADED" + ], + "commands": [ + "com.bktus.gpgfrontend.module.version_checking.check_for_updates" ], "translation_context": "ModuleVersionChecking" } diff --git a/src/m_ver_check/ui/main.lua b/src/m_ver_check/ui/main.lua new file mode 100644 index 0000000..ba9ee48 --- /dev/null +++ b/src/m_ver_check/ui/main.lua @@ -0,0 +1,14 @@ +-- Version checking: where it is offered. The dialog and the settings page +-- are the module's own widgets, in containers the Host owns. + +local check = commands.get("com.bktus.gpgfrontend.module.version_checking.check_for_updates") + +ui.mount { id = "update", anchor = ui.anchor.dialog {}, widget = native.widget("update") } + +ui.mount { + id = "settings", + anchor = ui.anchor.settings { section = "application" }, + widget = native.widget("settings"), +} + +ui.action { id = "help", anchor = ui.anchor("main.menu.help"), command = check } |
