diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 45d307ff6061d5..b0c5871f979862 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) @@ -71,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; @@ -1266,7 +1266,6 @@ bool OBSApp::OBSInit() thumbnailManager = new ThumbnailManager(this); mainWindow = new OBSBasic(); - mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); #ifndef __APPLE__ @@ -1342,11 +1341,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" @@ -1997,6 +1991,30 @@ 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::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 dabca78761c053..b50357b6ea98a9 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -51,6 +52,7 @@ class CrashHandler; enum class LogFileType { NoType, CurrentAppLog, LastAppLog, CrashLog }; enum class LogFileState { NoState, New, Uploaded }; +class HealthCheckService; class PluginManager; } // namespace OBS @@ -92,6 +94,8 @@ class OBSApp : public QApplication { std::deque translatorHooks; ThumbnailManager *thumbnailManager = nullptr; + QPointer healthService_; + QPointer healthCheckDialog; std::unique_ptr pluginManager_; @@ -195,7 +199,6 @@ private slots: std::string GetVersionString(bool platform = true) const; bool IsPortableMode(); bool IsUpdaterDisabled(); - bool IsMissingFilesCheckDisabled(); const char *InputAudioSource() const; const char *OutputAudioSource() const; @@ -232,6 +235,9 @@ 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 34f4c0e7fe3a1d..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 @@ -55,6 +59,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/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-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/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/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/locale/en-US.ini b/frontend/data/locale/en-US.ini index a86d2a729a33c6..988f61334eb60c 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1681,3 +1681,13 @@ 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" +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/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 739a0329e0084d..b129cdaa6a656a 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); @@ -333,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); @@ -916,6 +983,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 { @@ -2475,25 +2551,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; @@ -2503,11 +2579,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 { @@ -2515,17 +2592,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); } @@ -2622,7 +2703,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); @@ -2659,35 +2740,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); } @@ -2703,7 +2803,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); @@ -2714,32 +2814,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); } @@ -2782,3 +2863,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/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/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/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/obs-main.cpp b/frontend/obs-main.cpp index 8acdba5fdcbf1b..a06c016dfb3ed6 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; @@ -990,9 +989,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; @@ -1018,8 +1014,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); @@ -1046,11 +1041,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/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 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; +}; diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index efb90ac332b456..855878584b9dd1 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; @@ -588,6 +589,10 @@ private slots: QPointer colorWidgetAction; QPointer colorSelect; +#ifdef ENABLE_IDIAN_PLAYGROUND + QPointer playground; +#endif + QList visDialogs; QList modalDialogs; QList visMsgBoxes; @@ -1068,6 +1073,7 @@ private slots: bool projectChanged = false; bool clearingFailed = false; + QPointer missingFilesHealthCheck; QPointer missDialog; OBSSceneCollectionCache collections; @@ -1081,6 +1087,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/OBSBasicStatusBar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp index 1a202ed2d78942..25b83b61613f5b 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()); diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp index 65a7f1a88ad959..01d40e0722c76d 100644 --- a/frontend/widgets/OBSBasic_MainControls.cpp +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -644,10 +644,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/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index 0aa3e6331c8ae6..ac5302517097fd 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include @@ -1477,8 +1479,22 @@ 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")); + missingFilesHealthCheck->setMessage( + QTStr("HealthCheck.MissingFiles.Info").arg(QString::number(missingFilesCount))); + + OBS::HealthCheckAction *healthAction = missingFilesHealthCheck->createAction(); + healthAction->setText(QTStr("HealthCheck.MissingFiles.Action")); + healthAction->setCallback([this]() { on_actionShowMissingFiles_triggered(); }); + } + + missingFilesCount > 0 ? missingFilesHealthCheck->setStatus(OBS::HealthStatus::Warning) + : missingFilesHealthCheck->setStatus(OBS::HealthStatus::Valid); + + obs_missing_files_destroy(files); disableSaving--; @@ -1638,7 +1654,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 @@ -1648,8 +1669,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 */ @@ -1659,7 +1685,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(); @@ -1669,5 +1695,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); } 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 29d478a49944a9..2117733da0c7b3 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 6a5f83da8a21df..016aace653ea7d 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,121 +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) -{ - 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) +Row::Row(QWidget *parent) : QFrame(parent), Utils(this) { - 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) @@ -160,11 +65,7 @@ void Row::enterEvent(QEnterEvent *event) if (buddyWidget) Utils::repolish(buddyWidget); - if (hasPrefix() || hasSuffix()) { - Utils::polishChildren(); - } - - GenericRow::enterEvent(event); + QFrame::enterEvent(event); } void Row::leaveEvent(QEvent *event) @@ -172,11 +73,7 @@ void Row::leaveEvent(QEvent *event) if (buddyWidget) Utils::repolish(buddyWidget); - if (hasPrefix() || hasSuffix()) { - Utils::polishChildren(); - } - - GenericRow::leaveEvent(event); + QFrame::leaveEvent(event); } void Row::mouseReleaseEvent(QMouseEvent *event) @@ -197,14 +94,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; @@ -214,6 +112,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; @@ -223,158 +122,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 68% rename from shared/qt/idian/widgets/PropertiesList.cpp rename to shared/qt/idian/widgets/RowList.cpp index 342bbea1914834..fae414e53f8d54 100644 --- a/shared/qt/idian/widgets/PropertiesList.cpp +++ b/shared/qt/idian/widgets/RowList.cpp @@ -15,70 +15,75 @@ 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 - if (last) + if (last) { Utils::removeClass(last, "last"); + } // 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()) + if (item->widget()) { item->widget()->deleteLater(); + } delete item; - item = layout->takeAt(0); + item = rowLayout->takeAt(0); } adjustSize();