From a80af327d702ab0294163a37e8bac14267c2b23b Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Sat, 31 Dec 2022 15:41:01 +0100 Subject: [PATCH] Enable grouping of macros Based on OBS's source-tree model/view implementation --- CMakeLists.txt | 4 +- data/locale/en-US.ini | 6 +- forms/advanced-scene-switcher.ui | 70 +- src/advanced-scene-switcher.hpp | 9 +- src/macro-core/macro-action-edit.cpp | 9 +- src/macro-core/macro-condition-edit.cpp | 8 +- src/macro-core/macro-list-entry-widget.cpp | 72 -- src/macro-core/macro-list-entry-widget.hpp | 32 - src/macro-core/macro-selection.cpp | 12 +- src/macro-core/macro-tab.cpp | 280 ++--- src/macro-core/macro-tree.cpp | 1201 ++++++++++++++++++++ src/macro-core/macro-tree.hpp | 160 +++ src/macro-core/macro.cpp | 96 ++ src/macro-core/macro.hpp | 26 +- 14 files changed, 1653 insertions(+), 332 deletions(-) delete mode 100644 src/macro-core/macro-list-entry-widget.cpp delete mode 100644 src/macro-core/macro-list-entry-widget.hpp create mode 100644 src/macro-core/macro-tree.cpp create mode 100644 src/macro-core/macro-tree.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 781a990c..a82e11cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,8 +217,6 @@ target_sources( src/macro-core/macro-condition-window.hpp src/macro-core/macro-condition.cpp src/macro-core/macro-condition.hpp - src/macro-core/macro-list-entry-widget.cpp - src/macro-core/macro-list-entry-widget.hpp src/macro-core/macro-properties.cpp src/macro-core/macro-properties.hpp src/macro-core/macro-ref.cpp @@ -230,6 +228,8 @@ target_sources( src/macro-core/macro-selection.cpp src/macro-core/macro-selection.hpp src/macro-core/macro-tab.cpp + src/macro-core/macro-tree.cpp + src/macro-core/macro-tree.hpp src/macro-core/macro.cpp src/macro-core/macro.hpp) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 2ce84699..cd6219fe 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -79,8 +79,12 @@ AdvSceneSwitcher.macroTab.runFail="Running \"%1\" failed!\nEither one of the act AdvSceneSwitcher.macroTab.runInParallel="Run macro in parallel to other macros" AdvSceneSwitcher.macroTab.onChange="Perform actions only on condition change" AdvSceneSwitcher.macroTab.defaultname="Macro %1" +AdvSceneSwitcher.macroTab.defaultGroupName="Group %1" AdvSceneSwitcher.macroTab.exists="Macro name exists already" -AdvSceneSwitcher.macroTab.copy="Create copy" +AdvSceneSwitcher.macroTab.groupDeleteConfirm="Are you sure you want to delete \"%1\" and all its elements?" +AdvSceneSwitcher.macroTab.copy="Create copy of current macro" +AdvSceneSwitcher.macroTab.group="Group selected items" +AdvSceneSwitcher.macroTab.ungroup="Ungroup selected groups" AdvSceneSwitcher.macroTab.expandAll="Expand all" AdvSceneSwitcher.macroTab.collapseAll="Collapse all" AdvSceneSwitcher.macroTab.maximize="Maximize" diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui index 1cf0228c..29da7d31 100644 --- a/forms/advanced-scene-switcher.ui +++ b/forms/advanced-scene-switcher.ui @@ -634,49 +634,26 @@ - - - - - - 0 - 0 - - - - true - - - QAbstractItemView::InternalMove - - - Qt::MoveAction - - - 0 - - - false - - - - - - - false - - - AdvSceneSwitcher.macroTab.help - - - Qt::AlignCenter - - - true - - - - + + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::TargetMoveAction + + + QAbstractItemView::ExtendedSelection + + + 0 + + @@ -4560,6 +4537,13 @@ + + + MacroTree + QListView +
macro-tree.hpp
+
+
diff --git a/src/advanced-scene-switcher.hpp b/src/advanced-scene-switcher.hpp index 8bbc7e3a..fe19d5db 100644 --- a/src/advanced-scene-switcher.hpp +++ b/src/advanced-scene-switcher.hpp @@ -44,8 +44,9 @@ public: void OpenSequenceExtendEdit(SequenceWidget *sw); void SetEditSceneGroup(SceneGroup &sg); - bool addNewMacro(std::string &name, std::string format = ""); - Macro *getSelectedMacro(); + bool addNewMacro(std::shared_ptr &res, std::string &name, + std::string format = ""); + std::shared_ptr getSelectedMacro(); void SetEditMacro(Macro &m); void SetMacroEditAreaDisabled(bool); void HighlightAction(int idx); @@ -126,7 +127,6 @@ public slots: void on_runMacro_clicked(); void on_runMacroInParallel_stateChanged(int value); void on_runMacroOnChange_stateChanged(int value); - void on_macros_currentRowChanged(int idx); void on_conditionAdd_clicked(); void on_conditionRemove_clicked(); void on_conditionUp_clicked(); @@ -135,6 +135,8 @@ public slots: void on_actionRemove_clicked(); void on_actionUp_clicked(); void on_actionDown_clicked(); + void MacroSelectionChanged(const QItemSelection &, + const QItemSelection &); void UpMacroSegementHotkey(); void DownMacroSegementHotkey(); void DeleteMacroSegementHotkey(); @@ -165,7 +167,6 @@ public slots: void ResetOpacityActionControls(); void ResetOpacityConditionControls(); void HighlightControls(); - void MacroDragDropReorder(QModelIndex, int, int, QModelIndex, int); void HighlightOnChange(); void on_macroProperties_clicked(); diff --git a/src/macro-core/macro-action-edit.cpp b/src/macro-core/macro-action-edit.cpp index 06bce2f3..c06bcad8 100644 --- a/src/macro-core/macro-action-edit.cpp +++ b/src/macro-core/macro-action-edit.cpp @@ -172,8 +172,9 @@ void AdvSceneSwitcher::AddMacroAction(int idx) } { std::lock_guard lock(switcher->m); - macro->Actions().emplace(macro->Actions().begin() + idx, - MacroActionFactory::Create(id, macro)); + macro->Actions().emplace( + macro->Actions().begin() + idx, + MacroActionFactory::Create(id, macro.get())); if (idx - 1 >= 0) { auto data = obs_data_create(); macro->Actions().at(idx - 1)->Save(data); @@ -298,7 +299,7 @@ void AdvSceneSwitcher::MoveMacroActionUp(int idx) return; } - SwapActions(macro, idx, idx - 1); + SwapActions(macro.get(), idx, idx - 1); HighlightAction(idx - 1); } @@ -313,7 +314,7 @@ void AdvSceneSwitcher::MoveMacroActionDown(int idx) return; } - SwapActions(macro, idx, idx + 1); + SwapActions(macro.get(), idx, idx + 1); HighlightAction(idx + 1); } diff --git a/src/macro-core/macro-condition-edit.cpp b/src/macro-core/macro-condition-edit.cpp index 3806cb06..3550a060 100644 --- a/src/macro-core/macro-condition-edit.cpp +++ b/src/macro-core/macro-condition-edit.cpp @@ -380,7 +380,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx) logic = macro->Conditions().at(idx - 1)->GetLogicType(); } } else { - MacroConditionScene temp(macro); + MacroConditionScene temp(macro.get()); id = temp.GetId(); logic = LogicType::ROOT_NONE; } @@ -388,7 +388,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx) std::lock_guard lock(switcher->m); auto cond = macro->Conditions().emplace( macro->Conditions().begin() + idx, - MacroConditionFactory::Create(id, macro)); + MacroConditionFactory::Create(id, macro.get())); if (idx - 1 >= 0) { auto data = obs_data_create(); macro->Conditions().at(idx - 1)->Save(data); @@ -535,7 +535,7 @@ void AdvSceneSwitcher::MoveMacroConditionUp(int idx) return; } - SwapConditions(macro, idx, idx - 1); + SwapConditions(macro.get(), idx, idx - 1); HighlightCondition(idx - 1); } @@ -550,7 +550,7 @@ void AdvSceneSwitcher::MoveMacroConditionDown(int idx) return; } - SwapConditions(macro, idx, idx + 1); + SwapConditions(macro.get(), idx, idx + 1); HighlightCondition(idx + 1); } diff --git a/src/macro-core/macro-list-entry-widget.cpp b/src/macro-core/macro-list-entry-widget.cpp deleted file mode 100644 index 0131496a..00000000 --- a/src/macro-core/macro-list-entry-widget.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "macro-list-entry-widget.hpp" -#include "macro.hpp" -#include "utility.hpp" - -MacroListEntryWidget::MacroListEntryWidget(std::shared_ptr macro, - bool highlight, QWidget *parent) - : QWidget(parent), - _name(new QLabel(QString::fromStdString(macro->Name()))), - _running(new QCheckBox), - _macro(macro), - _highlightExecutedMacros(highlight) -{ - _running->setChecked(!macro->Paused()); - - setStyleSheet("\ - QCheckBox { background-color: rgba(0,0,0,0); }\ - QLabel { background-color: rgba(0,0,0,0); }"); - - auto layout = new QHBoxLayout; - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(_running); - layout->addWidget(_name); - layout->addStretch(); - layout->setSizeConstraint(QLayout::SetFixedSize); - setLayout(layout); - - connect(_running, SIGNAL(stateChanged(int)), this, - SLOT(PauseChanged(int))); - connect(window(), SIGNAL(HighlightMacrosChanged(bool)), this, - SLOT(EnableHighlight(bool))); - _timer.setInterval(1500); - connect(&_timer, SIGNAL(timeout()), this, SLOT(HighlightExecuted())); - connect(&_timer, SIGNAL(timeout()), this, SLOT(UpdatePaused())); - _timer.start(); -} - -void MacroListEntryWidget::PauseChanged(int state) -{ - _macro->SetPaused(!state); -} - -void MacroListEntryWidget::SetName(const QString &name) -{ - _name->setText(name); -} - -void MacroListEntryWidget::SetMacro(std::shared_ptr &m) -{ - _macro = m; -} - -void MacroListEntryWidget::EnableHighlight(bool value) -{ - _highlightExecutedMacros = value; -} - -void MacroListEntryWidget::HighlightExecuted() -{ - if (!_highlightExecutedMacros) { - return; - } - - if (_macro && _macro->WasExecutedRecently()) { - PulseWidget(this, Qt::green, QColor(0, 0, 0, 0), true); - } -} - -void MacroListEntryWidget::UpdatePaused() -{ - const QSignalBlocker b(_running); - _running->setChecked(!_macro->Paused()); -} diff --git a/src/macro-core/macro-list-entry-widget.hpp b/src/macro-core/macro-list-entry-widget.hpp deleted file mode 100644 index 8084cea1..00000000 --- a/src/macro-core/macro-list-entry-widget.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class Macro; - -class MacroListEntryWidget : public QWidget { - Q_OBJECT - -public: - MacroListEntryWidget(std::shared_ptr, bool highlight, - QWidget *parent); - void SetName(const QString &); - void SetMacro(std::shared_ptr &); - -private slots: - void PauseChanged(int); - void HighlightExecuted(); - void UpdatePaused(); - void EnableHighlight(bool); - -private: - QTimer _timer; - QLabel *_name; - QCheckBox *_running; - std::shared_ptr _macro; - - bool _highlightExecutedMacros = false; -}; diff --git a/src/macro-core/macro-selection.cpp b/src/macro-core/macro-selection.cpp index d480c358..260ba496 100644 --- a/src/macro-core/macro-selection.cpp +++ b/src/macro-core/macro-selection.cpp @@ -17,7 +17,10 @@ MacroSelection::MacroSelection(QWidget *parent) : QComboBox(parent) firstItem->setSelectable(false); firstItem->setEnabled(false); - for (auto &m : switcher->macros) { + for (const auto &m : switcher->macros) { + if (m->IsGroup()) { + continue; + } addItem(QString::fromStdString(m->Name())); } @@ -51,7 +54,12 @@ void MacroSelection::HideSelectedMacro() if (!ssWindow) { return; } - int idx = ssWindow->ui->macros->currentRow(); + + const auto m = ssWindow->ui->macros->GetCurrentMacro(); + if (!m) { + return; + } + int idx = findText(QString::fromStdString(m->Name())); if (idx == -1) { return; } diff --git a/src/macro-core/macro-tab.cpp b/src/macro-core/macro-tab.cpp index 2c5892c8..33e31cb6 100644 --- a/src/macro-core/macro-tab.cpp +++ b/src/macro-core/macro-tab.cpp @@ -1,5 +1,5 @@ #include "macro.hpp" -#include "macro-list-entry-widget.hpp" +#include "macro-tree.hpp" #include "macro-action-edit.hpp" #include "macro-condition-edit.hpp" #include "advanced-scene-switcher.hpp" @@ -20,7 +20,8 @@ bool macroNameExists(std::string name) return !!GetMacroByName(name.c_str()); } -bool AdvSceneSwitcher::addNewMacro(std::string &name, std::string format) +bool AdvSceneSwitcher::addNewMacro(std::shared_ptr &res, + std::string &name, std::string format) { QString fmt; int i = 1; @@ -58,116 +59,78 @@ bool AdvSceneSwitcher::addNewMacro(std::string &name, std::string format) { std::lock_guard lock(switcher->m); - switcher->macros.emplace_back(std::make_shared( + res = std::make_shared( name, - switcher->macroProperties._newMacroRegisterHotkeys)); + switcher->macroProperties._newMacroRegisterHotkeys); } return true; } -QListWidgetItem *AddNewMacroListEntry(QListWidget *list, - std::shared_ptr ¯o) -{ - QListWidgetItem *item = new QListWidgetItem(list); - item->setData(Qt::UserRole, QString::fromStdString(macro->Name())); - auto listEntry = new MacroListEntryWidget( - macro, switcher->macroProperties._highlightExecuted, list); - list->setItemWidget(item, listEntry); - return item; -} - void AdvSceneSwitcher::on_macroAdd_clicked() { std::string name; - if (!addNewMacro(name)) { + std::shared_ptr newMacro; + if (!addNewMacro(newMacro, name)) { return; } - QString text = QString::fromStdString(name); - auto item = AddNewMacroListEntry(ui->macros, switcher->macros.back()); - ui->macros->setCurrentItem(item); + { + std::lock_guard lock(switcher->m); + ui->macros->Add(newMacro); + } ui->macroAdd->disconnect(addPulse); - ui->macroHelp->setVisible(false); - emit MacroAdded(QString::fromStdString(name)); } void AdvSceneSwitcher::on_macroRemove_clicked() { - QListWidgetItem *item = ui->macros->currentItem(); - if (!item) { + auto macro = getSelectedMacro(); + if (!macro) { return; } - int idx = ui->macros->currentRow(); - delete item; - QString name; - { - std::lock_guard lock(switcher->m); - switcher->abortMacroWait = true; - switcher->macroWaitCv.notify_all(); - name = QString::fromStdString(switcher->macros[idx]->Name()); - switcher->macros.erase(switcher->macros.begin() + idx); - for (auto &m : switcher->macros) { - m->ResolveMacroRef(); + auto name = QString::fromStdString(macro->Name()); + if (macro->IsGroup()) { + QString deleteWarning = obs_module_text( + "AdvSceneSwitcher.macroTab.groupDeleteConfirm"); + if (!DisplayMessage(deleteWarning.arg(name), true)) { + return; } } - if (ui->macros->count() == 0) { - ui->macroHelp->setVisible(true); + { + std::lock_guard lock(switcher->m); + ui->macros->Remove(macro); } + emit MacroRemoved(name); } void AdvSceneSwitcher::on_macroUp_clicked() { std::lock_guard lock(switcher->m); - if (!listMoveUp(ui->macros)) { + auto macro = getSelectedMacro(); + if (!macro) { return; } - - int index = ui->macros->currentRow() + 1; - auto *entry1 = static_cast( - ui->macros->itemWidget(ui->macros->item(index))); - auto *entry2 = static_cast( - ui->macros->itemWidget(ui->macros->item(index - 1))); - entry1->SetMacro(*(switcher->macros.begin() + index - 1)); - entry2->SetMacro(*(switcher->macros.begin() + index)); - iter_swap(switcher->macros.begin() + index, - switcher->macros.begin() + index - 1); - - for (auto &m : switcher->macros) { - m->ResolveMacroRef(); - } + ui->macros->Up(macro); } void AdvSceneSwitcher::on_macroDown_clicked() { std::lock_guard lock(switcher->m); - if (!listMoveDown(ui->macros)) { + auto macro = getSelectedMacro(); + if (!macro) { return; } - - int index = ui->macros->currentRow() - 1; - auto *entry1 = static_cast( - ui->macros->itemWidget(ui->macros->item(index))); - auto *entry2 = static_cast( - ui->macros->itemWidget(ui->macros->item(index + 1))); - entry1->SetMacro(*(switcher->macros.begin() + index + 1)); - entry2->SetMacro(*(switcher->macros.begin() + index)); - iter_swap(switcher->macros.begin() + index, - switcher->macros.begin() + index + 1); - - for (auto &m : switcher->macros) { - m->ResolveMacroRef(); - } + ui->macros->Down(macro); } void AdvSceneSwitcher::on_macroName_editingFinished() { bool nameValid = true; - Macro *macro = getSelectedMacro(); + auto macro = getSelectedMacro(); if (!macro) { return; } @@ -189,22 +152,24 @@ void AdvSceneSwitcher::on_macroName_editingFinished() std::lock_guard lock(switcher->m); if (nameValid) { macro->SetName(newName.toUtf8().constData()); - QListWidgetItem *item = ui->macros->currentItem(); - item->setData(Qt::UserRole, newName); - auto listEntry = static_cast( - ui->macros->itemWidget(item)); - listEntry->SetName(newName); + auto macro = getSelectedMacro(); + if (!macro) { + return; + } + macro->SetName(newName.toStdString()); } else { ui->macroName->setText(oldName); } } - emit MacroRenamed(oldName, newName); + if (nameValid) { + emit MacroRenamed(oldName, newName); + } } void AdvSceneSwitcher::on_runMacro_clicked() { - Macro *macro = getSelectedMacro(); + auto macro = getSelectedMacro(); if (!macro) { return; } @@ -219,7 +184,7 @@ void AdvSceneSwitcher::on_runMacro_clicked() void AdvSceneSwitcher::on_runMacroInParallel_stateChanged(int value) { - Macro *macro = getSelectedMacro(); + auto macro = getSelectedMacro(); if (!macro) { return; } @@ -229,7 +194,7 @@ void AdvSceneSwitcher::on_runMacroInParallel_stateChanged(int value) void AdvSceneSwitcher::on_runMacroOnChange_stateChanged(int value) { - Macro *macro = getSelectedMacro(); + auto macro = getSelectedMacro(); if (!macro) { return; } @@ -313,6 +278,10 @@ void AdvSceneSwitcher::SetEditMacro(Macro &m) PopulateMacroConditions(m); PopulateMacroActions(m); SetMacroEditAreaDisabled(false); + if (m.IsGroup()) { + SetMacroEditAreaDisabled(true); + ui->macroName->setEnabled(true); + } currentActionIdx = -1; currentConditionIdx = -1; @@ -340,25 +309,20 @@ void AdvSceneSwitcher::HighlightCondition(int idx) conditionsList->Highlight(idx); } -Macro *AdvSceneSwitcher::getSelectedMacro() +std::shared_ptr AdvSceneSwitcher::getSelectedMacro() { - QListWidgetItem *item = ui->macros->currentItem(); - - if (!item) { - return nullptr; - } - - QString name = item->data(Qt::UserRole).toString(); - return GetMacroByQString(name); + return ui->macros->GetCurrentMacro(); } -void AdvSceneSwitcher::on_macros_currentRowChanged(int idx) +void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &, + const QItemSelection &) { if (loading) { return; } - if (idx == -1) { + auto macro = getSelectedMacro(); + if (!macro) { SetMacroEditAreaDisabled(true); conditionsList->Clear(); actionsList->Clear(); @@ -366,44 +330,21 @@ void AdvSceneSwitcher::on_macros_currentRowChanged(int idx) actionsList->SetHelpMsgVisible(true); return; } - - QListWidgetItem *item = ui->macros->item(idx); - QString macroName = item->data(Qt::UserRole).toString(); - - auto macro = GetMacroByQString(macroName); - if (macro) { - SetEditMacro(*macro); - } -} - -void AdvSceneSwitcher::MacroDragDropReorder(QModelIndex, int from, int, - QModelIndex, int to) -{ - std::lock_guard lock(switcher->m); - if (from > to) { - std::rotate(switcher->macros.rend() - from - 1, - switcher->macros.rend() - from, - switcher->macros.rend() - to); - } else { - std::rotate(switcher->macros.begin() + from, - switcher->macros.begin() + from + 1, - switcher->macros.begin() + to); - } - - for (auto &m : switcher->macros) { - m->ResolveMacroRef(); - } + SetEditMacro(*macro); } void AdvSceneSwitcher::HighlightOnChange() { + if (!switcher->macroProperties._highlightExecuted) { + return; + } + auto macro = getSelectedMacro(); if (!macro) { return; } - if (switcher->macroProperties._highlightExecuted && - macro->OnChangePreventedActionsRecently()) { + if (macro->OnChangePreventedActionsRecently()) { PulseWidget(ui->runMacroOnChange, Qt::yellow, Qt::transparent, true); } @@ -413,7 +354,7 @@ void AdvSceneSwitcher::on_macroProperties_clicked() { MacroProperties prop = switcher->macroProperties; bool accepted = MacroPropertiesDialog::AskForSettings( - this, prop, getSelectedMacro()); + this, prop, getSelectedMacro().get()); if (!accepted) { return; } @@ -440,26 +381,17 @@ bool shouldResotreSplitterPos(const QList &pos) void AdvSceneSwitcher::setupMacroTab() { - const QSignalBlocker signalBlocker(ui->macros); - ui->macros->clear(); - for (auto &m : switcher->macros) { - AddNewMacroListEntry(ui->macros, m); + if (switcher->macros.size() == 0 && !switcher->disableHints) { + addPulse = PulseWidget(ui->macroAdd, QColor(Qt::green)); } - - if (switcher->macros.size() == 0) { - if (!switcher->disableHints) { - addPulse = PulseWidget(ui->macroAdd, QColor(Qt::green)); - } - ui->macroHelp->setVisible(true); - } else { - ui->macroHelp->setVisible(false); - } - - connect(ui->macros->model(), - SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), + ui->macros->Reset(switcher->macros, + switcher->macroProperties._highlightExecuted); + connect(ui->macros->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection &, + const QItemSelection &)), this, - SLOT(MacroDragDropReorder(QModelIndex, int, int, QModelIndex, - int))); + SLOT(MacroSelectionChanged(const QItemSelection &, + const QItemSelection &))); delete conditionsList; conditionsList = new MacroSegmentList(this); @@ -531,42 +463,56 @@ void AdvSceneSwitcher::setupMacroTab() void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos) { QPoint globalPos = ui->macros->mapToGlobal(pos); - QMenu myMenu; - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.copy"), - this, &AdvSceneSwitcher::CopyMacro); - myMenu.exec(globalPos); + QMenu menu; + auto copy = menu.addAction( + obs_module_text("AdvSceneSwitcher.macroTab.copy"), this, + &AdvSceneSwitcher::CopyMacro); + copy->setDisabled(ui->macros->GroupsSelected()); + + auto group = menu.addAction( + obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros, + &MacroTree::GroupSelectedItems); + // Nested groups are not supported + group->setDisabled(ui->macros->GroupedItemsSelected() || + ui->macros->GroupsSelected() || + ui->macros->SelectionEmpty()); + + auto ungroup = menu.addAction( + obs_module_text("AdvSceneSwitcher.macroTab.ungroup"), + ui->macros, &MacroTree::UngroupSelectedGroups); + ungroup->setEnabled(ui->macros->GroupsSelected()); + + menu.exec(globalPos); } void AdvSceneSwitcher::ShowMacroActionsContextMenu(const QPoint &pos) { QPoint globalPos = actionsList->mapToGlobal(pos); - QMenu myMenu; - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"), - this, &AdvSceneSwitcher::ExpandAllActions); - myMenu.addAction( - obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"), this, - &AdvSceneSwitcher::CollapseAllActions); - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"), - this, &AdvSceneSwitcher::MinimizeConditions); - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"), - this, &AdvSceneSwitcher::MinimizeActions); - myMenu.exec(globalPos); + QMenu menu; + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"), + this, &AdvSceneSwitcher::ExpandAllActions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"), + this, &AdvSceneSwitcher::CollapseAllActions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"), + this, &AdvSceneSwitcher::MinimizeConditions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"), + this, &AdvSceneSwitcher::MinimizeActions); + menu.exec(globalPos); } void AdvSceneSwitcher::ShowMacroConditionsContextMenu(const QPoint &pos) { QPoint globalPos = conditionsList->mapToGlobal(pos); - QMenu myMenu; - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"), - this, &AdvSceneSwitcher::ExpandAllConditions); - myMenu.addAction( - obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"), this, - &AdvSceneSwitcher::CollapseAllConditions); - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"), - this, &AdvSceneSwitcher::MinimizeActions); - myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"), - this, &AdvSceneSwitcher::MinimizeConditions); - myMenu.exec(globalPos); + QMenu menu; + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"), + this, &AdvSceneSwitcher::ExpandAllConditions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"), + this, &AdvSceneSwitcher::CollapseAllConditions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"), + this, &AdvSceneSwitcher::MinimizeActions); + menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"), + this, &AdvSceneSwitcher::MinimizeConditions); + menu.exec(globalPos); } void AdvSceneSwitcher::CopyMacro() @@ -578,18 +524,20 @@ void AdvSceneSwitcher::CopyMacro() std::string format = macro->Name() + " %1"; std::string name; - if (!addNewMacro(name, format)) { + std::shared_ptr newMacro; + if (!addNewMacro(newMacro, name, format)) { return; } obs_data_t *data = obs_data_create(); macro->Save(data); - switcher->macros.back()->Load(data); - switcher->macros.back()->SetName(name); + newMacro->Load(data); + newMacro->SetName(name); obs_data_release(data); - auto item = AddNewMacroListEntry(ui->macros, switcher->macros.back()); - ui->macros->setCurrentItem(item); + ui->macros->Add(newMacro); + ui->macroAdd->disconnect(addPulse); + emit MacroAdded(QString::fromStdString(name)); } void AdvSceneSwitcher::ExpandAllActions() diff --git a/src/macro-core/macro-tree.cpp b/src/macro-core/macro-tree.cpp new file mode 100644 index 00000000..f2de5fc2 --- /dev/null +++ b/src/macro-core/macro-tree.cpp @@ -0,0 +1,1201 @@ +#include "macro-tree.hpp" +#include "macro.hpp" +#include "utility.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem, + bool highlight) + : _tree(tree), _highlight(highlight), _macro(macroItem) +{ + setAttribute(Qt::WA_TranslucentBackground); + + auto name = _macro->Name(); + bool macroPaused = _macro->Paused(); + bool isGroup = _macro->IsGroup(); + + if (isGroup) { + QIcon icon; + icon.addFile( + QString::fromUtf8(":/res/images/sources/group.svg"), + QSize(), QIcon::Normal, QIcon::Off); + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + _iconLabel = new QLabel(); + _iconLabel->setPixmap(pixmap); + _iconLabel->setStyleSheet("background: none"); + } + + _running = new QCheckBox(); + _running->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + _running->setChecked(!macroPaused); + _running->setStyleSheet("background: none"); + + _label = new QLabel(QString::fromStdString(name)); + _label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + _label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + _label->setAttribute(Qt::WA_TranslucentBackground); + +#ifdef __APPLE__ + _running->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + _boxLayout = new QHBoxLayout(); + _boxLayout->setContentsMargins(0, 0, 0, 0); + _boxLayout->addWidget(_running); + if (isGroup) { + _boxLayout->addWidget(_iconLabel); + _boxLayout->addSpacing(2); + _running->hide(); + } + _boxLayout->addWidget(_label); +#ifdef __APPLE__ + /* Hack: Fixes a bug where scrollbars would be above the lock icon */ + _boxLayout->addSpacing(16); +#endif + + Update(true); + setLayout(_boxLayout); + + auto setRunning = [this](bool val) { _macro->SetPaused(!val); }; + connect(_running, &QAbstractButton::clicked, setRunning); + connect(_tree->window(), SIGNAL(HighlightMacrosChanged(bool)), this, + SLOT(EnableHighlight(bool))); + connect(_tree->window(), + SIGNAL(MacroRenamed(const QString &, const QString &)), this, + SLOT(MacroRenamed(const QString &, const QString &))); + connect(&_timer, SIGNAL(timeout()), this, SLOT(HighlightIfExecuted())); + connect(&_timer, SIGNAL(timeout()), this, SLOT(UpdatePaused())); + _timer.start(1500); +} + +void MacroTreeItem::EnableHighlight(bool enable) +{ + _highlight = enable; +} + +void MacroTreeItem::UpdatePaused() +{ + const QSignalBlocker blocker(_running); + _running->setChecked(!_macro->Paused()); +} + +void MacroTreeItem::HighlightIfExecuted() +{ + if (!_highlight) { + // Run check regardless to reset "_wasExecutedRecently" + (void)_macro->WasExecutedRecently(); + return; + } + + if (_macro && _macro->WasExecutedRecently()) { + PulseWidget(this, Qt::green, QColor(0, 0, 0, 0), true); + } +} + +void MacroTreeItem::MacroRenamed(const QString &oldName, const QString &newName) +{ + if (_label->text() == oldName) { + _label->setText(newName); + } +} + +void MacroTreeItem::paintEvent(QPaintEvent *event) +{ + QStyleOption opt; + opt.initFrom(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + QWidget::paintEvent(event); +} + +void MacroTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + if (_expand) { + _expand->setChecked(!_expand->isChecked()); + } +} + +void MacroTreeItem::Update(bool force) +{ + Type newType; + + if (_macro->IsGroup()) { + newType = Type::Group; + } else if (_macro->IsSubitem()) { + newType = Type::SubItem; + } else { + newType = Type::Item; + } + + if (!force && newType == _type) { + return; + } + + if (_spacer) { + _boxLayout->removeItem(_spacer); + delete _spacer; + _spacer = nullptr; + } + + if (_type == Type::Group) { + _boxLayout->removeWidget(_expand); + _expand->deleteLater(); + _expand = nullptr; + } + + _type = newType; + if (_type == Type::SubItem) { + _spacer = new QSpacerItem(16, 1); + _boxLayout->insertItem(0, _spacer); + + } else if (_type == Type::Group) { + _expand = new SourceTreeSubItemCheckBox(); + _expand->setSizePolicy(QSizePolicy::Maximum, + QSizePolicy::Maximum); + _expand->setMaximumSize(10, 16); + _expand->setMinimumSize(10, 0); +#ifdef __APPLE__ + _expand->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + _boxLayout->insertWidget(0, _expand); + _expand->blockSignals(true); + _expand->setChecked(_macro->IsCollapsed()); + _expand->blockSignals(false); + connect(_expand, &QPushButton::toggled, this, + &MacroTreeItem::ExpandClicked); + } else { + _spacer = new QSpacerItem(3, 1); + _boxLayout->insertItem(0, _spacer); + } + _label->setText(QString::fromStdString(_macro->Name())); +} + +void MacroTreeItem::ExpandClicked(bool checked) +{ + if (!checked) { + _tree->GetModel()->ExpandGroup(_macro); + } else { + _tree->GetModel()->CollapseGroup(_macro); + } +} + +/* ========================================================================= */ + +void MacroTreeModel::Reset(std::deque> &newItems) +{ + beginResetModel(); + _macros = newItems; + endResetModel(); + + UpdateGroupState(false); + _mt->ResetWidgets(); +} + +static inline int +ModelIndexToMacroIndex(int modelIdx, + const std::deque> ¯os) +{ + assert(macros.size() >= modelIdx); + int realIdx = 0; + const auto &m = macros[0]; + bool inCollapsedGroup = m->IsGroup() && m->IsCollapsed(); + uint32_t groupSize = m->GroupSize(); + for (int i = 0; i < modelIdx; i++) { + if (inCollapsedGroup) { + realIdx += groupSize; + groupSize = 0; + inCollapsedGroup = false; + } + realIdx++; + const auto &m = macros.at(realIdx); + inCollapsedGroup = m->IsGroup() && m->IsCollapsed(); + groupSize = m->GroupSize(); + } + return realIdx; +} + +static inline int +MacroIndexToModelIndex(int realIdx, + const std::deque> ¯os) +{ + int modelIdx = 0; + bool inCollapsedGroup = false; + uint32_t groupSize = 0; + for (int i = 0; i < realIdx; i++) { + if (inCollapsedGroup) { + i += groupSize - 1; + groupSize = 0; + inCollapsedGroup = false; + continue; + } + const auto &m = macros[i]; + inCollapsedGroup = m->IsGroup() && m->IsCollapsed(); + groupSize = m->GroupSize(); + modelIdx++; + } + return modelIdx; +} + +void MacroTreeModel::MoveItemBefore(const std::shared_ptr &item, + const std::shared_ptr &before) +{ + if (item == before) { + return; + } + + auto modelFromEndIdx = GetItemModelIndex(item); + auto modelFromIdx = modelFromEndIdx; + auto modelTo = GetItemModelIndex(before); + auto macroFrom = GetItemMacroIndex(item); + auto macroTo = GetItemMacroIndex(before); + + if (before->IsSubitem()) { + modelTo -= before->IsCollapsed() ? 0 : before->GroupSize(); + macroTo -= before->GroupSize(); + } + + if (!item->IsGroup()) { + beginMoveRows(QModelIndex(), modelFromIdx, modelFromIdx, + QModelIndex(), modelTo); + auto it = std::next(_macros.begin(), macroFrom); + std::shared_ptr tmp = *it; + _macros.erase(it); + _macros.insert(std::next(_macros.begin(), macroTo), tmp); + endMoveRows(); + return; + } + + if (!item->IsCollapsed()) { + modelFromEndIdx += item->GroupSize(); + } + + beginMoveRows(QModelIndex(), modelFromIdx, modelFromEndIdx, + QModelIndex(), modelTo); + for (uint32_t i = 0; i <= item->GroupSize(); i++) { + auto it = std::next(_macros.begin(), macroFrom + i); + auto tmp = *it; + _macros.erase(it); + _macros.insert(std::next(_macros.begin(), macroTo + i), tmp); + } + endMoveRows(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::MoveItemAfter(const std::shared_ptr &item, + const std::shared_ptr &after) +{ + if (item == after) { + return; + } + + auto modelFromIdx = GetItemModelIndex(item); + auto modelFromEndIdx = modelFromIdx; + auto modelTo = GetItemModelIndex(after); + auto macroFrom = GetItemMacroIndex(item); + auto macroTo = GetItemMacroIndex(after); + + if (after->IsGroup()) { + modelTo += after->IsCollapsed() ? 0 : after->GroupSize(); + macroTo += after->GroupSize(); + } + + if (!item->IsGroup()) { + beginMoveRows(QModelIndex(), modelFromIdx, modelFromEndIdx, + QModelIndex(), modelTo + 1); + auto it = std::next(_macros.begin(), macroFrom); + std::shared_ptr tmp = *it; + _macros.erase(it); + _macros.insert(std::next(_macros.begin(), macroTo), tmp); + endMoveRows(); + return; + } + + if (!item->IsCollapsed()) { + modelFromEndIdx += item->GroupSize(); + } + + beginMoveRows(QModelIndex(), modelFromIdx, modelFromEndIdx, + QModelIndex(), modelTo + 1); + for (uint32_t i = 0; i <= item->GroupSize(); i++) { + auto it = std::next(_macros.begin(), macroFrom); + auto tmp = *it; + _macros.erase(it); + _macros.insert(std::next(_macros.begin(), macroTo), tmp); + } + endMoveRows(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +static inline int +CountItemsVisibleInModel(const std::deque> ¯os) +{ + int count = macros.size(); + for (const auto &m : macros) { + if (m->IsGroup() && m->IsCollapsed()) { + count -= m->GroupSize(); + } + } + return count; +} + +void MacroTreeModel::Add(std::shared_ptr item) +{ + auto idx = CountItemsVisibleInModel(_macros); + beginInsertRows(QModelIndex(), idx, idx); + _macros.emplace_back(item); + endInsertRows(); + _mt->UpdateWidget(createIndex(idx, 0, nullptr), item); + _mt->selectionModel()->clear(); + _mt->selectionModel()->select(createIndex(idx, 0, nullptr), + QItemSelectionModel::Select); + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::Remove(std::shared_ptr item) +{ + auto startIdx = GetItemModelIndex(item); + if (startIdx == -1) { + return; + } + auto macroStartIdx = ModelIndexToMacroIndex(startIdx, _macros); + + auto endIdx = startIdx; + auto macroEndIdx = macroStartIdx; + + bool isGroup = item->IsGroup(); + if (isGroup) { + macroEndIdx += item->GroupSize(); + if (!item->IsCollapsed()) { + endIdx = item->GroupSize(); + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + _macros.erase(std::next(_macros.begin(), macroStartIdx), + std::next(_macros.begin(), macroEndIdx + 1)); + endRemoveRows(); + + _mt->selectionModel()->clear(); + + if (isGroup) { + UpdateGroupState(true); + } + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +std::shared_ptr MacroTreeModel::Neighbor(const std::shared_ptr &m, + bool above) const +{ + if (!m) { + return std::shared_ptr(); + } + + auto it = std::find(_macros.begin(), _macros.end(), m); + if (it == _macros.end()) { + return std::shared_ptr(); + } + if (above) { + if (it == _macros.begin()) { + return std::shared_ptr(); + } + return *std::prev(it, 1); + } + auto result = std::next(it, 1); + if (result == _macros.end()) { + return std::shared_ptr(); + } + return *result; +} + +std::shared_ptr +MacroTreeModel::FindEndOfGroup(const std::shared_ptr &m, + bool above) const +{ + auto endOfGroup = Neighbor(m, above); + if (!endOfGroup) { + return m; + } + + if (above) { + while (!endOfGroup->IsGroup()) { + endOfGroup = Neighbor(endOfGroup, above); + } + } else { + while (endOfGroup->IsSubitem() && + GetItemMacroIndex(endOfGroup) + 1 != + (int)_macros.size()) { + endOfGroup = Neighbor(endOfGroup, above); + } + if (!endOfGroup->IsSubitem()) { + endOfGroup = Neighbor(endOfGroup, !above); + } + } + + return endOfGroup; +} + +std::shared_ptr MacroTreeModel::GetCurrentMacro() const +{ + auto sel = _mt->selectionModel()->selection(); + if (sel.empty()) { + return std::shared_ptr(); + } + auto idx = sel.indexes().back().row(); + if (idx >= (int)_macros.size()) { + return std::shared_ptr(); + } + return _macros[ModelIndexToMacroIndex(idx, _macros)]; +} + +MacroTreeModel::MacroTreeModel(MacroTree *st_, + std::deque> ¯os) + : QAbstractListModel(st_), _mt(st_), _macros(macros) +{ + UpdateGroupState(false); +} + +int MacroTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : CountItemsVisibleInModel(_macros); +} + +QVariant MacroTreeModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::AccessibleTextRole) { + std::shared_ptr item = + _macros[ModelIndexToMacroIndex(index.row(), _macros)]; + if (!item) { + return QVariant(); + } + return QVariant(QString::fromStdString(item->Name())); + } + + return QVariant(); +} + +Qt::ItemFlags MacroTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + } + + std::shared_ptr item = + _macros[ModelIndexToMacroIndex(index.row(), _macros)]; + bool isGroup = item->IsGroup(); + + return QAbstractListModel::flags(index) | Qt::ItemIsEditable | + Qt::ItemIsDragEnabled | + (isGroup ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions MacroTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString MacroTreeModel::GetNewGroupName() +{ + QString fmt = + obs_module_text("AdvSceneSwitcher.macroTab.defaultGroupName"); + QString name = fmt.arg("1"); + + int i = 2; + for (;;) { + if (!GetMacroByQString(name)) { + break; + } + name = fmt.arg(QString::number(i++)); + } + + return name; +} + +int MacroTreeModel::GetItemMacroIndex(const std::shared_ptr &item) const +{ + auto it = std::find(_macros.begin(), _macros.end(), item); + if (it == _macros.end()) { + return -1; + } + + return it - _macros.begin(); +} + +int MacroTreeModel::GetItemModelIndex(const std::shared_ptr &item) const +{ + return MacroIndexToModelIndex(GetItemMacroIndex(item), _macros); +} + +bool MacroTreeModel::IsLastItem(std::shared_ptr item) const +{ + return GetItemModelIndex(item) + 1 == (int)_macros.size(); +} + +void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) { + return; + } + + QString name = GetNewGroupName(); + std::vector> items; + items.reserve(indices.size()); + for (int i = 0; i < indices.count(); i++) { + std::shared_ptr item = _macros[ModelIndexToMacroIndex( + indices[i].row(), _macros)]; + items.emplace_back(item); + } + std::shared_ptr item = + Macro::CreateGroup(name.toStdString(), items); + if (!item) { + return; + } + + // A new list entry for group + Add(item); + + // Move all selected items after new group entry + for (const auto &item : items) { + auto it = _macros.begin() + GetItemMacroIndex(item); + std::rotate(it, std::next(it, 1), _macros.end()); + } + + _hasGroups = true; + _mt->selectionModel()->clear(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } + + Reset(_macros); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + if (indices.count() == 0) { + return; + } + + for (int i = indices.count() - 1; i >= 0; i--) { + std::shared_ptr item = _macros[ModelIndexToMacroIndex( + indices[i].row(), _macros)]; + if (item->IsGroup()) { + Macro::RemoveGroup(item); + } + } + + _mt->selectionModel()->clear(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } + + Reset(_macros); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::ExpandGroup(std::shared_ptr item) +{ + auto idx = GetItemModelIndex(item); + if (idx == -1 || !item->IsGroup() || !item->GroupSize() || + !item->IsCollapsed()) { + return; + } + + item->SetCollapsed(false); + Reset(_macros); + + _mt->selectionModel()->clear(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::CollapseGroup(std::shared_ptr item) +{ + auto idx = GetItemModelIndex(item); + if (idx == -1 || !item->IsGroup() || !item->GroupSize() || + item->IsCollapsed()) { + return; + } + + item->SetCollapsed(true); + Reset(_macros); + + _mt->selectionModel()->clear(); + + for (auto &m : _macros) { + m->ResolveMacroRef(); + } +} + +void MacroTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : _macros) { + if (item->IsGroup()) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != _hasGroups) { + _hasGroups = nowHasGroups; + if (update) { + _mt->UpdateWidgets(true); + } + } +} + +void MacroTree::Reset(std::deque> ¯os, + bool highlight) +{ + _highlight = highlight; + MacroTreeModel *mtm = new MacroTreeModel(this, macros); + setModel(mtm); + GetModel()->Reset(macros); +} + +void MacroTree::Add(std::shared_ptr item) const +{ + GetModel()->Add(item); +} + +std::shared_ptr MacroTree::GetCurrentMacro() const +{ + return GetModel()->GetCurrentMacro(); +} + +MacroTree::MacroTree(QWidget *parent_) : QListView(parent_) +{ + setStyleSheet(QString( + "*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + setItemDelegate(new MacroTreeDelegate(this)); +} + +void MacroTree::ResetWidgets() +{ + MacroTreeModel *mtm = GetModel(); + mtm->UpdateGroupState(false); + int modelIdx = 0; + for (int i = 0; i < (int)mtm->_macros.size(); i++) { + QModelIndex index = mtm->createIndex(modelIdx, 0, nullptr); + const auto ¯o = mtm->_macros[i]; + setIndexWidget(index, + new MacroTreeItem(this, macro, _highlight)); + + // Skip items of collapsed groups + if (macro->IsGroup() && macro->IsCollapsed()) { + i += macro->GroupSize(); + } + modelIdx++; + } +} + +void MacroTree::UpdateWidget(const QModelIndex &idx, + std::shared_ptr item) +{ + setIndexWidget(idx, new MacroTreeItem(this, item, _highlight)); +} + +void MacroTree::UpdateWidgets(bool force) +{ + MacroTreeModel *mtm = GetModel(); + + for (int i = 0; i < (int)mtm->_macros.size(); i++) { + std::shared_ptr item = mtm->_macros[i]; + MacroTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(mtm->createIndex(i, 0, nullptr), item); + } else { + widget->Update(force); + } + // Skip items of collapsed groups + if (item->IsGroup() && item->IsCollapsed()) { + i += item->GroupSize(); + } + } +} + +Q_DECLARE_METATYPE(std::shared_ptr); + +static inline void MoveItem(std::deque> &items, + std::shared_ptr &item, int to) +{ + auto it = std::find(items.begin(), items.end(), item); + if (it == items.end()) { + blog(LOG_ERROR, + "something went wrong during drag & drop reordering"); + return; + } + + items.erase(it); + items.insert(std::next(items.begin(), to), item); +} + +// Logic mostly based on OBS's source-tree dropEvent() with a few modifications +// regarding group handling +void MacroTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + MacroTreeModel *mtm = GetModel(); + auto &items = mtm->_macros; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt( +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + event->position().toPoint() +#else + event->pos() +#endif + ) + .row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (items.empty()) { + QListView::dropEvent(event); + return; + } + + row = mtm->rowCount(QModelIndex()) - 1; + indicator = QAbstractItemView::BelowItem; + } + + // Store destination group if moving to a group + Macro *dropItem = items[ModelIndexToMacroIndex(row, items)] + .get(); // Item being dropped on + bool itemIsGroup = dropItem->IsGroup(); + Macro *dropGroup = itemIsGroup ? dropItem : dropItem->Parent(); + + // Not a group if moving above the group + if (indicator == QAbstractItemView::AboveItem && itemIsGroup) { + dropGroup = nullptr; + } + if (emptyDrop) { + dropGroup = nullptr; + } + + // Remember to remove list items if dropping on collapsed group + bool dropOnCollapsed = false; + if (dropGroup) { + dropOnCollapsed = dropGroup->IsCollapsed(); + } + + if (indicator == QAbstractItemView::BelowItem || + indicator == QAbstractItemView::OnItem || + indicator == QAbstractItemView::OnViewport) + row++; + + if (row < 0 || row > (int)items.size()) { + QListView::dropEvent(event); + return; + } + + // Determine if any base group is selected + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + std::shared_ptr item = + items[ModelIndexToMacroIndex(indices[i].row(), items)]; + if (item->IsGroup()) { + hasGroups = true; + break; + } + } + + // If dropping a group, detect if it's below another group + std::shared_ptr itemBelow; + if (row == (int)items.size()) { + itemBelow = nullptr; + } else { + itemBelow = items[MacroIndexToModelIndex(row, items)]; + } + + if (hasGroups) { + if (!itemBelow || itemBelow->Parent() != dropGroup) { + dropGroup = nullptr; + dropOnCollapsed = false; + } + } + + // If dropping groups on other groups, disregard as invalid drag/drop + if (dropGroup && hasGroups) { + QListView::dropEvent(event); + return; + } + + // If selection includes base group items, include all group sub-items and + // treat them all as one + // + // Also gather list of (sub)items to move in backend + std::vector> subItemsToMove; + QModelIndexList indicesWithoutSubitems = indices; + + if (hasGroups) { + // Remove sub-items if selected + for (int i = indices.size() - 1; i >= 0; i--) { + std::shared_ptr item = + items[ModelIndexToMacroIndex(indices[i].row(), + items)]; + auto parent = item->Parent(); + if (parent != nullptr) { + indices.removeAt(i); + } + } + indicesWithoutSubitems = indices; + + // Add all sub-items of selected groups + for (int i = indices.size() - 1; i >= 0; i--) { + std::shared_ptr item = + items[ModelIndexToMacroIndex(indices[i].row(), + items)]; + if (!item->IsGroup()) { + continue; + } + bool collapsed = item->IsCollapsed(); + for (int j = items.size() - 1; j >= 0; j--) { + std::shared_ptr subitem = items[j]; + auto subitemGroup = subitem->Parent(); + + if (subitemGroup == item.get()) { + if (!collapsed) { + QModelIndex idx = mtm->createIndex( + MacroIndexToModelIndex( + j, items), + 0, nullptr); + indices.insert(i + 1, idx); + } + subItemsToMove.emplace_back(subitem); + } + } + } + } + + // Build persistent indices + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) { + persistentIndices.append(index); + } + std::sort(persistentIndices.begin(), persistentIndices.end()); + + // Prepare items to move in backend + std::sort(indicesWithoutSubitems.begin(), indicesWithoutSubitems.end()); + std::vector> itemsToMove; + for (const auto &subitemsIdx : indicesWithoutSubitems) { + auto idx = ModelIndexToMacroIndex(subitemsIdx.row(), items); + itemsToMove.emplace_back(items[idx]); + } + + // Move all items to destination index + int r = row; + for (auto &persistentIdx : persistentIndices) { + int from = persistentIdx.row(); + int to = r; + int itemTo = to; + + if (itemTo > from) { + itemTo--; + } + + if (itemTo != from) { + mtm->beginMoveRows(QModelIndex(), from, from, + QModelIndex(), to); + mtm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + // Remove items if dropped in to collapsed group + if (dropOnCollapsed) { + mtm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + mtm->endRemoveRows(); + } + + // Move items in backend + int to = row; + try { + to = ModelIndexToMacroIndex(row, items); + } catch (std::out_of_range const &) { + to = items.size(); + } + auto prev = *itemsToMove.rbegin(); + auto curIdx = mtm->GetItemMacroIndex(prev); + int toIdx = to; + if (toIdx > curIdx) { + toIdx--; + } + Macro::PrepareMoveToGroup(dropGroup, prev); + MoveItem(items, prev, toIdx); + for (auto it = std::next(itemsToMove.rbegin(), 1); + it != itemsToMove.rend(); ++it) { + auto &item = *it; + auto curIdx = mtm->GetItemMacroIndex(item); + int toIdx = mtm->GetItemMacroIndex(prev); + if (toIdx > curIdx) { + toIdx--; + } + Macro::PrepareMoveToGroup(dropGroup, item); + MoveItem(items, item, toIdx); + prev = item; + } + + // Move subitems of groups + for (const auto &i : subItemsToMove) { + auto removeIt = std::find(items.begin(), items.end(), i); + if (removeIt == items.end()) { + blog(LOG_ERROR, "Cannot move subitem '%s'", + i->Name().c_str()); + continue; + } + items.erase(removeIt); + auto targetName = i->Parent()->Name(); + auto GroupNameMatches = [targetName](std::shared_ptr m) { + return m->Name() == targetName; + }; + auto it = std::find_if(items.begin(), items.end(), + GroupNameMatches); + items.insert(std::next(it, 1), i); + } + + for (auto &m : items) { + m->ResolveMacroRef(); + } + + // Update widgets and accept event + UpdateWidgets(true); + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +bool MacroTree::GroupsSelected() const +{ + MacroTreeModel *mtm = GetModel(); + QModelIndexList selectedIndices = selectedIndexes(); + + if (SelectionEmpty()) { + return false; + } + + for (auto &idx : selectedIndices) { + std::shared_ptr item = + mtm->_macros[ModelIndexToMacroIndex(idx.row(), + mtm->_macros)]; + if (item->IsGroup()) { + return true; + } + } + return false; +} + +bool MacroTree::GroupedItemsSelected() const +{ + MacroTreeModel *mtm = GetModel(); + QModelIndexList selectedIndices = selectedIndexes(); + + if (SelectionEmpty()) { + return false; + } + + for (auto &idx : selectedIndices) { + std::shared_ptr item = + mtm->_macros[ModelIndexToMacroIndex(idx.row(), + mtm->_macros)]; + auto parent = item->Parent(); + if (parent) { + return true; + } + } + return false; +} + +bool MacroTree::SelectionEmpty() const +{ + return selectedIndexes().empty(); +} + +void MacroTree::MoveItemBefore(const std::shared_ptr &item, + const std::shared_ptr &after) const +{ + GetModel()->MoveItemBefore(item, after); +} + +void MacroTree::MoveItemAfter(const std::shared_ptr &item, + const std::shared_ptr &after) const +{ + GetModel()->MoveItemAfter(item, after); +} + +MacroTreeModel *MacroTree::GetModel() const +{ + return reinterpret_cast(model()); +} + +void MacroTree::Remove(std::shared_ptr item) const +{ + GetModel()->Remove(item); +} + +void MacroTree::Up(std::shared_ptr item) const +{ + auto above = GetModel()->Neighbor(item, true); + if (!above) { + return; + } + + if (item->IsSubitem()) { + // Nowhere to move to, when the item above is a group or + // regular entry + if (!above->IsSubitem()) { + return; + } + + MoveItemBefore(item, above); + return; + } + + if (above->IsSubitem()) { + above = GetModel()->FindEndOfGroup(above, true); + } + + MoveItemBefore(item, above); +} + +void MacroTree::Down(std::shared_ptr item) const +{ + auto below = GetModel()->Neighbor(item, false); + if (!below) { + return; + } + + if (item->IsSubitem()) { + // Nowhere to move to, when the item below is a group or + // regular entry + if (!below->IsSubitem()) { + return; + } + + MoveItemAfter(item, below); + return; + } + + if (item->IsGroup()) { + if (below->IsSubitem()) { + below = GetModel()->FindEndOfGroup(below, false); + + // Nowhere to move group to + if (GetModel()->IsLastItem(below)) { + return; + } + + below = GetModel()->Neighbor(below, false); + } + MoveItemAfter(item, below); + return; + } + + MoveItemAfter(item, below); +} + +void MacroTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetModel()->GroupSelectedItems(indices); +} + +void MacroTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetModel()->UngroupSelectedGroups(indices); +} + +inline MacroTreeItem *MacroTree::GetItemWidget(int idx) const +{ + QWidget *widget = indexWidget(GetModel()->createIndex(idx, 0, nullptr)); + return reinterpret_cast(widget); +} + +void MacroTree::paintEvent(QPaintEvent *event) +{ + MacroTreeModel *mtm = GetModel(); + if (mtm && mtm->_macros.empty()) { + QPainter painter(viewport()); + const QRect rectangle = + QRect(0, 0, size().width(), size().height()); + QRect boundingRect; + QString text(obs_module_text("AdvSceneSwitcher.macroTab.help")); + painter.drawText(rectangle, Qt::AlignCenter | Qt::TextWordWrap, + text, &boundingRect); + } else { + QListView::paintEvent(event); + } +} + +MacroTreeDelegate::MacroTreeDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +QSize MacroTreeDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + MacroTree *tree = qobject_cast(parent()); + QWidget *item = tree->indexWidget(index); + + if (!item) { + return (QSize(0, 0)); + } + + return (QSize(option.widget->minimumWidth(), item->height())); +} diff --git a/src/macro-core/macro-tree.hpp b/src/macro-core/macro-tree.hpp new file mode 100644 index 00000000..1eaa9967 --- /dev/null +++ b/src/macro-core/macro-tree.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class Macro; + +class QLabel; +class MacroTree; +class QSpacerItem; +class QHBoxLayout; + +class MacroTreeItem : public QFrame { + Q_OBJECT + +public: + explicit MacroTreeItem(MacroTree *tree, std::shared_ptr macro, + bool highlight); + +private slots: + void ExpandClicked(bool checked); + void EnableHighlight(bool enable); + void UpdatePaused(); + void HighlightIfExecuted(); + void MacroRenamed(const QString &, const QString &); + +private: + virtual void paintEvent(QPaintEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + Type _type = Type::Unknown; + + QSpacerItem *_spacer = nullptr; + QCheckBox *_expand = nullptr; + QLabel *_iconLabel = nullptr; + QCheckBox *_running = nullptr; + QHBoxLayout *_boxLayout = nullptr; + QLabel *_label = nullptr; + MacroTree *_tree; + bool _highlight; + QTimer _timer; + std::shared_ptr _macro; + + friend class MacroTree; + friend class MacroTreeModel; +}; + +// Only used to enable applying "SourceTreeSubItemCheckBox" stylesheet +class SourceTreeSubItemCheckBox : public QCheckBox { + Q_OBJECT +}; + +class MacroTreeModel : public QAbstractListModel { + Q_OBJECT + +public: + explicit MacroTreeModel(MacroTree *st, + std::deque> ¯os); + ~MacroTreeModel() = default; + virtual int rowCount(const QModelIndex &parent) const override; + virtual QVariant data(const QModelIndex &index, + int role) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual Qt::DropActions supportedDropActions() const override; + +private: + void Reset(std::deque> &); + void MoveItemBefore(const std::shared_ptr &item, + const std::shared_ptr &before); + void MoveItemAfter(const std::shared_ptr &item, + const std::shared_ptr &after); + void Add(std::shared_ptr item); + void Remove(std::shared_ptr item); + std::shared_ptr Neighbor(const std::shared_ptr &m, + bool above) const; + std::shared_ptr FindEndOfGroup(const std::shared_ptr &m, + bool above) const; + std::shared_ptr GetCurrentMacro() const; + QString GetNewGroupName(); + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + void ExpandGroup(std::shared_ptr item); + void CollapseGroup(std::shared_ptr item); + void UpdateGroupState(bool update); + int GetItemMacroIndex(const std::shared_ptr &item) const; + int GetItemModelIndex(const std::shared_ptr &item) const; + bool IsLastItem(std::shared_ptr item) const; + + MacroTree *_mt; + std::deque> &_macros; + bool _hasGroups = false; + + friend class MacroTree; + friend class MacroTreeItem; +}; + +class MacroTree : public QListView { + Q_OBJECT + +public: + explicit MacroTree(QWidget *parent = nullptr); + void Reset(std::deque> &, bool highlight); + void Add(std::shared_ptr item) const; + void Remove(std::shared_ptr item) const; + void Up(std::shared_ptr item) const; + void Down(std::shared_ptr item) const; + std::shared_ptr GetCurrentMacro() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + bool SelectionEmpty() const; + +public slots: + void GroupSelectedItems(); + void UngroupSelectedGroups(); + +protected: + virtual void dropEvent(QDropEvent *event) override; + virtual void paintEvent(QPaintEvent *event) override; + +private: + MacroTreeItem *GetItemWidget(int idx) const; + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, std::shared_ptr item); + void UpdateWidgets(bool force = false); + void MoveItemBefore(const std::shared_ptr &item, + const std::shared_ptr &after) const; + void MoveItemAfter(const std::shared_ptr &item, + const std::shared_ptr &after) const; + MacroTreeModel *GetModel() const; + + bool _highlight = false; + + friend class MacroTreeModel; + friend class MacroTreeItem; +}; + +class MacroTreeDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + MacroTreeDelegate(QObject *parent); + virtual QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; diff --git a/src/macro-core/macro.cpp b/src/macro-core/macro.cpp index 8c0d82a9..367fe2c1 100644 --- a/src/macro-core/macro.cpp +++ b/src/macro-core/macro.cpp @@ -28,8 +28,72 @@ Macro::~Macro() ClearHotkeys(); } +std::shared_ptr +Macro::CreateGroup(const std::string &name, + std::vector> &children) +{ + auto group = std::make_shared(name, false); + for (auto &c : children) { + c->SetParent(group.get()); + } + group->_isGroup = true; + group->_groupSize = children.size(); + return group; +} + +void Macro::RemoveGroup(std::shared_ptr group) +{ + auto it = std::find(switcher->macros.begin(), switcher->macros.end(), + group); + if (it == switcher->macros.end()) { + return; + } + + auto size = group->GroupSize(); + for (uint32_t i = 1; i <= size; i++) { + auto m = std::next(it, i); + (*m)->SetParent(nullptr); + } + + switcher->macros.erase(it); +} + +void Macro::PrepareMoveToGroup(Macro *group, std::shared_ptr item) +{ + for (auto &m : switcher->macros) { + if (m.get() == group) { + PrepareMoveToGroup(m, item); + return; + } + } + PrepareMoveToGroup(std::shared_ptr(), item); +} + +void Macro::PrepareMoveToGroup(std::shared_ptr group, + std::shared_ptr item) +{ + if (!item) { + return; + } + + // Potentially remove from old group + auto oldGroup = item->Parent(); + if (oldGroup) { + oldGroup->_groupSize--; + } + + item->SetParent(group.get()); + if (group) { + group->_groupSize++; + } +} + bool Macro::CeckMatch() { + if (_isGroup) { + return false; + } + _matched = false; for (auto &c : _conditions) { if (_paused) { @@ -140,6 +204,9 @@ bool Macro::PerformActions(bool forceParallel, bool ignorePause) RunActions(ret, ignorePause); } _wasExecutedRecently = true; + if (_parent) { + _parent->_wasExecutedRecently = true; + } return ret; } @@ -253,6 +320,16 @@ bool Macro::Save(obs_data_t *obj) const obs_data_set_bool(obj, "parallel", _runInParallel); obs_data_set_bool(obj, "onChange", _matchOnChange); + obs_data_set_bool(obj, "group", _isGroup); + if (_isGroup) { + auto groupData = obs_data_create(); + obs_data_set_bool(groupData, "collapsed", _isCollapsed); + obs_data_set_int(groupData, "size", _groupSize); + obs_data_set_obj(obj, "groupData", groupData); + obs_data_release(groupData); + return true; + } + obs_data_set_bool(obj, "registerHotkeys", _registerHotkeys); obs_data_array_t *pauseHotkey = obs_hotkey_save(_pauseHotkey); obs_data_set_array(obj, "pauseHotkey", pauseHotkey); @@ -333,6 +410,15 @@ bool Macro::Load(obs_data_t *obj) _runInParallel = obs_data_get_bool(obj, "parallel"); _matchOnChange = obs_data_get_bool(obj, "onChange"); + _isGroup = obs_data_get_bool(obj, "group"); + if (_isGroup) { + auto groupData = obs_data_get_obj(obj, "groupData"); + _isCollapsed = obs_data_get_bool(groupData, "collapsed"); + _groupSize = obs_data_get_int(groupData, "size"); + obs_data_release(groupData); + return true; + } + obs_data_set_default_bool(obj, "registerHotkeys", true); _registerHotkeys = obs_data_get_bool(obj, "registerHotkeys"); if (_registerHotkeys) { @@ -610,8 +696,18 @@ void SwitcherData::loadMacros(obs_data_t *obj) } obs_data_array_release(macroArray); + int groupCount = 0; + Macro *group = nullptr; for (auto &m : macros) { m->ResolveMacroRef(); + if (groupCount) { + m->SetParent(group); + groupCount--; + } + if (m->IsGroup()) { + groupCount = m->GroupSize(); + group = m.get(); + } } } diff --git a/src/macro-core/macro.hpp b/src/macro-core/macro.hpp index bdef3214..3fd18c87 100644 --- a/src/macro-core/macro.hpp +++ b/src/macro-core/macro.hpp @@ -18,7 +18,6 @@ class Macro { public: Macro(const std::string &name = "", const bool addHotkey = false); virtual ~Macro(); - bool CeckMatch(); bool PerformActions(bool forceParallel = false, bool ignorePause = false); @@ -41,9 +40,26 @@ public: { return _conditions; } + std::deque> &Actions() { return _actions; } void UpdateActionIndices(); void UpdateConditionIndices(); - std::deque> &Actions() { return _actions; } + + // Group controls + static std::shared_ptr + CreateGroup(const std::string &name, + std::vector> &children); + static void RemoveGroup(std::shared_ptr); + static void PrepareMoveToGroup(Macro *group, + std::shared_ptr item); + static void PrepareMoveToGroup(std::shared_ptr group, + std::shared_ptr item); + bool IsGroup() { return _isGroup; } + uint32_t GroupSize() { return _groupSize; } + bool IsSubitem() { return !!_parent; } + void SetCollapsed(bool val) { _isCollapsed = val; } + bool IsCollapsed() { return _isCollapsed; } + void SetParent(Macro *m) { _parent = m; } + Macro *Parent() { return _parent; } bool Save(obs_data_t *obj) const; bool Load(obs_data_t *obj); @@ -75,6 +91,10 @@ private: std::string _name = ""; std::deque> _conditions; std::deque> _actions; + + Macro *_parent = nullptr; + uint32_t _groupSize = 0; + bool _runInParallel = false; bool _matched = false; bool _lastMatched = false; @@ -87,6 +107,8 @@ private: obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID; // UI helpers for the macro tab + bool _isGroup = false; + bool _isCollapsed = false; bool _wasExecutedRecently = false; bool _onChangeTriggered = false;