diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt
index 0780cf554..ae007eac0 100644
--- a/UI/CMakeLists.txt
+++ b/UI/CMakeLists.txt
@@ -236,6 +236,7 @@ set(obs_SOURCES
window-remux.cpp
auth-base.cpp
source-tree.cpp
+ scene-tree.cpp
properties-view.cpp
focus-list.cpp
menu-button.cpp
@@ -288,6 +289,7 @@ set(obs_HEADERS
window-remux.hpp
auth-base.hpp
source-tree.hpp
+ scene-tree.hpp
properties-view.hpp
properties-view.moc.hpp
display-helpers.hpp
diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index a0afca4cd..7daf8f420 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -530,6 +530,8 @@ Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)"
Basic.Main.Group="Group %1"
Basic.Main.GroupItems="Group Selected Items"
Basic.Main.Ungroup="Ungroup"
+Basic.Main.GridMode="Grid Mode"
+Basic.Main.ListMode="List Mode"
# basic mode file menu
Basic.MainMenu.File="&File"
diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss
index 4fa146dff..ffd972b3c 100644
--- a/UI/data/themes/Acri.qss
+++ b/UI/data/themes/Acri.qss
@@ -1022,3 +1022,25 @@ OBSBasic {
qproperty-sceneIcon: url(./Dark/sources/scene.svg);
qproperty-defaultIcon: url(./Dark/sources/default.svg);
}
+
+/* Scene Tree */
+
+SceneTree#scenes {
+ qproperty-gridItemWidth: 180;
+ qproperty-gridItemHeight: 35;
+}
+
+*[gridMode="true"] SceneTree#scenes {
+ border-bottom: none;
+}
+
+*[gridMode="false"] SceneTree#scenes {
+ border-bottom: 2px solid #2f2f2f;
+}
+
+*[gridMode="true"] SceneTree::item {
+ padding: 4px;
+ padding-left: 10px;
+ padding-right: 10px;
+ margin: 0px;
+}
diff --git a/UI/data/themes/Dark.qss b/UI/data/themes/Dark.qss
index 383753025..94819ae70 100644
--- a/UI/data/themes/Dark.qss
+++ b/UI/data/themes/Dark.qss
@@ -12,6 +12,7 @@
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
+/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see . */
/******************************************************************************/
@@ -774,3 +775,37 @@ OBSBasic {
qproperty-sceneIcon: url(./Dark/sources/scene.svg);
qproperty-defaultIcon: url(./Dark/sources/default.svg);
}
+
+/* Scene Tree */
+
+SceneTree {
+ qproperty-gridItemWidth: 150;
+ qproperty-gridItemHeight: 27;
+}
+
+*[gridMode="true"] SceneTree::item {
+ color: rgb(225,224,225); /* veryLight */
+ background-color: rgb(76,76,76);
+ border: none;
+ border-radius: 3px;
+ padding: 4px;
+ padding-left: 10px;
+ padding-right: 10px;
+ margin: 1px;
+}
+
+*[gridMode="true"] SceneTree::item:selected {
+ background-color: rgb(122,121,122); /* light */
+}
+
+*[gridMode="true"] SceneTree::item:hover {
+ background-color: rgb(122,121,122); /* light */
+}
+
+*[gridMode="true"] SceneTree::item:pressed {
+ background-color: rgb(31,30,31); /* veryDark */
+}
+
+*[gridMode="true"] SceneTree::item:checked {
+ background-color: rgb(122,121,122); /* light */
+}
diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss
index 21f6870d3..2aa304f9b 100644
--- a/UI/data/themes/Rachni.qss
+++ b/UI/data/themes/Rachni.qss
@@ -1365,3 +1365,10 @@ OBSBasic {
qproperty-sceneIcon: url(./Dark/sources/scene.svg);
qproperty-defaultIcon: url(./Dark/sources/default.svg);
}
+
+/* Scene Tree */
+
+SceneTree#scenes {
+ qproperty-gridItemWidth: 150;
+ qproperty-gridItemHeight: 30;
+}
diff --git a/UI/data/themes/System.qss b/UI/data/themes/System.qss
index 3fe69c885..572946219 100644
--- a/UI/data/themes/System.qss
+++ b/UI/data/themes/System.qss
@@ -233,3 +233,10 @@ QListWidget::item,
SourceTree::item {
height: 24px;
}
+
+/* Scene Tree */
+
+SceneTree {
+ qproperty-gridItemWidth: 150;
+ qproperty-gridItemHeight: 24;
+}
diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui
index 114b7aba5..e6ea7ed62 100644
--- a/UI/forms/OBSBasic.ui
+++ b/UI/forms/OBSBasic.ui
@@ -474,7 +474,7 @@
0
-
-
+
0
@@ -1818,6 +1818,11 @@
QListView
+
+ SceneTree
+ QListWidget
+
+
OBSDock
QDockWidget
diff --git a/UI/scene-tree.cpp b/UI/scene-tree.cpp
new file mode 100644
index 000000000..120eab674
--- /dev/null
+++ b/UI/scene-tree.cpp
@@ -0,0 +1,188 @@
+#include "obs.hpp"
+#include "scene-tree.hpp"
+#include "obs-app.hpp"
+
+#include
+#include
+#include
+#include
+
+SceneTree::SceneTree(QWidget *parent_) : QListWidget(parent_)
+{
+ installEventFilter(this);
+ setDragDropMode(InternalMove);
+ setMovement(QListView::Snap);
+}
+
+void SceneTree::SetGridMode(bool grid)
+{
+ config_set_bool(App()->GlobalConfig(), "BasicWindow", "gridMode", grid);
+ parent()->setProperty("gridMode", grid);
+ gridMode = grid;
+
+ if (gridMode) {
+ setResizeMode(QListView::Adjust);
+ setViewMode(QListView::IconMode);
+ setUniformItemSizes(true);
+ setStyleSheet("*{padding: 0; margin: 0;}");
+ } else {
+ setViewMode(QListView::ListMode);
+ setResizeMode(QListView::Fixed);
+ setStyleSheet("");
+ }
+
+ resizeEvent(new QResizeEvent(size(), size()));
+}
+
+bool SceneTree::GetGridMode()
+{
+ return gridMode;
+}
+
+void SceneTree::SetGridItemWidth(int width)
+{
+ maxWidth = width;
+}
+
+void SceneTree::SetGridItemHeight(int height)
+{
+ itemHeight = height;
+}
+
+int SceneTree::GetGridItemWidth()
+{
+ return maxWidth;
+}
+
+int SceneTree::GetGridItemHeight()
+{
+ return itemHeight;
+}
+
+bool SceneTree::eventFilter(QObject *obj, QEvent *event)
+{
+ return QObject::eventFilter(obj, event);
+}
+
+void SceneTree::resizeEvent(QResizeEvent *event)
+{
+ QListWidget::resizeEvent(event);
+
+ if (gridMode) {
+ int scrollWid = verticalScrollBar()->sizeHint().width();
+ int h = visualItemRect(item(count() - 1)).bottom();
+
+ if (h < height()) {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ scrollWid = 0;
+ } else {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ }
+
+ int wid = contentsRect().width() - scrollWid - 1;
+ int items = (int)ceil((float)wid / maxWidth);
+ int itemWidth = wid / items;
+
+ setGridSize(QSize(itemWidth, itemHeight));
+
+ for (int i = 0; i < count(); i++) {
+ item(i)->setSizeHint(QSize(itemWidth, itemHeight));
+ }
+ } else {
+ setGridSize(QSize());
+ setSpacing(0);
+ for (int i = 0; i < count(); i++) {
+ item(i)->setData(Qt::SizeHintRole, QVariant());
+ }
+ }
+}
+
+void SceneTree::startDrag(Qt::DropActions supportedActions)
+{
+ QListWidget::startDrag(supportedActions);
+}
+
+void SceneTree::dropEvent(QDropEvent *event)
+{
+ QListWidget::dropEvent(event);
+ if (event->source() == this && gridMode) {
+ int scrollWid = verticalScrollBar()->sizeHint().width();
+ int h = visualItemRect(item(count() - 1)).bottom();
+
+ if (h < height()) {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ scrollWid = 0;
+ } else {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ }
+
+ float wid = contentsRect().width() - scrollWid - 1;
+
+ QPoint point = event->pos();
+
+ int x = (float)point.x() / wid * ceil(wid / maxWidth);
+ int y = point.y() / itemHeight;
+
+ int r = x + y * ceil(wid / maxWidth);
+
+ QListWidgetItem *item = takeItem(selectedIndexes()[0].row());
+ insertItem(r, item);
+ setCurrentItem(item);
+ resize(size());
+ }
+}
+
+void SceneTree::dragMoveEvent(QDragMoveEvent *event)
+{
+ if (gridMode) {
+ int scrollWid = verticalScrollBar()->sizeHint().width();
+ int h = visualItemRect(item(count() - 1)).bottom();
+
+ if (h < height()) {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ scrollWid = 0;
+ } else {
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ }
+
+ float wid = contentsRect().width() - scrollWid - 1;
+
+ QPoint point = event->pos();
+
+ int x = (float)point.x() / wid * ceil(wid / maxWidth);
+ int y = point.y() / itemHeight;
+
+ int r = x + y * ceil(wid / maxWidth);
+ int orig = selectedIndexes()[0].row();
+
+ for (int i = 0; i < count(); i++) {
+ auto *wItem = item(i);
+
+ if (wItem->isSelected())
+ continue;
+
+ QModelIndex index = indexFromItem(wItem);
+
+ int off = (i >= r ? 1 : 0) -
+ (i > orig && i > r ? 1 : 0) -
+ (i > orig && i == r ? 2 : 0);
+
+ int xPos = (i + off) % (int)ceil(wid / maxWidth);
+ int yPos = (i + off) / (int)ceil(wid / maxWidth);
+ QSize g = gridSize();
+
+ QPoint position(xPos * g.width(), yPos * g.height());
+ setPositionForIndex(position, index);
+ }
+ } else {
+ QListWidget::dragMoveEvent(event);
+ }
+}
+
+void SceneTree::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ QListWidget::rowsInserted(parent, start, end);
+
+ QResizeEvent *event = new QResizeEvent(size(), size());
+ SceneTree::resizeEvent(event);
+}
diff --git a/UI/scene-tree.hpp b/UI/scene-tree.hpp
new file mode 100644
index 000000000..4b04dda54
--- /dev/null
+++ b/UI/scene-tree.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+#include
+#include
+
+class SceneTree : public QListWidget {
+ Q_OBJECT
+ Q_PROPERTY(int gridItemWidth READ GetGridItemWidth WRITE
+ SetGridItemWidth DESIGNABLE true)
+ Q_PROPERTY(int gridItemHeight READ GetGridItemHeight WRITE
+ SetGridItemHeight DESIGNABLE true)
+
+ bool gridMode = false;
+ int maxWidth = 150;
+ int itemHeight = 24;
+
+public:
+ void SetGridMode(bool grid);
+ bool GetGridMode();
+
+ void SetGridItemWidth(int width);
+ void SetGridItemHeight(int height);
+ int GetGridItemWidth();
+ int GetGridItemHeight();
+
+ explicit SceneTree(QWidget *parent = nullptr);
+
+protected:
+ virtual bool eventFilter(QObject *obj, QEvent *event) override;
+ virtual void resizeEvent(QResizeEvent *event) override;
+ virtual void startDrag(Qt::DropActions supportedActions) override;
+ virtual void dropEvent(QDropEvent *event) override;
+ virtual void dragMoveEvent(QDragMoveEvent *event) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start,
+ int end) override;
+};
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index f5972b559..51a0d539b 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
#include
#include
@@ -94,7 +95,6 @@ template struct SignalContainer {
OBSRef ref;
vector> handlers;
};
-
}
extern volatile long insideEventLoop;
@@ -105,6 +105,18 @@ Q_DECLARE_METATYPE(OBSSource);
Q_DECLARE_METATYPE(obs_order_movement);
Q_DECLARE_METATYPE(SignalContainer);
+QDataStream &operator<<(QDataStream &out, const SignalContainer &v)
+{
+ out << v.ref;
+ return out;
+}
+
+QDataStream &operator>>(QDataStream &in, SignalContainer &v)
+{
+ in >> v.ref;
+ return in;
+}
+
template static T GetOBSRef(QListWidgetItem *item)
{
return item->data(static_cast(QtDataRole::OBSRef)).value();
@@ -195,6 +207,9 @@ extern void RegisterRestreamAuth();
OBSBasic::OBSBasic(QWidget *parent)
: OBSMainWindow(parent), ui(new Ui::OBSBasic)
{
+ qRegisterMetaTypeStreamOperators>(
+ "SignalContainer");
+
setAttribute(Qt::WA_NativeWindow);
#if TWITCH_ENABLED
@@ -252,6 +267,10 @@ OBSBasic::OBSBasic(QWidget *parent)
ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false);
ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false);
+ bool sceneGrid = config_get_bool(App()->GlobalConfig(), "BasicWindow",
+ "gridMode");
+ ui->scenes->SetGridMode(sceneGrid);
+
ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes));
auto displayResize = [this]() {
@@ -4051,6 +4070,7 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
QMenu popup(this);
QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this);
+
popup.addAction(QTStr("Add"), this,
SLOT(on_actionAddScene_triggered()));
@@ -4132,9 +4152,26 @@ void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos)
std::bind(showInMultiview, data));
}
+ popup.addSeparator();
+
+ bool grid = ui->scenes->GetGridMode();
+
+ QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode")
+ : QTStr("Basic.Main.GridMode"),
+ this);
+ connect(gridAction, SIGNAL(triggered()), this,
+ SLOT(on_actionGridMode_triggered()));
+ popup.addAction(gridAction);
+
popup.exec(QCursor::pos());
}
+void OBSBasic::on_actionGridMode_triggered()
+{
+ bool gridMode = !ui->scenes->GetGridMode();
+ ui->scenes->SetGridMode(gridMode);
+}
+
void OBSBasic::on_actionAddScene_triggered()
{
string name;
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index a92da91d3..83e0ca08c 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -816,6 +816,7 @@ private slots:
void on_scenes_currentItemChanged(QListWidgetItem *current,
QListWidgetItem *prev);
void on_scenes_customContextMenuRequested(const QPoint &pos);
+ void on_actionGridMode_triggered();
void on_actionAddScene_triggered();
void on_actionRemoveScene_triggered();
void on_actionSceneUp_triggered();