mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-05-14 02:14:39 -04:00
frontend: Replace add source dropdown with dialog
Co-Authored-By: Lain <134130700+Lain-B@users.noreply.github.com>
This commit is contained in:
@@ -929,6 +929,7 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
|
||||
#endif
|
||||
|
||||
setDesktopFileName("com.obsproject.Studio");
|
||||
|
||||
pluginManager_ = std::make_unique<OBS::PluginManager>();
|
||||
}
|
||||
|
||||
@@ -1262,6 +1263,8 @@ bool OBSApp::OBSInit()
|
||||
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
thumbnailManager = new ThumbnailManager(this);
|
||||
|
||||
mainWindow = new OBSBasic();
|
||||
|
||||
mainWindow->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility/OBSTheme.hpp>
|
||||
#include <utility/NativeEventFilter.hpp>
|
||||
#include <utility/OBSTheme.hpp>
|
||||
#include <utility/ThumbnailManager.hpp>
|
||||
#include <widgets/OBSMainWindow.hpp>
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
@@ -90,6 +91,8 @@ private:
|
||||
|
||||
std::deque<obs_frontend_translate_ui_cb> translatorHooks;
|
||||
|
||||
ThumbnailManager *thumbnailManager = nullptr;
|
||||
|
||||
std::unique_ptr<OBS::PluginManager> pluginManager_;
|
||||
|
||||
bool UpdatePre22MultiviewLayout(const char *layout);
|
||||
@@ -229,6 +232,8 @@ public:
|
||||
|
||||
void loadAppModules(struct obs_module_failure_info &mfi);
|
||||
|
||||
ThumbnailManager *thumbnails() const { return thumbnailManager; }
|
||||
|
||||
// Plugin Manager Accessors
|
||||
void pluginManagerOpenDialog();
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -63,6 +67,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
|
||||
|
||||
@@ -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,12 @@ target_sources(
|
||||
utility/SimpleOutput.hpp
|
||||
utility/StartMultiTrackVideoStreamingGuard.hpp
|
||||
utility/SurfaceEventFilter.hpp
|
||||
utility/ThumbnailItem.cpp
|
||||
utility/ThumbnailItem.hpp
|
||||
utility/ThumbnailManager.cpp
|
||||
utility/ThumbnailManager.hpp
|
||||
utility/ThumbnailView.cpp
|
||||
utility/ThumbnailView.hpp
|
||||
utility/VCamConfig.hpp
|
||||
utility/audio-encoders.cpp
|
||||
utility/audio-encoders.hpp
|
||||
|
||||
134
frontend/components/FlowFrame.cpp
Normal file
134
frontend/components/FlowFrame.cpp
Normal 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
frontend/components/FlowFrame.hpp
Normal file
37
frontend/components/FlowFrame.hpp
Normal 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
frontend/components/FlowLayout.cpp
Normal file
170
frontend/components/FlowLayout.cpp
Normal 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
frontend/components/FlowLayout.hpp
Normal file
40
frontend/components/FlowLayout.hpp
Normal 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;
|
||||
};
|
||||
222
frontend/components/SourceSelectButton.cpp
Normal file
222
frontend/components/SourceSelectButton.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
/******************************************************************************
|
||||
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 <utility/ThumbnailView.hpp>
|
||||
#include <widgets/OBSBasic.hpp>
|
||||
|
||||
#include <QDrag>
|
||||
#include <QFrame>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionButton>
|
||||
|
||||
SourceSelectButton::SourceSelectButton(OBSWeakSource weak, QWidget *parent) : QAbstractButton(parent), weakSource(weak)
|
||||
{
|
||||
OBSSource source = OBSGetStrongRef(weak);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceUuid = obs_source_get_uuid(source);
|
||||
const char *sourceName = obs_source_get_name(source);
|
||||
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
setCheckable(true);
|
||||
setAccessibleName(sourceName);
|
||||
|
||||
QVBoxLayout *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);
|
||||
|
||||
thumbnail = App()->thumbnails()->createView(this, source);
|
||||
connect(thumbnail, &ThumbnailView::updated, this, &SourceSelectButton::updatePixmap);
|
||||
updatePixmap(thumbnail->getPixmap());
|
||||
|
||||
layout->addWidget(image);
|
||||
layout->addWidget(label);
|
||||
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
signalHandlers.reserve(2);
|
||||
signalHandlers.emplace_back(obs_source_get_signal_handler(source), "destroy",
|
||||
&SourceSelectButton::obsSourceRemoved, this);
|
||||
signalHandlers.emplace_back(obs_source_get_signal_handler(source), "rename",
|
||||
&SourceSelectButton::obsSourceRenamed, this);
|
||||
|
||||
connect(this, &QAbstractButton::pressed, this, &SourceSelectButton::buttonPressed);
|
||||
}
|
||||
|
||||
SourceSelectButton::~SourceSelectButton() {}
|
||||
|
||||
void SourceSelectButton::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter{this};
|
||||
|
||||
QStyleOptionButton option;
|
||||
option.initFrom(this);
|
||||
|
||||
if (isChecked()) {
|
||||
option.state |= QStyle::State_On;
|
||||
}
|
||||
if (isDown()) {
|
||||
option.state |= QStyle::State_Sunken;
|
||||
}
|
||||
if (hasFocus()) {
|
||||
option.state |= QStyle::State_HasFocus;
|
||||
}
|
||||
if (underMouse()) {
|
||||
option.state |= QStyle::State_MouseOver;
|
||||
}
|
||||
|
||||
style()->drawControl(QStyle::CE_PushButton, &option, &painter, this);
|
||||
}
|
||||
|
||||
void SourceSelectButton::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
QStyleOptionButton option;
|
||||
option.initFrom(this);
|
||||
|
||||
QRect contentRect = style()->subElementRect(QStyle::SE_PushButtonContents, &option, this);
|
||||
if (!contentRect.isValid()) {
|
||||
contentRect = rect();
|
||||
}
|
||||
|
||||
int left = contentRect.left();
|
||||
int top = contentRect.top();
|
||||
int right = rect().width() - contentRect.right() - 1;
|
||||
int bottom = rect().height() - contentRect.bottom() - 1;
|
||||
|
||||
left = std::max(0, left);
|
||||
top = std::max(0, top);
|
||||
right = std::max(0, right);
|
||||
bottom = std::max(0, bottom);
|
||||
|
||||
if (QLayout *layout = this->layout()) {
|
||||
layout->setContentsMargins(left, top, right, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSelectButton::enterEvent(QEnterEvent *)
|
||||
{
|
||||
update();
|
||||
thumbnail->requestUpdate();
|
||||
}
|
||||
|
||||
void SourceSelectButton::leaveEvent(QEvent *)
|
||||
{
|
||||
update();
|
||||
}
|
||||
|
||||
void SourceSelectButton::buttonPressed()
|
||||
{
|
||||
dragStartPosition = mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
void SourceSelectButton::obsSourceRemoved(void *data, calldata_t *)
|
||||
{
|
||||
QMetaObject::invokeMethod(static_cast<SourceSelectButton *>(data), &SourceSelectButton::handleSourceRemoved);
|
||||
}
|
||||
|
||||
void SourceSelectButton::obsSourceRenamed(void *data, calldata_t *params)
|
||||
{
|
||||
const char *newNamePtr = static_cast<const char *>(calldata_ptr(params, "new_name"));
|
||||
if (!newNamePtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(static_cast<SourceSelectButton *>(data), "handleSourceRenamed", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QString::fromUtf8(newNamePtr)));
|
||||
}
|
||||
|
||||
void SourceSelectButton::handleSourceRemoved()
|
||||
{
|
||||
emit sourceRemoved();
|
||||
}
|
||||
|
||||
void SourceSelectButton::handleSourceRenamed(QString name)
|
||||
{
|
||||
label->setText(name);
|
||||
}
|
||||
|
||||
void SourceSelectButton::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!(event->buttons() & Qt::LeftButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance() * 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMimeData *mimeData = new QMimeData;
|
||||
|
||||
mimeData->setData("application/x-obs-source-uuid", sourceUuid.data());
|
||||
|
||||
QDrag *drag = new QDrag(this);
|
||||
drag->setMimeData(mimeData);
|
||||
drag->setPixmap(this->grab());
|
||||
drag->exec(Qt::CopyAction);
|
||||
|
||||
if (isDown()) {
|
||||
setDown(false);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSelectButton::setThumbnailEnabled(bool enabled)
|
||||
{
|
||||
OBSSource source = OBSGetStrongRef(weakSource);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailEnabled != enabled) {
|
||||
thumbnailEnabled = enabled;
|
||||
|
||||
thumbnail->setEnabled(thumbnailEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSelectButton::updateThumbnail()
|
||||
{
|
||||
thumbnail->requestUpdate();
|
||||
}
|
||||
|
||||
void SourceSelectButton::updatePixmap(QPixmap pixmap)
|
||||
{
|
||||
if (!pixmap.isNull()) {
|
||||
image->setPixmap(
|
||||
pixmap.scaled(image->width(), image->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
}
|
||||
75
frontend/components/SourceSelectButton.hpp
Normal file
75
frontend/components/SourceSelectButton.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
/******************************************************************************
|
||||
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 ThumbnailView;
|
||||
|
||||
class SourceSelectButton : public QAbstractButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SourceSelectButton(OBSWeakSource weak, QWidget *parent = nullptr);
|
||||
~SourceSelectButton();
|
||||
|
||||
std::string_view uuid() const { return sourceUuid; };
|
||||
|
||||
void setThumbnailEnabled(bool enabled);
|
||||
void updateThumbnail();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void buttonPressed();
|
||||
|
||||
private:
|
||||
OBSWeakSource weakSource;
|
||||
QPointer<ThumbnailView> thumbnail;
|
||||
QPointer<QLabel> image;
|
||||
std::string sourceUuid;
|
||||
|
||||
std::vector<OBSSignal> signalHandlers;
|
||||
static void obsSourceRemoved(void *param, calldata_t *calldata);
|
||||
static void obsSourceRenamed(void *param, calldata_t *calldata);
|
||||
|
||||
QLabel *label = nullptr;
|
||||
bool thumbnailEnabled = true;
|
||||
|
||||
QPoint dragStartPosition;
|
||||
|
||||
private slots:
|
||||
void updatePixmap(QPixmap pixmap);
|
||||
void handleSourceRemoved();
|
||||
void handleSourceRenamed(QString name);
|
||||
|
||||
signals:
|
||||
void sourceRemoved();
|
||||
};
|
||||
@@ -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 may be removed in the future."
|
||||
|
||||
# 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"
|
||||
|
||||
@@ -2430,6 +2430,48 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
|
||||
border: 1px solid var(--input_border_hover);
|
||||
}
|
||||
|
||||
/* Add Source Dialog */
|
||||
SourceSelectButton {
|
||||
background: var(--grey5);
|
||||
padding: var(--padding_base) var(--padding_large);
|
||||
margin: var(--spacing_base);
|
||||
border-radius: var(--border_radius);
|
||||
border: 1px solid var(--grey3);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
SourceSelectButton QLabel {
|
||||
padding: var(--padding_large) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
SourceSelectButton #thumbnail {
|
||||
background: var(--grey6);
|
||||
border-radius: var(--border_radius);
|
||||
padding: 0;
|
||||
margin-top: var(--spacing_base);
|
||||
}
|
||||
|
||||
SourceSelectButton:hover {
|
||||
background-color: var(--button_bg_hover);
|
||||
}
|
||||
|
||||
SourceSelectButton:checked {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary_light);
|
||||
}
|
||||
|
||||
SourceSelectButton:focus,
|
||||
SourceSelectButton:checked:focus {
|
||||
border-color: var(--white3);
|
||||
}
|
||||
|
||||
SourceSelectButton:pressed,
|
||||
SourceSelectButton:pressed:hover {
|
||||
background-color: var(--button_bg_down);
|
||||
border-color: var(--button_border);
|
||||
}
|
||||
|
||||
/* Idian Widgets */
|
||||
idian--Group {
|
||||
border-radius: var(--border_radius);
|
||||
|
||||
@@ -201,3 +201,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -253,3 +261,8 @@ VolumeMeter {
|
||||
OBSBasicStats {
|
||||
background: var(--bg_window);
|
||||
}
|
||||
|
||||
/* Add Source Dialog */
|
||||
SourceSelectButton QPushButton {
|
||||
background-color: var(--grey7);
|
||||
}
|
||||
|
||||
@@ -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,14 @@
|
||||
|
||||
--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_light: var(--black3);
|
||||
--text_muted: var(--black4);
|
||||
|
||||
--text_disabled: var(--text_muted);
|
||||
@@ -33,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);
|
||||
|
||||
@@ -43,6 +47,13 @@
|
||||
--scrollbar_border: var(--grey7);
|
||||
}
|
||||
|
||||
/* --------------------- */
|
||||
/* General Styling Hints */
|
||||
|
||||
.button-primary:hover {
|
||||
border-color: var(--button_border_hover);
|
||||
}
|
||||
|
||||
/* Mute Button */
|
||||
|
||||
.btn-mute {
|
||||
@@ -397,3 +408,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);
|
||||
}
|
||||
|
||||
@@ -200,3 +200,9 @@ VolumeMeter {
|
||||
qproperty-majorTickColor: palette(window-text);
|
||||
qproperty-minorTickColor: palette(mid);
|
||||
}
|
||||
|
||||
/* Add Source Dialog */
|
||||
SourceSelectButton QPushButton,
|
||||
SourceSelectButton #thumbnail {
|
||||
border-color: var(--grey3);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,40 +18,79 @@
|
||||
|
||||
#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>
|
||||
|
||||
class OBSBasicSourceSelect : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
|
||||
const char *id;
|
||||
undo_stack &undo_s;
|
||||
|
||||
static bool EnumSources(void *data, obs_source_t *source);
|
||||
static bool EnumGroups(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 SourceAdded(OBSSource source);
|
||||
void SourceRemoved(OBSSource source);
|
||||
|
||||
public:
|
||||
OBSBasicSourceSelect(OBSBasic *parent, const char *id, undo_stack &undo_s);
|
||||
OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s);
|
||||
~OBSBasicSourceSelect();
|
||||
|
||||
OBSSource newSource;
|
||||
|
||||
static void SourcePaste(SourceCopyInfo &info, bool duplicate);
|
||||
static void sourcePaste(SourceCopyInfo &info, bool duplicate);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::OBSBasicSourceSelect> ui;
|
||||
QString selectedTypeId;
|
||||
undo_stack &undo_s;
|
||||
|
||||
QPointer<QButtonGroup> sourceButtons;
|
||||
|
||||
std::vector<OBSSignal> signalHandlers;
|
||||
static void obsSourceCreated(void *param, calldata_t *calldata);
|
||||
static void obsSourceRemoved(void *param, calldata_t *calldata);
|
||||
|
||||
std::vector<OBSWeakSource> weakSources;
|
||||
|
||||
QPointer<FlowLayout> existingFlowLayout = nullptr;
|
||||
|
||||
void refreshSources();
|
||||
void updateExistingSources(int limit = 0);
|
||||
|
||||
static bool enumSourcesCallback(void *data, obs_source_t *source);
|
||||
|
||||
void rebuildSourceTypeList();
|
||||
|
||||
int lastSelectedIndex = -1;
|
||||
std::vector<std::string> selectedItems;
|
||||
void addSelectedItem(const std::string &uuid);
|
||||
void removeSelectedItem(const std::string &uuid);
|
||||
void clearSelectedItems();
|
||||
|
||||
SourceSelectButton *findButtonForUuid(const std::string &uuid);
|
||||
|
||||
void createNew();
|
||||
void addExisting(const std::string &uuid, bool visible);
|
||||
|
||||
void updateButtonVisibility();
|
||||
|
||||
signals:
|
||||
void sourcesUpdated();
|
||||
void selectedItemsChanged();
|
||||
|
||||
public slots:
|
||||
void on_createNewSource_clicked(bool checked);
|
||||
void addSelectedSources();
|
||||
|
||||
void handleSourceCreated();
|
||||
void handleSourceRemoved(QString uuid);
|
||||
|
||||
void sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *previous);
|
||||
|
||||
void sourceButtonToggled(QAbstractButton *button, bool checked);
|
||||
void sourceDropped(QString uuid);
|
||||
};
|
||||
|
||||
@@ -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>1010</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
frontend/utility/ResizeSignaler.hpp
Normal file
39
frontend/utility/ResizeSignaler.hpp
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
{
|
||||
ScreenshotObj *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{0};
|
||||
int y{0};
|
||||
int scaledWidth{0};
|
||||
int scaledHeight{0};
|
||||
float scale{0.0};
|
||||
|
||||
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,44 +134,75 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
gs_stagesurface_unmap(stagesurf);
|
||||
}
|
||||
}
|
||||
|
||||
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 +221,10 @@ void ScreenshotObj::Save()
|
||||
path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists,
|
||||
GetFormatString(filenameFormat, "Screenshot", nullptr).c_str());
|
||||
|
||||
th = std::thread([this] { MuxAndFinish(); });
|
||||
thread = std::thread([this] {
|
||||
muxFile();
|
||||
QMetaObject::invokeMethod(this, &ScreenshotObj::onFinished, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -185,36 +238,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 +285,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 +338,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 (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
obs_enter_graphics();
|
||||
if (outputWidth > 0 && outputHeight > 0) {
|
||||
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();
|
||||
|
||||
data->stage++;
|
||||
this->deleteLater();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
std::thread th;
|
||||
QSize customSize;
|
||||
uint32_t sourceWidth = 0;
|
||||
uint32_t sourceHeight = 0;
|
||||
uint32_t outputWidth = 0;
|
||||
uint32_t outputHeight = 0;
|
||||
|
||||
int stage = 0;
|
||||
std::thread thread;
|
||||
std::shared_ptr<QImage> imagePtr;
|
||||
bool outputToFile = true;
|
||||
|
||||
public slots:
|
||||
void Save();
|
||||
signals:
|
||||
void imageReady(QImage image);
|
||||
|
||||
private slots:
|
||||
void handleSave();
|
||||
};
|
||||
|
||||
196
frontend/utility/ThumbnailItem.cpp
Normal file
196
frontend/utility/ThumbnailItem.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
/******************************************************************************
|
||||
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 "ThumbnailItem.hpp"
|
||||
|
||||
#include <utility/ScreenshotObj.hpp>
|
||||
#include <utility/ThumbnailView.hpp>
|
||||
#include <widgets/OBSBasic.hpp>
|
||||
|
||||
#include <QIcon>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
static constexpr int kDefaultWidth = 320;
|
||||
static constexpr int kDefaultHeight = 180;
|
||||
|
||||
namespace {
|
||||
QPixmap getDefaultThumbnail(obs_source_t *source)
|
||||
{
|
||||
const char *id = obs_source_get_id(source);
|
||||
OBSBasic *main = OBSBasic::Get();
|
||||
if (main && id) {
|
||||
QIcon icon = OBSBasic::Get()->GetSourceIcon(id);
|
||||
QPixmap iconPixmap = icon.pixmap(90, 90);
|
||||
|
||||
QPixmap defaultPixmap(kDefaultWidth, kDefaultHeight);
|
||||
defaultPixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&defaultPixmap);
|
||||
|
||||
const int x = (defaultPixmap.width() - iconPixmap.width()) / 2;
|
||||
const int y = (defaultPixmap.height() - iconPixmap.height()) / 2;
|
||||
|
||||
painter.drawPixmap(x, y, iconPixmap);
|
||||
|
||||
return defaultPixmap;
|
||||
}
|
||||
|
||||
return QPixmap();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ThumbnailItem::ThumbnailItem(const std::string &uuid, ThumbnailManager *manager) : QObject(manager), uuid(uuid)
|
||||
{
|
||||
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
weakSource = OBSGetWeakRef(source);
|
||||
|
||||
std::optional<QPixmap> cachedPixmap = manager->getCachedPixmap(uuid);
|
||||
if (cachedPixmap.has_value()) {
|
||||
setPixmap(cachedPixmap.value());
|
||||
isDefaultPixmap_ = false;
|
||||
} else {
|
||||
setPixmap(getDefaultThumbnail(source));
|
||||
}
|
||||
|
||||
if ((obs_source_get_output_flags(source) & OBS_SOURCE_VIDEO) != 0) {
|
||||
isVideoSource = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailItem::updatePixmapFromImage(const QImage &image)
|
||||
{
|
||||
if (!image.isNull()) {
|
||||
setPixmap(QPixmap::fromImage(image));
|
||||
isDefaultPixmap_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ThumbnailItem::shouldUpdate() const
|
||||
{
|
||||
return isValid() && (viewCount > 0) && (enabledCount > 0);
|
||||
}
|
||||
|
||||
QPixmap ThumbnailItem::getPixmap() const
|
||||
{
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
void ThumbnailItem::setPixmap(QPixmap pixmap)
|
||||
{
|
||||
this->pixmap = std::move(pixmap);
|
||||
|
||||
emit pixmapUpdated(this->pixmap);
|
||||
}
|
||||
|
||||
void ThumbnailItem::incrementViewCount()
|
||||
{
|
||||
++viewCount;
|
||||
}
|
||||
|
||||
ThumbnailView *ThumbnailItem::createView(QObject *parent)
|
||||
{
|
||||
auto *view = new ThumbnailView(parent, this);
|
||||
|
||||
incrementViewCount();
|
||||
if (view->isEnabled()) {
|
||||
incrementEnabledCount();
|
||||
}
|
||||
|
||||
// Connect ThumbnailView signals
|
||||
connect(view, &QObject::destroyed, this, [this, view]() {
|
||||
decrementViewCount();
|
||||
if (view->isEnabled()) {
|
||||
decrementEnabledCount();
|
||||
}
|
||||
});
|
||||
connect(view, &ThumbnailView::enabledChanged, this, [this](bool enabled) {
|
||||
if (enabled) {
|
||||
incrementEnabledCount();
|
||||
} else {
|
||||
decrementEnabledCount();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
void ThumbnailItem::decrementViewCount()
|
||||
{
|
||||
if (viewCount > 0) {
|
||||
--viewCount;
|
||||
}
|
||||
|
||||
if (viewCount <= 0) {
|
||||
emit noViewsRemaining();
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailItem::incrementEnabledCount()
|
||||
{
|
||||
++enabledCount;
|
||||
}
|
||||
|
||||
void ThumbnailItem::decrementEnabledCount()
|
||||
{
|
||||
if (enabledCount > 0) {
|
||||
--enabledCount;
|
||||
}
|
||||
}
|
||||
|
||||
bool ThumbnailItem::update()
|
||||
{
|
||||
if (!isVideoSource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shouldUpdate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OBSSource source = OBSGetStrongRef(weakSource);
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QPixmap pixmap;
|
||||
if (this->pixmap.isNull()) {
|
||||
this->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 false;
|
||||
}
|
||||
|
||||
auto *obj = new ScreenshotObj(source);
|
||||
obj->setSaveToFile(false);
|
||||
obj->setSize(kDefaultWidth, kDefaultHeight);
|
||||
|
||||
connect(obj, &ScreenshotObj::imageReady, this, &ThumbnailItem::updatePixmapFromImage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
70
frontend/utility/ThumbnailItem.hpp
Normal file
70
frontend/utility/ThumbnailItem.hpp
Normal file
@@ -0,0 +1,70 @@
|
||||
/******************************************************************************
|
||||
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>
|
||||
|
||||
class ThumbnailManager;
|
||||
class ThumbnailView;
|
||||
|
||||
class ThumbnailItem : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
std::string uuid;
|
||||
OBSWeakSource weakSource;
|
||||
QPixmap pixmap;
|
||||
bool isVideoSource = false;
|
||||
|
||||
int viewCount{0};
|
||||
int enabledCount{0};
|
||||
|
||||
bool isDefaultPixmap_ = true;
|
||||
void updatePixmapFromImage(const QImage &image);
|
||||
[[nodiscard]] bool shouldUpdate() const;
|
||||
|
||||
public:
|
||||
ThumbnailItem(const std::string &uuid, ThumbnailManager *manager);
|
||||
~ThumbnailItem() = default;
|
||||
|
||||
bool update();
|
||||
[[nodiscard]] QPixmap getPixmap() const;
|
||||
void setPixmap(QPixmap pixmap);
|
||||
|
||||
[[nodiscard]] bool isDefaultPixmap() const { return isDefaultPixmap_; }
|
||||
[[nodiscard]] bool isValid() const
|
||||
{
|
||||
return isVideoSource && weakSource && !obs_weak_source_expired(weakSource);
|
||||
}
|
||||
[[nodiscard]] const std::string &getUuid() const { return uuid; }
|
||||
|
||||
ThumbnailView *createView(QObject *parent);
|
||||
|
||||
public slots:
|
||||
void incrementViewCount();
|
||||
void decrementViewCount();
|
||||
void incrementEnabledCount();
|
||||
void decrementEnabledCount();
|
||||
|
||||
signals:
|
||||
void pixmapUpdated(QPixmap pixmap);
|
||||
void noViewsRemaining();
|
||||
};
|
||||
268
frontend/utility/ThumbnailManager.cpp
Normal file
268
frontend/utility/ThumbnailManager.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
/******************************************************************************
|
||||
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/ThumbnailView.hpp>
|
||||
#include <widgets/OBSBasic.hpp>
|
||||
|
||||
#include "display-helpers.hpp"
|
||||
|
||||
#include <QImageWriter>
|
||||
|
||||
constexpr int kMinimumThumbnailUpdateInterval = 100;
|
||||
constexpr int kThumbnailUpdateInterval = 5000;
|
||||
|
||||
namespace {
|
||||
bool updateItem(ThumbnailItem *item)
|
||||
{
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item->update();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ThumbnailManager::ThumbnailCache::put(const std::string &key, const QPixmap &value)
|
||||
{
|
||||
auto it = cacheMap.find(key);
|
||||
if (it != cacheMap.end()) {
|
||||
cacheList.erase(it->second);
|
||||
cacheMap.erase(it);
|
||||
}
|
||||
cacheList.emplace_front(key, value);
|
||||
cacheMap[key] = cacheList.begin();
|
||||
if (cacheMap.size() > maxSize) {
|
||||
const CacheEntry &lastEntry = cacheList.back();
|
||||
cacheMap.erase(lastEntry.first);
|
||||
cacheList.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QPixmap> ThumbnailManager::ThumbnailCache::get(const std::string &key)
|
||||
{
|
||||
auto it = cacheMap.find(key);
|
||||
if (it == cacheMap.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto entry = it->second;
|
||||
return entry->second;
|
||||
}
|
||||
|
||||
ThumbnailManager::ThumbnailManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
elapsedTimer.start();
|
||||
connect(&updateTimer, &QTimer::timeout, this, &ThumbnailManager::updateTick);
|
||||
updateTickInterval(kMinimumThumbnailUpdateInterval);
|
||||
|
||||
signalHandlers.emplace_back(obs_get_signal_handler(), "source_destroy", &ThumbnailManager::obsSourceRemoved,
|
||||
this);
|
||||
}
|
||||
|
||||
ThumbnailView *ThumbnailManager::createView(QWidget *parent, obs_source_t *source)
|
||||
{
|
||||
if (!source) {
|
||||
return new ThumbnailView(parent, nullptr);
|
||||
}
|
||||
|
||||
const char *uuidPointer = obs_source_get_uuid(source);
|
||||
if (!uuidPointer) {
|
||||
return new ThumbnailView(parent, nullptr);
|
||||
}
|
||||
|
||||
std::string uuid{uuidPointer};
|
||||
|
||||
ThumbnailItem *item = getThumbnailItem(uuid);
|
||||
ThumbnailView *view = item->createView(parent);
|
||||
|
||||
connect(view, &ThumbnailView::updateRequested, this, [this](const std::string &uuid) {
|
||||
bool updateImmediately = true;
|
||||
addToPriorityQueue(uuid, updateImmediately);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
std::optional<QPixmap> ThumbnailManager::getCachedPixmap(const std::string &uuid)
|
||||
{
|
||||
return thumbnailCache.get(uuid);
|
||||
}
|
||||
|
||||
void ThumbnailManager::createThumbnailItem(const std::string &uuid)
|
||||
{
|
||||
QPointer<ThumbnailItem> item = new ThumbnailItem(uuid, this);
|
||||
thumbnailList[uuid] = item;
|
||||
|
||||
if (item->isDefaultPixmap()) {
|
||||
updateQueue.emplace_front(uuid);
|
||||
updateTickInterval(kMinimumThumbnailUpdateInterval);
|
||||
} else {
|
||||
updateQueue.emplace_back(uuid);
|
||||
}
|
||||
|
||||
connect(item, &ThumbnailItem::noViewsRemaining, this, [this, uuid]() { deleteItemById(uuid); });
|
||||
}
|
||||
|
||||
ThumbnailItem *ThumbnailManager::getThumbnailItem(const std::string &uuid)
|
||||
{
|
||||
if (thumbnailList.find(uuid) == thumbnailList.end()) {
|
||||
createThumbnailItem(uuid);
|
||||
}
|
||||
|
||||
return thumbnailList[uuid];
|
||||
}
|
||||
|
||||
void ThumbnailManager::obsSourceRemoved(void *data, calldata_t *params)
|
||||
{
|
||||
auto *source = static_cast<obs_source_t *>(calldata_ptr(params, "source"));
|
||||
const char *uuidPointer = obs_source_get_uuid(source);
|
||||
|
||||
if (!uuidPointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *manager = static_cast<ThumbnailManager *>(data);
|
||||
QMetaObject::invokeMethod(manager, "deleteItemById", Qt::QueuedConnection, Q_ARG(std::string, uuidPointer));
|
||||
}
|
||||
|
||||
void ThumbnailManager::updateTickInterval(int newInterval)
|
||||
{
|
||||
if (updateTimer.interval() != newInterval) {
|
||||
elapsedTimer.restart();
|
||||
updateTimer.start(newInterval);
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailManager::updateNextItem(size_t cycleDepth)
|
||||
{
|
||||
if (thumbnailList.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPointer<ThumbnailItem> item;
|
||||
bool quickUpdate = false;
|
||||
|
||||
if (!priorityQueue.empty()) {
|
||||
std::string uuid = priorityQueue.front();
|
||||
priorityQueue.pop_front();
|
||||
|
||||
item = thumbnailList[uuid];
|
||||
updateQueue.emplace_back(std::move(uuid));
|
||||
|
||||
if (!updateItem(item) && cycleDepth < thumbnailList.size()) {
|
||||
updateNextItem(cycleDepth + 1);
|
||||
return;
|
||||
}
|
||||
} else if (!updateQueue.empty()) {
|
||||
std::string uuid = updateQueue.front();
|
||||
|
||||
updateQueue.pop_front();
|
||||
item = thumbnailList[uuid];
|
||||
updateQueue.emplace_back(std::move(uuid));
|
||||
|
||||
if (item->isDefaultPixmap()) {
|
||||
quickUpdate = true;
|
||||
}
|
||||
|
||||
if (!updateItem(item) && cycleDepth < thumbnailList.size()) {
|
||||
updateNextItem(cycleDepth + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int nextIntervalMS = kMinimumThumbnailUpdateInterval;
|
||||
if (!priorityQueue.empty() && !quickUpdate) {
|
||||
nextIntervalMS = kThumbnailUpdateInterval;
|
||||
}
|
||||
|
||||
updateTickInterval(nextIntervalMS);
|
||||
}
|
||||
|
||||
void ThumbnailManager::updateTick()
|
||||
{
|
||||
updateNextItem();
|
||||
}
|
||||
|
||||
void ThumbnailManager::removeIdFromQueues(const std::string &uuid)
|
||||
{
|
||||
auto it = std::find(updateQueue.begin(), updateQueue.end(), uuid);
|
||||
if (it != updateQueue.end()) {
|
||||
updateQueue.erase(it);
|
||||
}
|
||||
|
||||
it = std::find(priorityQueue.begin(), priorityQueue.end(), uuid);
|
||||
if (it != priorityQueue.end()) {
|
||||
priorityQueue.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailManager::addToPriorityQueue(const std::string &uuid, bool immediate)
|
||||
{
|
||||
// Skip if uuid is already at the front of the priority queue
|
||||
if (!priorityQueue.empty() && priorityQueue[0] == uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeIdFromQueues(uuid);
|
||||
|
||||
if (immediate) {
|
||||
priorityQueue.emplace_front(uuid);
|
||||
|
||||
qint64 elapsed = elapsedTimer.elapsed();
|
||||
if (elapsed > kMinimumThumbnailUpdateInterval) {
|
||||
updateTick();
|
||||
}
|
||||
} else {
|
||||
priorityQueue.emplace_back(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailManager::addItemToCache(std::string &uuid, QPixmap &pixmap)
|
||||
{
|
||||
if (pixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
thumbnailCache.put(uuid, pixmap);
|
||||
}
|
||||
|
||||
void ThumbnailManager::deleteItem(ThumbnailItem *item)
|
||||
{
|
||||
deleteItemById(item->getUuid());
|
||||
item->deleteLater();
|
||||
}
|
||||
|
||||
void ThumbnailManager::deleteItemById(const std::string &uuid)
|
||||
{
|
||||
auto entry = thumbnailList.find(uuid);
|
||||
if (entry != thumbnailList.end()) {
|
||||
removeIdFromQueues(uuid);
|
||||
|
||||
QPointer<ThumbnailItem> item = entry->second;
|
||||
|
||||
if (item && !item->isDefaultPixmap()) {
|
||||
std::string uuid = item->getUuid();
|
||||
QPixmap pixmap = item->getPixmap();
|
||||
addItemToCache(uuid, pixmap);
|
||||
}
|
||||
|
||||
thumbnailList.erase(uuid);
|
||||
}
|
||||
}
|
||||
91
frontend/utility/ThumbnailManager.hpp
Normal file
91
frontend/utility/ThumbnailManager.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/******************************************************************************
|
||||
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 <utility/ScreenshotObj.hpp>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
class ThumbnailItem;
|
||||
class ThumbnailView;
|
||||
|
||||
class ThumbnailManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
std::vector<OBSSignal> signalHandlers;
|
||||
static void obsSourceRemoved(void *data, calldata_t *params);
|
||||
|
||||
struct ThumbnailCache {
|
||||
size_t maxSize;
|
||||
|
||||
using CacheEntry = std::pair<std::string, QPixmap>;
|
||||
|
||||
std::list<CacheEntry> cacheList;
|
||||
std::unordered_map<std::string, std::list<CacheEntry>::iterator> cacheMap;
|
||||
|
||||
explicit ThumbnailCache(size_t maxSize) : maxSize(maxSize) {}
|
||||
|
||||
void put(const std::string &key, const QPixmap &value);
|
||||
std::optional<QPixmap> get(const std::string &key);
|
||||
};
|
||||
|
||||
std::deque<std::string> priorityQueue;
|
||||
std::deque<std::string> updateQueue;
|
||||
std::unordered_map<std::string, ThumbnailItem *> thumbnailList;
|
||||
static constexpr int kMaxThumbnails = 64;
|
||||
ThumbnailCache thumbnailCache{kMaxThumbnails};
|
||||
|
||||
QTimer updateTimer;
|
||||
QElapsedTimer elapsedTimer;
|
||||
|
||||
public:
|
||||
explicit ThumbnailManager(QObject *parent = nullptr);
|
||||
~ThumbnailManager() = default;
|
||||
|
||||
ThumbnailView *createView(QWidget *parent, obs_source_t *source);
|
||||
std::optional<QPixmap> getCachedPixmap(const std::string &uuid);
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY_MOVE(ThumbnailManager);
|
||||
|
||||
void updateNextItem(size_t cycleDepth = 0);
|
||||
|
||||
void removeIdFromQueues(const std::string &uuid);
|
||||
void updateTickInterval(int newInterval);
|
||||
|
||||
void createThumbnailItem(const std::string &uuid);
|
||||
ThumbnailItem *getThumbnailItem(const std::string &uuid);
|
||||
|
||||
private slots:
|
||||
void updateTick();
|
||||
void addToPriorityQueue(const std::string &uuid, bool immediate = false);
|
||||
void addItemToCache(std::string &uuid, QPixmap &pixmap);
|
||||
void deleteItem(ThumbnailItem *item);
|
||||
void deleteItemById(const std::string &uuid);
|
||||
};
|
||||
65
frontend/utility/ThumbnailView.cpp
Normal file
65
frontend/utility/ThumbnailView.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/******************************************************************************
|
||||
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 "ThumbnailView.hpp"
|
||||
|
||||
ThumbnailView::ThumbnailView(QObject *parent, const QPointer<ThumbnailItem> &item) : QObject(parent)
|
||||
{
|
||||
if (!parent) {
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
uuid = item->getUuid();
|
||||
setPixmap(item->getPixmap());
|
||||
|
||||
connect(item, &ThumbnailItem::pixmapUpdated, this, &ThumbnailView::setPixmap);
|
||||
|
||||
if (!item->isValid()) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailView::setEnabled(bool enabled)
|
||||
{
|
||||
if (this->enabled != enabled) {
|
||||
this->enabled = enabled;
|
||||
|
||||
emit enabledChanged(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void ThumbnailView::requestUpdate()
|
||||
{
|
||||
if (!this->enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit updateRequested(uuid);
|
||||
}
|
||||
|
||||
void ThumbnailView::setPixmap(QPixmap pixmap)
|
||||
{
|
||||
this->pixmap = std::move(pixmap);
|
||||
|
||||
emit updated(this->pixmap);
|
||||
}
|
||||
54
frontend/utility/ThumbnailView.hpp
Normal file
54
frontend/utility/ThumbnailView.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
/******************************************************************************
|
||||
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 <utility/ThumbnailItem.hpp>
|
||||
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
#include <QPointer>
|
||||
|
||||
class ThumbnailView : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
QPixmap pixmap{};
|
||||
bool enabled = true;
|
||||
|
||||
public:
|
||||
ThumbnailView(QObject *parent, const QPointer<ThumbnailItem> &item);
|
||||
~ThumbnailView() = default;
|
||||
|
||||
std::string uuid;
|
||||
[[nodiscard]] QPixmap getPixmap() const { return pixmap; }
|
||||
|
||||
[[nodiscard]] bool isEnabled() const { return enabled; }
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
void requestUpdate();
|
||||
|
||||
signals:
|
||||
void updated(QPixmap pixmap);
|
||||
void enabledChanged(bool enabled);
|
||||
void updateRequested(std::string &uuid);
|
||||
|
||||
public slots:
|
||||
void setPixmap(QPixmap pixmap);
|
||||
};
|
||||
@@ -52,6 +52,7 @@ class OBSBasicAdvAudio;
|
||||
class OBSBasicFilters;
|
||||
class OBSBasicInteraction;
|
||||
class OBSBasicProperties;
|
||||
class OBSBasicSourceSelect;
|
||||
class OBSBasicTransform;
|
||||
class OBSLogViewer;
|
||||
class OBSMissingFiles;
|
||||
@@ -471,6 +472,9 @@ private:
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
signals:
|
||||
void sourceUuidDropped(QString uuid);
|
||||
|
||||
/* -------------------------------------
|
||||
* MARK: - OBSBasic_Hotkeys
|
||||
* -------------------------------------
|
||||
@@ -566,6 +570,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 +1173,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);
|
||||
|
||||
@@ -134,7 +134,7 @@ void OBSBasic::on_actionPasteRef_triggered()
|
||||
continue;
|
||||
}
|
||||
|
||||
OBSBasicSourceSelect::SourcePaste(copyInfo, false);
|
||||
OBSBasicSourceSelect::sourcePaste(copyInfo, false);
|
||||
RefreshSources(scene);
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ void OBSBasic::on_actionPasteDup_triggered()
|
||||
|
||||
for (size_t i = clipboard.size(); i > 0; i--) {
|
||||
SourceCopyInfo ©Info = clipboard[i - 1];
|
||||
OBSBasicSourceSelect::SourcePaste(copyInfo, true);
|
||||
OBSBasicSourceSelect::sourcePaste(copyInfo, true);
|
||||
RefreshSources(GetCurrentScene());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -198,8 +217,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;
|
||||
@@ -220,11 +237,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);
|
||||
}
|
||||
@@ -568,10 +585,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, &OBSBasic::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
|
||||
@@ -687,14 +708,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);
|
||||
@@ -774,115 +793,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)
|
||||
|
||||
Reference in New Issue
Block a user