From 204483360882342d8cce40f2ab4947a2d74e4637 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 00:00:21 -0400 Subject: [PATCH 1/7] frontend: Refactor Idian widgets The idian widgets were built when we had a much lower understanding of how Qt expects you to build custom complex widgets and without a direct use-case. They were also built far too rigid for what every attempted use of them thus far has necessitated. These changes break them into simpler 'pieces' and provide getters for internals that are intended to be accessed. --- frontend/data/themes/Yami.obt | 86 ++--- frontend/dialogs/OBSIdianPlayground.cpp | 102 +++--- frontend/widgets/OBSBasic.hpp | 4 + frontend/widgets/OBSBasic_MainControls.cpp | 9 +- shared/qt/idian/CMakeLists.txt | 14 +- shared/qt/idian/components/ExpandButton.cpp | 52 +++ shared/qt/idian/components/RowInfo.cpp | 93 ++++++ shared/qt/idian/components/ToggleSwitch.cpp | 2 + .../idian/include/Idian/CollapsibleGroup.hpp | 67 ++++ .../qt/idian/include/Idian/ExpandButton.hpp | 44 +++ shared/qt/idian/include/Idian/Idian.hpp | 3 +- .../Idian/{Group.hpp => ListHeader.hpp} | 30 +- shared/qt/idian/include/Idian/Row.hpp | 145 ++------- shared/qt/idian/include/Idian/RowInfo.hpp | 54 ++++ .../Idian/{PropertiesList.hpp => RowList.hpp} | 22 +- shared/qt/idian/include/Idian/Utils.cpp | 3 + shared/qt/idian/widgets/CollapsibleGroup.cpp | 107 +++++++ shared/qt/idian/widgets/Group.cpp | 126 -------- shared/qt/idian/widgets/ListHeader.cpp | 101 ++++++ shared/qt/idian/widgets/Row.cpp | 297 ++---------------- .../{PropertiesList.cpp => RowList.cpp} | 48 +-- 21 files changed, 728 insertions(+), 681 deletions(-) create mode 100644 shared/qt/idian/components/ExpandButton.cpp create mode 100644 shared/qt/idian/components/RowInfo.cpp create mode 100644 shared/qt/idian/include/Idian/CollapsibleGroup.hpp create mode 100644 shared/qt/idian/include/Idian/ExpandButton.hpp rename shared/qt/idian/include/Idian/{Group.hpp => ListHeader.hpp} (69%) create mode 100644 shared/qt/idian/include/Idian/RowInfo.hpp rename shared/qt/idian/include/Idian/{PropertiesList.hpp => RowList.hpp} (75%) create mode 100644 shared/qt/idian/widgets/CollapsibleGroup.cpp delete mode 100644 shared/qt/idian/widgets/Group.cpp create mode 100644 shared/qt/idian/widgets/ListHeader.cpp rename shared/qt/idian/widgets/{PropertiesList.cpp => RowList.cpp} (70%) diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index e1f26ef14606ad..4089f95c48ddff 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -221,7 +221,6 @@ --action_row_border: 3px; --action_row_input_width: calc(var(--action_row_base) * 4); --action_row_collapse: calc(var(--action_row_base) + var(--padding_large)); - --action_row_collapse_radius: calc(var(--action_row_collapse) / 2); --action_row_padding: calc(var(--padding_large) * 1.5); --action_row_padding_x: calc(var(--action_row_padding) * 2); --action_row_padding_nested: calc(var(--action_row_padding_x) * 1.5); @@ -2489,25 +2488,25 @@ SourceSelectButton:pressed:hover { } /* Idian Widgets */ -idian--Group { +idian--ListHeader { border-radius: var(--border_radius); font-weight: bold; margin: 0 0 var(--spacing_base); + padding: 0 var(--padding_base); min-width: 300px; - max-width: 600px; } -idian--Group .header .title { +idian--ListHeader .title { font-weight: bold; padding: var(--padding_large) 0; } -idian--Group .header .description { +idian--ListHeader .description { color: var(--text_muted); padding: var(--spacing_small) 0; } -idian--PropertiesList { +idian--RowList { border-width: 0; padding: 0; margin: var(--spacing_base) 0; @@ -2517,11 +2516,12 @@ idian--Row { background: var(--grey5); margin: 0; padding: var(--action_row_padding) var(--action_row_padding_x); + border-radius: var(--border_radius); } idian--Row.keyFocus { background: var(--grey4); - border: var(--highlight_width) solid var(--grey4); + border: var(--highlight_width) solid var(--highlight_color); } idian--Row.cursor-pointer.hover { @@ -2529,17 +2529,21 @@ idian--Row.cursor-pointer.hover { border: var(--highlight_width) solid var(--grey1); } -idian--Row.first { +idian--RowList idian--Row { + border-radius: 0; +} + +idian--RowList idian--Row.first { border-top-left-radius: var(--border_radius); border-top-right-radius: var(--border_radius); } -idian--Row.last { +idian--RowList idian--Row.last { border-bottom-left-radius: var(--border_radius); border-bottom-right-radius: var(--border_radius); } -idian--Row > QLabel.description { +idian--RowInfo QLabel.description { font-size: var(--font_small); color: var(--text_muted); } @@ -2636,7 +2640,7 @@ idian--Row QDoubleSpinBox { padding: var(--padding_base) var(--action_row_padding_x); } -idian--PropertiesListSpacer { +idian--RowListSpacer { max-height: var(--spacing_small); min-height: var(--spacing_small); background-color: var(--bg_window); @@ -2673,35 +2677,54 @@ idian--CheckBox.keyFocus.checked::indicator { border-color: var(--highlight_color); } -idian--CollapsibleRow { +idian--CollapsibleGroup { margin: 0; padding: 0; border: none; } -idian--CollapsibleRow.keyFocus { +idian--CollapsibleGroup.expanded > idian--Row { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +idian--CollapsibleGroup idian--Row.keyFocus { border: var(--highlight_width) solid var(--highlight_color); } -idian--CollapsibleRow idian--PropertiesList { +idian--CollapsibleGroup idian--RowList { border-radius: 0; border-left: 1px solid var(--grey5); border-right: 1px solid var(--grey5); border-bottom: 1px solid var(--grey5); + border-bottom: 1px solid var(--grey5); margin: var(--spacing_small) 0px 0px; } -idian--CollapsibleRow idian--PropertiesList idian--Row { +idian--CollapsibleGroup idian--RowList idian--Row { background-color: var(--grey6); padding-left: var(--action_row_padding_nested); } -idian--CollapsibleRow idian--Row.first, -idian--CollapsibleRow idian--Row.last { +idian--CollapsibleGroup idian--RowList idian--Row.first { + border-radius: 0; +} + +idian--CollapsibleGroup idian--RowList idian--Row.last { + border-bottom-left-radius: var(--border_radius); + border-bottom-right-radius: var(--border_radius); +} + +idian--RowList idian--CollapsibleGroup idian--Row.last { border-radius: 0; } -idian--CollapsibleRow idian--PropertiesList idian--ToggleSwitch { +idian--RowList idian--CollapsibleGroup.last idian--Row.last { + border-bottom-left-radius: var(--border_radius); + border-bottom-right-radius: var(--border_radius); +} + +idian--CollapsibleGroup idian--RowList idian--ToggleSwitch { qproperty-background: var(--grey7); qproperty-background_hover: var(--grey6); } @@ -2717,7 +2740,7 @@ idian--ExpandButton { idian--ExpandButton::indicator { background: var(--grey5); - border-radius: var(--action_row_collapse_radius); + border-radius: var(--border_radius); padding: var(--padding_large); image: url(theme:Dark/down.svg); border: var(--highlight_width) solid var(--grey5); @@ -2728,32 +2751,13 @@ idian--ExpandButton::indicator:checked { } idian--ExpandButton.keyFocus, -idian--ExpandButton.keyFocus::indicator { +idian--Row.keyFocus idian--ExpandButton::indicator { border-color: var(--highlight_color); } -idian--RowFrame .btn-frame { - background: var(--grey5); - padding: var(--action_row_padding) var(--action_row_padding_x); -} - -idian--RowFrame.hover .btn-frame { - background: var(--grey4); -} - -idian--RowFrame.hover idian--Row { +idian--ExpandButton.hover::indicator, +idian--Row.hover idian--ExpandButton.row-buddy::indicator { background: var(--grey4); - border: var(--highlight_width) solid var(--grey1); - border-right: none; -} - -idian--RowFrame.hover .row-buddy { - background: var(--grey4); - border: var(--highlight_width) solid var(--grey1); - border-left: none; -} - -idian--RowFrame.hover idian--ExpandButton::indicator { border-color: var(--grey1); } diff --git a/frontend/dialogs/OBSIdianPlayground.cpp b/frontend/dialogs/OBSIdianPlayground.cpp index c28db477443e16..da45f0aab8fdb5 100644 --- a/frontend/dialogs/OBSIdianPlayground.cpp +++ b/frontend/dialogs/OBSIdianPlayground.cpp @@ -17,7 +17,10 @@ #include "OBSIdianPlayground.hpp" +#include #include +#include +#include #include @@ -31,7 +34,9 @@ OBSIdianPlayground::OBSIdianPlayground(QWidget *parent) : QDialog(parent), ui(ne setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); - Group *test; + QLayout *contents = ui->scrollAreaWidgetContents->layout(); + + RowList *rowList; Row *tmp; ComboBox *cbox = new ComboBox; @@ -39,31 +44,29 @@ OBSIdianPlayground::OBSIdianPlayground(QWidget *parent) : QDialog(parent), ui(ne cbox->addItem("Test 2"); // Group box 1 - test = new Group(this); + rowList = new RowList(this); tmp = new Row(); - tmp->setTitle("Row with a dropdown"); - tmp->setSuffix(cbox); - test->properties()->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Row with a dropdown")); + tmp->addBuddy(cbox); + rowList->addRow(tmp); cbox = new ComboBox; cbox->addItem("Test 3"); cbox->addItem("Test 4"); tmp = new Row(); - tmp->setTitle("Row with a dropdown"); - tmp->setDescription("And a subtitle!"); - tmp->setSuffix(cbox); - test->properties()->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Row with a dropdown", "And a subtitle!")); + tmp->addBuddy(cbox); + rowList->addRow(tmp); tmp = new Row(); - tmp->setTitle("Toggle Switch"); - tmp->setSuffix(new ToggleSwitch()); - test->properties()->addRow(tmp); - ui->scrollAreaWidgetContents->layout()->addWidget(test); + tmp->addWidget(new RowInfo(tmp, "Toggle Switch")); + tmp->addBuddy(new ToggleSwitch()); + rowList->addRow(tmp); + contents->addWidget(rowList); tmp = new Row(); - tmp->setTitle("Delayed toggle switch"); - tmp->setDescription("The state can be set separately"); + tmp->addWidget(new RowInfo(tmp, "Delayed toggle switch", "The state can be set separately")); auto tswitch = new ToggleSwitch; tswitch->setDelayed(true); connect(tswitch, &ToggleSwitch::pendingChecked, this, [=]() { @@ -74,64 +77,65 @@ OBSIdianPlayground::OBSIdianPlayground(QWidget *parent) : QDialog(parent), ui(ne // Do async disable stuff, then set toggle status when complete QTimer::singleShot(1000, [=]() { tswitch->setStatus(false); }); }); - tmp->setSuffix(tswitch); - test->properties()->addRow(tmp); + tmp->addBuddy(tswitch); + rowList->addRow(tmp); // Group box 2 - test = new Group(); - test->setTitle("Just a few checkboxes"); + contents->addWidget(new ListHeader(rowList, "Just a few checkboxes")); + rowList = new RowList(); tmp = new Row(); - tmp->setTitle("Box 1"); - tmp->setPrefix(new CheckBox); - test->properties()->addRow(tmp); + tmp->addBuddy(new CheckBox); + tmp->addWidget(new RowInfo(tmp, "Box 1")); + rowList->addRow(tmp); tmp = new Row(); - tmp->setTitle("Box 2"); - tmp->setPrefix(new CheckBox); - test->properties()->addRow(tmp); + tmp->addBuddy(new CheckBox); + tmp->addWidget(new RowInfo(tmp, "Box 2")); + rowList->addRow(tmp); - ui->scrollAreaWidgetContents->layout()->addWidget(test); + ui->scrollAreaWidgetContents->layout()->addWidget(rowList); // Group box 2 - test = new Group(); - test->setTitle("Another Group"); - test->setDescription("With a subtitle"); + rowList = new RowList(); + contents->addWidget(new ListHeader(rowList, "Another Group", "With a subtitle")); tmp = new Row(); - tmp->setTitle("Placeholder"); - tmp->setSuffix(new ToggleSwitch); - test->properties()->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Placeholder")); + tmp->addBuddy(new ToggleSwitch); + rowList->addRow(tmp); - CollapsibleRow *tmp2 = new CollapsibleRow(this); - tmp2->setTitle("A Collapsible row!"); + CollapsibleGroup *tmp2 = new CollapsibleGroup(this); + tmp2->row()->layout()->insertWidget(0, new RowInfo(tmp, "A Collapsible row!")); tmp2->setCheckable(true); - test->addRow(tmp2); + rowList->addRow(tmp2); tmp = new Row(); - tmp->setTitle("Spin box demo"); - tmp->setSuffix(new DoubleSpinBox()); - tmp2->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Spin box demo")); + tmp->addBuddy(new DoubleSpinBox()); + tmp2->list()->addRow(tmp); tmp = new Row(); - tmp->setTitle("Just another placeholder"); - tmp->setSuffix(new ToggleSwitch(true)); - tmp2->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Just another placeholder")); + tmp->addBuddy(new ToggleSwitch(true)); + tmp2->list()->addRow(tmp); tmp = new Row(); - tmp->setTitle("Placeholder 2"); - tmp->setSuffix(new ToggleSwitch); - test->properties()->addRow(tmp); + tmp->addWidget(new RowInfo(tmp, "Placeholder 2")); + tmp->addBuddy(new ToggleSwitch); + rowList->addRow(tmp); ui->scrollAreaWidgetContents->setContentsMargins(0, 0, 0, 0); ui->scrollAreaWidgetContents->layout()->setContentsMargins(0, 0, 0, 0); - ui->scrollAreaWidgetContents->layout()->addWidget(test); + ui->scrollAreaWidgetContents->layout()->addWidget(rowList); ui->scrollAreaWidgetContents->layout()->setAlignment(Qt::AlignTop | Qt::AlignHCenter); // Test Checkable Group - Group *test2 = new Group(); - test2->setTitle("Checkable Group"); - test2->setDescription("Description goes here"); - test2->setCheckable(true); + auto *test2 = new RowList(this); + + auto *header = new ListHeader(this, "Checkable Group", "Description goes here"); + header->setCheckable(true); + test2->addHeader(header); + ui->scrollAreaWidgetContents->layout()->addWidget(test2); } diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 4343e16054c4da..64bcf0ca4d7d4e 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -595,6 +595,10 @@ private slots: QPointer colorWidgetAction; QPointer colorSelect; +#ifdef ENABLE_IDIAN_PLAYGROUND + QPointer playground; +#endif + QList visDialogs; QList modalDialogs; QList visMsgBoxes; diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp index 40b3aef444b418..6b089586db6c4c 100644 --- a/frontend/widgets/OBSBasic_MainControls.cpp +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -670,10 +670,11 @@ void OBSBasic::on_stats_triggered() void OBSBasic::on_idianPlayground_triggered() { #ifdef ENABLE_IDIAN_PLAYGROUND - OBSIdianPlayground playground(this); - playground.setModal(true); - playground.show(); - playground.exec(); + if (!playground) { + playground = new OBSIdianPlayground(this); + playground->setAttribute(Qt::WA_DeleteOnClose); + playground->show(); + } #endif } diff --git a/shared/qt/idian/CMakeLists.txt b/shared/qt/idian/CMakeLists.txt index 7ed39f3526f309..88e3d7c445d746 100644 --- a/shared/qt/idian/CMakeLists.txt +++ b/shared/qt/idian/CMakeLists.txt @@ -11,24 +11,30 @@ target_sources( components/CheckBox.cpp components/ComboBox.cpp components/DoubleSpinBox.cpp + components/ExpandButton.cpp + components/RowInfo.cpp components/SpinBox.cpp components/ToggleSwitch.cpp include/Idian/CheckBox.hpp + include/Idian/CollapsibleGroup.hpp include/Idian/ComboBox.hpp include/Idian/DoubleSpinBox.hpp - include/Idian/Group.hpp + include/Idian/ExpandButton.hpp include/Idian/Idian.hpp - include/Idian/PropertiesList.hpp + include/Idian/ListHeader.hpp include/Idian/Row.hpp + include/Idian/RowInfo.hpp + include/Idian/RowList.hpp include/Idian/SpinBox.hpp include/Idian/StateEventFilter.cpp include/Idian/StateEventFilter.hpp include/Idian/ToggleSwitch.hpp include/Idian/Utils.cpp include/Idian/Utils.hpp - widgets/Group.cpp - widgets/PropertiesList.cpp + widgets/CollapsibleGroup.cpp + widgets/ListHeader.cpp widgets/Row.cpp + widgets/RowList.cpp ) target_sources(idian PUBLIC include/Idian/Idian.hpp) diff --git a/shared/qt/idian/components/ExpandButton.cpp b/shared/qt/idian/components/ExpandButton.cpp new file mode 100644 index 00000000000000..29bba1ea16d63b --- /dev/null +++ b/shared/qt/idian/components/ExpandButton.cpp @@ -0,0 +1,52 @@ +/****************************************************************************** + Copyright (C) 2026 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 +#include + +namespace idian { +ExpandButton::ExpandButton(QWidget *parent) : QAbstractButton(parent) +{ + widgetUtils = new Utils(this); + widgetUtils->applyStateStylingEventFilter(this); + + setCheckable(true); +} + +void ExpandButton::paintEvent(QPaintEvent *) +{ + QStyleOptionButton opt; + opt.initFrom(this); + QPainter p(this); + + bool checked = isChecked(); + + opt.state.setFlag(QStyle::State_On, checked); + opt.state.setFlag(QStyle::State_Off, !checked); + + opt.state.setFlag(QStyle::State_Sunken, checked); + + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + + style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this); + style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &opt, &p, this); +} +} // namespace idian diff --git a/shared/qt/idian/components/RowInfo.cpp b/shared/qt/idian/components/RowInfo.cpp new file mode 100644 index 00000000000000..7991012747a040 --- /dev/null +++ b/shared/qt/idian/components/RowInfo.cpp @@ -0,0 +1,93 @@ +/****************************************************************************** + Copyright (C) 2026 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 +#include + +namespace idian { +RowInfo::RowInfo(QWidget *parent) : QWidget(parent) +{ + widgetUtils = new idian::Utils(this); + + layout_ = new QVBoxLayout(); + layout_->setSpacing(0); + layout_->setContentsMargins(0, 0, 0, 0); + + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + setLayout(layout_); + + nameLabel = new QLabel(); + nameLabel->setVisible(false); + widgetUtils->addClass(nameLabel, "title"); + + descriptionLabel = new QLabel(); + descriptionLabel->setVisible(false); + widgetUtils->addClass(descriptionLabel, "description"); + + layout_->addWidget(nameLabel); + layout_->addWidget(descriptionLabel); +} + +RowInfo::RowInfo(QWidget *parent, QString title) : RowInfo(parent) +{ + setTitle(title); +} + +RowInfo::RowInfo(QWidget *parent, QString title, QString description) : RowInfo(parent) +{ + setTitle(title); + setDescription(description); +} + +void RowInfo::setTitle(const QString &name) +{ + if (name.isEmpty()) { + showTitle(false); + return; + } + + nameLabel->setText(name); + setAccessibleName(name); + showTitle(true); +} + +void RowInfo::setDescription(const QString &description) +{ + if (description.isEmpty()) { + showDescription(false); + return; + } + + descriptionLabel->setText(description); + setAccessibleDescription(description); + showDescription(true); +} + +void RowInfo::showTitle(bool visible) +{ + nameLabel->setVisible(visible); +} + +void RowInfo::showDescription(bool visible) +{ + descriptionLabel->setVisible(visible); +} +} // namespace idian diff --git a/shared/qt/idian/components/ToggleSwitch.cpp b/shared/qt/idian/components/ToggleSwitch.cpp index 7fda7ba973dd7a..042ad19218b0c0 100644 --- a/shared/qt/idian/components/ToggleSwitch.cpp +++ b/shared/qt/idian/components/ToggleSwitch.cpp @@ -50,6 +50,8 @@ ToggleSwitch::ToggleSwitch(QWidget *parent) installEventFilter(this); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + connect(this, &ToggleSwitch::clicked, this, &ToggleSwitch::onClicked); connect(animHandle, &QVariantAnimation::valueChanged, this, &ToggleSwitch::updateBackgroundColor); diff --git a/shared/qt/idian/include/Idian/CollapsibleGroup.hpp b/shared/qt/idian/include/Idian/CollapsibleGroup.hpp new file mode 100644 index 00000000000000..d0fc92050be904 --- /dev/null +++ b/shared/qt/idian/include/Idian/CollapsibleGroup.hpp @@ -0,0 +1,67 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include + +class QPixmap; +class QVBoxLayout; + +namespace idian { +class Row; +class RowInfo; +class ExpandButton; +class RowList; +class ToggleSwitch; + +class CollapsibleGroup : public QFrame, public Utils { + Q_OBJECT + +public: + CollapsibleGroup(QWidget *parent = nullptr); + + void setCheckable(bool check); + bool isCheckable() { return checkable; } + + void setChecked(bool checked); + bool isChecked(); + + void setExpanded(bool expand = true); + + Row *row() { return rowWidget; } + RowList *list() { return propertyList; } + +protected: + void toggleVisibility(); + + QVBoxLayout *mainLayout; + + Row *rowWidget; + ExpandButton *expandButton; + + RowList *propertyList; + + ToggleSwitch *toggleSwitch = nullptr; + bool checkable = false; + +signals: + void toggled(bool checked); +}; +} // namespace idian diff --git a/shared/qt/idian/include/Idian/ExpandButton.hpp b/shared/qt/idian/include/Idian/ExpandButton.hpp new file mode 100644 index 00000000000000..e9368efd99c5c3 --- /dev/null +++ b/shared/qt/idian/include/Idian/ExpandButton.hpp @@ -0,0 +1,44 @@ +/****************************************************************************** + Copyright (C) 2026 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 QPixmap; + +namespace idian { +class Utils; + +class ExpandButton : public QAbstractButton { + Q_OBJECT + +public: + explicit ExpandButton(QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + Utils *widgetUtils; + + QPixmap extendDown; + QPixmap extendUp; + + friend class CollapsibleGroup; +}; +} // namespace idian diff --git a/shared/qt/idian/include/Idian/Idian.hpp b/shared/qt/idian/include/Idian/Idian.hpp index c69add2d88daa7..aa8dc3fb6a46fc 100644 --- a/shared/qt/idian/include/Idian/Idian.hpp +++ b/shared/qt/idian/include/Idian/Idian.hpp @@ -24,8 +24,7 @@ #include #include #include -#include -#include +#include #include #include #include diff --git a/shared/qt/idian/include/Idian/Group.hpp b/shared/qt/idian/include/Idian/ListHeader.hpp similarity index 69% rename from shared/qt/idian/include/Idian/Group.hpp rename to shared/qt/idian/include/Idian/ListHeader.hpp index 030361995f07c0..9c90d03d6e5f47 100644 --- a/shared/qt/idian/include/Idian/Group.hpp +++ b/shared/qt/idian/include/Idian/ListHeader.hpp @@ -17,8 +17,7 @@ #pragma once -#include -#include +#include #include #include @@ -28,15 +27,13 @@ namespace idian { -class Group : public QFrame, public Utils { +class ListHeader : public QFrame, public Utils { Q_OBJECT public: - Group(QWidget *parent = nullptr); - - PropertiesList *properties() const { return propertyList; } - - void addRow(GenericRow *row) const; + ListHeader(QWidget *parent = nullptr); + ListHeader(QWidget *parent, QString title); + ListHeader(QWidget *parent, QString title, QString description); void setTitle(QString name); void setDescription(QString desc); @@ -48,24 +45,17 @@ class Group : public QFrame, public Utils { bool isCheckable() { return checkable; } private: - QVBoxLayout *layout = nullptr; - - QWidget *headerContainer = nullptr; - QHBoxLayout *headerLayout = nullptr; - QWidget *labelContainer = nullptr; - QVBoxLayout *labelLayout = nullptr; - QWidget *controlContainer = nullptr; - QVBoxLayout *controlLayout = nullptr; + Utils *widgetUtils; - QWidget *contentsContainer = nullptr; - QVBoxLayout *contentsLayout = nullptr; + QHBoxLayout *layout_ = nullptr; QLabel *nameLabel = nullptr; QLabel *descriptionLabel = nullptr; - PropertiesList *propertyList = nullptr; - ToggleSwitch *toggleSwitch = nullptr; bool checkable = false; + +signals: + void toggled(bool enable); }; } // namespace idian diff --git a/shared/qt/idian/include/Idian/Row.hpp b/shared/qt/idian/include/Idian/Row.hpp index 4f324957dc0696..20875a8408e8b1 100644 --- a/shared/qt/idian/include/Idian/Row.hpp +++ b/shared/qt/idian/include/Idian/Row.hpp @@ -17,10 +17,7 @@ #pragma once -#include -#include -#include -#include +#include #include #include @@ -33,53 +30,38 @@ #include namespace idian { - -// Base class mostly so adding stuff to a list is easier -class GenericRow : public QFrame, public Utils { - Q_OBJECT - -public: - GenericRow(QWidget *parent = nullptr) : QFrame(parent), Utils(this) - { - setAttribute(Qt::WA_Hover, true); - - applyStateStylingEventFilter(this); - setAccessibleName(""); - }; - - virtual void setTitle(const QString &title) = 0; - virtual void setDescription(const QString &description) = 0; -}; +class ExpandButton; +class RowList; +class RowInfo; // Row widget containing one or more controls -class Row : public GenericRow { +class Row : public QFrame, public Utils { Q_OBJECT public: Row(QWidget *parent = nullptr); - void setPrefix(QWidget *w, bool autoConnect = true); - void setSuffix(QWidget *w, bool autoConnect = true); - - bool hasPrefix() { return prefix_; } - bool hasSuffix() { return suffix_; } - - QWidget *prefix() const { return prefix_; } - QWidget *suffix() const { return suffix_; } - - void setPrefixEnabled(bool enabled); - void setSuffixEnabled(bool enabled); + void setChangeCursor(bool change); - virtual void setTitle(const QString &title) override; - virtual void setDescription(const QString &description) override; + QHBoxLayout *layout() { return rowLayout; } - void showTitle(bool visible); - void showDescription(bool visible); + // Convenience function to add a widget to the rows layout. + void addWidget(QWidget *widget) { layout()->addWidget(widget); } + // Convenience function to add a widget to the rows layout and then set it as the buddy. + void addBuddy(QWidget *widget) + { + layout()->addWidget(widget); + setBuddy(widget); + } + + // Sets this widget as the buddy of the Row. + // The idian StateEventFilter on Row means certain style-able classes are applied to it based on interaction + // events. The buddy widget will be repolished when these events happen. This allows the buddy widget to be + // styled differently based on the state of the row. For certain widgets, a slot will also be triggered + // on the buddy widget. void setBuddy(QWidget *w); - void setChangeCursor(bool change); - signals: void clicked(); @@ -88,96 +70,13 @@ class Row : public GenericRow { void leaveEvent(QEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; void keyReleaseEvent(QKeyEvent *) override; - bool hasDescription() const { return descriptionLabel != nullptr; } private: - QGridLayout *layout; - - QVBoxLayout *labelLayout = nullptr; - - QLabel *nameLabel = nullptr; - QLabel *descriptionLabel = nullptr; - - QWidget *prefix_ = nullptr; - QWidget *suffix_ = nullptr; + QHBoxLayout *rowLayout; QWidget *buddyWidget = nullptr; void connectBuddyWidget(QWidget *widget); bool changeCursor = false; }; - -// Collapsible row expand button -class ExpandButton : public QAbstractButton, public Utils { - Q_OBJECT - -private: - QPixmap extendDown; - QPixmap extendUp; - - friend class CollapsibleRow; - -protected: - explicit ExpandButton(QWidget *parent = nullptr); - - void paintEvent(QPaintEvent *) override; -}; - -class RowFrame : protected QFrame, protected Utils { - Q_OBJECT - -signals: - void clicked(); - -protected: - explicit RowFrame(QWidget *parent = nullptr); - - void enterEvent(QEnterEvent *) override; - void leaveEvent(QEvent *) override; - -private: - friend class CollapsibleRow; -}; - -// Collapsible Generic OBS property container -class CollapsibleRow : public GenericRow { - Q_OBJECT - -public: - CollapsibleRow(QWidget *parent = nullptr); - - void setCheckable(bool check); - bool isCheckable() { return checkable; } - - void setChecked(bool checked); - bool isChecked() { return toggleSwitch->isChecked(); }; - - virtual void setTitle(const QString &title) override; - virtual void setDescription(const QString &description) override; - - void addRow(GenericRow *actionRow); - -signals: - void toggled(bool checked); - -private: - void toggleVisibility(); - - QPixmap extendDown; - QPixmap extendUp; - - QVBoxLayout *layout; - RowFrame *rowWidget; - QHBoxLayout *rowLayout; - - Row *actionRow; - QFrame *expandFrame; - QHBoxLayout *btnLayout; - ExpandButton *expandButton; - PropertiesList *propertyList; - - ToggleSwitch *toggleSwitch = nullptr; - bool checkable = false; -}; - } // namespace idian diff --git a/shared/qt/idian/include/Idian/RowInfo.hpp b/shared/qt/idian/include/Idian/RowInfo.hpp new file mode 100644 index 00000000000000..010d35d188fd75 --- /dev/null +++ b/shared/qt/idian/include/Idian/RowInfo.hpp @@ -0,0 +1,54 @@ +/****************************************************************************** + Copyright (C) 2026 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 QLabel; +class QVBoxLayout; + +namespace idian { +class Utils; + +class RowInfo : public QWidget { + Q_OBJECT + +public: + RowInfo(QWidget *parent); + RowInfo(QWidget *parent, QString title); + RowInfo(QWidget *parent, QString title, QString description); + ~RowInfo() = default; + + void setTitle(const QString &title); + void setDescription(const QString &description); + + void showTitle(bool visible); + void showDescription(bool visible); + + QLabel *title() { return nameLabel; } + QLabel *description() { return descriptionLabel; } + +private: + Utils *widgetUtils; + + QVBoxLayout *layout_ = nullptr; + + QLabel *nameLabel = nullptr; + QLabel *descriptionLabel = nullptr; +}; +} // namespace idian diff --git a/shared/qt/idian/include/Idian/PropertiesList.hpp b/shared/qt/idian/include/Idian/RowList.hpp similarity index 75% rename from shared/qt/idian/include/Idian/PropertiesList.hpp rename to shared/qt/idian/include/Idian/RowList.hpp index b0fe840d65fcfe..518e2cef3c427e 100644 --- a/shared/qt/idian/include/Idian/PropertiesList.hpp +++ b/shared/qt/idian/include/Idian/RowList.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include @@ -24,32 +25,29 @@ #include namespace idian { -class GenericRow; - -class PropertiesList : public QFrame { +class RowList : public QFrame { Q_OBJECT public: - PropertiesList(QWidget *parent = nullptr); + RowList(QWidget *parent = nullptr); - void addRow(GenericRow *row); + void addHeader(QWidget *widget); + void addRow(QWidget *row); void clear(); - QList rows() const { return rowsList; } - private: - GenericRow *first = nullptr; - GenericRow *last = nullptr; + QWidget *first = nullptr; + QWidget *last = nullptr; QVBoxLayout *layout; - QList rowsList; + QVBoxLayout *rowLayout; }; // Spacer with only cosmetic functionality -class PropertiesListSpacer : public QFrame { +class RowListSpacer : public QFrame { Q_OBJECT public: - PropertiesListSpacer(QWidget *parent = nullptr) : QFrame(parent) + RowListSpacer(QWidget *parent = nullptr) : QFrame(parent) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } diff --git a/shared/qt/idian/include/Idian/Utils.cpp b/shared/qt/idian/include/Idian/Utils.cpp index f1ecd4068222d6..890eb3beebd98f 100644 --- a/shared/qt/idian/include/Idian/Utils.cpp +++ b/shared/qt/idian/include/Idian/Utils.cpp @@ -52,6 +52,9 @@ QPixmap Utils::recolorPixmap(const QPixmap &src, const QColor &color) return QPixmap::fromImage(img); } +// Updates the dynamic property 'class' on a widget with values in response to certain interaction events. +// Ex. `hover` when the widget is hovered. +// Widgets can then be styled via CSS class-style rules like .hover. void Utils::applyStateStylingEventFilter(QWidget *widget) { widget->installEventFilter(new StateEventFilter(this, widget)); diff --git a/shared/qt/idian/widgets/CollapsibleGroup.cpp b/shared/qt/idian/widgets/CollapsibleGroup.cpp new file mode 100644 index 00000000000000..223cac9715187c --- /dev/null +++ b/shared/qt/idian/widgets/CollapsibleGroup.cpp @@ -0,0 +1,107 @@ +/****************************************************************************** + Copyright (C) 2026 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 +#include +#include +#include +#include + +#include +#include + +namespace idian { +CollapsibleGroup::CollapsibleGroup(QWidget *parent) : QFrame(parent), Utils(this) +{ + mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + setLayout(mainLayout); + + rowWidget = new Row(this); + + mainLayout->addWidget(rowWidget); + + auto *headerInfo = new RowInfo(rowWidget); + rowWidget->layout()->addWidget(headerInfo); + + expandButton = new ExpandButton(this); + rowWidget->layout()->addWidget(expandButton); + rowWidget->setBuddy(expandButton); + + propertyList = new RowList(this); + propertyList->setVisible(false); + mainLayout->addWidget(propertyList); + + connect(expandButton, &QAbstractButton::clicked, this, &CollapsibleGroup::toggleVisibility); +} + +void CollapsibleGroup::setCheckable(bool check) +{ + checkable = check; + + if (checkable && !toggleSwitch) { + propertyList->setEnabled(false); + Utils::polishChildren(propertyList); + + toggleSwitch = new ToggleSwitch(false); + + rowWidget->layout()->insertWidget(rowWidget->layout()->count() - 1, toggleSwitch); + connect(toggleSwitch, &ToggleSwitch::toggled, propertyList, &RowList::setEnabled); + connect(toggleSwitch, &ToggleSwitch::toggled, this, &CollapsibleGroup::toggled); + } + + if (!checkable && toggleSwitch) { + propertyList->setEnabled(true); + Utils::polishChildren(propertyList); + + toggleSwitch->deleteLater(); + } +} + +void CollapsibleGroup::setChecked(bool checked) +{ + if (!isCheckable()) { + throw std::logic_error("Called setChecked on a non-checkable collapse row."); + } + + toggleSwitch->setChecked(checked); +} + +bool CollapsibleGroup::isChecked() +{ + return toggleSwitch->isChecked(); +} + +void CollapsibleGroup::toggleVisibility() +{ + bool visible = !propertyList->isVisible(); + + setExpanded(visible); +} + +void CollapsibleGroup::setExpanded(bool expand) +{ + Utils::toggleClass("expanded", expand); + Utils::repolish(rowWidget); + + propertyList->setVisible(expand); + expandButton->setChecked(expand); +} +} // namespace idian diff --git a/shared/qt/idian/widgets/Group.cpp b/shared/qt/idian/widgets/Group.cpp deleted file mode 100644 index 89b14e4b9e0085..00000000000000 --- a/shared/qt/idian/widgets/Group.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/****************************************************************************** - Copyright (C) 2023 by Dennis Sädtler - - 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 - -using idian::Group; - -Group::Group(QWidget *parent) : QFrame(parent), Utils(this) -{ - layout = new QVBoxLayout(this); - layout->setSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); - - headerContainer = new QWidget(); - headerLayout = new QHBoxLayout(); - headerLayout->setSpacing(0); - headerLayout->setContentsMargins(0, 0, 0, 0); - headerContainer->setLayout(headerLayout); - Utils::addClass(headerContainer, "header"); - - labelContainer = new QWidget(); - labelLayout = new QVBoxLayout(); - labelLayout->setSpacing(0); - labelLayout->setContentsMargins(0, 0, 0, 0); - labelContainer->setLayout(labelLayout); - labelContainer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - controlContainer = new QWidget(); - controlLayout = new QVBoxLayout(); - controlLayout->setSpacing(0); - controlLayout->setContentsMargins(0, 0, 0, 0); - controlContainer->setLayout(controlLayout); - - headerLayout->addWidget(labelContainer); - headerLayout->addWidget(controlContainer); - - contentsContainer = new QWidget(); - contentsLayout = new QVBoxLayout(); - contentsLayout->setSpacing(0); - contentsLayout->setContentsMargins(0, 0, 0, 0); - contentsContainer->setLayout(contentsLayout); - Utils::addClass(contentsContainer, "contents"); - - layout->addWidget(headerContainer); - layout->addWidget(contentsContainer); - - propertyList = new PropertiesList(this); - - setLayout(layout); - - contentsLayout->addWidget(propertyList); - - nameLabel = new QLabel(); - Utils::addClass(nameLabel, "title"); - nameLabel->setVisible(false); - labelLayout->addWidget(nameLabel); - - descriptionLabel = new QLabel(); - Utils::addClass(descriptionLabel, "description"); - descriptionLabel->setVisible(false); - labelLayout->addWidget(descriptionLabel); -} - -void Group::addRow(GenericRow *row) const -{ - propertyList->addRow(row); -} - -void Group::setTitle(QString name) -{ - nameLabel->setText(name); - setAccessibleName(name); - showTitle(true); -} - -void Group::setDescription(QString desc) -{ - descriptionLabel->setText(desc); - setAccessibleDescription(desc); - showDescription(true); -} - -void Group::showTitle(bool visible) -{ - nameLabel->setVisible(visible); -} - -void Group::showDescription(bool visible) -{ - descriptionLabel->setVisible(visible); -} - -void Group::setCheckable(bool check) -{ - checkable = check; - - if (checkable && !toggleSwitch) { - toggleSwitch = new ToggleSwitch(true); - controlLayout->addWidget(toggleSwitch); - connect(toggleSwitch, &ToggleSwitch::toggled, this, - [this](bool checked) { propertyList->setEnabled(checked); }); - } - - if (!checkable && toggleSwitch) { - controlLayout->removeWidget(toggleSwitch); - toggleSwitch->deleteLater(); - } -} diff --git a/shared/qt/idian/widgets/ListHeader.cpp b/shared/qt/idian/widgets/ListHeader.cpp new file mode 100644 index 00000000000000..b0047c46f30072 --- /dev/null +++ b/shared/qt/idian/widgets/ListHeader.cpp @@ -0,0 +1,101 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + 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 + +using idian::ListHeader; + +ListHeader::ListHeader(QWidget *parent) : QFrame(parent), Utils(this) +{ + widgetUtils = new idian::Utils(this); + + layout_ = new QHBoxLayout(); + layout_->setSpacing(0); + layout_->setContentsMargins(0, 0, 0, 0); + + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + setLayout(layout_); + + auto *textLayout = new QVBoxLayout(); + nameLabel = new QLabel(); + nameLabel->setVisible(false); + widgetUtils->addClass(nameLabel, "title"); + + descriptionLabel = new QLabel(); + descriptionLabel->setVisible(false); + widgetUtils->addClass(descriptionLabel, "description"); + + textLayout->addWidget(nameLabel); + textLayout->addWidget(descriptionLabel); + + layout_->addLayout(textLayout); +} + +ListHeader::ListHeader(QWidget *parent, QString title) : ListHeader(parent) +{ + setTitle(title); +} + +ListHeader::ListHeader(QWidget *parent, QString title, QString description) : ListHeader(parent) +{ + setTitle(title); + setDescription(description); +} + +void ListHeader::setTitle(QString name) +{ + nameLabel->setText(name); + setAccessibleName(name); + showTitle(true); +} + +void ListHeader::setDescription(QString desc) +{ + descriptionLabel->setText(desc); + setAccessibleDescription(desc); + showDescription(true); +} + +void ListHeader::showTitle(bool visible) +{ + nameLabel->setVisible(visible); +} + +void ListHeader::showDescription(bool visible) +{ + descriptionLabel->setVisible(visible); +} + +void ListHeader::setCheckable(bool check) +{ + checkable = check; + + if (checkable && !toggleSwitch) { + toggleSwitch = new ToggleSwitch(true); + layout_->addWidget(toggleSwitch); + connect(toggleSwitch, &ToggleSwitch::toggled, this, [this](bool checked) { emit toggled(checked); }); + } + + if (!checkable && toggleSwitch) { + layout_->removeWidget(toggleSwitch); + toggleSwitch->deleteLater(); + } +} diff --git a/shared/qt/idian/widgets/Row.cpp b/shared/qt/idian/widgets/Row.cpp index 96d195794d8dcf..56bd0983651067 100644 --- a/shared/qt/idian/widgets/Row.cpp +++ b/shared/qt/idian/widgets/Row.cpp @@ -15,7 +15,14 @@ along with this program. If not, see . ******************************************************************************/ +#include +#include +#include +#include #include +#include +#include +#include #include #include @@ -25,129 +32,19 @@ #include namespace idian { - -Row::Row(QWidget *parent) : GenericRow(parent) -{ - layout = new QGridLayout(this); - layout->setVerticalSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); - - labelLayout = new QVBoxLayout(); - labelLayout->setSpacing(0); - labelLayout->setContentsMargins(0, 0, 0, 0); - - setFocusPolicy(Qt::StrongFocus); - setLayout(layout); - - layout->setColumnMinimumWidth(0, 0); - layout->setColumnStretch(0, 0); - layout->setColumnStretch(1, 40); - layout->setColumnStretch(2, 55); - - nameLabel = new QLabel(); - nameLabel->setVisible(false); - Utils::addClass(nameLabel, "title"); - - descriptionLabel = new QLabel(); - descriptionLabel->setVisible(false); - Utils::addClass(descriptionLabel, "description"); - - labelLayout->addWidget(nameLabel); - labelLayout->addWidget(descriptionLabel); - - layout->addLayout(labelLayout, 0, 1, Qt::AlignLeft); -} - -void Row::setPrefix(QWidget *w, bool auto_connect) -{ - setSuffixEnabled(false); - - prefix_ = w; - - if (auto_connect) { - this->connectBuddyWidget(w); - } - - prefix_->setParent(this); - layout->addWidget(prefix_, 0, 0, Qt::AlignLeft); - layout->setColumnStretch(0, 3); -} - -void Row::setSuffix(QWidget *w, bool auto_connect) -{ - setPrefixEnabled(false); - - suffix_ = w; - - if (auto_connect) { - this->connectBuddyWidget(w); - } - - suffix_->setParent(this); - layout->addWidget(suffix_, 0, 2, Qt::AlignRight | Qt::AlignVCenter); -} - -void Row::setPrefixEnabled(bool enabled) +Row::Row(QWidget *parent) : QFrame(parent), Utils(this) { - if (!prefix_) { - return; - } - if (enabled) { - setSuffixEnabled(false); - } - if (enabled == prefix_->isEnabled() && enabled == prefix_->isVisible()) { - return; - } - - layout->setColumnStretch(0, enabled ? 3 : 0); - prefix_->setEnabled(enabled); - prefix_->setVisible(enabled); -} - -void Row::setSuffixEnabled(bool enabled) -{ - if (!suffix_) { - return; - } - if (enabled) { - setPrefixEnabled(false); - } - if (enabled == suffix_->isEnabled() && enabled == suffix_->isVisible()) { - return; - } - - suffix_->setEnabled(enabled); - suffix_->setVisible(enabled); -} - -void Row::setTitle(const QString &name) -{ - nameLabel->setText(name); - setAccessibleName(name); - showTitle(true); -} - -void Row::setDescription(const QString &description) -{ - descriptionLabel->setText(description); - setAccessibleDescription(description); - showDescription(true); -} - -void Row::showTitle(bool visible) -{ - nameLabel->setVisible(visible); -} + rowLayout = new QHBoxLayout(this); + rowLayout->setContentsMargins(0, 0, 0, 0); -void Row::showDescription(bool visible) -{ - descriptionLabel->setVisible(visible); + setLayout(rowLayout); } void Row::setBuddy(QWidget *widget) { buddyWidget = widget; Utils::addClass(widget, "row-buddy"); + connectBuddyWidget(widget); } void Row::setChangeCursor(bool change) @@ -170,11 +67,7 @@ void Row::enterEvent(QEnterEvent *event) Utils::repolish(buddyWidget); } - if (hasPrefix() || hasSuffix()) { - Utils::polishChildren(); - } - - GenericRow::enterEvent(event); + QFrame::enterEvent(event); } void Row::leaveEvent(QEvent *event) @@ -183,11 +76,7 @@ void Row::leaveEvent(QEvent *event) Utils::repolish(buddyWidget); } - if (hasPrefix() || hasSuffix()) { - Utils::polishChildren(); - } - - GenericRow::leaveEvent(event); + QFrame::leaveEvent(event); } void Row::mouseReleaseEvent(QMouseEvent *event) @@ -208,14 +97,15 @@ void Row::keyReleaseEvent(QKeyEvent *event) void Row::connectBuddyWidget(QWidget *widget) { - setAccessibleName(nameLabel->text()); - setFocusProxy(widget); - setBuddy(widget); + setAttribute(Qt::WA_Hover, true); + setFocusPolicy(Qt::StrongFocus); + applyStateStylingEventFilter(this); // If element is a ToggleSwitch and checkable, forward clicks to the widget ToggleSwitch *obsToggle = dynamic_cast(widget); if (obsToggle && obsToggle->isCheckable()) { setChangeCursor(true); + widget->setFocusProxy(this); connect(this, &Row::clicked, obsToggle, &ToggleSwitch::click); return; @@ -225,6 +115,7 @@ void Row::connectBuddyWidget(QWidget *widget) QAbstractButton *button = dynamic_cast(widget); if (button && button->isCheckable()) { setChangeCursor(true); + widget->setFocusProxy(this); connect(this, &Row::clicked, button, &QAbstractButton::click); return; @@ -234,158 +125,10 @@ void Row::connectBuddyWidget(QWidget *widget) ComboBox *obsCombo = dynamic_cast(widget); if (obsCombo) { setChangeCursor(true); + widget->setFocusProxy(this); connect(this, &Row::clicked, obsCombo, &ComboBox::togglePopup); return; } } - -// Button for expanding a collapsible ActionRow -ExpandButton::ExpandButton(QWidget *parent) : QAbstractButton(parent), Utils(this) -{ - Utils::applyStateStylingEventFilter(this); - setCheckable(true); -} - -void ExpandButton::paintEvent(QPaintEvent *) -{ - QStyleOptionButton opt; - opt.initFrom(this); - QPainter p(this); - - bool checked = isChecked(); - - opt.state.setFlag(QStyle::State_On, checked); - opt.state.setFlag(QStyle::State_Off, !checked); - - opt.state.setFlag(QStyle::State_Sunken, checked); - - p.setRenderHint(QPainter::Antialiasing, true); - p.setRenderHint(QPainter::SmoothPixmapTransform, true); - - style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this); - style()->drawPrimitive(QStyle::PE_IndicatorCheckBox, &opt, &p, this); -} - -// Row variant that can be expanded to show another properties list -CollapsibleRow::CollapsibleRow(QWidget *parent) : GenericRow(parent) -{ - layout = new QVBoxLayout; - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - setLayout(layout); - - rowWidget = new RowFrame(); - rowLayout = new QHBoxLayout(); - rowLayout->setContentsMargins(0, 0, 0, 0); - rowLayout->setSpacing(0); - rowWidget->setLayout(rowLayout); - - actionRow = new Row(); - actionRow->setChangeCursor(false); - - rowLayout->addWidget(actionRow); - - propertyList = new PropertiesList(this); - propertyList->setVisible(false); - - expandFrame = new QFrame(); - btnLayout = new QHBoxLayout(); - btnLayout->setContentsMargins(0, 0, 0, 0); - btnLayout->setSpacing(0); - expandFrame->setLayout(btnLayout); - Utils::addClass(expandFrame, "btn-frame"); - actionRow->setBuddy(expandFrame); - - expandButton = new ExpandButton(this); - btnLayout->addWidget(expandButton); - - rowLayout->addWidget(expandFrame); - - layout->addWidget(rowWidget); - layout->addWidget(propertyList); - - actionRow->setFocusProxy(expandButton); - - connect(expandButton, &QAbstractButton::clicked, this, &CollapsibleRow::toggleVisibility); - connect(actionRow, &Row::clicked, expandButton, &QAbstractButton::click); -} - -void CollapsibleRow::setCheckable(bool check) -{ - checkable = check; - - if (checkable && !toggleSwitch) { - propertyList->setEnabled(false); - Utils::polishChildren(propertyList); - - toggleSwitch = new ToggleSwitch(false); - - actionRow->setSuffix(toggleSwitch, false); - connect(toggleSwitch, &ToggleSwitch::toggled, propertyList, &PropertiesList::setEnabled); - connect(toggleSwitch, &ToggleSwitch::toggled, this, &CollapsibleRow::toggled); - } - - if (!checkable && toggleSwitch) { - propertyList->setEnabled(true); - Utils::polishChildren(propertyList); - - actionRow->suffix()->deleteLater(); - } -} - -void CollapsibleRow::setChecked(bool checked) -{ - if (!isCheckable()) { - throw std::logic_error("Called setChecked on a non-checkable row."); - } - - toggleSwitch->setChecked(checked); -} - -void CollapsibleRow::setTitle(const QString &name) -{ - actionRow->setTitle(name); -} - -void CollapsibleRow::setDescription(const QString &description) -{ - actionRow->setDescription(description); -} - -void CollapsibleRow::toggleVisibility() -{ - bool visible = !propertyList->isVisible(); - - propertyList->setVisible(visible); - expandButton->setChecked(visible); -} - -void CollapsibleRow::addRow(GenericRow *actionRow) -{ - propertyList->addRow(actionRow); -} - -RowFrame::RowFrame(QWidget *parent) : QFrame(parent), Utils(this) -{ - setAttribute(Qt::WA_Hover, true); - - Utils::applyStateStylingEventFilter(this); -} - -void RowFrame::enterEvent(QEnterEvent *event) -{ - setCursor(Qt::PointingHandCursor); - - Utils::polishChildren(); - - QWidget::enterEvent(event); -} - -void RowFrame::leaveEvent(QEvent *event) -{ - Utils::polishChildren(); - - QWidget::leaveEvent(event); -} } // namespace idian diff --git a/shared/qt/idian/widgets/PropertiesList.cpp b/shared/qt/idian/widgets/RowList.cpp similarity index 70% rename from shared/qt/idian/widgets/PropertiesList.cpp rename to shared/qt/idian/widgets/RowList.cpp index d28935ece89499..fae414e53f8d54 100644 --- a/shared/qt/idian/widgets/PropertiesList.cpp +++ b/shared/qt/idian/widgets/RowList.cpp @@ -15,42 +15,47 @@ along with this program. If not, see . ******************************************************************************/ -#include - -#include +#include #include -#include +#include -using idian::PropertiesList; +using idian::RowList; -PropertiesList::PropertiesList(QWidget *parent) : QFrame(parent) +RowList::RowList(QWidget *parent) : QFrame(parent) { layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); - rowsList = QList(); - setLayout(layout); + + rowLayout = new QVBoxLayout(); + + layout->addLayout(rowLayout); +} + +void idian::RowList::addHeader(QWidget *widget) +{ + layout->insertWidget(layout->indexOf(rowLayout) - 1, widget); } // Note: This function takes ownership of the added widget // and it may be deleted when the properties list is destroyed // or the clear() method is called! -void PropertiesList::addRow(GenericRow *row) +void RowList::addRow(QWidget *widget) { - // Add custom spacer once more than one element exists - if (layout->count() > 0) { - layout->addWidget(new PropertiesListSpacer(this)); + // Add custom spacer when more than one row exists + if (rowLayout->count() > 0) { + rowLayout->addWidget(new RowListSpacer(this)); } // Custom properties to work around :first and :last not existing. if (!first) { - Utils::addClass(row, "first"); - first = row; + Utils::addClass(widget, "first"); + first = widget; } // Remove last property from existing last item @@ -59,21 +64,18 @@ void PropertiesList::addRow(GenericRow *row) } // Most recently added item is also always last - Utils::addClass(row, "last"); - last = row; + Utils::addClass(widget, "last"); + last = widget; - row->setParent(this); - rowsList.append(row); - layout->addWidget(row); + rowLayout->addWidget(widget); adjustSize(); } -void PropertiesList::clear() +void RowList::clear() { - rowsList.clear(); first = nullptr; last = nullptr; - QLayoutItem *item = layout->takeAt(0); + QLayoutItem *item = rowLayout->takeAt(0); while (item) { if (item->widget()) { @@ -81,7 +83,7 @@ void PropertiesList::clear() } delete item; - item = layout->takeAt(0); + item = rowLayout->takeAt(0); } adjustSize(); From 97dd89bc24adf9b41a9054a618df3b4a86675b43 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 16:11:09 -0400 Subject: [PATCH 2/7] frontend: Add colored notice widgets --- frontend/cmake/ui-components.cmake | 4 ++ frontend/components/NoticeButton.cpp | 68 ++++++++++++++++++++++++++++ frontend/components/NoticeButton.hpp | 39 ++++++++++++++++ frontend/components/NoticeLabel.cpp | 65 ++++++++++++++++++++++++++ frontend/components/NoticeLabel.hpp | 41 +++++++++++++++++ frontend/data/themes/Yami.obt | 68 ++++++++++++++++++++++++++++ 6 files changed, 285 insertions(+) create mode 100644 frontend/components/NoticeButton.cpp create mode 100644 frontend/components/NoticeButton.hpp create mode 100644 frontend/components/NoticeLabel.cpp create mode 100644 frontend/components/NoticeLabel.hpp diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index 34f4c0e7fe3a1d..42a811a5086e7f 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -55,6 +55,10 @@ target_sources( components/Multiview.cpp components/Multiview.hpp components/MuteCheckBox.hpp + components/NoticeButton.cpp + components/NoticeButton.hpp + components/NoticeLabel.cpp + components/NoticeLabel.hpp components/OBSAdvAudioCtrl.cpp components/OBSAdvAudioCtrl.hpp components/OBSPreviewScalingComboBox.cpp diff --git a/frontend/components/NoticeButton.cpp b/frontend/components/NoticeButton.cpp new file mode 100644 index 00000000000000..de046e2d7ebde5 --- /dev/null +++ b/frontend/components/NoticeButton.cpp @@ -0,0 +1,68 @@ +/****************************************************************************** + Copyright (C) 2026 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 "NoticeButton.hpp" + +#include + +namespace OBS { +NoticeButton::NoticeButton(QWidget *parent) : QPushButton(parent), idian::Utils(this) +{ + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + idian::Utils::addClass("text-bold"); +} + +NoticeButton::NoticeButton(QWidget *parent, QString text, OBS::NoticeStyle style) : NoticeButton(parent) +{ + setText(text); + setStyle(style); +} + +void NoticeButton::setStyle(NoticeStyle style) +{ + applyStyle(currentStyle, false); + applyStyle(style, true); + + currentStyle = style; +} + +void NoticeButton::applyStyle(NoticeStyle style, bool enable) +{ + switch (style) { + case NoticeStyle::Primary: + idian::Utils::toggleClass("primary", enable); + break; + case NoticeStyle::Secondary: + idian::Utils::toggleClass("secondary", enable); + break; + case NoticeStyle::Info: + idian::Utils::toggleClass("info", enable); + break; + case NoticeStyle::Success: + idian::Utils::toggleClass("success", enable); + break; + case NoticeStyle::Warning: + idian::Utils::toggleClass("warning", enable); + break; + case NoticeStyle::Danger: + idian::Utils::toggleClass("danger", enable); + break; + default: + break; + } +} +} // namespace OBS diff --git a/frontend/components/NoticeButton.hpp b/frontend/components/NoticeButton.hpp new file mode 100644 index 00000000000000..cc01425191c8bf --- /dev/null +++ b/frontend/components/NoticeButton.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include + +namespace OBS { +class NoticeButton : public QPushButton, public idian::Utils { + Q_OBJECT + +public: + NoticeButton(QWidget *parent); + NoticeButton(QWidget *parent, QString text, NoticeStyle style); + ~NoticeButton() = default; + + void setStyle(NoticeStyle style); + void applyStyle(NoticeStyle style, bool enable); + +protected: + NoticeStyle currentStyle = NoticeStyle::None; +}; +} // namespace OBS diff --git a/frontend/components/NoticeLabel.cpp b/frontend/components/NoticeLabel.cpp new file mode 100644 index 00000000000000..0cc33d040de1e1 --- /dev/null +++ b/frontend/components/NoticeLabel.cpp @@ -0,0 +1,65 @@ +/****************************************************************************** + Copyright (C) 2026 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 "NoticeLabel.hpp" + +namespace OBS { +NoticeLabel::NoticeLabel(QWidget *parent) : QLabel(parent), idian::Utils(this) +{ + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + idian::Utils::addClass("text-bold"); +} + +NoticeLabel::NoticeLabel(QWidget *parent, NoticeStyle style) : NoticeLabel(parent) +{ + setStyle(style); +} + +void NoticeLabel::setStyle(NoticeStyle style) +{ + applyStyle(currentStyle, false); + applyStyle(style, true); + + currentStyle = style; +} + +void NoticeLabel::applyStyle(NoticeStyle style, bool enable) +{ + switch (style) { + case NoticeStyle::Primary: + idian::Utils::toggleClass("primary", enable); + break; + case NoticeStyle::Secondary: + idian::Utils::toggleClass("secondary", enable); + break; + case NoticeStyle::Info: + idian::Utils::toggleClass("info", enable); + break; + case NoticeStyle::Success: + idian::Utils::toggleClass("success", enable); + break; + case NoticeStyle::Warning: + idian::Utils::toggleClass("warning", enable); + break; + case NoticeStyle::Danger: + idian::Utils::toggleClass("danger", enable); + break; + default: + break; + } +} +} // namespace OBS diff --git a/frontend/components/NoticeLabel.hpp b/frontend/components/NoticeLabel.hpp new file mode 100644 index 00000000000000..098a3ca864fc71 --- /dev/null +++ b/frontend/components/NoticeLabel.hpp @@ -0,0 +1,41 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include + +namespace OBS { +enum class NoticeStyle { None, Primary, Secondary, Info, Success, Warning, Danger }; + +class NoticeLabel : public QLabel, public idian::Utils { + Q_OBJECT + +public: + NoticeLabel(QWidget *parent); + NoticeLabel(QWidget *parent, NoticeStyle style); + ~NoticeLabel() = default; + + void setStyle(NoticeStyle style); + void applyStyle(NoticeStyle style, bool enable); + +protected: + NoticeStyle currentStyle = NoticeStyle::None; +}; +} // namespace OBS diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 4089f95c48ddff..73a68d5a8b75d7 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -332,6 +332,74 @@ color: var(--green3); } +/* Color styles */ +QWidget.primary { + background: var(--blue5); + border: 1px solid var(--blue3); + border-radius: var(--border_radius); +} + +QWidget.secondary { + background: var(--teal5); + border: 1px solid var(--teal3); + border-radius: var(--border_radius); +} + +QWidget.info { + background: var(--grey8); + border: 1px solid var(--grey6); + border-radius: var(--border_radius); +} + +QWidget.success { + background: var(--green5); + border: 1px solid var(--green3); + border-radius: var(--border_radius); +} + +QWidget.warning { + background: var(--yellow5); + border: 1px solid var(--yellow3); + border-radius: var(--border_radius); +} + +QWidget.danger { + background: var(--red5); + border: 1px solid var(--red3); + border-radius: var(--border_radius); +} + +QPushButton.primary:hover { + background-color: var(--blue4); + border-color: var(--blue2); +} + +QPushButton.secondary:hover { + background-color: var(--teal4); + border-color: var(--teal2); +} + +QPushButton.info:hover { + background-color: var(--grey7); + border-color: var(--grey4); +} + +QPushButton.success:hover { + background-color: var(--green4); + border-color: var(--green2); +} + +QPushButton.warning:hover { + background-color: var(--yellow4); + border-color: var(--yellow2); +} + +QPushButton.danger:hover { + background-color: var(--red4); + border-color: var(--red2); +} + + .frame-notice { background: var(--grey8); border: 1px solid var(--grey6); From 9ea0feb4a597797e6f65a5ca85d80010e052f1ea Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Mon, 11 May 2026 13:40:34 -0400 Subject: [PATCH 3/7] frontend: Add HealthCheck service --- frontend/cmake/ui-utility.cmake | 6 ++ frontend/data/locale/en-US.ini | 6 ++ frontend/utility/HealthCheckAction.cpp | 44 +++++++++ frontend/utility/HealthCheckAction.hpp | 51 ++++++++++ frontend/utility/HealthCheckItem.cpp | 70 ++++++++++++++ frontend/utility/HealthCheckItem.hpp | 84 ++++++++++++++++ frontend/utility/HealthCheckService.cpp | 121 ++++++++++++++++++++++++ frontend/utility/HealthCheckService.hpp | 63 ++++++++++++ 8 files changed, 445 insertions(+) create mode 100644 frontend/utility/HealthCheckAction.cpp create mode 100644 frontend/utility/HealthCheckAction.hpp create mode 100644 frontend/utility/HealthCheckItem.cpp create mode 100644 frontend/utility/HealthCheckItem.hpp create mode 100644 frontend/utility/HealthCheckService.cpp create mode 100644 frontend/utility/HealthCheckService.hpp diff --git a/frontend/cmake/ui-utility.cmake b/frontend/cmake/ui-utility.cmake index 387385da8d2f0d..3aa1151d857e5d 100644 --- a/frontend/cmake/ui-utility.cmake +++ b/frontend/cmake/ui-utility.cmake @@ -19,6 +19,12 @@ target_sources( utility/GoLiveAPI_Network.hpp utility/GoLiveAPI_PostData.cpp utility/GoLiveAPI_PostData.hpp + utility/HealthCheckAction.cpp + utility/HealthCheckAction.hpp + utility/HealthCheckItem.cpp + utility/HealthCheckItem.hpp + utility/HealthCheckService.cpp + utility/HealthCheckService.hpp utility/MissingFilesModel.cpp utility/MissingFilesModel.hpp utility/MissingFilesPathItemDelegate.cpp diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index b5cc6384cf0834..9a682cbf15b313 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1680,3 +1680,9 @@ PluginManager.Section.Manage.Title="Manage Enabled Plugins" # Custom Widget Localization Accessible.Widget.Name.AlignmentSelector="Alignment Selector" + +# OBS Configuration Health Check +HealthCheck.Status.Invalid="Invalid" +HealthCheck.Status.Valid="Valid" +HealthCheck.Status.Warning="Warning" +HealthCheck.Status.Critical="Critical" diff --git a/frontend/utility/HealthCheckAction.cpp b/frontend/utility/HealthCheckAction.cpp new file mode 100644 index 00000000000000..fe04904ddba669 --- /dev/null +++ b/frontend/utility/HealthCheckAction.cpp @@ -0,0 +1,44 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckAction.hpp" + +namespace OBS { +HealthCheckAction::HealthCheckAction(PassKey, QObject *parent) : QObject(parent) {} + +void HealthCheckAction::setText(QString text) +{ + text_ = text; +} + +const QString &HealthCheckAction::text() +{ + return text_; +} + +void HealthCheckAction::setCallback(std::function func) +{ + callback = func; +} + +void HealthCheckAction::trigger() +{ + if (callback) { + callback(); + } +} +} // namespace OBS diff --git a/frontend/utility/HealthCheckAction.hpp b/frontend/utility/HealthCheckAction.hpp new file mode 100644 index 00000000000000..bc3dac5ced3f56 --- /dev/null +++ b/frontend/utility/HealthCheckAction.hpp @@ -0,0 +1,51 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +namespace OBS { +class HealthCheckItem; +class HealthCheckAction : public QObject { + +public: + // Limits constructor to only HealthCheckItem. + class PassKey { + friend class HealthCheckItem; + PassKey() = default; + }; + + HealthCheckAction(PassKey, QObject *parent); + ~HealthCheckAction() = default; + + HealthCheckAction(const HealthCheckAction &) = delete; + HealthCheckAction &operator=(const HealthCheckAction &) = delete; + + void setText(QString text); + const QString &text(); + + void setCallback(std::function func); + +private: + QString text_{""}; + std::function callback = nullptr; + +public slots: + void trigger(); +}; +} // namespace OBS diff --git a/frontend/utility/HealthCheckItem.cpp b/frontend/utility/HealthCheckItem.cpp new file mode 100644 index 00000000000000..ca56f143ea3d2d --- /dev/null +++ b/frontend/utility/HealthCheckItem.cpp @@ -0,0 +1,70 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckItem.hpp" + +#include + +namespace OBS { +HealthCheckItem::HealthCheckItem(PassKey, QObject *parent, QString id, QString title) + : QObject(parent), + id_(std::move(id)), + title_(std::move(title)) +{ +} + +void HealthCheckItem::setMessage(QString message) +{ + message_ = message; + + emit statusChanged(); +} + +void HealthCheckItem::setStatus(HealthStatus status) +{ + status_ = status; + + emit statusChanged(); +} + +void HealthCheckItem::setStatus(HealthStatus status, QString message) +{ + status_ = status; + message_ = message; + + emit statusChanged(); +} + +QString HealthCheckItem::statusText() const +{ + return HealthCheckItem::statusText(status()); +} + +QString HealthCheckItem::statusText(HealthStatus status) +{ + switch (status) { + case HealthStatus::Valid: + return QTStr("HealthCheck.Status.Valid"); + case HealthStatus::Warning: + return QTStr("HealthCheck.Status.Warning"); + case HealthStatus::Critical: + return QTStr("HealthCheck.Status.Critical"); + default: + return QTStr("HealthCheck.Status.Invalid"); + } +} +} // namespace OBS diff --git a/frontend/utility/HealthCheckItem.hpp b/frontend/utility/HealthCheckItem.hpp new file mode 100644 index 00000000000000..f749fb3d4054a0 --- /dev/null +++ b/frontend/utility/HealthCheckItem.hpp @@ -0,0 +1,84 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include +#include + +#include +#include + +namespace OBS { +enum class HealthStatus { Valid, Warning, Critical }; + +class HealthCheckService; +class HealthCheckAction; + +// Created via HealthCheckService::createItem(). +class HealthCheckItem : public QObject { + Q_OBJECT + +public: + // Limits constructor to only HealthCheckService. + class PassKey { + friend class HealthCheckService; + PassKey() = default; + }; + + HealthCheckItem(PassKey, QObject *parent, QString id, QString title); + ~HealthCheckItem() = default; + + HealthCheckItem(const HealthCheckItem &) = delete; + HealthCheckItem &operator=(const HealthCheckItem &) = delete; + + const QString &id() const { return id_; } + const QString &title() const { return title_; } + + void setMessage(QString message); + const QString &message() const { return message_; } + + void setStatus(HealthStatus status); + void setStatus(HealthStatus status, QString message); + const HealthStatus &status() const { return status_; } + QString statusText() const; + + static QString statusText(HealthStatus status); + + HealthCheckAction *action() { return action_; } + HealthCheckAction *createAction() + { + action_ = new HealthCheckAction(HealthCheckAction::PassKey{}, this); + return action_; + }; + +private: + QString id_{""}; + + QString title_{""}; + QString message_{""}; + + HealthStatus status_ = HealthStatus::Valid; + + QPointer action_; + +signals: + void statusChanged(); +}; +} // namespace OBS diff --git a/frontend/utility/HealthCheckService.cpp b/frontend/utility/HealthCheckService.cpp new file mode 100644 index 00000000000000..b37f58230a900a --- /dev/null +++ b/frontend/utility/HealthCheckService.cpp @@ -0,0 +1,121 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckService.hpp" + +#include +#include + +namespace OBS { +HealthCheckService::HealthCheckService(QObject *parent) : QObject(parent) {} + +void HealthCheckService::registerItem(HealthCheckItem *item) +{ + if (!item) { + return; + } + + QString id = item->id(); + registry[id] = QPointer(item); + + connect(item, &QObject::destroyed, this, [this, id]() { unregisterItem(id); }); + connect(item, &HealthCheckItem::statusChanged, this, [this, item]() { + if (item->status() != getGlobalStatus()) { + refreshGlobalStatus(); + } + }); + + emit itemListChanged(); +} + +void HealthCheckService::unregisterItem(const QString &id) +{ + registry.erase(id); + refreshGlobalStatus(); + + emit itemListChanged(); +} + +std::vector> HealthCheckService::getInvalidItems() +{ + std::vector> activeIssues; + + for (const auto &[id, item] : registry) { + if (item.isNull()) { + continue; + } + + if (item->status() != HealthStatus::Valid) { + activeIssues.emplace_back(item); + } + } + + return activeIssues; +} + +HealthStatus HealthCheckService::getGlobalStatus() +{ + return globalStatus; +} + +void HealthCheckService::refreshGlobalStatus() +{ + totalInvalidCount = 0; + HealthStatus globalStatus = HealthStatus::Valid; + + auto it = registry.begin(); + while (it != registry.end()) { + if (it->second.isNull()) { + it = registry.erase(it); + continue; + } + + HealthStatus itemStatus = it->second->status(); + + if (itemStatus != HealthStatus::Valid) { + ++totalInvalidCount; + } + + switch (globalStatus) { + case HealthStatus::Warning: + if (itemStatus == HealthStatus::Critical) { + globalStatus = itemStatus; + } + break; + case HealthStatus::Valid: + globalStatus = itemStatus; + break; + case HealthStatus::Critical: + default: + break; + } + + ++it; + } + + this->globalStatus = globalStatus; + emit globalStatusChanged(globalStatus); +} + +HealthCheckItem *HealthCheckService::createItem(QObject *parent, QString id, QString title) +{ + auto *item = new HealthCheckItem(HealthCheckItem::PassKey{}, parent, id, title); + registerItem(item); + + return item; +} +} // namespace OBS diff --git a/frontend/utility/HealthCheckService.hpp b/frontend/utility/HealthCheckService.hpp new file mode 100644 index 00000000000000..aee7273ac78b64 --- /dev/null +++ b/frontend/utility/HealthCheckService.hpp @@ -0,0 +1,63 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include +#include + +#include +#include + +class HealthCheckDialog; + +namespace OBS { +class HealthCheckService : public QObject { + Q_OBJECT + +public: + HealthCheckService(QObject *parent); + ~HealthCheckService() = default; + + HealthCheckService(const HealthCheckService &) = delete; + HealthCheckService &operator=(const HealthCheckService &) = delete; + + void registerItem(HealthCheckItem *item); + void unregisterItem(const QString &id); + + std::vector> getInvalidItems(); + int getInvalidCount() { return totalInvalidCount; } + + HealthStatus getGlobalStatus(); + + void refreshGlobalStatus(); + + HealthCheckItem *createItem(QObject *parent, QString id, QString title); + +private: + int totalInvalidCount; + HealthStatus globalStatus{HealthStatus::Valid}; + + std::unordered_map> registry; + +signals: + void globalStatusChanged(HealthStatus status); + void itemListChanged(); +}; +} // namespace OBS From c547044823e3f5b711536d82dc9f4d3e21f8d361 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 00:03:12 -0400 Subject: [PATCH 4/7] frontend: Implement HealthCheckService --- frontend/OBSApp.cpp | 11 ++++++++++- frontend/OBSApp.hpp | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 0d6d0e28e35fbd..498b72f6928150 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) @@ -1304,7 +1305,6 @@ bool OBSApp::OBSInit() thumbnailManager = new ThumbnailManager(this); mainWindow = new OBSBasic(); - mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); mainWindow->OBSInit(); @@ -2049,6 +2049,15 @@ void OBSApp::loadAppModules(struct obs_module_failure_info &mfi) pluginManager_->postLoad(); } +OBS::HealthCheckService *OBSApp::healthService() +{ + if (healthService_.isNull()) { + healthService_ = new OBS::HealthCheckService(this); + } + + return healthService_; +} + void OBSApp::pluginManagerOpenDialog() { pluginManager_->open(); diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index fc5774e047398e..4b8aa8bcd0a394 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -52,6 +52,7 @@ class CrashHandler; enum class LogFileType { NoType, CurrentAppLog, LastAppLog, CrashLog }; enum class LogFileState { NoState, New, Uploaded }; +class HealthCheckService; class PluginManager; } // namespace OBS @@ -93,6 +94,7 @@ class OBSApp : public QApplication { std::deque translatorHooks; ThumbnailManager *thumbnailManager = nullptr; + QPointer healthService_; std::unique_ptr pluginManager_; @@ -238,6 +240,7 @@ private slots: void loadAppModules(struct obs_module_failure_info &mfi); + OBS::HealthCheckService *healthService(); ThumbnailManager *thumbnails() const { return thumbnailManager; } // Plugin Manager Accessors From b0225de942c8f6409be79f172e97ba92008042b3 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 00:01:59 -0400 Subject: [PATCH 5/7] frontend: Add HealthCheck dialog and widgets --- frontend/OBSApp.cpp | 15 +++ frontend/OBSApp.hpp | 4 + frontend/cmake/ui-components.cmake | 4 + frontend/cmake/ui-dialogs.cmake | 2 + frontend/cmake/ui-widgets.cmake | 2 + frontend/components/HealthCheckInfoRow.cpp | 45 ++++++++ frontend/components/HealthCheckInfoRow.hpp | 32 ++++++ .../components/HealthCheckStatusLabel.cpp | 39 +++++++ .../components/HealthCheckStatusLabel.hpp | 34 ++++++ frontend/data/locale/en-US.ini | 1 + frontend/data/themes/Yami.obt | 17 +++ frontend/dialogs/HealthCheckDialog.cpp | 100 ++++++++++++++++++ frontend/dialogs/HealthCheckDialog.hpp | 28 +++++ frontend/widgets/HealthCheckWidget.cpp | 44 ++++++++ frontend/widgets/HealthCheckWidget.hpp | 39 +++++++ 15 files changed, 406 insertions(+) create mode 100644 frontend/components/HealthCheckInfoRow.cpp create mode 100644 frontend/components/HealthCheckInfoRow.hpp create mode 100644 frontend/components/HealthCheckStatusLabel.cpp create mode 100644 frontend/components/HealthCheckStatusLabel.hpp create mode 100644 frontend/dialogs/HealthCheckDialog.cpp create mode 100644 frontend/dialogs/HealthCheckDialog.hpp create mode 100644 frontend/widgets/HealthCheckWidget.cpp create mode 100644 frontend/widgets/HealthCheckWidget.hpp diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 498b72f6928150..b47ed01e7c2ef9 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -2058,6 +2058,21 @@ OBS::HealthCheckService *OBSApp::healthService() return healthService_; } +void OBSApp::openHealthCheckDialog() +{ + if (!mainWindow) { + return; + } + + if (!healthCheckDialog) { + healthCheckDialog = new HealthCheckDialog(mainWindow); + healthCheckDialog->setAttribute(Qt::WA_DeleteOnClose); + healthCheckDialog->show(); + } else { + healthCheckDialog->raise(); + } +} + void OBSApp::pluginManagerOpenDialog() { pluginManager_->open(); diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index 4b8aa8bcd0a394..6ba8173e10ffbd 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -95,6 +96,7 @@ class OBSApp : public QApplication { ThumbnailManager *thumbnailManager = nullptr; QPointer healthService_; + QPointer healthCheckDialog; std::unique_ptr pluginManager_; @@ -241,6 +243,8 @@ private slots: void loadAppModules(struct obs_module_failure_info &mfi); OBS::HealthCheckService *healthService(); + void openHealthCheckDialog(); + ThumbnailManager *thumbnails() const { return thumbnailManager; } // Plugin Manager Accessors diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index 42a811a5086e7f..c898347728e197 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -44,6 +44,10 @@ target_sources( components/FocusList.hpp components/GameCaptureToolbar.cpp components/GameCaptureToolbar.hpp + components/HealthCheckInfoRow.cpp + components/HealthCheckInfoRow.hpp + components/HealthCheckStatusLabel.cpp + components/HealthCheckStatusLabel.hpp components/ImageSourceToolbar.cpp components/ImageSourceToolbar.hpp components/MediaControls.cpp diff --git a/frontend/cmake/ui-dialogs.cmake b/frontend/cmake/ui-dialogs.cmake index ec1c96c44ea83b..094d7dc0f2b7e9 100644 --- a/frontend/cmake/ui-dialogs.cmake +++ b/frontend/cmake/ui-dialogs.cmake @@ -13,6 +13,8 @@ target_link_libraries(obs-studio PRIVATE OBS::properties-view) target_sources( obs-studio PRIVATE + dialogs/HealthCheckDialog.cpp + dialogs/HealthCheckDialog.hpp dialogs/LogUploadDialog.cpp dialogs/LogUploadDialog.hpp dialogs/NameDialog.cpp diff --git a/frontend/cmake/ui-widgets.cmake b/frontend/cmake/ui-widgets.cmake index dd60fbe4bbef2f..be47e555091ba7 100644 --- a/frontend/cmake/ui-widgets.cmake +++ b/frontend/cmake/ui-widgets.cmake @@ -14,6 +14,8 @@ target_sources( widgets/AudioMixer.hpp widgets/ColorSelect.cpp widgets/ColorSelect.hpp + widgets/HealthCheckWidget.cpp + widgets/HealthCheckWidget.hpp widgets/OBSBasic.cpp widgets/OBSBasic.hpp widgets/OBSBasic_Browser.cpp diff --git a/frontend/components/HealthCheckInfoRow.cpp b/frontend/components/HealthCheckInfoRow.cpp new file mode 100644 index 00000000000000..073e7acb5837db --- /dev/null +++ b/frontend/components/HealthCheckInfoRow.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckInfoRow.hpp" + +#include + +#include + +#include + +HealthCheckInfoRow::HealthCheckInfoRow(QWidget *parent, OBS::HealthCheckItem *item) : idian::Row(parent) +{ + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + auto *rowInfo = new idian::RowInfo(this, "", item->message()); + addWidget(rowInfo); + + rowInfo->description()->setWordWrap(true); + rowInfo->description()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + + if (item->action() == nullptr) { + return; + } + + QPushButton *healthActionButton = new QPushButton(this); + healthActionButton->setText(item->action()->text()); + + addWidget(healthActionButton); + connect(healthActionButton, &QPushButton::pressed, item->action(), &OBS::HealthCheckAction::trigger); +} diff --git a/frontend/components/HealthCheckInfoRow.hpp b/frontend/components/HealthCheckInfoRow.hpp new file mode 100644 index 00000000000000..08cfdcc8ab1947 --- /dev/null +++ b/frontend/components/HealthCheckInfoRow.hpp @@ -0,0 +1,32 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +namespace OBS { +class HealthCheckItem; +} + +class HealthCheckInfoRow : public idian::Row { + Q_OBJECT + +public: + HealthCheckInfoRow(QWidget *parent, OBS::HealthCheckItem *item); + ~HealthCheckInfoRow() = default; +}; diff --git a/frontend/components/HealthCheckStatusLabel.cpp b/frontend/components/HealthCheckStatusLabel.cpp new file mode 100644 index 00000000000000..053c3214bff390 --- /dev/null +++ b/frontend/components/HealthCheckStatusLabel.cpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckStatusLabel.hpp" + +#include + +#include + +HealthCheckStatusLabel::HealthCheckStatusLabel(QWidget *parent, OBS::HealthCheckItem *item) : OBS::NoticeLabel(parent) +{ + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + setText(item->statusText()); + + switch (item->status()) { + case OBS::HealthStatus::Warning: + setStyle(OBS::NoticeStyle::Warning); + break; + case OBS::HealthStatus::Critical: + setStyle(OBS::NoticeStyle::Danger); + break; + default: + break; + } +} diff --git a/frontend/components/HealthCheckStatusLabel.hpp b/frontend/components/HealthCheckStatusLabel.hpp new file mode 100644 index 00000000000000..25f4fd4e8d28eb --- /dev/null +++ b/frontend/components/HealthCheckStatusLabel.hpp @@ -0,0 +1,34 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include + +namespace OBS { +class HealthCheckItem; +} + +class HealthCheckStatusLabel : public OBS::NoticeLabel { + Q_OBJECT + +public: + HealthCheckStatusLabel(QWidget *parent, OBS::HealthCheckItem *item); + ~HealthCheckStatusLabel() = default; +}; diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index 9a682cbf15b313..c09fbaeeaf3890 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1686,3 +1686,4 @@ HealthCheck.Status.Invalid="Invalid" HealthCheck.Status.Valid="Valid" HealthCheck.Status.Warning="Warning" HealthCheck.Status.Critical="Critical" +HealthCheck.StatusBar="%1 Issue(s) detected" diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 73a68d5a8b75d7..049cda88287238 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -2868,3 +2868,20 @@ AlignmentSelector::indicator:disabled { AlignmentSelector::indicator:checked:disabled { background: var(--grey3); } + +HealthCheckWidget idian--RowList { + border-radius: 0; + border-bottom-left-radius: var(--border_radius); + border-bottom-right-radius: var(--border_radius); +} + +HealthCheckWidget idian--RowList idian--Row { + padding: var(--action_row_padding) var(--action_row_padding_x); +} + +HealthCheckStatusLabel { + border-radius: var(--border_radius); + border: 1px solid var(--grey3); + padding: var(--padding_large) var(--padding_xlarge); + font-weight: bold; +} diff --git a/frontend/dialogs/HealthCheckDialog.cpp b/frontend/dialogs/HealthCheckDialog.cpp new file mode 100644 index 00000000000000..c0e4d3e1fe58b6 --- /dev/null +++ b/frontend/dialogs/HealthCheckDialog.cpp @@ -0,0 +1,100 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckDialog.hpp" + +#include + +#include +#include +#include + +#include +#include +#include +#include + +HealthCheckDialog::HealthCheckDialog(QWidget *parent) : QWidget(parent) +{ + setWindowFlag(Qt::Window, true); + setWindowTitle("Detected Issues"); + resize(600, 320); + + auto *buttons = new QDialogButtonBox({QDialogButtonBox::Close}, this); + connect(buttons, &QDialogButtonBox::rejected, this, &QWidget::close); + + QVBoxLayout *windowLayout = new QVBoxLayout(this); + setLayout(windowLayout); + + auto *healthScroll = new QScrollArea(this); + + auto *healthScrollContents = new QFrame(healthScroll); + auto *scrollLayout = new QVBoxLayout(healthScrollContents); + scrollLayout->setContentsMargins(0, 0, 0, 0); + + healthScrollContents->setLayout(scrollLayout); + healthScrollContents->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + healthScroll->setFrameShape(QFrame::NoFrame); + healthScroll->setLineWidth(0); + + healthScroll->setWidget(healthScrollContents); + healthScroll->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + healthScroll->setWidgetResizable(true); + healthScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + healthScroll->viewport()->setAutoFillBackground(false); + healthScrollContents->setAutoFillBackground(false); + + auto healthIssues = App()->healthService()->getInvalidItems(); + + auto criticalLayout = new QVBoxLayout(); + auto warningLayout = new QVBoxLayout(); + + scrollLayout->addLayout(criticalLayout); + scrollLayout->addLayout(warningLayout); + + auto addWidgetToStatusLayout = [criticalLayout, warningLayout](HealthCheckWidget *widget) { + if (widget->item()->status() == OBS::HealthStatus::Critical) { + criticalLayout->addWidget(widget); + } else if (widget->item()->status() == OBS::HealthStatus::Warning) { + warningLayout->addWidget(widget); + } + }; + + for (const auto &item : healthIssues) { + auto *healthCheckWidget = new HealthCheckWidget(healthScrollContents, item); + + addWidgetToStatusLayout(healthCheckWidget); + + connect(item, &OBS::HealthCheckItem::statusChanged, healthCheckWidget, [healthCheckWidget, item]() { + if (item->status() == OBS::HealthStatus::Valid) { + healthCheckWidget->hide(); + } + }); + } + + connect(App()->healthService(), &OBS::HealthCheckService::globalStatusChanged, this, [this]() { + if (App()->healthService()->getInvalidCount() == 0) { + close(); + } + }); + + scrollLayout->addStretch(1); + + windowLayout->addWidget(healthScroll); + windowLayout->addWidget(buttons); +} diff --git a/frontend/dialogs/HealthCheckDialog.hpp b/frontend/dialogs/HealthCheckDialog.hpp new file mode 100644 index 00000000000000..b3cf5f40b12c50 --- /dev/null +++ b/frontend/dialogs/HealthCheckDialog.hpp @@ -0,0 +1,28 @@ +/****************************************************************************** + Copyright (C) 2026 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 HealthCheckDialog : public QWidget { + Q_OBJECT + +public: + HealthCheckDialog(QWidget *parent); + ~HealthCheckDialog() = default; +}; diff --git a/frontend/widgets/HealthCheckWidget.cpp b/frontend/widgets/HealthCheckWidget.cpp new file mode 100644 index 00000000000000..ed92825ef92c79 --- /dev/null +++ b/frontend/widgets/HealthCheckWidget.cpp @@ -0,0 +1,44 @@ +/****************************************************************************** + Copyright (C) 2026 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 "HealthCheckWidget.hpp" + +#include +#include +#include + +#include + +#include + +HealthCheckWidget::HealthCheckWidget(QWidget *parent, OBS::HealthCheckItem *item) + : idian::CollapsibleGroup(parent), + healthItem(item) +{ + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + setExpanded(true); + + auto *statusLabel = new HealthCheckStatusLabel(this, item); + row()->layout()->insertWidget(0, statusLabel); + + auto *titleLabel = new QLabel(QString("%2").arg(item->title()), this); + idian::Utils::addClass(titleLabel, "text-title"); + row()->layout()->insertWidget(1, titleLabel); + + auto *infoRow = new HealthCheckInfoRow(this, item); + list()->addRow(infoRow); +} diff --git a/frontend/widgets/HealthCheckWidget.hpp b/frontend/widgets/HealthCheckWidget.hpp new file mode 100644 index 00000000000000..e618ecea4e02f7 --- /dev/null +++ b/frontend/widgets/HealthCheckWidget.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2026 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 + +#include + +namespace OBS { +class HealthCheckItem; +} + +class HealthCheckWidget : public idian::CollapsibleGroup { + Q_OBJECT + +public: + HealthCheckWidget(QWidget *parent, OBS::HealthCheckItem *item); + ~HealthCheckWidget() = default; + + OBS::HealthCheckItem *item() { return healthItem; } + +private: + QPointer healthItem; +}; From 84d3f5e7a4fbf607361f41b9c9e6f0516053dbd4 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 16:19:19 -0400 Subject: [PATCH 6/7] frontend: Add HealthCheck to status bar --- frontend/data/themes/Yami.obt | 9 +++++++ frontend/forms/StatusBarWidget.ui | 37 ++++++++++++++++++++++++-- frontend/widgets/OBSBasicStatusBar.cpp | 30 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 049cda88287238..e7ab3c2a9626e8 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -987,6 +987,15 @@ StatusBarWidget > QFrame { padding: 0px var(--padding_xlarge) var(--padding_small); } +StatusBarWidget #healthFrame { + border-left-width: 0px; + border-right-width: 1px; +} + +StatusBarWidget QPushButton { + padding: 0px var(--padding_xlarge); +} + /* Group Box */ QGroupBox { diff --git a/frontend/forms/StatusBarWidget.ui b/frontend/forms/StatusBarWidget.ui index 594dcb10af4de8..d3a101a659f158 100644 --- a/frontend/forms/StatusBarWidget.ui +++ b/frontend/forms/StatusBarWidget.ui @@ -24,17 +24,50 @@ 0 - 0 + 2 0 - 6 + 2 0 + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + diff --git a/frontend/widgets/OBSBasicStatusBar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp index fba31fc4f10010..8359b43264b9b3 100644 --- a/frontend/widgets/OBSBasicStatusBar.cpp +++ b/frontend/widgets/OBSBasicStatusBar.cpp @@ -1,6 +1,9 @@ #include "OBSBasicStatusBar.hpp" #include "ui_StatusBarWidget.h" +#include +#include +#include #include #include "moc_OBSBasicStatusBar.cpp" @@ -36,6 +39,33 @@ OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) statusWidget->ui->issuesFrame->hide(); statusWidget->ui->kbps->hide(); + if (auto *healthService = App()->healthService()) { + auto *healthIssuesButton = new OBS::NoticeButton(this); + statusWidget->ui->healthFrame->layout()->addWidget(healthIssuesButton); + statusWidget->ui->healthFrame->setVisible(false); + + connect(healthService, &OBS::HealthCheckService::globalStatusChanged, healthIssuesButton, + [statusWidget = this->statusWidget, healthService, + healthIssuesButton](OBS::HealthStatus status) { + int invalidCount = healthService->getInvalidCount(); + + healthIssuesButton->setText(QTStr("HealthCheck.StatusBar").arg(invalidCount)); + switch (status) { + case OBS::HealthStatus::Warning: + healthIssuesButton->setStyle(OBS::NoticeStyle::Warning); + break; + case OBS::HealthStatus::Critical: + healthIssuesButton->setStyle(OBS::NoticeStyle::Danger); + break; + default: + break; + } + statusWidget->ui->healthFrame->setVisible(status != OBS::HealthStatus::Valid); + }); + + connect(healthIssuesButton, &QPushButton::clicked, App(), &OBSApp::openHealthCheckDialog); + } + addPermanentWidget(statusWidget, 1); setMinimumHeight(statusWidget->height()); From d7aad5aa00e22200bae477f9f8ce7eae2827b930 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Thu, 14 May 2026 19:45:28 -0400 Subject: [PATCH 7/7] frontend: Switch missing files popup to health check --- frontend/OBSApp.cpp | 6 --- frontend/OBSApp.hpp | 1 - frontend/data/locale/en-US.ini | 3 ++ frontend/dialogs/OBSMissingFiles.cpp | 4 ++ frontend/dialogs/OBSMissingFiles.hpp | 3 ++ frontend/obs-main.cpp | 12 +----- frontend/widgets/OBSBasic.hpp | 3 ++ .../widgets/OBSBasic_SceneCollections.cpp | 40 +++++++++++++++++-- 8 files changed, 50 insertions(+), 22 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index b47ed01e7c2ef9..72f1847e38e48e 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -72,7 +72,6 @@ extern bool safe_mode; extern bool multi; extern bool disable_3p_plugins; extern bool opt_disable_updater; -extern bool opt_disable_missing_files_check; extern string opt_starting_collection; extern string opt_starting_profile; @@ -1367,11 +1366,6 @@ bool OBSApp::IsUpdaterDisabled() return opt_disable_updater; } -bool OBSApp::IsMissingFilesCheckDisabled() -{ - return opt_disable_missing_files_check; -} - #ifdef __APPLE__ #define INPUT_AUDIO_SOURCE "coreaudio_input_capture" #define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index 6ba8173e10ffbd..210326694b1720 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -200,7 +200,6 @@ private slots: std::string GetVersionString(bool platform = true) const; bool IsPortableMode(); bool IsUpdaterDisabled(); - bool IsMissingFilesCheckDisabled(); const char *InputAudioSource() const; const char *OutputAudioSource() const; diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index c09fbaeeaf3890..f0585587656381 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1687,3 +1687,6 @@ HealthCheck.Status.Valid="Valid" HealthCheck.Status.Warning="Warning" HealthCheck.Status.Critical="Critical" HealthCheck.StatusBar="%1 Issue(s) detected" +HealthCheck.MissingFiles.Title="Missing Files" +HealthCheck.MissingFiles.Info="%1 file(s) appear to be missing from your current Scene Collection.\n\nMissing files can cause performance issues and should be either fixed or removed." +HealthCheck.MissingFiles.Action="Review Missing Files" diff --git a/frontend/dialogs/OBSMissingFiles.cpp b/frontend/dialogs/OBSMissingFiles.cpp index 029945301fe7e5..f01fd847ea780f 100644 --- a/frontend/dialogs/OBSMissingFiles.cpp +++ b/frontend/dialogs/OBSMissingFiles.cpp @@ -106,6 +106,10 @@ void OBSMissingFiles::saveFiles() } } + if (filesModel->found() == filesModel->files.length()) { + emit allFilesResolved(); + } + QDialog::accept(); } diff --git a/frontend/dialogs/OBSMissingFiles.hpp b/frontend/dialogs/OBSMissingFiles.hpp index 2d5bb41e600b77..588bbbcca16105 100644 --- a/frontend/dialogs/OBSMissingFiles.hpp +++ b/frontend/dialogs/OBSMissingFiles.hpp @@ -50,4 +50,7 @@ class OBSMissingFiles : public QDialog { public slots: void dataChanged(); + +signals: + void allFilesResolved(); }; diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp index df0ec6366b5af9..72956c3bd9b165 100644 --- a/frontend/obs-main.cpp +++ b/frontend/obs-main.cpp @@ -73,7 +73,6 @@ bool opt_minimize_tray = false; bool opt_allow_opengl = false; bool opt_always_on_top = false; bool opt_disable_updater = false; -bool opt_disable_missing_files_check = false; string opt_starting_collection; string opt_starting_profile; string opt_starting_scene; @@ -1018,9 +1017,6 @@ int main(int argc, char *argv[]) } else if (arg_is(argv[i], "--disable-updater", nullptr)) { opt_disable_updater = true; - } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { - opt_disable_missing_files_check = true; - } else if (arg_is(argv[i], "--steam", nullptr)) { steam = true; @@ -1046,8 +1042,7 @@ int main(int argc, char *argv[]) "--verbose: Make log more verbose.\n" "--always-on-top: Start in 'always on top' mode.\n\n" "--unfiltered_log: Make log unfiltered.\n\n" - "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" - "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n"; #ifdef _WIN32 MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); @@ -1074,11 +1069,6 @@ int main(int argc, char *argv[]) opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || os_file_exists(BASE_PATH "/disable_updater.txt"); } - - if (!opt_disable_missing_files_check) { - opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || - os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); - } #endif fstream logFile; diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 64bcf0ca4d7d4e..93b959d37cb6da 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -66,6 +66,7 @@ class QWidgetAction; struct QuickTransition; namespace OBS { +class HealthCheckItem; class SceneCollection; struct Rect; enum class LogFileType; @@ -1080,6 +1081,7 @@ private slots: bool projectChanged = false; bool clearingFailed = false; + QPointer missingFilesHealthCheck; QPointer missDialog; OBSSceneCollectionCache collections; @@ -1093,6 +1095,7 @@ private slots: void ClearSceneData(); void LogScenes(); void SaveProjectNow(); + obs_missing_files_t *getMissingFiles(); void ShowMissingFilesDialog(obs_missing_files_t *files); void SetupNewSceneCollection(const std::string &collectionName); diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index fbbcc37b5d9725..35f0d6f83e1aba 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include @@ -1505,10 +1507,23 @@ void OBSBasic::LoadData(obs_data_t *data, SceneCollection &collection) LogScenes(); - if (!App()->IsMissingFilesCheckDisabled()) { - ShowMissingFilesDialog(files); + size_t missingFilesCount = obs_missing_files_count(files); + if (!missingFilesHealthCheck) { + missingFilesHealthCheck = App()->healthService()->createItem(this, "missing_files", + QTStr("HealthCheck.MissingFiles.Title")); + + OBS::HealthCheckAction *healthAction = missingFilesHealthCheck->createAction(); + healthAction->setText(QTStr("HealthCheck.MissingFiles.Action")); + healthAction->setCallback([this]() { on_actionShowMissingFiles_triggered(); }); } + missingFilesHealthCheck->setMessage( + QTStr("HealthCheck.MissingFiles.Info").arg(QString::number(missingFilesCount))); + missingFilesHealthCheck->setStatus(missingFilesCount > 0 ? OBS::HealthStatus::Warning + : OBS::HealthStatus::Valid); + + obs_missing_files_destroy(files); + disableSaving--; if (vcamEnabled) { @@ -1673,7 +1688,12 @@ void OBSBasic::ClearSceneData() void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) { - if (obs_missing_files_count(files) > 0) { + size_t missingCount = obs_missing_files_count(files); + if (missingCount > 0) { + missingFilesHealthCheck->setStatus(OBS::HealthStatus::Warning); + missingFilesHealthCheck->setMessage( + QTStr("HealthCheck.MissingFiles.Info").arg(QString::number(missingCount))); + /* When loading the missing files dialog on launch, the * window hasn't fully initialized by this point on macOS, * so put this at the end of the current task queue. Fixes @@ -1683,8 +1703,13 @@ void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) missDialog->setAttribute(Qt::WA_DeleteOnClose, true); missDialog->show(); missDialog->raise(); + + connect(missDialog, &OBSMissingFiles::allFilesResolved, this, + [&]() { missingFilesHealthCheck->setStatus(OBS::HealthStatus::Valid); }); }); } else { + missingFilesHealthCheck->setStatus(OBS::HealthStatus::Valid); + obs_missing_files_destroy(files); /* Only raise dialog if triggered manually */ @@ -1695,7 +1720,7 @@ void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) } } -void OBSBasic::on_actionShowMissingFiles_triggered() +obs_missing_files_t *OBSBasic::getMissingFiles() { obs_missing_files_t *files = obs_missing_files_create(); @@ -1705,5 +1730,12 @@ void OBSBasic::on_actionShowMissingFiles_triggered() }; obs_enum_all_sources(cb_sources, files); + + return files; +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = getMissingFiles(); ShowMissingFilesDialog(files); }