From 16ff4c5679e97d7ef428b8df495485d148f70409 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:48:34 +0200 Subject: [PATCH] Add option to prompt user for input on macro import --- CMakeLists.txt | 6 + data/locale/en-US.ini | 16 + lib/macro/macro-export-import-dialog.cpp | 133 +++++++- lib/macro/macro-export-import-dialog.hpp | 12 + lib/macro/macro-export-prompts-dialog.cpp | 358 ++++++++++++++++++++++ lib/macro/macro-export-prompts-dialog.hpp | 57 ++++ lib/macro/macro-import-prompts-dialog.cpp | 64 ++++ lib/macro/macro-import-prompts-dialog.hpp | 30 ++ lib/macro/macro-import-prompts.cpp | 74 +++++ lib/macro/macro-import-prompts.hpp | 24 ++ lib/utils/json-helpers.cpp | 88 ++++++ lib/utils/json-helpers.hpp | 9 + 12 files changed, 863 insertions(+), 8 deletions(-) create mode 100644 lib/macro/macro-export-prompts-dialog.cpp create mode 100644 lib/macro/macro-export-prompts-dialog.hpp create mode 100644 lib/macro/macro-import-prompts-dialog.cpp create mode 100644 lib/macro/macro-import-prompts-dialog.hpp create mode 100644 lib/macro/macro-import-prompts.cpp create mode 100644 lib/macro/macro-import-prompts.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 86378a8a..fff481f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,8 +144,14 @@ target_sources( lib/macro/macro-export-extensions.hpp lib/macro/macro-export-import-dialog.cpp lib/macro/macro-export-import-dialog.hpp + lib/macro/macro-export-prompts-dialog.cpp + lib/macro/macro-export-prompts-dialog.hpp lib/macro/macro-helpers.cpp lib/macro/macro-helpers.hpp + lib/macro/macro-import-prompts.cpp + lib/macro/macro-import-prompts.hpp + lib/macro/macro-import-prompts-dialog.cpp + lib/macro/macro-import-prompts-dialog.hpp lib/macro/macro-input.cpp lib/macro/macro-input.hpp lib/macro/macro-list.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index cad3b4f3..217f5d1c 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -233,10 +233,26 @@ AdvSceneSwitcher.macroTab.export.httpServers="HTTP Servers" AdvSceneSwitcher.macroTab.export.macroScheduleEntries="Macro Schedule" AdvSceneSwitcher.macroTab.export.twitchConnections="Twitch Connections" AdvSceneSwitcher.macroTab.export.websocketConnections="Websocket Connections" +AdvSceneSwitcher.macroTab.export.importPrompts.edit="Define import prompts..." +AdvSceneSwitcher.macroTab.export.importPrompts.info="Mark values below as \"replaceable\": whoever imports this macro will be prompted to supply their own value for each one you add (e.g. a scene, a source, or a file path that only exists on your setup)." +AdvSceneSwitcher.macroTab.export.importPrompts.scope="Apply to:" +AdvSceneSwitcher.macroTab.export.importPrompts.scope.all="All exported macros" +AdvSceneSwitcher.macroTab.export.importPrompts.values="Values found in the selected macro(s)" +AdvSceneSwitcher.macroTab.export.importPrompts.value="Value" +AdvSceneSwitcher.macroTab.export.importPrompts.occurrences="Occurrences" +AdvSceneSwitcher.macroTab.export.importPrompts.add="Mark as replaceable..." +AdvSceneSwitcher.macroTab.export.importPrompts.defined="Prompts defined for this export" +AdvSceneSwitcher.macroTab.export.importPrompts.remove="Remove" +AdvSceneSwitcher.macroTab.export.importPrompts.label="Prompt shown to the recipient" +AdvSceneSwitcher.macroTab.export.importPrompts.type="Type of value" +AdvSceneSwitcher.macroTab.export.importPrompts.defaultValue="Default value" +AdvSceneSwitcher.macroTab.export.importPrompts.options="Options to choose from:" 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!" AdvSceneSwitcher.macroTab.import.nameConflict="Cannot continue with import of \"%1\" as the name is already in use!\nDo want to choose a new name for the imported \"%2\"? (It will be skipped otherwise)" +AdvSceneSwitcher.macroTab.import.prompts.header="Provide values for the imported macros" +AdvSceneSwitcher.macroTab.import.prompts.info="This import contains values the macro's author marked as needing your input.\nDefaults provided by the author are already filled in.\nFields left empty are imported as empty values." AdvSceneSwitcher.macroTab.expandAll="Expand all" AdvSceneSwitcher.macroTab.collapseAll="Collapse all" AdvSceneSwitcher.macroTab.maximize="Maximize" diff --git a/lib/macro/macro-export-import-dialog.cpp b/lib/macro/macro-export-import-dialog.cpp index 7256dfcb..5ae5ea47 100644 --- a/lib/macro/macro-export-import-dialog.cpp +++ b/lib/macro/macro-export-import-dialog.cpp @@ -1,10 +1,13 @@ #include "macro-export-import-dialog.hpp" +#include "json-helpers.hpp" #include "macro-export-extensions.hpp" +#include "macro-import-prompts-dialog.hpp" #include "obs-module-helper.hpp" #include "section.hpp" #include #include +#include #include #include #include @@ -43,10 +46,60 @@ static bool isValidData(const QString &json) return !!data; } +static bool runImportPrompts(obs_data_t *data) +{ + auto prompts = LoadImportPrompts(data); + if (prompts.empty()) { + return true; + } + + MacroImportPromptsDialog dialog(nullptr, prompts); + if (dialog.exec() != QDialog::Accepted) { + return false; + } + + auto replacements = dialog.GetReplacements(); + if (replacements.empty()) { + return true; + } + + auto macrosJson = + GetJsonField(obs_data_get_json(data), "macros").value_or("[]"); + for (auto &[placeholder, value] : replacements) { + macrosJson = + ReplaceJsonStringValue(macrosJson, placeholder, value); + } + + std::string wrapped = "{\"macros\":" + macrosJson + "}"; + OBSDataAutoRelease wrapper = obs_data_create_from_json(wrapped.c_str()); + if (!wrapper) { + return true; + } + OBSDataArrayAutoRelease macros = obs_data_get_array(wrapper, "macros"); + obs_data_set_array(data, "macros", macros); + return true; +} + // --------------------------------------------------------------------------- // Extension section // --------------------------------------------------------------------------- +class ContentSizedScrollArea : public QScrollArea { +public: + using QScrollArea::QScrollArea; + + // Skip scrollbars and so on to avoid gap in layout + QSize minimumSizeHint() const override + { + auto hint = QScrollArea::minimumSizeHint(); + if (widget()) { + hint.setHeight(std::min(hint.height(), + widget()->sizeHint().height())); + } + return hint; + } +}; + QWidget *MacroExportImportDialog::BuildExtensionWidget() { auto wrapper = new QWidget(this); @@ -147,6 +200,8 @@ QWidget *MacroExportImportDialog::BuildExtensionWidget() }); innerSection->SetContent(subWidget, true); + connect(innerSection, &Section::AnimationFinished, this, + &MacroExportImportDialog::FitExtensionSection); outerLayout->addWidget(innerSection); _extensionUIs.append(std::move(ui)); _extensionOrder.push_back(extIdx); @@ -154,13 +209,19 @@ QWidget *MacroExportImportDialog::BuildExtensionWidget() outerLayout->addStretch(); - auto scrollArea = new QScrollArea(wrapper); + auto scrollArea = new ContentSizedScrollArea(wrapper); + _extensionScrollArea = scrollArea; scrollArea->setWidget(outerContent); scrollArea->setWidgetResizable(true); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setFrameShape(QFrame::NoFrame); wrapperLayout->addWidget(scrollArea); + auto promptsLayout = new QHBoxLayout(); + promptsLayout->addWidget(_editImportPrompts); + promptsLayout->addStretch(); + wrapperLayout->addLayout(promptsLayout); + return wrapper; } @@ -174,8 +235,18 @@ MacroExportImportDialog::MacroExportImportDialog(Type type, _baseJson(baseJson), _importExportString(new QPlainTextEdit(this)), _usePlainText(new QCheckBox(obs_module_text( - "AdvSceneSwitcher.macroTab.export.usePlainText"))) + "AdvSceneSwitcher.macroTab.export.usePlainText"))), + _editImportPrompts(new QPushButton(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.edit"))) { + if (type == Type::EXPORT_MACRO) { + OBSDataAutoRelease data = obs_data_create_from_json( + _baseJson.toStdString().c_str()); + if (data) { + _promptsState.Load(data); + } + } + _importExportString->setReadOnly(type == Type::EXPORT_MACRO); auto label = new QLabel(obs_module_text( @@ -200,12 +271,16 @@ MacroExportImportDialog::MacroExportImportDialog(Type type, connect(_usePlainText, &QCheckBox::stateChanged, this, &MacroExportImportDialog::UsePlainTextChanged); + _editImportPrompts->setVisible(type == Type::EXPORT_MACRO); + connect(_editImportPrompts, &QPushButton::clicked, this, + &MacroExportImportDialog::EditImportPrompts); + auto layout = new QVBoxLayout(this); if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) { auto splitter = new QSplitter(Qt::Vertical, this); splitter->setChildrenCollapsible(false); - splitter->addWidget(BuildExtensionWidget()); + _splitter = splitter; auto textWidget = new QWidget(splitter); auto textLayout = new QVBoxLayout(textWidget); @@ -214,12 +289,16 @@ MacroExportImportDialog::MacroExportImportDialog(Type type, textLayout->addWidget(_importExportString); textLayout->addWidget(_usePlainText); splitter->addWidget(textWidget); + splitter->addWidget(BuildExtensionWidget()); + splitter->setStretchFactor(0, 1); + splitter->setStretchFactor(1, 0); layout->addWidget(splitter); } else { layout->addWidget(label); layout->addWidget(_importExportString); layout->addWidget(_usePlainText); + layout->addWidget(_editImportPrompts); } layout->addWidget(buttons); @@ -244,6 +323,8 @@ QString MacroExportImportDialog::BuildExportJson() const return _baseJson; } + _promptsState.Apply(data); + const auto &extensions = GetMacroExportExtensions(); for (int i = 0; i < (int)_extensionUIs.size(); ++i) { const auto &ui = _extensionUIs[i]; @@ -286,6 +367,36 @@ void MacroExportImportDialog::UpdateExportString() RefreshExportText(); } +void MacroExportImportDialog::FitExtensionSection() +{ + if (!_splitter || !_extensionScrollArea || + !_extensionScrollArea->widget()) { + return; + } + + const int delta = _extensionScrollArea->widget()->sizeHint().height() - + _extensionScrollArea->viewport()->height(); + if (delta == 0) { + return; + } + + auto sizes = _splitter->sizes(); + const int minTextHeight = + _splitter->widget(0)->minimumSizeHint().height(); + const int change = std::min(delta, sizes[0] - minTextHeight); + sizes[0] -= change; + sizes[1] += change; + _splitter->setSizes(sizes); +} + +void MacroExportImportDialog::EditImportPrompts() +{ + if (!MacroExportPromptsDialog::Edit(this, _promptsState)) { + return; + } + RefreshExportText(); +} + void MacroExportImportDialog::UsePlainTextChanged(int value) { if (usePlainText == (!!value)) { @@ -330,13 +441,19 @@ bool MacroExportImportDialog::ImportMacros(QString &json) 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, {}); - } + if (!data) { + return true; + } + + if (!runImportPrompts(data)) { + return false; + } + json = QString::fromUtf8(obs_data_get_json(data)); + + for (const auto &ext : GetMacroExportExtensions()) { + ext.load(data, {}); } return true; diff --git a/lib/macro/macro-export-import-dialog.hpp b/lib/macro/macro-export-import-dialog.hpp index e634bba4..94e23931 100644 --- a/lib/macro/macro-export-import-dialog.hpp +++ b/lib/macro/macro-export-import-dialog.hpp @@ -1,13 +1,19 @@ #pragma once +#include "macro-export-prompts-dialog.hpp" + #include #include #include #include +#include #include #include +class QScrollArea; +class QSplitter; + namespace advss { class MacroExportImportDialog : public QDialog { @@ -21,6 +27,8 @@ public: private slots: void UsePlainTextChanged(int); void UpdateExportString(); + void EditImportPrompts(); + void FitExtensionSection(); private: explicit MacroExportImportDialog(Type type, @@ -38,8 +46,12 @@ private: void RefreshExportText(); QString _baseJson; + ExportPromptsState _promptsState; QPlainTextEdit *_importExportString; QCheckBox *_usePlainText; + QPushButton *_editImportPrompts; + QSplitter *_splitter = nullptr; + QScrollArea *_extensionScrollArea = nullptr; QList _extensionUIs; std::vector _extensionOrder; }; diff --git a/lib/macro/macro-export-prompts-dialog.cpp b/lib/macro/macro-export-prompts-dialog.cpp new file mode 100644 index 00000000..7f8d8c63 --- /dev/null +++ b/lib/macro/macro-export-prompts-dialog.cpp @@ -0,0 +1,358 @@ +#include "macro-export-prompts-dialog.hpp" +#include "json-helpers.hpp" +#include "obs-module-helper.hpp" +#include "string-list.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace advss { + +static std::string getArrayElementJson(const std::string &arrayJson, int index) +{ + try { + auto j = nlohmann::json::parse(arrayJson); + if (!j.is_array() || index < 0 || index >= (int)j.size()) { + return "{}"; + } + return j.at(index).dump(); + } catch (const nlohmann::json::exception &) { + return "{}"; + } +} + +static std::string setArrayElementJson(const std::string &arrayJson, int index, + const std::string &elementJson) +{ + try { + auto j = nlohmann::json::parse(arrayJson); + if (j.is_array() && index >= 0 && index < (int)j.size()) { + j[index] = nlohmann::json::parse(elementJson); + } + return j.dump(); + } catch (const nlohmann::json::exception &) { + return arrayJson; + } +} + +static int arraySize(const std::string &arrayJson) +{ + try { + auto j = nlohmann::json::parse(arrayJson); + return j.is_array() ? (int)j.size() : 0; + } catch (const nlohmann::json::exception &) { + return 0; + } +} + +void ExportPromptsState::Load(obs_data_t *data) +{ + OBSDataArrayAutoRelease macros = obs_data_get_array(data, "macros"); + OBSDataAutoRelease wrapper = obs_data_create(); + obs_data_set_array(wrapper, "macros", macros); + macrosArrayJson = GetJsonField(obs_data_get_json(wrapper), "macros") + .value_or("[]"); + entries.clear(); +} + +void ExportPromptsState::Apply(obs_data_t *data) const +{ + if (entries.empty()) { + return; + } + + std::string wrapped = "{\"macros\":" + macrosArrayJson + "}"; + OBSDataAutoRelease wrapper = obs_data_create_from_json(wrapped.c_str()); + if (!wrapper) { + return; + } + OBSDataArrayAutoRelease macros = obs_data_get_array(wrapper, "macros"); + obs_data_set_array(data, "macros", macros); + + std::vector prompts; + for (auto &entry : entries) { + prompts.push_back(entry.prompt); + } + SaveImportPrompts(data, prompts); +} + +MacroExportPromptsDialog::MacroExportPromptsDialog( + QWidget *parent, const ExportPromptsState &state) + : QDialog(parent), + _state(state) +{ + setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); + + auto layout = new QVBoxLayout(this); + + auto info = new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.info")); + info->setWordWrap(true); + layout->addWidget(info); + + auto scopeLayout = new QHBoxLayout(); + scopeLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.scope"))); + _scope = new QComboBox(this); + _scope->addItem(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.scope.all")); + int count = arraySize(_state.macrosArrayJson); + for (int i = 0; i < count; i++) { + auto name = GetJsonField(getArrayElementJson( + _state.macrosArrayJson, i), + "name") + .value_or(""); + _scope->addItem(QString::fromStdString(name)); + } + connect(_scope, qOverload(&QComboBox::currentIndexChanged), this, + &MacroExportPromptsDialog::ScopeChanged); + scopeLayout->addWidget(_scope, 1); + layout->addLayout(scopeLayout); + + auto contentLayout = new QHBoxLayout(); + + auto valuesLayout = new QVBoxLayout(); + valuesLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.values"))); + _values = new QTableWidget(0, 2, this); + _values->setHorizontalHeaderLabels( + {obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.value"), + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.occurrences")}); + _values->horizontalHeader()->setStretchLastSection(false); + _values->horizontalHeader()->setSectionResizeMode(0, + QHeaderView::Stretch); + _values->setEditTriggers(QAbstractItemView::NoEditTriggers); + _values->setSelectionBehavior(QAbstractItemView::SelectRows); + _values->setSelectionMode(QAbstractItemView::SingleSelection); + valuesLayout->addWidget(_values); + auto addButton = new QPushButton( + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.add"), + this); + connect(addButton, &QPushButton::clicked, this, + &MacroExportPromptsDialog::AddPrompt); + valuesLayout->addWidget(addButton); + contentLayout->addLayout(valuesLayout, 1); + + auto promptsLayout = new QVBoxLayout(); + promptsLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.defined"))); + _promptsList = new QListWidget(this); + promptsLayout->addWidget(_promptsList); + auto removeButton = new QPushButton( + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.remove"), + this); + connect(removeButton, &QPushButton::clicked, this, + &MacroExportPromptsDialog::RemovePrompt); + promptsLayout->addWidget(removeButton); + contentLayout->addLayout(promptsLayout, 1); + + layout->addLayout(contentLayout); + + auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttons->setCenterButtons(true); + layout->addWidget(buttons); + + resize(600, 400); + + RefreshValuesTable(); + RefreshPromptsList(); +} + +std::string MacroExportPromptsDialog::CurrentScopeJson() const +{ + int idx = _scope->currentIndex(); + if (idx <= 0) { + return _state.macrosArrayJson; + } + return getArrayElementJson(_state.macrosArrayJson, idx - 1); +} + +void MacroExportPromptsDialog::ApplyCurrentScopeJson(const std::string &json) +{ + int idx = _scope->currentIndex(); + if (idx <= 0) { + _state.macrosArrayJson = json; + return; + } + _state.macrosArrayJson = + setArrayElementJson(_state.macrosArrayJson, idx - 1, json); +} + +void MacroExportPromptsDialog::RefreshValuesTable() +{ + auto values = CollectDistinctJsonStringValues(CurrentScopeJson()); + _values->setRowCount(0); + for (auto &[value, occurrences] : values) { + int row = _values->rowCount(); + _values->insertRow(row); + _values->setItem( + row, 0, + new QTableWidgetItem(QString::fromStdString(value))); + _values->setItem( + row, 1, + new QTableWidgetItem(QString::number(occurrences))); + } +} + +void MacroExportPromptsDialog::RefreshPromptsList() +{ + _promptsList->clear(); + for (auto &entry : _state.entries) { + _promptsList->addItem(QString("%1 (%2)").arg( + QString::fromStdString(entry.prompt.label), + QString::fromStdString(entry.scope))); + } +} + +void MacroExportPromptsDialog::ScopeChanged(int) +{ + RefreshValuesTable(); +} + +void MacroExportPromptsDialog::AddPrompt() +{ + int row = _values->currentRow(); + if (row < 0) { + return; + } + auto value = _values->item(row, 0)->text(); + + QDialog dialog(this); + dialog.setWindowTitle(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.add")); + auto layout = new QVBoxLayout(&dialog); + auto form = new QFormLayout(); + + auto labelEdit = new QLineEdit(&dialog); + form->addRow( + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.label"), + labelEdit); + + auto typeCombo = new QComboBox(&dialog); + PopulateValueInputTypeSelection(typeCombo); + typeCombo->setCurrentIndex( + typeCombo->findData(static_cast(ValueInputType::String))); + form->addRow( + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.type"), + typeCombo); + + auto defaultValueEdit = new QLineEdit(value, &dialog); + form->addRow( + obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.defaultValue"), + defaultValueEdit); + + auto optionsWidget = new QWidget(&dialog); + auto optionsLayout = new QVBoxLayout(optionsWidget); + optionsLayout->setContentsMargins(0, 0, 0, 0); + optionsLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.export.importPrompts.options"))); + auto optionsEdit = new StringListEdit(optionsWidget); + optionsEdit->SetStringList({value.toStdString()}); + optionsLayout->addWidget(optionsEdit); + optionsWidget->hide(); + connect(typeCombo, qOverload(&QComboBox::currentIndexChanged), + &dialog, [&dialog, typeCombo, optionsWidget](int) { + optionsWidget->setVisible( + typeCombo->currentData().toInt() == + static_cast(ValueInputType::Selection)); + dialog.adjustSize(); + }); + + layout->addLayout(form); + layout->addWidget(optionsWidget); + + auto buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttons, &QDialogButtonBox::accepted, &dialog, + &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, + &QDialog::reject); + buttons->setCenterButtons(true); + layout->addWidget(buttons); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + auto label = labelEdit->text().toStdString(); + if (label.empty()) { + return; + } + auto type = + static_cast(typeCombo->currentData().toInt()); + + ExportPromptsState::Entry entry; + entry.prompt.label = label; + entry.prompt.type = type; + entry.prompt.defaultValue = defaultValueEdit->text().toStdString(); + if (type == ValueInputType::Selection) { + for (const auto &option : optionsEdit->GetStringList()) { + entry.prompt.options.push_back( + option.UnresolvedValue()); + } + } + entry.scope = _scope->currentText().toStdString(); + + auto placeholder = GenerateImportPlaceholder(); + ApplyCurrentScopeJson(ReplaceJsonStringValue( + CurrentScopeJson(), value.toStdString(), placeholder)); + entry.prompt.placeholders.push_back(placeholder); + entry.originalValues.push_back(value.toStdString()); + + _state.entries.emplace_back(std::move(entry)); + + RefreshValuesTable(); + RefreshPromptsList(); +} + +void MacroExportPromptsDialog::RemovePrompt() +{ + int idx = _promptsList->currentRow(); + if (idx < 0 || idx >= (int)_state.entries.size()) { + return; + } + + auto &entry = _state.entries[idx]; + for (size_t i = 0; i < entry.prompt.placeholders.size(); i++) { + _state.macrosArrayJson = ReplaceJsonStringValue( + _state.macrosArrayJson, entry.prompt.placeholders[i], + entry.originalValues[i]); + } + + _state.entries.erase(_state.entries.begin() + idx); + RefreshValuesTable(); + RefreshPromptsList(); +} + +bool MacroExportPromptsDialog::Edit(QWidget *parent, ExportPromptsState &state) +{ + MacroExportPromptsDialog dialog(parent, state); + if (dialog.exec() != QDialog::Accepted) { + return false; + } + state = dialog._state; + return true; +} + +} // namespace advss diff --git a/lib/macro/macro-export-prompts-dialog.hpp b/lib/macro/macro-export-prompts-dialog.hpp new file mode 100644 index 00000000..67c4dded --- /dev/null +++ b/lib/macro/macro-export-prompts-dialog.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "macro-import-prompts.hpp" + +#include +#include +#include +#include + +class QComboBox; +class QTableWidget; +class QListWidget; + +namespace advss { + +struct ExportPromptsState { + struct Entry { + ImportPrompt prompt; + std::vector originalValues; + std::string scope; + }; + + void Load(obs_data_t *data); + void Apply(obs_data_t *data) const; + + std::string macrosArrayJson; + std::vector entries; +}; + +class MacroExportPromptsDialog : public QDialog { + Q_OBJECT + +public: + static bool Edit(QWidget *parent, ExportPromptsState &state); + +private slots: + void ScopeChanged(int); + void AddPrompt(); + void RemovePrompt(); + +private: + MacroExportPromptsDialog(QWidget *parent, + const ExportPromptsState &state); + + void RefreshValuesTable(); + void RefreshPromptsList(); + std::string CurrentScopeJson() const; + void ApplyCurrentScopeJson(const std::string &json); + + ExportPromptsState _state; + + QComboBox *_scope; + QTableWidget *_values; + QListWidget *_promptsList; +}; + +} // namespace advss diff --git a/lib/macro/macro-import-prompts-dialog.cpp b/lib/macro/macro-import-prompts-dialog.cpp new file mode 100644 index 00000000..b7cb6067 --- /dev/null +++ b/lib/macro/macro-import-prompts-dialog.cpp @@ -0,0 +1,64 @@ +#include "macro-import-prompts-dialog.hpp" +#include "help-icon.hpp" +#include "obs-module-helper.hpp" + +#include +#include +#include +#include +#include + +namespace advss { + +MacroImportPromptsDialog::MacroImportPromptsDialog( + QWidget *parent, const std::vector &prompts) + : QDialog(parent) +{ + setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); + + auto layout = new QVBoxLayout(this); + + auto headerLayout = new QHBoxLayout(); + headerLayout->addWidget(new QLabel(obs_module_text( + "AdvSceneSwitcher.macroTab.import.prompts.header"))); + headerLayout->addWidget(new HelpIcon( + obs_module_text("AdvSceneSwitcher.macroTab.import.prompts.info"), + this)); + headerLayout->addStretch(); + layout->addLayout(headerLayout); + + auto form = new QFormLayout(); + for (const auto &prompt : prompts) { + QStringList options; + for (const auto &option : prompt.options) { + options << QString::fromStdString(option); + } + auto input = new ValueInputWidget( + prompt.type, prompt.defaultValue, options, this); + form->addRow(QString::fromStdString(prompt.label), input); + _rows.push_back({prompt, input}); + } + layout->addLayout(form); + + auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttons->setCenterButtons(true); + layout->addWidget(buttons); +} + +std::unordered_map +MacroImportPromptsDialog::GetReplacements() const +{ + std::unordered_map result; + for (const auto &row : _rows) { + auto value = row.input->Value(); + for (const auto &placeholder : row.prompt.placeholders) { + result[placeholder] = value; + } + } + return result; +} + +} // namespace advss diff --git a/lib/macro/macro-import-prompts-dialog.hpp b/lib/macro/macro-import-prompts-dialog.hpp new file mode 100644 index 00000000..3e918021 --- /dev/null +++ b/lib/macro/macro-import-prompts-dialog.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "macro-import-prompts.hpp" + +#include +#include +#include +#include + +namespace advss { + +class MacroImportPromptsDialog : public QDialog { + Q_OBJECT + +public: + MacroImportPromptsDialog(QWidget *parent, + const std::vector &prompts); + + std::unordered_map GetReplacements() const; + +private: + struct Row { + ImportPrompt prompt; + ValueInputWidget *input; + }; + + std::vector _rows; +}; + +} // namespace advss diff --git a/lib/macro/macro-import-prompts.cpp b/lib/macro/macro-import-prompts.cpp new file mode 100644 index 00000000..ead1f80b --- /dev/null +++ b/lib/macro/macro-import-prompts.cpp @@ -0,0 +1,74 @@ +#include "macro-import-prompts.hpp" + +namespace advss { + +static void saveStrings(obs_data_t *obj, const char *name, + const std::vector &strings) +{ + obs_data_array_t *array = obs_data_array_create(); + for (auto &string : strings) { + obs_data_t *entry = obs_data_create(); + obs_data_set_string(entry, "value", string.c_str()); + obs_data_array_push_back(array, entry); + obs_data_release(entry); + } + obs_data_set_array(obj, name, array); + obs_data_array_release(array); +} + +static std::vector loadStrings(obs_data_t *obj, const char *name) +{ + std::vector result; + obs_data_array_t *array = obs_data_get_array(obj, name); + size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + obs_data_t *entry = obs_data_array_item(array, i); + result.emplace_back(obs_data_get_string(entry, "value")); + obs_data_release(entry); + } + obs_data_array_release(array); + return result; +} + +void SaveImportPrompts(obs_data_t *obj, + const std::vector &prompts) +{ + obs_data_array_t *array = obs_data_array_create(); + for (auto &prompt : prompts) { + obs_data_t *entry = obs_data_create(); + obs_data_set_string(entry, "label", prompt.label.c_str()); + obs_data_set_int(entry, "type", static_cast(prompt.type)); + saveStrings(entry, "placeholders", prompt.placeholders); + obs_data_set_string(entry, "defaultValue", + prompt.defaultValue.c_str()); + saveStrings(entry, "options", prompt.options); + obs_data_array_push_back(array, entry); + obs_data_release(entry); + } + obs_data_set_array(obj, "importPrompts", array); + obs_data_array_release(array); +} + +std::vector LoadImportPrompts(obs_data_t *obj) +{ + std::vector result; + obs_data_array_t *array = obs_data_get_array(obj, "importPrompts"); + size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + obs_data_t *entry = obs_data_array_item(array, i); + ImportPrompt prompt; + prompt.label = obs_data_get_string(entry, "label"); + prompt.type = static_cast( + obs_data_get_int(entry, "type")); + prompt.placeholders = loadStrings(entry, "placeholders"); + prompt.defaultValue = + obs_data_get_string(entry, "defaultValue"); + prompt.options = loadStrings(entry, "options"); + result.emplace_back(std::move(prompt)); + obs_data_release(entry); + } + obs_data_array_release(array); + return result; +} + +} // namespace advss diff --git a/lib/macro/macro-import-prompts.hpp b/lib/macro/macro-import-prompts.hpp new file mode 100644 index 00000000..84565635 --- /dev/null +++ b/lib/macro/macro-import-prompts.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "export-symbol-helper.hpp" +#include "value-input.hpp" + +#include +#include +#include + +namespace advss { + +struct ImportPrompt { + std::string label; + ValueInputType type = ValueInputType::String; + std::vector placeholders; + std::string defaultValue; + std::vector options; +}; + +EXPORT void SaveImportPrompts(obs_data_t *obj, + const std::vector &prompts); +EXPORT std::vector LoadImportPrompts(obs_data_t *obj); + +} // namespace advss diff --git a/lib/utils/json-helpers.cpp b/lib/utils/json-helpers.cpp index 5987afa0..03dbd45d 100644 --- a/lib/utils/json-helpers.cpp +++ b/lib/utils/json-helpers.cpp @@ -6,6 +6,9 @@ #endif #include #include +#include +#include +#include namespace advss { @@ -118,4 +121,89 @@ ExtractSingleJsonArrayElement(const std::string &jsonStr) return {}; } +static void collectStrings(const nlohmann::json &json, + std::vector> &result, + std::unordered_map &index) +{ + if (json.is_string()) { + auto value = json.get(); + auto it = index.find(value); + if (it == index.end()) { + index[value] = result.size(); + result.emplace_back(value, 1); + } else { + result[it->second].second++; + } + return; + } + if (json.is_object()) { + for (const auto &item : json.items()) { + collectStrings(item.value(), result, index); + } + return; + } + if (json.is_array()) { + for (const auto &value : json) { + collectStrings(value, result, index); + } + } +} + +std::vector> +CollectDistinctJsonStringValues(const std::string &jsonStr) +{ + std::vector> result; + try { + nlohmann::json json = nlohmann::json::parse(jsonStr); + std::unordered_map index; + collectStrings(json, result, index); + } catch (const nlohmann::json::exception &) { + } + return result; +} + +static void replaceStrings(nlohmann::json &json, const std::string &oldValue, + const std::string &newValue) +{ + if (json.is_string()) { + if (json.get() == oldValue) { + json = newValue; + } + return; + } + if (json.is_object()) { + for (auto &item : json.items()) { + replaceStrings(item.value(), oldValue, newValue); + } + return; + } + if (json.is_array()) { + for (auto &value : json) { + replaceStrings(value, oldValue, newValue); + } + } +} + +std::string ReplaceJsonStringValue(const std::string &jsonStr, + const std::string &oldValue, + const std::string &newValue) +{ + try { + nlohmann::json json = nlohmann::json::parse(jsonStr); + replaceStrings(json, oldValue, newValue); + return json.dump(); + } catch (const nlohmann::json::exception &) { + return jsonStr; + } +} + +std::string GenerateImportPlaceholder() +{ + char *uuid = os_generate_uuid(); + std::string result = + "ADVSS_IMPORT_PLACEHOLDER_" + std::string(uuid ? uuid : ""); + bfree(uuid); + return result; +} + } // namespace advss diff --git a/lib/utils/json-helpers.hpp b/lib/utils/json-helpers.hpp index be66af58..c53251aa 100644 --- a/lib/utils/json-helpers.hpp +++ b/lib/utils/json-helpers.hpp @@ -4,6 +4,8 @@ #include #include +#include +#include namespace advss { @@ -20,4 +22,11 @@ EXPORT std::optional AccessJsonArrayIndex(const std::string &json, EXPORT std::optional ExtractSingleJsonArrayElement(const std::string &json); +EXPORT std::vector> +CollectDistinctJsonStringValues(const std::string &json); +EXPORT std::string ReplaceJsonStringValue(const std::string &json, + const std::string &oldValue, + const std::string &newValue); +EXPORT std::string GenerateImportPlaceholder(); + } // namespace advss