0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 21:13:04 +02:00
obs-studio/UI/source-tree.cpp
gxalpha 85891630b3 UI: Dim hidden source items in source tree
Dims the items of hidden sources in the sources dock to make it more
obvious that they are not visible. Both the icon and the sources name
are affected.
2021-08-17 08:24:28 -07:00

1754 lines
43 KiB
C++

#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 "platform.hpp"
#include <obs-frontend-api.h>
#include <obs.h>
#include <string>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QAccessible>
#include <QStylePainter>
#include <QStyleOptionFocusRect>
static inline OBSScene GetCurrentScene()
{
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
return main->GetCurrentScene();
}
/* ========================================================================= */
SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_)
: tree(tree_), sceneitem(sceneitem_)
{
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
obs_source_t *source = obs_sceneitem_get_source(sceneitem);
const char *name = obs_source_get_name(source);
obs_data_t *privData = obs_sceneitem_get_private_settings(sceneitem);
int preset = obs_data_get_int(privData, "color-preset");
if (preset == 1) {
const char *color = obs_data_get_string(privData, "color");
std::string col = "background: ";
col += color;
setStyleSheet(col.c_str());
} else if (preset > 1) {
setStyleSheet("");
setProperty("bgColor", preset - 1);
} else {
setStyleSheet("background: none");
}
obs_data_release(privData);
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
const char *id = obs_source_get_id(source);
bool sourceVisible = obs_sceneitem_visible(sceneitem);
if (tree->iconsVisible) {
QIcon icon;
if (strcmp(id, "scene") == 0)
icon = main->GetSceneIcon();
else if (strcmp(id, "group") == 0)
icon = main->GetGroupIcon();
else
icon = main->GetSourceIcon(id);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
iconLabel = new QLabel();
iconLabel->setPixmap(pixmap);
iconLabel->setFixedSize(16, 16);
iconLabel->setEnabled(sourceVisible);
iconLabel->setStyleSheet("background: none");
}
vis = new VisibilityCheckBox();
vis->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
vis->setFixedSize(16, 16);
vis->setChecked(sourceVisible);
vis->setStyleSheet("background: none");
vis->setAccessibleName(QTStr("Basic.Main.Sources.Visibility"));
vis->setAccessibleDescription(
QTStr("Basic.Main.Sources.VisibilityDescription").arg(name));
lock = new LockedCheckBox();
lock->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
lock->setFixedSize(16, 16);
lock->setChecked(obs_sceneitem_locked(sceneitem));
lock->setStyleSheet("background: none");
lock->setAccessibleName(QTStr("Basic.Main.Sources.Lock"));
lock->setAccessibleDescription(
QTStr("Basic.Main.Sources.LockDescription").arg(name));
label = new QLabel(QT_UTF8(name));
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
label->setAttribute(Qt::WA_TranslucentBackground);
label->setEnabled(sourceVisible);
#ifdef __APPLE__
vis->setAttribute(Qt::WA_LayoutUsesWidgetRect);
lock->setAttribute(Qt::WA_LayoutUsesWidgetRect);
#endif
boxLayout = new QHBoxLayout();
boxLayout->setContentsMargins(0, 0, 0, 0);
if (iconLabel) {
boxLayout->addWidget(iconLabel);
boxLayout->addSpacing(2);
}
boxLayout->addWidget(label);
boxLayout->addWidget(vis);
boxLayout->addSpacing(1);
boxLayout->addWidget(lock);
#ifdef __APPLE__
/* Hack: Fixes a bug where scrollbars would be above the lock icon */
boxLayout->addSpacing(16);
#endif
Update(false);
setLayout(boxLayout);
/* --------------------------------------------------------- */
auto setItemVisible = [this](bool val) {
obs_scene_t *scene = obs_sceneitem_get_scene(sceneitem);
obs_source_t *scenesource = obs_scene_get_source(scene);
int64_t id = obs_sceneitem_get_id(sceneitem);
const char *name = obs_source_get_name(scenesource);
obs_source_t *source = obs_sceneitem_get_source(sceneitem);
auto undo_redo = [](const std::string &name, int64_t id,
bool val) {
obs_source_t *s = obs_get_source_by_name(name.c_str());
obs_scene_t *sc = obs_group_or_scene_from_source(s);
obs_sceneitem_t *si =
obs_scene_find_sceneitem_by_id(sc, id);
if (si)
obs_sceneitem_set_visible(si, val);
obs_source_release(s);
};
QString str = QTStr(val ? "Undo.ShowSceneItem"
: "Undo.HideSceneItem");
OBSBasic *main = OBSBasic::Get();
main->undo_s.add_action(
str.arg(obs_source_get_name(source), name),
std::bind(undo_redo, std::placeholders::_1, id, !val),
std::bind(undo_redo, std::placeholders::_1, id, val),
name, name);
SignalBlocker sourcesSignalBlocker(this);
obs_sceneitem_set_visible(sceneitem, val);
};
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::paintEvent(QPaintEvent *event)
{
QStyleOption opt;
opt.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
QWidget::paintEvent(event);
}
void SourceTreeItem::DisconnectSignals()
{
sceneRemoveSignal.Disconnect();
itemRemoveSignal.Disconnect();
selectSignal.Disconnect();
deselectSignal.Disconnect();
visibleSignal.Disconnect();
lockedSignal.Disconnect();
renameSignal.Disconnect();
removeSignal.Disconnect();
}
void SourceTreeItem::Clear()
{
DisconnectSignals();
sceneitem = nullptr;
}
void SourceTreeItem::ReconnectSignals()
{
if (!sceneitem)
return;
DisconnectSignals();
/* --------------------------------------------------------- */
auto removeItem = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(data);
obs_sceneitem_t *curItem =
(obs_sceneitem_t *)calldata_ptr(cd, "item");
if (curItem == this_->sceneitem) {
QMetaObject::invokeMethod(this_->tree, "Remove",
Q_ARG(OBSSceneItem, curItem));
curItem = nullptr;
}
if (!curItem)
QMetaObject::invokeMethod(this_, "Clear");
};
auto itemVisible = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(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));
};
auto itemLocked = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(data);
obs_sceneitem_t *curItem =
(obs_sceneitem_t *)calldata_ptr(cd, "item");
bool locked = calldata_bool(cd, "locked");
if (curItem == this_->sceneitem)
QMetaObject::invokeMethod(this_, "LockedChanged",
Q_ARG(bool, locked));
};
auto itemSelect = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(data);
obs_sceneitem_t *curItem =
(obs_sceneitem_t *)calldata_ptr(cd, "item");
if (curItem == this_->sceneitem)
QMetaObject::invokeMethod(this_, "Select");
};
auto itemDeselect = [](void *data, calldata_t *cd) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(data);
obs_sceneitem_t *curItem =
(obs_sceneitem_t *)calldata_ptr(cd, "item");
if (curItem == this_->sceneitem)
QMetaObject::invokeMethod(this_, "Deselect");
};
auto reorderGroup = [](void *data, calldata_t *) {
SourceTreeItem *this_ =
reinterpret_cast<SourceTreeItem *>(data);
QMetaObject::invokeMethod(this_->tree, "ReorderItems");
};
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);
lockedSignal.Connect(signal, "item_locked", itemLocked, this);
selectSignal.Connect(signal, "item_select", itemSelect, this);
deselectSignal.Connect(signal, "item_deselect", itemDeselect, this);
if (obs_sceneitem_is_group(sceneitem)) {
obs_source_t *source = obs_sceneitem_get_source(sceneitem);
signal = obs_source_get_signal_handler(source);
groupReorderSignal.Connect(signal, "reorder", reorderGroup,
this);
}
/* --------------------------------------------------------- */
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);
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<OBSBasic *>(App()->GetMainWindow());
if (source) {
main->CreatePropertiesWindow(source);
}
}
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void SourceTreeItem::enterEvent(QEnterEvent *event)
#else
void SourceTreeItem::enterEvent(QEvent *event)
#endif
{
QWidget::enterEvent(event);
OBSBasicPreview *preview = OBSBasicPreview::Get();
std::lock_guard<std::mutex> lock(preview->selectMutex);
preview->hoveredPreviewItems.clear();
preview->hoveredPreviewItems.push_back(sceneitem);
}
void SourceTreeItem::leaveEvent(QEvent *event)
{
QWidget::leaveEvent(event);
OBSBasicPreview *preview = OBSBasicPreview::Get();
std::lock_guard<std::mutex> lock(preview->selectMutex);
preview->hoveredPreviewItems.clear();
}
bool SourceTreeItem::IsEditing()
{
return editor != nullptr;
}
void SourceTreeItem::EnterEditMode()
{
setFocusPolicy(Qt::StrongFocus);
int index = boxLayout->indexOf(label);
boxLayout->removeWidget(label);
editor = new QLineEdit(label->text());
editor->setStyleSheet("background: none");
editor->selectAll();
editor->installEventFilter(this);
boxLayout->insertWidget(index, editor);
setFocusProxy(editor);
}
void SourceTreeItem::ExitEditMode(bool save)
{
ExitEditModeInternal(save);
if (tree->undoSceneData) {
OBSBasic *main = OBSBasic::Get();
main->undo_s.pop_disabled();
OBSData redoSceneData = main->BackupScene(GetCurrentScene());
QString text = QTStr("Undo.GroupItems").arg(newName.c_str());
main->CreateSceneUndoRedoAction(text, tree->undoSceneData,
redoSceneData);
tree->undoSceneData = nullptr;
}
}
void SourceTreeItem::ExitEditModeInternal(bool save)
{
if (!editor) {
return;
}
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
OBSScene scene = main->GetCurrentScene();
newName = QT_TO_UTF8(editor->text());
setFocusProxy(nullptr);
int index = boxLayout->indexOf(editor);
boxLayout->removeWidget(editor);
delete editor;
editor = nullptr;
setFocusPolicy(Qt::NoFocus);
boxLayout->insertWidget(index, label);
label->setFocus();
/* ----------------------------------------- */
/* 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 */
obs_source_t *existingSource = obs_get_source_by_name(newName.c_str());
obs_source_release(existingSource);
bool exists = !!existingSource;
if (exists) {
OBSMessageBox::information(main, QTStr("NameExists.Title"),
QTStr("NameExists.Text"));
return;
}
/* ----------------------------------------- */
/* rename */
SignalBlocker sourcesSignalBlocker(this);
std::string prevName(obs_source_get_name(source));
std::string scene_name =
obs_source_get_name(main->GetCurrentSceneSource());
auto undo = [scene_name, prevName, main](const std::string &data) {
obs_source_t *source = obs_get_source_by_name(data.c_str());
obs_source_set_name(source, prevName.c_str());
obs_source_release(source);
obs_source_t *scene_source =
obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source, true);
obs_source_release(scene_source);
};
std::string editedName = newName;
auto redo = [scene_name, main, editedName](const std::string &data) {
obs_source_t *source = obs_get_source_by_name(data.c_str());
obs_source_set_name(source, editedName.c_str());
obs_source_release(source);
obs_source_t *scene_source =
obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source, true);
obs_source_release(scene_source);
};
main->undo_s.add_action(QTStr("Undo.Rename").arg(newName.c_str()), undo,
redo, newName, prevName);
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 (LineEditCanceled(event)) {
QMetaObject::invokeMethod(this, "ExitEditMode",
Qt::QueuedConnection,
Q_ARG(bool, false));
return true;
}
if (LineEditChanged(event)) {
QMetaObject::invokeMethod(this, "ExitEditMode",
Qt::QueuedConnection,
Q_ARG(bool, true));
return true;
}
return false;
}
void SourceTreeItem::VisibilityChanged(bool visible)
{
if (iconLabel) {
iconLabel->setEnabled(visible);
}
label->setEnabled(visible);
vis->setChecked(visible);
}
void SourceTreeItem::LockedChanged(bool locked)
{
lock->setChecked(locked);
}
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);
#ifdef __APPLE__
expand->setAttribute(Qt::WA_LayoutUsesWidgetRect);
#endif
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 SourceTreeItem::Select()
{
tree->SelectItem(sceneitem, true);
OBSBasic::Get()->UpdateContextBarDeferred();
}
void SourceTreeItem::Deselect()
{
tree->SelectItem(sceneitem, false);
OBSBasic::Get()->UpdateContextBarDeferred();
}
/* ========================================================================= */
void SourceTreeModel::OBSFrontendEvent(enum obs_frontend_event event, void *ptr)
{
SourceTreeModel *stm = reinterpret_cast<SourceTreeModel *>(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<OBSSceneItem> &items =
*reinterpret_cast<QVector<OBSSceneItem> *>(ptr);
obs_source_t *src = obs_sceneitem_get_source(item);
if (obs_source_removed(src)) {
return true;
}
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<OBSSceneItem> &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<OBSSceneItem> 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)
{
if (obs_sceneitem_is_group(item)) {
SceneChanged();
} else {
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 &index, int role) const
{
if (role == Qt::AccessibleTextRole) {
OBSSceneItem item = items[index.row()];
obs_source_t *source = obs_sceneitem_get_source(item);
return QVariant(QT_UTF8(obs_source_get_name(source)));
}
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 = QTStr("Group");
int i = 2;
for (;;) {
obs_source_t *group = obs_get_source_by_name(QT_TO_UTF8(name));
obs_source_release(group);
if (!group)
break;
name = QTStr("Basic.Main.Group").arg(QString::number(i++));
}
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;
OBSBasic *main = OBSBasic::Get();
OBSScene scene = GetCurrentScene();
QString name = GetNewGroupName();
QVector<obs_sceneitem_t *> item_order;
for (int i = indices.count() - 1; i >= 0; i--) {
obs_sceneitem_t *item = items[indices[i].row()];
item_order << item;
}
st->undoSceneData = main->BackupScene(scene);
obs_sceneitem_t *item = obs_scene_insert_group(
scene, QT_TO_UTF8(name), item_order.data(), item_order.size());
if (!item) {
st->undoSceneData = nullptr;
return;
}
main->undo_s.push_disabled();
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, "NewGroupEdit", Qt::QueuedConnection,
Q_ARG(int, newIdx));
}
void SourceTreeModel::UngroupSelectedGroups(QModelIndexList &indices)
{
OBSBasic *main = OBSBasic::Get();
if (indices.count() == 0)
return;
OBSScene scene = main->GetCurrentScene();
OBSData undoData = main->BackupScene(scene);
for (int i = indices.count() - 1; i >= 0; i--) {
obs_sceneitem_t *item = items[indices[i].row()];
obs_sceneitem_group_ungroup(item);
}
SceneChanged();
OBSData redoData = main->BackupScene(scene);
main->CreateSceneUndoRedoAction(QTStr("Basic.Main.Ungroup"), undoData,
redoData);
}
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<OBSSceneItem> 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_);
setStyleSheet(QString(
"*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}"
"*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}"
"*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}"
"*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}"
"*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}"
"*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}"
"*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}"
"*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}"));
setMouseTracking(true);
UpdateNoSourcesMessage();
connect(App(), &OBSApp::StyleChanged, this,
&SourceTree::UpdateNoSourcesMessage);
connect(App(), &OBSApp::StyleChanged, this, &SourceTree::UpdateIcons);
}
void SourceTree::UpdateIcons()
{
SourceTreeModel *stm = GetStm();
stm->SceneChanged();
}
void SourceTree::SetIconsVisible(bool visible)
{
SourceTreeModel *stm = GetStm();
iconsVisible = visible;
stm->SceneChanged();
}
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;
}
OBSBasic *main = OBSBasic::Get();
OBSScene scene = GetCurrentScene();
obs_source_t *scenesource = obs_scene_get_source(scene);
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(scene, 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 ||
indicator == QAbstractItemView::OnViewport)
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;
if (row == stm->items.count())
itemBelow = nullptr;
else
itemBelow = stm->items[row];
if (hasGroups) {
if (!itemBelow ||
obs_sceneitem_get_group(scene, 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;
}
/* --------------------------------------- */
/* save undo data */
std::vector<obs_source_t *> sources;
for (int i = 0; i < indices.size(); i++) {
obs_sceneitem_t *item = items[indices[i].row()];
if (obs_sceneitem_get_scene(item) != scene)
sources.push_back(obs_scene_get_source(
obs_sceneitem_get_scene(item)));
}
if (dropGroup)
sources.push_back(obs_sceneitem_get_source(dropGroup));
OBSData undo_data = main->BackupScene(scene, &sources);
/* --------------------------------------- */
/* 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(
scene, subitem);
if (subitemGroup == item) {
QModelIndex idx =
stm->createIndex(j, 0);
indices.insert(i + 1, idx);
}
}
}
}
}
/* --------------------------------------- */
/* build persistent indices */
QList<QPersistentModelIndex> 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<struct obs_sceneitem_order_info> 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<insertCollapsed_t *>(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(scene, 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<updateScene_t *>(data))();
};
ignoreReorder = true;
obs_scene_atomic_update(scene, preUpdateScene, &updateScene);
ignoreReorder = false;
/* --------------------------------------- */
/* save redo data */
OBSData redo_data = main->BackupScene(scene, &sources);
/* --------------------------------------- */
/* add undo/redo action */
const char *scene_name = obs_source_get_name(scenesource);
QString action_name = QTStr("Undo.ReorderSources").arg(scene_name);
main->CreateSceneUndoRedoAction(action_name, undo_data, redo_data);
/* --------------------------------------- */
/* 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::mouseMoveEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
SourceTreeItem *item = qobject_cast<SourceTreeItem *>(childAt(pos));
OBSBasicPreview *preview = OBSBasicPreview::Get();
QListView::mouseMoveEvent(event);
std::lock_guard<std::mutex> lock(preview->selectMutex);
preview->hoveredPreviewItems.clear();
if (item)
preview->hoveredPreviewItems.push_back(item->sceneitem);
}
void SourceTree::leaveEvent(QEvent *event)
{
OBSBasicPreview *preview = OBSBasicPreview::Get();
QListView::leaveEvent(event);
std::lock_guard<std::mutex> lock(preview->selectMutex);
preview->hoveredPreviewItems.clear();
}
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::NewGroupEdit(int row)
{
if (!Edit(row)) {
OBSBasic *main = OBSBasic::Get();
main->undo_s.pop_disabled();
blog(LOG_WARNING, "Uh, somehow the edit didn't process, this "
"code should never be reached.\nAnd by "
"\"never be reached\", I mean that "
"theoretically, it should be\nimpossible "
"for this code to be reached. But if this "
"code is reached,\nfeel free to laugh at "
"Jim, because apparently it is, in fact, "
"actually\npossible for this code to be "
"reached. But I mean, again, theoretically\n"
"it should be impossible. So if you see "
"this in your log, just know that\nit's "
"really dumb, and depressing. But at least "
"the undo/redo action is\nstill covered, so "
"in theory things *should* be fine. But "
"it's entirely\npossible that they might "
"not be exactly. But again, yea. This "
"really\nshould not be possible.");
OBSData redoSceneData = main->BackupScene(GetCurrentScene());
QString text = QTStr("Undo.GroupItems").arg("Unknown");
main->CreateSceneUndoRedoAction(text, undoSceneData,
redoSceneData);
undoSceneData = nullptr;
}
}
bool SourceTree::Edit(int row)
{
SourceTreeModel *stm = GetStm();
if (row < 0 || row >= stm->items.count())
return false;
QModelIndex index = stm->createIndex(row, 0);
QWidget *widget = indexWidget(index);
SourceTreeItem *itemWidget = reinterpret_cast<SourceTreeItem *>(widget);
if (itemWidget->IsEditing())
return false;
itemWidget->EnterEditMode();
edit(index);
return true;
}
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::Remove(OBSSceneItem item)
{
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
GetStm()->Remove(item);
main->SaveProject();
if (!main->SavingDisabled()) {
obs_scene_t *scene = obs_sceneitem_get_scene(item);
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));
}
}
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();
}
void SourceTree::UpdateNoSourcesMessage()
{
std::string darkPath;
GetDataFilePath("themes/Dark/no_sources.svg", darkPath);
QColor color = palette().text().color();
bool lightTheme = (color.redF() < 0.5);
QString file = lightTheme ? ":res/images/no_sources.svg"
: darkPath.c_str();
iconNoSources.load(file);
QTextOption opt(Qt::AlignHCenter);
opt.setWrapMode(QTextOption::WordWrap);
textNoSources.setTextOption(opt);
textNoSources.setText(QTStr("NoSources.Label").replace("\n", "<br/>"));
textPrepared = false;
}
void SourceTree::paintEvent(QPaintEvent *event)
{
SourceTreeModel *stm = GetStm();
if (stm && !stm->items.count()) {
QPainter p(viewport());
if (!textPrepared) {
textNoSources.prepare(QTransform(), p.font());
textPrepared = true;
}
QRectF iconRect = iconNoSources.viewBoxF();
QSizeF iconSize = iconRect.size();
QSizeF textSize = textNoSources.size();
QSizeF thisSize = size();
qreal totalHeight = textSize.height() + iconSize.height();
qreal x = thisSize.width() / 2.0 - textSize.width() / 2.0;
qreal y = thisSize.height() / 2.0 - totalHeight / 2.0;
p.drawStaticText(x, y, textNoSources);
x = thisSize.width() / 2.0 - iconSize.width() / 2.0;
y += textSize.height();
iconRect.moveTo(x, y);
iconNoSources.render(&p, iconRect);
} else {
QListView::paintEvent(event);
}
}