UI:Implement drag-and-drop from sources to scenes

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 <andre.veloso.bento@tecnico.ulisboa.pt>
This commit is contained in:
Diogo Guerreiro 2024-05-22 19:49:03 +01:00
parent e79fea301d
commit 1bb8c25710
4 changed files with 319 additions and 31 deletions

View file

@ -1,16 +1,25 @@
#include "obs-frontend-api.h"
#include "obs-source.h"
#include "obs.h"
#include "scene-tree.hpp"
#include <obs-module.h>
#include <QSizePolicy>
#include <QScrollBar>
#include <QDropEvent>
#include <QMimeData>
#include <QPushButton>
#include <QScrollBar>
#include <QSizePolicy>
#include <QTimer>
#include <cmath>
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);
}

View file

@ -1,8 +1,8 @@
#pragma once
#include <QListWidget>
#include <QEvent>
#include <QItemDelegate>
#include <QListWidget>
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)

View file

@ -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 <obs-frontend-api.h>
#include <obs.h>
#include <obs.hpp>
#include <string>
#include <QAccessible>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QAccessible>
#include <QPushButton>
#include <QSpacerItem>
#include <QVBoxLayout>
#include <QDrag>
#include <QMimeData>
#include <QStylePainter>
#include <QStyleOptionFocusRect>
static inline OBSScene GetCurrentScene()
{
OBSBasic *main = reinterpret_cast<OBSBasic *>(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<SourceTreeItem *>(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<SourceTreeItem *>(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<QStringList *>(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();

View file

@ -1,19 +1,20 @@
#pragma once
#include <QList>
#include <QVector>
#include <QPointer>
#include <QListView>
#include <QCheckBox>
#include <QStaticText>
#include <QSvgRenderer>
#include <QAbstractListModel>
#include <QStyledItemDelegate>
#include <obs.hpp>
#include <obs-frontend-api.h>
#include <obs.h>
#include <QAbstractListModel>
#include <QCheckBox>
#include <QList>
#include <QListView>
#include <QPointer>
#include <QStaticText>
#include <QStyledItemDelegate>
#include <QSvgRenderer>
#include <QVector>
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 {