mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-07-04 10:33:30 +00:00
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:
parent
e79fea301d
commit
1bb8c25710
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue