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