SceneSwitcher/lib/macro/macro-tree.cpp
WarmUpTill 7d0332dd0e Restructure library and plugins
The "core" macro conditions and actions have been extracted out to the
"base" plugin.

The library now mostly contains functionality which is required across
all plugins and (e.g. definitions for macro segments).

The goal is to reduce the complexity and cross-dependencies and group
the source files in a better way.

This should relsove the "library limit of 65535 objects exceeded" build
issue occuring in some Windows build environments.
2024-01-27 14:10:34 +01:00

1308 lines
32 KiB
C++

#include "macro-tree.hpp"
#include "macro.hpp"
#include "sync-helpers.hpp"
#include "utility.hpp"
#include <obs.h>
#include <string>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QStylePainter>
Q_DECLARE_METATYPE(std::shared_ptr<advss::Macro>);
namespace advss {
MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr<Macro> macroItem,
bool highlight)
: _tree(tree), _highlight(highlight), _macro(macroItem)
{
setAttribute(Qt::WA_TranslucentBackground);
// This is necessary for some theses to display active selections properly
setStyleSheet("background: none");
auto name = _macro->Name();
bool macroPaused = _macro->Paused();
bool isGroup = _macro->IsGroup();
if (isGroup) {
const auto path = QString::fromStdString(GetDataFilePath(
"res/images/" + GetThemeTypeName() + "Group.svg"));
QIcon icon(path);
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 || !_macro) {
return;
}
if (_lastHighlightCheckTime.time_since_epoch().count() != 0 &&
_macro->ExecutedSince(_lastHighlightCheckTime)) {
PulseWidget(this, Qt::green, QColor(0, 0, 0, 0), true);
}
_lastHighlightCheckTime = std::chrono::high_resolution_clock::now();
}
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<std::shared_ptr<Macro>> &newItems)
{
beginResetModel();
_macros = newItems;
endResetModel();
UpdateGroupState(false);
_mt->ResetWidgets();
}
static inline int
ModelIndexToMacroIndex(int modelIdx,
const std::deque<std::shared_ptr<Macro>> &macros)
{
assert((int)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<std::shared_ptr<Macro>> &macros)
{
if (realIdx == -1) {
return -1;
}
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.at(i);
inCollapsedGroup = m->IsGroup() && m->IsCollapsed();
groupSize = m->GroupSize();
modelIdx++;
}
return modelIdx;
}
void MacroTreeModel::MoveItemBefore(const std::shared_ptr<Macro> &item,
const std::shared_ptr<Macro> &before)
{
if (!item || !before || item == before ||
Neighbor(item, false) == before) {
return;
}
int modelFromIdx = 0, modelFromEndIdx = 0, modelTo = 0, macroFrom = 0,
macroTo = 0;
try {
modelFromEndIdx = GetItemModelIndex(item);
modelFromIdx = modelFromEndIdx;
modelTo = GetItemModelIndex(before);
macroFrom = GetItemMacroIndex(item);
macroTo = GetItemMacroIndex(before);
} catch (const std::out_of_range &) {
blog(LOG_WARNING,
"error while trying to move item \"%s\" before \"%s\"",
item->Name().c_str(), before->Name().c_str());
assert(IsInValidState());
return;
}
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<Macro> tmp = *it;
_macros.erase(it);
_macros.insert(std::next(_macros.begin(), macroTo), tmp);
endMoveRows();
assert(IsInValidState());
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();
assert(IsInValidState());
}
void MacroTreeModel::MoveItemAfter(const std::shared_ptr<Macro> &item,
const std::shared_ptr<Macro> &after)
{
if (!item || !after || item == after || Neighbor(item, true) == after) {
return;
}
auto moveAfter = after;
int modelFromIdx = 0, modelFromEndIdx = 0, modelTo = 0;
try {
modelFromIdx = GetItemModelIndex(item);
modelFromEndIdx = modelFromIdx;
modelTo = GetItemModelIndex(after);
} catch (const std::out_of_range &) {
blog(LOG_WARNING,
"error while trying to move item \"%s\" after \"%s\"",
item->Name().c_str(), after->Name().c_str());
assert(IsInValidState());
return;
}
if (after->IsGroup()) {
modelTo += after->IsCollapsed() ? 0 : after->GroupSize();
moveAfter = FindEndOfGroup(after, false);
}
if (!item->IsGroup()) {
beginMoveRows(QModelIndex(), modelFromIdx, modelFromEndIdx,
QModelIndex(), modelTo + 1);
auto it = std::find(_macros.begin(), _macros.end(), item);
std::shared_ptr<Macro> tmp = *it;
_macros.erase(it);
it = std::find(_macros.begin(), _macros.end(), moveAfter);
if (it == _macros.end()) {
_macros.insert(it, tmp);
} else {
_macros.insert(std::next(it), tmp);
}
endMoveRows();
assert(IsInValidState());
return;
}
if (!item->IsCollapsed()) {
modelFromEndIdx += item->GroupSize();
}
beginMoveRows(QModelIndex(), modelFromIdx, modelFromEndIdx,
QModelIndex(), modelTo + 1);
auto groupStart = std::find(_macros.begin(), _macros.end(), item);
auto groupEnd = std::next(groupStart, item->GroupSize() + 1);
std::deque<std::shared_ptr<Macro>> elementsToMove(groupStart, groupEnd);
for (const auto &element : elementsToMove) {
auto it = std::find(_macros.begin(), _macros.end(), element);
_macros.erase(it);
it = std::find(_macros.begin(), _macros.end(), moveAfter);
if (it == _macros.end()) {
_macros.insert(it, element);
} else {
_macros.insert(std::next(it), element);
}
moveAfter = element;
}
endMoveRows();
assert(IsInValidState());
}
static inline int
CountItemsVisibleInModel(const std::deque<std::shared_ptr<Macro>> &macros)
{
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<Macro> item)
{
auto lock = LockContext();
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);
}
void MacroTreeModel::Remove(std::shared_ptr<Macro> item)
{
auto lock = LockContext();
auto uiStartIdx = GetItemModelIndex(item);
if (uiStartIdx == -1) {
return;
}
auto macroStartIdx = ModelIndexToMacroIndex(uiStartIdx, _macros);
auto uiEndIdx = uiStartIdx;
auto macroEndIdx = macroStartIdx;
bool isGroup = item->IsGroup();
if (isGroup) {
macroEndIdx += item->GroupSize();
if (!item->IsCollapsed()) {
uiEndIdx += item->GroupSize();
}
} else if (item->IsSubitem()) {
Macro::PrepareMoveToGroup(nullptr, item);
}
beginRemoveRows(QModelIndex(), uiStartIdx, uiEndIdx);
_macros.erase(std::next(_macros.begin(), macroStartIdx),
std::next(_macros.begin(), macroEndIdx + 1));
endRemoveRows();
_mt->selectionModel()->clear();
if (isGroup) {
UpdateGroupState(true);
}
assert(IsInValidState());
}
std::shared_ptr<Macro> MacroTreeModel::Neighbor(const std::shared_ptr<Macro> &m,
bool above) const
{
if (!m) {
return std::shared_ptr<Macro>();
}
auto it = std::find(_macros.begin(), _macros.end(), m);
if (it == _macros.end()) {
return std::shared_ptr<Macro>();
}
if (above) {
if (it == _macros.begin()) {
return std::shared_ptr<Macro>();
}
return *std::prev(it, 1);
}
auto result = std::next(it, 1);
if (result == _macros.end()) {
return std::shared_ptr<Macro>();
}
return *result;
}
std::shared_ptr<Macro>
MacroTreeModel::FindEndOfGroup(const std::shared_ptr<Macro> &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<Macro> MacroTreeModel::GetCurrentMacro() const
{
auto sel = _mt->selectionModel()->selection();
if (sel.empty()) {
return std::shared_ptr<Macro>();
}
auto idx = sel.indexes().back().row();
if (idx >= (int)_macros.size()) {
return std::shared_ptr<Macro>();
}
return _macros[ModelIndexToMacroIndex(idx, _macros)];
}
std::vector<std::shared_ptr<Macro>>
MacroTreeModel::GetCurrentMacros(const QModelIndexList &selection) const
{
std::vector<std::shared_ptr<Macro>> result;
result.reserve(selection.size());
for (const auto &sel : selection) {
try {
result.emplace_back(_macros.at(
ModelIndexToMacroIndex(sel.row(), _macros)));
} catch (const std::out_of_range &) {
}
}
return result;
}
MacroTreeModel::MacroTreeModel(MacroTree *st_,
std::deque<std::shared_ptr<Macro>> &macros)
: 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<Macro> 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<Macro> item;
try {
item = _macros.at(ModelIndexToMacroIndex(index.row(), _macros));
} catch (const std::out_of_range &) {
return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled;
}
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<Macro> &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<Macro> &item) const
{
return MacroIndexToModelIndex(GetItemMacroIndex(item), _macros);
}
bool MacroTreeModel::IsLastItem(std::shared_ptr<Macro> item) const
{
return GetItemModelIndex(item) + 1 == (int)_macros.size();
}
bool MacroTreeModel::IsInValidState()
{
// Check for reordering erros
for (size_t i = 0, j = 0; i < _macros.size(); i++) {
const auto &m = _macros[i];
if (QString::fromStdString(m->Name()) !=
data(index(j, 0), Qt::AccessibleTextRole)) {
return false;
}
if (m->IsGroup() && m->IsCollapsed()) {
i += m->GroupSize();
}
j++;
}
// Check for group errors
for (size_t i = 0; i < _macros.size(); i++) {
const auto &m = _macros[i];
if (!m->IsGroup()) {
continue;
}
const auto groupSize = m->GroupSize();
assert(i + groupSize < _macros.size());
for (uint32_t j = 1; j <= groupSize; j++) {
assert(_macros[i + j]->IsSubitem());
}
}
return true;
}
void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices)
{
if (indices.count() == 0) {
return;
}
auto lock = LockContext();
QString name = GetNewGroupName();
std::vector<std::shared_ptr<Macro>> itemsToGroup;
itemsToGroup.reserve(indices.size());
int insertGroupAt = indices[0].row();
for (int i = 0; i < indices.count(); i++) {
int selectedIdx = indices[i].row();
if (selectedIdx < insertGroupAt) {
insertGroupAt = selectedIdx;
}
std::shared_ptr<Macro> item =
_macros[ModelIndexToMacroIndex(selectedIdx, _macros)];
itemsToGroup.emplace_back(item);
}
std::shared_ptr<Macro> group =
Macro::CreateGroup(name.toStdString(), itemsToGroup);
if (!group) {
return;
}
// Add new list entry for group
insertGroupAt = ModelIndexToMacroIndex(insertGroupAt, _macros);
_macros.insert(_macros.begin() + insertGroupAt, group);
// Move all selected items after new group entry
int offset = 1;
for (const auto &item : itemsToGroup) {
auto it = _macros.begin() + GetItemMacroIndex(item);
_macros.erase(it);
_macros.insert(_macros.begin() + insertGroupAt + offset, item);
offset++;
}
_hasGroups = true;
_mt->selectionModel()->clear();
Reset(_macros);
assert(IsInValidState());
}
void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices)
{
if (indices.count() == 0) {
return;
}
auto lock = LockContext();
for (int i = indices.count() - 1; i >= 0; i--) {
std::shared_ptr<Macro> item = _macros[ModelIndexToMacroIndex(
indices[i].row(), _macros)];
if (item->IsGroup()) {
Macro::RemoveGroup(item);
}
}
_mt->selectionModel()->clear();
Reset(_macros);
assert(IsInValidState());
}
void MacroTreeModel::ExpandGroup(std::shared_ptr<Macro> item)
{
auto idx = GetItemModelIndex(item);
if (idx == -1 || !item->IsGroup() || !item->GroupSize() ||
!item->IsCollapsed()) {
return;
}
item->SetCollapsed(false);
Reset(_macros);
_mt->selectionModel()->clear();
assert(IsInValidState());
}
void MacroTreeModel::CollapseGroup(std::shared_ptr<Macro> item)
{
auto idx = GetItemModelIndex(item);
if (idx == -1 || !item->IsGroup() || !item->GroupSize() ||
item->IsCollapsed()) {
return;
}
item->SetCollapsed(true);
Reset(_macros);
_mt->selectionModel()->clear();
assert(IsInValidState());
}
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<std::shared_ptr<Macro>> &macros,
bool highlight)
{
_highlight = highlight;
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<Macro> item,
std::shared_ptr<Macro> after) const
{
GetModel()->Add(item);
if (after) {
MoveItemAfter(item, after);
}
assert(GetModel()->IsInValidState());
}
std::shared_ptr<Macro> MacroTree::GetCurrentMacro() const
{
return GetModel()->GetCurrentMacro();
}
std::vector<std::shared_ptr<Macro>> MacroTree::GetCurrentMacros() const
{
QModelIndexList indices = selectedIndexes();
return GetModel()->GetCurrentMacros(indices);
}
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 &macro = mtm->_macros[i];
setIndexWidget(index,
new MacroTreeItem(this, macro, _highlight));
// Skip items of collapsed groups
if (macro->IsGroup() && macro->IsCollapsed()) {
i += macro->GroupSize();
}
modelIdx++;
}
assert(GetModel()->IsInValidState());
}
void MacroTree::UpdateWidget(const QModelIndex &idx,
std::shared_ptr<Macro> 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<Macro> 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();
}
}
}
static inline void MoveItem(std::deque<std::shared_ptr<Macro>> &items,
std::shared_ptr<Macro> &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().get();
// 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<Macro> 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<Macro> itemBelow;
if (row == (int)items.size()) {
itemBelow = nullptr;
} else {
itemBelow = items[MacroIndexToModelIndex(row, items)];
}
if (hasGroups) {
if (!itemBelow || itemBelow->Parent().get() != 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<std::shared_ptr<Macro>> subItemsToMove;
QModelIndexList indicesWithoutSubitems = indices;
if (hasGroups) {
// Remove sub-items if selected
for (int i = indices.size() - 1; i >= 0; i--) {
std::shared_ptr<Macro> 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<Macro> 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<Macro> subitem = items[j];
auto subitemGroup = subitem->Parent().get();
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<QPersistentModelIndex> 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<std::shared_ptr<Macro>> 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
auto lock = LockContext();
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<Macro> m) {
return m->Name() == targetName;
};
auto it = std::find_if(items.begin(), items.end(),
GroupNameMatches);
items.insert(std::next(it, 1), i);
}
// Update widgets and accept event
UpdateWidgets(true);
event->accept();
event->setDropAction(Qt::CopyAction);
QListView::dropEvent(event);
assert(GetModel()->IsInValidState());
}
bool MacroTree::GroupsSelected() const
{
if (SelectionEmpty()) {
return false;
}
MacroTreeModel *mtm = GetModel();
for (auto &idx : selectedIndexes()) {
std::shared_ptr<Macro> item =
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
mtm->_macros)];
if (item->IsGroup()) {
return true;
}
}
return false;
}
bool MacroTree::GroupedItemsSelected() const
{
if (SelectionEmpty()) {
return false;
}
auto *mtm = GetModel();
for (auto &idx : selectedIndexes()) {
std::shared_ptr<Macro> item =
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
mtm->_macros)];
auto parent = item->Parent();
if (parent) {
return true;
}
}
return false;
}
bool MacroTree::SingleItemSelected() const
{
return selectedIndexes().size() == 1;
}
bool MacroTree::SelectionEmpty() const
{
return selectedIndexes().empty();
}
void MacroTree::MoveItemBefore(const std::shared_ptr<Macro> &item,
const std::shared_ptr<Macro> &after) const
{
GetModel()->MoveItemBefore(item, after);
}
void MacroTree::MoveItemAfter(const std::shared_ptr<Macro> &item,
const std::shared_ptr<Macro> &after) const
{
GetModel()->MoveItemAfter(item, after);
}
MacroTreeModel *MacroTree::GetModel() const
{
return reinterpret_cast<MacroTreeModel *>(model());
}
void MacroTree::Remove(std::shared_ptr<Macro> item) const
{
GetModel()->Remove(item);
}
void MacroTree::Up(std::shared_ptr<Macro> item) const
{
auto lock = LockContext();
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<Macro> item) const
{
auto lock = LockContext();
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);
assert(GetModel()->IsInValidState());
}
void MacroTree::UngroupSelectedGroups()
{
QModelIndexList indices = selectedIndexes();
GetModel()->UngroupSelectedGroups(indices);
assert(GetModel()->IsInValidState());
}
void MacroTree::SelectionChangedHelper(const QItemSelection &,
const QItemSelection &)
{
emit MacroSelectionAboutToChange();
emit MacroSelectionChanged();
}
inline MacroTreeItem *MacroTree::GetItemWidget(int idx) const
{
QWidget *widget = indexWidget(GetModel()->createIndex(idx, 0, nullptr));
return reinterpret_cast<MacroTreeItem *>(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<MacroTree *>(parent());
QWidget *item = tree->indexWidget(index);
if (!item) {
return (QSize(0, 0));
}
return (QSize(option.widget->minimumWidth(), item->height()));
}
} // namespace advss