From 515b4313dc67f079b6525bc84d22f537db7251c1 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Sat, 27 Sep 2025 00:53:02 -0400 Subject: [PATCH 1/4] frontend: Replace add source dropdown with dialog Co-Authored-By: Lain <134130700+Lain-B@users.noreply.github.com> --- frontend/cmake/ui-components.cmake | 6 + frontend/cmake/ui-utility.cmake | 2 + frontend/components/FlowFrame.cpp | 134 ++++ frontend/components/FlowFrame.hpp | 37 + frontend/components/FlowLayout.cpp | 176 +++++ frontend/components/FlowLayout.hpp | 50 ++ frontend/components/SourceSelectButton.cpp | 184 +++++ frontend/components/SourceSelectButton.hpp | 68 ++ frontend/data/locale/en-US.ini | 14 +- frontend/data/themes/Yami.obt | 28 + frontend/dialogs/OBSBasicSourceSelect.cpp | 741 +++++++++++++++------ frontend/dialogs/OBSBasicSourceSelect.hpp | 59 +- frontend/forms/OBSBasicSourceSelect.ui | 720 +++++++++++++++++--- frontend/utility/ScreenshotObj.cpp | 236 ++++--- frontend/utility/ScreenshotObj.hpp | 42 +- frontend/utility/ThumbnailManager.cpp | 238 +++++++ frontend/utility/ThumbnailManager.hpp | 107 +++ frontend/widgets/OBSBasic.cpp | 2 + frontend/widgets/OBSBasic.hpp | 15 +- frontend/widgets/OBSBasic_Dropfiles.cpp | 9 + frontend/widgets/OBSBasic_SceneItems.cpp | 132 +--- shared/qt/wrappers/qt-wrappers.cpp | 8 + shared/qt/wrappers/qt-wrappers.hpp | 13 + 23 files changed, 2519 insertions(+), 502 deletions(-) create mode 100644 frontend/components/FlowFrame.cpp create mode 100644 frontend/components/FlowFrame.hpp create mode 100644 frontend/components/FlowLayout.cpp create mode 100644 frontend/components/FlowLayout.hpp create mode 100644 frontend/components/SourceSelectButton.cpp create mode 100644 frontend/components/SourceSelectButton.hpp create mode 100644 frontend/utility/ThumbnailManager.cpp create mode 100644 frontend/utility/ThumbnailManager.hpp diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index ad0506a0858ff2..d09bd1e03f76f2 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -30,6 +30,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 @@ -57,6 +61,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 diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 0e1193c4110be4..6b2db164465cdf 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -65,6 +65,8 @@ target_sources( utility/StartMultiTrackVideoStreamingGuard.hpp utility/SurfaceEventFilter.hpp utility/system-info.hpp + utility/ThumbnailManager.cpp + utility/ThumbnailManager.hpp utility/undo_stack.cpp utility/undo_stack.hpp utility/VCamConfig.hpp diff --git a/frontend/components/FlowFrame.cpp b/frontend/components/FlowFrame.cpp new file mode 100644 index 00000000000000..d0da758b39e7c0 --- /dev/null +++ b/frontend/components/FlowFrame.cpp @@ -0,0 +1,134 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#include + +#include +#include + +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(scrollParent); + if (scrollArea) { + scrollArea->ensureWidgetVisible(nextFocus, 20, 20); + break; + } + scrollParent = scrollParent->parentWidget(); + } + } +} diff --git a/frontend/components/FlowFrame.hpp b/frontend/components/FlowFrame.hpp new file mode 100644 index 00000000000000..0ad1164ce6442d --- /dev/null +++ b/frontend/components/FlowFrame.hpp @@ -0,0 +1,37 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#pragma once + +#include "FlowLayout.hpp" + +#include + +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; +}; diff --git a/frontend/components/FlowLayout.cpp b/frontend/components/FlowLayout.cpp new file mode 100644 index 00000000000000..659e906c17d212 --- /dev/null +++ b/frontend/components/FlowLayout.cpp @@ -0,0 +1,176 @@ +/****************************************************************************** + Example provided by Qt + + + 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 . +******************************************************************************/ + +#include "FlowLayout.hpp" + +#include + +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, top, right, 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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(parent)->spacing(); + } +} diff --git a/frontend/components/FlowLayout.hpp b/frontend/components/FlowLayout.hpp new file mode 100644 index 00000000000000..ec12cc9bd24ce0 --- /dev/null +++ b/frontend/components/FlowLayout.hpp @@ -0,0 +1,50 @@ +/****************************************************************************** + Example provided by Qt + + + 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 . +******************************************************************************/ + +#pragma once + +#include +#include + +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 itemList; + int m_hSpace; + int m_vSpace; +}; diff --git a/frontend/components/SourceSelectButton.cpp b/frontend/components/SourceSelectButton.cpp new file mode 100644 index 00000000000000..0e8d1cb9cd0d7e --- /dev/null +++ b/frontend/components/SourceSelectButton.cpp @@ -0,0 +1,184 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + Lain Bailey + + 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 . +******************************************************************************/ + +#include "SourceSelectButton.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +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 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); + + setFocusProxy(button); + + connect(button, &QAbstractButton::pressed, this, &SourceSelectButton::buttonPressed); +} + +SourceSelectButton::~SourceSelectButton() {} + +QPointer 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.clear(); + } + } + + 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(); + } +} diff --git a/frontend/components/SourceSelectButton.hpp b/frontend/components/SourceSelectButton.hpp new file mode 100644 index 00000000000000..20187cc57490d7 --- /dev/null +++ b/frontend/components/SourceSelectButton.hpp @@ -0,0 +1,68 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + Lain Bailey + + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +class Thumbnail; +class QLabel; + +class SourceSelectButton : public QFrame { + Q_OBJECT + +public: + SourceSelectButton(obs_source_t *source, QWidget *parent = nullptr); + ~SourceSelectButton(); + + QPointer 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; + QSharedPointer thumbnail; + QPointer 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); +}; diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index 908af15890c577..c6f6d77c8d18e0 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -630,15 +630,20 @@ 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" # source box Basic.Main.Sources.Visibility="Visibility" @@ -774,6 +779,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" diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index d00b26708fc4a1..f59a7ce351c21c 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -2242,6 +2242,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); diff --git a/frontend/dialogs/OBSBasicSourceSelect.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp index 76db07802068d5..99d3265e7eb79f 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -1,5 +1,6 @@ /****************************************************************************** Copyright (C) 2023 by Lain Bailey + Copyright (C) 2025 by Taylor Giampaolo 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,7 +18,13 @@ #include "OBSBasicSourceSelect.hpp" -#include +#include +#include + +#include "qt-wrappers.hpp" +#include "OBSApp.hpp" + +#include #include "moc_OBSBasicSourceSelect.cpp" @@ -34,80 +41,36 @@ struct AddSourceData { obs_sceneitem_t *scene_item = nullptr; }; -bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source) -{ - if (obs_source_is_hidden(source)) - return true; - - OBSBasicSourceSelect *window = static_cast(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) +namespace { +const char *getSourceDisplayName(QString type) { - OBSBasicSourceSelect *window = static_cast(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(); + std::string typeId = type.toStdString(); + const char *unversionedId = typeId.c_str(); - obs_sceneitem_t *existing = obs_scene_get_group(scene, name); - if (!existing) - window->ui->sourceList->addItem(QT_UTF8(name)); + if (strcmp(unversionedId, "scene") == 0) { + return Str("Basic.Scene"); } - - return true; -} - -void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata) -{ - OBSBasicSourceSelect *window = static_cast(data); - obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source"); - - QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source)); -} - -void OBSBasicSourceSelect::OBSSourceRemoved(void *data, calldata_t *calldata) -{ - OBSBasicSourceSelect *window = static_cast(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); + const char *id = obs_get_latest_input_type_id(unversionedId); + return obs_source_get_display_name(id); } +} // namespace -void OBSBasicSourceSelect::SourceRemoved(OBSSource source) +char *getNewSourceName(const char *name, const char *format) { - const char *name = obs_source_get_name(source); - const char *sourceId = obs_source_get_unversioned_id(source); + struct dstr new_name = {0}; + int inc = 0; - if (strcmp(sourceId, id) != 0) - return; + dstr_copy(&new_name, name); - QList items = ui->sourceList->findItems(name, Qt::MatchFixedString); + for (;;) { + OBSSourceAutoRelease existing_source = obs_get_source_by_name(new_name.array); + if (!existing_source) + break; - if (!items.count()) - return; + dstr_printf(&new_name, format, name, ++inc + 1); + } - delete items[0]; + return new_name.array; } static void AddSource(void *_data, obs_scene_t *scene) @@ -131,24 +94,6 @@ 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, obs_transform_info *transform, obs_sceneitem_crop *crop, obs_blending_method *blend_method, obs_blending_type *blend_mode) { @@ -159,7 +104,7 @@ static void AddExisting(OBSSource source, bool visible, bool duplicate, obs_tran if (duplicate) { OBSSource from = source; - char *new_name = get_new_source_name(obs_source_get_name(source), "%s %d"); + char *new_name = getNewSourceName(obs_source_get_name(source), "%s %d"); source = obs_source_duplicate(from, new_name, false); obs_source_release(source); bfree(new_name); @@ -232,59 +177,475 @@ 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) { - bool useExisting = ui->selectExisting->isChecked(); - bool visible = ui->sourceVisible->isChecked(); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - if (useExisting) { - QListWidgetItem *item = ui->sourceList->currentItem(); - if (!item) - return; + 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); + + getSourceTypes(); + getSources(); + + updateExistingSources(16); + + connect(ui->lineEdit, &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 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::cy); + scrollAreaRect.setBottom(scrollAreaRect.bottom() + Thumbnail::cy); + + for (QAbstractButton *button : buttons) { + SourceSelectButton *sourceButton = qobject_cast(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(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) +{ + sourceButtons = new QButtonGroup(this); + sourceButtons->setExclusive(false); + + auto sourcesList = &sources; + + std::vector reversedSources; + bool isReverseListOrder = sourceTypeId.isNull(); + if (isReverseListOrder) { + reversedSources = {sources.rbegin(), sources.rend()}; + sourcesList = &reversedSources; + } + + int count = 0; + for (obs_source_t *source : *sourcesList) { + if (limit > 0 && count >= limit) { + break; + } + + const char *id = obs_source_get_unversioned_id(source); + + if (QString(id).compare("group") == 0) { + continue; + } + + QWidget *prevTabItem = ui->sourceTypeList; + if (sourceTypeId.compare(QString(id)) == 0 || sourceTypeId.isNull()) { + SourceSelectButton *newButton = new SourceSelectButton(source, ui->existingScrollContents); + existingFlowLayout->addWidget(newButton); + sourceButtons->addButton(newButton->getButton()); + + setTabOrder(prevTabItem, newButton->getButton()); + prevTabItem = newButton->getButton(); + + count++; + } + } + + connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled); + + ui->existingListFrame->adjustSize(); + QTimer::singleShot(100, this, [this] { checkSourceVisibility(); }); +} + +bool OBSBasicSourceSelect::enumSourcesCallback(void *data, obs_source_t *source) +{ + if (obs_source_is_hidden(source)) + return true; + + OBSBasicSourceSelect *window = static_cast(data); - QString source_name = item->text(); - AddExisting(QT_TO_UTF8(source_name), visible, false, nullptr, nullptr, nullptr, nullptr); + window->sources.push_back(source); + return true; +} + +bool OBSBasicSourceSelect::enumGroupsCallback(void *data, obs_source_t *source) +{ + OBSBasicSourceSelect *window = static_cast(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(); - const char *scene_name = obs_source_get_name(main->GetCurrentSceneSource()); + OBSScene scene = main->GetCurrentScene(); - auto undo = [scene_name, main](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); + obs_sceneitem_t *existing = obs_scene_get_group(scene, name); + if (!existing) { + QPushButton *button = new QPushButton(name); + connect(button, &QPushButton::clicked, window, &OBSBasicSourceSelect::addSelectedSources); + } + } - obs_scene_t *scene = obs_get_scene_by_name(scene_name); - OBSSceneItem item; - auto cb = [](obs_scene_t *, obs_sceneitem_t *sceneitem, void *data) { - OBSSceneItem &last = *static_cast(data); - last = sceneitem; - return true; - }; - obs_scene_enum_items(scene, cb, &item); + return true; +} - obs_sceneitem_remove(item); - obs_scene_release(scene); - }; +void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata) +{ + OBSBasicSourceSelect *window = static_cast(data); + obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source"); - auto redo = [scene_name, main, source_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, nullptr, nullptr, nullptr, nullptr); - }; + QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source)); +} + +void OBSBasicSourceSelect::getSourceTypes() +{ + OBSBasic *main = qobject_cast(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(); + newItem->setData(Qt::DisplayRole, name); + newItem->setData(UNVERSIONED_ID_ROLE, unversioned_type); - undo_s.add_action(QTStr("Undo.Add").arg(source_name), undo, redo, "", ""); + QIcon icon; + icon = main->GetSourceIcon(type); + newItem->setIcon(icon); + + ui->sourceTypeList->addItem(newItem); + } + + QListWidgetItem *newItem = new QListWidgetItem(); + newItem->setData(Qt::DisplayRole, Str("Basic.Scene")); + newItem->setData(UNVERSIONED_ID_ROLE, "scene"); + + QIcon icon; + icon = main->GetSceneIcon(); + newItem->setIcon(icon); + + ui->sourceTypeList->addItem(newItem); + + ui->sourceTypeList->sortItems(); + + 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 data = item->data(UNVERSIONED_ID_ROLE); + + if (data.isNull()) { + setSelectedSource(nullptr); + sourceTypeId.clear(); + ui->createNewFrame->setVisible(false); + updateExistingSources(16); + return; + } + + QString type = data.toString(); + if (type.compare(sourceTypeId) == 0) { + return; + } + + ui->createNewFrame->setVisible(true); + + sourceTypeId = type; + + QString placeHolderText{QT_UTF8(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->lineEdit->setText(text); + ui->lineEdit->selectAll(); + + if (sourceTypeId.compare("scene") == 0) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSource curSceneSource = main->GetCurrentSceneSource(); + + 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(item); + OBSSource sceneSource = obs_scene_get_source(scene); + + if (curSceneSource == sceneSource) + continue; + + SourceSelectButton *newButton = new SourceSelectButton(sceneSource, ui->existingScrollContents); + existingFlowLayout->addWidget(newButton); + sourceButtons->addButton(newButton->getButton()); + + setTabOrder(prevTabItem, newButton->getButton()); + prevTabItem = newButton->getButton(); + } + QTimer::singleShot(100, this, [this] { checkSourceVisibility(); }); + + } else if (sourceTypeId.compare("group") == 0) { + obs_enum_sources(enumGroupsCallback, this); } else { - if (ui->sourceName->text().isEmpty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; + 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(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(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); } - OBSSceneItem item; - if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()), visible, newSource, item)) - return; + 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; + } - OBSBasic *main = OBSBasic::Get(); + auto widget = item->widget(); + if (!widget) { + continue; + } + + auto entry = dynamic_cast(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 visible = ui->sourceVisible->isChecked(); + + if (ui->lineEdit->text().isEmpty()) + return; + + OBSSceneItem item; + std::string sourceType = sourceTypeId.toStdString(); + const char *id = sourceType.c_str(); + if (!AddNew(this, id, QT_TO_UTF8(ui->lineEdit->text()), visible, newSource, item)) + return; + + if (newSource && strcmp(obs_source_get_id(newSource.Get()), "group") != 0) { + OBSBasic *main = reinterpret_cast(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()); @@ -296,7 +657,7 @@ void OBSBasicSourceSelect::on_buttonBox_accepted() 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_string(wrapper, "name", ui->lineEdit->text().toUtf8().constData()); obs_data_set_bool(wrapper, "visible", visible); auto redo = [scene_name, main](const std::string &data) { @@ -310,98 +671,84 @@ void OBSBasicSourceSelect::on_buttonBox_accepted() 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, + undo_s.add_action(QTStr("Undo.Add").arg(ui->lineEdit->text()), undo, redo, std::string(obs_source_get_name(newSource)), std::string(obs_data_get_json(wrapper))); - } - done(DialogCode::Accepted); + main->CreatePropertiesWindow(newSource); + } + close(); } -void OBSBasicSourceSelect::on_buttonBox_rejected() +void OBSBasicSourceSelect::addExistingSource(QString name, bool visible) { - done(DialogCode::Rejected); -} + OBSSourceAutoRelease source = obs_get_source_by_name(name.toStdString().c_str()); + if (source) { + AddExisting(source.Get(), visible, false, nullptr, nullptr, nullptr, nullptr); -static inline const char *GetSourceDisplayName(const char *id) -{ - 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); -} + OBSBasic *main = OBSBasic::Get(); + const char *scene_name = obs_source_get_name(main->GetCurrentSceneSource()); -OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_, undo_stack &undo_s) - : QDialog(parent), - ui(new Ui::OBSBasicSourceSelect), - id(id_), - undo_s(undo_s) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto undo = [scene_name, main](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); - ui->setupUi(this); + obs_scene_t *scene = obs_get_scene_by_name(scene_name); + OBSSceneItem item; + auto cb = [](obs_scene_t *, obs_sceneitem_t *sceneitem, void *data) { + OBSSceneItem &last = *static_cast(data); + last = sceneitem; + return true; + }; + obs_scene_enum_items(scene, cb, &item); - ui->sourceList->setAttribute(Qt::WA_MacShowFocusRect, false); + obs_sceneitem_remove(item); + obs_scene_release(scene); + }; - QString placeHolderText{QT_UTF8(GetSourceDisplayName(id))}; + 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(name), visible, false, nullptr, nullptr, nullptr, nullptr); + }; - 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++); + undo_s.add_action(QTStr("Undo.Add").arg(name), undo, redo, "", ""); } +} - ui->sourceName->setText(text); - ui->sourceName->setFocus(); //Fixes deselect of text. - ui->sourceName->selectAll(); - - installEventFilter(CreateShortcutFilter()); +void OBSBasicSourceSelect::on_createNewSource_clicked(bool) +{ + createNewSource(); +} - connect(ui->createNew, &QRadioButton::pressed, [&]() { - QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - if (!button->isEnabled()) - button->setEnabled(true); - }); - connect(ui->selectExisting, &QRadioButton::pressed, [&]() { - 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, [&]() { - QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - if (!button->isEnabled()) - button->setEnabled(true); - }); +void OBSBasicSourceSelect::addSelectedSources() +{ + if (selectedItems.size() == 0) { + return; + } - if (strcmp(id_, "scene") == 0) { - OBSBasic *main = OBSBasic::Get(); - OBSSource curSceneSource = main->GetCurrentSceneSource(); + bool visible = ui->sourceVisible->isChecked(); - ui->selectExisting->setChecked(true); - ui->createNew->setChecked(false); - ui->createNew->setEnabled(false); - ui->sourceName->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + for (auto &item : selectedItems) { + QString sourceName = item->text(); + addExistingSource(sourceName, visible); + } + close(); +} - int count = main->ui->scenes->count(); - for (int i = 0; i < count; i++) { - QListWidgetItem *item = main->ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - OBSSource sceneSource = obs_scene_get_source(scene); +void OBSBasicSourceSelect::sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *) +{ + setSelectedSourceType(current); +} - if (curSceneSource == sceneSource) - continue; +void OBSBasicSourceSelect::sourceTypeClicked(QListWidgetItem *clicked) +{ + setSelectedSourceType(clicked); - 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); + QVariant type = clicked->data(UNVERSIONED_ID_ROLE); + if (!type.isNull()) { + ui->lineEdit->setFocus(); } } diff --git a/frontend/dialogs/OBSBasicSourceSelect.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp index 077d8d2988e908..7c215e988bfb74 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.hpp +++ b/frontend/dialogs/OBSBasicSourceSelect.hpp @@ -1,5 +1,6 @@ /****************************************************************************** Copyright (C) 2023 by Lain Bailey + Copyright (C) 2025 by Taylor Giampaolo 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 @@ -19,36 +20,74 @@ #include "ui_OBSBasicSourceSelect.h" +#include +#include +#include #include #include -#include +#include #include +constexpr int UNVERSIONED_ID_ROLE = Qt::UserRole + 1; + class OBSBasicSourceSelect : public QDialog { Q_OBJECT private: std::unique_ptr 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 sourceButtons; + + std::vector sources; + std::vector groups; + + QPointer 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); + + int lastSelectedIndex = -1; + std::vector 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 sourceTypeClicked(QListWidgetItem *clicked); - void SourceAdded(OBSSource source); - void SourceRemoved(OBSSource source); + 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; diff --git a/frontend/forms/OBSBasicSourceSelect.ui b/frontend/forms/OBSBasicSourceSelect.ui index 07f64ab4762126..a665d4b3d069cf 100644 --- a/frontend/forms/OBSBasicSourceSelect.ui +++ b/frontend/forms/OBSBasicSourceSelect.ui @@ -3,106 +3,654 @@ OBSBasicSourceSelect - Qt::WindowModal + Qt::WindowModality::NonModal 0 0 - 352 - 314 + 1000 + 614 + + + 0 + 0 + + + + + 16777215 + 1000 + + Basic.SourceSelect - - - - - - - Basic.SourceSelect.CreateNew - - - true - - - - - - - - - - Basic.SourceSelect.AddExisting - - - - - - - false - - - true - - - - - - - Basic.SourceSelect.AddVisible - - - true - - - - - + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + 0 + 0 + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + dialog-container + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 0 + 0 + + + + Basic.SourceSelect.SelectType + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 460 + + + + Basic.SourceSelect.SelectType + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + false + + + QAbstractItemView::ScrollMode::ScrollPerPixel + + + QAbstractItemView::ScrollMode::ScrollPerPixel + + + QListView::ResizeMode::Adjust + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Minimum + + + + 17 + 4 + + + + + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.AddExisting + + + QFrame::Shadow::Plain + + + 0 + + + dialog-container dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Basic.SourceSelect.Existing + + + text-title + + + + + + + Qt::FocusPolicy::NoFocus + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + + + true + + + + + 0 + 0 + 800 + 510 + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 10 + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Horizontal + + + + 40 + 10 + + + + + + + + Qt::Orientation::Horizontal + + + + 259 + 10 + + + + + + + + false + + + + 0 + 0 + + + + Add Existing + + + button-primary + + + + + + + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + dialog-container dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.NewSource + + + text-title + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Basic.SourceSelect.Accessible.SourceName + + + + + + + + 0 + 0 + + + + Basic.SourceSelect.CreateButton + + + button-primary margin-left + + + + + + + + + + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + dialog-container + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.AddVisible + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + QDialogButtonBox::StandardButton::Cancel + + + false + + + + + + + + + + - - - - createNew - toggled(bool) - sourceName - setEnabled(bool) - - - 79 - 29 - - - 108 - 53 - - - - - selectExisting - toggled(bool) - sourceList - setEnabled(bool) - - - 51 - 80 - - - 65 - 128 - - - - + + + FlowFrame + QFrame +
components/FlowFrame.hpp
+ 1 +
+
+ + existingScrollArea + sourceVisible + + + + + diff --git a/frontend/utility/ScreenshotObj.cpp b/frontend/utility/ScreenshotObj.cpp index 0fff65d6f526d1..e433c267bfba96 100644 --- a/frontend/utility/ScreenshotObj.cpp +++ b/frontend/utility/ScreenshotObj.cpp @@ -1,5 +1,6 @@ /****************************************************************************** Copyright (C) 2023 by Lain Bailey + Copyright (C) 2025 by Taylor Giampaolo 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,6 +18,7 @@ #include "ScreenshotObj.hpp" +#include "display-helpers.hpp" #include #include @@ -30,11 +32,23 @@ #include "moc_ScreenshotObj.cpp" -static void ScreenshotTick(void *param, float); +namespace { +void renderTick(void *param, float) +{ + auto *self = static_cast(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,32 @@ void ScreenshotObj::Screenshot() #endif const enum gs_color_format format = gs_get_format_from_space(space); + outputWidth = customSize.isValid() ? customSize.width() : sourceWidth; + outputHeight = customSize.isValid() ? customSize.height() : sourceHeight; + texrender = gs_texrender_create(format, GS_ZS_NONE); - stagesurf = gs_stagesurface_create(cx, cy, format); + stagesurf = gs_stagesurface_create(outputWidth, outputHeight, format); - if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) { + if (gs_texrender_begin_with_color_space(texrender, outputWidth, outputHeight, space)) { vec4 zero; vec4_zero(&zero); + int x, y; + int scaledWidth, 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 +132,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 +193,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 +218,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 +235,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 +282,50 @@ static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy) Microsoft::WRL::ComPtr 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 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 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 frameEncode; Microsoft::WRL::ComPtr 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 +335,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(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(); } diff --git a/frontend/utility/ScreenshotObj.hpp b/frontend/utility/ScreenshotObj.hpp index 01a4fefd8b131c..56e128a0886472 100644 --- a/frontend/utility/ScreenshotObj.hpp +++ b/frontend/utility/ScreenshotObj.hpp @@ -1,5 +1,6 @@ /****************************************************************************** Copyright (C) 2023 by Lain Bailey + Copyright (C) 2025 by Taylor Giampaolo 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 @@ class ScreenshotObj : public QObject { std::string path; QImage image; std::vector 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 imagePtr; + bool outputToFile = true; - int stage = 0; +signals: + void imageReady(QImage image); -public slots: - void Save(); +private slots: + void handleSave(); }; diff --git a/frontend/utility/ThumbnailManager.cpp b/frontend/utility/ThumbnailManager.cpp new file mode 100644 index 00000000000000..815fd28f508887 --- /dev/null +++ b/frontend/utility/ThumbnailManager.cpp @@ -0,0 +1,238 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + Lain Bailey + + 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 . +******************************************************************************/ + +#include "display-helpers.hpp" +#include "ThumbnailManager.hpp" +#include +#include + +#include + +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(QWeakPointer 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 = weakActiveItem; + } +} + +ThumbnailItem::~ThumbnailItem() +{ + auto thumbnailManager = OBSBasic::Get()->thumbnails(); + if (!thumbnailManager) { + return; + } + + auto &cachedItem = thumbnailManager->cachedThumbnails[uuid]; + cachedItem.pixmap = pixmap; + cachedItem.weakActiveItem.clear(); +} + +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() {} + +QSharedPointer ThumbnailManager::getThumbnail(OBSSource source) +{ + std::string uuid = obs_source_get_uuid(source); + + for (auto it = thumbnails.begin(); it != thumbnails.end(); ++it) { + auto item = it->toStrongRef(); + if (item && item->uuid == uuid) { + return QSharedPointer::create(item); + } + } + + QSharedPointer thumbnail; + if ((obs_source_get_output_flags(source) & OBS_SOURCE_VIDEO) != 0) { + auto item = QSharedPointer::create(uuid, source); + item->init(item.toWeakRef()); + + thumbnail = QSharedPointer::create(item); + connect(item.get(), &ThumbnailItem::updateThumbnail, thumbnail.get(), &Thumbnail::thumbnailUpdated); + + newThumbnails.push_back(item.toWeakRef()); + } + + updateIntervalChanged(thumbnails.size()); + return thumbnail; +} + +bool ThumbnailManager::updatePixmap(QSharedPointer &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::cx, Thumbnail::cy); + + 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() +{ + QSharedPointer item; + bool changed = false; + bool newThumbnail = false; + + while (newThumbnails.size() > 0) { + changed = true; + item = newThumbnails.front().toStrongRef(); + + newThumbnails.pop_front(); + if (item) { + newThumbnail = true; + break; + } + } + + if (!item) { + while (thumbnails.size() > 0) { + item = thumbnails.front().toStrongRef(); + 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(item.toWeakRef()); + } else { + thumbnails.push_front(item.toWeakRef()); + } +} + +std::optional ThumbnailManager::getCachedThumbnail(OBSSource source) +{ + 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; + } else { + auto activeItem = cachedItem.weakActiveItem.toStrongRef(); + return !activeItem.isNull() ? std::make_optional(activeItem->pixmap) : std::nullopt; + } + } else { + return std::nullopt; + } +} + +void ThumbnailManager::preloadThumbnail(OBSSource source, QObject *object, std::function callback) +{ + 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::cx, Thumbnail::cy); + + connect(obj, &ScreenshotObj::imageReady, this, [=](QImage image) { + QPixmap pixmap; + if (!image.isNull()) { + pixmap = QPixmap::fromImage(image); + } + cachedThumbnails[uuid].pixmap = pixmap; + + QMetaObject::invokeMethod(object, std::bind(callback, pixmap)); + }); + } +} diff --git a/frontend/utility/ThumbnailManager.hpp b/frontend/utility/ThumbnailManager.hpp new file mode 100644 index 00000000000000..333dbdd1629c79 --- /dev/null +++ b/frontend/utility/ThumbnailManager.hpp @@ -0,0 +1,107 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + Lain Bailey + + 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 . +******************************************************************************/ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include + +class ThumbnailItem : public QObject { + Q_OBJECT + + friend class ThumbnailManager; + friend class Thumbnail; + + std::string uuid; + OBSWeakSource weakSource; + QPixmap pixmap; + + void init(QWeakPointer 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; + + QSharedPointer item; + +private slots: + void thumbnailUpdated(QPixmap pixmap); + +public: + inline Thumbnail(QSharedPointer item) : item(item) {} + + inline QPixmap getPixmap() const { return item->pixmap; } + + static constexpr int cx = 320; + static constexpr int cy = 180; + +signals: + void updateThumbnail(QPixmap pixmap); +}; + +class ThumbnailManager : public QObject { + Q_OBJECT + + friend class ThumbnailItem; + + struct CachedItem { + std::optional pixmap; + QWeakPointer weakActiveItem; + }; + + QList> newThumbnails; + QList> thumbnails; + std::unordered_map cachedThumbnails; + QTimer updateTimer; + + bool updatePixmap(QSharedPointer &item); + void updateTick(); + + void updateIntervalChanged(size_t newCount); + +public: + explicit ThumbnailManager(QObject *parent = nullptr); + ~ThumbnailManager(); + + QSharedPointer getThumbnail(OBSSource source); + std::optional getCachedThumbnail(OBSSource source); + void preloadThumbnail(OBSSource source, QObject *object, std::function callback); + +private: + Q_DISABLE_COPY_MOVE(ThumbnailManager); +}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index b0698b29b54755..be4e54f4a0d283 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -242,6 +242,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); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index ca213433e0beb8..f4553b517ad00e 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -49,6 +50,7 @@ class OBSBasicAdvAudio; class OBSBasicFilters; class OBSBasicInteraction; class OBSBasicProperties; +class OBSBasicSourceSelect; class OBSBasicTransform; class OBSLogViewer; class OBSMissingFiles; @@ -299,6 +301,8 @@ class OBSBasic : public OBSMainWindow { // TODO: Remove, orphaned instance method void LoadProject(); + ThumbnailManager *thumbnailManager = nullptr; + public slots: void UpdatePatronJson(const QString &text, const QString &error); void UpdateEditMenu(); @@ -327,6 +331,8 @@ public slots: inline bool Closing() { return closing; } + ThumbnailManager *thumbnails() const { return thumbnailManager; } + protected: virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; @@ -465,6 +471,9 @@ private slots: void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; +signals: + void sourceUuidDropped(QString uuid); + /* ------------------------------------- * MARK: - OBSBasic_Hotkeys * ------------------------------------- @@ -560,6 +569,7 @@ private slots: QPointer advAudioWindow; QPointer filters; QPointer about; + QPointer addWindow; QPointer logView; QPointer stats; QPointer remux; @@ -1166,11 +1176,8 @@ public slots: static void SourceAudioDeactivated(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 ActivateAudioSource(OBSSource source); diff --git a/frontend/widgets/OBSBasic_Dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp index 9492a4f9c1c643..71ed6248363241 100644 --- a/frontend/widgets/OBSBasic_Dropfiles.cpp +++ b/frontend/widgets/OBSBasic_Dropfiles.cpp @@ -215,6 +215,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); @@ -325,5 +329,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(); } } diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp index ce60b71ec046e4..626c85984e0510 100644 --- a/frontend/widgets/OBSBasic_SceneItems.cpp +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -330,7 +330,7 @@ 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); +extern char *getNewSourceName(const char *name, const char *format); void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) { @@ -352,7 +352,7 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, cons } } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + BPtr name = getNewSourceName(deviceDesc, "%s (%d)"); settings = obs_data_create(); obs_data_set_string(settings, "device_id", deviceId); @@ -592,10 +592,14 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) } // Add new source - QPointer 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 @@ -704,14 +708,12 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) // Source grouping if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { popup.addSeparator(); + } else if (ui->sources->GroupsSelected()) { popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + popup.addSeparator(); } - popup.addSeparator(); popup.addAction(ui->actionCopySource); popup.addAction(ui->actionPasteRef); @@ -767,115 +769,27 @@ static inline bool should_show_properties(obs_source_t *source, const char *id) return true; } -void OBSBasic::AddSource(const char *id) -{ - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() +void OBSBasic::AddSourceDialog() { - 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 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, 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]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; + QAction *action = qobject_cast(sender()); + if (!action) { + return; } - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); + if (addWindow) { + addWindow->close(); } - 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")); - return; - } + addWindow = new OBSBasicSourceSelect(this, undo_s); + addWindow->show(); - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); + 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) diff --git a/shared/qt/wrappers/qt-wrappers.cpp b/shared/qt/wrappers/qt-wrappers.cpp index 490f59bb1f8358..a0b51ae173183f 100644 --- a/shared/qt/wrappers/qt-wrappers.cpp +++ b/shared/qt/wrappers/qt-wrappers.cpp @@ -373,3 +373,11 @@ void RefreshToolBarStyling(QToolBar *toolBar) widget->style()->polish(widget); } } + +bool ResizeSignaler::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::Resize) { + emit resized(); + } + return QObject::eventFilter(object, event); +} diff --git a/shared/qt/wrappers/qt-wrappers.hpp b/shared/qt/wrappers/qt-wrappers.hpp index 72ab917a5636c7..b8b50f99c2f1a3 100644 --- a/shared/qt/wrappers/qt-wrappers.hpp +++ b/shared/qt/wrappers/qt-wrappers.hpp @@ -99,3 +99,16 @@ QStringList OpenFiles(QWidget *parent, QString title, QString path, QString exte void TruncateLabel(QLabel *label, QString newText, int length = MAX_LABEL_LENGTH); void RefreshToolBarStyling(QToolBar *toolBar); + +class ResizeSignaler : public QObject { + Q_OBJECT + +public: + inline ResizeSignaler(QObject *parent) : QObject(parent) {} + +signals: + void resized(); + +protected: + bool eventFilter(QObject *object, QEvent *event) override; +}; From 134a58bb21241aeebdb1f589044bd676209891f0 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 30 Oct 2025 19:57:21 -0400 Subject: [PATCH 2/4] squashme: Tweaks and cleanup --- frontend/components/FlowFrame.cpp | 2 +- frontend/components/SourceSelectButton.cpp | 1 + frontend/data/themes/Yami_Acri.ovt | 11 +++ frontend/data/themes/Yami_Classic.ovt | 13 +++ frontend/data/themes/Yami_Light.ovt | 32 +++++- frontend/data/themes/Yami_Rachni.ovt | 6 ++ frontend/dialogs/OBSBasicSourceSelect.cpp | 110 +++++++++++++-------- frontend/dialogs/OBSBasicSourceSelect.hpp | 1 - frontend/forms/OBSBasicSourceSelect.ui | 8 +- frontend/utility/ThumbnailManager.cpp | 10 +- 10 files changed, 145 insertions(+), 49 deletions(-) diff --git a/frontend/components/FlowFrame.cpp b/frontend/components/FlowFrame.cpp index d0da758b39e7c0..3974f1d8561aff 100644 --- a/frontend/components/FlowFrame.cpp +++ b/frontend/components/FlowFrame.cpp @@ -62,7 +62,7 @@ void FlowFrame::keyPressEvent(QKeyEvent *event) case Qt::Key_Down: case Qt::Key_Left: case Qt::Key_Up: { - /* Find next widget in the given direction */ + // Find next widget in the given direction int bestDistance = INT_MAX; for (int i = 0; i < layout->count(); ++i) { if (i == index) { diff --git a/frontend/components/SourceSelectButton.cpp b/frontend/components/SourceSelectButton.cpp index 0e8d1cb9cd0d7e..01bb2e40a65316 100644 --- a/frontend/components/SourceSelectButton.cpp +++ b/frontend/components/SourceSelectButton.cpp @@ -71,6 +71,7 @@ SourceSelectButton::SourceSelectButton(obs_source_t *source_, QWidget *parent) : button->setFixedSize(width(), height()); button->move(0, 0); + setFocusPolicy(Qt::StrongFocus); setFocusProxy(button); connect(button, &QAbstractButton::pressed, this, &SourceSelectButton::buttonPressed); diff --git a/frontend/data/themes/Yami_Acri.ovt b/frontend/data/themes/Yami_Acri.ovt index 76d3f215e7bc61..ec88dbcc18ab87 100644 --- a/frontend/data/themes/Yami_Acri.ovt +++ b/frontend/data/themes/Yami_Acri.ovt @@ -229,3 +229,14 @@ QCalendarWidget QToolButton:pressed { QCalendarWidget QSpinBox { background-color: var(--primary_dark); } + +/* 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); +} diff --git a/frontend/data/themes/Yami_Classic.ovt b/frontend/data/themes/Yami_Classic.ovt index 3224f0b3807523..ac766a5ab12bdf 100644 --- a/frontend/data/themes/Yami_Classic.ovt +++ b/frontend/data/themes/Yami_Classic.ovt @@ -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); +} diff --git a/frontend/data/themes/Yami_Light.ovt b/frontend/data/themes/Yami_Light.ovt index 09bd53b1e8e0ad..e275854136cf99 100644 --- a/frontend/data/themes/Yami_Light.ovt +++ b/frontend/data/themes/Yami_Light.ovt @@ -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: rgb(66,112,24); qproperty-backgroundWarningColor: rgb(112,91,28); @@ -324,3 +334,19 @@ QCalendarWidget #qt_calendar_prevmonth { QCalendarWidget #qt_calendar_nextmonth { qproperty-icon: url(theme:Light/right.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); +} diff --git a/frontend/data/themes/Yami_Rachni.ovt b/frontend/data/themes/Yami_Rachni.ovt index d51a0d2ae0abd2..f79bf42c053cd1 100644 --- a/frontend/data/themes/Yami_Rachni.ovt +++ b/frontend/data/themes/Yami_Rachni.ovt @@ -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); +} diff --git a/frontend/dialogs/OBSBasicSourceSelect.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp index 99d3265e7eb79f..5fc75277e68298 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -29,7 +29,7 @@ #include "moc_OBSBasicSourceSelect.cpp" struct AddSourceData { - /* Input data */ + // Input data obs_source_t *source; bool visible; obs_transform_info *transform = nullptr; @@ -37,7 +37,7 @@ struct AddSourceData { obs_blending_method *blend_method = nullptr; obs_blending_type *blend_mode = nullptr; - /* Return data */ + // Return data obs_sceneitem_t *scene_item = nullptr; }; @@ -64,8 +64,9 @@ char *getNewSourceName(const char *name, const char *format) for (;;) { OBSSourceAutoRelease existing_source = obs_get_source_by_name(new_name.array); - if (!existing_source) + if (!existing_source) { break; + } dstr_printf(&new_name, format, name, ++inc + 1); } @@ -80,14 +81,18 @@ 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); + } obs_sceneitem_set_visible(sceneitem, data->visible); @@ -99,8 +104,9 @@ static void AddExisting(OBSSource source, bool visible, bool duplicate, obs_tran { OBSBasic *main = OBSBasic::Get(); OBSScene scene = main->GetCurrentScene(); - if (!scene) + if (!scene) { return; + } if (duplicate) { OBSSource from = source; @@ -109,8 +115,9 @@ static void AddExisting(OBSSource source, bool visible, bool duplicate, obs_tran obs_source_release(source); bfree(new_name); - if (!source) + if (!source) { return; + } } AddSourceData data; @@ -141,8 +148,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) { @@ -164,7 +172,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); @@ -214,7 +222,8 @@ OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s) updateExistingSources(16); - connect(ui->lineEdit, &QLineEdit::returnPressed, this, &OBSBasicSourceSelect::createNewSource); + 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); @@ -239,7 +248,7 @@ void OBSBasicSourceSelect::checkSourceVisibility() { QList buttons = sourceButtons->buttons(); - /* Allow some room for previous/next rows to make scrolling a bit more seamless */ + // 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::cy); scrollAreaRect.setBottom(scrollAreaRect.bottom() + Thumbnail::cy); @@ -285,6 +294,7 @@ void OBSBasicSourceSelect::getSources() void OBSBasicSourceSelect::updateExistingSources(int limit) { + delete sourceButtons; sourceButtons = new QButtonGroup(this); sourceButtons->setExclusive(false); @@ -297,6 +307,7 @@ void OBSBasicSourceSelect::updateExistingSources(int limit) sourcesList = &reversedSources; } + QWidget *prevTabWidget = ui->sourceTypeList; int count = 0; for (obs_source_t *source : *sourcesList) { if (limit > 0 && count >= limit) { @@ -304,34 +315,43 @@ void OBSBasicSourceSelect::updateExistingSources(int limit) } const char *id = obs_source_get_unversioned_id(source); + QString stringId = QString(id); - if (QString(id).compare("group") == 0) { + if (stringId.compare("group") == 0) { continue; } - QWidget *prevTabItem = ui->sourceTypeList; - if (sourceTypeId.compare(QString(id)) == 0 || sourceTypeId.isNull()) { - SourceSelectButton *newButton = new SourceSelectButton(source, ui->existingScrollContents); + if (sourceTypeId.compare(stringId) == 0 || sourceTypeId.isNull()) { + SourceSelectButton *newButton = new SourceSelectButton(source, ui->existingListFrame); + std::string name = obs_source_get_name(source); + existingFlowLayout->addWidget(newButton); sourceButtons->addButton(newButton->getButton()); - setTabOrder(prevTabItem, newButton->getButton()); - prevTabItem = newButton->getButton(); + if (!prevTabWidget) { + setTabOrder(ui->existingListFrame, newButton->getButton()); + } else { + setTabOrder(prevTabWidget, newButton->getButton()); + } + + prevTabWidget = newButton->getButton(); count++; } } + setTabOrder(prevTabWidget, ui->addExistingContainer); connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled); ui->existingListFrame->adjustSize(); - QTimer::singleShot(100, this, [this] { checkSourceVisibility(); }); + QTimer::singleShot(10, this, [this] { checkSourceVisibility(); }); } bool OBSBasicSourceSelect::enumSourcesCallback(void *data, obs_source_t *source) { - if (obs_source_is_hidden(source)) + if (obs_source_is_hidden(source)) { return true; + } OBSBasicSourceSelect *window = static_cast(data); @@ -381,8 +401,9 @@ void OBSBasicSourceSelect::getSourceTypes() 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) + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) { continue; + } QListWidgetItem *newItem = new QListWidgetItem(); newItem->setData(Qt::DisplayRole, name); @@ -460,13 +481,14 @@ void OBSBasicSourceSelect::setSelectedSourceType(QListWidgetItem *item) text = QString("%1 %2").arg(placeHolderText).arg(i++); } - ui->lineEdit->setText(text); - ui->lineEdit->selectAll(); + ui->newSourceName->setText(text); + ui->newSourceName->selectAll(); if (sourceTypeId.compare("scene") == 0) { OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); OBSSource curSceneSource = main->GetCurrentSceneSource(); + delete sourceButtons; sourceButtons = new QButtonGroup(this); int count = main->ui->scenes->count(); @@ -476,18 +498,23 @@ void OBSBasicSourceSelect::setSelectedSourceType(QListWidgetItem *item) OBSScene scene = GetOBSRef(item); OBSSource sceneSource = obs_scene_get_source(scene); - if (curSceneSource == sceneSource) + if (curSceneSource == sceneSource) { continue; + } - SourceSelectButton *newButton = new SourceSelectButton(sceneSource, ui->existingScrollContents); + 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 { @@ -635,14 +662,24 @@ void OBSBasicSourceSelect::createNewSource() { bool visible = ui->sourceVisible->isChecked(); - if (ui->lineEdit->text().isEmpty()) + if (ui->newSourceName->text().isEmpty()) { + return; + } + + 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->lineEdit->text()), visible, newSource, item)) + 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(App()->GetMainWindow()); @@ -657,7 +694,7 @@ void OBSBasicSourceSelect::createNewSource() 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->lineEdit->text().toUtf8().constData()); + 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) { @@ -671,7 +708,7 @@ void OBSBasicSourceSelect::createNewSource() 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->lineEdit->text()), undo, redo, + 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); @@ -742,21 +779,12 @@ void OBSBasicSourceSelect::sourceTypeSelected(QListWidgetItem *current, QListWid setSelectedSourceType(current); } -void OBSBasicSourceSelect::sourceTypeClicked(QListWidgetItem *clicked) -{ - setSelectedSourceType(clicked); - - QVariant type = clicked->data(UNVERSIONED_ID_ROLE); - if (!type.isNull()) { - ui->lineEdit->setFocus(); - } -} - void OBSBasicSourceSelect::SourcePaste(SourceCopyInfo &info, bool dup) { OBSSource source = OBSGetStrongRef(info.weak_source); - if (!source) + if (!source) { return; + } AddExisting(source, info.visible, dup, &info.transform, &info.crop, &info.blend_method, &info.blend_mode); } diff --git a/frontend/dialogs/OBSBasicSourceSelect.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp index 7c215e988bfb74..e8851838e7b640 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.hpp +++ b/frontend/dialogs/OBSBasicSourceSelect.hpp @@ -80,7 +80,6 @@ public slots: void addSelectedSources(); void sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *previous); - void sourceTypeClicked(QListWidgetItem *clicked); void sourceButtonToggled(QAbstractButton *button, bool checked); void sourceDropped(QString uuid); diff --git a/frontend/forms/OBSBasicSourceSelect.ui b/frontend/forms/OBSBasicSourceSelect.ui index a665d4b3d069cf..1280b57209ec1b 100644 --- a/frontend/forms/OBSBasicSourceSelect.ui +++ b/frontend/forms/OBSBasicSourceSelect.ui @@ -381,7 +381,7 @@ - + 0 @@ -520,7 +520,7 @@ 0 - + 0 @@ -646,7 +646,11 @@ + sourceTypeList existingScrollArea + addExistingButton + newSourceName + createNewSource sourceVisible diff --git a/frontend/utility/ThumbnailManager.cpp b/frontend/utility/ThumbnailManager.cpp index 815fd28f508887..74b55fcd58e091 100644 --- a/frontend/utility/ThumbnailManager.cpp +++ b/frontend/utility/ThumbnailManager.cpp @@ -225,6 +225,7 @@ void ThumbnailManager::preloadThumbnail(OBSSource source, QObject *object, std:: obj->setSaveToFile(false); obj->setSize(Thumbnail::cx, Thumbnail::cy); + QPointer safeObject = qobject_cast(object); connect(obj, &ScreenshotObj::imageReady, this, [=](QImage image) { QPixmap pixmap; if (!image.isNull()) { @@ -232,7 +233,14 @@ void ThumbnailManager::preloadThumbnail(OBSSource source, QObject *object, std:: } cachedThumbnails[uuid].pixmap = pixmap; - QMetaObject::invokeMethod(object, std::bind(callback, pixmap)); + QMetaObject::invokeMethod( + safeObject, + [safeObject, callback, pixmap]() { + if (safeObject) { + callback(pixmap); + } + }, + Qt::QueuedConnection); }); } } From 6bea4f1272d69c4f57d31a32bdc751d740faca9d Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 30 Oct 2025 19:58:41 -0400 Subject: [PATCH 3/4] squashme: Revert fully qualified Qt enums --- frontend/forms/OBSBasicSourceSelect.ui | 74 +++++++++++++------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/frontend/forms/OBSBasicSourceSelect.ui b/frontend/forms/OBSBasicSourceSelect.ui index 1280b57209ec1b..3f9b6a5fd2f0cc 100644 --- a/frontend/forms/OBSBasicSourceSelect.ui +++ b/frontend/forms/OBSBasicSourceSelect.ui @@ -3,7 +3,7 @@ OBSBasicSourceSelect - Qt::WindowModality::NonModal + Qt::NonModal @@ -56,10 +56,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -98,10 +98,10 @@ Basic.SourceSelect.SelectType - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -143,31 +143,31 @@ Basic.SourceSelect.SelectType - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 - QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow + QAbstractScrollArea::AdjustToContentsOnFirstShow - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers false - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel - QAbstractItemView::ScrollMode::ScrollPerPixel + QAbstractItemView::ScrollPerPixel - QListView::ResizeMode::Adjust + QListView::Adjust @@ -177,10 +177,10 @@ - Qt::Orientation::Vertical + Qt::Vertical - QSizePolicy::Policy::Minimum + QSizePolicy::Minimum @@ -196,10 +196,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -226,10 +226,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -259,7 +259,7 @@ Basic.SourceSelect.AddExisting - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -302,22 +302,22 @@ - Qt::FocusPolicy::NoFocus + Qt::NoFocus - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + QAbstractScrollArea::AdjustToContents true @@ -353,10 +353,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -366,7 +366,7 @@ - Qt::Orientation::Vertical + Qt::Vertical @@ -404,7 +404,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -417,7 +417,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -455,10 +455,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -495,10 +495,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -560,10 +560,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain 0 @@ -600,7 +600,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -619,7 +619,7 @@ - QDialogButtonBox::StandardButton::Cancel + QDialogButtonBox::Cancel false From daab453727ff3a62fbb2a5bf72467052a8ebab45 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 27 Nov 2025 17:58:10 -0500 Subject: [PATCH 4/4] squashme: Addressing feedback --- frontend/cmake/ui-utility.cmake | 1 + frontend/components/FlowLayout.cpp | 14 ++------ frontend/components/FlowLayout.hpp | 14 ++------ frontend/components/SourceSelectButton.cpp | 8 ++--- frontend/components/SourceSelectButton.hpp | 10 +++--- frontend/data/locale/en-US.ini | 4 +-- frontend/dialogs/OBSBasicSourceSelect.cpp | 9 ++--- frontend/dialogs/OBSBasicSourceSelect.hpp | 5 ++- frontend/utility/ResizeSignaler.hpp | 39 ++++++++++++++++++++ frontend/utility/ThumbnailManager.cpp | 42 +++++++++++----------- frontend/utility/ThumbnailManager.hpp | 20 +++++------ frontend/widgets/OBSBasic.hpp | 2 +- shared/qt/wrappers/qt-wrappers.cpp | 8 ----- shared/qt/wrappers/qt-wrappers.hpp | 13 ------- 14 files changed, 95 insertions(+), 94 deletions(-) create mode 100644 frontend/utility/ResizeSignaler.hpp diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 6b2db164465cdf..4a4eba7aa54283 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -55,6 +55,7 @@ target_sources( utility/RemuxQueueModel.hpp utility/RemuxWorker.cpp utility/RemuxWorker.hpp + utility/ResizeSignaler.hpp utility/SceneRenameDelegate.cpp utility/SceneRenameDelegate.hpp utility/ScreenshotObj.cpp diff --git a/frontend/components/FlowLayout.cpp b/frontend/components/FlowLayout.cpp index 659e906c17d212..c3be1a46a2ddd6 100644 --- a/frontend/components/FlowLayout.cpp +++ b/frontend/components/FlowLayout.cpp @@ -2,18 +2,8 @@ Example provided by Qt - 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 . + Copyright (C) 2016 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause ******************************************************************************/ #include "FlowLayout.hpp" diff --git a/frontend/components/FlowLayout.hpp b/frontend/components/FlowLayout.hpp index ec12cc9bd24ce0..9d3cb32be971cc 100644 --- a/frontend/components/FlowLayout.hpp +++ b/frontend/components/FlowLayout.hpp @@ -2,18 +2,8 @@ Example provided by Qt - 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 . + Copyright (C) 2016 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause ******************************************************************************/ #pragma once diff --git a/frontend/components/SourceSelectButton.cpp b/frontend/components/SourceSelectButton.cpp index 01bb2e40a65316..dd08dfaa33e3d1 100644 --- a/frontend/components/SourceSelectButton.cpp +++ b/frontend/components/SourceSelectButton.cpp @@ -18,15 +18,15 @@ #include "SourceSelectButton.hpp" +#include +#include + #include #include #include #include #include -#include -#include - SourceSelectButton::SourceSelectButton(obs_source_t *source_, QWidget *parent) : QFrame(parent) { OBSSource source = source_; @@ -159,7 +159,7 @@ void SourceSelectButton::setRectVisible(bool visible) thumbnailUpdated(thumbnail->getPixmap()); } } else { - thumbnail.clear(); + thumbnail.reset(); } } diff --git a/frontend/components/SourceSelectButton.hpp b/frontend/components/SourceSelectButton.hpp index 20187cc57490d7..add6790bbc9a0a 100644 --- a/frontend/components/SourceSelectButton.hpp +++ b/frontend/components/SourceSelectButton.hpp @@ -18,13 +18,13 @@ #pragma once +#include + +#include #include #include -#include -#include #include - -#include +#include class Thumbnail; class QLabel; @@ -50,7 +50,7 @@ class SourceSelectButton : public QFrame { private: OBSWeakSource weakSource; - QSharedPointer thumbnail; + std::shared_ptr thumbnail; QPointer image; QPushButton *button = nullptr; diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index c6f6d77c8d18e0..676043bf00cace 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -637,8 +637,8 @@ Basic.Main.PreviewDisabled="Preview is currently disabled" 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.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" diff --git a/frontend/dialogs/OBSBasicSourceSelect.cpp b/frontend/dialogs/OBSBasicSourceSelect.cpp index 5fc75277e68298..50613fb95fe9b9 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.cpp +++ b/frontend/dialogs/OBSBasicSourceSelect.cpp @@ -16,15 +16,16 @@ along with this program. If not, see . ******************************************************************************/ +#include "OBSApp.hpp" #include "OBSBasicSourceSelect.hpp" -#include -#include +#include +#include #include "qt-wrappers.hpp" -#include "OBSApp.hpp" -#include +#include +#include #include "moc_OBSBasicSourceSelect.cpp" diff --git a/frontend/dialogs/OBSBasicSourceSelect.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp index e8851838e7b640..4c563843bd3749 100644 --- a/frontend/dialogs/OBSBasicSourceSelect.hpp +++ b/frontend/dialogs/OBSBasicSourceSelect.hpp @@ -18,16 +18,15 @@ #pragma once +#include #include "ui_OBSBasicSourceSelect.h" -#include #include #include #include #include -#include - +#include #include constexpr int UNVERSIONED_ID_ROLE = Qt::UserRole + 1; diff --git a/frontend/utility/ResizeSignaler.hpp b/frontend/utility/ResizeSignaler.hpp new file mode 100644 index 00000000000000..f3337f734931c6 --- /dev/null +++ b/frontend/utility/ResizeSignaler.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + 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 . +******************************************************************************/ + +#pragma once + +#include + +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); + } +}; diff --git a/frontend/utility/ThumbnailManager.cpp b/frontend/utility/ThumbnailManager.cpp index 74b55fcd58e091..89a0fee2e9deb9 100644 --- a/frontend/utility/ThumbnailManager.cpp +++ b/frontend/utility/ThumbnailManager.cpp @@ -16,11 +16,13 @@ along with this program. If not, see . ******************************************************************************/ -#include "display-helpers.hpp" #include "ThumbnailManager.hpp" + #include #include +#include "display-helpers.hpp" + #include constexpr int MIN_THUMBNAIL_UPDATE_INTERVAL_MS = 100; @@ -28,7 +30,7 @@ constexpr int MIN_SOURCE_UPDATE_INTERVAL_MS = 5000; ThumbnailItem::ThumbnailItem(std::string uuid, OBSSource source) : uuid(uuid), weakSource(OBSGetWeakRef(source)) {} -void ThumbnailItem::init(QWeakPointer weakActiveItem) +void ThumbnailItem::init(std::weak_ptr weakActiveItem) { auto thumbnailManager = OBSBasic::Get()->thumbnails(); if (!thumbnailManager) { @@ -40,7 +42,7 @@ void ThumbnailItem::init(QWeakPointer weakActiveItem) auto &cachedItem = it->second; pixmap = cachedItem.pixmap.value_or(QPixmap()); cachedItem.pixmap.reset(); - cachedItem.weakActiveItem = weakActiveItem; + cachedItem.weakActiveItem = std::move(weakActiveItem); } } @@ -53,7 +55,7 @@ ThumbnailItem::~ThumbnailItem() auto &cachedItem = thumbnailManager->cachedThumbnails[uuid]; cachedItem.pixmap = pixmap; - cachedItem.weakActiveItem.clear(); + cachedItem.weakActiveItem.reset(); } void ThumbnailItem::imageUpdated(QImage image) @@ -79,33 +81,33 @@ ThumbnailManager::ThumbnailManager(QObject *parent) : QObject(parent) ThumbnailManager::~ThumbnailManager() {} -QSharedPointer ThumbnailManager::getThumbnail(OBSSource source) +std::shared_ptr ThumbnailManager::getThumbnail(OBSSource source) { std::string uuid = obs_source_get_uuid(source); for (auto it = thumbnails.begin(); it != thumbnails.end(); ++it) { - auto item = it->toStrongRef(); + auto item = it->lock(); if (item && item->uuid == uuid) { - return QSharedPointer::create(item); + return std::make_shared(item); } } - QSharedPointer thumbnail; + std::shared_ptr thumbnail; if ((obs_source_get_output_flags(source) & OBS_SOURCE_VIDEO) != 0) { - auto item = QSharedPointer::create(uuid, source); - item->init(item.toWeakRef()); + auto item = std::make_shared(uuid, source); + item->init(std::weak_ptr(item)); - thumbnail = QSharedPointer::create(item); + thumbnail = std::make_shared(item); connect(item.get(), &ThumbnailItem::updateThumbnail, thumbnail.get(), &Thumbnail::thumbnailUpdated); - newThumbnails.push_back(item.toWeakRef()); + newThumbnails.push_back(std::weak_ptr(item)); } updateIntervalChanged(thumbnails.size()); return thumbnail; } -bool ThumbnailManager::updatePixmap(QSharedPointer &sharedPointerItem) +bool ThumbnailManager::updatePixmap(std::shared_ptr &sharedPointerItem) { ThumbnailItem *item = sharedPointerItem.get(); @@ -151,13 +153,13 @@ void ThumbnailManager::updateIntervalChanged(size_t newCount) void ThumbnailManager::updateTick() { - QSharedPointer item; + std::shared_ptr item; bool changed = false; bool newThumbnail = false; while (newThumbnails.size() > 0) { changed = true; - item = newThumbnails.front().toStrongRef(); + item = newThumbnails.front().lock(); newThumbnails.pop_front(); if (item) { @@ -168,7 +170,7 @@ void ThumbnailManager::updateTick() if (!item) { while (thumbnails.size() > 0) { - item = thumbnails.front().toStrongRef(); + item = thumbnails.front().lock(); thumbnails.pop_front(); if (item) { break; @@ -185,9 +187,9 @@ void ThumbnailManager::updateTick() } if (updatePixmap(item)) { - thumbnails.push_back(item.toWeakRef()); + thumbnails.push_back(std::weak_ptr(item)); } else { - thumbnails.push_front(item.toWeakRef()); + thumbnails.push_front(std::weak_ptr(item)); } } @@ -200,8 +202,8 @@ std::optional ThumbnailManager::getCachedThumbnail(OBSSource source) if (cachedItem.pixmap.has_value()) { return cachedItem.pixmap; } else { - auto activeItem = cachedItem.weakActiveItem.toStrongRef(); - return !activeItem.isNull() ? std::make_optional(activeItem->pixmap) : std::nullopt; + auto activeItem = cachedItem.weakActiveItem.lock(); + return activeItem ? std::make_optional(activeItem->pixmap) : std::nullopt; } } else { return std::nullopt; diff --git a/frontend/utility/ThumbnailManager.hpp b/frontend/utility/ThumbnailManager.hpp index 333dbdd1629c79..390cefec57cc2c 100644 --- a/frontend/utility/ThumbnailManager.hpp +++ b/frontend/utility/ThumbnailManager.hpp @@ -21,12 +21,12 @@ #include #include -#include #include +#include #include -#include #include +#include class ThumbnailItem : public QObject { Q_OBJECT @@ -38,7 +38,7 @@ class ThumbnailItem : public QObject { OBSWeakSource weakSource; QPixmap pixmap; - void init(QWeakPointer weakActiveItem); + void init(std::weak_ptr weakActiveItem); void imageUpdated(QImage image); public: @@ -57,13 +57,13 @@ class Thumbnail : public QObject { friend class ThumbnailManager; - QSharedPointer item; + std::shared_ptr item; private slots: void thumbnailUpdated(QPixmap pixmap); public: - inline Thumbnail(QSharedPointer item) : item(item) {} + inline Thumbnail(std::shared_ptr item) : item(item) {} inline QPixmap getPixmap() const { return item->pixmap; } @@ -81,15 +81,15 @@ class ThumbnailManager : public QObject { struct CachedItem { std::optional pixmap; - QWeakPointer weakActiveItem; + std::weak_ptr weakActiveItem; }; - QList> newThumbnails; - QList> thumbnails; + std::deque> newThumbnails; + std::deque> thumbnails; std::unordered_map cachedThumbnails; QTimer updateTimer; - bool updatePixmap(QSharedPointer &item); + bool updatePixmap(std::shared_ptr &item); void updateTick(); void updateIntervalChanged(size_t newCount); @@ -98,7 +98,7 @@ class ThumbnailManager : public QObject { explicit ThumbnailManager(QObject *parent = nullptr); ~ThumbnailManager(); - QSharedPointer getThumbnail(OBSSource source); + std::shared_ptr getThumbnail(OBSSource source); std::optional getCachedThumbnail(OBSSource source); void preloadThumbnail(OBSSource source, QObject *object, std::function callback); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index f4553b517ad00e..2dc6bafe1450e0 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -23,10 +23,10 @@ #include #include #include +#include #include #include #include -#include #include #include diff --git a/shared/qt/wrappers/qt-wrappers.cpp b/shared/qt/wrappers/qt-wrappers.cpp index a0b51ae173183f..490f59bb1f8358 100644 --- a/shared/qt/wrappers/qt-wrappers.cpp +++ b/shared/qt/wrappers/qt-wrappers.cpp @@ -373,11 +373,3 @@ void RefreshToolBarStyling(QToolBar *toolBar) widget->style()->polish(widget); } } - -bool ResizeSignaler::eventFilter(QObject *object, QEvent *event) -{ - if (event->type() == QEvent::Resize) { - emit resized(); - } - return QObject::eventFilter(object, event); -} diff --git a/shared/qt/wrappers/qt-wrappers.hpp b/shared/qt/wrappers/qt-wrappers.hpp index b8b50f99c2f1a3..72ab917a5636c7 100644 --- a/shared/qt/wrappers/qt-wrappers.hpp +++ b/shared/qt/wrappers/qt-wrappers.hpp @@ -99,16 +99,3 @@ QStringList OpenFiles(QWidget *parent, QString title, QString path, QString exte void TruncateLabel(QLabel *label, QString newText, int length = MAX_LABEL_LENGTH); void RefreshToolBarStyling(QToolBar *toolBar); - -class ResizeSignaler : public QObject { - Q_OBJECT - -public: - inline ResizeSignaler(QObject *parent) : QObject(parent) {} - -signals: - void resized(); - -protected: - bool eventFilter(QObject *object, QEvent *event) override; -};