frontend: Replace add source dropdown with dialog

Co-Authored-By: Lain <134130700+Lain-B@users.noreply.github.com>
This commit is contained in:
Warchamp7
2026-01-14 14:49:38 -05:00
committed by Ryan Foster
co-authored by Lain
parent 4cfcfc8c35
commit 5fc2476913
26 changed files with 2748 additions and 545 deletions
+6
View File
@@ -36,6 +36,10 @@ target_sources(
components/DisplayCaptureToolbar.cpp
components/DisplayCaptureToolbar.hpp
components/EditWidget.hpp
components/FlowFrame.cpp
components/FlowFrame.hpp
components/FlowLayout.cpp
components/FlowLayout.hpp
components/FocusList.cpp
components/FocusList.hpp
components/GameCaptureToolbar.cpp
@@ -65,6 +69,8 @@ target_sources(
components/SceneTree.hpp
components/SilentUpdateCheckBox.hpp
components/SilentUpdateSpinBox.hpp
components/SourceSelectButton.cpp
components/SourceSelectButton.hpp
components/SourceToolbar.cpp
components/SourceToolbar.hpp
components/SourceTree.cpp
+3
View File
@@ -49,6 +49,7 @@ target_sources(
utility/RemuxQueueModel.hpp
utility/RemuxWorker.cpp
utility/RemuxWorker.hpp
utility/ResizeSignaler.hpp
utility/SceneRenameDelegate.cpp
utility/SceneRenameDelegate.hpp
utility/ScreenshotObj.cpp
@@ -58,6 +59,8 @@ target_sources(
utility/SimpleOutput.hpp
utility/StartMultiTrackVideoStreamingGuard.hpp
utility/SurfaceEventFilter.hpp
utility/ThumbnailManager.cpp
utility/ThumbnailManager.hpp
utility/VCamConfig.hpp
utility/audio-encoders.cpp
utility/audio-encoders.hpp
+134
View File
@@ -0,0 +1,134 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include <components/FlowFrame.hpp>
#include <QKeyEvent>
#include <QScrollArea>
FlowFrame::FlowFrame(QWidget *parent) : QFrame(parent)
{
layout = new FlowLayout(this);
setLayout(layout);
}
void FlowFrame::keyPressEvent(QKeyEvent *event)
{
QWidget *focused = focusWidget();
if (!focused) {
return;
}
int index = -1;
for (int i = 0; i < layout->count(); ++i) {
if (!layout->itemAt(i)->widget()) {
continue;
}
auto focusProxy = layout->itemAt(i)->widget()->focusProxy();
if (layout->itemAt(i)->widget() == focused || focusProxy == focused) {
if (focusProxy == focused) {
focused = layout->itemAt(i)->widget();
}
index = i;
break;
}
}
if (index == -1) {
return;
}
const QRect focusedRect = focused->geometry();
QWidget *nextFocus = nullptr;
switch (event->key()) {
case Qt::Key_Right:
case Qt::Key_Down:
case Qt::Key_Left:
case Qt::Key_Up: {
// Find next widget in the given direction
int bestDistance = INT_MAX;
for (int i = 0; i < layout->count(); ++i) {
if (i == index) {
continue;
}
QWidget *widget = layout->itemAt(i)->widget();
const QRect rect = widget->geometry();
bool isCandidate = false;
int distance = INT_MAX;
switch (event->key()) {
case Qt::Key_Right:
if (rect.left() > focusedRect.right()) {
distance = (rect.left() - focusedRect.right()) +
qAbs(rect.center().y() - focusedRect.center().y());
isCandidate = true;
}
break;
case Qt::Key_Left:
if (rect.right() < focusedRect.left()) {
distance = (focusedRect.left() - rect.right()) +
qAbs(rect.center().y() - focusedRect.center().y());
isCandidate = true;
}
break;
case Qt::Key_Down:
if (rect.top() > focusedRect.bottom()) {
distance = (rect.top() - focusedRect.bottom()) +
qAbs(rect.center().x() - focusedRect.center().x());
isCandidate = true;
}
break;
case Qt::Key_Up:
if (rect.bottom() < focusedRect.top()) {
distance = (focusedRect.top() - rect.bottom()) +
qAbs(rect.center().x() - focusedRect.center().x());
isCandidate = true;
}
break;
}
if (isCandidate && distance < bestDistance) {
bestDistance = distance;
nextFocus = widget;
}
}
break;
}
default:
QWidget::keyPressEvent(event);
return;
}
if (nextFocus) {
nextFocus->setFocus();
QWidget *scrollParent = nextFocus->parentWidget();
while (scrollParent) {
QScrollArea *scrollArea = qobject_cast<QScrollArea *>(scrollParent);
if (scrollArea) {
scrollArea->ensureWidgetVisible(nextFocus, 20, 20);
break;
}
scrollParent = scrollParent->parentWidget();
}
}
}
+37
View File
@@ -0,0 +1,37 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include "FlowLayout.hpp"
#include <QFrame>
class FlowFrame : public QFrame {
Q_OBJECT
public:
explicit FlowFrame(QWidget *parent = nullptr);
FlowLayout *flowLayout() const { return layout; }
protected:
void keyPressEvent(QKeyEvent *event) override;
private:
FlowLayout *layout;
};
+170
View File
@@ -0,0 +1,170 @@
/******************************************************************************
Example provided by Qt
<https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html>
Copyright (C) 2016 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
******************************************************************************/
#include "FlowLayout.hpp"
#include <QWidget>
FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
: QLayout(parent),
m_hSpace(hSpacing),
m_vSpace(vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) : m_hSpace(hSpacing), m_vSpace(vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
FlowLayout::~FlowLayout()
{
QLayoutItem *item;
while ((item = takeAt(0))) {
delete item;
}
}
void FlowLayout::addItem(QLayoutItem *item)
{
itemList.append(item);
}
int FlowLayout::horizontalSpacing() const
{
if (m_hSpace >= 0) {
return m_hSpace;
} else {
return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
}
}
int FlowLayout::verticalSpacing() const
{
if (m_vSpace >= 0) {
return m_vSpace;
} else {
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
}
}
int FlowLayout::count() const
{
return itemList.size();
}
QLayoutItem *FlowLayout::itemAt(int index) const
{
return itemList.value(index);
}
QLayoutItem *FlowLayout::takeAt(int index)
{
if (index >= 0 && index < itemList.size()) {
return itemList.takeAt(index);
}
return nullptr;
}
Qt::Orientations FlowLayout::expandingDirections() const
{
return {};
}
bool FlowLayout::hasHeightForWidth() const
{
return true;
}
int FlowLayout::heightForWidth(int width) const
{
int height = doLayout(QRect(0, 0, width, 0), true);
return height;
}
void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
doLayout(rect, false);
}
QSize FlowLayout::sizeHint() const
{
return minimumSize();
}
QSize FlowLayout::minimumSize() const
{
QSize size;
for (const QLayoutItem *item : std::as_const(itemList)) {
size = size.expandedTo(item->minimumSize());
}
const QMargins margins = contentsMargins();
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
return size;
}
int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
{
int left;
int top;
int right;
int bottom;
getContentsMargins(&left, &top, &right, &bottom);
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
for (QLayoutItem *item : std::as_const(itemList)) {
const QWidget *wid = item->widget();
int spaceX = horizontalSpacing();
if (spaceX == -1) {
spaceX = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton,
Qt::Horizontal);
}
int spaceY = verticalSpacing();
if (spaceY == -1) {
spaceY = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton,
Qt::Vertical);
}
int nextX = x + item->sizeHint().width() + spaceX;
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
x = effectiveRect.x();
y = y + lineHeight + spaceY;
nextX = x + item->sizeHint().width() + spaceX;
lineHeight = 0;
}
if (!testOnly) {
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
}
x = nextX;
lineHeight = qMax(lineHeight, item->sizeHint().height());
}
return y + lineHeight - rect.y() + bottom;
}
int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
{
QObject *parent = this->parent();
if (!parent) {
return -1;
} else if (parent->isWidgetType()) {
QWidget *pw = static_cast<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw);
} else {
return static_cast<QLayout *>(parent)->spacing();
}
}
+40
View File
@@ -0,0 +1,40 @@
/******************************************************************************
Example provided by Qt
<https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html>
Copyright (C) 2016 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
******************************************************************************/
#pragma once
#include <QLayout>
#include <QStyle>
class FlowLayout : public QLayout {
public:
explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
~FlowLayout();
void addItem(QLayoutItem *item) override;
int horizontalSpacing() const;
int verticalSpacing() const;
Qt::Orientations expandingDirections() const override;
bool hasHeightForWidth() const override;
int heightForWidth(int) const override;
int count() const override;
QLayoutItem *itemAt(int index) const override;
QSize minimumSize() const override;
void setGeometry(const QRect &rect) override;
QSize sizeHint() const override;
QLayoutItem *takeAt(int index) override;
private:
int doLayout(const QRect &rect, bool testOnly) const;
int smartSpacing(QStyle::PixelMetric pm) const;
QList<QLayoutItem *> itemList;
int m_hSpace;
int m_vSpace;
};
+185
View File
@@ -0,0 +1,185 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
Lain Bailey <lain@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "SourceSelectButton.hpp"
#include <utility/ThumbnailManager.hpp>
#include <widgets/OBSBasic.hpp>
#include <QDrag>
#include <QFrame>
#include <QMimeData>
#include <QPainter>
#include <QStyleOptionButton>
SourceSelectButton::SourceSelectButton(obs_source_t *source_, QWidget *parent) : QFrame(parent)
{
OBSSource source = source_;
weakSource = OBSGetWeakRef(source);
const char *sourceName = obs_source_get_name(source);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
button = new QPushButton(this);
button->setCheckable(true);
button->setAttribute(Qt::WA_Moved);
button->setAccessibleName(sourceName);
button->show();
layout = new QVBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
setLayout(layout);
label = new QLabel(sourceName);
label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setObjectName("name");
image = new QLabel(this);
image->setObjectName("thumbnail");
image->setAttribute(Qt::WA_TransparentForMouseEvents);
image->setMinimumSize(160, 90);
image->setMaximumSize(160, 90);
image->setAlignment(Qt::AlignCenter);
std::optional<QPixmap> cachedThumbnail = OBSBasic::Get()->thumbnails()->getCachedThumbnail(source);
if (cachedThumbnail.has_value()) {
thumbnailUpdated(*cachedThumbnail);
} else {
setDefaultThumbnail();
}
layout->addWidget(image);
layout->addWidget(label);
button->setFixedSize(width(), height());
button->move(0, 0);
setFocusPolicy(Qt::StrongFocus);
setFocusProxy(button);
connect(button, &QAbstractButton::pressed, this, &SourceSelectButton::buttonPressed);
}
SourceSelectButton::~SourceSelectButton() {}
QPointer<QPushButton> SourceSelectButton::getButton()
{
return button;
}
QString SourceSelectButton::text()
{
return label->text();
}
void SourceSelectButton::resizeEvent(QResizeEvent *)
{
button->setFixedSize(width(), height());
button->move(0, 0);
}
void SourceSelectButton::moveEvent(QMoveEvent *)
{
button->setFixedSize(width(), height());
button->move(0, 0);
}
void SourceSelectButton::buttonPressed()
{
dragStartPosition = QCursor::pos();
}
void SourceSelectButton::setDefaultThumbnail()
{
OBSSource source = OBSGetStrongRef(weakSource);
if (source) {
const char *id = obs_source_get_id(source);
QIcon icon = OBSBasic::Get()->GetSourceIcon(id);
image->setPixmap(icon.pixmap(45, 45));
}
}
void SourceSelectButton::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton)) {
return;
}
if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) {
return;
}
QMimeData *mimeData = new QMimeData;
OBSSource source = OBSGetStrongRef(weakSource);
if (source) {
std::string uuid = obs_source_get_uuid(source);
mimeData->setData("application/x-obs-source-uuid", uuid.c_str());
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setPixmap(this->grab());
drag->exec(Qt::CopyAction);
}
}
void SourceSelectButton::setRectVisible(bool visible)
{
OBSSource source = OBSGetStrongRef(weakSource);
if (!source) {
return;
}
if (rectVisible != visible) {
rectVisible = visible;
if (visible) {
uint32_t flags = obs_source_get_output_flags(source);
bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO;
if (hasVideo) {
thumbnail = OBSBasic::Get()->thumbnails()->getThumbnail(source);
connect(thumbnail.get(), &Thumbnail::updateThumbnail, this,
&SourceSelectButton::thumbnailUpdated);
thumbnailUpdated(thumbnail->getPixmap());
}
} else {
thumbnail.reset();
}
}
if (preload && !rectVisible) {
OBSBasic::Get()->thumbnails()->preloadThumbnail(source, this,
[=](QPixmap pixmap) { thumbnailUpdated(pixmap); });
}
preload = false;
}
void SourceSelectButton::setPreload(bool preload)
{
this->preload = preload;
}
void SourceSelectButton::thumbnailUpdated(QPixmap pixmap)
{
if (!pixmap.isNull()) {
image->setPixmap(pixmap.scaled(160, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation));
} else {
setDefaultThumbnail();
}
}
@@ -0,0 +1,68 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
Lain Bailey <lain@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <obs.hpp>
#include <QLabel>
#include <QPointer>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
class QLabel;
class Thumbnail;
class SourceSelectButton : public QFrame {
Q_OBJECT
public:
SourceSelectButton(obs_source_t *source, QWidget *parent = nullptr);
~SourceSelectButton();
QPointer<QPushButton> getButton();
QString text();
void setRectVisible(bool visible);
void setPreload(bool preload);
protected:
void resizeEvent(QResizeEvent *event) override;
void moveEvent(QMoveEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void buttonPressed();
private:
OBSWeakSource weakSource;
std::shared_ptr<Thumbnail> thumbnail;
QPointer<QLabel> image;
QPushButton *button = nullptr;
QVBoxLayout *layout = nullptr;
QLabel *label = nullptr;
bool preload = true;
bool rectVisible = false;
void setDefaultThumbnail();
QPoint dragStartPosition;
private slots:
void thumbnailUpdated(QPixmap pixmap);
};
+11 -4
View File
@@ -637,15 +637,21 @@ RenameProfile.Title="Rename Profile"
Basic.Main.MixerRename.Title="Rename Audio Source"
Basic.Main.MixerRename.Text="Please enter the name of the audio source"
# preview window disabled
Basic.Main.PreviewDisabled="Preview is currently disabled"
# add source dialog
Basic.SourceSelect="Create/Select Source"
Basic.SourceSelect.CreateNew="Create new"
Basic.SourceSelect.AddExisting="Add Existing"
Basic.SourceSelect="Add Source"
Basic.SourceSelect.SelectType="Source Type"
Basic.SourceSelect.Recent="Recently Added"
Basic.SourceSelect.NewSource="Create a New Source"
Basic.SourceSelect.Existing="Add an Existing Source"
Basic.SourceSelect.CreateButton="Create New"
Basic.SourceSelect.AddVisible="Make source visible"
Basic.SourceSelect.NoExisting="No existing %1 sources"
Basic.SourceSelect.Accessible.SourceName="Source Name"
Basic.SourceSelect.Accessible.Existing="Add an Existing Source"
Basic.SourceSelect.Deprecated.Create="This source type is marked as deprecated and does allow creating new sources."
# source box
Basic.Main.Sources.Visibility="Visibility"
@@ -783,6 +789,7 @@ Basic.Main.ShowContextBar="Show Source Toolbar"
Basic.Main.HideContextBar="Hide Source Toolbar"
Basic.Main.StopVirtualCam="Stop Virtual Camera"
Basic.Main.Group="Group %1"
Basic.Main.NewGroup="New Group"
Basic.Main.GroupItems="Group Selected Items"
Basic.Main.Ungroup="Ungroup"
Basic.Main.GridMode="Grid Mode"
+28
View File
@@ -2415,6 +2415,34 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
border: 1px solid var(--input_border_hover);
}
/* Add Source Dialog */
SourceSelectButton {
text-align: center;
padding: var(--padding_base) var(--padding_base);
margin: var(--spacing_base);
}
SourceSelectButton QLabel {
padding: var(--padding_large) 0;
text-align: center;
}
SourceSelectButton #thumbnail {
background: var(--grey6);
border: 1px solid var(--grey4);
padding: 0;
margin-top: var(--spacing_base);
}
SourceSelectButton QPushButton {
background: var(--grey5);
}
SourceSelectButton QPushButton:checked:focus,
SourceSelectButton QPushButton:focus {
border-color: var(--white3);
}
/* Idian Widgets */
idian--Group {
border-radius: var(--border_radius);
+11
View File
@@ -234,3 +234,14 @@ idian--ToggleSwitch {
qproperty-background_checked: var(--button_bg);
qproperty-background_checked_hover: var(--primary_light);
}
/* Add Source Dialog */
SourceSelectButton QPushButton,
SourceSelectButton #thumbnail {
border-color: var(--grey3);
}
SourceSelectButton QPushButton:checked {
background: var(--button_bg_red);
border-color: var(--button_bg_red_hover);
}
+13
View File
@@ -22,6 +22,7 @@
--primary: rgb(25,52,76);
--primary_light: rgb(33,71,109);
--primary_dark: rgb(19, 40, 58);
/* Layout */
--padding_large: min(max(0px, calc(1px * var(--padding_base_value))), 5px);
@@ -83,6 +84,13 @@
--padding_menu_y: calc(3px + calc(1 * var(--padding_base_value)));
}
/* --------------------- */
/* General Styling Hints */
.dialog-frame {
background: var(--bg_window);
}
QStatusBar {
background-color: var(--bg_window);
}
@@ -303,3 +311,8 @@ VolumeMeter {
OBSBasicStats {
background: var(--bg_window);
}
/* Add Source Dialog */
SourceSelectButton QPushButton {
background-color: var(--grey7);
}
+29 -3
View File
@@ -10,7 +10,7 @@
--grey1: rgb(140,140,140);
--grey2: rgb(254,254,254);
--grey3: rgb(254,254,254);
--grey4: rgb(243,243,243);
--grey4: rgb(245,245,245);
--grey5: rgb(236,236,236);
--grey6: rgb(229,229,229);
--grey7: rgb(211,211,211);
@@ -18,13 +18,13 @@
--primary: rgb(140,181,255);
--primary_light: rgb(178,207,255);
--primary_dark: rgb(22,31,65);
--primary_dark: rgb(122, 164, 243);
--bg_window: var(--grey7);
--bg_base: var(--grey6);
--bg_preview: var(--grey8);
--text: var(--black1);
--text: #030303
--text_light: var(--black3);
--text_muted: var(--black4);
@@ -34,7 +34,10 @@
--input_bg_hover: var(--grey3);
--input_bg_focus: var(--grey3);
--input_border_hover: var(--black5);
--button_bg_disabled: var(--grey7);
--button_border_hover: var(--black5);
--separator_hover: var(--black1);
@@ -44,6 +47,13 @@
--scrollbar_border: var(--grey7);
}
/* --------------------- */
/* General Styling Hints */
.button-primary:hover {
border-color: var(--button_border_hover);
}
VolumeMeter {
qproperty-backgroundNominalColor: var(--green4);
qproperty-backgroundWarningColor: var(--yellow4);
@@ -354,3 +364,19 @@ idian--ExpandButton::indicator {
idian--ExpandButton::indicator:checked {
image: url(theme:Light/up.svg);
}
/* Add Source Dialog */
SourceSelectButton QPushButton,
SourceSelectButton #thumbnail {
border-color: var(--grey3);
}
SourceSelectButton QPushButton:checked {
background: var(--primary);
border-color: var(--primary_light);
}
SourceSelectButton QPushButton:checked:focus,
SourceSelectButton QPushButton:focus {
border-color: var(--black3);
}
+6
View File
@@ -233,3 +233,9 @@ VolumeMeter {
qproperty-majorTickColor: palette(window-text);
qproperty-minorTickColor: palette(mid);
}
/* Add Source Dialog */
SourceSelectButton QPushButton,
SourceSelectButton #thumbnail {
border-color: var(--grey3);
}
+648 -231
View File
@@ -1,5 +1,6 @@
/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,12 +18,19 @@
#include "OBSBasicSourceSelect.hpp"
#include <qt-wrappers.hpp>
#include <OBSApp.hpp>
#include <utility/ResizeSignaler.hpp>
#include <utility/ThumbnailManager.hpp>
#include "qt-wrappers.hpp"
#include <QList>
#include <QMessageBox>
#include "moc_OBSBasicSourceSelect.cpp"
struct AddSourceData {
/* Input data */
// Input data
obs_source_t *source;
bool visible;
obs_transform_info *transform = nullptr;
@@ -38,85 +46,47 @@ struct AddSourceData {
uint32_t hide_transition_duration = 300;
OBSData private_settings;
/* Return data */
// Return data
obs_sceneitem_t *scene_item = nullptr;
};
bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source)
namespace {
QString getSourceDisplayName(QString type)
{
if (obs_source_is_hidden(source))
return true;
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
const char *name = obs_source_get_name(source);
const char *id = obs_source_get_unversioned_id(source);
if (strcmp(id, window->id) == 0)
window->ui->sourceList->addItem(QT_UTF8(name));
return true;
}
bool OBSBasicSourceSelect::EnumGroups(void *data, obs_source_t *source)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
const char *name = obs_source_get_name(source);
const char *id = obs_source_get_unversioned_id(source);
if (strcmp(id, window->id) == 0) {
OBSBasic *main = OBSBasic::Get();
OBSScene scene = main->GetCurrentScene();
obs_sceneitem_t *existing = obs_scene_get_group(scene, name);
if (!existing)
window->ui->sourceList->addItem(QT_UTF8(name));
if (type == "scene") {
return QTStr("Basic.Scene");
}
return true;
const char *inputChar = obs_get_latest_input_type_id(type.toUtf8().constData());
const char *displayChar = obs_source_get_display_name(inputChar);
std::string displayId = (displayChar) ? displayChar : "";
if (!displayId.empty()) {
return QString::fromStdString(displayId);
} else {
return QString();
}
}
void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata)
std::string getNewSourceName(std::string_view name)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source");
std::string newName{name};
int suffix = 1;
QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source));
}
void OBSBasicSourceSelect::OBSSourceRemoved(void *data, calldata_t *calldata)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source");
QMetaObject::invokeMethod(window, "SourceRemoved", Q_ARG(OBSSource, source));
}
void OBSBasicSourceSelect::SourceAdded(OBSSource source)
{
const char *name = obs_source_get_name(source);
const char *sourceId = obs_source_get_unversioned_id(source);
if (strcmp(sourceId, id) != 0)
return;
ui->sourceList->addItem(name);
}
void OBSBasicSourceSelect::SourceRemoved(OBSSource source)
{
const char *name = obs_source_get_name(source);
const char *sourceId = obs_source_get_unversioned_id(source);
if (strcmp(sourceId, id) != 0)
return;
QList<QListWidgetItem *> items = ui->sourceList->findItems(name, Qt::MatchFixedString);
if (!items.count())
return;
delete items[0];
for (;;) {
OBSSourceAutoRelease existing_source = obs_get_source_by_name(newName.c_str());
if (!existing_source) {
break;
}
char nextName[256];
std::snprintf(nextName, sizeof(nextName), "%s %d", name.data(), ++suffix);
newName = nextName;
}
return newName;
}
} // namespace
static void AddSource(void *_data, obs_scene_t *scene)
{
@@ -125,16 +95,21 @@ static void AddSource(void *_data, obs_scene_t *scene)
sceneitem = obs_scene_add(scene, data->source);
if (data->transform != nullptr)
if (data->transform != nullptr) {
obs_sceneitem_set_info2(sceneitem, data->transform);
if (data->crop != nullptr)
}
if (data->crop != nullptr) {
obs_sceneitem_set_crop(sceneitem, data->crop);
if (data->blend_method != nullptr)
}
if (data->blend_method != nullptr) {
obs_sceneitem_set_blending_method(sceneitem, *data->blend_method);
if (data->blend_mode != nullptr)
}
if (data->blend_mode != nullptr) {
obs_sceneitem_set_blending_mode(sceneitem, *data->blend_mode);
if (data->scale_type != nullptr)
}
if (data->scale_type != nullptr) {
obs_sceneitem_set_scale_filter(sceneitem, *data->scale_type);
}
if (data->show_transition_id && *data->show_transition_id) {
OBSSourceAutoRelease source = obs_source_create(data->show_transition_id, data->show_transition_id,
@@ -165,40 +140,23 @@ static void AddSource(void *_data, obs_scene_t *scene)
data->scene_item = sceneitem;
}
char *get_new_source_name(const char *name, const char *format)
{
struct dstr new_name = {0};
int inc = 0;
dstr_copy(&new_name, name);
for (;;) {
OBSSourceAutoRelease existing_source = obs_get_source_by_name(new_name.array);
if (!existing_source)
break;
dstr_printf(&new_name, format, name, ++inc + 1);
}
return new_name.array;
}
static void AddExisting(OBSSource source, bool visible, bool duplicate, SourceCopyInfo *info = nullptr)
{
OBSBasic *main = OBSBasic::Get();
OBSScene scene = main->GetCurrentScene();
if (!scene)
if (!scene) {
return;
}
if (duplicate) {
OBSSource from = source;
char *new_name = get_new_source_name(obs_source_get_name(source), "%s %d");
source = obs_source_duplicate(from, new_name, false);
std::string new_name = getNewSourceName(obs_source_get_name(source));
source = obs_source_duplicate(from, new_name.c_str(), false);
obs_source_release(source);
bfree(new_name);
if (!source)
if (!source) {
return;
}
}
AddSourceData data;
@@ -239,8 +197,9 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl
OBSBasic *main = OBSBasic::Get();
OBSScene scene = main->GetCurrentScene();
bool success = false;
if (!scene)
if (!scene) {
return false;
}
OBSSourceAutoRelease source = obs_get_source_by_name(name);
if (source && parent) {
@@ -262,7 +221,7 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl
newSource = source;
newSceneItem = data.scene_item;
/* set monitoring if source monitors by default */
// Set monitoring if source monitors by default
uint32_t flags = obs_source_get_output_flags(source);
if ((flags & OBS_SOURCE_MONITOR_BY_DEFAULT) != 0) {
obs_source_set_monitoring_type(source, OBS_MONITORING_TYPE_MONITOR_ONLY);
@@ -275,18 +234,578 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl
return success;
}
void OBSBasicSourceSelect::on_buttonBox_accepted()
OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s)
: QDialog(parent),
ui(new Ui::OBSBasicSourceSelect),
undo_s(undo_s)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
existingFlowLayout = ui->existingListFrame->flowLayout();
existingFlowLayout->setContentsMargins(0, 0, 0, 0);
existingFlowLayout->setSpacing(0);
/* The scroll viewport is not accessible via Designer, so we have to disable autoFillBackground here.
*
* Additionally when Qt calls setWidget on a scrollArea to set the contents widget, it force sets
* autoFillBackground to true overriding whatever is set in Designer so we have to do that here too.
*/
ui->existingScrollArea->viewport()->setAutoFillBackground(false);
ui->existingScrollContents->setAutoFillBackground(false);
auto resizeSignaler = new ResizeSignaler(ui->existingScrollArea);
ui->existingScrollArea->installEventFilter(resizeSignaler);
connect(resizeSignaler, &ResizeSignaler::resized, this, &OBSBasicSourceSelect::checkSourceVisibility);
connect(ui->existingScrollArea->verticalScrollBar(), &QScrollBar::valueChanged, this,
&OBSBasicSourceSelect::checkSourceVisibility);
connect(ui->existingScrollArea->horizontalScrollBar(), &QScrollBar::valueChanged, this,
&OBSBasicSourceSelect::checkSourceVisibility);
ui->createNewFrame->setVisible(false);
ui->deprecatedCreateLabel->setVisible(false);
ui->deprecatedCreateLabel->setProperty("class", "text-muted");
getSourceTypes();
getSources();
updateExistingSources(16);
connect(ui->sourceTypeList, &QListWidget::itemDoubleClicked, this, &OBSBasicSourceSelect::createNewSource);
connect(ui->newSourceName, &QLineEdit::returnPressed, this, &OBSBasicSourceSelect::createNewSource);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(ui->addExistingButton, &QAbstractButton::clicked, this, &OBSBasicSourceSelect::addSelectedSources);
connect(this, &OBSBasicSourceSelect::selectedItemsChanged, this, [=]() {
ui->addExistingButton->setEnabled(selectedItems.size() > 0);
if (selectedItems.size() > 0) {
ui->addExistingButton->setText(QTStr("Add %1 Existing").arg(selectedItems.size()));
} else {
ui->addExistingButton->setText("Add Existing");
}
});
App()->DisableHotkeys();
}
OBSBasicSourceSelect::~OBSBasicSourceSelect()
{
App()->UpdateHotkeyFocusSetting();
}
void OBSBasicSourceSelect::checkSourceVisibility()
{
QList<QAbstractButton *> buttons = sourceButtons->buttons();
// Allow some room for previous/next rows to make scrolling a bit more seamless
QRect scrollAreaRect(QPoint(0, 0), ui->existingScrollArea->size());
scrollAreaRect.setTop(scrollAreaRect.top() - Thumbnail::size.width());
scrollAreaRect.setBottom(scrollAreaRect.bottom() + Thumbnail::size.height());
for (QAbstractButton *button : buttons) {
SourceSelectButton *sourceButton = qobject_cast<SourceSelectButton *>(button->parent());
if (sourceButton) {
QRect buttonRect = button->rect();
buttonRect.moveTo(button->mapTo(ui->existingScrollArea, buttonRect.topLeft()));
if (scrollAreaRect.intersects(buttonRect)) {
sourceButton->setPreload(true);
} else {
sourceButton->setPreload(false);
}
}
}
scrollAreaRect = QRect(QPoint(0, 0), ui->existingScrollArea->size());
for (QAbstractButton *button : buttons) {
SourceSelectButton *sourceButton = qobject_cast<SourceSelectButton *>(button->parent());
if (sourceButton) {
QRect buttonRect = button->rect();
buttonRect.moveTo(button->mapTo(ui->existingScrollArea, buttonRect.topLeft()));
if (scrollAreaRect.intersects(buttonRect)) {
sourceButton->setRectVisible(true);
} else {
sourceButton->setRectVisible(false);
}
}
}
}
void OBSBasicSourceSelect::getSources()
{
sources.clear();
obs_enum_sources(enumSourcesCallback, this);
emit sourcesUpdated();
}
void OBSBasicSourceSelect::updateExistingSources(int limit)
{
delete sourceButtons;
sourceButtons = new QButtonGroup(this);
sourceButtons->setExclusive(false);
std::vector<obs_source_t *> matchingSources{};
std::copy_if(sources.begin(), sources.end(), std::back_inserter(matchingSources), [this](obs_source_t *source) {
if (!source || obs_source_removed(source)) {
return false;
}
const char *id = obs_source_get_unversioned_id(source);
QString stringId = QString(id);
if (stringId.compare("group") == 0) {
return false;
}
if (sourceTypeId.compare(stringId) == 0 || sourceTypeId.isNull()) {
return true;
}
return false;
});
QWidget *prevTabWidget = ui->sourceTypeList;
auto createSourceButton = [this, &prevTabWidget](obs_source_t *source) {
SourceSelectButton *newButton = new SourceSelectButton(source, ui->existingListFrame);
std::string name = obs_source_get_name(source);
existingFlowLayout->addWidget(newButton);
sourceButtons->addButton(newButton->getButton());
if (!prevTabWidget) {
setTabOrder(ui->existingListFrame, newButton->getButton());
} else {
setTabOrder(prevTabWidget, newButton->getButton());
}
prevTabWidget = newButton->getButton();
};
bool isReverseListOrder = sourceTypeId.isNull();
size_t iterationLimit = limit > 0 ? std::min(static_cast<size_t>(limit), matchingSources.size())
: matchingSources.size();
if (isReverseListOrder) {
std::for_each(matchingSources.rbegin(), matchingSources.rbegin() + iterationLimit, createSourceButton);
} else {
std::for_each(matchingSources.begin(), matchingSources.begin() + iterationLimit, createSourceButton);
}
setTabOrder(prevTabWidget, ui->addExistingContainer);
connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled);
ui->existingListFrame->adjustSize();
QTimer::singleShot(10, this, [this] { checkSourceVisibility(); });
}
bool OBSBasicSourceSelect::enumSourcesCallback(void *data, obs_source_t *source)
{
if (obs_source_is_hidden(source)) {
return true;
}
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
window->sources.push_back(source);
return true;
}
bool OBSBasicSourceSelect::enumGroupsCallback(void *data, obs_source_t *source)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
const char *name = obs_source_get_name(source);
const char *id = obs_source_get_unversioned_id(source);
if (window->sourceTypeId.compare(QString(id)) == 0) {
OBSBasic *main = OBSBasic::Get();
OBSScene scene = main->GetCurrentScene();
obs_sceneitem_t *existing = obs_scene_get_group(scene, name);
if (!existing) {
QPushButton *button = new QPushButton(name);
connect(button, &QPushButton::clicked, window, &OBSBasicSourceSelect::addSelectedSources);
}
}
return true;
}
void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source");
QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source));
}
void OBSBasicSourceSelect::getSourceTypes()
{
OBSBasic *main = qobject_cast<OBSBasic *>(App()->GetMainWindow());
const char *unversioned_type;
const char *type;
size_t idx = 0;
while (obs_enum_input_types2(idx++, &type, &unversioned_type)) {
const char *name = obs_source_get_display_name(type);
uint32_t caps = obs_get_source_output_flags(type);
if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) {
continue;
}
QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList);
newItem->setData(Qt::DisplayRole, name);
newItem->setData(UNVERSIONED_ID_ROLE, unversioned_type);
if ((caps & OBS_SOURCE_DEPRECATED) != 0) {
newItem->setData(DEPRECATED_ROLE, true);
} else {
newItem->setData(DEPRECATED_ROLE, false);
QIcon icon;
icon = main->GetSourceIcon(type);
newItem->setIcon(icon);
}
}
QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList);
newItem->setData(Qt::DisplayRole, Str("Basic.Scene"));
newItem->setData(UNVERSIONED_ID_ROLE, "scene");
QIcon icon;
icon = main->GetSceneIcon();
newItem->setIcon(icon);
ui->sourceTypeList->sortItems();
// Shift Deprecated sources to the bottom
QList<QListWidgetItem *> deprecatedItems;
for (int i = 0; i < ui->sourceTypeList->count(); i++) {
QListWidgetItem *item = ui->sourceTypeList->item(i);
if (!item) {
break;
}
bool isDeprecated = item->data(DEPRECATED_ROLE).toBool();
if (isDeprecated) {
ui->sourceTypeList->takeItem(i);
deprecatedItems.append(item);
}
}
for (auto &item : deprecatedItems) {
ui->sourceTypeList->addItem(item);
}
QListWidgetItem *allSources = new QListWidgetItem();
allSources->setData(Qt::DisplayRole, Str("Basic.SourceSelect.Recent"));
allSources->setData(UNVERSIONED_ID_ROLE, QVariant());
ui->sourceTypeList->insertItem(0, allSources);
ui->sourceTypeList->setCurrentItem(allSources);
ui->sourceTypeList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
connect(ui->sourceTypeList, &QListWidget::currentItemChanged, this, &OBSBasicSourceSelect::sourceTypeSelected);
}
void OBSBasicSourceSelect::setSelectedSourceType(QListWidgetItem *item)
{
setSelectedSource(nullptr);
QLayout *layout = ui->existingListFrame->flowLayout();
// Clear existing buttons when switching types
QLayoutItem *child = nullptr;
while ((child = layout->takeAt(0)) != nullptr) {
if (child->widget()) {
child->widget()->deleteLater();
}
delete child;
}
QVariant unversionedIdData = item->data(UNVERSIONED_ID_ROLE);
QVariant deprecatedData = item->data(DEPRECATED_ROLE);
if (unversionedIdData.isNull()) {
setSelectedSource(nullptr);
sourceTypeId.clear();
ui->createNewFrame->setVisible(false);
updateExistingSources(16);
return;
}
QString type = unversionedIdData.toString();
if (type.compare(sourceTypeId) == 0) {
return;
}
ui->createNewFrame->setVisible(true);
bool isDeprecatedType = deprecatedData.toBool();
ui->newSourceName->setVisible(!isDeprecatedType);
ui->createNewSource->setVisible(!isDeprecatedType);
ui->deprecatedCreateLabel->setVisible(isDeprecatedType);
sourceTypeId = type;
QString placeHolderText{getSourceDisplayName(sourceTypeId)};
QString text{placeHolderText};
int i = 2;
OBSSourceAutoRelease source = nullptr;
while ((source = obs_get_source_by_name(QT_TO_UTF8(text)))) {
text = QString("%1 %2").arg(placeHolderText).arg(i++);
}
ui->newSourceName->setText(text);
ui->newSourceName->selectAll();
if (sourceTypeId.compare("scene") == 0) {
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
OBSSource curSceneSource = main->GetCurrentSceneSource();
delete sourceButtons;
sourceButtons = new QButtonGroup(this);
int count = main->ui->scenes->count();
QWidget *prevTabItem = ui->sourceTypeList;
for (int i = 0; i < count; i++) {
QListWidgetItem *item = main->ui->scenes->item(i);
OBSScene scene = GetOBSRef<OBSScene>(item);
OBSSource sceneSource = obs_scene_get_source(scene);
if (curSceneSource == sceneSource) {
continue;
}
SourceSelectButton *newButton = new SourceSelectButton(sceneSource, ui->existingListFrame);
existingFlowLayout->addWidget(newButton);
sourceButtons->addButton(newButton->getButton());
setTabOrder(prevTabItem, newButton->getButton());
prevTabItem = newButton->getButton();
}
connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled);
QTimer::singleShot(100, this, [this] { checkSourceVisibility(); });
ui->createNewFrame->setVisible(false);
} else if (sourceTypeId.compare("group") == 0) {
obs_enum_sources(enumGroupsCallback, this);
} else {
updateExistingSources();
}
if (layout->count() == 0) {
QLabel *noExisting = new QLabel();
noExisting->setText(QTStr("Basic.SourceSelect.NoExisting").arg(getSourceDisplayName(sourceTypeId)));
noExisting->setProperty("class", "text-muted");
layout->addWidget(noExisting);
}
}
void OBSBasicSourceSelect::OBSSourceRemoved(void *data, calldata_t *calldata)
{
OBSBasicSourceSelect *window = static_cast<OBSBasicSourceSelect *>(data);
obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source");
QMetaObject::invokeMethod(window, "SourceRemoved", Q_ARG(OBSSource, source));
}
void OBSBasicSourceSelect::sourceButtonToggled(QAbstractButton *button, bool checked)
{
SourceSelectButton *buttonParent = dynamic_cast<SourceSelectButton *>(button->parentWidget());
Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
bool ctrlDown = (modifiers & Qt::ControlModifier);
bool shiftDown = (modifiers & Qt::ShiftModifier);
if (!buttonParent) {
clearSelectedItems();
return;
}
int selectedIndex = existingFlowLayout->indexOf(buttonParent);
if (ctrlDown && !shiftDown) {
if (checked) {
addSelectedItem(buttonParent);
} else {
removeSelectedItem(buttonParent);
}
lastSelectedIndex = existingFlowLayout->indexOf(buttonParent);
return;
} else if (shiftDown) {
if (!ctrlDown) {
clearSelectedItems();
}
sourceButtons->blockSignals(true);
int start = std::min(selectedIndex, lastSelectedIndex);
int end = std::max(selectedIndex, lastSelectedIndex);
for (int i = start; i <= end; i++) {
auto item = existingFlowLayout->itemAt(i);
if (!item) {
continue;
}
auto widget = item->widget();
if (!widget) {
continue;
}
auto entry = dynamic_cast<SourceSelectButton *>(widget);
if (entry) {
entry->getButton()->setChecked(true);
addSelectedItem(entry);
}
}
sourceButtons->blockSignals(false);
} else {
lastSelectedIndex = existingFlowLayout->indexOf(buttonParent);
bool reselectItem = selectedItems.size() > 1;
clearSelectedItems();
if (checked) {
addSelectedItem(buttonParent);
} else if (reselectItem) {
button->setChecked(true);
addSelectedItem(buttonParent);
}
}
}
void OBSBasicSourceSelect::sourceDropped(QString uuid)
{
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.toStdString().c_str());
if (source) {
const char *name = obs_source_get_name(source);
bool visible = ui->sourceVisible->isChecked();
addExistingSource(name, visible);
}
}
void OBSBasicSourceSelect::setSelectedSource(SourceSelectButton *button)
{
clearSelectedItems();
addSelectedItem(button);
}
void OBSBasicSourceSelect::addSelectedItem(SourceSelectButton *button)
{
if (button == nullptr) {
return;
}
auto it = std::find(selectedItems.begin(), selectedItems.end(), button);
if (it == selectedItems.end()) {
selectedItems.push_back(button);
emit selectedItemsChanged();
}
}
void OBSBasicSourceSelect::removeSelectedItem(SourceSelectButton *button)
{
if (button == nullptr) {
return;
}
auto it = std::find(selectedItems.begin(), selectedItems.end(), button);
if (it != selectedItems.end()) {
selectedItems.erase(it);
emit selectedItemsChanged();
}
}
void OBSBasicSourceSelect::clearSelectedItems()
{
if (selectedItems.size() == 0) {
return;
}
sourceButtons->blockSignals(true);
for (auto &item : selectedItems) {
item->getButton()->setChecked(false);
}
sourceButtons->blockSignals(false);
selectedItems.clear();
emit selectedItemsChanged();
}
void OBSBasicSourceSelect::createNewSource()
{
bool useExisting = ui->selectExisting->isChecked();
bool visible = ui->sourceVisible->isChecked();
if (useExisting) {
QListWidgetItem *item = ui->sourceList->currentItem();
if (!item)
return;
if (ui->newSourceName->text().isEmpty()) {
return;
}
QString source_name = item->text();
AddExisting(QT_TO_UTF8(source_name), visible, false);
if (sourceTypeId.isNull()) {
return;
}
if (sourceTypeId.compare("scene") == 0) {
return;
}
OBSSceneItem item;
std::string sourceType = sourceTypeId.toStdString();
const char *id = sourceType.c_str();
if (!AddNew(this, id, QT_TO_UTF8(ui->newSourceName->text()), visible, newSource, item)) {
return;
}
if (newSource && strcmp(obs_source_get_id(newSource.Get()), "group") != 0) {
OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
std::string scene_name = obs_source_get_name(main->GetCurrentSceneSource());
auto undo = [scene_name, main](const std::string &data) {
OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str());
obs_source_remove(source);
OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source.Get(), true);
};
OBSDataAutoRelease wrapper = obs_data_create();
obs_data_set_string(wrapper, "id", id);
obs_data_set_int(wrapper, "item_id", obs_sceneitem_get_id(item));
obs_data_set_string(wrapper, "name", ui->newSourceName->text().toUtf8().constData());
obs_data_set_bool(wrapper, "visible", visible);
auto redo = [scene_name, main](const std::string &data) {
OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source.Get(), true);
OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str());
OBSSource source;
OBSSceneItem item;
AddNew(NULL, obs_data_get_string(dat, "id"), obs_data_get_string(dat, "name"),
obs_data_get_bool(dat, "visible"), source, item);
obs_sceneitem_set_id(item, (int64_t)obs_data_get_int(dat, "item_id"));
};
undo_s.add_action(QTStr("Undo.Add").arg(ui->newSourceName->text()), undo, redo,
std::string(obs_source_get_name(newSource)), std::string(obs_data_get_json(wrapper)));
main->CreatePropertiesWindow(newSource);
}
close();
}
void OBSBasicSourceSelect::addExistingSource(QString name, bool visible)
{
OBSSourceAutoRelease source = obs_get_source_by_name(name.toStdString().c_str());
if (source) {
AddExisting(source.Get(), visible, false);
OBSBasic *main = OBSBasic::Get();
const char *scene_name = obs_source_get_name(main->GetCurrentSceneSource());
@@ -309,150 +828,48 @@ void OBSBasicSourceSelect::on_buttonBox_accepted()
obs_scene_release(scene);
};
auto redo = [scene_name, main, source_name, visible](const std::string &) {
auto redo = [scene_name, main, name, visible](const std::string &) {
obs_source_t *scene_source = obs_get_source_by_name(scene_name);
main->SetCurrentScene(scene_source, true);
obs_source_release(scene_source);
AddExisting(QT_TO_UTF8(source_name), visible, false);
AddExisting(QT_TO_UTF8(name), visible, false);
};
undo_s.add_action(QTStr("Undo.Add").arg(source_name), undo, redo, "", "");
} else {
if (ui->sourceName->text().isEmpty()) {
OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text"));
return;
}
OBSSceneItem item;
if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()), visible, newSource, item))
return;
OBSBasic *main = OBSBasic::Get();
std::string scene_name = obs_source_get_name(main->GetCurrentSceneSource());
auto undo = [scene_name, main](const std::string &data) {
OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str());
obs_source_remove(source);
OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source.Get(), true);
};
OBSDataAutoRelease wrapper = obs_data_create();
obs_data_set_string(wrapper, "id", id);
obs_data_set_int(wrapper, "item_id", obs_sceneitem_get_id(item));
obs_data_set_string(wrapper, "name", ui->sourceName->text().toUtf8().constData());
obs_data_set_bool(wrapper, "visible", visible);
auto redo = [scene_name, main](const std::string &data) {
OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str());
main->SetCurrentScene(scene_source.Get(), true);
OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str());
OBSSource source;
OBSSceneItem item;
AddNew(NULL, obs_data_get_string(dat, "id"), obs_data_get_string(dat, "name"),
obs_data_get_bool(dat, "visible"), source, item);
obs_sceneitem_set_id(item, (int64_t)obs_data_get_int(dat, "item_id"));
};
undo_s.add_action(QTStr("Undo.Add").arg(ui->sourceName->text()), undo, redo,
std::string(obs_source_get_name(newSource)), std::string(obs_data_get_json(wrapper)));
undo_s.add_action(QTStr("Undo.Add").arg(name), undo, redo, "", "");
}
done(DialogCode::Accepted);
}
void OBSBasicSourceSelect::on_buttonBox_rejected()
void OBSBasicSourceSelect::on_createNewSource_clicked(bool)
{
done(DialogCode::Rejected);
createNewSource();
}
static inline const char *GetSourceDisplayName(const char *id)
void OBSBasicSourceSelect::addSelectedSources()
{
if (strcmp(id, "scene") == 0)
return Str("Basic.Scene");
else if (strcmp(id, "group") == 0)
return Str("Group");
const char *v_id = obs_get_latest_input_type_id(id);
return obs_source_get_display_name(v_id);
if (selectedItems.size() == 0) {
return;
}
bool visible = ui->sourceVisible->isChecked();
for (auto &item : selectedItems) {
QString sourceName = item->text();
addExistingSource(sourceName, visible);
}
close();
}
OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_, undo_stack &undo_s)
: QDialog(parent),
ui(new Ui::OBSBasicSourceSelect),
id(id_),
undo_s(undo_s)
void OBSBasicSourceSelect::sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
ui->sourceList->setAttribute(Qt::WA_MacShowFocusRect, false);
QString placeHolderText{QT_UTF8(GetSourceDisplayName(id))};
QString text{placeHolderText};
int i = 2;
OBSSourceAutoRelease source = nullptr;
while ((source = obs_get_source_by_name(QT_TO_UTF8(text)))) {
text = QString("%1 %2").arg(placeHolderText).arg(i++);
}
ui->sourceName->setText(text);
ui->sourceName->setFocus(); //Fixes deselect of text.
ui->sourceName->selectAll();
installEventFilter(CreateShortcutFilter());
connect(ui->createNew, &QRadioButton::pressed, this, [&]() {
QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok);
if (!button->isEnabled())
button->setEnabled(true);
});
connect(ui->selectExisting, &QRadioButton::pressed, this, [&]() {
QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok);
bool enabled = ui->sourceList->selectedItems().size() != 0;
if (button->isEnabled() != enabled)
button->setEnabled(enabled);
});
connect(ui->sourceList, &QListWidget::itemSelectionChanged, this, [&]() {
QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok);
if (!button->isEnabled())
button->setEnabled(true);
});
if (strcmp(id_, "scene") == 0) {
OBSBasic *main = OBSBasic::Get();
OBSSource curSceneSource = main->GetCurrentSceneSource();
ui->selectExisting->setChecked(true);
ui->createNew->setChecked(false);
ui->createNew->setEnabled(false);
ui->sourceName->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
int count = main->ui->scenes->count();
for (int i = 0; i < count; i++) {
QListWidgetItem *item = main->ui->scenes->item(i);
OBSScene scene = GetOBSRef<OBSScene>(item);
OBSSource sceneSource = obs_scene_get_source(scene);
if (curSceneSource == sceneSource)
continue;
const char *name = obs_source_get_name(sceneSource);
ui->sourceList->addItem(QT_UTF8(name));
}
} else if (strcmp(id_, "group") == 0) {
obs_enum_sources(EnumGroups, this);
} else {
obs_enum_sources(EnumSources, this);
}
setSelectedSourceType(current);
}
void OBSBasicSourceSelect::SourcePaste(SourceCopyInfo &info, bool dup)
{
OBSSource source = OBSGetStrongRef(info.weak_source);
if (!source)
if (!source) {
return;
}
AddExisting(source, info.visible, dup, &info);
}
+49 -11
View File
@@ -1,5 +1,6 @@
/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,38 +18,75 @@
#pragma once
#include <OBSApp.hpp>
#include "ui_OBSBasicSourceSelect.h"
#include <components/FlowLayout.hpp>
#include <components/SourceSelectButton.hpp>
#include <utility/undo_stack.hpp>
#include <widgets/OBSBasic.hpp>
#include <obs.hpp>
#include <QButtonGroup>
#include <QDialog>
constexpr int UNVERSIONED_ID_ROLE = Qt::UserRole + 1;
constexpr int DEPRECATED_ROLE = Qt::UserRole + 2;
class OBSBasicSourceSelect : public QDialog {
Q_OBJECT
private:
std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
const char *id;
QString sourceTypeId;
undo_stack &undo_s;
static bool EnumSources(void *data, obs_source_t *source);
static bool EnumGroups(void *data, obs_source_t *source);
QPointer<QButtonGroup> sourceButtons;
std::vector<obs_source_t *> sources;
std::vector<obs_source_t *> groups;
QPointer<FlowLayout> existingFlowLayout = nullptr;
void getSources();
void updateExistingSources(int limit = 0);
static bool enumSourcesCallback(void *data, obs_source_t *source);
static bool enumGroupsCallback(void *data, obs_source_t *source);
static void OBSSourceRemoved(void *data, calldata_t *calldata);
static void OBSSourceAdded(void *data, calldata_t *calldata);
private slots:
void on_buttonBox_accepted();
void on_buttonBox_rejected();
void getSourceTypes();
void setSelectedSourceType(QListWidgetItem *item);
void SourceAdded(OBSSource source);
void SourceRemoved(OBSSource source);
int lastSelectedIndex = -1;
std::vector<SourceSelectButton *> selectedItems;
void setSelectedSource(SourceSelectButton *button);
void addSelectedItem(SourceSelectButton *button);
void removeSelectedItem(SourceSelectButton *button);
void clearSelectedItems();
void createNewSource();
void addExistingSource(QString name, bool visible);
void checkSourceVisibility();
signals:
void sourcesUpdated();
void selectedItemsChanged();
public slots:
void on_createNewSource_clicked(bool checked);
void addSelectedSources();
void sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *previous);
void sourceButtonToggled(QAbstractButton *button, bool checked);
void sourceDropped(QString uuid);
public:
OBSBasicSourceSelect(OBSBasic *parent, const char *id, undo_stack &undo_s);
OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s);
~OBSBasicSourceSelect();
OBSSource newSource;
+654 -86
View File
@@ -3,106 +3,674 @@
<class>OBSBasicSourceSelect</class>
<widget class="QDialog" name="OBSBasicSourceSelect">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>352</width>
<height>314</height>
<width>1000</width>
<height>614</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>1000</height>
</size>
</property>
<property name="windowTitle">
<string>Basic.SourceSelect</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="class" stdset="0">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="createNew">
<property name="text">
<string>Basic.SourceSelect.CreateNew</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="sourceName"/>
</item>
<item>
<widget class="QRadioButton" name="selectExisting">
<property name="text">
<string>Basic.SourceSelect.AddExisting</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="sourceList">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="sourceVisible">
<property name="text">
<string>Basic.SourceSelect.AddVisible</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<widget class="QFrame" name="dialogInner">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="selectTypeFrame">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Basic.SourceSelect.SelectType</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-frame</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="sourceTypeList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>460</height>
</size>
</property>
<property name="accessibleName">
<string>Basic.SourceSelect.SelectType</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="class" stdset="0">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>17</width>
<height>4</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="sourcesFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8" stretch="0,0">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="selectSourceContainer">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="selectExistingFrame">
<property name="accessibleName">
<string>Basic.SourceSelect.AddExisting</string>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container dialog-frame</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="existingLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Basic.SourceSelect.Existing</string>
</property>
<property name="class" stdset="0">
<string>text-title</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="existingScrollArea">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="existingScrollContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>510</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FlowFrame" name="existingListFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="addExistingContainer" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>259</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addExistingButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Add Existing</string>
</property>
<property name="class" stdset="0">
<string>button-primary</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="createNewFrame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container dialog-frame</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Basic.SourceSelect.NewSource</string>
</property>
<property name="class" stdset="0">
<string>text-title</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="deprecatedCreateLabel">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>Basic.SourceSelect.Deprecated.Create</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="newSourceName">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Basic.SourceSelect.Accessible.SourceName</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="createNewSource">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Basic.SourceSelect.CreateButton</string>
</property>
<property name="class" stdset="0">
<string>button-primary margin-left</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="footer">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="sourceVisible">
<property name="text">
<string>Basic.SourceSelect.AddVisible</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>createNew</sender>
<signal>toggled(bool)</signal>
<receiver>sourceName</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>79</x>
<y>29</y>
</hint>
<hint type="destinationlabel">
<x>108</x>
<y>53</y>
</hint>
</hints>
</connection>
<connection>
<sender>selectExisting</sender>
<signal>toggled(bool)</signal>
<receiver>sourceList</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>51</x>
<y>80</y>
</hint>
<hint type="destinationlabel">
<x>65</x>
<y>128</y>
</hint>
</hints>
</connection>
</connections>
<customwidgets>
<customwidget>
<class>FlowFrame</class>
<extends>QFrame</extends>
<header>components/FlowFrame.hpp</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>sourceTypeList</tabstop>
<tabstop>existingScrollArea</tabstop>
<tabstop>addExistingButton</tabstop>
<tabstop>newSourceName</tabstop>
<tabstop>createNewSource</tabstop>
<tabstop>sourceVisible</tabstop>
</tabstops>
<resources>
<include location="obs.qrc"/>
</resources>
<connections/>
</ui>
+39
View File
@@ -0,0 +1,39 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <QObject>
class ResizeSignaler : public QObject {
Q_OBJECT
public:
inline ResizeSignaler(QObject *parent) : QObject(parent) {}
signals:
void resized();
protected:
bool eventFilter(QObject *object, QEvent *event) override
{
if (event->type() == QEvent::Resize) {
emit resized();
}
return QObject::eventFilter(object, event);
}
};
+156 -84
View File
@@ -1,5 +1,6 @@
/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -16,6 +17,7 @@
******************************************************************************/
#include "ScreenshotObj.hpp"
#include "display-helpers.hpp"
#include <widgets/OBSBasic.hpp>
@@ -30,11 +32,23 @@
#include "moc_ScreenshotObj.cpp"
static void ScreenshotTick(void *param, float);
namespace {
void renderTick(void *param, float)
{
auto *self = static_cast<ScreenshotObj *>(param);
if (self->stage() == ScreenshotObj::Stage::Finished) {
return;
}
obs_enter_graphics();
self->processStage();
obs_leave_graphics();
}
} // namespace
ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source))
{
obs_add_tick_callback(ScreenshotTick, this);
obs_add_tick_callback(renderTick, this);
}
ScreenshotObj::~ScreenshotObj()
@@ -44,40 +58,26 @@ ScreenshotObj::~ScreenshotObj()
gs_texrender_destroy(texrender);
obs_leave_graphics();
obs_remove_tick_callback(ScreenshotTick, this);
if (th.joinable()) {
th.join();
if (cx && cy) {
OBSBasic *main = OBSBasic::Get();
main->ShowStatusBarMessage(
QTStr("Basic.StatusBar.ScreenshotSavedTo").arg(QT_UTF8(path.c_str())));
main->lastScreenshot = path;
main->OnEvent(OBS_FRONTEND_EVENT_SCREENSHOT_TAKEN);
}
}
obs_remove_tick_callback(renderTick, this);
}
void ScreenshotObj::Screenshot()
void ScreenshotObj::renderScreenshot()
{
OBSSource source = OBSGetStrongRef(weakSource);
OBSSourceAutoRelease source = OBSGetStrongRef(weakSource);
if (source) {
cx = obs_source_get_width(source);
cy = obs_source_get_height(source);
sourceWidth = obs_source_get_width(source);
sourceHeight = obs_source_get_height(source);
} else {
obs_video_info ovi;
obs_get_video_info(&ovi);
cx = ovi.base_width;
cy = ovi.base_height;
sourceWidth = ovi.base_width;
sourceHeight = ovi.base_height;
}
if (!cx || !cy) {
blog(LOG_WARNING, "Cannot screenshot, invalid target size");
obs_remove_tick_callback(ScreenshotTick, this);
if (!sourceWidth || !sourceHeight) {
blog(LOG_WARNING, "Cannot render source, invalid target size");
obs_remove_tick_callback(renderTick, this);
deleteLater();
return;
}
@@ -94,15 +94,34 @@ void ScreenshotObj::Screenshot()
#endif
const enum gs_color_format format = gs_get_format_from_space(space);
texrender = gs_texrender_create(format, GS_ZS_NONE);
stagesurf = gs_stagesurface_create(cx, cy, format);
outputWidth = customSize.isValid() ? customSize.width() : sourceWidth;
outputHeight = customSize.isValid() ? customSize.height() : sourceHeight;
if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) {
texrender = gs_texrender_create(format, GS_ZS_NONE);
stagesurf = gs_stagesurface_create(outputWidth, outputHeight, format);
if (gs_texrender_begin_with_color_space(texrender, outputWidth, outputHeight, space)) {
vec4 zero;
vec4_zero(&zero);
int x;
int y;
int scaledWidth;
int scaledHeight;
float scale;
GetScaleAndCenterPos(sourceWidth, sourceHeight, outputWidth, outputHeight, x, y, scale);
scaledWidth = int(scale * float(sourceWidth));
scaledHeight = int(scale * float(sourceHeight));
gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0);
gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f);
gs_viewport_push();
gs_projection_push();
gs_ortho(0.0f, (float)sourceWidth, 0.0f, (float)sourceHeight, -100.0f, 100.0f);
gs_set_viewport(x, y, scaledWidth, scaledHeight);
gs_blend_state_push();
gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);
@@ -115,35 +134,60 @@ void ScreenshotObj::Screenshot()
obs_render_main_texture();
}
gs_projection_pop();
gs_viewport_pop();
gs_blend_state_pop();
gs_texrender_end(texrender);
}
}
void ScreenshotObj::Download()
void ScreenshotObj::processStage()
{
switch (stage_) {
case Stage::Render:
renderScreenshot();
stage_ = Stage::Download;
break;
case Stage::Download:
downloadData();
stage_ = Stage::Output;
break;
case Stage::Output:
copyData();
QMetaObject::invokeMethod(this, &ScreenshotObj::handleSave, Qt::QueuedConnection);
obs_remove_tick_callback(renderTick, this);
stage_ = Stage::Finished;
break;
case Stage::Finished:
break;
}
}
void ScreenshotObj::downloadData()
{
gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender));
}
void ScreenshotObj::Copy()
void ScreenshotObj::copyData()
{
uint8_t *videoData = nullptr;
uint32_t videoLinesize = 0;
if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) {
if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) {
const uint32_t linesize = cx * 8;
half_bytes.reserve(cx * cy * 8);
const uint32_t linesize = outputWidth * 8;
half_bytes.reserve(outputWidth * outputHeight * 8);
for (uint32_t y = 0; y < cy; y++) {
for (uint32_t y = 0; y < outputHeight; y++) {
const uint8_t *const line = videoData + (y * videoLinesize);
half_bytes.insert(half_bytes.end(), line, line + linesize);
}
} else {
image = QImage(cx, cy, QImage::Format::Format_RGBX8888);
image = QImage(outputWidth, outputHeight, QImage::Format::Format_RGBX8888);
int linesize = image.bytesPerLine();
for (int y = 0; y < (int)cy; y++)
for (int y = 0; y < (int)outputHeight; y++)
memcpy(image.scanLine(y), videoData + (y * videoLinesize), linesize);
}
@@ -151,8 +195,13 @@ void ScreenshotObj::Copy()
}
}
void ScreenshotObj::Save()
void ScreenshotObj::saveToFile()
{
if (!outputToFile) {
QMetaObject::invokeMethod(this, &ScreenshotObj::onFinished, Qt::QueuedConnection);
return;
}
OBSBasic *main = OBSBasic::Get();
config_t *config = main->Config();
@@ -171,7 +220,10 @@ void ScreenshotObj::Save()
path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists,
GetFormatString(filenameFormat, "Screenshot", nullptr).c_str());
th = std::thread([this] { MuxAndFinish(); });
th = std::thread([this] {
muxFile();
QMetaObject::invokeMethod(this, &ScreenshotObj::onFinished, Qt::QueuedConnection);
});
}
#ifdef _WIN32
@@ -185,36 +237,44 @@ static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t
value.vt = VT_BOOL;
value.bVal = TRUE;
HRESULT hr = options->Write(1, &bag, &value);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = frameEncode->Initialize(options);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = frameEncode->SetSize(cx, cy);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = frameEncode->SetResolution(72, 72);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf;
hr = frameEncode->SetPixelFormat(&pixelFormat);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0)
if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) {
return E_FAIL;
}
hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = frameEncode->Commit();
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
return S_OK;
}
@@ -224,43 +284,50 @@ static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy)
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(factory.GetAddressOf()));
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
Microsoft::WRL::ComPtr<IWICStream> stream;
hr = factory->CreateStream(stream.GetAddressOf());
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = stream->InitializeFromFilename(path, GENERIC_WRITE);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
Microsoft::WRL::ComPtr<IWICBitmapEncoder> encoder = NULL;
hr = factory->CreateEncoder(GUID_ContainerFormatWmp, NULL, encoder.GetAddressOf());
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
Microsoft::WRL::ComPtr<IWICBitmapFrameEncode> frameEncode;
Microsoft::WRL::ComPtr<IPropertyBag2> options;
hr = encoder->CreateNewFrame(frameEncode.GetAddressOf(), options.GetAddressOf());
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
hr = SaveJxrImage(path, pixels, cx, cy, frameEncode.Get(), options.Get());
if (FAILED(hr))
if (FAILED(hr)) {
return hr;
}
encoder->Commit();
return S_OK;
}
#endif // #ifdef _WIN32
void ScreenshotObj::MuxAndFinish()
void ScreenshotObj::muxFile()
{
if (half_bytes.empty()) {
image.save(QT_UTF8(path.c_str()));
@@ -270,45 +337,50 @@ void ScreenshotObj::MuxAndFinish()
wchar_t *path_w = nullptr;
os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w);
if (path_w) {
SaveJxr(path_w, half_bytes.data(), cx, cy);
SaveJxr(path_w, half_bytes.data(), outputWidth, outputHeight);
bfree(path_w);
}
#endif // #ifdef _WIN32
#endif
}
deleteLater();
}
#define STAGE_SCREENSHOT 0
#define STAGE_DOWNLOAD 1
#define STAGE_COPY_AND_SAVE 2
#define STAGE_FINISH 3
static void ScreenshotTick(void *param, float)
void ScreenshotObj::onFinished()
{
ScreenshotObj *data = static_cast<ScreenshotObj *>(param);
if (data->stage == STAGE_FINISH) {
return;
if (th.joinable()) {
th.join();
}
obs_enter_graphics();
if (outputWidth && outputHeight) {
if (outputToFile) {
OBSBasic *main = OBSBasic::Get();
main->ShowStatusBarMessage(
QTStr("Basic.StatusBar.ScreenshotSavedTo").arg(QT_UTF8(path.c_str())));
main->lastScreenshot = path;
main->OnEvent(OBS_FRONTEND_EVENT_SCREENSHOT_TAKEN);
}
switch (data->stage) {
case STAGE_SCREENSHOT:
data->Screenshot();
break;
case STAGE_DOWNLOAD:
data->Download();
break;
case STAGE_COPY_AND_SAVE:
data->Copy();
QMetaObject::invokeMethod(data, "Save");
obs_remove_tick_callback(ScreenshotTick, data);
break;
emit imageReady(image.copy());
}
obs_leave_graphics();
this->deleteLater();
}
data->stage++;
void ScreenshotObj::setSize(QSize size)
{
customSize = size;
}
void ScreenshotObj::setSize(int width, int height)
{
setSize(QSize(width, height));
}
void ScreenshotObj::setSaveToFile(bool save)
{
outputToFile = save;
}
void ScreenshotObj::handleSave()
{
saveToFile();
}
+33 -9
View File
@@ -1,5 +1,6 @@
/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -30,10 +31,26 @@ class ScreenshotObj : public QObject {
public:
ScreenshotObj(obs_source_t *source);
~ScreenshotObj() override;
void Screenshot();
void Download();
void Copy();
void MuxAndFinish();
enum class Stage { Render, Download, Output, Finished };
void processStage();
void renderScreenshot();
void downloadData();
void copyData();
void saveToFile();
void muxFile();
void onFinished();
Stage stage() { return stage_; }
void setStage(Stage stage) { stage_ = stage; }
void setSize(QSize size);
void setSize(int width, int height);
void setSaveToFile(bool save);
private:
Stage stage_ = Stage::Render;
gs_texrender_t *texrender = nullptr;
gs_stagesurf_t *stagesurf = nullptr;
@@ -41,12 +58,19 @@ public:
std::string path;
QImage image;
std::vector<uint8_t> half_bytes;
uint32_t cx;
uint32_t cy;
QSize customSize;
uint32_t sourceWidth = 0;
uint32_t sourceHeight = 0;
uint32_t outputWidth = 0;
uint32_t outputHeight = 0;
std::thread th;
std::shared_ptr<QImage> imagePtr;
bool outputToFile = true;
int stage = 0;
signals:
void imageReady(QImage image);
public slots:
void Save();
private slots:
void handleSave();
};
+256
View File
@@ -0,0 +1,256 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
Lain Bailey <lain@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "ThumbnailManager.hpp"
#include <utility/ScreenshotObj.hpp>
#include <widgets/OBSBasic.hpp>
#include "display-helpers.hpp"
#include <QImageWriter>
constexpr int MIN_THUMBNAIL_UPDATE_INTERVAL_MS = 100;
constexpr int MIN_SOURCE_UPDATE_INTERVAL_MS = 5000;
ThumbnailItem::ThumbnailItem(std::string uuid, OBSSource source) : uuid(uuid), weakSource(OBSGetWeakRef(source)) {}
void ThumbnailItem::init(std::weak_ptr<ThumbnailItem> weakActiveItem)
{
auto thumbnailManager = OBSBasic::Get()->thumbnails();
if (!thumbnailManager) {
return;
}
auto it = thumbnailManager->cachedThumbnails.find(uuid);
if (it != thumbnailManager->cachedThumbnails.end()) {
auto &cachedItem = it->second;
pixmap = cachedItem.pixmap.value_or(QPixmap());
cachedItem.pixmap.reset();
cachedItem.weakActiveItem = std::move(weakActiveItem);
}
}
ThumbnailItem::~ThumbnailItem()
{
auto thumbnailManager = OBSBasic::Get()->thumbnails();
if (!thumbnailManager) {
return;
}
auto &cachedItem = thumbnailManager->cachedThumbnails[uuid];
cachedItem.pixmap = pixmap;
cachedItem.weakActiveItem.reset();
}
void ThumbnailItem::imageUpdated(QImage image)
{
QPixmap newPixmap;
if (!image.isNull()) {
newPixmap = QPixmap::fromImage(image);
}
pixmap = newPixmap;
emit updateThumbnail(pixmap);
}
void Thumbnail::thumbnailUpdated(QPixmap pixmap)
{
emit updateThumbnail(pixmap);
}
ThumbnailManager::ThumbnailManager(QObject *parent) : QObject(parent)
{
connect(&updateTimer, &QTimer::timeout, this, &ThumbnailManager::updateTick);
}
ThumbnailManager::~ThumbnailManager() {}
std::shared_ptr<Thumbnail> ThumbnailManager::getThumbnail(OBSSource source)
{
std::string uuid = obs_source_get_uuid(source);
for (auto it = thumbnails.begin(); it != thumbnails.end(); ++it) {
auto item = it->lock();
if (item && item->uuid == uuid) {
return std::make_shared<Thumbnail>(item);
}
}
std::shared_ptr<Thumbnail> thumbnail;
if ((obs_source_get_output_flags(source) & OBS_SOURCE_VIDEO) != 0) {
auto item = std::make_shared<ThumbnailItem>(uuid, source);
item->init(std::weak_ptr<ThumbnailItem>(item));
thumbnail = std::make_shared<Thumbnail>(item);
connect(item.get(), &ThumbnailItem::updateThumbnail, thumbnail.get(), &Thumbnail::thumbnailUpdated);
newThumbnails.push_back(std::weak_ptr<ThumbnailItem>(item));
}
updateIntervalChanged(thumbnails.size());
return thumbnail;
}
bool ThumbnailManager::updatePixmap(std::shared_ptr<ThumbnailItem> &sharedPointerItem)
{
ThumbnailItem *item = sharedPointerItem.get();
OBSSource source = OBSGetStrongRef(item->weakSource);
if (!source) {
return true;
}
QPixmap pixmap;
item->pixmap = pixmap;
if (source) {
uint32_t sourceWidth = obs_source_get_width(source);
uint32_t sourceHeight = obs_source_get_height(source);
if (sourceWidth == 0 || sourceHeight == 0) {
return true;
}
auto obj = new ScreenshotObj(source);
obj->setSaveToFile(false);
obj->setSize(Thumbnail::size);
connect(obj, &ScreenshotObj::imageReady, item, &ThumbnailItem::imageUpdated);
}
return true;
}
void ThumbnailManager::updateIntervalChanged(size_t newCount)
{
int intervalMS = MIN_THUMBNAIL_UPDATE_INTERVAL_MS;
if (newThumbnails.size() == 0 && newCount > 0) {
int count = (int)newCount;
intervalMS = MIN_SOURCE_UPDATE_INTERVAL_MS / count;
if (intervalMS < MIN_THUMBNAIL_UPDATE_INTERVAL_MS) {
intervalMS = MIN_THUMBNAIL_UPDATE_INTERVAL_MS;
}
}
updateTimer.start(intervalMS);
}
void ThumbnailManager::updateTick()
{
std::shared_ptr<ThumbnailItem> item;
bool changed = false;
bool newThumbnail = false;
while (newThumbnails.size() > 0) {
changed = true;
item = newThumbnails.front().lock();
newThumbnails.pop_front();
if (item) {
newThumbnail = true;
break;
}
}
if (!item) {
while (thumbnails.size() > 0) {
item = thumbnails.front().lock();
thumbnails.pop_front();
if (item) {
break;
} else {
changed = true;
}
}
}
if (changed && newThumbnails.size() == 0) {
updateIntervalChanged(thumbnails.size() + (item ? 1 : 0));
}
if (!item) {
return;
}
if (updatePixmap(item)) {
thumbnails.push_back(std::weak_ptr<ThumbnailItem>(item));
} else {
thumbnails.push_front(std::weak_ptr<ThumbnailItem>(item));
}
}
std::optional<QPixmap> ThumbnailManager::getCachedThumbnail(OBSSource source)
{
if (!source) {
return std::nullopt;
}
std::string uuid = obs_source_get_uuid(source);
auto it = cachedThumbnails.find(uuid);
if (it != cachedThumbnails.end()) {
auto &cachedItem = it->second;
if (cachedItem.pixmap.has_value()) {
return cachedItem.pixmap;
}
auto activeItem = cachedItem.weakActiveItem.lock();
return activeItem ? std::make_optional(activeItem->pixmap) : std::nullopt;
}
return std::nullopt;
}
void ThumbnailManager::preloadThumbnail(OBSSource source, QObject *object, std::function<void(QPixmap)> callback)
{
if (!source) {
return;
}
std::string uuid = obs_source_get_uuid(source);
if (cachedThumbnails.find(uuid) == cachedThumbnails.end()) {
uint32_t sourceWidth = obs_source_get_width(source);
uint32_t sourceHeight = obs_source_get_height(source);
cachedThumbnails[uuid].pixmap = QPixmap();
if (sourceWidth == 0 || sourceHeight == 0) {
return;
}
auto obj = new ScreenshotObj(source);
obj->setSaveToFile(false);
obj->setSize(Thumbnail::size);
QPointer<QObject> safeObject = qobject_cast<QObject *>(object);
connect(obj, &ScreenshotObj::imageReady, this, [=](QImage image) {
QPixmap pixmap;
if (!image.isNull()) {
pixmap = QPixmap::fromImage(image);
}
cachedThumbnails[uuid].pixmap = pixmap;
QMetaObject::invokeMethod(
safeObject,
[safeObject, callback, pixmap]() {
if (safeObject) {
callback(pixmap);
}
},
Qt::QueuedConnection);
});
}
}
+106
View File
@@ -0,0 +1,106 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
Lain Bailey <lain@obsproject.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <obs.hpp>
#include <QObject>
#include <QPixmap>
#include <QPointer>
#include <QTimer>
#include <deque>
#include <functional>
class ThumbnailItem : public QObject {
Q_OBJECT
friend class ThumbnailManager;
friend class Thumbnail;
std::string uuid;
OBSWeakSource weakSource;
QPixmap pixmap;
void init(std::weak_ptr<ThumbnailItem> weakActiveItem);
void imageUpdated(QImage image);
public:
ThumbnailItem(std::string uuid, OBSSource source);
~ThumbnailItem();
inline bool isNull() const { return !weakSource || obs_weak_source_expired(weakSource); }
inline const std::string &getUuid() const { return uuid; }
signals:
void updateThumbnail(QPixmap pixmap);
};
class Thumbnail : public QObject {
Q_OBJECT
friend class ThumbnailManager;
std::shared_ptr<ThumbnailItem> item;
private slots:
void thumbnailUpdated(QPixmap pixmap);
public:
inline Thumbnail(std::shared_ptr<ThumbnailItem> item) : item(item) {}
inline QPixmap getPixmap() const { return item->pixmap; }
static constexpr QSize size = {320, 180};
signals:
void updateThumbnail(QPixmap pixmap);
};
class ThumbnailManager : public QObject {
Q_OBJECT
friend class ThumbnailItem;
struct CachedItem {
std::optional<QPixmap> pixmap;
std::weak_ptr<ThumbnailItem> weakActiveItem;
};
std::deque<std::weak_ptr<ThumbnailItem>> newThumbnails;
std::deque<std::weak_ptr<ThumbnailItem>> thumbnails;
std::unordered_map<std::string, CachedItem> cachedThumbnails;
QTimer updateTimer;
bool updatePixmap(std::shared_ptr<ThumbnailItem> &item);
void updateTick();
void updateIntervalChanged(size_t newCount);
public:
explicit ThumbnailManager(QObject *parent = nullptr);
~ThumbnailManager();
std::shared_ptr<Thumbnail> getThumbnail(OBSSource source);
std::optional<QPixmap> getCachedThumbnail(OBSSource source);
void preloadThumbnail(OBSSource source, QObject *object, std::function<void(QPixmap)> callback);
private:
Q_DISABLE_COPY_MOVE(ThumbnailManager);
};
+2
View File
@@ -243,6 +243,8 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new
api = InitializeAPIInterface(this);
thumbnailManager = new ThumbnailManager(this);
ui->setupUi(this);
ui->previewDisabledWidget->setVisible(false);
+11 -4
View File
@@ -26,6 +26,7 @@
#include <utility/BasicOutputHandler.hpp>
#include <utility/OBSCanvas.hpp>
#include <utility/PreviewProgramSizeObserver.hpp>
#include <utility/ThumbnailManager.hpp>
#include <utility/VCamConfig.hpp>
#include <utility/platform.hpp>
#include <utility/undo_stack.hpp>
@@ -52,6 +53,7 @@ class OBSBasicAdvAudio;
class OBSBasicFilters;
class OBSBasicInteraction;
class OBSBasicProperties;
class OBSBasicSourceSelect;
class OBSBasicTransform;
class OBSLogViewer;
class OBSMissingFiles;
@@ -282,6 +284,8 @@ private:
// TODO: Remove, orphaned instance method
void LoadProject();
ThumbnailManager *thumbnailManager = nullptr;
public slots:
void close();
void UpdatePatronJson(const QString &text, const QString &error);
@@ -316,6 +320,8 @@ public:
inline bool isClosePromptOpen() { return isClosePromptOpen_; }
void closeWindow();
ThumbnailManager *thumbnails() const { return thumbnailManager; }
protected:
bool isReadyToClose();
bool promptToClose();
@@ -471,6 +477,9 @@ private:
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
signals:
void sourceUuidDropped(QString uuid);
/* -------------------------------------
* MARK: - OBSBasic_Hotkeys
* -------------------------------------
@@ -566,6 +575,7 @@ private:
QPointer<OBSBasicAdvAudio> advAudioWindow;
QPointer<OBSBasicFilters> filters;
QPointer<OBSAbout> about;
QPointer<OBSBasicSourceSelect> addWindow;
QPointer<OBSLogViewer> logView;
QPointer<QWidget> stats;
QPointer<QWidget> remux;
@@ -1168,11 +1178,8 @@ private:
static void SourceRemoved(void *data, calldata_t *params);
static void SourceRenamed(void *data, calldata_t *params);
void AddSource(const char *id);
QMenu *CreateAddSourcePopupMenu();
void AddSourcePopupMenu(const QPoint &pos);
private slots:
void AddSourceDialog();
void RenameSources(OBSSource source, QString newName, QString prevName);
void ReorderSources(OBSScene scene);
+9
View File
@@ -214,6 +214,10 @@ void OBSBasic::AddDropSource(const char *data, DropType image)
void OBSBasic::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) {
event->acceptProposedAction();
}
// refuse drops of our own widgets
if (event->source() != nullptr) {
event->setDropAction(Qt::IgnoreAction);
@@ -324,5 +328,10 @@ void OBSBasic::dropEvent(QDropEvent *event)
}
} else if (mimeData->hasText()) {
AddDropSource(QT_TO_UTF8(mimeData->text()), DropType_RawText);
} else if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) {
QString uuid = QString::fromUtf8(event->mimeData()->data("application/x-obs-source-uuid"));
emit sourceUuidDropped(uuid);
event->acceptProposedAction();
}
}
+44 -113
View File
@@ -49,6 +49,25 @@ void setHiddenInMixer(obs_source_t *source, bool hidden)
OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source);
obs_data_set_bool(priv_settings, "mixer_hidden", hidden);
}
std::string getNewSourceName(std::string_view name)
{
std::string newName{name};
int suffix = 1;
for (;;) {
OBSSourceAutoRelease existing_source = obs_get_source_by_name(newName.c_str());
if (!existing_source) {
break;
}
char nextName[256];
std::snprintf(nextName, sizeof(nextName), "%s (%d)", name.data(), ++suffix);
newName = nextName;
}
return newName;
}
} // namespace
static inline bool HasAudioDevices(const char *source_id)
@@ -200,8 +219,6 @@ void OBSBasic::SourceRenamed(void *data, calldata_t *params)
blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName);
}
extern char *get_new_source_name(const char *name, const char *format);
void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel)
{
bool disable = deviceId && strcmp(deviceId, "disabled") == 0;
@@ -222,11 +239,11 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, cons
}
} else if (!disable) {
BPtr<char> name = get_new_source_name(deviceDesc, "%s (%d)");
std::string name = getNewSourceName(deviceDesc);
settings = obs_data_create();
obs_data_set_string(settings, "device_id", deviceId);
source = obs_source_create(sourceId, name, settings, nullptr);
source = obs_source_create(sourceId, name.c_str(), settings, nullptr);
obs_set_output_source(channel, source);
}
@@ -570,10 +587,14 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
}
// Add new source
QPointer<QMenu> addSourceMenu = CreateAddSourcePopupMenu();
if (addSourceMenu) {
popup.addMenu(addSourceMenu);
popup.addSeparator();
QAction *addSource = popup.addAction(QTStr("AddSource"), this, SLOT(AddSourceDialog()));
popup.addAction(addSource);
popup.addSeparator();
if (!preview && !sourceSelected) {
QAction *addGroup = new QAction(QTStr("Basic.Main.NewGroup"), this);
connect(addGroup, &QAction::triggered, ui->sources, &SourceTree::AddGroup);
popup.addAction(addGroup);
}
// Preview menu entries
@@ -689,14 +710,12 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
// Source grouping
if (ui->sources->MultipleBaseSelected()) {
popup.addSeparator();
popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems);
} else if (ui->sources->GroupsSelected()) {
popup.addSeparator();
} else if (ui->sources->GroupsSelected()) {
popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups);
popup.addSeparator();
}
popup.addSeparator();
popup.addAction(ui->actionCopySource);
popup.addAction(ui->actionPasteRef);
@@ -776,115 +795,27 @@ static inline bool should_show_properties(obs_source_t *source, const char *id)
return true;
}
void OBSBasic::AddSource(const char *id)
void OBSBasic::AddSourceDialog()
{
if (id && *id) {
OBSBasicSourceSelect sourceSelect(this, id, undo_s);
sourceSelect.exec();
if (should_show_properties(sourceSelect.newSource, id)) {
CreatePropertiesWindow(sourceSelect.newSource);
}
}
}
QMenu *OBSBasic::CreateAddSourcePopupMenu()
{
const char *unversioned_type;
const char *type;
bool foundValues = false;
bool foundDeprecated = false;
size_t idx = 0;
QMenu *popup = new QMenu(QTStr("AddSource"), this);
QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup);
auto getActionAfter = [](QMenu *menu, const QString &name) {
QList<QAction *> actions = menu->actions();
for (QAction *menuAction : actions) {
if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0)
return menuAction;
}
return (QAction *)nullptr;
};
auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) {
QString qname = QT_UTF8(name);
QAction *popupItem = new QAction(qname, this);
connect(popupItem, &QAction::triggered, this, [this, type]() { AddSource(type); });
QIcon icon;
if (strcmp(type, "scene") == 0)
icon = GetSceneIcon();
else
icon = GetSourceIcon(type);
popupItem->setIcon(icon);
QAction *after = getActionAfter(popup, qname);
popup->insertAction(after, popupItem);
};
while (obs_enum_input_types2(idx++, &type, &unversioned_type)) {
const char *name = obs_source_get_display_name(type);
uint32_t caps = obs_get_source_output_flags(type);
if ((caps & OBS_SOURCE_CAP_DISABLED) != 0)
continue;
if ((caps & OBS_SOURCE_DEPRECATED) == 0) {
addSource(popup, unversioned_type, name);
} else {
addSource(deprecated, unversioned_type, name);
foundDeprecated = true;
}
foundValues = true;
}
addSource(popup, "scene", Str("Basic.Scene"));
popup->addSeparator();
QAction *addGroup = new QAction(QTStr("Group"), this);
addGroup->setIcon(GetGroupIcon());
connect(addGroup, &QAction::triggered, this, [this]() { AddSource("group"); });
popup->addAction(addGroup);
if (!foundDeprecated) {
delete deprecated;
deprecated = nullptr;
}
if (!foundValues) {
delete popup;
popup = nullptr;
} else if (foundDeprecated) {
popup->addSeparator();
popup->addMenu(deprecated);
}
return popup;
}
void OBSBasic::AddSourcePopupMenu(const QPoint &pos)
{
if (!GetCurrentScene()) {
// Tell the user he needs a scene first (help beginners).
OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"),
QTStr("Basic.Main.AddSourceHelp.Text"));
QAction *action = qobject_cast<QAction *>(sender());
if (!action) {
return;
}
QScopedPointer<QMenu> popup(CreateAddSourcePopupMenu());
if (popup)
popup->exec(pos);
if (addWindow) {
addWindow->close();
}
addWindow = new OBSBasicSourceSelect(this, undo_s);
addWindow->show();
addWindow->setAttribute(Qt::WA_DeleteOnClose, true);
connect(this, &OBSBasic::sourceUuidDropped, addWindow, &OBSBasicSourceSelect::sourceDropped);
}
void OBSBasic::on_actionAddSource_triggered()
{
AddSourcePopupMenu(QCursor::pos());
AddSourceDialog();
}
static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param)