diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index 5bcbfecb9..cee19a84c 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -36,6 +36,10 @@ target_sources( components/DisplayCaptureToolbar.cpp components/DisplayCaptureToolbar.hpp components/EditWidget.hpp + components/FlowFrame.cpp + components/FlowFrame.hpp + components/FlowLayout.cpp + components/FlowLayout.hpp components/FocusList.cpp components/FocusList.hpp components/GameCaptureToolbar.cpp @@ -65,6 +69,8 @@ target_sources( components/SceneTree.hpp components/SilentUpdateCheckBox.hpp components/SilentUpdateSpinBox.hpp + components/SourceSelectButton.cpp + components/SourceSelectButton.hpp components/SourceToolbar.cpp components/SourceToolbar.hpp components/SourceTree.cpp diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 8b02d5ec7..aa9c6cd58 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -49,6 +49,7 @@ target_sources( utility/RemuxQueueModel.hpp utility/RemuxWorker.cpp utility/RemuxWorker.hpp + utility/ResizeSignaler.hpp utility/SceneRenameDelegate.cpp utility/SceneRenameDelegate.hpp utility/ScreenshotObj.cpp @@ -58,6 +59,8 @@ target_sources( utility/SimpleOutput.hpp utility/StartMultiTrackVideoStreamingGuard.hpp utility/SurfaceEventFilter.hpp + utility/ThumbnailManager.cpp + utility/ThumbnailManager.hpp utility/VCamConfig.hpp utility/audio-encoders.cpp utility/audio-encoders.hpp diff --git a/frontend/components/FlowFrame.cpp b/frontend/components/FlowFrame.cpp new file mode 100644 index 000000000..3974f1d85 --- /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 000000000..0ad1164ce --- /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 000000000..448e168f9 --- /dev/null +++ b/frontend/components/FlowLayout.cpp @@ -0,0 +1,170 @@ +/****************************************************************************** + Example provided by Qt + + + Copyright (C) 2016 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +******************************************************************************/ + +#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; + int top; + int right; + int bottom; + + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + for (QLayoutItem *item : std::as_const(itemList)) { + const QWidget *wid = item->widget(); + int spaceX = horizontalSpacing(); + if (spaceX == -1) { + spaceX = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton, + Qt::Horizontal); + } + + int spaceY = verticalSpacing(); + if (spaceY == -1) { + spaceY = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton, + Qt::Vertical); + } + + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) { + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + bottom; +} + +int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) { + return -1; + } else if (parent->isWidgetType()) { + QWidget *pw = static_cast(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 000000000..9d3cb32be --- /dev/null +++ b/frontend/components/FlowLayout.hpp @@ -0,0 +1,40 @@ +/****************************************************************************** + Example provided by Qt + + + Copyright (C) 2016 The Qt Company Ltd. + SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +******************************************************************************/ + +#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 000000000..dd08dfaa3 --- /dev/null +++ b/frontend/components/SourceSelectButton.cpp @@ -0,0 +1,185 @@ +/****************************************************************************** + 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); + + setFocusPolicy(Qt::StrongFocus); + 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.reset(); + } + } + + if (preload && !rectVisible) { + OBSBasic::Get()->thumbnails()->preloadThumbnail(source, this, + [=](QPixmap pixmap) { thumbnailUpdated(pixmap); }); + } + preload = false; +} + +void SourceSelectButton::setPreload(bool preload) +{ + this->preload = preload; +} + +void SourceSelectButton::thumbnailUpdated(QPixmap pixmap) +{ + if (!pixmap.isNull()) { + image->setPixmap(pixmap.scaled(160, 90, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } else { + setDefaultThumbnail(); + } +} diff --git a/frontend/components/SourceSelectButton.hpp b/frontend/components/SourceSelectButton.hpp new file mode 100644 index 000000000..083acfa38 --- /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 QLabel; +class Thumbnail; + +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; + std::shared_ptr 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 949d5bf5d..7c75a6c6a 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -637,15 +637,21 @@ RenameProfile.Title="Rename Profile" Basic.Main.MixerRename.Title="Rename Audio Source" Basic.Main.MixerRename.Text="Please enter the name of the audio source" - # preview window disabled Basic.Main.PreviewDisabled="Preview is currently disabled" # add source dialog -Basic.SourceSelect="Create/Select Source" -Basic.SourceSelect.CreateNew="Create new" -Basic.SourceSelect.AddExisting="Add Existing" +Basic.SourceSelect="Add Source" +Basic.SourceSelect.SelectType="Source Type" +Basic.SourceSelect.Recent="Recently Added" +Basic.SourceSelect.NewSource="Create a New Source" +Basic.SourceSelect.Existing="Add an Existing Source" +Basic.SourceSelect.CreateButton="Create New" Basic.SourceSelect.AddVisible="Make source visible" +Basic.SourceSelect.NoExisting="No existing %1 sources" +Basic.SourceSelect.Accessible.SourceName="Source Name" +Basic.SourceSelect.Accessible.Existing="Add an Existing Source" +Basic.SourceSelect.Deprecated.Create="This source type is marked as deprecated and does allow creating new sources." # source box Basic.Main.Sources.Visibility="Visibility" @@ -783,6 +789,7 @@ Basic.Main.ShowContextBar="Show Source Toolbar" Basic.Main.HideContextBar="Hide Source Toolbar" Basic.Main.StopVirtualCam="Stop Virtual Camera" Basic.Main.Group="Group %1" +Basic.Main.NewGroup="New Group" Basic.Main.GroupItems="Group Selected Items" Basic.Main.Ungroup="Ungroup" Basic.Main.GridMode="Grid Mode" diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index e3bc0bc82..224e3397b 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -2415,6 +2415,34 @@ OBSBasicAdvAudio #scrollAreaWidgetContents { border: 1px solid var(--input_border_hover); } +/* Add Source Dialog */ +SourceSelectButton { + text-align: center; + padding: var(--padding_base) var(--padding_base); + margin: var(--spacing_base); +} + +SourceSelectButton QLabel { + padding: var(--padding_large) 0; + text-align: center; +} + +SourceSelectButton #thumbnail { + background: var(--grey6); + border: 1px solid var(--grey4); + padding: 0; + margin-top: var(--spacing_base); +} + +SourceSelectButton QPushButton { + background: var(--grey5); +} + +SourceSelectButton QPushButton:checked:focus, +SourceSelectButton QPushButton:focus { + border-color: var(--white3); +} + /* Idian Widgets */ idian--Group { border-radius: var(--border_radius); diff --git a/frontend/data/themes/Yami_Acri.ovt b/frontend/data/themes/Yami_Acri.ovt index 8d054a4d0..9d4981d93 100644 --- a/frontend/data/themes/Yami_Acri.ovt +++ b/frontend/data/themes/Yami_Acri.ovt @@ -234,3 +234,14 @@ idian--ToggleSwitch { qproperty-background_checked: var(--button_bg); qproperty-background_checked_hover: var(--primary_light); } + +/* Add Source Dialog */ +SourceSelectButton QPushButton, +SourceSelectButton #thumbnail { + border-color: var(--grey3); +} + +SourceSelectButton QPushButton:checked { + background: var(--button_bg_red); + border-color: var(--button_bg_red_hover); +} diff --git a/frontend/data/themes/Yami_Classic.ovt b/frontend/data/themes/Yami_Classic.ovt index 214dd397c..77b9a00ff 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 2cbcbe2f7..550db0321 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: var(--green4); qproperty-backgroundWarningColor: var(--yellow4); @@ -354,3 +364,19 @@ idian--ExpandButton::indicator { idian--ExpandButton::indicator:checked { image: url(theme:Light/up.svg); } + +/* Add Source Dialog */ +SourceSelectButton QPushButton, +SourceSelectButton #thumbnail { + border-color: var(--grey3); +} + +SourceSelectButton QPushButton:checked { + background: var(--primary); + border-color: var(--primary_light); +} + +SourceSelectButton QPushButton:checked:focus, +SourceSelectButton QPushButton:focus { + border-color: var(--black3); +} diff --git a/frontend/data/themes/Yami_Rachni.ovt b/frontend/data/themes/Yami_Rachni.ovt index d51a0d2ae..f79bf42c0 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 44c4ee766..410199bc4 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,12 +18,19 @@ #include "OBSBasicSourceSelect.hpp" -#include +#include +#include +#include + +#include "qt-wrappers.hpp" + +#include +#include #include "moc_OBSBasicSourceSelect.cpp" struct AddSourceData { - /* Input data */ + // Input data obs_source_t *source; bool visible; obs_transform_info *transform = nullptr; @@ -38,85 +46,47 @@ struct AddSourceData { uint32_t hide_transition_duration = 300; OBSData private_settings; - /* Return data */ + // Return data obs_sceneitem_t *scene_item = nullptr; }; -bool OBSBasicSourceSelect::EnumSources(void *data, obs_source_t *source) +namespace { +QString getSourceDisplayName(QString type) { - if (obs_source_is_hidden(source)) - return true; - - OBSBasicSourceSelect *window = static_cast(data); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_unversioned_id(source); - - if (strcmp(id, window->id) == 0) - window->ui->sourceList->addItem(QT_UTF8(name)); - - return true; -} - -bool OBSBasicSourceSelect::EnumGroups(void *data, obs_source_t *source) -{ - OBSBasicSourceSelect *window = static_cast(data); - const char *name = obs_source_get_name(source); - const char *id = obs_source_get_unversioned_id(source); - - if (strcmp(id, window->id) == 0) { - OBSBasic *main = OBSBasic::Get(); - OBSScene scene = main->GetCurrentScene(); - - obs_sceneitem_t *existing = obs_scene_get_group(scene, name); - if (!existing) - window->ui->sourceList->addItem(QT_UTF8(name)); + if (type == "scene") { + return QTStr("Basic.Scene"); } - return true; + const char *inputChar = obs_get_latest_input_type_id(type.toUtf8().constData()); + const char *displayChar = obs_source_get_display_name(inputChar); + std::string displayId = (displayChar) ? displayChar : ""; + + if (!displayId.empty()) { + return QString::fromStdString(displayId); + } else { + return QString(); + } } -void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata) +std::string getNewSourceName(std::string_view name) { - OBSBasicSourceSelect *window = static_cast(data); - obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source"); + std::string newName{name}; + int suffix = 1; - QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source)); -} - -void OBSBasicSourceSelect::OBSSourceRemoved(void *data, calldata_t *calldata) -{ - OBSBasicSourceSelect *window = static_cast(data); - obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source"); - - QMetaObject::invokeMethod(window, "SourceRemoved", Q_ARG(OBSSource, source)); -} - -void OBSBasicSourceSelect::SourceAdded(OBSSource source) -{ - const char *name = obs_source_get_name(source); - const char *sourceId = obs_source_get_unversioned_id(source); - - if (strcmp(sourceId, id) != 0) - return; - - ui->sourceList->addItem(name); -} - -void OBSBasicSourceSelect::SourceRemoved(OBSSource source) -{ - const char *name = obs_source_get_name(source); - const char *sourceId = obs_source_get_unversioned_id(source); - - if (strcmp(sourceId, id) != 0) - return; - - QList items = ui->sourceList->findItems(name, Qt::MatchFixedString); - - if (!items.count()) - return; - - delete items[0]; + for (;;) { + OBSSourceAutoRelease existing_source = obs_get_source_by_name(newName.c_str()); + if (!existing_source) { + break; + } + + char nextName[256]; + std::snprintf(nextName, sizeof(nextName), "%s %d", name.data(), ++suffix); + newName = nextName; + } + + return newName; } +} // namespace static void AddSource(void *_data, obs_scene_t *scene) { @@ -125,16 +95,21 @@ static void AddSource(void *_data, obs_scene_t *scene) sceneitem = obs_scene_add(scene, data->source); - if (data->transform != nullptr) + if (data->transform != nullptr) { obs_sceneitem_set_info2(sceneitem, data->transform); - if (data->crop != nullptr) + } + if (data->crop != nullptr) { obs_sceneitem_set_crop(sceneitem, data->crop); - if (data->blend_method != nullptr) + } + if (data->blend_method != nullptr) { obs_sceneitem_set_blending_method(sceneitem, *data->blend_method); - if (data->blend_mode != nullptr) + } + if (data->blend_mode != nullptr) { obs_sceneitem_set_blending_mode(sceneitem, *data->blend_mode); - if (data->scale_type != nullptr) + } + if (data->scale_type != nullptr) { obs_sceneitem_set_scale_filter(sceneitem, *data->scale_type); + } if (data->show_transition_id && *data->show_transition_id) { OBSSourceAutoRelease source = obs_source_create(data->show_transition_id, data->show_transition_id, @@ -165,40 +140,23 @@ static void AddSource(void *_data, obs_scene_t *scene) data->scene_item = sceneitem; } -char *get_new_source_name(const char *name, const char *format) -{ - struct dstr new_name = {0}; - int inc = 0; - - dstr_copy(&new_name, name); - - for (;;) { - OBSSourceAutoRelease existing_source = obs_get_source_by_name(new_name.array); - if (!existing_source) - break; - - dstr_printf(&new_name, format, name, ++inc + 1); - } - - return new_name.array; -} - static void AddExisting(OBSSource source, bool visible, bool duplicate, SourceCopyInfo *info = nullptr) { OBSBasic *main = OBSBasic::Get(); OBSScene scene = main->GetCurrentScene(); - if (!scene) + if (!scene) { return; + } if (duplicate) { OBSSource from = source; - char *new_name = get_new_source_name(obs_source_get_name(source), "%s %d"); - source = obs_source_duplicate(from, new_name, false); + std::string new_name = getNewSourceName(obs_source_get_name(source)); + source = obs_source_duplicate(from, new_name.c_str(), false); obs_source_release(source); - bfree(new_name); - if (!source) + if (!source) { return; + } } AddSourceData data; @@ -239,8 +197,9 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl OBSBasic *main = OBSBasic::Get(); OBSScene scene = main->GetCurrentScene(); bool success = false; - if (!scene) + if (!scene) { return false; + } OBSSourceAutoRelease source = obs_get_source_by_name(name); if (source && parent) { @@ -262,7 +221,7 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl newSource = source; newSceneItem = data.scene_item; - /* set monitoring if source monitors by default */ + // Set monitoring if source monitors by default uint32_t flags = obs_source_get_output_flags(source); if ((flags & OBS_SOURCE_MONITOR_BY_DEFAULT) != 0) { obs_source_set_monitoring_type(source, OBS_MONITORING_TYPE_MONITOR_ONLY); @@ -275,18 +234,578 @@ bool AddNew(QWidget *parent, const char *id, const char *name, const bool visibl return success; } -void OBSBasicSourceSelect::on_buttonBox_accepted() +OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, undo_stack &undo_s) + : QDialog(parent), + ui(new Ui::OBSBasicSourceSelect), + undo_s(undo_s) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + existingFlowLayout = ui->existingListFrame->flowLayout(); + existingFlowLayout->setContentsMargins(0, 0, 0, 0); + existingFlowLayout->setSpacing(0); + + /* The scroll viewport is not accessible via Designer, so we have to disable autoFillBackground here. + * + * Additionally when Qt calls setWidget on a scrollArea to set the contents widget, it force sets + * autoFillBackground to true overriding whatever is set in Designer so we have to do that here too. + */ + ui->existingScrollArea->viewport()->setAutoFillBackground(false); + ui->existingScrollContents->setAutoFillBackground(false); + + auto resizeSignaler = new ResizeSignaler(ui->existingScrollArea); + ui->existingScrollArea->installEventFilter(resizeSignaler); + + connect(resizeSignaler, &ResizeSignaler::resized, this, &OBSBasicSourceSelect::checkSourceVisibility); + connect(ui->existingScrollArea->verticalScrollBar(), &QScrollBar::valueChanged, this, + &OBSBasicSourceSelect::checkSourceVisibility); + connect(ui->existingScrollArea->horizontalScrollBar(), &QScrollBar::valueChanged, this, + &OBSBasicSourceSelect::checkSourceVisibility); + + ui->createNewFrame->setVisible(false); + ui->deprecatedCreateLabel->setVisible(false); + ui->deprecatedCreateLabel->setProperty("class", "text-muted"); + + getSourceTypes(); + getSources(); + + updateExistingSources(16); + + connect(ui->sourceTypeList, &QListWidget::itemDoubleClicked, this, &OBSBasicSourceSelect::createNewSource); + connect(ui->newSourceName, &QLineEdit::returnPressed, this, &OBSBasicSourceSelect::createNewSource); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(ui->addExistingButton, &QAbstractButton::clicked, this, &OBSBasicSourceSelect::addSelectedSources); + connect(this, &OBSBasicSourceSelect::selectedItemsChanged, this, [=]() { + ui->addExistingButton->setEnabled(selectedItems.size() > 0); + if (selectedItems.size() > 0) { + ui->addExistingButton->setText(QTStr("Add %1 Existing").arg(selectedItems.size())); + } else { + ui->addExistingButton->setText("Add Existing"); + } + }); + + App()->DisableHotkeys(); +} + +OBSBasicSourceSelect::~OBSBasicSourceSelect() +{ + App()->UpdateHotkeyFocusSetting(); +} + +void OBSBasicSourceSelect::checkSourceVisibility() +{ + QList buttons = sourceButtons->buttons(); + + // Allow some room for previous/next rows to make scrolling a bit more seamless + QRect scrollAreaRect(QPoint(0, 0), ui->existingScrollArea->size()); + scrollAreaRect.setTop(scrollAreaRect.top() - Thumbnail::size.width()); + scrollAreaRect.setBottom(scrollAreaRect.bottom() + Thumbnail::size.height()); + + for (QAbstractButton *button : buttons) { + SourceSelectButton *sourceButton = qobject_cast(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) +{ + delete sourceButtons; + sourceButtons = new QButtonGroup(this); + sourceButtons->setExclusive(false); + + std::vector matchingSources{}; + std::copy_if(sources.begin(), sources.end(), std::back_inserter(matchingSources), [this](obs_source_t *source) { + if (!source || obs_source_removed(source)) { + return false; + } + + const char *id = obs_source_get_unversioned_id(source); + QString stringId = QString(id); + + if (stringId.compare("group") == 0) { + return false; + } + + if (sourceTypeId.compare(stringId) == 0 || sourceTypeId.isNull()) { + return true; + } + + return false; + }); + + QWidget *prevTabWidget = ui->sourceTypeList; + + auto createSourceButton = [this, &prevTabWidget](obs_source_t *source) { + SourceSelectButton *newButton = new SourceSelectButton(source, ui->existingListFrame); + std::string name = obs_source_get_name(source); + + existingFlowLayout->addWidget(newButton); + sourceButtons->addButton(newButton->getButton()); + + if (!prevTabWidget) { + setTabOrder(ui->existingListFrame, newButton->getButton()); + } else { + setTabOrder(prevTabWidget, newButton->getButton()); + } + + prevTabWidget = newButton->getButton(); + }; + + bool isReverseListOrder = sourceTypeId.isNull(); + size_t iterationLimit = limit > 0 ? std::min(static_cast(limit), matchingSources.size()) + : matchingSources.size(); + if (isReverseListOrder) { + std::for_each(matchingSources.rbegin(), matchingSources.rbegin() + iterationLimit, createSourceButton); + } else { + std::for_each(matchingSources.begin(), matchingSources.begin() + iterationLimit, createSourceButton); + } + + setTabOrder(prevTabWidget, ui->addExistingContainer); + + connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled); + + ui->existingListFrame->adjustSize(); + QTimer::singleShot(10, this, [this] { checkSourceVisibility(); }); +} + +bool OBSBasicSourceSelect::enumSourcesCallback(void *data, obs_source_t *source) +{ + if (obs_source_is_hidden(source)) { + return true; + } + + OBSBasicSourceSelect *window = static_cast(data); + + 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(); + OBSScene scene = main->GetCurrentScene(); + + obs_sceneitem_t *existing = obs_scene_get_group(scene, name); + if (!existing) { + QPushButton *button = new QPushButton(name); + connect(button, &QPushButton::clicked, window, &OBSBasicSourceSelect::addSelectedSources); + } + } + + return true; +} + +void OBSBasicSourceSelect::OBSSourceAdded(void *data, calldata_t *calldata) +{ + OBSBasicSourceSelect *window = static_cast(data); + obs_source_t *source = (obs_source_t *)calldata_ptr(calldata, "source"); + + QMetaObject::invokeMethod(window, "SourceAdded", Q_ARG(OBSSource, source)); +} + +void OBSBasicSourceSelect::getSourceTypes() +{ + OBSBasic *main = qobject_cast(App()->GetMainWindow()); + + const char *unversioned_type; + const char *type; + + size_t idx = 0; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) { + continue; + } + + QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList); + newItem->setData(Qt::DisplayRole, name); + newItem->setData(UNVERSIONED_ID_ROLE, unversioned_type); + + if ((caps & OBS_SOURCE_DEPRECATED) != 0) { + newItem->setData(DEPRECATED_ROLE, true); + } else { + newItem->setData(DEPRECATED_ROLE, false); + + QIcon icon; + icon = main->GetSourceIcon(type); + newItem->setIcon(icon); + } + } + + QListWidgetItem *newItem = new QListWidgetItem(ui->sourceTypeList); + newItem->setData(Qt::DisplayRole, Str("Basic.Scene")); + newItem->setData(UNVERSIONED_ID_ROLE, "scene"); + + QIcon icon; + icon = main->GetSceneIcon(); + newItem->setIcon(icon); + + ui->sourceTypeList->sortItems(); + + // Shift Deprecated sources to the bottom + QList deprecatedItems; + for (int i = 0; i < ui->sourceTypeList->count(); i++) { + QListWidgetItem *item = ui->sourceTypeList->item(i); + if (!item) { + break; + } + + bool isDeprecated = item->data(DEPRECATED_ROLE).toBool(); + if (isDeprecated) { + ui->sourceTypeList->takeItem(i); + deprecatedItems.append(item); + } + } + + for (auto &item : deprecatedItems) { + ui->sourceTypeList->addItem(item); + } + + QListWidgetItem *allSources = new QListWidgetItem(); + allSources->setData(Qt::DisplayRole, Str("Basic.SourceSelect.Recent")); + allSources->setData(UNVERSIONED_ID_ROLE, QVariant()); + ui->sourceTypeList->insertItem(0, allSources); + + ui->sourceTypeList->setCurrentItem(allSources); + ui->sourceTypeList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + + connect(ui->sourceTypeList, &QListWidget::currentItemChanged, this, &OBSBasicSourceSelect::sourceTypeSelected); +} + +void OBSBasicSourceSelect::setSelectedSourceType(QListWidgetItem *item) +{ + setSelectedSource(nullptr); + QLayout *layout = ui->existingListFrame->flowLayout(); + + // Clear existing buttons when switching types + QLayoutItem *child = nullptr; + while ((child = layout->takeAt(0)) != nullptr) { + if (child->widget()) { + child->widget()->deleteLater(); + } + delete child; + } + + QVariant unversionedIdData = item->data(UNVERSIONED_ID_ROLE); + QVariant deprecatedData = item->data(DEPRECATED_ROLE); + + if (unversionedIdData.isNull()) { + setSelectedSource(nullptr); + sourceTypeId.clear(); + ui->createNewFrame->setVisible(false); + updateExistingSources(16); + return; + } + + QString type = unversionedIdData.toString(); + if (type.compare(sourceTypeId) == 0) { + return; + } + + ui->createNewFrame->setVisible(true); + + bool isDeprecatedType = deprecatedData.toBool(); + ui->newSourceName->setVisible(!isDeprecatedType); + ui->createNewSource->setVisible(!isDeprecatedType); + + ui->deprecatedCreateLabel->setVisible(isDeprecatedType); + + sourceTypeId = type; + + QString placeHolderText{getSourceDisplayName(sourceTypeId)}; + + QString text{placeHolderText}; + int i = 2; + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(text)))) { + text = QString("%1 %2").arg(placeHolderText).arg(i++); + } + + ui->newSourceName->setText(text); + ui->newSourceName->selectAll(); + + if (sourceTypeId.compare("scene") == 0) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + OBSSource curSceneSource = main->GetCurrentSceneSource(); + + delete sourceButtons; + sourceButtons = new QButtonGroup(this); + + int count = main->ui->scenes->count(); + QWidget *prevTabItem = ui->sourceTypeList; + for (int i = 0; i < count; i++) { + QListWidgetItem *item = main->ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + OBSSource sceneSource = obs_scene_get_source(scene); + + if (curSceneSource == sceneSource) { + continue; + } + + SourceSelectButton *newButton = new SourceSelectButton(sceneSource, ui->existingListFrame); + existingFlowLayout->addWidget(newButton); + sourceButtons->addButton(newButton->getButton()); + + setTabOrder(prevTabItem, newButton->getButton()); + prevTabItem = newButton->getButton(); + } + connect(sourceButtons, &QButtonGroup::buttonToggled, this, &OBSBasicSourceSelect::sourceButtonToggled); + + QTimer::singleShot(100, this, [this] { checkSourceVisibility(); }); + + ui->createNewFrame->setVisible(false); + + } else if (sourceTypeId.compare("group") == 0) { + obs_enum_sources(enumGroupsCallback, this); + } else { + updateExistingSources(); + } + + if (layout->count() == 0) { + QLabel *noExisting = new QLabel(); + noExisting->setText(QTStr("Basic.SourceSelect.NoExisting").arg(getSourceDisplayName(sourceTypeId))); + noExisting->setProperty("class", "text-muted"); + layout->addWidget(noExisting); + } +} + +void OBSBasicSourceSelect::OBSSourceRemoved(void *data, calldata_t *calldata) +{ + OBSBasicSourceSelect *window = static_cast(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); + } + + lastSelectedIndex = existingFlowLayout->indexOf(buttonParent); + return; + } else if (shiftDown) { + if (!ctrlDown) { + clearSelectedItems(); + } + sourceButtons->blockSignals(true); + int start = std::min(selectedIndex, lastSelectedIndex); + int end = std::max(selectedIndex, lastSelectedIndex); + for (int i = start; i <= end; i++) { + auto item = existingFlowLayout->itemAt(i); + if (!item) { + continue; + } + + auto widget = item->widget(); + if (!widget) { + continue; + } + + auto entry = dynamic_cast(widget); + if (entry) { + entry->getButton()->setChecked(true); + addSelectedItem(entry); + } + } + sourceButtons->blockSignals(false); + } else { + lastSelectedIndex = existingFlowLayout->indexOf(buttonParent); + + bool reselectItem = selectedItems.size() > 1; + clearSelectedItems(); + if (checked) { + addSelectedItem(buttonParent); + } else if (reselectItem) { + button->setChecked(true); + addSelectedItem(buttonParent); + } + } +} + +void OBSBasicSourceSelect::sourceDropped(QString uuid) +{ + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.toStdString().c_str()); + if (source) { + const char *name = obs_source_get_name(source); + bool visible = ui->sourceVisible->isChecked(); + + addExistingSource(name, visible); + } +} + +void OBSBasicSourceSelect::setSelectedSource(SourceSelectButton *button) +{ + clearSelectedItems(); + addSelectedItem(button); +} + +void OBSBasicSourceSelect::addSelectedItem(SourceSelectButton *button) +{ + if (button == nullptr) { + return; + } + + auto it = std::find(selectedItems.begin(), selectedItems.end(), button); + if (it == selectedItems.end()) { + selectedItems.push_back(button); + emit selectedItemsChanged(); + } +} + +void OBSBasicSourceSelect::removeSelectedItem(SourceSelectButton *button) +{ + if (button == nullptr) { + return; + } + + auto it = std::find(selectedItems.begin(), selectedItems.end(), button); + if (it != selectedItems.end()) { + selectedItems.erase(it); + emit selectedItemsChanged(); + } +} + +void OBSBasicSourceSelect::clearSelectedItems() +{ + if (selectedItems.size() == 0) { + return; + } + + sourceButtons->blockSignals(true); + for (auto &item : selectedItems) { + item->getButton()->setChecked(false); + } + sourceButtons->blockSignals(false); + selectedItems.clear(); + emit selectedItemsChanged(); +} + +void OBSBasicSourceSelect::createNewSource() { - bool useExisting = ui->selectExisting->isChecked(); bool visible = ui->sourceVisible->isChecked(); - if (useExisting) { - QListWidgetItem *item = ui->sourceList->currentItem(); - if (!item) - return; + if (ui->newSourceName->text().isEmpty()) { + return; + } - QString source_name = item->text(); - AddExisting(QT_TO_UTF8(source_name), visible, false); + if (sourceTypeId.isNull()) { + return; + } + + if (sourceTypeId.compare("scene") == 0) { + return; + } + + OBSSceneItem item; + std::string sourceType = sourceTypeId.toStdString(); + const char *id = sourceType.c_str(); + if (!AddNew(this, id, QT_TO_UTF8(ui->newSourceName->text()), visible, newSource, item)) { + return; + } + + if (newSource && strcmp(obs_source_get_id(newSource.Get()), "group") != 0) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + std::string scene_name = obs_source_get_name(main->GetCurrentSceneSource()); + auto undo = [scene_name, main](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + + OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + }; + OBSDataAutoRelease wrapper = obs_data_create(); + obs_data_set_string(wrapper, "id", id); + obs_data_set_int(wrapper, "item_id", obs_sceneitem_get_id(item)); + obs_data_set_string(wrapper, "name", ui->newSourceName->text().toUtf8().constData()); + obs_data_set_bool(wrapper, "visible", visible); + + auto redo = [scene_name, main](const std::string &data) { + OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str()); + main->SetCurrentScene(scene_source.Get(), true); + + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSource source; + OBSSceneItem item; + AddNew(NULL, obs_data_get_string(dat, "id"), obs_data_get_string(dat, "name"), + obs_data_get_bool(dat, "visible"), source, item); + obs_sceneitem_set_id(item, (int64_t)obs_data_get_int(dat, "item_id")); + }; + undo_s.add_action(QTStr("Undo.Add").arg(ui->newSourceName->text()), undo, redo, + std::string(obs_source_get_name(newSource)), std::string(obs_data_get_json(wrapper))); + + main->CreatePropertiesWindow(newSource); + } + close(); +} + +void OBSBasicSourceSelect::addExistingSource(QString name, bool visible) +{ + OBSSourceAutoRelease source = obs_get_source_by_name(name.toStdString().c_str()); + if (source) { + AddExisting(source.Get(), visible, false); OBSBasic *main = OBSBasic::Get(); const char *scene_name = obs_source_get_name(main->GetCurrentSceneSource()); @@ -309,150 +828,48 @@ void OBSBasicSourceSelect::on_buttonBox_accepted() obs_scene_release(scene); }; - auto redo = [scene_name, main, source_name, visible](const std::string &) { + auto redo = [scene_name, main, name, visible](const std::string &) { obs_source_t *scene_source = obs_get_source_by_name(scene_name); main->SetCurrentScene(scene_source, true); obs_source_release(scene_source); - AddExisting(QT_TO_UTF8(source_name), visible, false); + AddExisting(QT_TO_UTF8(name), visible, false); }; - undo_s.add_action(QTStr("Undo.Add").arg(source_name), undo, redo, "", ""); - } else { - if (ui->sourceName->text().isEmpty()) { - OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); - return; - } - - OBSSceneItem item; - if (!AddNew(this, id, QT_TO_UTF8(ui->sourceName->text()), visible, newSource, item)) - return; - - OBSBasic *main = OBSBasic::Get(); - std::string scene_name = obs_source_get_name(main->GetCurrentSceneSource()); - auto undo = [scene_name, main](const std::string &data) { - OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); - obs_source_remove(source); - - OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - }; - OBSDataAutoRelease wrapper = obs_data_create(); - obs_data_set_string(wrapper, "id", id); - obs_data_set_int(wrapper, "item_id", obs_sceneitem_get_id(item)); - obs_data_set_string(wrapper, "name", ui->sourceName->text().toUtf8().constData()); - obs_data_set_bool(wrapper, "visible", visible); - - auto redo = [scene_name, main](const std::string &data) { - OBSSourceAutoRelease scene_source = obs_get_source_by_name(scene_name.c_str()); - main->SetCurrentScene(scene_source.Get(), true); - - OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); - OBSSource source; - OBSSceneItem item; - AddNew(NULL, obs_data_get_string(dat, "id"), obs_data_get_string(dat, "name"), - obs_data_get_bool(dat, "visible"), source, item); - obs_sceneitem_set_id(item, (int64_t)obs_data_get_int(dat, "item_id")); - }; - undo_s.add_action(QTStr("Undo.Add").arg(ui->sourceName->text()), undo, redo, - std::string(obs_source_get_name(newSource)), std::string(obs_data_get_json(wrapper))); + undo_s.add_action(QTStr("Undo.Add").arg(name), undo, redo, "", ""); } - - done(DialogCode::Accepted); } -void OBSBasicSourceSelect::on_buttonBox_rejected() +void OBSBasicSourceSelect::on_createNewSource_clicked(bool) { - done(DialogCode::Rejected); + createNewSource(); } -static inline const char *GetSourceDisplayName(const char *id) +void OBSBasicSourceSelect::addSelectedSources() { - if (strcmp(id, "scene") == 0) - return Str("Basic.Scene"); - else if (strcmp(id, "group") == 0) - return Str("Group"); - const char *v_id = obs_get_latest_input_type_id(id); - return obs_source_get_display_name(v_id); + if (selectedItems.size() == 0) { + return; + } + + bool visible = ui->sourceVisible->isChecked(); + + for (auto &item : selectedItems) { + QString sourceName = item->text(); + addExistingSource(sourceName, visible); + } + close(); } -OBSBasicSourceSelect::OBSBasicSourceSelect(OBSBasic *parent, const char *id_, undo_stack &undo_s) - : QDialog(parent), - ui(new Ui::OBSBasicSourceSelect), - id(id_), - undo_s(undo_s) +void OBSBasicSourceSelect::sourceTypeSelected(QListWidgetItem *current, QListWidgetItem *) { - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - ui->setupUi(this); - - ui->sourceList->setAttribute(Qt::WA_MacShowFocusRect, false); - - QString placeHolderText{QT_UTF8(GetSourceDisplayName(id))}; - - QString text{placeHolderText}; - int i = 2; - OBSSourceAutoRelease source = nullptr; - while ((source = obs_get_source_by_name(QT_TO_UTF8(text)))) { - text = QString("%1 %2").arg(placeHolderText).arg(i++); - } - - ui->sourceName->setText(text); - ui->sourceName->setFocus(); //Fixes deselect of text. - ui->sourceName->selectAll(); - - installEventFilter(CreateShortcutFilter()); - - connect(ui->createNew, &QRadioButton::pressed, this, [&]() { - QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - if (!button->isEnabled()) - button->setEnabled(true); - }); - connect(ui->selectExisting, &QRadioButton::pressed, this, [&]() { - QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - bool enabled = ui->sourceList->selectedItems().size() != 0; - if (button->isEnabled() != enabled) - button->setEnabled(enabled); - }); - connect(ui->sourceList, &QListWidget::itemSelectionChanged, this, [&]() { - QPushButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - if (!button->isEnabled()) - button->setEnabled(true); - }); - - if (strcmp(id_, "scene") == 0) { - OBSBasic *main = OBSBasic::Get(); - OBSSource curSceneSource = main->GetCurrentSceneSource(); - - ui->selectExisting->setChecked(true); - ui->createNew->setChecked(false); - ui->createNew->setEnabled(false); - ui->sourceName->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - int count = main->ui->scenes->count(); - for (int i = 0; i < count; i++) { - QListWidgetItem *item = main->ui->scenes->item(i); - OBSScene scene = GetOBSRef(item); - OBSSource sceneSource = obs_scene_get_source(scene); - - if (curSceneSource == sceneSource) - continue; - - const char *name = obs_source_get_name(sceneSource); - ui->sourceList->addItem(QT_UTF8(name)); - } - } else if (strcmp(id_, "group") == 0) { - obs_enum_sources(EnumGroups, this); - } else { - obs_enum_sources(EnumSources, this); - } + setSelectedSourceType(current); } void OBSBasicSourceSelect::SourcePaste(SourceCopyInfo &info, bool dup) { OBSSource source = OBSGetStrongRef(info.weak_source); - if (!source) + if (!source) { return; + } AddExisting(source, info.visible, dup, &info); } diff --git a/frontend/dialogs/OBSBasicSourceSelect.hpp b/frontend/dialogs/OBSBasicSourceSelect.hpp index 077d8d298..b857f0c06 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 @@ -17,38 +18,75 @@ #pragma once +#include #include "ui_OBSBasicSourceSelect.h" +#include +#include #include #include -#include - +#include #include +constexpr int UNVERSIONED_ID_ROLE = Qt::UserRole + 1; +constexpr int DEPRECATED_ROLE = Qt::UserRole + 2; + class OBSBasicSourceSelect : public QDialog { Q_OBJECT private: std::unique_ptr ui; - 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); - void SourceAdded(OBSSource source); - void SourceRemoved(OBSSource source); + 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 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 07f64ab47..30d7e34fd 100644 --- a/frontend/forms/OBSBasicSourceSelect.ui +++ b/frontend/forms/OBSBasicSourceSelect.ui @@ -3,106 +3,674 @@ OBSBasicSourceSelect - Qt::WindowModal + Qt::NonModal 0 0 - 352 - 314 + 1000 + 614 + + + 0 + 0 + + + + + 16777215 + 1000 + + Basic.SourceSelect - + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - - - - - Basic.SourceSelect.CreateNew - - - true - - - - - - - - - - Basic.SourceSelect.AddExisting - - - - - - - false - - - true - - - - - - - Basic.SourceSelect.AddVisible - - - true - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + 0 + 0 + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + dialog-container + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 0 + 0 + + + + Basic.SourceSelect.SelectType + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 460 + + + + Basic.SourceSelect.SelectType + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + QListView::Adjust + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 17 + 4 + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.AddExisting + + + QFrame::Plain + + + 0 + + + dialog-container dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Basic.SourceSelect.Existing + + + text-title + + + + + + + Qt::NoFocus + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 800 + 510 + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 10 + + + + + + + + Qt::Horizontal + + + + 259 + 10 + + + + + + + + false + + + + 0 + 0 + + + + Add Existing + + + button-primary + + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + dialog-container dialog-frame + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.NewSource + + + text-title + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Basic.SourceSelect.Deprecated.Create + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Basic.SourceSelect.Accessible.SourceName + + + + + + + + 0 + 0 + + + + Basic.SourceSelect.CreateButton + + + button-primary margin-left + + + + + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + dialog-container + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Basic.SourceSelect.AddVisible + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + QDialogButtonBox::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 +
+
+ + sourceTypeList + existingScrollArea + addExistingButton + newSourceName + createNewSource + sourceVisible + + + + + diff --git a/frontend/utility/ResizeSignaler.hpp b/frontend/utility/ResizeSignaler.hpp new file mode 100644 index 000000000..f3337f734 --- /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/ScreenshotObj.cpp b/frontend/utility/ScreenshotObj.cpp index 0fff65d6f..3ff65be99 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 @@ -16,6 +17,7 @@ ******************************************************************************/ #include "ScreenshotObj.hpp" +#include "display-helpers.hpp" #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,34 @@ void ScreenshotObj::Screenshot() #endif const enum gs_color_format format = gs_get_format_from_space(space); - texrender = gs_texrender_create(format, GS_ZS_NONE); - stagesurf = gs_stagesurface_create(cx, cy, format); + outputWidth = customSize.isValid() ? customSize.width() : sourceWidth; + outputHeight = customSize.isValid() ? customSize.height() : sourceHeight; - if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) { + texrender = gs_texrender_create(format, GS_ZS_NONE); + stagesurf = gs_stagesurface_create(outputWidth, outputHeight, format); + + if (gs_texrender_begin_with_color_space(texrender, outputWidth, outputHeight, space)) { vec4 zero; vec4_zero(&zero); + int x; + int y; + int scaledWidth; + int scaledHeight; + float scale; + + GetScaleAndCenterPos(sourceWidth, sourceHeight, outputWidth, outputHeight, x, y, scale); + + scaledWidth = int(scale * float(sourceWidth)); + scaledHeight = int(scale * float(sourceHeight)); + gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0); - gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f); + + gs_viewport_push(); + gs_projection_push(); + + gs_ortho(0.0f, (float)sourceWidth, 0.0f, (float)sourceHeight, -100.0f, 100.0f); + gs_set_viewport(x, y, scaledWidth, scaledHeight); gs_blend_state_push(); gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); @@ -115,35 +134,60 @@ void ScreenshotObj::Screenshot() obs_render_main_texture(); } + gs_projection_pop(); + gs_viewport_pop(); + gs_blend_state_pop(); gs_texrender_end(texrender); } } -void ScreenshotObj::Download() +void ScreenshotObj::processStage() +{ + switch (stage_) { + case Stage::Render: + renderScreenshot(); + stage_ = Stage::Download; + break; + case Stage::Download: + downloadData(); + stage_ = Stage::Output; + break; + case Stage::Output: + copyData(); + QMetaObject::invokeMethod(this, &ScreenshotObj::handleSave, Qt::QueuedConnection); + obs_remove_tick_callback(renderTick, this); + stage_ = Stage::Finished; + break; + case Stage::Finished: + break; + } +} + +void ScreenshotObj::downloadData() { gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender)); } -void ScreenshotObj::Copy() +void ScreenshotObj::copyData() { uint8_t *videoData = nullptr; uint32_t videoLinesize = 0; if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) { if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) { - const uint32_t linesize = cx * 8; - half_bytes.reserve(cx * cy * 8); + const uint32_t linesize = outputWidth * 8; + half_bytes.reserve(outputWidth * outputHeight * 8); - for (uint32_t y = 0; y < cy; y++) { + for (uint32_t y = 0; y < outputHeight; y++) { const uint8_t *const line = videoData + (y * videoLinesize); half_bytes.insert(half_bytes.end(), line, line + linesize); } } else { - image = QImage(cx, cy, QImage::Format::Format_RGBX8888); + image = QImage(outputWidth, outputHeight, QImage::Format::Format_RGBX8888); int linesize = image.bytesPerLine(); - for (int y = 0; y < (int)cy; y++) + for (int y = 0; y < (int)outputHeight; y++) memcpy(image.scanLine(y), videoData + (y * videoLinesize), linesize); } @@ -151,8 +195,13 @@ void ScreenshotObj::Copy() } } -void ScreenshotObj::Save() +void ScreenshotObj::saveToFile() { + if (!outputToFile) { + QMetaObject::invokeMethod(this, &ScreenshotObj::onFinished, Qt::QueuedConnection); + return; + } + OBSBasic *main = OBSBasic::Get(); config_t *config = main->Config(); @@ -171,7 +220,10 @@ void ScreenshotObj::Save() path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists, GetFormatString(filenameFormat, "Screenshot", nullptr).c_str()); - th = std::thread([this] { MuxAndFinish(); }); + th = std::thread([this] { + muxFile(); + QMetaObject::invokeMethod(this, &ScreenshotObj::onFinished, Qt::QueuedConnection); + }); } #ifdef _WIN32 @@ -185,36 +237,44 @@ static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t value.vt = VT_BOOL; value.bVal = TRUE; HRESULT hr = options->Write(1, &bag, &value); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } hr = frameEncode->Initialize(options); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } hr = frameEncode->SetSize(cx, cy); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } hr = frameEncode->SetResolution(72, 72); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf; hr = frameEncode->SetPixelFormat(&pixelFormat); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } - if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) + if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) { return E_FAIL; + } hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } hr = frameEncode->Commit(); - if (FAILED(hr)) + if (FAILED(hr)) { return hr; + } return S_OK; } @@ -224,43 +284,50 @@ static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy) Microsoft::WRL::ComPtr 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 +337,50 @@ void ScreenshotObj::MuxAndFinish() wchar_t *path_w = nullptr; os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w); if (path_w) { - SaveJxr(path_w, half_bytes.data(), cx, cy); + SaveJxr(path_w, half_bytes.data(), outputWidth, outputHeight); bfree(path_w); } -#endif // #ifdef _WIN32 +#endif } - - deleteLater(); } -#define STAGE_SCREENSHOT 0 -#define STAGE_DOWNLOAD 1 -#define STAGE_COPY_AND_SAVE 2 -#define STAGE_FINISH 3 - -static void ScreenshotTick(void *param, float) +void ScreenshotObj::onFinished() { - ScreenshotObj *data = static_cast(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(); - - data->stage++; + this->deleteLater(); +} + +void ScreenshotObj::setSize(QSize size) +{ + customSize = size; +} + +void ScreenshotObj::setSize(int width, int height) +{ + setSize(QSize(width, height)); +} + +void ScreenshotObj::setSaveToFile(bool save) +{ + outputToFile = save; +} + +void ScreenshotObj::handleSave() +{ + saveToFile(); } diff --git a/frontend/utility/ScreenshotObj.hpp b/frontend/utility/ScreenshotObj.hpp index 01a4fefd8..56e128a08 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 @@ public: 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 000000000..412b831fb --- /dev/null +++ b/frontend/utility/ThumbnailManager.cpp @@ -0,0 +1,256 @@ +/****************************************************************************** + 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 "ThumbnailManager.hpp" + +#include +#include + +#include "display-helpers.hpp" + +#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(std::weak_ptr weakActiveItem) +{ + auto thumbnailManager = OBSBasic::Get()->thumbnails(); + if (!thumbnailManager) { + return; + } + + auto it = thumbnailManager->cachedThumbnails.find(uuid); + if (it != thumbnailManager->cachedThumbnails.end()) { + auto &cachedItem = it->second; + pixmap = cachedItem.pixmap.value_or(QPixmap()); + cachedItem.pixmap.reset(); + cachedItem.weakActiveItem = std::move(weakActiveItem); + } +} + +ThumbnailItem::~ThumbnailItem() +{ + auto thumbnailManager = OBSBasic::Get()->thumbnails(); + if (!thumbnailManager) { + return; + } + + auto &cachedItem = thumbnailManager->cachedThumbnails[uuid]; + cachedItem.pixmap = pixmap; + cachedItem.weakActiveItem.reset(); +} + +void ThumbnailItem::imageUpdated(QImage image) +{ + QPixmap newPixmap; + if (!image.isNull()) { + newPixmap = QPixmap::fromImage(image); + } + + pixmap = newPixmap; + emit updateThumbnail(pixmap); +} + +void Thumbnail::thumbnailUpdated(QPixmap pixmap) +{ + emit updateThumbnail(pixmap); +} + +ThumbnailManager::ThumbnailManager(QObject *parent) : QObject(parent) +{ + connect(&updateTimer, &QTimer::timeout, this, &ThumbnailManager::updateTick); +} + +ThumbnailManager::~ThumbnailManager() {} + +std::shared_ptr ThumbnailManager::getThumbnail(OBSSource source) +{ + std::string uuid = obs_source_get_uuid(source); + + for (auto it = thumbnails.begin(); it != thumbnails.end(); ++it) { + auto item = it->lock(); + if (item && item->uuid == uuid) { + return std::make_shared(item); + } + } + + std::shared_ptr thumbnail; + if ((obs_source_get_output_flags(source) & OBS_SOURCE_VIDEO) != 0) { + auto item = std::make_shared(uuid, source); + item->init(std::weak_ptr(item)); + + thumbnail = std::make_shared(item); + connect(item.get(), &ThumbnailItem::updateThumbnail, thumbnail.get(), &Thumbnail::thumbnailUpdated); + + newThumbnails.push_back(std::weak_ptr(item)); + } + + updateIntervalChanged(thumbnails.size()); + return thumbnail; +} + +bool ThumbnailManager::updatePixmap(std::shared_ptr &sharedPointerItem) +{ + ThumbnailItem *item = sharedPointerItem.get(); + + OBSSource source = OBSGetStrongRef(item->weakSource); + if (!source) { + return true; + } + + QPixmap pixmap; + item->pixmap = pixmap; + + if (source) { + uint32_t sourceWidth = obs_source_get_width(source); + uint32_t sourceHeight = obs_source_get_height(source); + + if (sourceWidth == 0 || sourceHeight == 0) { + return true; + } + + auto obj = new ScreenshotObj(source); + obj->setSaveToFile(false); + obj->setSize(Thumbnail::size); + + connect(obj, &ScreenshotObj::imageReady, item, &ThumbnailItem::imageUpdated); + } + + return true; +} + +void ThumbnailManager::updateIntervalChanged(size_t newCount) +{ + int intervalMS = MIN_THUMBNAIL_UPDATE_INTERVAL_MS; + if (newThumbnails.size() == 0 && newCount > 0) { + int count = (int)newCount; + intervalMS = MIN_SOURCE_UPDATE_INTERVAL_MS / count; + if (intervalMS < MIN_THUMBNAIL_UPDATE_INTERVAL_MS) { + intervalMS = MIN_THUMBNAIL_UPDATE_INTERVAL_MS; + } + } + + updateTimer.start(intervalMS); +} + +void ThumbnailManager::updateTick() +{ + std::shared_ptr item; + bool changed = false; + bool newThumbnail = false; + + while (newThumbnails.size() > 0) { + changed = true; + item = newThumbnails.front().lock(); + + newThumbnails.pop_front(); + if (item) { + newThumbnail = true; + break; + } + } + + if (!item) { + while (thumbnails.size() > 0) { + item = thumbnails.front().lock(); + thumbnails.pop_front(); + if (item) { + break; + } else { + changed = true; + } + } + } + if (changed && newThumbnails.size() == 0) { + updateIntervalChanged(thumbnails.size() + (item ? 1 : 0)); + } + if (!item) { + return; + } + + if (updatePixmap(item)) { + thumbnails.push_back(std::weak_ptr(item)); + } else { + thumbnails.push_front(std::weak_ptr(item)); + } +} + +std::optional ThumbnailManager::getCachedThumbnail(OBSSource source) +{ + if (!source) { + return std::nullopt; + } + + std::string uuid = obs_source_get_uuid(source); + auto it = cachedThumbnails.find(uuid); + if (it != cachedThumbnails.end()) { + auto &cachedItem = it->second; + if (cachedItem.pixmap.has_value()) { + return cachedItem.pixmap; + } + + auto activeItem = cachedItem.weakActiveItem.lock(); + return activeItem ? std::make_optional(activeItem->pixmap) : std::nullopt; + } + + return std::nullopt; +} + +void ThumbnailManager::preloadThumbnail(OBSSource source, QObject *object, std::function callback) +{ + if (!source) { + return; + } + + std::string uuid = obs_source_get_uuid(source); + + if (cachedThumbnails.find(uuid) == cachedThumbnails.end()) { + uint32_t sourceWidth = obs_source_get_width(source); + uint32_t sourceHeight = obs_source_get_height(source); + + cachedThumbnails[uuid].pixmap = QPixmap(); + if (sourceWidth == 0 || sourceHeight == 0) { + return; + } + + auto obj = new ScreenshotObj(source); + obj->setSaveToFile(false); + obj->setSize(Thumbnail::size); + + QPointer safeObject = qobject_cast(object); + connect(obj, &ScreenshotObj::imageReady, this, [=](QImage image) { + QPixmap pixmap; + if (!image.isNull()) { + pixmap = QPixmap::fromImage(image); + } + cachedThumbnails[uuid].pixmap = pixmap; + + QMetaObject::invokeMethod( + safeObject, + [safeObject, callback, pixmap]() { + if (safeObject) { + callback(pixmap); + } + }, + Qt::QueuedConnection); + }); + } +} diff --git a/frontend/utility/ThumbnailManager.hpp b/frontend/utility/ThumbnailManager.hpp new file mode 100644 index 000000000..1c7fea2b2 --- /dev/null +++ b/frontend/utility/ThumbnailManager.hpp @@ -0,0 +1,106 @@ +/****************************************************************************** + 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(std::weak_ptr weakActiveItem); + void imageUpdated(QImage image); + +public: + ThumbnailItem(std::string uuid, OBSSource source); + ~ThumbnailItem(); + + inline bool isNull() const { return !weakSource || obs_weak_source_expired(weakSource); } + inline const std::string &getUuid() const { return uuid; } + +signals: + void updateThumbnail(QPixmap pixmap); +}; + +class Thumbnail : public QObject { + Q_OBJECT + + friend class ThumbnailManager; + + std::shared_ptr item; + +private slots: + void thumbnailUpdated(QPixmap pixmap); + +public: + inline Thumbnail(std::shared_ptr item) : item(item) {} + + inline QPixmap getPixmap() const { return item->pixmap; } + + static constexpr QSize size = {320, 180}; + +signals: + void updateThumbnail(QPixmap pixmap); +}; + +class ThumbnailManager : public QObject { + Q_OBJECT + + friend class ThumbnailItem; + + struct CachedItem { + std::optional pixmap; + std::weak_ptr weakActiveItem; + }; + + std::deque> newThumbnails; + std::deque> thumbnails; + std::unordered_map cachedThumbnails; + QTimer updateTimer; + + bool updatePixmap(std::shared_ptr &item); + void updateTick(); + + void updateIntervalChanged(size_t newCount); + +public: + explicit ThumbnailManager(QObject *parent = nullptr); + ~ThumbnailManager(); + + std::shared_ptr 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 f08527cbc..bef205694 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -243,6 +243,8 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new api = InitializeAPIInterface(this); + thumbnailManager = new ThumbnailManager(this); + ui->setupUi(this); ui->previewDisabledWidget->setVisible(false); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index b6ccb48f3..07cd02ff1 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ class OBSBasicAdvAudio; class OBSBasicFilters; class OBSBasicInteraction; class OBSBasicProperties; +class OBSBasicSourceSelect; class OBSBasicTransform; class OBSLogViewer; class OBSMissingFiles; @@ -282,6 +284,8 @@ private: // TODO: Remove, orphaned instance method void LoadProject(); + ThumbnailManager *thumbnailManager = nullptr; + public slots: void close(); void UpdatePatronJson(const QString &text, const QString &error); @@ -316,6 +320,8 @@ public: inline bool isClosePromptOpen() { return isClosePromptOpen_; } void closeWindow(); + ThumbnailManager *thumbnails() const { return thumbnailManager; } + protected: bool isReadyToClose(); bool promptToClose(); @@ -471,6 +477,9 @@ private: void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; +signals: + void sourceUuidDropped(QString uuid); + /* ------------------------------------- * MARK: - OBSBasic_Hotkeys * ------------------------------------- @@ -566,6 +575,7 @@ private: QPointer advAudioWindow; QPointer filters; QPointer about; + QPointer addWindow; QPointer logView; QPointer stats; QPointer remux; @@ -1168,11 +1178,8 @@ private: static void SourceRemoved(void *data, calldata_t *params); static void SourceRenamed(void *data, calldata_t *params); - void AddSource(const char *id); - QMenu *CreateAddSourcePopupMenu(); - void AddSourcePopupMenu(const QPoint &pos); - private slots: + void AddSourceDialog(); void RenameSources(OBSSource source, QString newName, QString prevName); void ReorderSources(OBSScene scene); diff --git a/frontend/widgets/OBSBasic_Dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp index 53c2f3e4f..4e28ecfe4 100644 --- a/frontend/widgets/OBSBasic_Dropfiles.cpp +++ b/frontend/widgets/OBSBasic_Dropfiles.cpp @@ -214,6 +214,10 @@ void OBSBasic::AddDropSource(const char *data, DropType image) void OBSBasic::dragEnterEvent(QDragEnterEvent *event) { + if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) { + event->acceptProposedAction(); + } + // refuse drops of our own widgets if (event->source() != nullptr) { event->setDropAction(Qt::IgnoreAction); @@ -324,5 +328,10 @@ void OBSBasic::dropEvent(QDropEvent *event) } } else if (mimeData->hasText()) { AddDropSource(QT_TO_UTF8(mimeData->text()), DropType_RawText); + } else if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) { + QString uuid = QString::fromUtf8(event->mimeData()->data("application/x-obs-source-uuid")); + + emit sourceUuidDropped(uuid); + event->acceptProposedAction(); } } diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp index 84eed637d..79dd80bbd 100644 --- a/frontend/widgets/OBSBasic_SceneItems.cpp +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -49,6 +49,25 @@ void setHiddenInMixer(obs_source_t *source, bool hidden) OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); obs_data_set_bool(priv_settings, "mixer_hidden", hidden); } + +std::string getNewSourceName(std::string_view name) +{ + std::string newName{name}; + int suffix = 1; + + for (;;) { + OBSSourceAutoRelease existing_source = obs_get_source_by_name(newName.c_str()); + if (!existing_source) { + break; + } + + char nextName[256]; + std::snprintf(nextName, sizeof(nextName), "%s (%d)", name.data(), ++suffix); + newName = nextName; + } + + return newName; +} } // namespace static inline bool HasAudioDevices(const char *source_id) @@ -200,8 +219,6 @@ void OBSBasic::SourceRenamed(void *data, calldata_t *params) blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); } -extern char *get_new_source_name(const char *name, const char *format); - void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) { bool disable = deviceId && strcmp(deviceId, "disabled") == 0; @@ -222,11 +239,11 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, cons } } else if (!disable) { - BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + std::string name = getNewSourceName(deviceDesc); settings = obs_data_create(); obs_data_set_string(settings, "device_id", deviceId); - source = obs_source_create(sourceId, name, settings, nullptr); + source = obs_source_create(sourceId, name.c_str(), settings, nullptr); obs_set_output_source(channel, source); } @@ -570,10 +587,14 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) } // Add new source - QPointer addSourceMenu = CreateAddSourcePopupMenu(); - if (addSourceMenu) { - popup.addMenu(addSourceMenu); - popup.addSeparator(); + QAction *addSource = popup.addAction(QTStr("AddSource"), this, SLOT(AddSourceDialog())); + popup.addAction(addSource); + popup.addSeparator(); + + if (!preview && !sourceSelected) { + QAction *addGroup = new QAction(QTStr("Basic.Main.NewGroup"), this); + connect(addGroup, &QAction::triggered, ui->sources, &SourceTree::AddGroup); + popup.addAction(addGroup); } // Preview menu entries @@ -689,14 +710,12 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) // Source grouping if (ui->sources->MultipleBaseSelected()) { - popup.addSeparator(); popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); - - } else if (ui->sources->GroupsSelected()) { popup.addSeparator(); + } else if (ui->sources->GroupsSelected()) { popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + popup.addSeparator(); } - popup.addSeparator(); popup.addAction(ui->actionCopySource); popup.addAction(ui->actionPasteRef); @@ -776,115 +795,27 @@ static inline bool should_show_properties(obs_source_t *source, const char *id) return true; } -void OBSBasic::AddSource(const char *id) +void OBSBasic::AddSourceDialog() { - if (id && *id) { - OBSBasicSourceSelect sourceSelect(this, id, undo_s); - sourceSelect.exec(); - if (should_show_properties(sourceSelect.newSource, id)) { - CreatePropertiesWindow(sourceSelect.newSource); - } - } -} - -QMenu *OBSBasic::CreateAddSourcePopupMenu() -{ - const char *unversioned_type; - const char *type; - bool foundValues = false; - bool foundDeprecated = false; - size_t idx = 0; - - QMenu *popup = new QMenu(QTStr("AddSource"), this); - QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); - - auto getActionAfter = [](QMenu *menu, const QString &name) { - QList actions = menu->actions(); - - for (QAction *menuAction : actions) { - if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) - return menuAction; - } - - return (QAction *)nullptr; - }; - - auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { - QString qname = QT_UTF8(name); - QAction *popupItem = new QAction(qname, this); - connect(popupItem, &QAction::triggered, this, [this, type]() { AddSource(type); }); - - QIcon icon; - - if (strcmp(type, "scene") == 0) - icon = GetSceneIcon(); - else - icon = GetSourceIcon(type); - - popupItem->setIcon(icon); - - QAction *after = getActionAfter(popup, qname); - popup->insertAction(after, popupItem); - }; - - while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { - const char *name = obs_source_get_display_name(type); - uint32_t caps = obs_get_source_output_flags(type); - - if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) - continue; - - if ((caps & OBS_SOURCE_DEPRECATED) == 0) { - addSource(popup, unversioned_type, name); - } else { - addSource(deprecated, unversioned_type, name); - foundDeprecated = true; - } - foundValues = true; - } - - addSource(popup, "scene", Str("Basic.Scene")); - - popup->addSeparator(); - QAction *addGroup = new QAction(QTStr("Group"), this); - addGroup->setIcon(GetGroupIcon()); - connect(addGroup, &QAction::triggered, this, [this]() { AddSource("group"); }); - popup->addAction(addGroup); - - if (!foundDeprecated) { - delete deprecated; - deprecated = nullptr; - } - - if (!foundValues) { - delete popup; - popup = nullptr; - - } else if (foundDeprecated) { - popup->addSeparator(); - popup->addMenu(deprecated); - } - - return popup; -} - -void OBSBasic::AddSourcePopupMenu(const QPoint &pos) -{ - if (!GetCurrentScene()) { - // Tell the user he needs a scene first (help beginners). - OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), - QTStr("Basic.Main.AddSourceHelp.Text")); + QAction *action = qobject_cast(sender()); + if (!action) { return; } - QScopedPointer popup(CreateAddSourcePopupMenu()); - if (popup) - popup->exec(pos); + if (addWindow) { + addWindow->close(); + } + + addWindow = new OBSBasicSourceSelect(this, undo_s); + addWindow->show(); + + addWindow->setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, &OBSBasic::sourceUuidDropped, addWindow, &OBSBasicSourceSelect::sourceDropped); } void OBSBasic::on_actionAddSource_triggered() { - AddSourcePopupMenu(QCursor::pos()); + AddSourceDialog(); } static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param)