From 1bb8c25710b74d760e4abf7c69ae3d61424fe9b1 Mon Sep 17 00:00:00 2001 From: Diogo Guerreiro Date: Wed, 22 May 2024 19:49:03 +0100 Subject: [PATCH] UI:Implement drag-and-drop from sources to scenes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description: The Drag-and-Drop feature allows users to effortlessly move sources or groups of sources into scenes within the user interface. This intuitive functionality enhances user interaction by providing a visual and interactive way to organize and customize content. The feature mimics the behaviour of a copy and paste. For example if we drag a "Input Audio Device" from Scene 1 to Scene 2, after the event, both Scene 1 and Scene 2 will have the "Input Audio Device" source. Co-authored-by: André Bento --- UI/scene-tree.cpp | 98 +++++++++++++++++++-- UI/scene-tree.hpp | 8 +- UI/source-tree.cpp | 206 ++++++++++++++++++++++++++++++++++++++++++--- UI/source-tree.hpp | 38 ++++++--- 4 files changed, 319 insertions(+), 31 deletions(-) diff --git a/UI/scene-tree.cpp b/UI/scene-tree.cpp index f5386e1e6..2b2a16400 100644 --- a/UI/scene-tree.cpp +++ b/UI/scene-tree.cpp @@ -1,16 +1,25 @@ +#include "obs-frontend-api.h" +#include "obs-source.h" +#include "obs.h" #include "scene-tree.hpp" +#include -#include -#include #include +#include #include +#include +#include #include #include + +OBS_DECLARE_MODULE() + SceneTree::SceneTree(QWidget *parent_) : QListWidget(parent_) { + setAcceptDrops(true); installEventFilter(this); - setDragDropMode(InternalMove); + setDragDropMode(DragDrop); setMovement(QListView::Snap); } @@ -104,10 +113,32 @@ void SceneTree::startDrag(Qt::DropActions supportedActions) void SceneTree::dropEvent(QDropEvent *event) { - if (event->source() != this) { - QListWidget::dropEvent(event); - return; - } + if (event->mimeData()->hasFormat("application/indexes")) { + QByteArray encodedData = event->mimeData()->data( + "application/indexes"); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + + QStringList sourceNames; + stream >> sourceNames; + + QPoint dropPos = event->position().toPoint(); + QListWidgetItem *sceneItem = itemAt(dropPos); + + if (sceneItem) { + QString sceneName = sceneItem->text(); + + for (const QString &sourceName : sourceNames) { + if (!sceneName.isEmpty() && + !sourceName.isEmpty()) { + + addSourceToScene(sourceName, sceneName); + } + } + } + + event->setDropAction(Qt::MoveAction); + event->accept(); + } if (gridMode) { int scrollWid = verticalScrollBar()->sizeHint().width(); @@ -148,6 +179,34 @@ void SceneTree::dropEvent(QDropEvent *event) QTimer::singleShot(100, [this]() { emit scenesReordered(); }); } +void SceneTree::addSourceToScene(const QString &sourceName, + const QString &sceneName) +{ + obs_source_t *source = obs_get_source_by_name(qPrintable(sourceName)); + if (!source) { + return; + } + obs_source_t *sceneSource = + obs_get_source_by_name(qPrintable(sceneName)); + if (!sceneSource) { + obs_source_release(source); + return; + } + + obs_scene_t *scene = obs_scene_from_source(sceneSource); + if (!scene) { + obs_source_release(source); + obs_source_release(sceneSource); + return; + } + + obs_sceneitem_t *sceneItem = obs_scene_add(scene, source); + + obs_source_release(source); + obs_source_release(sceneSource); +} + + void SceneTree::RepositionGrid(QDragMoveEvent *event) { int scrollWid = verticalScrollBar()->sizeHint().width(); @@ -212,8 +271,33 @@ void SceneTree::RepositionGrid(QDragMoveEvent *event) } } +void SceneTree::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasFormat("application/indexes")) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } + + if (event->source() == this) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } + + QListWidget::dragEnterEvent(event); +} + void SceneTree::dragMoveEvent(QDragMoveEvent *event) { + if (event->mimeData()->hasFormat("application/indexes")) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } + + if (event->source() == this) { + event->setDropAction(Qt::MoveAction); + event->accept(); + } + if (gridMode) { RepositionGrid(event); } diff --git a/UI/scene-tree.hpp b/UI/scene-tree.hpp index 0950f12ea..ff0c104bb 100644 --- a/UI/scene-tree.hpp +++ b/UI/scene-tree.hpp @@ -1,8 +1,8 @@ #pragma once -#include #include #include +#include class SceneTree : public QListWidget { Q_OBJECT @@ -26,6 +26,11 @@ public: explicit SceneTree(QWidget *parent = nullptr); + QString GetSceneNameFromDrop(QDropEvent *event); + void handleDropEvent(const QString &sceneName); + void addSourceToScene(const QString &sourceName, + const QString &sceneName); + private: void RepositionGrid(QDragMoveEvent *event = nullptr); @@ -36,6 +41,7 @@ protected: virtual void dropEvent(QDropEvent *event) override; virtual void dragMoveEvent(QDragMoveEvent *event) override; virtual void dragLeaveEvent(QDragLeaveEvent *event) override; + virtual void dragEnterEvent(QDragEnterEvent *event) override; virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; #if QT_VERSION < QT_VERSION_CHECK(6, 4, 3) diff --git a/UI/source-tree.cpp b/UI/source-tree.cpp index 14fb89dea..a67e62b2a 100644 --- a/UI/source-tree.cpp +++ b/UI/source-tree.cpp @@ -1,27 +1,30 @@ -#include "window-basic-main.hpp" #include "obs-app.hpp" -#include "source-tree.hpp" -#include "qt-wrappers.hpp" #include "platform.hpp" -#include "source-label.hpp" +#include "qt-wrappers.hpp" +#include "source-tree.hpp" +#include "window-basic-main.hpp" #include #include +#include #include +#include +#include #include #include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include #include #include + static inline OBSScene GetCurrentScene() { OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); @@ -97,7 +100,7 @@ SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) lock->setAccessibleDescription( QTStr("Basic.Main.Sources.LockDescription").arg(name)); - label = new OBSSourceLabel(source); + label = new QLabel(QT_UTF8(name)); label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); label->setAttribute(Qt::WA_TranslucentBackground); @@ -289,6 +292,15 @@ void SourceTreeItem::ReconnectSignals() /* --------------------------------------------------------- */ + 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); @@ -299,6 +311,7 @@ void SourceTreeItem::ReconnectSignals() obs_source_t *source = obs_sceneitem_get_source(sceneitem); signal = obs_source_get_signal_handler(source); + sigs.emplace_back(signal, "rename", renamed, this); sigs.emplace_back(signal, "remove", removeSource, this); } @@ -461,6 +474,7 @@ void SourceTreeItem::ExitEditModeInternal(bool save) redo, uuid, uuid); obs_source_set_name(source, newName.c_str()); + label->setText(QT_UTF8(newName.c_str())); } bool SourceTreeItem::eventFilter(QObject *object, QEvent *event) @@ -499,6 +513,11 @@ void SourceTreeItem::LockedChanged(bool locked) OBSBasic::Get()->UpdateEditMenu(); } +void SourceTreeItem::Renamed(const QString &name) +{ + label->setText(name); +} + void SourceTreeItem::Update(bool force) { OBSScene scene = GetCurrentScene(); @@ -1049,6 +1068,8 @@ void SourceTreeModel::UpdateGroupState(bool update) SourceTree::SourceTree(QWidget *parent_) : QListView(parent_) { + setSelectionMode(QAbstractItemView::ExtendedSelection); + setDragEnabled(true); SourceTreeModel *stm_ = new SourceTreeModel(this); setModel(stm_); setStyleSheet(QString( @@ -1075,6 +1096,119 @@ void SourceTree::UpdateIcons() stm->SceneChanged(); } +void SourceTree::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasFormat("application/indexes")) { + event->acceptProposedAction(); + } else { + event->ignore(); + } +} + +void SourceTree::dragMoveEvent(QDragMoveEvent *event) +{ + if (event->mimeData()->hasFormat("application/indexes")) { + event->acceptProposedAction(); + } else { + event->ignore(); + } +} + +void SourceTree::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + dragStartPosition = event->pos(); + } + QListView::mousePressEvent(event); +} + +void SourceTree::startDrag(Qt::DropActions supportedActions) +{ + obs_source_t *current_scene_source = obs_frontend_get_current_scene(); + if (!current_scene_source) { + return; + } + + obs_scene_t *current_scene = + obs_scene_from_source(current_scene_source); + if (!current_scene) { + obs_source_release(current_scene_source); + return; + } + + QStringList selectedSourceNames; + obs_scene_enum_items(current_scene, get_selected_source_names, + &selectedSourceNames); + obs_source_release(current_scene_source); + + if (selectedSourceNames.isEmpty()) { + return; + } + + QByteArray itemData; + QDataStream dataStream(&itemData, QIODevice::WriteOnly); + dataStream << selectedSourceNames; + QMimeData *mimeData = new QMimeData; + mimeData->setData("application/indexes", itemData); + + QDrag *drag = new QDrag(this); + drag->setMimeData(mimeData); + + QRect boundingRect; + QModelIndexList indexes = selectedIndexes(); + for (const QModelIndex &index : indexes) { + QRect rect = visualRect(index); + boundingRect = boundingRect.isNull() + ? rect + : boundingRect.united(rect); + } + + QPixmap pixmap(boundingRect.size()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + + for (const QModelIndex &index : indexes) { + QRect rect = visualRect(index); + painter.save(); + painter.translate(rect.topLeft() - boundingRect.topLeft()); + itemDelegate()->paint(&painter, QStyleOptionViewItem(), index); + painter.restore(); + } + painter.end(); + + QPixmap blueBox(pixmap.size()); + blueBox.fill(QColor(0, 0, 153, 127)); + QPainter boxPainter(&blueBox); + boxPainter.drawPixmap(0, 0, pixmap); + boxPainter.end(); + + drag->setPixmap(blueBox); + drag->setHotSpot(dragStartPosition - boundingRect.topLeft()); + + Qt::DropAction dropAction = drag->exec(supportedActions); +} + + +bool SourceTree::get_selected_source_names(obs_scene_t *scene, + obs_sceneitem_t *sceneitem, + void *param) +{ + QStringList *list = static_cast(param); + if (obs_sceneitem_selected(sceneitem)) { + obs_source_t *source = obs_sceneitem_get_source(sceneitem); + if (!source) { + return true; + } + const char *name = obs_source_get_name(source); + if (!name) { + return true; + } + list->append(QString::fromUtf8(name)); + } + return true; +} + void SourceTree::SetIconsVisible(bool visible) { SourceTreeModel *stm = GetStm(); @@ -1613,16 +1747,64 @@ void SourceTree::Remove(OBSSceneItem item, OBSScene scene) GetStm()->Remove(item); main->SaveProject(); + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + if (!main->SavingDisabled()) { - obs_source_t *sceneSource = obs_scene_get_source(scene); - obs_source_t *itemSource = obs_sceneitem_get_source(item); blog(LOG_INFO, "User Removed source '%s' (%s) from scene '%s'", obs_source_get_name(itemSource), obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); } + + QString sourceName = QString::fromUtf8(obs_source_get_name(itemSource)); + QString sceneName = QString::fromUtf8(obs_source_get_name(sceneSource)); + + removeSourceFromScene(sourceName, sceneName); + } +void SourceTree::removeSourceFromScene(const QString &sourceName, + const QString &sceneName) +{ + if (sourceName.isEmpty()) { + return; + } + + if (sceneName.isEmpty()) { + return; + } + + obs_source_t *source = obs_get_source_by_name(qPrintable(sourceName)); + if (!source) { + return; + } + + obs_source_t *sceneSource = + obs_get_source_by_name(qPrintable(sceneName)); + if (!sceneSource) { + obs_source_release(source); + return; + } + + obs_scene_t *scene = obs_scene_from_source(sceneSource); + if (!scene) { + obs_source_release(source); + obs_source_release(sceneSource); + return; + } + + obs_sceneitem_t *sceneItem = + obs_scene_find_source(scene, qPrintable(sourceName)); + if (sceneItem) { + obs_sceneitem_remove(sceneItem); + } + + obs_source_release(source); + obs_source_release(sceneSource); +} + + void SourceTree::GroupSelectedItems() { QModelIndexList indices = selectedIndexes(); diff --git a/UI/source-tree.hpp b/UI/source-tree.hpp index cdf2e205b..dbb57516e 100644 --- a/UI/source-tree.hpp +++ b/UI/source-tree.hpp @@ -1,19 +1,20 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include class QLabel; -class OBSSourceLabel; class QCheckBox; class QLineEdit; class SourceTree; @@ -58,7 +59,7 @@ private: QCheckBox *vis = nullptr; QCheckBox *lock = nullptr; QHBoxLayout *boxLayout = nullptr; - OBSSourceLabel *label = nullptr; + QLabel *label = nullptr; QLineEdit *editor = nullptr; @@ -80,6 +81,7 @@ private slots: void VisibilityChanged(bool visible); void LockedChanged(bool locked); + void Renamed(const QString &name); void ExpandClicked(bool checked); @@ -172,6 +174,13 @@ public: void SelectItem(obs_sceneitem_t *sceneitem, bool select); + static bool get_selected_source_names(obs_scene_t *scene, + obs_sceneitem_t *sceneitem, + void *param); + void removeSourceFromScene(const QString &sourceName, + const QString &sceneName); + + bool MultipleBaseSelected() const; bool GroupsSelected() const; bool GroupedItemsSelected() const; @@ -193,10 +202,17 @@ protected: virtual void mouseDoubleClickEvent(QMouseEvent *event) override; virtual void dropEvent(QDropEvent *event) override; virtual void paintEvent(QPaintEvent *event) override; + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void startDrag(Qt::DropActions supportedActions) override; virtual void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; + virtual void dragMoveEvent(QDragMoveEvent *event) override; + virtual void dragEnterEvent(QDragEnterEvent *event) override; + +private: + QPoint dragStartPosition; }; class SourceTreeDelegate : public QStyledItemDelegate {