From 312f95659c588dbf5e56529cdae8c42e3ef782d8 Mon Sep 17 00:00:00 2001
From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com>
Date: Wed, 21 Oct 2026 21:27:22 +0200
Subject: [PATCH] Rewrite macro tree as tree model
Basically switch from list faking the group to QAbstractItemModel used
in combination with QTreeView
---
forms/advanced-scene-switcher.ui | 5 +-
lib/macro/macro-tree.cpp | 2038 ++++++++++++------------------
lib/macro/macro-tree.hpp | 165 +--
tests/test-macro-tree.cpp | 56 +-
4 files changed, 946 insertions(+), 1318 deletions(-)
diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui
index a1ea130a..255cacfa 100644
--- a/forms/advanced-scene-switcher.ui
+++ b/forms/advanced-scene-switcher.ui
@@ -699,9 +699,6 @@
QAbstractItemView::ExtendedSelection
-
- 0
-
-
@@ -4071,7 +4068,7 @@
advss::MacroTree
- QListView
+ QTreeView
diff --git a/lib/macro/macro-tree.cpp b/lib/macro/macro-tree.cpp
index 25203448..40189cda 100644
--- a/lib/macro/macro-tree.cpp
+++ b/lib/macro/macro-tree.cpp
@@ -11,24 +11,28 @@
#include
+#include
#include
#include
-#include
+#include
#include
+#include
#include
-#include
+#include
#include
-#include
+#include
+#include
#include
-Q_DECLARE_METATYPE(std::shared_ptr);
-
namespace advss {
MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem,
bool highlight)
- : _tree(tree),
+ : _running(new QCheckBox()),
+ _boxLayout(new QHBoxLayout()),
+ _label(new QLabel(QString::fromStdString(macroItem->Name()))),
+ _tree(tree),
_highlight(highlight),
_macro(macroItem)
{
@@ -36,26 +40,10 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem,
// 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->setChecked(!_macro->Paused());
_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);
@@ -64,11 +52,34 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem,
_running->setAttribute(Qt::WA_LayoutUsesWidgetRect);
#endif
- _boxLayout = new QHBoxLayout();
_boxLayout->setContentsMargins(0, 0, 0, 0);
- if (isGroup) {
+ if (_macro->IsGroup()) {
+ _expand = new SourceTreeSubItemCheckBox();
+ _expand->setProperty("sourceTreeSubItem", true);
+ _expand->setSizePolicy(QSizePolicy::Maximum,
+ QSizePolicy::Maximum);
+ _expand->setMaximumSize(10, 16);
+ _expand->setMinimumSize(10, 0);
+ _expand->setProperty("class", "checkbox-icon indicator-expand");
+#ifdef __APPLE__
+ _expand->setAttribute(Qt::WA_LayoutUsesWidgetRect);
+#endif
+ _expand->setChecked(_macro->IsCollapsed());
+ connect(_expand, &QCheckBox::toggled, this,
+ &MacroTreeItem::ExpandClicked);
+ _boxLayout->addWidget(_expand);
+
+ 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");
_boxLayout->addWidget(_iconLabel);
_boxLayout->addSpacing(2);
+ } else {
+ _boxLayout->addSpacing(3);
}
_boxLayout->addWidget(_running);
_boxLayout->addWidget(_label);
@@ -76,8 +87,6 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem,
/* Hack: Fixes a bug where scrollbars would be above the lock icon */
_boxLayout->addSpacing(16);
#endif
-
- Update(true);
setLayout(_boxLayout);
connect(_running, SIGNAL(clicked(bool)), this,
@@ -94,6 +103,15 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr macroItem,
_timer.start(1500);
}
+void MacroTreeItem::UpdateExpandState()
+{
+ if (!_expand) {
+ return;
+ }
+ const QSignalBlocker blocker(_expand);
+ _expand->setChecked(_macro->IsCollapsed());
+}
+
bool MacroTreeItem::event(QEvent *event)
{
if (event->type() != QEvent::ToolTip) {
@@ -130,55 +148,17 @@ void MacroTreeItem::EnableHighlight(bool enable)
void MacroTreeItem::RunningClicked(bool running)
{
- const auto updateWidget = [this](Macro *macro) {
- if (!macro) {
- return;
+ if (_macro->IsGroup()) {
+ for (const auto ¯o : GetGroupMacroEntries(_macro.get())) {
+ macro->SetPaused(!running);
}
-
- const auto ¯os = GetTopLevelMacros();
-
- bool found = false;
- int idx = 0;
- for (const auto &m : macros) {
- if (m.get() == macro) {
- found = true;
- break;
- }
- idx++;
- }
-
- if (!found) {
- return;
- }
-
- const auto widget = _tree->GetItemWidget(idx);
- if (widget) {
- widget->UpdateRunning();
- }
- };
-
- if (!_macro->IsGroup()) {
+ } else {
_macro->SetPaused(!running);
- if (!_macro->IsSubitem()) {
- return;
- }
-
- const auto group = _macro->Parent();
- updateWidget(group.get());
- return;
- }
-
- const auto macros = GetGroupMacroEntries(_macro.get());
- for (const auto ¯o : macros) {
- macro->SetPaused(!running);
}
// Update backend values before updating UI to prevent flickering in
// running state of the group
- for (const auto ¯o : macros) {
- updateWidget(macro.get());
- }
- UpdateRunning();
+ _tree->UpdateRunningStates();
}
void MacroTreeItem::UpdateRunning()
@@ -262,457 +242,736 @@ void MacroTreeItem::mouseDoubleClickEvent(QMouseEvent *event)
}
}
-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 && _expand) {
- _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->setProperty("sourceTreeSubItem", true);
- _expand->setSizePolicy(QSizePolicy::Maximum,
- QSizePolicy::Maximum);
- _expand->setMaximumSize(10, 16);
- _expand->setMinimumSize(10, 0);
- _expand->setProperty("class", "checkbox-icon indicator-expand");
-#ifdef __APPLE__
- _expand->setAttribute(Qt::WA_LayoutUsesWidgetRect);
-#endif
- _boxLayout->insertWidget(0, _expand);
- _expand->blockSignals(true);
- _expand->setChecked(_macro->IsCollapsed());
- _expand->blockSignals(false);
- connect(_expand, &QCheckBox::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);
- }
+ _tree->SetGroupExpanded(_macro.get(), !checked);
}
/* ========================================================================= */
-void MacroTreeModel::Reset(std::deque> &newItems)
-{
- beginResetModel();
- _macros = newItems;
- endResetModel();
-
- _mt->ResetWidgets();
-}
-
-static inline int
-ModelIndexToMacroIndex(int modelIdx,
- const std::deque> ¯os)
-{
- 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> ¯os)
-{
- 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 &item,
- const std::shared_ptr &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 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 &item,
- const std::shared_ptr &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 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> 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> ¯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 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::MoveToBeginningOfGroup(std::shared_ptr item,
- std::shared_ptr group)
-{
- auto it = std::find(_macros.begin(), _macros.end(), item);
- assert(it != _macros.end());
- std::shared_ptr tmp = *it;
- _macros.erase(it);
- it = std::find(_macros.begin(), _macros.end(), group);
- assert(it != _macros.end());
- _macros.insert(std::next(it), tmp);
-
- Reset(_macros);
-}
-
-void MacroTreeModel::Remove(std::shared_ptr 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();
-
- assert(IsInValidState());
-}
-
-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)];
-}
-
-std::vector>
-MacroTreeModel::GetCurrentMacros(const QModelIndexList &selection) const
-{
- std::vector> 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_,
+MacroTreeModel::MacroTreeModel(QObject *parent,
std::deque> ¯os)
- : QAbstractListModel(st_),
- _mt(st_),
+ : QAbstractItemModel(parent),
_macros(macros)
{
+ RebuildCache();
+}
+
+const char *MacroTreeModel::MimeType()
+{
+ return "application/x-advss-macro-tree";
+}
+
+void MacroTreeModel::RebuildCache()
+{
+ _nodes.clear();
+ _children.clear();
+ _children[nullptr];
+
+ for (size_t i = 0; i < _macros.size(); i++) {
+ auto macro = _macros[i].get();
+ auto parent = macro->Parent().get();
+ if (parent && _nodes.find(parent) == _nodes.end()) {
+ blog(LOG_WARNING,
+ "parent of macro \"%s\" not found in macro tree",
+ macro->Name().c_str());
+ parent = nullptr;
+ }
+ auto &siblings = _children[parent];
+ _nodes[macro] = {parent, (int)siblings.size(), i};
+ siblings.emplace_back(macro);
+ }
+}
+
+static bool isCachedDescendant(
+ const std::unordered_map> &children,
+ const Macro *macro, const Macro *ancestor)
+{
+ auto it = children.find(ancestor);
+ if (it == children.end()) {
+ return false;
+ }
+ for (const auto child : it->second) {
+ if (child == macro ||
+ isCachedDescendant(children, macro, child)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+size_t MacroTreeModel::SubtreeEnd(size_t position) const
+{
+ const auto root = _macros[position].get();
+ size_t end = position + 1;
+ while (end < _macros.size() &&
+ isCachedDescendant(_children, _macros[end].get(), root)) {
+ end++;
+ }
+ return end;
+}
+
+size_t MacroTreeModel::InsertPosition(const Macro *parent, int row) const
+{
+ if (row < ChildCount(parent)) {
+ return _nodes.at(ChildAt(parent, row)).position;
+ }
+ if (parent) {
+ return SubtreeEnd(_nodes.at(parent).position);
+ }
+ return _macros.size();
+}
+
+std::shared_ptr MacroTreeModel::SharedMacro(const Macro *macro) const
+{
+ auto it = _nodes.find(macro);
+ if (it == _nodes.end()) {
+ return {};
+ }
+ return _macros[it->second.position];
+}
+
+QModelIndex MacroTreeModel::index(int row, int column,
+ const QModelIndex &parent) const
+{
+ if (column != 0 || row < 0) {
+ return {};
+ }
+ auto parentMacro = static_cast(parent.internalPointer());
+ auto it = _children.find(parent.isValid() ? parentMacro : nullptr);
+ if (it == _children.end() || row >= (int)it->second.size()) {
+ return {};
+ }
+ return createIndex(row, 0, it->second[row]);
+}
+
+QModelIndex MacroTreeModel::parent(const QModelIndex &index) const
+{
+ if (!index.isValid()) {
+ return {};
+ }
+ auto it = _nodes.find(
+ static_cast(index.internalPointer()));
+ if (it == _nodes.end() || !it->second.parent) {
+ return {};
+ }
+ return IndexOf(it->second.parent);
}
int MacroTreeModel::rowCount(const QModelIndex &parent) const
{
- return parent.isValid() ? 0 : CountItemsVisibleInModel(_macros);
+ if (parent.column() > 0) {
+ return 0;
+ }
+ return ChildCount(parent.isValid() ? static_cast(
+ parent.internalPointer())
+ : nullptr);
+}
+
+int MacroTreeModel::columnCount(const QModelIndex &) const
+{
+ return 1;
}
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()));
+ if (role != Qt::AccessibleTextRole) {
+ return {};
}
-
- return QVariant();
+ auto macro = MacroAt(index);
+ if (!macro) {
+ return {};
+ }
+ return QString::fromStdString(macro->Name());
}
Qt::ItemFlags MacroTreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
- return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled;
+ return Qt::ItemIsDropEnabled;
}
- std::shared_ptr item;
- try {
- item = _macros.at(ModelIndexToMacroIndex(index.row(), _macros));
- } catch (const std::out_of_range &) {
- return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled;
+ auto macro = MacroAt(index);
+ if (!macro) {
+ return Qt::NoItemFlags;
}
- bool isGroup = item->IsGroup();
- return QAbstractListModel::flags(index) | Qt::ItemIsEditable |
- Qt::ItemIsDragEnabled |
- (isGroup ? Qt::ItemIsDropEnabled : Qt::NoItemFlags);
+ const Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled |
+ Qt::ItemIsDragEnabled;
+ return macro->IsGroup() ? flags | Qt::ItemIsDropEnabled : flags;
}
Qt::DropActions MacroTreeModel::supportedDropActions() const
{
- return QAbstractItemModel::supportedDropActions() | Qt::MoveAction;
+ return Qt::MoveAction;
}
-QString MacroTreeModel::GetNewGroupName()
+QStringList MacroTreeModel::mimeTypes() const
+{
+ return {MimeType()};
+}
+
+QMimeData *MacroTreeModel::mimeData(const QModelIndexList &indexes) const
+{
+ QStringList names;
+ for (const auto &index : indexes) {
+ names << index.data(Qt::AccessibleTextRole).toString();
+ }
+ auto data = new QMimeData();
+ data->setData(MimeType(), names.join('\n').toUtf8());
+ return data;
+}
+
+QModelIndex MacroTreeModel::IndexOf(const Macro *macro) const
+{
+ auto it = _nodes.find(macro);
+ if (it == _nodes.end()) {
+ return {};
+ }
+ return createIndex(it->second.row, 0, const_cast(macro));
+}
+
+std::shared_ptr MacroTreeModel::MacroAt(const QModelIndex &index) const
+{
+ if (!index.isValid() || index.model() != this) {
+ return {};
+ }
+ return SharedMacro(static_cast(index.internalPointer()));
+}
+
+Macro *MacroTreeModel::ParentOf(const Macro *macro) const
+{
+ auto it = _nodes.find(macro);
+ return it == _nodes.end() ? nullptr : it->second.parent;
+}
+
+int MacroTreeModel::RowOf(const Macro *macro) const
+{
+ auto it = _nodes.find(macro);
+ return it == _nodes.end() ? -1 : it->second.row;
+}
+
+int MacroTreeModel::PositionOf(const Macro *macro) const
+{
+ auto it = _nodes.find(macro);
+ return it == _nodes.end() ? -1 : (int)it->second.position;
+}
+
+int MacroTreeModel::ChildCount(const Macro *parent) const
+{
+ auto it = _children.find(parent);
+ return it == _children.end() ? 0 : (int)it->second.size();
+}
+
+Macro *MacroTreeModel::ChildAt(const Macro *parent, int row) const
+{
+ auto it = _children.find(parent);
+ if (it == _children.end() || row < 0 || row >= (int)it->second.size()) {
+ return nullptr;
+ }
+ return it->second[row];
+}
+
+void MacroTreeModel::Insert(const std::shared_ptr &item, Macro *parent,
+ int row)
+{
+ if (!item || _nodes.find(item.get()) != _nodes.end() ||
+ (parent && _nodes.find(parent) == _nodes.end())) {
+ return;
+ }
+
+ row = std::clamp(row, 0, ChildCount(parent));
+ const auto position = InsertPosition(parent, row);
+ const auto parentMacro = SharedMacro(parent);
+
+ beginInsertRows(IndexOf(parent), row, row);
+ Macro::PrepareMoveToGroup(parentMacro, item);
+ _macros.insert(_macros.begin() + position, item);
+ RebuildCache();
+ endInsertRows();
+}
+
+void MacroTreeModel::Remove(const std::shared_ptr &item)
+{
+ auto it = _nodes.find(item.get());
+ if (it == _nodes.end()) {
+ return;
+ }
+
+ const auto node = it->second;
+ const auto end = SubtreeEnd(node.position);
+
+ beginRemoveRows(IndexOf(node.parent), node.row, node.row);
+ Macro::PrepareMoveToGroup(nullptr, item);
+ _macros.erase(_macros.begin() + node.position, _macros.begin() + end);
+ RebuildCache();
+ endRemoveRows();
+}
+
+bool MacroTreeModel::Move(const std::shared_ptr &item, Macro *newParent,
+ int row)
+{
+ auto it = _nodes.find(item.get());
+ if (it == _nodes.end() ||
+ (newParent && _nodes.find(newParent) == _nodes.end())) {
+ return false;
+ }
+
+ const auto node = it->second;
+ if (newParent == item.get() ||
+ isCachedDescendant(_children, newParent, item.get())) {
+ return false;
+ }
+
+ row = std::clamp(row, 0, ChildCount(newParent));
+ if (node.parent == newParent &&
+ (row == node.row || row == node.row + 1)) {
+ return false;
+ }
+
+ const auto insertBefore = ChildAt(newParent, row);
+ const auto newParentMacro = SharedMacro(newParent);
+ if (!beginMoveRows(IndexOf(node.parent), node.row, node.row,
+ IndexOf(newParent), row)) {
+ return false;
+ }
+
+ const auto start = _macros.begin() + node.position;
+ const auto end = _macros.begin() + SubtreeEnd(node.position);
+ const std::vector> subtree(start, end);
+ _macros.erase(start, end);
+
+ auto position = _macros.end();
+ if (insertBefore) {
+ position = std::find_if(
+ _macros.begin(), _macros.end(),
+ [insertBefore](const std::shared_ptr ¯o) {
+ return macro.get() == insertBefore;
+ });
+ } else if (newParent) {
+ position = std::find(_macros.begin(), _macros.end(),
+ newParentMacro);
+ position = std::find_if(
+ std::next(position), _macros.end(),
+ [this, newParent](const std::shared_ptr &m) {
+ return !isCachedDescendant(_children, m.get(),
+ newParent);
+ });
+ }
+ _macros.insert(position, subtree.begin(), subtree.end());
+
+ Macro::PrepareMoveToGroup(newParentMacro, item);
+ RebuildCache();
+ endMoveRows();
+ return true;
+}
+
+/* ========================================================================= */
+
+MacroTree::MacroTree(QWidget *parent) : QTreeView(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%);}"));
+
+ setHeaderHidden(true);
+ setRootIsDecorated(false);
+ setIndentation(16);
+ setExpandsOnDoubleClick(false);
+ // Groups are expanded and collapsed using the checkbox of the group
+ setItemsExpandable(false);
+ setEditTriggers(QAbstractItemView::NoEditTriggers);
+ setItemDelegate(new MacroTreeDelegate(this));
+
+ connect(this, &QTreeView::expanded, this, &MacroTree::SyncExpandState);
+ connect(this, &QTreeView::collapsed, this, &MacroTree::SyncExpandState);
+}
+
+void MacroTree::Reset(std::deque> ¯os,
+ bool highlight)
+{
+ _highlight = highlight;
+
+ auto oldModel = model();
+ auto oldSelectionModel = selectionModel();
+ auto newModel = new MacroTreeModel(this, macros);
+ setModel(newModel);
+ if (oldSelectionModel) {
+ oldSelectionModel->deleteLater();
+ }
+ if (oldModel) {
+ oldModel->deleteLater();
+ }
+
+ connect(newModel, &QAbstractItemModel::rowsInserted, this,
+ qOverload<>(&MacroTree::UpdateWidgets));
+ connect(newModel, &QAbstractItemModel::rowsMoved, this,
+ qOverload<>(&MacroTree::UpdateWidgets));
+ connect(newModel, &QAbstractItemModel::rowsRemoved, this,
+ qOverload<>(&MacroTree::UpdateWidgets));
+ connect(selectionModel(), &QItemSelectionModel::selectionChanged, this,
+ &MacroTree::SelectionChangedHelper);
+
+ UpdateWidgets();
+}
+
+void MacroTree::UpdateWidgets()
+{
+ UpdateWidgets(QModelIndex());
+ ApplyExpandState(QModelIndex());
+ ApplyFilter(QModelIndex());
+}
+
+void MacroTree::UpdateWidgets(const QModelIndex &parent)
+{
+ auto treeModel = GetModel();
+ for (int row = 0; row < treeModel->rowCount(parent); row++) {
+ const auto index = treeModel->index(row, 0, parent);
+ const auto macro = treeModel->MacroAt(index);
+ auto widget = qobject_cast(indexWidget(index));
+ if (!widget || widget->GetMacro() != macro.get()) {
+ setIndexWidget(index, new MacroTreeItem(this, macro,
+ _highlight));
+ }
+ UpdateWidgets(index);
+ }
+}
+
+void MacroTree::ApplyExpandState(const QModelIndex &parent)
+{
+ auto treeModel = GetModel();
+ for (int row = 0; row < treeModel->rowCount(parent); row++) {
+ const auto index = treeModel->index(row, 0, parent);
+ const auto macro = treeModel->MacroAt(index);
+ if (!macro->IsGroup()) {
+ continue;
+ }
+ const bool expand = !macro->IsCollapsed();
+ if (isExpanded(index) != expand) {
+ setExpanded(index, expand);
+ }
+ ApplyExpandState(index);
+ }
+}
+
+void MacroTree::ApplyFilter(const QModelIndex &parent)
+{
+ auto treeModel = GetModel();
+ for (int row = 0; row < treeModel->rowCount(parent); row++) {
+ const auto index = treeModel->index(row, 0, parent);
+ const auto macro = treeModel->MacroAt(index);
+ setRowHidden(row, parent,
+ !MacroMatchesSearchFilter(macro.get()));
+ ApplyFilter(index);
+ }
+}
+
+void MacroTree::SyncExpandState(const QModelIndex &index)
+{
+ const auto macro = GetModel()->MacroAt(index);
+ if (!macro || !macro->IsGroup()) {
+ return;
+ }
+
+ const bool collapsed = !isExpanded(index);
+ macro->SetCollapsed(collapsed);
+ if (auto widget = qobject_cast(indexWidget(index))) {
+ widget->UpdateExpandState();
+ }
+
+ if (!collapsed) {
+ return;
+ }
+
+ // Avoid hidden macros being modified by actions on the selection
+ for (const auto &selected : selectionModel()->selectedIndexes()) {
+ const auto selectedMacro = GetModel()->MacroAt(selected);
+ if (IsMacroInGroup(selectedMacro.get(), macro.get())) {
+ selectionModel()->select(selected,
+ QItemSelectionModel::Deselect);
+ }
+ }
+}
+
+void MacroTree::SetGroupExpanded(Macro *group, bool expanded)
+{
+ if (!group || !group->IsGroup()) {
+ return;
+ }
+
+ group->SetCollapsed(!expanded);
+ const auto index = GetModel()->IndexOf(group);
+ if (!index.isValid()) {
+ return;
+ }
+ setExpanded(index, expanded);
+ SyncExpandState(index);
+}
+
+void MacroTree::Add(std::shared_ptr item,
+ std::shared_ptr after) const
+{
+ auto treeModel = GetModel();
+ {
+ auto lock = LockContext();
+ Macro *parent = nullptr;
+ int row = treeModel->ChildCount(nullptr);
+ if (after && treeModel->RowOf(after.get()) != -1) {
+ parent = treeModel->ParentOf(after.get());
+ row = treeModel->RowOf(after.get()) + 1;
+ }
+ treeModel->Insert(item, parent, row);
+ }
+ selectionModel()->setCurrentIndex(treeModel->IndexOf(item.get()),
+ QItemSelectionModel::ClearAndSelect);
+}
+
+void MacroTree::AddToGroup(std::shared_ptr item,
+ std::shared_ptr group) const
+{
+ auto treeModel = GetModel();
+ {
+ auto lock = LockContext();
+ treeModel->Insert(item, group.get(), 0);
+ }
+ selectionModel()->setCurrentIndex(treeModel->IndexOf(item.get()),
+ QItemSelectionModel::ClearAndSelect);
+}
+
+void MacroTree::Remove(std::shared_ptr item) const
+{
+ {
+ auto lock = LockContext();
+ GetModel()->Remove(item);
+ }
+ selectionModel()->clear();
+}
+
+void MacroTree::Up(std::shared_ptr item) const
+{
+ auto lock = LockContext();
+ auto treeModel = GetModel();
+ const int row = treeModel->RowOf(item.get());
+ if (row <= 0) {
+ return;
+ }
+ treeModel->Move(item, treeModel->ParentOf(item.get()), row - 1);
+}
+
+void MacroTree::Down(std::shared_ptr item) const
+{
+ auto lock = LockContext();
+ auto treeModel = GetModel();
+ const int row = treeModel->RowOf(item.get());
+ const auto parent = treeModel->ParentOf(item.get());
+ if (row == -1 || row + 1 >= treeModel->ChildCount(parent)) {
+ return;
+ }
+ treeModel->Move(item, parent, row + 2);
+}
+
+std::shared_ptr MacroTree::GetCurrentMacro() const
+{
+ const auto selection = selectionModel()->selectedIndexes();
+ if (selection.empty()) {
+ return {};
+ }
+ return GetModel()->MacroAt(selection.back());
+}
+
+std::vector> MacroTree::GetCurrentMacros() const
+{
+ std::vector> result;
+ for (const auto &index : selectionModel()->selectedIndexes()) {
+ auto macro = GetModel()->MacroAt(index);
+ if (macro) {
+ result.emplace_back(macro);
+ }
+ }
+ return result;
+}
+
+std::vector> MacroTree::GetSelectedSubtreeRoots() const
+{
+ auto selected = GetCurrentMacros();
+ std::vector> roots;
+ for (const auto ¯o : selected) {
+ const bool hasSelectedAncestor = std::any_of(
+ selected.begin(), selected.end(),
+ [¯o](const std::shared_ptr &other) {
+ return IsMacroInGroup(macro.get(), other.get());
+ });
+ if (!hasSelectedAncestor) {
+ roots.emplace_back(macro);
+ }
+ }
+
+ auto treeModel = GetModel();
+ std::sort(roots.begin(), roots.end(),
+ [treeModel](const std::shared_ptr &a,
+ const std::shared_ptr &b) {
+ return treeModel->PositionOf(a.get()) <
+ treeModel->PositionOf(b.get());
+ });
+ return roots;
+}
+
+bool MacroTree::GroupsSelected() const
+{
+ const auto selected = GetCurrentMacros();
+ return std::any_of(selected.begin(), selected.end(),
+ [](const std::shared_ptr ¯o) {
+ return macro->IsGroup();
+ });
+}
+
+bool MacroTree::GroupedItemsSelected() const
+{
+ const auto selected = GetCurrentMacros();
+ return std::any_of(selected.begin(), selected.end(),
+ [](const std::shared_ptr ¯o) {
+ return !!macro->Parent();
+ });
+}
+
+bool MacroTree::SingleItemSelected() const
+{
+ return selectionModel()->selectedIndexes().size() == 1;
+}
+
+bool MacroTree::SelectionEmpty() const
+{
+ return selectionModel()->selectedIndexes().empty();
+}
+
+bool MacroTree::GroupsExist() const
+{
+ const auto ¯os = GetTopLevelMacros();
+ return std::any_of(macros.begin(), macros.end(),
+ [](const std::shared_ptr ¯o) {
+ return macro->IsGroup();
+ });
+}
+
+void MacroTree::ExpandGroup(std::shared_ptr item)
+{
+ SetGroupExpanded(item.get(), true);
+}
+
+void MacroTree::CollapseGroup(std::shared_ptr item)
+{
+ SetGroupExpanded(item.get(), false);
+}
+
+void MacroTree::RefreshFilter()
+{
+ ApplyFilter(QModelIndex());
+}
+
+void MacroTree::UpdateRunningStates()
+{
+ std::function update =
+ [this, &update](const QModelIndex &parent) {
+ for (int row = 0; row < model()->rowCount(parent);
+ row++) {
+ const auto index =
+ model()->index(row, 0, parent);
+ if (auto widget = qobject_cast(
+ indexWidget(index))) {
+ widget->UpdateRunning();
+ }
+ update(index);
+ }
+ };
+ update(QModelIndex());
+}
+
+MacroTree::DropTarget MacroTree::DropTargetAt(const QPoint &pos) const
+{
+ auto treeModel = GetModel();
+ const auto index = indexAt(pos);
+ if (!index.isValid()) {
+ return {nullptr, treeModel->ChildCount(nullptr)};
+ }
+
+ const auto macro = treeModel->MacroAt(index).get();
+ const auto parent = treeModel->ParentOf(macro);
+ const auto rect = visualRect(index);
+
+ if (macro->IsGroup()) {
+ // Same margin QAbstractItemView uses for the drop indicator
+ const int margin = std::clamp(int(rect.height() / 5.5), 2, 12);
+ const bool onItem = pos.y() - rect.top() >= margin &&
+ rect.bottom() - pos.y() >= margin;
+ const bool belowExpanded = pos.y() > rect.center().y() &&
+ isExpanded(index);
+ if (onItem || belowExpanded) {
+ return {macro, 0};
+ }
+ }
+
+ if (pos.y() < rect.center().y()) {
+ return {parent, index.row()};
+ }
+ return {parent, index.row() + 1};
+}
+
+bool MacroTree::MoveSelection(const DropTarget &target)
+{
+ const auto items = GetSelectedSubtreeRoots();
+ if (items.empty()) {
+ return false;
+ }
+
+ for (const auto &item : items) {
+ if (target.parent == item.get() ||
+ IsMacroInGroup(target.parent, item.get())) {
+ return false;
+ }
+ // Nested groups are not supported yet
+ if (target.parent && item->IsGroup()) {
+ return false;
+ }
+ }
+
+ auto lock = LockContext();
+ auto treeModel = GetModel();
+ const auto insertBefore = treeModel->ChildAt(target.parent, target.row);
+ bool moved = false;
+ for (const auto &item : items) {
+ const int row = insertBefore
+ ? treeModel->RowOf(insertBefore)
+ : treeModel->ChildCount(target.parent);
+ moved |= treeModel->Move(item, target.parent, row);
+ }
+ return moved;
+}
+
+void MacroTree::dropEvent(QDropEvent *event)
+{
+ if (event->source() != this ||
+ !event->mimeData()->hasFormat(MacroTreeModel::MimeType())) {
+ QTreeView::dropEvent(event);
+ return;
+ }
+
+ MoveSelection(DropTargetAt(event->position().toPoint()));
+
+ // Prevent QAbstractItemView::startDrag() from removing the dragged
+ // rows, as they were already moved
+ event->setDropAction(Qt::CopyAction);
+ event->accept();
+ stopAutoScroll();
+ setState(NoState);
+ viewport()->update();
+}
+
+QString MacroTree::GetNewGroupName() const
{
QString fmt =
obs_module_text("AdvSceneSwitcher.macroTab.defaultGroupName");
@@ -729,717 +988,82 @@ QString MacroTreeModel::GetNewGroupName()
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();
-}
-
-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> 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 item =
- _macros[ModelIndexToMacroIndex(selectedIdx, _macros)];
- itemsToGroup.emplace_back(item);
- }
-
- std::shared_ptr 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++;
- }
-
- _mt->selectionModel()->clear();
-
- Reset(_macros);
- assert(IsInValidState());
-
- RegisterGroupCreateUndoRedo(name.toStdString());
-}
-
-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 item = _macros[ModelIndexToMacroIndex(
- indices[i].row(), _macros)];
- if (item->IsGroup()) {
- RegisterGroupRemoveUndoRedo(item->Name());
- Macro::RemoveGroup(item);
- }
- }
-
- _mt->selectionModel()->clear();
-
- Reset(_macros);
- assert(IsInValidState());
-}
-
-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();
- assert(IsInValidState());
-}
-
-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();
- assert(IsInValidState());
-}
-
-void MacroTree::Reset(std::deque> ¯os,
- bool highlight)
-{
- _highlight = highlight;
-
- auto mtm = new MacroTreeModel(this, macros);
- setModel(mtm);
- GetModel()->Reset(macros);
- connect(selectionModel(),
- SIGNAL(selectionChanged(const QItemSelection &,
- const QItemSelection &)),
- this,
- SLOT(SelectionChangedHelper(const QItemSelection &,
- const QItemSelection &)));
-}
-
-void MacroTree::Add(std::shared_ptr item,
- std::shared_ptr after) const
-{
- GetModel()->Add(item);
- if (after) {
- MoveItemAfter(item, after);
- }
- assert(GetModel()->IsInValidState());
-}
-
-void MacroTree::AddToGroup(std::shared_ptr item,
- std::shared_ptr group) const
-{
- GetModel()->Add(item);
- GetModel()->MoveToBeginningOfGroup(item, group);
-}
-
-std::shared_ptr MacroTree::GetCurrentMacro() const
-{
- return GetModel()->GetCurrentMacro();
-}
-
-std::vector> 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();
- 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++;
- }
- assert(GetModel()->IsInValidState());
-}
-
-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];
- auto 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();
- }
- }
-}
-
-void MacroTree::UpdateRunningStates()
-{
- MacroTreeModel *mtm = GetModel();
- for (int i = 0; i < (int)mtm->_macros.size(); i++) {
- auto widget = GetItemWidget(i);
- if (widget) {
- widget->UpdateRunning();
- }
- }
-}
-
-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(event->position().toPoint()).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 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().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> 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().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 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
- 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 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 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 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();
-}
-
-bool MacroTree::GroupsExist() const
-{
- for (const auto ¯o : GetTopLevelMacros()) {
- if (macro->IsGroup()) {
- return true;
- }
- }
- return false;
-}
-
-void MacroTree::ExpandGroup(std::shared_ptr item) const
-{
- auto *mtm = GetModel();
- mtm->ExpandGroup(item);
-}
-
-void MacroTree::CollapseGroup(std::shared_ptr item) const
-{
- auto *mtm = GetModel();
- mtm->CollapseGroup(item);
-}
-
-void MacroTree::RefreshFilter()
-{
- UpdateWidgets();
- doItemsLayout();
-}
-
-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 qobject_cast(model());
-}
-
-void MacroTree::Remove(std::shared_ptr item) const
-{
- GetModel()->Remove(item);
-}
-
-void MacroTree::Up(std::shared_ptr 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 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());
+ const auto items = GetSelectedSubtreeRoots();
+ if (items.empty()) {
+ return;
+ }
+
+ auto treeModel = GetModel();
+ const auto parent = treeModel->ParentOf(items.front().get());
+ for (const auto &item : items) {
+ if (treeModel->ParentOf(item.get()) != parent) {
+ return;
+ }
+ }
+
+ const auto name = GetNewGroupName().toStdString();
+ std::shared_ptr group;
+ {
+ auto lock = LockContext();
+ std::vector> noChildren;
+ group = Macro::CreateGroup(name, noChildren);
+ treeModel->Insert(group, parent,
+ treeModel->RowOf(items.front().get()));
+ int row = 0;
+ for (const auto &item : items) {
+ treeModel->Move(item, group.get(), row++);
+ }
+ RegisterGroupCreateUndoRedo(name);
+ }
+
+ selectionModel()->clear();
+ SetGroupExpanded(group.get(), true);
}
void MacroTree::UngroupSelectedGroups()
{
- QModelIndexList indices = selectedIndexes();
- GetModel()->UngroupSelectedGroups(indices);
- assert(GetModel()->IsInValidState());
+ std::vector> groups;
+ for (const auto ¯o : GetCurrentMacros()) {
+ if (macro->IsGroup()) {
+ groups.emplace_back(macro);
+ }
+ }
+ if (groups.empty()) {
+ return;
+ }
+
+ {
+ auto lock = LockContext();
+ auto treeModel = GetModel();
+ for (const auto &group : groups) {
+ RegisterGroupRemoveUndoRedo(group->Name());
+ const auto parent = treeModel->ParentOf(group.get());
+ const int row = treeModel->RowOf(group.get());
+ const auto children = GetGroupChildren(group.get());
+ int offset = 1;
+ for (const auto &child : children) {
+ treeModel->Move(child, parent, row + offset++);
+ }
+ treeModel->Remove(group);
+ }
+ }
+
+ selectionModel()->clear();
}
void MacroTree::ExpandAll()
{
for (const auto ¯o : GetTopLevelMacros()) {
- if (!macro->IsGroup()) {
- continue;
- }
- ExpandGroup(macro);
+ SetGroupExpanded(macro.get(), true);
}
}
void MacroTree::CollapseAll()
{
for (const auto ¯o : GetTopLevelMacros()) {
- if (!macro->IsGroup()) {
- continue;
- }
- CollapseGroup(macro);
+ SetGroupExpanded(macro.get(), false);
}
}
@@ -1450,16 +1074,9 @@ void MacroTree::SelectionChangedHelper(const QItemSelection &,
emit MacroSelectionChanged();
}
-inline MacroTreeItem *MacroTree::GetItemWidget(int idx) const
-{
- auto widget = indexWidget(GetModel()->createIndex(idx, 0, nullptr));
- return qobject_cast(widget);
-}
-
void MacroTree::paintEvent(QPaintEvent *event)
{
- MacroTreeModel *mtm = GetModel();
- if (mtm && mtm->_macros.empty()) {
+ if (model() && model()->rowCount() == 0) {
QPainter painter(viewport());
const QRect rectangle =
QRect(0, 0, size().width(), size().height());
@@ -1468,10 +1085,30 @@ void MacroTree::paintEvent(QPaintEvent *event)
painter.drawText(rectangle, Qt::AlignCenter | Qt::TextWordWrap,
text, &boundingRect);
} else {
- QListView::paintEvent(event);
+ QTreeView::paintEvent(event);
}
}
+void MacroTree::drawBranches(QPainter *painter, const QRect &rect,
+ const QModelIndex &index) const
+{
+ // Only draw the background, as the branch indicators would duplicate
+ // the expand checkbox of nested groups
+ QStyleOptionViewItem option;
+ initViewItemOption(&option);
+ option.rect = rect;
+ if (selectionModel()->isSelected(index)) {
+ option.state |= QStyle::State_Selected;
+ }
+ style()->drawPrimitive(QStyle::PE_PanelItemViewRow, &option, painter,
+ this);
+}
+
+MacroTreeModel *MacroTree::GetModel() const
+{
+ return qobject_cast(model());
+}
+
MacroTreeDelegate::MacroTreeDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
@@ -1481,24 +1118,11 @@ QSize MacroTreeDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
auto tree = qobject_cast(parent());
- auto widget = tree->indexWidget(index);
-
+ auto widget = tree ? tree->indexWidget(index) : nullptr;
if (!widget) {
return QStyledItemDelegate::sizeHint(option, index);
}
-
- auto name = index.data(Qt::AccessibleTextRole).toString();
- auto macro = GetMacroByQString(name);
-
- if (!macro) {
- return QStyledItemDelegate::sizeHint(option, index);
- }
-
- if (MacroMatchesSearchFilter(macro)) {
- return QSize(widget->sizeHint());
- }
-
- return QSize(0, 0);
+ return widget->sizeHint();
}
} // namespace advss
diff --git a/lib/macro/macro-tree.hpp b/lib/macro/macro-tree.hpp
index 65bef68d..2538eea7 100644
--- a/lib/macro/macro-tree.hpp
+++ b/lib/macro/macro-tree.hpp
@@ -1,19 +1,19 @@
#pragma once
-#include
-#include
+#include
#include
-#include
-#include
-#include
+#include
#include
+#include
+#include
-#include
-#include
#include
+#include
+#include
+#include
+#include
class QLabel;
-class QSpacerItem;
class QHBoxLayout;
// Only used to enable applying "SourceTreeSubItemCheckBox" stylesheet
@@ -33,33 +33,26 @@ class MacroTreeItem : public QFrame {
public:
explicit MacroTreeItem(MacroTree *tree, std::shared_ptr macro,
bool highlight);
+ Macro *GetMacro() const { return _macro.get(); }
+ void UpdateExpandState();
protected:
bool event(QEvent *) override;
+public slots:
+ void UpdateRunning();
+
private slots:
void ExpandClicked(bool checked);
void EnableHighlight(bool enable);
void RunningClicked(bool);
- void UpdateRunning();
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;
@@ -70,59 +63,65 @@ private:
std::chrono::high_resolution_clock::time_point _lastHighlightCheckTime{};
QTimer _timer;
std::shared_ptr _macro;
-
- friend class MacroTree;
- friend class MacroTreeModel;
};
-class MacroTreeModel : public QAbstractListModel {
+// Exposes the flat, pre-ordered list of macros as a tree.
+// The structure reported to Qt is only updated from the macro list between the
+// begin*() and end*() calls of each modification, so changes of the macro
+// parents made before a modification do not confuse the views.
+class MacroTreeModel : public QAbstractItemModel {
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;
+ MacroTreeModel(QObject *parent,
+ std::deque> ¯os);
+
+ QModelIndex index(int row, int column,
+ const QModelIndex &parent = {}) const override;
+ QModelIndex parent(const QModelIndex &index) const override;
+ int rowCount(const QModelIndex &parent = {}) const override;
+ int columnCount(const QModelIndex &parent = {}) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ Qt::DropActions supportedDropActions() const override;
+ QStringList mimeTypes() const override;
+ QMimeData *mimeData(const QModelIndexList &indexes) const override;
+
+ static const char *MimeType();
+
+ QModelIndex IndexOf(const Macro *macro) const;
+ std::shared_ptr MacroAt(const QModelIndex &index) const;
+ Macro *ParentOf(const Macro *macro) const;
+ int RowOf(const Macro *macro) const;
+ int PositionOf(const Macro *macro) const;
+ int ChildCount(const Macro *parent) const;
+ Macro *ChildAt(const Macro *parent, int row) const;
+
+ void Insert(const std::shared_ptr &item, Macro *parent, int row);
+ void Remove(const std::shared_ptr &item);
+ // row is the position among the children of the new parent before
+ // the move via beginMoveRows()
+ bool Move(const std::shared_ptr &item, Macro *newParent,
+ int row);
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 MoveToBeginningOfGroup(std::shared_ptr item,
- std::shared_ptr);
- 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;
- std::vector>
- GetCurrentMacros(const QModelIndexList &) const;
- QString GetNewGroupName();
- void GroupSelectedItems(QModelIndexList &indices);
- void UngroupSelectedGroups(QModelIndexList &indices);
- void ExpandGroup(std::shared_ptr item);
- void CollapseGroup(std::shared_ptr item);
- int GetItemMacroIndex(const std::shared_ptr &item) const;
- int GetItemModelIndex(const std::shared_ptr &item) const;
- bool IsLastItem(std::shared_ptr item) const;
- bool IsInValidState();
+ struct Node {
+ Macro *parent = nullptr;
+ int row = 0;
+ size_t position = 0;
+ };
+
+ void RebuildCache();
+ size_t SubtreeEnd(size_t position) const;
+ size_t InsertPosition(const Macro *parent, int row) const;
+ std::shared_ptr SharedMacro(const Macro *macro) const;
- MacroTree *_mt;
std::deque> &_macros;
-
- friend class MacroTree;
- friend class MacroTreeItem;
+ std::unordered_map _nodes;
+ std::unordered_map> _children;
};
-class MacroTree : public QListView {
+class MacroTree : public QTreeView {
Q_OBJECT
public:
@@ -142,8 +141,9 @@ public:
bool SingleItemSelected() const;
bool SelectionEmpty() const;
bool GroupsExist() const;
- void ExpandGroup(std::shared_ptr item) const;
- void CollapseGroup(std::shared_ptr item) const;
+ void ExpandGroup(std::shared_ptr item);
+ void CollapseGroup(std::shared_ptr item);
+ void SetGroupExpanded(Macro *group, bool expanded);
void RefreshFilter();
void UpdateRunningStates();
@@ -160,24 +160,31 @@ signals:
void MacroSelectionChanged();
protected:
- virtual void dropEvent(QDropEvent *event) override;
- virtual void paintEvent(QPaintEvent *event) override;
+ void dropEvent(QDropEvent *event) override;
+ void paintEvent(QPaintEvent *event) override;
+ void drawBranches(QPainter *painter, const QRect &rect,
+ const QModelIndex &index) const override;
+
+private slots:
+ void SyncExpandState(const QModelIndex &index);
+ void UpdateWidgets();
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;
+ struct DropTarget {
+ Macro *parent = nullptr;
+ int row = 0;
+ };
+
+ DropTarget DropTargetAt(const QPoint &pos) const;
+ bool MoveSelection(const DropTarget &target);
+ void UpdateWidgets(const QModelIndex &parent);
+ void ApplyExpandState(const QModelIndex &parent);
+ void ApplyFilter(const QModelIndex &parent);
+ std::vector> GetSelectedSubtreeRoots() const;
+ QString GetNewGroupName() const;
MacroTreeModel *GetModel() const;
bool _highlight = false;
-
- friend class MacroTreeModel;
- friend class MacroTreeItem;
};
class MacroTreeDelegate : public QStyledItemDelegate {
@@ -185,8 +192,8 @@ class MacroTreeDelegate : public QStyledItemDelegate {
public:
MacroTreeDelegate(QObject *parent);
- virtual QSize sizeHint(const QStyleOptionViewItem &option,
- const QModelIndex &index) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
};
} // namespace advss
diff --git a/tests/test-macro-tree.cpp b/tests/test-macro-tree.cpp
index 8ecef1ec..e2e84e8f 100644
--- a/tests/test-macro-tree.cpp
+++ b/tests/test-macro-tree.cpp
@@ -226,18 +226,12 @@ public:
return result;
}
- // MacroTree::dropEvent() emits row move signals before the underlying
- // data is changed
- void IgnoreModelSignalErrors() { _ignoreModelSignalErrors = true; }
-
void CheckInvariants() const
{
ProcessEvents();
_checker->CheckStructure();
for (const auto &error : _checker->Errors()) {
- if (!_ignoreModelSignalErrors) {
- FAIL_CHECK(error);
- }
+ FAIL_CHECK(error);
}
CheckGroupStructure();
REQUIRE(VisibleRows() == ExpectedVisibleRows());
@@ -353,7 +347,6 @@ private:
MacroTree *_tree;
ModelChecker *_checker = nullptr;
QLineEdit *_searchText = nullptr;
- bool _ignoreModelSignalErrors = false;
};
} // namespace
@@ -365,6 +358,26 @@ TEST_CASE("Macro tree shows macros and groups", "[macro-tree]")
REQUIRE(f.VisibleRows() == Names{"A", "G1", "B", "C", "D", "G2", "H"});
}
+TEST_CASE("Macro tree shows nested groups", "[macro-tree]")
+{
+ TreeFixture f("A, G1[B, G2{C, G3[D]}, E], F");
+ f.CheckInvariants();
+ REQUIRE(f.VisibleRows() == Names{"A", "G1", "B", "G2", "E", "F"});
+
+ f.ClickCheckBox(f.ExpandCheckBox("G2"));
+ f.CheckInvariants();
+ REQUIRE(f.VisibleRows() ==
+ Names{"A", "G1", "B", "G2", "C", "G3", "D", "E", "F"});
+
+ f.Tree()->Up(GetMacro("G3"));
+ f.CheckInvariants();
+ REQUIRE(DescribeMacros() == "A, G1[B, G2[G3[D], C], E], F");
+
+ f.Tree()->Remove(GetMacro("G2"));
+ f.CheckInvariants();
+ REQUIRE(DescribeMacros() == "A, G1[B, E], F");
+}
+
TEST_CASE("Macro tree selection", "[macro-tree]")
{
TreeFixture f("A, G1{B, C}, D, G2[E, F], H");
@@ -715,7 +728,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro below macro")
{
TreeFixture f("A, B, C, D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("A"), f.RowBottom("C"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "B, C, A, D");
@@ -724,7 +736,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro above macro")
{
TreeFixture f("A, B, C, D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "D, A, B, C");
@@ -733,7 +744,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro below last row")
{
TreeFixture f("A, B, C");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("A"),
QPoint(40, f.Viewport()->height() - 5));
f.CheckInvariants();
@@ -743,7 +753,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro onto expanded group")
{
TreeFixture f("A, G1[B, C], D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("A"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "G1[A, B, C], D");
@@ -752,7 +761,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro between subitems")
{
TreeFixture f("A, G1[B, C], D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("C"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "A, G1[B, D, C]");
@@ -761,7 +769,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Subitem out of group")
{
TreeFixture f("A, G1[B, C], D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("B"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "A, G1[C], D, B");
@@ -770,7 +777,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Subitem to top")
{
TreeFixture f("A, G1[B, C], D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("C"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "C, A, G1[B], D");
@@ -779,7 +785,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Macro past collapsed group")
{
TreeFixture f("A, G1{B, C}, D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowTop("A"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "D, A, G1{B, C}");
@@ -788,7 +793,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Expanded group")
{
TreeFixture f("A, G1[B, C], D, E");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G1"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "A, D, G1[B, C], E");
@@ -797,7 +801,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Collapsed group")
{
TreeFixture f("A, G1{B, C}, D, E");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G1"), f.RowBottom("D"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "A, D, G1{B, C}, E");
@@ -806,7 +809,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Group onto group is rejected")
{
TreeFixture f("G1[A], G2[B]");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("G2"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "G1[A], G2[B]");
@@ -815,7 +817,6 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
SECTION("Multiple macros")
{
TreeFixture f("A, B, C, D");
- f.IgnoreModelSignalErrors();
f.SelectRows({"A", "C"});
f.Drag(f.RowCenter("C"), f.RowBottom("D"));
f.CheckInvariants();
@@ -823,12 +824,14 @@ TEST_CASE("Macro tree drag and drop", "[macro-tree]")
}
}
-// Known issue: dropping onto an expanded group inserts at the start of the
-// group, dropping onto a collapsed group appends at the end
-TEST_CASE("Macro tree drop onto collapsed group", "[macro-tree][!shouldfail]")
+TEST_CASE("Macro tree drop onto collapsed group", "[macro-tree]")
{
+ if (!DragAndDropSupported()) {
+ WARN("Skipped: drag and drop is not supported by the Qt platform");
+ return;
+ }
+
TreeFixture f("A, G1{B, C}, D");
- f.IgnoreModelSignalErrors();
f.Drag(f.RowCenter("D"), f.RowCenter("G1"));
f.CheckInvariants();
REQUIRE(DescribeMacros() == "A, G1{D, B, C}");
@@ -914,10 +917,7 @@ TEST_CASE("Macro tree running state", "[macro-tree]")
}
}
-// Known issue: MacroTreeItem::RunningClicked() uses the position in the macro
-// list as model row, which is wrong if a collapsed group is above
-TEST_CASE("Macro tree running state after collapsed group",
- "[macro-tree][!shouldfail]")
+TEST_CASE("Macro tree running state after collapsed group", "[macro-tree]")
{
TreeFixture f("G0{X}, A, G1[B, C]");
f.ClickCheckBox(f.RunningCheckBox("B"));