diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index d33e931b3..85a130669 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -152,6 +152,7 @@ set(obs_SOURCES window-log-reply.cpp window-projector.cpp window-remux.cpp + source-tree.cpp properties-view.cpp focus-list.cpp menu-button.cpp @@ -198,6 +199,7 @@ set(obs_HEADERS window-log-reply.hpp window-projector.hpp window-remux.hpp + source-tree.hpp properties-view.hpp properties-view.moc.hpp display-helpers.hpp @@ -211,6 +213,7 @@ set(obs_HEADERS visibility-checkbox.hpp locked-checkbox.hpp horizontal-scroll-area.hpp + expand-checkbox.hpp vertical-scroll-area.hpp visibility-item-widget.hpp slider-absoluteset-style.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 162a09085..00f6c3fe1 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -84,6 +84,7 @@ StudioMode.Preview="Preview" StudioMode.Program="Program" ShowInMultiview="Show in Multiview" VerticalLayout="Vertical Layout" +Group="Group" # warning if program already open AlreadyRunning.Title="OBS is already running" @@ -458,6 +459,9 @@ Basic.Main.StoppingReplayBuffer="Stopping Replay Buffer..." Basic.Main.StopStreaming="Stop Streaming" Basic.Main.StoppingStreaming="Stopping Stream..." Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" +Basic.Main.Group="Group %1" +Basic.Main.GroupItems="Group Selected Items" +Basic.Main.Ungroup="Ungroup" # basic mode file menu Basic.MainMenu.File="&File" diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index f4b767e48..216976ed6 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -94,7 +94,8 @@ QMenuBar::item:selected { } /* Listbox item */ -QListWidget::item { +QListWidget::item, +SourceTree::item { padding: 4px 2px; margin-bottom: 2px; margin-top: 0px; @@ -110,11 +111,6 @@ QListWidget QLineEdit { border-radius: none; } -SourceListWidget::item { - margin-bottom: 1px; - padding: -4px 2px; -} - /* Dock stuff */ QDockWidget { background: transparent; @@ -157,6 +153,23 @@ SourceListWidget { border-bottom: 2px solid #2f2f2f; } +SourceTree { + border: none; + border-bottom: 1px solid #2f2f2f; +} + +SourceTree QLabel { + padding: 2px 0px; + margin: -2px 4px -2px; +} + +SourceTree QLineEdit { + background-color: #0c101e; + padding: 2px; + margin: -2px 6px -2px 3px; + font-size: 12px; +} + #scenesFrame, #sourcesFrame { margin-left: -7px; @@ -179,13 +192,15 @@ SourceListWidget { } /* Listbox item selected, unfocused */ -QListWidget::item:hover { +QListWidget::item:hover, +SourceTree::item:hover { background-color: #212121; border: 1px solid #333336; } /* Listbox item selected */ -QListWidget::item:selected { +QListWidget::item:selected, +SourceTree::item:selected { background-color: #131a30; border: 1px solid #252a45; } @@ -727,6 +742,30 @@ OBSHotkeyLabel[hotkeyPairHover=true] { } +/* Group Collapse Checkbox */ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; + padding: 0px; +} + +SourceTreeSubItemCheckBox::indicator { + width: 12px; + height: 12px; +} + +SourceTreeSubItemCheckBox::indicator:checked, +SourceTreeSubItemCheckBox::indicator:checked:hover { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked, +SourceTreeSubItemCheckBox::indicator:unchecked:hover { + image: url(./Dark/collapse.png); +} + + /* Label warning/error */ QLabel#warningLabel { @@ -753,10 +792,6 @@ OBSBasicProperties, background: #101010; } -#OBSBasicSourceSelect #sourceList { - border-bottom: 2px solid #333336; -} - FocusList::item { padding: 0px 2px; } diff --git a/UI/data/themes/Dark.qss b/UI/data/themes/Dark.qss index 78c343dbf..6140a3234 100644 --- a/UI/data/themes/Dark.qss +++ b/UI/data/themes/Dark.qss @@ -551,6 +551,27 @@ OBSHotkeyLabel[hotkeyPairHover=true] { } +/* Group Collapse Checkbox */ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked { + image: url(./Dark/collapse.png); +} + + /* Label warning/error */ QLabel#warningLabel { diff --git a/UI/data/themes/Dark/collapse.png b/UI/data/themes/Dark/collapse.png new file mode 100644 index 000000000..1fd72a116 Binary files /dev/null and b/UI/data/themes/Dark/collapse.png differ diff --git a/UI/data/themes/Dark/expand.png b/UI/data/themes/Dark/expand.png new file mode 100644 index 000000000..dd5d2b5ec Binary files /dev/null and b/UI/data/themes/Dark/expand.png differ diff --git a/UI/data/themes/Default.qss b/UI/data/themes/Default.qss index 0b88dd451..37f4b2063 100644 --- a/UI/data/themes/Default.qss +++ b/UI/data/themes/Default.qss @@ -51,6 +51,24 @@ MuteCheckBox::indicator:unchecked { image: url(:/res/images/unmute.png); } +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked { + image: url(:/res/images/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked { + image: url(:/res/images/collapse.png); +} + OBSHotkeyLabel[hotkeyPairHover=true] { color: red; } diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 5249acc91..be067c067 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -701,6 +701,30 @@ MuteCheckBox::indicator:unchecked:disabled { image: url(./Dark/unmute.png); } +/****************************/ +/* --- Group Checkboxes --- */ +/****************************/ + +SourceTreeSubItemCheckBox { + background: transparent; + outline: none; +} + +SourceTreeSubItemCheckBox::indicator { + width: 10px; + height: 10px; +} + +SourceTreeSubItemCheckBox::indicator:checked, +SourceTreeSubItemCheckBox::indicator:checked:hover { + image: url(./Dark/expand.png); +} + +SourceTreeSubItemCheckBox::indicator:unchecked, +SourceTreeSubItemCheckBox::indicator:unchecked:hover { + image: url(./Dark/collapse.png); +} + /*************************/ /* --- Progress bars --- */ /*************************/ diff --git a/UI/expand-checkbox.hpp b/UI/expand-checkbox.hpp new file mode 100644 index 000000000..375a4ce55 --- /dev/null +++ b/UI/expand-checkbox.hpp @@ -0,0 +1,5 @@ +#include + +class ExpandCheckBox : public QCheckBox { + Q_OBJECT +}; diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 5f06b0ce3..103db2c58 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -518,7 +518,7 @@ 0 - + 0 @@ -1682,9 +1682,9 @@ 1 - SourceListWidget - QListWidget -
source-list-widget.hpp
+ SourceTree + QListView +
source-tree.hpp
diff --git a/UI/forms/images/collapse.png b/UI/forms/images/collapse.png new file mode 100644 index 000000000..04707a44d Binary files /dev/null and b/UI/forms/images/collapse.png differ diff --git a/UI/forms/images/expand.png b/UI/forms/images/expand.png new file mode 100644 index 000000000..1222bcdc1 Binary files /dev/null and b/UI/forms/images/expand.png differ diff --git a/UI/forms/obs.qrc b/UI/forms/obs.qrc index 367fc13ba..a68bc705e 100644 --- a/UI/forms/obs.qrc +++ b/UI/forms/obs.qrc @@ -17,6 +17,8 @@ images/tray_active.png images/locked_mask.png images/unlocked_mask.png + images/collapse.png + images/expand.png images/settings/advanced.png diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp new file mode 100644 index 000000000..e02e3fceb --- /dev/null +++ b/UI/source-tree.cpp @@ -0,0 +1,1286 @@ +#include "window-basic-main.hpp" +#include "obs-app.hpp" +#include "source-tree.hpp" +#include "qt-wrappers.hpp" +#include "visibility-checkbox.hpp" +#include "locked-checkbox.hpp" +#include "expand-checkbox.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline OBSScene GetCurrentScene() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + return main->GetCurrentScene(); +} + +/* ========================================================================= */ + +SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) + : tree (tree_), + sceneitem (sceneitem_) +{ + setAttribute(Qt::WA_TranslucentBackground); + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + const char *name = obs_source_get_name(source); + + vis = new VisibilityCheckBox(); + vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + vis->setMaximumSize(16, 16); + vis->setChecked(obs_sceneitem_visible(sceneitem)); + + lock = new LockedCheckBox(); + lock->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + lock->setMaximumSize(16, 16); + lock->setChecked(obs_sceneitem_locked(sceneitem)); + + label = new QLabel(QT_UTF8(name)); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + label->setAttribute(Qt::WA_TranslucentBackground); + + boxLayout = new QHBoxLayout(); + boxLayout->setContentsMargins(1, 1, 2, 1); + boxLayout->setSpacing(1); + boxLayout->addWidget(label); + boxLayout->addWidget(vis); + boxLayout->addWidget(lock); + + Update(false); + + setLayout(boxLayout); + + /* --------------------------------------------------------- */ + + auto setItemVisible = [this] (bool checked) + { + SignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_visible(sceneitem, checked); + }; + + auto setItemLocked = [this] (bool checked) + { + SignalBlocker sourcesSignalBlocker(this); + obs_sceneitem_set_locked(sceneitem, checked); + }; + + connect(vis, &QAbstractButton::clicked, setItemVisible); + connect(lock, &QAbstractButton::clicked, setItemLocked); +} + +void SourceTreeItem::DisconnectSignals() +{ + sceneRemoveSignal.Disconnect(); + itemRemoveSignal.Disconnect(); + visibleSignal.Disconnect(); + renameSignal.Disconnect(); + removeSignal.Disconnect(); +} + +void SourceTreeItem::ReconnectSignals() +{ + if (!sceneitem) + return; + + DisconnectSignals(); + + /* --------------------------------------------------------- */ + + auto removeItem = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = + (obs_sceneitem_t*)calldata_ptr(cd, "item"); + + if (!curItem || curItem == this_->sceneitem) { + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + } + }; + + auto itemVisible = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + obs_sceneitem_t *curItem = + (obs_sceneitem_t*)calldata_ptr(cd, "item"); + bool visible = calldata_bool(cd, "visible"); + + if (curItem == this_->sceneitem) + QMetaObject::invokeMethod(this_, "VisibilityChanged", + Q_ARG(bool, visible)); + }; + + obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem); + obs_source_t *sceneSource = obs_scene_get_source(scene); + signal_handler_t *signal = obs_source_get_signal_handler(sceneSource); + + sceneRemoveSignal.Connect(signal, "remove", removeItem, this); + itemRemoveSignal.Connect(signal, "item_remove", removeItem, this); + visibleSignal.Connect(signal, "item_visible", itemVisible, this); + + /* --------------------------------------------------------- */ + + auto renamed = [] (void *data, calldata_t *cd) + { + SourceTreeItem *this_ = reinterpret_cast(data); + const char *name = calldata_string(cd, "new_name"); + + QMetaObject::invokeMethod(this_, "Renamed", + Q_ARG(QString, QT_UTF8(name))); + }; + + auto removeSource = [] (void *data, calldata_t *) + { + SourceTreeItem *this_ = reinterpret_cast(data); + this_->DisconnectSignals(); + this_->sceneitem = nullptr; + }; + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + signal = obs_source_get_signal_handler(source); + renameSignal.Connect(signal, "rename", renamed, this); + removeSignal.Connect(signal, "remove", removeSource, this); +} + +void SourceTreeItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + if (expand) { + expand->setChecked(!expand->isChecked()); + } else { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + OBSBasic *main = + reinterpret_cast(App()->GetMainWindow()); + if (source) { + main->CreatePropertiesWindow(source); + } + } +} + +void SourceTreeItem::EnterEditMode() +{ + setFocusPolicy(Qt::StrongFocus); + boxLayout->removeWidget(label); + editor = new QLineEdit(label->text()); + editor->installEventFilter(this); + boxLayout->insertWidget(1, editor); + setFocusProxy(editor); +} + +void SourceTreeItem::ExitEditMode(bool save) +{ + if (!editor) + return; + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSScene scene = main->GetCurrentScene(); + + std::string newName = QT_TO_UTF8(editor->text()); + + setFocusProxy(nullptr); + boxLayout->removeWidget(editor); + delete editor; + editor = nullptr; + setFocusPolicy(Qt::NoFocus); + boxLayout->insertWidget(1, label); + + /* ----------------------------------------- */ + /* check for empty string */ + + if (!save) + return; + + if (newName.empty()) { + OBSMessageBox::information(main, + QTStr("NoNameEntered.Title"), + QTStr("NoNameEntered.Text")); + return; + } + + /* ----------------------------------------- */ + /* Check for same name */ + + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (newName == obs_source_get_name(source)) + return; + + /* ----------------------------------------- */ + /* check for existing source */ + + bool exists = false; + + if (obs_sceneitem_is_group(sceneitem)) { + exists = !!obs_scene_get_group(scene, newName.c_str()); + } else { + obs_source_t *existingSource = + obs_get_source_by_name(newName.c_str()); + obs_source_release(existingSource); + exists = !!existingSource; + } + + if (exists) { + OBSMessageBox::information(main, + QTStr("NameExists.Title"), + QTStr("NameExists.Text")); + return; + } + + /* ----------------------------------------- */ + /* rename */ + + SignalBlocker sourcesSignalBlocker(this); + obs_source_set_name(source, newName.c_str()); + label->setText(QT_UTF8(newName.c_str())); +} + +bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) +{ + if (editor != object) + return false; + + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + + switch (keyEvent->key()) { + case Qt::Key_Escape: + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, false)); + return true; + case Qt::Key_Tab: + case Qt::Key_Backtab: + case Qt::Key_Enter: + case Qt::Key_Return: + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, true)); + return true; + } + } else if (event->type() == QEvent::FocusOut) { + QMetaObject::invokeMethod(this, "ExitEditMode", + Qt::QueuedConnection, + Q_ARG(bool, false)); + return true; + } + + return false; +} + +void SourceTreeItem::VisibilityChanged(bool visible) +{ + vis->setChecked(visible); +} + +void SourceTreeItem::Renamed(const QString &name) +{ + label->setText(name); +} + +void SourceTreeItem::Update(bool force) +{ + OBSScene scene = GetCurrentScene(); + obs_scene_t *itemScene = obs_sceneitem_get_scene(sceneitem); + + Type newType; + + /* ------------------------------------------------- */ + /* if it's a group item, insert group checkbox */ + + if (obs_sceneitem_is_group(sceneitem)) { + newType = Type::Group; + + /* ------------------------------------------------- */ + /* if it's a group sub-item */ + + } else if (itemScene != scene) { + newType = Type::SubItem; + + /* ------------------------------------------------- */ + /* if it's a regular item */ + + } else { + newType = Type::Item; + } + + /* ------------------------------------------------- */ + + if (!force && newType == type) { + return; + } + + /* ------------------------------------------------- */ + + ReconnectSignals(); + + 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); + boxLayout->insertWidget(0, expand); + + obs_data_t *data = obs_sceneitem_get_private_settings(sceneitem); + expand->blockSignals(true); + expand->setChecked(obs_data_get_bool(data, "collapsed")); + expand->blockSignals(false); + obs_data_release(data); + + connect(expand, &QPushButton::toggled, + this, &SourceTreeItem::ExpandClicked); + + } else { + spacer = new QSpacerItem(3, 1); + boxLayout->insertItem(0, spacer); + } +} + +void SourceTreeItem::ExpandClicked(bool checked) +{ + OBSData data = obs_sceneitem_get_private_settings(sceneitem); + obs_data_release(data); + + obs_data_set_bool(data, "collapsed", checked); + + if (!checked) + tree->GetStm()->ExpandGroup(sceneitem); + else + tree->GetStm()->CollapseGroup(sceneitem); +} + +/* ========================================================================= */ + +void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SourceTreeModel *stm = reinterpret_cast(ptr); + + switch ((int)event) { + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + stm->SceneChanged(); + break; + case OBS_FRONTEND_EVENT_EXIT: + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP: + stm->Clear(); + break; + } +} + +void SourceTreeModel::Clear() +{ + beginResetModel(); + items.clear(); + endResetModel(); + + hasGroups = false; +} + +static bool enumItem(obs_scene_t*, obs_sceneitem_t *item, void *ptr) +{ + QVector &items = + *reinterpret_cast*>(ptr); + + if (obs_sceneitem_is_group(item)) { + obs_data_t *data = obs_sceneitem_get_private_settings(item); + + bool collapse = obs_data_get_bool(data, "collapsed"); + if (!collapse) { + obs_scene_t *scene = + obs_sceneitem_group_get_scene(item); + + obs_scene_enum_items(scene, enumItem, &items); + } + + obs_data_release(data); + } + + items.insert(0, item); + return true; +} + +void SourceTreeModel::SceneChanged() +{ + OBSScene scene = GetCurrentScene(); + + beginResetModel(); + items.clear(); + obs_scene_enum_items(scene, enumItem, &items); + endResetModel(); + + UpdateGroupState(false); + st->ResetWidgets(); + + for (int i = 0; i < items.count(); i++) { + bool select = obs_sceneitem_selected(items[i]); + QModelIndex index = createIndex(i, 0); + + st->selectionModel()->select(index, select + ? QItemSelectionModel::Select + : QItemSelectionModel::Deselect); + } +} + +/* moves a scene item index (blame linux distros for using older Qt builds) */ +static inline void MoveItem(QVector &items, int oldIdx, int newIdx) +{ + OBSSceneItem item = items[oldIdx]; + items.remove(oldIdx); + items.insert(newIdx, item); +} + +/* reorders list optimally with model reorder funcs */ +void SourceTreeModel::ReorderItems() +{ + OBSScene scene = GetCurrentScene(); + + QVector newitems; + obs_scene_enum_items(scene, enumItem, &newitems); + + /* if item list has changed size, do full reset */ + if (newitems.count() != items.count()) { + SceneChanged(); + return; + } + + for (;;) { + int idx1Old = 0; + int idx1New = 0; + int count; + int i; + + /* find first starting changed item index */ + for (i = 0; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[i]; + obs_sceneitem_t *newItem = newitems[i]; + if (oldItem != newItem) { + idx1Old = i; + break; + } + } + + /* if everything is the same, break */ + if (i == newitems.count()) { + break; + } + + /* find new starting index */ + for (i = idx1Old + 1; i < newitems.count(); i++) { + obs_sceneitem_t *oldItem = items[idx1Old]; + obs_sceneitem_t *newItem = newitems[i]; + + if (oldItem == newItem) { + idx1New = i; + break; + } + } + + /* if item could not be found, do full reset */ + if (i == newitems.count()) { + SceneChanged(); + return; + } + + /* get move count */ + for (count = 1; (idx1New + count) < newitems.count(); count++) { + int oldIdx = idx1Old + count; + int newIdx = idx1New + count; + + obs_sceneitem_t *oldItem = items[oldIdx]; + obs_sceneitem_t *newItem = newitems[newIdx]; + + if (oldItem != newItem) { + break; + } + } + + /* move items */ + beginMoveRows(QModelIndex(), idx1Old, idx1Old + count - 1, + QModelIndex(), idx1New + count); + for (i = 0; i < count; i++) { + int to = idx1New + count; + if (to > idx1Old) + to--; + MoveItem(items, idx1Old, to); + } + endMoveRows(); + } +} + +void SourceTreeModel::Add(obs_sceneitem_t *item) +{ + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, item); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), item); +} + +void SourceTreeModel::Remove(obs_sceneitem_t *item) +{ + int idx = -1; + for (int i = 0; i < items.count(); i++) { + if (items[i] == item) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + int startIdx = idx; + int endIdx = idx; + + bool is_group = obs_sceneitem_is_group(item); + if (is_group) { + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = endIdx + 1; i < items.count(); i++) { + obs_sceneitem_t *subitem = items[i]; + obs_scene_t *subscene = + obs_sceneitem_get_scene(subitem); + + if (subscene == scene) + endIdx = i; + else + break; + } + } + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(idx, endIdx - startIdx + 1); + endRemoveRows(); + + if (is_group) + UpdateGroupState(true); +} + +OBSSceneItem SourceTreeModel::Get(int idx) +{ + if (idx == -1 || idx >= items.count()) + return OBSSceneItem(); + return items[idx]; +} + +SourceTreeModel::SourceTreeModel(SourceTree *st_) + : QAbstractListModel (st_), + st (st_) +{ + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +SourceTreeModel::~SourceTreeModel() +{ + obs_frontend_remove_event_callback(OBSFrontendEvent, this); +} + +int SourceTreeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.count(); +} + +QVariant SourceTreeModel::data(const QModelIndex &, int) const +{ + return QVariant(); +} + +Qt::ItemFlags SourceTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index) | Qt::ItemIsDropEnabled; + + obs_sceneitem_t *item = items[index.row()]; + bool is_group = obs_sceneitem_is_group(item); + + return QAbstractListModel::flags(index) | + Qt::ItemIsEditable | + Qt::ItemIsDragEnabled | + (is_group ? Qt::ItemIsDropEnabled : Qt::NoItemFlags); +} + +Qt::DropActions SourceTreeModel::supportedDropActions() const +{ + return QAbstractItemModel::supportedDropActions() | Qt::MoveAction; +} + +QString SourceTreeModel::GetNewGroupName() +{ + OBSScene scene = GetCurrentScene(); + QString name; + + int i = 1; + for (;;) { + name = QTStr("Basic.Main.Group").arg(QString::number(i++)); + obs_sceneitem_t *group = obs_scene_get_group(scene, + QT_TO_UTF8(name)); + if (!group) + break; + } + + return name; +} + +void SourceTreeModel::AddGroup() +{ + QString name = GetNewGroupName(); + obs_sceneitem_t *group = obs_scene_add_group(GetCurrentScene(), + QT_TO_UTF8(name)); + if (!group) + return; + + beginInsertRows(QModelIndex(), 0, 0); + items.insert(0, group); + endInsertRows(); + + st->UpdateWidget(createIndex(0, 0, nullptr), group); + UpdateGroupState(true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, + Q_ARG(int, 0)); +} + +void SourceTreeModel::GroupSelectedItems(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + OBSScene scene = GetCurrentScene(); + QString name = GetNewGroupName(); + + QVector item_order; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + item_order << item; + } + + obs_sceneitem_t *item = obs_scene_insert_group( + scene, QT_TO_UTF8(name), + item_order.data(), item_order.size()); + if (!item) { + return; + } + + for (obs_sceneitem_t *item : item_order) + obs_sceneitem_select(item, false); + + int newIdx = indices[0].row(); + + beginInsertRows(QModelIndex(), newIdx, newIdx); + items.insert(newIdx, item); + endInsertRows(); + + for (int i = 0; i < indices.size(); i++) { + int fromIdx = indices[i].row() + 1; + int toIdx = newIdx + i + 1; + if (fromIdx != toIdx) { + beginMoveRows(QModelIndex(), fromIdx, fromIdx, + QModelIndex(), toIdx); + MoveItem(items, fromIdx, toIdx); + endMoveRows(); + } + } + + hasGroups = true; + st->UpdateWidgets(true); + + obs_sceneitem_select(item, true); + + QMetaObject::invokeMethod(st, "Edit", Qt::QueuedConnection, + Q_ARG(int, newIdx)); +} + +void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices) +{ + if (indices.count() == 0) + return; + + for (int i = indices.count() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_sceneitem_group_ungroup(item); + } + + SceneChanged(); +} + +void SourceTreeModel::ExpandGroup(obs_sceneitem_t *item) +{ + int itemIdx = items.indexOf(item); + if (itemIdx == -1) + return; + + itemIdx++; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + QVector subItems; + obs_scene_enum_items(scene, enumItem, &subItems); + + if (!subItems.size()) + return; + + beginInsertRows(QModelIndex(), itemIdx, itemIdx + subItems.size() - 1); + for (int i = 0; i < subItems.size(); i++) + items.insert(i + itemIdx, subItems[i]); + endInsertRows(); + + st->UpdateWidgets(); +} + +void SourceTreeModel::CollapseGroup(obs_sceneitem_t *item) +{ + int startIdx = -1; + int endIdx = -1; + + obs_scene_t *scene = obs_sceneitem_group_get_scene(item); + + for (int i = 0; i < items.size(); i++) { + obs_scene_t *itemScene = obs_sceneitem_get_scene(items[i]); + + if (itemScene == scene) { + if (startIdx == -1) + startIdx = i; + endIdx = i; + } + } + + if (startIdx == -1) + return; + + beginRemoveRows(QModelIndex(), startIdx, endIdx); + items.remove(startIdx, endIdx - startIdx + 1); + endRemoveRows(); +} + +void SourceTreeModel::UpdateGroupState(bool update) +{ + bool nowHasGroups = false; + for (auto &item : items) { + if (obs_sceneitem_is_group(item)) { + nowHasGroups = true; + break; + } + } + + if (nowHasGroups != hasGroups) { + hasGroups = nowHasGroups; + if (update) { + st->UpdateWidgets(true); + } + } +} + +/* ========================================================================= */ + +SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) +{ + SourceTreeModel *stm_ = new SourceTreeModel(this); + setModel(stm_); +} + +void SourceTree::ResetWidgets() +{ + OBSScene scene = GetCurrentScene(); + + SourceTreeModel *stm = GetStm(); + stm->UpdateGroupState(false); + + for (int i = 0; i < stm->items.count(); i++) { + QModelIndex index = stm->createIndex(i, 0, nullptr); + setIndexWidget(index, new SourceTreeItem(this, stm->items[i])); + } +} + +void SourceTree::UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item) +{ + setIndexWidget(idx, new SourceTreeItem(this, item)); +} + +void SourceTree::UpdateWidgets(bool force) +{ + SourceTreeModel *stm = GetStm(); + + for (int i = 0; i < stm->items.size(); i++) { + obs_sceneitem_t *item = stm->items[i]; + SourceTreeItem *widget = GetItemWidget(i); + + if (!widget) { + UpdateWidget(stm->createIndex(i, 0), item); + } else { + widget->Update(force); + } + } +} + +void SourceTree::SelectItem(obs_sceneitem_t *sceneitem, bool select) +{ + SourceTreeModel *stm = GetStm(); + int i = 0; + + for (; i < stm->items.count(); i++) { + if (stm->items[i] == sceneitem) + break; + } + + if (i == stm->items.count()) + return; + + QModelIndex index = stm->createIndex(i, 0); + if (index.isValid()) + selectionModel()->select(index, select + ? QItemSelectionModel::Select + : QItemSelectionModel::Deselect); +} + +Q_DECLARE_METATYPE(OBSSceneItem); + +void SourceTree::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + QListView::mouseDoubleClickEvent(event); +} + +void SourceTree::dropEvent(QDropEvent *event) +{ + if (event->source() != this) { + QListView::dropEvent(event); + return; + } + + OBSScene scene = GetCurrentScene(); + SourceTreeModel *stm = GetStm(); + auto &items = stm->items; + QModelIndexList indices = selectedIndexes(); + + DropIndicatorPosition indicator = dropIndicatorPosition(); + int row = indexAt(event->pos()).row(); + bool emptyDrop = row == -1; + + if (emptyDrop) { + if (!items.size()) { + QListView::dropEvent(event); + return; + } + + row = items.size() - 1; + indicator = QAbstractItemView::BelowItem; + } + + /* --------------------------------------- */ + /* store destination group if moving to a */ + /* group */ + + obs_sceneitem_t *dropItem = items[row]; /* item being dropped on */ + bool itemIsGroup = obs_sceneitem_is_group(dropItem); + + obs_sceneitem_t *dropGroup = itemIsGroup + ? dropItem + : obs_sceneitem_get_group(dropItem); + + /* 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) { + obs_data_t *data = obs_sceneitem_get_private_settings(dropGroup); + dropOnCollapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + } + + if (indicator == QAbstractItemView::BelowItem || + indicator == QAbstractItemView::OnItem) + row++; + + if (row < 0 || row > stm->items.count()) { + QListView::dropEvent(event); + return; + } + + /* --------------------------------------- */ + /* determine if any base group is selected */ + + bool hasGroups = false; + for (int i = 0; i < indices.size(); i++) { + obs_sceneitem_t *item = items[indices[i].row()]; + if (obs_sceneitem_is_group(item)) { + hasGroups = true; + break; + } + } + + /* --------------------------------------- */ + /* if dropping a group, detect if it's */ + /* below another group */ + + obs_sceneitem_t *itemBelow = row == stm->items.count() + ? nullptr + : stm->items[row]; + if (hasGroups) { + if (!itemBelow || + obs_sceneitem_get_group(itemBelow) != dropGroup) { + indicator = QAbstractItemView::BelowItem; + 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 */ + + if (hasGroups) { + /* remove sub-items if selected */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + obs_scene_t *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + indices.removeAt(i); + } + } + + /* add all sub-items of selected groups */ + for (int i = indices.size() - 1; i >= 0; i--) { + obs_sceneitem_t *item = items[indices[i].row()]; + + if (obs_sceneitem_is_group(item)) { + for (int j = items.size() - 1; j >= 0; j--) { + obs_sceneitem_t *subitem = items[j]; + obs_sceneitem_t *subitemGroup = + obs_sceneitem_get_group(subitem); + + if (subitemGroup == item) { + QModelIndex idx = + stm->createIndex(j, 0); + indices.insert(i + 1, idx); + } + } + } + } + } + + /* --------------------------------------- */ + /* build persistent indices */ + + QList persistentIndices; + persistentIndices.reserve(indices.count()); + for (QModelIndex &index : indices) + persistentIndices.append(index); + std::sort(persistentIndices.begin(), persistentIndices.end()); + + /* --------------------------------------- */ + /* 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) { + stm->beginMoveRows(QModelIndex(), from, from, + QModelIndex(), to); + MoveItem(items, from, itemTo); + stm->endMoveRows(); + } + + r = persistentIdx.row() + 1; + } + + std::sort(persistentIndices.begin(), persistentIndices.end()); + int firstIdx = persistentIndices.front().row(); + int lastIdx = persistentIndices.back().row(); + + /* --------------------------------------- */ + /* reorder scene items in back-end */ + + QVector orderList; + obs_sceneitem_t *lastGroup = nullptr; + int insertCollapsedIdx = 0; + + auto insertCollapsed = [&] (obs_sceneitem_t *item) + { + struct obs_sceneitem_order_info info; + info.group = lastGroup; + info.item = item; + + orderList.insert(insertCollapsedIdx++, info); + }; + + using insertCollapsed_t = decltype(insertCollapsed); + + auto preInsertCollapsed = [] (obs_scene_t *, obs_sceneitem_t *item, + void *param) + { + (*reinterpret_cast(param))(item); + return true; + }; + + auto insertLastGroup = [&] () + { + obs_data_t *data = obs_sceneitem_get_private_settings(lastGroup); + bool collapsed = obs_data_get_bool(data, "collapsed"); + obs_data_release(data); + + if (collapsed) { + insertCollapsedIdx = 0; + obs_sceneitem_group_enum_items( + lastGroup, + preInsertCollapsed, + &insertCollapsed); + } + + struct obs_sceneitem_order_info info; + info.group = nullptr; + info.item = lastGroup; + orderList.insert(0, info); + }; + + auto updateScene = [&] () + { + struct obs_sceneitem_order_info info; + + for (int i = 0; i < items.size(); i++) { + obs_sceneitem_t *item = items[i]; + obs_sceneitem_t *group; + + if (obs_sceneitem_is_group(item)) { + if (lastGroup) { + insertLastGroup(); + } + lastGroup = item; + continue; + } + + if (!hasGroups && i >= firstIdx && i <= lastIdx) + group = dropGroup; + else + group = obs_sceneitem_get_group(item); + + if (lastGroup && lastGroup != group) { + insertLastGroup(); + } + + lastGroup = group; + + info.group = group; + info.item = item; + orderList.insert(0, info); + } + + if (lastGroup) { + insertLastGroup(); + } + + obs_scene_reorder_items2(scene, + orderList.data(), orderList.size()); + }; + + using updateScene_t = decltype(updateScene); + + auto preUpdateScene = [] (void *data, obs_scene_t *) + { + (*reinterpret_cast(data))(); + }; + + ignoreReorder = true; + obs_scene_atomic_update(scene, preUpdateScene, &updateScene); + ignoreReorder = false; + + /* --------------------------------------- */ + /* remove items if dropped in to collapsed */ + /* group */ + + if (dropOnCollapsed) { + stm->beginRemoveRows(QModelIndex(), firstIdx, lastIdx); + items.remove(firstIdx, lastIdx - firstIdx + 1); + stm->endRemoveRows(); + } + + /* --------------------------------------- */ + /* update widgets and accept event */ + + UpdateWidgets(true); + + event->accept(); + event->setDropAction(Qt::CopyAction); + + QListView::dropEvent(event); +} + +void SourceTree::selectionChanged( + const QItemSelection &selected, + const QItemSelection &deselected) +{ + { + SignalBlocker sourcesSignalBlocker(this); + SourceTreeModel *stm = GetStm(); + + QModelIndexList selectedIdxs = selected.indexes(); + QModelIndexList deselectedIdxs = deselected.indexes(); + + for (int i = 0; i < selectedIdxs.count(); i++) { + int idx = selectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], true); + } + + for (int i = 0; i < deselectedIdxs.count(); i++) { + int idx = deselectedIdxs[i].row(); + obs_sceneitem_select(stm->items[idx], false); + } + } + QListView::selectionChanged(selected, deselected); +} + +void SourceTree::Edit(int row) +{ + SourceTreeModel *stm = GetStm(); + if (row < 0 || row >= stm->items.count()) + return; + + QWidget *widget = indexWidget(stm->createIndex(row, 0)); + SourceTreeItem *itemWidget = reinterpret_cast(widget); + itemWidget->EnterEditMode(); + edit(stm->createIndex(row, 0)); +} + +bool SourceTree::MultipleBaseSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (obs_sceneitem_is_group(item)) { + return false; + } + + obs_scene *itemScene = obs_sceneitem_get_scene(item); + if (itemScene != scene) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + + OBSScene scene = GetCurrentScene(); + + if (selectedIndices.size() < 1) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + if (!obs_sceneitem_is_group(item)) { + return false; + } + } + + return true; +} + +bool SourceTree::GroupedItemsSelected() const +{ + SourceTreeModel *stm = GetStm(); + QModelIndexList selectedIndices = selectedIndexes(); + OBSScene scene = GetCurrentScene(); + + if (!selectedIndices.size()) { + return false; + } + + for (auto &idx : selectedIndices) { + obs_sceneitem_t *item = stm->items[idx.row()]; + obs_scene *itemScene = obs_sceneitem_get_scene(item); + + if (itemScene != scene) { + return true; + } + } + + return false; +} + +void SourceTree::GroupSelectedItems() +{ + QModelIndexList indices = selectedIndexes(); + std::sort(indices.begin(), indices.end()); + GetStm()->GroupSelectedItems(indices); +} + +void SourceTree::UngroupSelectedGroups() +{ + QModelIndexList indices = selectedIndexes(); + GetStm()->UngroupSelectedGroups(indices); +} + +void SourceTree::AddGroup() +{ + GetStm()->AddGroup(); +} diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp new file mode 100644 index 000000000..c1326d4d0 --- /dev/null +++ b/UI/source-tree.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QLabel; +class QCheckBox; +class QLineEdit; +class SourceTree; +class QSpacerItem; +class QHBoxLayout; +class LockedCheckBox; +class VisibilityCheckBox; +class VisibilityItemWidget; + +class SourceTreeSubItemCheckBox : public QCheckBox { + Q_OBJECT +}; + +class SourceTreeItem : public QWidget { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeModel; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + + void Update(bool force); + + enum class Type { + Unknown, + Item, + Group, + SubItem, + }; + + void DisconnectSignals(); + void ReconnectSignals(); + + Type type = Type::Unknown; + +public: + explicit SourceTreeItem(SourceTree *tree, OBSSceneItem sceneitem); + +private: + QSpacerItem *spacer = nullptr; + QCheckBox *expand = nullptr; + VisibilityCheckBox *vis = nullptr; + LockedCheckBox *lock = nullptr; + QHBoxLayout *boxLayout = nullptr; + QLabel *label = nullptr; + + QLineEdit *editor = nullptr; + + SourceTree *tree; + OBSSceneItem sceneitem; + OBSSignal sceneRemoveSignal; + OBSSignal itemRemoveSignal; + OBSSignal visibleSignal; + OBSSignal renameSignal; + OBSSignal removeSignal; + +private slots: + void EnterEditMode(); + void ExitEditMode(bool save); + + void VisibilityChanged(bool visible); + void Renamed(const QString &name); + + void ExpandClicked(bool checked); +}; + +class SourceTreeModel : public QAbstractListModel { + Q_OBJECT + + friend class SourceTree; + friend class SourceTreeItem; + + SourceTree *st; + QVector items; + bool hasGroups = false; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + void Clear(); + void SceneChanged(); + void ReorderItems(); + + void Add(obs_sceneitem_t *item); + void Remove(obs_sceneitem_t *item); + OBSSceneItem Get(int idx); + QString GetNewGroupName(); + void AddGroup(); + + void GroupSelectedItems(QModelIndexList &indices); + void UngroupSelectedGroups(QModelIndexList &indices); + + void ExpandGroup(obs_sceneitem_t *item); + void CollapseGroup(obs_sceneitem_t *item); + + void UpdateGroupState(bool update); + +public: + explicit SourceTreeModel(SourceTree *st); + ~SourceTreeModel(); + + 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; +}; + +class SourceTree : public QListView { + Q_OBJECT + + bool ignoreReorder = false; + + friend class SourceTreeModel; + friend class SourceTreeItem; + + void ResetWidgets(); + void UpdateWidget(const QModelIndex &idx, obs_sceneitem_t *item); + void UpdateWidgets(bool force = false); + + inline SourceTreeModel *GetStm() const + { + return reinterpret_cast(model()); + } + + inline SourceTreeItem *GetItemWidget(int idx) + { + QWidget *widget = indexWidget(GetStm()->createIndex(idx, 0)); + return reinterpret_cast(widget); + } + +public: + explicit SourceTree(QWidget *parent = nullptr); + + inline bool IgnoreReorder() const {return ignoreReorder;} + inline void ReorderItems() {GetStm()->ReorderItems();} + inline void Clear() {GetStm()->Clear();} + + inline void Add(obs_sceneitem_t *item) {GetStm()->Add(item);} + inline void Remove(obs_sceneitem_t *item) {GetStm()->Remove(item);} + inline OBSSceneItem Get(int idx) {return GetStm()->Get(idx);} + inline QString GetNewGroupName() {return GetStm()->GetNewGroupName();} + + void SelectItem(obs_sceneitem_t *sceneitem, bool select); + + bool MultipleBaseSelected() const; + bool GroupsSelected() const; + bool GroupedItemsSelected() const; + +public slots: + void GroupSelectedItems(); + void UngroupSelectedGroups(); + void AddGroup(); + void Edit(int idx); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void dropEvent(QDropEvent *event) override; + + virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; +}; diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 2adccd734..fb60cedf1 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -54,6 +54,7 @@ #include "display-helpers.hpp" #include "volume-control.hpp" #include "remote-text.hpp" +#include "source-tree.hpp" #ifdef _WIN32 #include "win-update/win-update.hpp" @@ -148,8 +149,6 @@ OBSBasic::OBSBasic(QWidget *parent) copyActionsDynamicProperties(); - ui->sources->setItemDelegate(new VisibilityItemDelegate(ui->sources)); - char styleSheetPath[512]; int ret = GetProfilePath(styleSheetPath, sizeof(styleSheetPath), "stylesheet.qss"); @@ -201,13 +200,6 @@ OBSBasic::OBSBasic(QWidget *parent) SLOT(SceneNameEdited(QWidget*, QAbstractItemDelegate::EndEditHint))); - connect(ui->sources->itemDelegate(), - SIGNAL(closeEditor(QWidget*, - QAbstractItemDelegate::EndEditHint)), - this, - SLOT(SceneItemNameEdited(QWidget*, - QAbstractItemDelegate::EndEditHint))); - cpuUsageInfo = os_cpu_usage_info_start(); cpuUsageTimer = new QTimer(this); connect(cpuUsageTimer, SIGNAL(timeout()), @@ -2024,7 +2016,7 @@ OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) OBSSceneItem OBSBasic::GetCurrentSceneItem() { - return GetSceneItem(GetTopSelectedSourceItem()); + return ui->sources->Get(GetTopSelectedSourceItem()); } void OBSBasic::UpdatePreviewScalingMenu() @@ -2047,32 +2039,6 @@ void OBSBasic::UpdatePreviewScalingMenu() scalingAmount == float(ovi.output_width) / float(ovi.base_width)); } -void OBSBasic::UpdateSources(OBSScene scene) -{ - ClearListItems(ui->sources); - - obs_scene_enum_items(scene, - [] (obs_scene_t *scene, obs_sceneitem_t *item, void *p) - { - OBSBasic *window = static_cast(p); - window->InsertSceneItem(item); - - UNUSED_PARAMETER(scene); - return true; - }, this); -} - -void OBSBasic::InsertSceneItem(obs_sceneitem_t *item) -{ - QListWidgetItem *listItem = new QListWidgetItem(); - SetOBSRef(listItem, OBSSceneItem(item)); - - ui->sources->insertItem(0, listItem); - ui->sources->setCurrentRow(0, QItemSelectionModel::ClearAndSelect); - - SetupVisibilityItem(ui->sources, listItem, item); -} - void OBSBasic::CreateInteractionWindow(obs_source_t *source) { if (interaction) @@ -2199,7 +2165,7 @@ void OBSBasic::RemoveScene(OBSSource source) if (sel != nullptr) { if (sel == ui->scenes->currentItem()) - ClearListItems(ui->sources); + ui->sources->Clear(); delete sel; } @@ -2221,7 +2187,7 @@ void OBSBasic::AddSceneItem(OBSSceneItem item) obs_scene_t *scene = obs_sceneitem_get_scene(item); if (GetCurrentScene() == scene) - InsertSceneItem(item); + ui->sources->Add(item); SaveProject(); @@ -2237,14 +2203,7 @@ void OBSBasic::AddSceneItem(OBSSceneItem item) void OBSBasic::RemoveSceneItem(OBSSceneItem item) { - for (int i = 0; i < ui->sources->count(); i++) { - QListWidgetItem *listItem = ui->sources->item(i); - - if (GetOBSRef(listItem) == item) { - DeleteListItem(ui->sources, listItem); - break; - } - } + ui->sources->Remove(item); SaveProject(); @@ -2276,8 +2235,6 @@ void OBSBasic::UpdateSceneSelection(OBSSource source) ui->scenes->setCurrentItem(items.first()); sceneChanging = false; - UpdateSources(scene); - OBSScene curScene = GetOBSRef(ui->scenes->currentItem()); if (api && scene != curScene) @@ -2322,19 +2279,7 @@ void OBSBasic::SelectSceneItem(OBSScene scene, OBSSceneItem item, bool select) if (scene != GetCurrentScene() || ignoreSelectionUpdate) return; - for (int i = 0; i < ui->sources->count(); i++) { - QListWidgetItem *witem = ui->sources->item(i); - QVariant data = - witem->data(static_cast(QtDataRole::OBSRef)); - if (!data.canConvert()) - continue; - - if (item != data.value()) - continue; - - witem->setSelected(select); - break; - } + ui->sources->SelectItem(item, select); } static inline bool SourceMixerHidden(obs_source_t *source) @@ -2664,7 +2609,8 @@ void OBSBasic::DeactivateAudioSource(OBSSource source) bool OBSBasic::QueryRemoveSource(obs_source_t *source) { - if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE) { + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && + !obs_sceneitem_group_from_source(source)) { int count = ui->scenes->count(); if (count == 1) { @@ -2830,62 +2776,12 @@ void OBSBasic::RemoveSelectedSceneItem() } } -struct ReorderInfo { - int idx = 0; - OBSBasic *window; - - inline ReorderInfo(OBSBasic *window_) : window(window_) {} -}; - -void OBSBasic::ReorderSceneItem(obs_sceneitem_t *item, size_t idx) -{ - int count = ui->sources->count(); - int idx_inv = count - (int)idx - 1; - - for (int i = 0; i < count; i++) { - QListWidgetItem *listItem = ui->sources->item(i); - OBSSceneItem sceneItem = GetOBSRef(listItem); - - if (sceneItem == item) { - if ((int)idx_inv != i) { - bool sel = (ui->sources->currentRow() == i); - - listItem = TakeListItem(ui->sources, i); - if (listItem) { - ui->sources->insertItem(idx_inv, - listItem); - SetupVisibilityItem(ui->sources, - listItem, item); - - if (sel) - ui->sources->setCurrentRow( - idx_inv); - } - } - - break; - } - } -} - void OBSBasic::ReorderSources(OBSScene scene) { - ReorderInfo info(this); - if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) return; - obs_scene_enum_items(scene, - [] (obs_scene_t*, obs_sceneitem_t *item, void *p) - { - ReorderInfo *info = - reinterpret_cast(p); - - info->window->ReorderSceneItem(item, - info->idx++); - return true; - }, &info); - + ui->sources->ReorderItems(); SaveProject(); } @@ -3410,7 +3306,7 @@ void OBSBasic::ClearSceneData() ClearVolumeControls(); ClearListItems(ui->scenes); - ClearListItems(ui->sources); + ui->sources->Clear(); ClearQuickTransitions(); ui->transitions->clear(); @@ -3800,44 +3696,10 @@ void OBSBasic::MoveSceneToBottom() ui->scenes->count() - 1); } -void OBSBasic::on_sources_itemSelectionChanged() -{ - SignalBlocker sourcesSignalBlocker(ui->sources); - - auto updateItemSelection = [&]() - { - ignoreSelectionUpdate = true; - for (int i = 0; i < ui->sources->count(); i++) - { - QListWidgetItem *wItem = ui->sources->item(i); - OBSSceneItem item = GetOBSRef(wItem); - - obs_sceneitem_select(item, wItem->isSelected()); - } - ignoreSelectionUpdate = false; - }; - using updateItemSelection_t = decltype(updateItemSelection); - - obs_scene_atomic_update(GetCurrentScene(), - [](void *data, obs_scene_t *) - { - (*static_cast(data))(); - }, static_cast(&updateItemSelection)); -} - void OBSBasic::EditSceneItemName() { - QListWidgetItem *item = GetTopSelectedSourceItem(); - Qt::ItemFlags flags = item->flags(); - OBSSceneItem sceneItem= GetOBSRef(item); - obs_source_t *source = obs_sceneitem_get_source(sceneItem); - const char *name = obs_source_get_name(source); - - item->setText(QT_UTF8(name)); - item->setFlags(flags | Qt::ItemIsEditable); - ui->sources->removeItemWidget(item); - ui->sources->editItem(item); - item->setFlags(flags); + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); } void OBSBasic::SetDeinterlacingMode() @@ -3937,7 +3799,7 @@ QMenu *OBSBasic::AddScaleFilteringMenu(obs_sceneitem_t *item) return menu; } -void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) { QMenu popup(this); QPointer previewProjector; @@ -3978,6 +3840,17 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) ui->actionCopyFilters->setEnabled(false); ui->actionCopySource->setEnabled(false); + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), + ui->sources, SLOT(GroupSelectedItems())); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), + ui->sources, SLOT(UngroupSelectedGroups())); + } + popup.addSeparator(); popup.addAction(ui->actionCopySource); popup.addAction(ui->actionPasteRef); @@ -3989,11 +3862,11 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) popup.addAction(ui->actionPasteFilters); popup.addSeparator(); - if (item) { + if (idx != -1) { if (addSourceMenu) popup.addSeparator(); - OBSSceneItem sceneItem = GetSceneItem(item); + OBSSceneItem sceneItem = ui->sources->Get(idx); obs_source_t *source = obs_sceneitem_get_source(sceneItem); uint32_t flags = obs_source_get_output_flags(source); bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == @@ -4064,20 +3937,10 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview) void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) { - if (ui->scenes->count()) - CreateSourcePopupMenu(ui->sources->itemAt(pos), false); -} - -void OBSBasic::on_sources_itemDoubleClicked(QListWidgetItem *witem) -{ - if (!witem) - return; - - OBSSceneItem item = GetSceneItem(witem); - OBSSource source = obs_sceneitem_get_source(item); - - if (source) - CreatePropertiesWindow(source); + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } } void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) @@ -4161,6 +4024,12 @@ QMenu *OBSBasic::CreateAddSourcePopupMenu() addSource(popup, "scene", Str("Basic.Scene")); + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + connect(addGroup, SIGNAL(triggered(bool)), + ui->sources, SLOT(AddGroup())); + popup->addAction(addGroup); + if (!foundDeprecated) { delete deprecated; deprecated = nullptr; @@ -4171,6 +4040,7 @@ QMenu *OBSBasic::CreateAddSourcePopupMenu() popup = nullptr; } else if (foundDeprecated) { + popup->addSeparator(); popup->addMenu(deprecated); } @@ -4206,20 +4076,24 @@ void OBSBasic::on_actionAddSource_triggered() AddSourcePopupMenu(QCursor::pos()); } +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = + *reinterpret_cast*>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + void OBSBasic::on_actionRemoveSource_triggered() { vector items; - auto func = [] (obs_scene_t *, obs_sceneitem_t *item, void *param) - { - vector &items = - *reinterpret_cast*>(param); - if (obs_sceneitem_selected(item)) - items.emplace_back(item); - return true; - }; - - obs_scene_enum_items(GetCurrentScene(), func, &items); + obs_scene_enum_items(GetCurrentScene(), remove_items, &items); if (!items.size()) return; @@ -4484,26 +4358,6 @@ void OBSBasic::SceneNameEdited(QWidget *editor, UNUSED_PARAMETER(endHint); } -void OBSBasic::SceneItemNameEdited(QWidget *editor, - QAbstractItemDelegate::EndEditHint endHint) -{ - OBSSceneItem item = GetCurrentSceneItem(); - QLineEdit *edit = qobject_cast(editor); - string text = QT_TO_UTF8(edit->text().trimmed()); - - if (!item) - return; - - obs_source_t *source = obs_sceneitem_get_source(item); - RenameListItem(this, ui->sources, source, text); - - QListWidgetItem *listItem = ui->sources->currentItem(); - listItem->setText(QString()); - SetupVisibilityItem(ui->sources, listItem, item); - - UNUSED_PARAMETER(endHint); -} - void OBSBasic::OpenFilters() { OBSSceneItem item = GetCurrentSceneItem(); @@ -5150,13 +5004,11 @@ void OBSBasic::on_actionShowProfileFolder_triggered() QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } -QListWidgetItem *OBSBasic::GetTopSelectedSourceItem() +int OBSBasic::GetTopSelectedSourceItem() { - QList selectedItems = ui->sources->selectedItems(); - QListWidgetItem *topItem = nullptr; - if (selectedItems.size() != 0) - topItem = selectedItems[0]; - return topItem; + QModelIndexList selectedItems = + ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; } void OBSBasic::on_preview_customContextMenuRequested(const QPoint &pos) @@ -5613,6 +5465,22 @@ static bool nudge_callback(obs_scene_t*, obs_sceneitem_t *item, void *param) struct vec2 pos; if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + struct vec3 offset3; + vec3_set(&offset3, offset.x, offset.y, 0.0f); + + struct matrix4 matrix; + obs_sceneitem_get_draw_transform(item, &matrix); + vec4_set(&matrix.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&matrix, &matrix); + vec3_transform(&offset3, &offset3, &matrix); + + struct vec2 new_offset; + vec2_set(&new_offset, offset3.x, offset3.y); + obs_sceneitem_group_enum_items(item, nudge_callback, + &new_offset); + } + return true; } @@ -6188,12 +6056,13 @@ bool OBSBasic::sysTrayMinimizeToTray() void OBSBasic::on_actionCopySource_triggered() { - on_actionCopyTransform_triggered(); - OBSSceneItem item = GetCurrentSceneItem(); - if (!item) return; + if (!!obs_sceneitem_is_group(item)) + return; + + on_actionCopyTransform_triggered(); OBSSource source = obs_sceneitem_get_source(item); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 277d6f6e2..6abab8ae5 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -232,9 +232,6 @@ private: void UpdatePreviewScalingMenu(); - void UpdateSources(OBSScene scene); - void InsertSceneItem(obs_sceneitem_t *item); - void LoadSceneListOrder(obs_data_array_t *array); obs_data_array_t *SaveSceneListOrder(); void ChangeSceneIndex(bool relative, int idx, int invalidIdx); @@ -243,10 +240,6 @@ private: void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); - void CreateInteractionWindow(obs_source_t *source); - void CreatePropertiesWindow(obs_source_t *source); - void CreateFiltersWindow(obs_source_t *source); - void CloseDialogs(); void ClearSceneData(); @@ -275,7 +268,7 @@ private: void SaveProjectNow(); - QListWidgetItem *GetTopSelectedSourceItem(); + int GetTopSelectedSourceItem(); obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, replayBufHotkeys; @@ -550,11 +543,9 @@ public: } } - void ReorderSceneItem(obs_sceneitem_t *item, size_t idx); - QMenu *AddDeinterlacingMenu(obs_source_t *source); QMenu *AddScaleFilteringMenu(obs_sceneitem_t *item); - void CreateSourcePopupMenu(QListWidgetItem *item, bool preview); + void CreateSourcePopupMenu(int idx, bool preview); void UpdateTitleBar(); void UpdateSceneSelection(OBSSource source); @@ -564,6 +555,10 @@ public: void OpenSavedProjectors(); + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + protected: virtual void closeEvent(QCloseEvent *event) override; virtual void changeEvent(QEvent *event) override; @@ -605,9 +600,7 @@ private slots: void on_actionRemoveScene_triggered(); void on_actionSceneUp_triggered(); void on_actionSceneDown_triggered(); - void on_sources_itemSelectionChanged(); void on_sources_customContextMenuRequested(const QPoint &pos); - void on_sources_itemDoubleClicked(QListWidgetItem *item); void on_scenes_itemDoubleClicked(QListWidgetItem *item); void on_actionAddSource_triggered(); void on_actionRemoveSource_triggered(); @@ -689,8 +682,6 @@ private slots: void SceneNameEdited(QWidget *editor, QAbstractItemDelegate::EndEditHint endHint); - void SceneItemNameEdited(QWidget *editor, - QAbstractItemDelegate::EndEditHint endHint); void OpenSceneFilters(); void OpenFilters(); diff --git a/UI/window-basic-preview.cpp b/UI/window-basic-preview.cpp index 3adb56eb4..516b13b86 100644 --- a/UI/window-basic-preview.cpp +++ b/UI/window-basic-preview.cpp @@ -39,6 +39,8 @@ struct SceneFindData { OBSSceneItem item; bool selectBelow; + obs_sceneitem_t *group = nullptr; + SceneFindData(const SceneFindData &) = delete; SceneFindData(SceneFindData &&) = delete; SceneFindData& operator=(const SceneFindData &) = delete; @@ -214,11 +216,26 @@ static bool CheckItemSelected(obs_scene_t *scene, obs_sceneitem_t *item, if (!SceneItemHasVideo(item)) return true; + if (obs_sceneitem_is_group(item)) { + data->group = item; + obs_sceneitem_group_enum_items(item, CheckItemSelected, param); + data->group = nullptr; + + if (data->item) { + return false; + } + } vec3_set(&pos3, data->pos.x, data->pos.y, 0.0f); obs_sceneitem_get_box_transform(item, &transform); + if (data->group) { + matrix4 parent_transform; + obs_sceneitem_get_draw_transform(data->group, &parent_transform); + matrix4_mul(&transform, &transform, &parent_transform); + } + matrix4_inv(&transform, &transform); vec3_transform(&transformedPos, &pos3, &transform); @@ -268,10 +285,35 @@ struct HandleFindData { static bool FindHandleAtPos(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { - if (!obs_sceneitem_selected(item)) - return true; - HandleFindData *data = reinterpret_cast(param); + + if (!obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item)) { + matrix4 transform; + vec3 new_pos3; + vec3_set(&new_pos3, data->pos.x, data->pos.y, 0.0f); + vec3_divf(&new_pos3, &new_pos3, data->scale); + + obs_sceneitem_get_draw_transform(item, &transform); + matrix4_inv(&transform, &transform); + vec3_transform(&new_pos3, &new_pos3, &transform); + + vec2 new_pos; + vec2_set(&new_pos, new_pos3.x, new_pos3.y); + HandleFindData findData(new_pos, 1.0f); + findData.item = data->item; + findData.handle = data->handle; + + obs_sceneitem_group_enum_items(item, FindHandleAtPos, + &findData); + + data->item = findData.item; + data->handle = findData.handle; + } + + return true; + } + matrix4 transform; vec3 pos3; float closestHandle = HANDLE_SEL_RADIUS; @@ -377,6 +419,15 @@ void OBSBasicPreview::GetStretchHandleData(const vec2 &pos) startCrop.left - startCrop.right); cropSize.y = float(obs_source_get_height(source) - startCrop.top - startCrop.bottom); + + stretchGroup = obs_sceneitem_get_group(stretchItem); + if (stretchGroup) { + obs_sceneitem_get_draw_transform(stretchGroup, + &invGroupTransform); + matrix4_inv(&invGroupTransform, + &invGroupTransform); + obs_sceneitem_defer_group_resize_begin(stretchGroup); + } } } @@ -482,6 +533,9 @@ static bool select_one(obs_scene_t *scene, obs_sceneitem_t *item, void *param) { obs_sceneitem_t *selectedItem = reinterpret_cast(param); + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, select_one, param); + obs_sceneitem_select(item, (selectedItem == item)); UNUSED_PARAMETER(scene); @@ -534,10 +588,15 @@ void OBSBasicPreview::mouseReleaseEvent(QMouseEvent *event) if (!mouseMoved) ProcessClick(pos); - stretchItem = nullptr; - mouseDown = false; - mouseMoved = false; - cropping = false; + if (stretchGroup) { + obs_sceneitem_defer_group_resize_end(stretchGroup); + } + + stretchItem = nullptr; + stretchGroup = nullptr; + mouseDown = false; + mouseMoved = false; + cropping = false; } } @@ -692,9 +751,22 @@ static bool move_items(obs_scene_t *scene, obs_sceneitem_t *item, void *param) if (obs_sceneitem_locked(item)) return true; + bool selected = obs_sceneitem_selected(item); vec2 *offset = reinterpret_cast(param); - if (obs_sceneitem_selected(item)) { + if (obs_sceneitem_is_group(item) && !selected) { + matrix4 transform; + vec3 new_offset; + vec3_set(&new_offset, offset->x, offset->y, 0.0f); + + obs_sceneitem_get_draw_transform(item, &transform); + vec4_set(&transform.t, 0.0f, 0.0f, 0.0f, 1.0f); + matrix4_inv(&transform, &transform); + vec3_transform(&new_offset, &new_offset, &transform); + obs_sceneitem_group_enum_items(item, move_items, &new_offset); + } + + if (selected) { vec2 pos; obs_sceneitem_get_pos(item, &pos); vec2_add(&pos, &pos, offset); @@ -1063,6 +1135,17 @@ void OBSBasicPreview::mouseMoveEvent(QMouseEvent *event) pos.y = std::round(pos.y); if (stretchHandle != ItemHandle::None) { + obs_sceneitem_t *group = obs_sceneitem_get_group( + stretchItem); + if (group) { + vec3 group_pos; + vec3_set(&group_pos, pos.x, pos.y, 0.0f); + vec3_transform(&group_pos, &group_pos, + &invGroupTransform); + pos.x = group_pos.x; + pos.y = group_pos.y; + } + if (cropping) CropItem(pos); else @@ -1110,6 +1193,16 @@ bool OBSBasicPreview::DrawSelectedItem(obs_scene_t *scene, if (!SceneItemHasVideo(item)) return true; + if (obs_sceneitem_is_group(item)) { + matrix4 mat; + obs_sceneitem_get_draw_transform(item, &mat); + + gs_matrix_push(); + gs_matrix_mul(&mat); + obs_sceneitem_group_enum_items(item, DrawSelectedItem, param); + gs_matrix_pop(); + } + if (!obs_sceneitem_selected(item)) return true; diff --git a/UI/window-basic-preview.hpp b/UI/window-basic-preview.hpp index 4042281f1..c177dd9a4 100644 --- a/UI/window-basic-preview.hpp +++ b/UI/window-basic-preview.hpp @@ -35,11 +35,13 @@ private: obs_sceneitem_crop startCrop; vec2 startItemPos; vec2 cropSize; + OBSSceneItem stretchGroup; OBSSceneItem stretchItem; ItemHandle stretchHandle = ItemHandle::None; vec2 stretchItemSize; matrix4 screenToItem; matrix4 itemToScreen; + matrix4 invGroupTransform; vec2 startPos; vec2 lastMoveOffset;