diff --git a/CMakeLists.txt b/CMakeLists.txt index f55b43a3..6e17bc23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,6 +263,8 @@ target_sources( src/utils/filter-combo-box.hpp src/utils/filter-selection.cpp src/utils/filter-selection.hpp + src/utils/macro-export-import-dialog.cpp + src/utils/macro-export-import-dialog.hpp src/utils/macro-list.cpp src/utils/macro-list.hpp src/utils/macro-segment-selection.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 3bfedef3..a6ea5bab 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -92,6 +92,12 @@ AdvSceneSwitcher.macroTab.group="Group Selected Macros" AdvSceneSwitcher.macroTab.ungroup="Ungroup Selected Groups" AdvSceneSwitcher.macroTab.rename="Rename" 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.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 importing macro \"%1\" as a macro with the same name already exists!\nDo you want to continue with the import of \"%2\" and choose a new name? (Will be skipped otherwise)" AdvSceneSwitcher.macroTab.expandAll="Expand all" AdvSceneSwitcher.macroTab.collapseAll="Collapse all" AdvSceneSwitcher.macroTab.maximize="Maximize" diff --git a/src/advanced-scene-switcher.hpp b/src/advanced-scene-switcher.hpp index 318793a0..c62e562c 100644 --- a/src/advanced-scene-switcher.hpp +++ b/src/advanced-scene-switcher.hpp @@ -112,8 +112,7 @@ public slots: void on_actionUp_clicked(); void on_actionDown_clicked(); void on_actionBottom_clicked(); - void MacroSelectionChanged(const QItemSelection &, - const QItemSelection &); + void MacroSelectionChanged(); void UpMacroSegementHotkey(); void DownMacroSegementHotkey(); void DeleteMacroSegementHotkey(); @@ -122,6 +121,8 @@ public slots: void ShowMacroConditionsContextMenu(const QPoint &); void CopyMacro(); void RenameCurrentMacro(); + void ExportMacros(); + void ImportMacros(); void ExpandAllActions(); void ExpandAllConditions(); void CollapseAllActions(); @@ -165,6 +166,7 @@ signals: void VariableRemoved(const QString &); private: + bool ResolveMacroImportNameConflict(std::shared_ptr &); bool MacroTabIsInFocus(); MacroSegmentList *conditionsList = nullptr; diff --git a/src/macro-core/macro-tab.cpp b/src/macro-core/macro-tab.cpp index 91542c27..25eaaace 100644 --- a/src/macro-core/macro-tab.cpp +++ b/src/macro-core/macro-tab.cpp @@ -6,7 +6,9 @@ #include "switcher-data.hpp" #include "name-dialog.hpp" #include "macro-properties.hpp" +#include "macro-export-import-dialog.hpp" #include "utility.hpp" +#include "version.h" #include #include @@ -18,7 +20,7 @@ namespace advss { static QMetaObject::Connection addPulse; static QTimer onChangeHighlightTimer; -static bool macroNameExists(std::string name) +static bool macroNameExists(const std::string &name) { return !!GetMacroByName(name.c_str()); } @@ -60,12 +62,8 @@ bool AdvSceneSwitcher::AddNewMacro(std::shared_ptr &res, return false; } - { - auto lock = LockContext(); - res = std::make_shared( - name, - switcher->macroProperties._newMacroRegisterHotkeys); - } + res = std::make_shared( + name, switcher->macroProperties._newMacroRegisterHotkeys); return true; } @@ -77,11 +75,7 @@ void AdvSceneSwitcher::on_macroAdd_clicked() return; } - { - auto lock = LockContext(); - ui->macros->Add(newMacro); - } - + ui->macros->Add(newMacro); ui->macroAdd->disconnect(addPulse); emit MacroAdded(QString::fromStdString(name)); } @@ -101,11 +95,7 @@ void AdvSceneSwitcher::RemoveMacro(std::shared_ptr ¯o) } } - { - auto lock = LockContext(); - ui->macros->Remove(macro); - } - + ui->macros->Remove(macro); emit MacroRemoved(name); } @@ -152,7 +142,6 @@ void AdvSceneSwitcher::on_macroRemove_clicked() void AdvSceneSwitcher::on_macroUp_clicked() { - auto lock = LockContext(); auto macro = GetSelectedMacro(); if (!macro) { return; @@ -162,7 +151,6 @@ void AdvSceneSwitcher::on_macroUp_clicked() void AdvSceneSwitcher::on_macroDown_clicked() { - auto lock = LockContext(); auto macro = GetSelectedMacro(); if (!macro) { return; @@ -202,6 +190,179 @@ void AdvSceneSwitcher::RenameCurrentMacro() ui->macroName->setText(QString::fromStdString(name)); } +static void addGroupSubitems(std::vector> ¯os, + const std::shared_ptr &group) +{ + std::vector> subitems; + subitems.reserve(group->GroupSize()); + + // Find all subitems + for (auto it = switcher->macros.begin(); it < switcher->macros.end(); + it++) { + if ((*it)->Name() == group->Name()) { + for (uint32_t i = 1; i <= group->GroupSize(); i++) { + subitems.emplace_back(*std::next(it, i)); + } + break; + } + } + + // Remove subitems which were already selected to avoid duplicates + for (const auto &subitem : subitems) { + auto it = std::find(macros.begin(), macros.end(), subitem); + if (it == macros.end()) { + continue; + } + macros.erase(it); + } + + // Add group subitems + auto it = std::find(macros.begin(), macros.end(), group); + if (it == macros.end()) { + return; + } + it = std::next(it); + macros.insert(it, subitems.begin(), subitems.end()); +} + +void AdvSceneSwitcher::ExportMacros() +{ + auto selectedMacros = GetSelectedMacros(); + auto macros = selectedMacros; + for (const auto ¯o : selectedMacros) { + if (macro->IsGroup() && macro->GroupSize() > 0) { + addGroupSubitems(macros, macro); + } + } + + auto data = obs_data_create(); + auto macroArray = obs_data_array_create(); + for (const auto ¯o : macros) { + obs_data_t *obj = obs_data_create(); + macro->Save(obj); + obs_data_array_push_back(macroArray, obj); + obs_data_release(obj); + } + obs_data_set_array(data, "macros", macroArray); + obs_data_array_release(macroArray); + obs_data_set_string(data, "version", g_GIT_TAG); + auto json = obs_data_get_json(data); + QString exportString(json); + obs_data_release(data); + + MacroExportImportDialog::ExportMacros(exportString); +} + +bool AdvSceneSwitcher::ResolveMacroImportNameConflict( + std::shared_ptr ¯o) +{ + QString errorMesg = obs_module_text( + "AdvSceneSwitcher.macroTab.import.nameConflict"); + errorMesg = errorMesg.arg(QString::fromStdString(macro->Name()), + QString::fromStdString(macro->Name())); + bool continueResolve = DisplayMessage(errorMesg, true); + if (!continueResolve) { + return false; + } + + QString format = QString::fromStdString(macro->Name()) + " %1"; + int i = 2; + + QString placeHolderText = format.arg(i); + while ((macroNameExists(placeHolderText.toUtf8().constData()))) { + placeHolderText = format.arg(++i); + } + + std::string newName; + bool accepted = AdvSSNameDialog::AskForName( + this, obs_module_text("AdvSceneSwitcher.macroTab.add"), + obs_module_text("AdvSceneSwitcher.macroTab.name"), newName, + placeHolderText); + + if (!accepted) { + return false; + } + + if (newName.empty()) { + return false; + } + + if (macroNameExists(newName)) { + DisplayMessage( + obs_module_text("AdvSceneSwitcher.macroTab.exists")); + return ResolveMacroImportNameConflict(macro); + } + + macro->SetName(newName); + return true; +} + +void AdvSceneSwitcher::ImportMacros() +{ + QString json; + if (!MacroExportImportDialog::ImportMacros(json)) { + return; + } + auto data = obs_data_create_from_json(json.toStdString().c_str()); + if (!data) { + DisplayMessage(obs_module_text( + "AdvSceneSwitcher.macroTab.import.invalid")); + ImportMacros(); + return; + } + + auto version = obs_data_get_string(data, "version"); + if (strcmp(version, g_GIT_TAG) != 0) { + blog(LOG_WARNING, + "importing macros from non matching plugin version \"%s\"", + version); + } + + auto array = obs_data_get_array(data, "macros"); + size_t count = obs_data_array_count(array); + + int groupSize = 0; + std::shared_ptr group; + + auto lock = LockContext(); + + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(array, i); + auto macro = std::make_shared(); + macro->Load(array_obj); + macro->PostLoad(); + + if (macroNameExists(macro->Name()) && + !ResolveMacroImportNameConflict(macro)) { + obs_data_release(array_obj); + groupSize--; + continue; + } + + switcher->macros.emplace_back(macro); + if (groupSize > 0 && !macro->IsGroup()) { + Macro::PrepareMoveToGroup(group, macro); + groupSize--; + } + + if (macro->IsGroup()) { + group = macro; + groupSize = macro->GroupSize(); + // We are not sure if all elements will be added so we + // have to reset the group size to zero and add elements + // to the group as they come up. + macro->ResetGroupSize(); + } + + obs_data_release(array_obj); + } + obs_data_array_release(array); + obs_data_release(data); + + ui->macros->Reset(switcher->macros, + switcher->macroProperties._highlightExecuted); +} + void AdvSceneSwitcher::on_macroName_editingFinished() { auto macro = GetSelectedMacro(); @@ -372,8 +533,7 @@ std::vector> AdvSceneSwitcher::GetSelectedMacros() return ui->macros->GetCurrentMacros(); } -void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &, - const QItemSelection &) +void AdvSceneSwitcher::MacroSelectionChanged() { if (loading) { return; @@ -445,12 +605,8 @@ void AdvSceneSwitcher::SetupMacroTab() } ui->macros->Reset(switcher->macros, switcher->macroProperties._highlightExecuted); - connect(ui->macros->selectionModel(), - SIGNAL(selectionChanged(const QItemSelection &, - const QItemSelection &)), - this, - SLOT(MacroSelectionChanged(const QItemSelection &, - const QItemSelection &))); + connect(ui->macros, SIGNAL(MacroSelectionChanged()), this, + SLOT(MacroSelectionChanged())); delete conditionsList; conditionsList = new MacroSegmentList(this); @@ -569,6 +725,15 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos) obs_module_text("AdvSceneSwitcher.macroTab.remove"), this, &AdvSceneSwitcher::on_macroRemove_clicked); remove->setDisabled(ui->macros->SelectionEmpty()); + menu.addSeparator(); + + auto exportAction = menu.addAction( + obs_module_text("AdvSceneSwitcher.macroTab.export"), this, + &AdvSceneSwitcher::ExportMacros); + exportAction->setDisabled(ui->macros->SelectionEmpty()); + auto import = menu.addAction( + obs_module_text("AdvSceneSwitcher.macroTab.import"), this, + &AdvSceneSwitcher::ImportMacros); menu.exec(globalPos); } diff --git a/src/macro-core/macro-tree.cpp b/src/macro-core/macro-tree.cpp index a93314b7..ea94e027 100644 --- a/src/macro-core/macro-tree.cpp +++ b/src/macro-core/macro-tree.cpp @@ -395,6 +395,7 @@ CountItemsVisibleInModel(const std::deque> ¯os) void MacroTreeModel::Add(std::shared_ptr item) { + std::lock_guard lock(switcher->m); auto idx = CountItemsVisibleInModel(_macros); beginInsertRows(QModelIndex(), idx, idx); _macros.emplace_back(item); @@ -407,6 +408,7 @@ void MacroTreeModel::Add(std::shared_ptr item) void MacroTreeModel::Remove(std::shared_ptr item) { + std::lock_guard lock(switcher->m); auto uiStartIdx = GetItemModelIndex(item); if (uiStartIdx == -1) { return; @@ -759,6 +761,12 @@ void MacroTree::Reset(std::deque> ¯os, MacroTreeModel *mtm = new MacroTreeModel(this, macros); setModel(mtm); GetModel()->Reset(macros); + connect(selectionModel(), + SIGNAL(selectionChanged(const QItemSelection &, + const QItemSelection &)), + this, + SLOT(SelectionChangedHelper(const QItemSelection &, + const QItemSelection &))); } void MacroTree::Add(std::shared_ptr item, @@ -1173,6 +1181,7 @@ void MacroTree::Remove(std::shared_ptr item) const void MacroTree::Up(std::shared_ptr item) const { + std::lock_guard lock(switcher->m); auto above = GetModel()->Neighbor(item, true); if (!above) { return; @@ -1198,6 +1207,7 @@ void MacroTree::Up(std::shared_ptr item) const void MacroTree::Down(std::shared_ptr item) const { + std::lock_guard lock(switcher->m); auto below = GetModel()->Neighbor(item, false); if (!below) { return; @@ -1247,6 +1257,12 @@ void MacroTree::UngroupSelectedGroups() assert(GetModel()->IsInValidState()); } +void MacroTree::SelectionChangedHelper(const QItemSelection &, + const QItemSelection &) +{ + emit MacroSelectionChanged(); +} + inline MacroTreeItem *MacroTree::GetItemWidget(int idx) const { QWidget *widget = indexWidget(GetModel()->createIndex(idx, 0, nullptr)); diff --git a/src/macro-core/macro-tree.hpp b/src/macro-core/macro-tree.hpp index 00774cbf..eb40f9e9 100644 --- a/src/macro-core/macro-tree.hpp +++ b/src/macro-core/macro-tree.hpp @@ -138,6 +138,11 @@ public: public slots: void GroupSelectedItems(); void UngroupSelectedGroups(); + void SelectionChangedHelper(const QItemSelection &, + const QItemSelection &); + +signals: + void MacroSelectionChanged(); protected: virtual void dropEvent(QDropEvent *event) override; diff --git a/src/macro-core/macro.hpp b/src/macro-core/macro.hpp index f4b3a117..efbdde24 100644 --- a/src/macro-core/macro.hpp +++ b/src/macro-core/macro.hpp @@ -60,6 +60,7 @@ public: std::shared_ptr item); bool IsGroup() const { return _isGroup; } uint32_t GroupSize() const { return _groupSize; } + void ResetGroupSize() { _groupSize = 0; } bool IsSubitem() const { return !_parent.expired(); } void SetCollapsed(bool val) { _isCollapsed = val; } bool IsCollapsed() const { return _isCollapsed; } diff --git a/src/utils/macro-export-import-dialog.cpp b/src/utils/macro-export-import-dialog.cpp new file mode 100644 index 00000000..5eb66f37 --- /dev/null +++ b/src/utils/macro-export-import-dialog.cpp @@ -0,0 +1,61 @@ +#include "macro-export-import-dialog.hpp" +#include "obs-module-helper.hpp" + +#include +#include +#include + +namespace advss { + +MacroExportImportDialog::MacroExportImportDialog(Type type) + : QDialog(nullptr), _importExportString(new QPlainTextEdit(this)) +{ + _importExportString->setReadOnly(type == Type::EXPORT_MACRO); + auto label = new QLabel(obs_module_text( + type == Type::EXPORT_MACRO + ? "AdvSceneSwitcher.macroTab.export.info" + : "AdvSceneSwitcher.macroTab.import.info")); + label->setWordWrap(true); + + QDialogButtonBox *buttons = nullptr; + if (type == Type::EXPORT_MACRO) { + buttons = new QDialogButtonBox(QDialogButtonBox::Ok); + } else { + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + } + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttons->setCenterButtons(true); + + auto layout = new QVBoxLayout; + layout->addWidget(label); + layout->addWidget(_importExportString); + layout->addWidget(buttons); + setLayout(layout); + + setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); +} + +void MacroExportImportDialog::ExportMacros(const QString &json) +{ + MacroExportImportDialog dialog( + MacroExportImportDialog::Type::EXPORT_MACRO); + dialog._importExportString->setPlainText(json); + dialog.adjustSize(); + dialog.updateGeometry(); + dialog.exec(); +} + +bool MacroExportImportDialog::ImportMacros(QString &json) +{ + MacroExportImportDialog dialog( + MacroExportImportDialog::Type::IMPORT_MACRO); + if (dialog.exec() == QDialog::Accepted) { + json = dialog._importExportString->toPlainText(); + return true; + } + return false; +} + +} // namespace advss diff --git a/src/utils/macro-export-import-dialog.hpp b/src/utils/macro-export-import-dialog.hpp new file mode 100644 index 00000000..087196f1 --- /dev/null +++ b/src/utils/macro-export-import-dialog.hpp @@ -0,0 +1,20 @@ +#pragma once +#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 bool ImportMacros(QString &json); + +private: + QPlainTextEdit *_importExportString; +}; + +} // namespace advss