From c0d27cc0db5d069acf68c8b93ffb7c760efca03b Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:54:00 +0200 Subject: [PATCH] Add API to expand export / import dialog --- CMakeLists.txt | 3 + data/locale/en-US.ini | 3 + lib/macro/macro-export-builtin-extensions.cpp | 89 ++++++ lib/macro/macro-export-extensions.cpp | 17 ++ lib/macro/macro-export-extensions.hpp | 41 +++ lib/macro/macro-export-import-dialog.cpp | 257 +++++++++++++++--- lib/macro/macro-export-import-dialog.hpp | 26 +- lib/macro/macro-tab.cpp | 9 +- 8 files changed, 400 insertions(+), 45 deletions(-) create mode 100644 lib/macro/macro-export-builtin-extensions.cpp create mode 100644 lib/macro/macro-export-extensions.cpp create mode 100644 lib/macro/macro-export-extensions.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6afeb9cb..887b53ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,9 @@ target_sources( lib/macro/macro-dock-window.hpp lib/macro/macro-edit.cpp lib/macro/macro-edit.hpp + lib/macro/macro-export-builtin-extensions.cpp + lib/macro/macro-export-extensions.cpp + lib/macro/macro-export-extensions.hpp lib/macro/macro-export-import-dialog.cpp lib/macro/macro-export-import-dialog.hpp lib/macro/macro-helpers.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 1c58b335..83c87593 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -202,6 +202,9 @@ AdvSceneSwitcher.macroTab.remove="Remove" AdvSceneSwitcher.macroTab.export="Export" AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:" AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text" +AdvSceneSwitcher.macroTab.export.additionalContent="Export Additional Content" +AdvSceneSwitcher.macroTab.export.variables="Variables" +AdvSceneSwitcher.macroTab.export.actionQueues="Action Queues" AdvSceneSwitcher.macroTab.import="Import" AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:" AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!" diff --git a/lib/macro/macro-export-builtin-extensions.cpp b/lib/macro/macro-export-builtin-extensions.cpp new file mode 100644 index 00000000..d0291099 --- /dev/null +++ b/lib/macro/macro-export-builtin-extensions.cpp @@ -0,0 +1,89 @@ +#include "macro-export-extensions.hpp" + +#include "action-queue.hpp" +#include "variable.hpp" + +#include + +namespace advss { + +static bool setup(); +static bool setupDone = setup(); + +static bool setup() +{ + // --- Variables --- + AddMacroExportExtension( + {"AdvSceneSwitcher.macroTab.export.variables", "variables", + [](obs_data_t *data, const QStringList &selectedIds) { + if (selectedIds.isEmpty()) { + SaveVariables(data); + return; + } + OBSDataArrayAutoRelease array = + obs_data_array_create(); + for (const auto &v : GetVariables()) { + const QString name = + QString::fromStdString(v->Name()); + if (!selectedIds.contains(name)) { + continue; + } + OBSDataAutoRelease item = obs_data_create(); + v->Save(item); + obs_data_array_push_back(array, item); + } + obs_data_set_array(data, "variables", array); + }, + [](obs_data_t *data, const QStringList &) { + ImportVariables(data); + }, + []() -> QList> { + QList> items; + for (const auto &v : GetVariables()) { + const QString name = + QString::fromStdString(v->Name()); + items.append({name, name}); + } + return items; + }}); + + // --- Action Queues --- + AddMacroExportExtension( + {"AdvSceneSwitcher.macroTab.export.actionQueues", + "actionQueues", + [](obs_data_t *data, const QStringList &selectedIds) { + if (selectedIds.isEmpty()) { + SaveActionQueues(data); + return; + } + OBSDataArrayAutoRelease array = + obs_data_array_create(); + for (const auto &q : GetActionQueues()) { + const QString name = + QString::fromStdString(q->Name()); + if (!selectedIds.contains(name)) { + continue; + } + OBSDataAutoRelease item = obs_data_create(); + q->Save(item); + obs_data_array_push_back(array, item); + } + obs_data_set_array(data, "actionQueues", array); + }, + [](obs_data_t *data, const QStringList &) { + ImportQueues(data); + }, + []() -> QList> { + QList> items; + for (const auto &q : GetActionQueues()) { + const QString name = + QString::fromStdString(q->Name()); + items.append({name, name}); + } + return items; + }}); + + return true; +} + +} // namespace advss diff --git a/lib/macro/macro-export-extensions.cpp b/lib/macro/macro-export-extensions.cpp new file mode 100644 index 00000000..5a49fc08 --- /dev/null +++ b/lib/macro/macro-export-extensions.cpp @@ -0,0 +1,17 @@ +#include "macro-export-extensions.hpp" + +namespace advss { + +static std::vector extensions; + +void AddMacroExportExtension(const MacroExportExtension &ext) +{ + extensions.emplace_back(ext); +} + +const std::vector &GetMacroExportExtensions() +{ + return extensions; +} + +} // namespace advss diff --git a/lib/macro/macro-export-extensions.hpp b/lib/macro/macro-export-extensions.hpp new file mode 100644 index 00000000..f994bb43 --- /dev/null +++ b/lib/macro/macro-export-extensions.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "export-symbol-helper.hpp" + +#include +#include +#include +#include +#include +#include + +namespace advss { + +struct MacroExportExtension { + const char *displayNameKey; + const char *jsonKey; + + // Export callback. + // Write content into 'data'. 'selectedIds' contains the item IDs the + // user explicitly checked; if empty (no per-item widget provided) save + // everything. + std::function + save; + + // Import callback. + // Read content from 'data'. 'selectedIds' contains the item IDs the + // user explicitly checked; if empty import every item found in 'data'. + std::function + load; + + // Optional: enumerate items available for export as (id, displayName) + // pairs. When nullptr the extension appears as a single all-or-nothing + // checkbox in the export dialog. + std::function>()> getExportItems = + nullptr; +}; + +EXPORT void AddMacroExportExtension(const MacroExportExtension &ext); +const std::vector &GetMacroExportExtensions(); + +} // namespace advss diff --git a/lib/macro/macro-export-import-dialog.cpp b/lib/macro/macro-export-import-dialog.cpp index 35290e94..6b548318 100644 --- a/lib/macro/macro-export-import-dialog.cpp +++ b/lib/macro/macro-export-import-dialog.cpp @@ -1,22 +1,160 @@ #include "macro-export-import-dialog.hpp" +#include "macro-export-extensions.hpp" #include "obs-module-helper.hpp" #include -#include -#include #include +#include +#include +#include +#include namespace advss { static bool usePlainText = false; -MacroExportImportDialog::MacroExportImportDialog(Type type) +// --------------------------------------------------------------------------- +// Compress / decompress helpers +// --------------------------------------------------------------------------- + +static QString compressMacroString(const QString &input) +{ + QByteArray inputData = input.toUtf8(); + auto compressedData = qCompress(inputData); + QByteArray encodedData = compressedData.toBase64(); + return QString::fromUtf8(encodedData); +} + +static QString decompressMacroString(const QString &input) +{ + QByteArray encodedData = input.toUtf8(); + QByteArray compressedData = QByteArray::fromBase64(encodedData); + auto outputData = qUncompress(compressedData); + return QString::fromUtf8(outputData); +} + +static bool isValidData(const QString &json) +{ + OBSDataAutoRelease data = + obs_data_create_from_json(json.toStdString().c_str()); + return !!data; +} + +// --------------------------------------------------------------------------- +// Extension section +// --------------------------------------------------------------------------- + +Section *MacroExportImportDialog::BuildExtensionSection() +{ + auto outerSection = new Section(300, this); + outerSection->AddHeaderWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.additionalContent"))); + + auto outerContent = new QWidget(); + auto outerLayout = new QVBoxLayout(outerContent); + outerLayout->setContentsMargins(4, 4, 4, 4); + outerLayout->setSpacing(4); + + const auto &extensions = GetMacroExportExtensions(); + + // Build a sorted index list (alphabetical by translated display name). + std::vector order(extensions.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](int a, int b) { + return QString::localeAwareCompare( + obs_module_text(extensions[a].displayNameKey), + obs_module_text(extensions[b].displayNameKey)) < + 0; + }); + + for (int extIdx : order) { + const auto &ext = extensions[extIdx]; + ExtensionUI ui; + + // Simple all-or-nothing checkbox (no sub-items). + if (!ext.getExportItems) { + ui.mainCheck = new QCheckBox( + obs_module_text(ext.displayNameKey), + outerContent); + ui.mainCheck->setChecked(false); + connect(ui.mainCheck, &QCheckBox::stateChanged, this, + &MacroExportImportDialog::UpdateExportString); + outerLayout->addWidget(ui.mainCheck); + _extensionUIs.append(std::move(ui)); + continue; + } + + // Extensions with per-item selection get their own inner + // Section: the main checkbox sits in the header (next to the + // toggle arrow) and the per-item checkboxes live in the + // collapsible content area. + const auto items = ext.getExportItems(); + if (items.isEmpty()) { + continue; + } + + auto innerSection = new Section(200, outerContent); + + ui.mainCheck = + new QCheckBox(obs_module_text(ext.displayNameKey)); + ui.mainCheck->setChecked(false); + innerSection->AddHeaderWidget(ui.mainCheck); + + auto subWidget = new QWidget(); + auto subLayout = new QVBoxLayout(subWidget); + subLayout->setContentsMargins(4, 2, 4, 2); + subLayout->setSpacing(2); + + for (const auto &[id, displayName] : items) { + auto cb = new QCheckBox(displayName, subWidget); + cb->setChecked(false); + subLayout->addWidget(cb); + ui.itemChecks.append({id, cb}); + connect(cb, &QCheckBox::stateChanged, this, + &MacroExportImportDialog::UpdateExportString); + } + + // Main checkbox toggles all sub-items. + // Capture uiIdx (position in _extensionUIs after append). + const int uiIdx = _extensionUIs.size(); + connect(ui.mainCheck, &QCheckBox::stateChanged, this, + [this, uiIdx](int state) { + if (state == Qt::PartiallyChecked) + return; + const bool checked = (state == Qt::Checked); + if (uiIdx < _extensionUIs.size()) { + for (auto &[id, cb] : + _extensionUIs[uiIdx].itemChecks) { + QSignalBlocker b(cb); + cb->setChecked(checked); + } + } + UpdateExportString(); + }); + + innerSection->SetContent(subWidget, false); + outerLayout->addWidget(innerSection); + _extensionUIs.append(std::move(ui)); + } + + outerSection->SetContent(outerContent, true); + return outerSection; +} + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +MacroExportImportDialog::MacroExportImportDialog(Type type, + const QString &baseJson) : QDialog(nullptr), + _baseJson(baseJson), _importExportString(new QPlainTextEdit(this)), _usePlainText(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.export.usePlainText"))) { _importExportString->setReadOnly(type == Type::EXPORT_MACRO); + auto label = new QLabel(obs_module_text( type == Type::EXPORT_MACRO ? "AdvSceneSwitcher.macroTab.export.info" @@ -39,7 +177,12 @@ MacroExportImportDialog::MacroExportImportDialog(Type type) connect(_usePlainText, &QCheckBox::stateChanged, this, &MacroExportImportDialog::UsePlainTextChanged); - auto layout = new QVBoxLayout; + auto layout = new QVBoxLayout(this); + + if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) { + layout->addWidget(BuildExtensionSection()); + } + layout->addWidget(label); layout->addWidget(_importExportString); layout->addWidget(_usePlainText); @@ -47,32 +190,57 @@ MacroExportImportDialog::MacroExportImportDialog(Type type) setLayout(layout); setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); + + if (type == Type::EXPORT_MACRO) { + RefreshExportText(); + } } -QString compressMacroString(const QString &input) +// --------------------------------------------------------------------------- +// Export string helpers +// --------------------------------------------------------------------------- + +QString MacroExportImportDialog::BuildExportJson() const { - QByteArray inputData = input.toUtf8(); - auto compressedData = qCompress(inputData); - QByteArray encodedData = compressedData.toBase64(); - return QString::fromUtf8(encodedData); + OBSDataAutoRelease data = + obs_data_create_from_json(_baseJson.toStdString().c_str()); + if (!data) { + return _baseJson; + } + + const auto &extensions = GetMacroExportExtensions(); + for (int i = 0; + i < (int)extensions.size() && i < (int)_extensionUIs.size(); ++i) { + if (!_extensionUIs[i].mainCheck->isChecked()) { + continue; + } + + QStringList selectedIds; + for (const auto &[id, cb] : _extensionUIs[i].itemChecks) { + if (cb->isChecked()) { + selectedIds << id; + } + } + extensions[i].save(data, selectedIds); + } + + const char *json = obs_data_get_json(data); + return json ? QString::fromUtf8(json) : QString{}; } -QString decompressMacroString(const QString &input) +void MacroExportImportDialog::RefreshExportText() { - QByteArray encodedData = input.toUtf8(); - QByteArray compressedData = QByteArray::fromBase64(encodedData); - auto outputData = qUncompress(compressedData); - return QString::fromUtf8(outputData); + const QString json = BuildExportJson(); + if (usePlainText) { + _importExportString->setPlainText(json); + } else { + _importExportString->setPlainText(compressMacroString(json)); + } } -void MacroExportImportDialog::ExportMacros(const QString &json) +void MacroExportImportDialog::UpdateExportString() { - MacroExportImportDialog dialog( - MacroExportImportDialog::Type::EXPORT_MACRO); - dialog._importExportString->setPlainText(compressMacroString(json)); - dialog.adjustSize(); - dialog.updateGeometry(); - dialog.exec(); + RefreshExportText(); } void MacroExportImportDialog::UsePlainTextChanged(int value) @@ -90,26 +258,45 @@ void MacroExportImportDialog::UsePlainTextChanged(int value) usePlainText = value; } -static bool isValidData(const QString &json) +// --------------------------------------------------------------------------- +// Static entry points +// --------------------------------------------------------------------------- + +void MacroExportImportDialog::ExportMacros(obs_data_t *macroData) { - OBSDataAutoRelease data = - obs_data_create_from_json(json.toStdString().c_str()); - return !!data; + const char *rawJson = obs_data_get_json(macroData); + const QString baseJson = rawJson ? QString::fromUtf8(rawJson) + : QString{}; + + MacroExportImportDialog dialog(Type::EXPORT_MACRO, baseJson); + dialog.adjustSize(); + dialog.updateGeometry(); + dialog.exec(); } bool MacroExportImportDialog::ImportMacros(QString &json) { - MacroExportImportDialog dialog( - MacroExportImportDialog::Type::IMPORT_MACRO); - if (dialog.exec() == QDialog::Accepted) { - json = decompressMacroString( - dialog._importExportString->toPlainText()); - if (!isValidData(json)) { // Fallback to support raw json format - json = dialog._importExportString->toPlainText(); - } - return true; + MacroExportImportDialog dialog(Type::IMPORT_MACRO); + if (dialog.exec() != QDialog::Accepted) { + return false; } - return false; + + json = decompressMacroString(dialog._importExportString->toPlainText()); + if (!isValidData(json)) { + // Fallback: support raw (uncompressed) JSON pasted directly. + json = dialog._importExportString->toPlainText(); + } + + // Invoke all extension load callbacks. + OBSDataAutoRelease data = + obs_data_create_from_json(json.toStdString().c_str()); + if (data) { + for (const auto &ext : GetMacroExportExtensions()) { + ext.load(data, {}); + } + } + + return true; } } // namespace advss diff --git a/lib/macro/macro-export-import-dialog.hpp b/lib/macro/macro-export-import-dialog.hpp index c180f454..040dfacd 100644 --- a/lib/macro/macro-export-import-dialog.hpp +++ b/lib/macro/macro-export-import-dialog.hpp @@ -1,25 +1,47 @@ #pragma once + +#include "section.hpp" + #include #include +#include #include +#include + namespace advss { class MacroExportImportDialog : public QDialog { Q_OBJECT public: enum class Type { EXPORT_MACRO, IMPORT_MACRO }; - MacroExportImportDialog(Type type); - static void ExportMacros(const QString &json); + static void ExportMacros(obs_data_t *macroData); static bool ImportMacros(QString &json); private slots: void UsePlainTextChanged(int); + void UpdateExportString(); private: + explicit MacroExportImportDialog(Type type, + const QString &baseJson = {}); + + // Per-extension widgets + struct ExtensionUI { + QCheckBox *mainCheck = nullptr; + // (itemId, checkbox) pairs - empty when no per-item selection. + QList> itemChecks; + }; + + Section *BuildExtensionSection(); + QString BuildExportJson() const; + void RefreshExportText(); + + QString _baseJson; QPlainTextEdit *_importExportString; QCheckBox *_usePlainText; + QList _extensionUIs; }; } // namespace advss diff --git a/lib/macro/macro-tab.cpp b/lib/macro/macro-tab.cpp index d5aba05b..ecc3ad43 100644 --- a/lib/macro/macro-tab.cpp +++ b/lib/macro/macro-tab.cpp @@ -1,5 +1,4 @@ #include "advanced-scene-switcher.hpp" -#include "action-queue.hpp" #include "macro-action-edit.hpp" #include "macro-condition-edit.hpp" #include "macro-export-import-dialog.hpp" @@ -316,13 +315,9 @@ void AdvSceneSwitcher::ExportMacros() const obs_data_array_push_back(macroArray, obj); } obs_data_set_array(data, "macros", macroArray); - SaveVariables(data); - SaveActionQueues(data); obs_data_set_string(data, "version", g_GIT_TAG); - auto json = obs_data_get_json(data); - QString exportString(json); - MacroExportImportDialog::ExportMacros(exportString); + MacroExportImportDialog::ExportMacros(data); } bool AdvSceneSwitcher::ResolveMacroImportNameConflict( @@ -384,8 +379,6 @@ void AdvSceneSwitcher::ImportMacros() ImportMacros(); return; } - ImportVariables(data); - ImportQueues(data); auto version = obs_data_get_string(data, "version"); if (strcmp(version, g_GIT_TAG) != 0) {