From e8f6143769d77d76b9bda9cb5e5966fffe8b3198 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Tue, 18 Mar 2025 19:56:16 -0700 Subject: [PATCH] frontend: Add new Idian widgets Co-Authored-By: derrod --- frontend/CMakeLists.txt | 1 + frontend/cmake/feature-idian-playground.cmake | 9 + frontend/cmake/ui-components.cmake | 18 + frontend/cmake/ui-dialogs.cmake | 2 + frontend/components/idian/OBSActionRow.cpp | 372 ++++++++++++++++++ frontend/components/idian/OBSActionRow.hpp | 215 ++++++++++ frontend/components/idian/OBSCheckBox.cpp | 20 + frontend/components/idian/OBSCheckBox.hpp | 42 ++ frontend/components/idian/OBSComboBox.cpp | 65 +++ frontend/components/idian/OBSComboBox.hpp | 54 +++ .../components/idian/OBSDoubleSpinBox.cpp | 45 +++ .../components/idian/OBSDoubleSpinBox.hpp | 38 ++ frontend/components/idian/OBSGroupBox.cpp | 122 ++++++ frontend/components/idian/OBSGroupBox.hpp | 68 ++++ frontend/components/idian/OBSIdianWidget.hpp | 152 +++++++ .../components/idian/OBSPropertiesList.cpp | 80 ++++ .../components/idian/OBSPropertiesList.hpp | 57 +++ frontend/components/idian/OBSSpinBox.cpp | 42 ++ frontend/components/idian/OBSSpinBox.hpp | 38 ++ frontend/components/idian/OBSToggleSwitch.cpp | 247 ++++++++++++ frontend/components/idian/OBSToggleSwitch.hpp | 121 ++++++ frontend/components/idian/obs-widgets.hpp | 37 ++ frontend/data/themes/System.obt | 105 +++++ frontend/data/themes/Yami.obt | 326 ++++++++++++++- frontend/dialogs/OBSIdianPlayground.cpp | 132 +++++++ frontend/dialogs/OBSIdianPlayground.hpp | 36 ++ frontend/forms/OBSBasic.ui | 6 + frontend/forms/OBSIdianPlayground.ui | 55 +++ frontend/forms/images/hide.svg | 5 + frontend/forms/obs.qrc | 1 + frontend/widgets/OBSBasic.cpp | 4 + frontend/widgets/OBSBasic.hpp | 1 + frontend/widgets/OBSBasic_MainControls.cpp | 14 + 33 files changed, 2519 insertions(+), 11 deletions(-) create mode 100644 frontend/cmake/feature-idian-playground.cmake create mode 100644 frontend/components/idian/OBSActionRow.cpp create mode 100644 frontend/components/idian/OBSActionRow.hpp create mode 100644 frontend/components/idian/OBSCheckBox.cpp create mode 100644 frontend/components/idian/OBSCheckBox.hpp create mode 100644 frontend/components/idian/OBSComboBox.cpp create mode 100644 frontend/components/idian/OBSComboBox.hpp create mode 100644 frontend/components/idian/OBSDoubleSpinBox.cpp create mode 100644 frontend/components/idian/OBSDoubleSpinBox.hpp create mode 100644 frontend/components/idian/OBSGroupBox.cpp create mode 100644 frontend/components/idian/OBSGroupBox.hpp create mode 100644 frontend/components/idian/OBSIdianWidget.hpp create mode 100644 frontend/components/idian/OBSPropertiesList.cpp create mode 100644 frontend/components/idian/OBSPropertiesList.hpp create mode 100644 frontend/components/idian/OBSSpinBox.cpp create mode 100644 frontend/components/idian/OBSSpinBox.hpp create mode 100644 frontend/components/idian/OBSToggleSwitch.cpp create mode 100644 frontend/components/idian/OBSToggleSwitch.hpp create mode 100644 frontend/components/idian/obs-widgets.hpp create mode 100644 frontend/dialogs/OBSIdianPlayground.cpp create mode 100644 frontend/dialogs/OBSIdianPlayground.hpp create mode 100644 frontend/forms/OBSIdianPlayground.ui create mode 100644 frontend/forms/images/hide.svg diff --git a/frontend/CMakeLists.txt b/frontend/CMakeLists.txt index fed457ee5..90f09405f 100644 --- a/frontend/CMakeLists.txt +++ b/frontend/CMakeLists.txt @@ -63,6 +63,7 @@ include(cmake/feature-twitch.cmake) include(cmake/feature-restream.cmake) include(cmake/feature-youtube.cmake) include(cmake/feature-whatsnew.cmake) +include(cmake/feature-idian-playground.cmake) add_subdirectory(plugins) diff --git a/frontend/cmake/feature-idian-playground.cmake b/frontend/cmake/feature-idian-playground.cmake new file mode 100644 index 000000000..e66b5a307 --- /dev/null +++ b/frontend/cmake/feature-idian-playground.cmake @@ -0,0 +1,9 @@ +option(ENABLE_WIDGET_PLAYGROUND "Enable building custom widget demo window" OFF) + +if(ENABLE_WIDGET_PLAYGROUND) + target_sources( + obs-studio + PRIVATE forms/OBSIdianPlayground.ui dialogs/OBSIdianPlayground.hpp dialogs/OBSIdianPlayground.cpp + ) + target_enable_feature(obs-studio "Widget Playground" ENABLE_WIDGET_PLAYGROUND) +endif() diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index ad0506a08..98c2bba4d 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -81,4 +81,22 @@ target_sources( components/VolumeSlider.hpp components/WindowCaptureToolbar.cpp components/WindowCaptureToolbar.hpp + components/idian/OBSActionRow.cpp + components/idian/OBSActionRow.hpp + components/idian/OBSCheckBox.cpp + components/idian/OBSCheckBox.hpp + components/idian/OBSComboBox.cpp + components/idian/OBSComboBox.hpp + components/idian/OBSDoubleSpinBox.cpp + components/idian/OBSDoubleSpinBox.hpp + components/idian/OBSGroupBox.cpp + components/idian/OBSGroupBox.hpp + components/idian/OBSIdianWidget.hpp + components/idian/OBSPropertiesList.cpp + components/idian/OBSPropertiesList.hpp + components/idian/OBSSpinBox.cpp + components/idian/OBSSpinBox.hpp + components/idian/OBSToggleSwitch.cpp + components/idian/OBSToggleSwitch.hpp + components/idian/obs-widgets.hpp ) diff --git a/frontend/cmake/ui-dialogs.cmake b/frontend/cmake/ui-dialogs.cmake index de490a383..fad2e1b37 100644 --- a/frontend/cmake/ui-dialogs.cmake +++ b/frontend/cmake/ui-dialogs.cmake @@ -37,4 +37,6 @@ target_sources( dialogs/OBSRemux.hpp dialogs/OBSWhatsNew.cpp dialogs/OBSWhatsNew.hpp + dialogs/OBSIdianPlayground.cpp + dialogs/OBSIdianPlayground.hpp ) diff --git a/frontend/components/idian/OBSActionRow.cpp b/frontend/components/idian/OBSActionRow.cpp new file mode 100644 index 000000000..2944f6fe8 --- /dev/null +++ b/frontend/components/idian/OBSActionRow.cpp @@ -0,0 +1,372 @@ +/****************************************************************************** + 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 + +#include "OBSActionRow.hpp" +#include +#include + +OBSActionRowWidget::OBSActionRowWidget(QWidget *parent) : OBSActionRow(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); + OBSIdianUtils::addClass(nameLabel, "title"); + + descriptionLabel = new QLabel(); + descriptionLabel->setVisible(false); + OBSIdianUtils::addClass(descriptionLabel, "description"); + + labelLayout->addWidget(nameLabel); + labelLayout->addWidget(descriptionLabel); + + layout->addLayout(labelLayout, 0, 1, Qt::AlignLeft); +} + +void OBSActionRowWidget::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 OBSActionRowWidget::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 OBSActionRowWidget::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 OBSActionRowWidget::setSuffixEnabled(bool enabled) +{ + if (!_suffix) + return; + if (enabled) + setPrefixEnabled(false); + if (enabled == _suffix->isEnabled() && enabled == _suffix->isVisible()) + return; + + _suffix->setEnabled(enabled); + _suffix->setVisible(enabled); +} + +void OBSActionRowWidget::setTitle(QString name) +{ + nameLabel->setText(name); + setAccessibleName(name); + showTitle(true); +} + +void OBSActionRowWidget::setDescription(QString description) +{ + descriptionLabel->setText(description); + setAccessibleDescription(description); + showDescription(true); +} + +void OBSActionRowWidget::showTitle(bool visible) +{ + nameLabel->setVisible(visible); +} + +void OBSActionRowWidget::showDescription(bool visible) +{ + descriptionLabel->setVisible(visible); +} + +void OBSActionRowWidget::setBuddy(QWidget *widget) +{ + buddyWidget = widget; + OBSIdianUtils::addClass(widget, "row-buddy"); +} + +void OBSActionRowWidget::setChangeCursor(bool change) +{ + changeCursor = change; + OBSIdianUtils::toggleClass("cursor-pointer", change); +} + +void OBSActionRowWidget::enterEvent(QEnterEvent *event) +{ + if (!isEnabled()) + return; + + if (changeCursor) { + setCursor(Qt::PointingHandCursor); + } + + OBSIdianUtils::addClass("hover"); + + if (buddyWidget) + OBSIdianUtils::repolish(buddyWidget); + + if (hasPrefix() || hasSuffix()) { + OBSIdianUtils::polishChildren(); + } + + OBSActionRow::enterEvent(event); +} + +void OBSActionRowWidget::leaveEvent(QEvent *event) +{ + OBSIdianUtils::removeClass("hover"); + + if (buddyWidget) + OBSIdianUtils::repolish(buddyWidget); + + if (hasPrefix() || hasSuffix()) { + OBSIdianUtils::polishChildren(); + } + + OBSActionRow::leaveEvent(event); +} + +void OBSActionRowWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() & Qt::LeftButton) { + emit clicked(); + } + QFrame::mouseReleaseEvent(event); +} + +void OBSActionRowWidget::keyReleaseEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Enter) { + emit clicked(); + } + QFrame::keyReleaseEvent(event); +} + +void OBSActionRowWidget::connectBuddyWidget(QWidget *widget) +{ + setAccessibleName(nameLabel->text()); + setFocusProxy(widget); + setBuddy(widget); + + /* If element is an OBSToggleSwitch and checkable, forward + * clicks to the widget */ + OBSToggleSwitch *obsToggle = dynamic_cast(widget); + if (obsToggle && obsToggle->isCheckable()) { + setChangeCursor(true); + + connect(this, &OBSActionRowWidget::clicked, obsToggle, &OBSToggleSwitch::click); + return; + } + + /* If element is any other QAbstractButton subclass, + * and checkable, forward clicks to the widget. */ + QAbstractButton *button = dynamic_cast(widget); + if (button && button->isCheckable()) { + setChangeCursor(true); + + connect(this, &OBSActionRowWidget::clicked, button, &QAbstractButton::click); + return; + } + + /* If element is an OBSComboBox, clicks toggle the dropdown. */ + OBSComboBox *obsCombo = dynamic_cast(widget); + if (obsCombo) { + setChangeCursor(true); + + connect(this, &OBSActionRowWidget::clicked, obsCombo, &OBSComboBox::togglePopup); + return; + } +} + +/* +* Button for expanding a collapsible ActionRow +*/ +OBSActionRowExpandButton::OBSActionRowExpandButton(QWidget *parent) : QAbstractButton(parent), OBSIdianUtils(this) +{ + setCheckable(true); +} + +void OBSActionRowExpandButton::paintEvent(QPaintEvent *event) +{ + UNUSED_PARAMETER(event); + + 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); +} + +/* +* ActionRow variant that can be expanded to show another properties list +*/ +OBSCollapsibleRowWidget::OBSCollapsibleRowWidget(const QString &name, QWidget *parent) : OBSActionRow(parent) +{ + layout = new QVBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + setLayout(layout); + + rowWidget = new OBSCollapsibleRowFrame(); + rowLayout = new QHBoxLayout(); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(0); + rowWidget->setLayout(rowLayout); + + actionRow = new OBSActionRowWidget(); + actionRow->setTitle(name); + actionRow->setChangeCursor(false); + + rowLayout->addWidget(actionRow); + + propertyList = new OBSPropertiesList(this); + propertyList->setVisible(false); + + expandFrame = new QFrame(); + btnLayout = new QHBoxLayout(); + btnLayout->setContentsMargins(0, 0, 0, 0); + btnLayout->setSpacing(0); + expandFrame->setLayout(btnLayout); + OBSIdianUtils::addClass(expandFrame, "btn-frame"); + actionRow->setBuddy(expandFrame); + + expandButton = new OBSActionRowExpandButton(this); + btnLayout->addWidget(expandButton); + + rowLayout->addWidget(expandFrame); + + layout->addWidget(rowWidget); + layout->addWidget(propertyList); + + actionRow->setFocusProxy(expandButton); + + connect(expandButton, &QAbstractButton::clicked, this, &OBSCollapsibleRowWidget::toggleVisibility); + + connect(actionRow, &OBSActionRowWidget::clicked, expandButton, &QAbstractButton::click); +} + +OBSCollapsibleRowWidget::OBSCollapsibleRowWidget(const QString &name, const QString &desc, QWidget *parent) + : OBSCollapsibleRowWidget(name, parent) +{ + actionRow->setDescription(desc); +} + +void OBSCollapsibleRowWidget::setCheckable(bool check) +{ + checkable = check; + + if (checkable && !toggleSwitch) { + propertyList->setEnabled(false); + OBSIdianUtils::polishChildren(propertyList); + + toggleSwitch = new OBSToggleSwitch(false); + + actionRow->setSuffix(toggleSwitch, false); + connect(toggleSwitch, &OBSToggleSwitch::toggled, propertyList, &OBSPropertiesList::setEnabled); + } + + if (!checkable && toggleSwitch) { + propertyList->setEnabled(true); + OBSIdianUtils::polishChildren(propertyList); + + actionRow->suffix()->deleteLater(); + } +} + +void OBSCollapsibleRowWidget::toggleVisibility() +{ + bool visible = !propertyList->isVisible(); + + propertyList->setVisible(visible); + expandButton->setChecked(visible); +} + +void OBSCollapsibleRowWidget::addRow(OBSActionRow *actionRow) +{ + propertyList->addRow(actionRow); +} + +OBSCollapsibleRowFrame::OBSCollapsibleRowFrame(QWidget *parent) : QFrame(parent), OBSIdianUtils(this) {} + +void OBSCollapsibleRowFrame::enterEvent(QEnterEvent *event) +{ + setCursor(Qt::PointingHandCursor); + + OBSIdianUtils::addClass("hover"); + OBSIdianUtils::polishChildren(); + + QWidget::enterEvent(event); +} + +void OBSCollapsibleRowFrame::leaveEvent(QEvent *event) +{ + OBSIdianUtils::removeClass("hover"); + OBSIdianUtils::polishChildren(); + + QWidget::leaveEvent(event); +} diff --git a/frontend/components/idian/OBSActionRow.hpp b/frontend/components/idian/OBSActionRow.hpp new file mode 100644 index 000000000..ee875efad --- /dev/null +++ b/frontend/components/idian/OBSActionRow.hpp @@ -0,0 +1,215 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "OBSIdianWidget.hpp" +#include "OBSPropertiesList.hpp" +#include "OBSToggleSwitch.hpp" +#include "OBSComboBox.hpp" +#include "OBSSpinBox.hpp" +#include "OBSDoubleSpinBox.hpp" + +/** +* Base class mostly so adding stuff to a list is easier +*/ +class OBSActionRow : public QFrame, public OBSIdianUtils { + Q_OBJECT + +public: + OBSActionRow(QWidget *parent = nullptr) : QFrame(parent), OBSIdianUtils(this) { setAccessibleName(""); }; +}; + +/** +* Proxy for QScrollArea for QSS styling +*/ +class OBSScrollArea : public QScrollArea { + Q_OBJECT +public: + OBSScrollArea(QWidget *parent = nullptr) : QScrollArea(parent) {} +}; + +/** +* Generic OBS row widget containing one or more controls +*/ +class OBSActionRowWidget : public OBSActionRow { + Q_OBJECT + +public: + OBSActionRowWidget(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 setTitle(QString name); + void setDescription(QString description); + + void showTitle(bool visible); + void showDescription(bool visible); + + void setBuddy(QWidget *w); + + void setChangeCursor(bool change); + +signals: + void clicked(); + +protected: + void enterEvent(QEnterEvent *) override; + void leaveEvent(QEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; + void keyReleaseEvent(QKeyEvent *) override; + bool hasDescription() const { return descriptionLabel != nullptr; } + + void focusInEvent(QFocusEvent *event) override + { + OBSIdianUtils::showKeyFocused(event); + QFrame::focusInEvent(event); + } + + void focusOutEvent(QFocusEvent *event) override + { + OBSIdianUtils::hideKeyFocused(event); + QFrame::focusOutEvent(event); + } + +private: + QGridLayout *layout; + + QVBoxLayout *labelLayout = nullptr; + + QLabel *nameLabel = nullptr; + QLabel *descriptionLabel = nullptr; + + QWidget *_prefix = nullptr; + QWidget *_suffix = nullptr; + + QWidget *buddyWidget = nullptr; + + void connectBuddyWidget(QWidget *widget); + bool changeCursor = false; +}; + +/** +* Collapsible row expand button +*/ +class OBSActionRowExpandButton : public QAbstractButton, public OBSIdianUtils { + Q_OBJECT + +private: + QPixmap extendDown; + QPixmap extendUp; + + friend class OBSCollapsibleRowWidget; + +protected: + explicit OBSActionRowExpandButton(QWidget *parent = nullptr); + + void paintEvent(QPaintEvent *) override; + + void focusInEvent(QFocusEvent *event) override + { + OBSIdianUtils::showKeyFocused(event); + QAbstractButton::focusInEvent(event); + } + + void focusOutEvent(QFocusEvent *event) override + { + OBSIdianUtils::hideKeyFocused(event); + QAbstractButton::focusOutEvent(event); + } +}; + +class OBSCollapsibleRowFrame : protected QFrame, protected OBSIdianUtils { + Q_OBJECT + +signals: + void clicked(); + +protected: + explicit OBSCollapsibleRowFrame(QWidget *parent = nullptr); + + void enterEvent(QEnterEvent *) override; + void leaveEvent(QEvent *) override; + + void focusInEvent(QFocusEvent *event) override + { + OBSIdianUtils::showKeyFocused(event); + QWidget::focusInEvent(event); + } + + void focusOutEvent(QFocusEvent *event) override + { + OBSIdianUtils::hideKeyFocused(event); + QWidget::focusOutEvent(event); + } + +private: + friend class OBSCollapsibleRowWidget; +}; + +/** +* Collapsible Generic OBS property container +*/ +class OBSCollapsibleRowWidget : public OBSActionRow { + Q_OBJECT + +public: + OBSCollapsibleRowWidget(const QString &name, QWidget *parent = nullptr); + OBSCollapsibleRowWidget(const QString &name, const QString &desc = nullptr, QWidget *parent = nullptr); + + void setCheckable(bool check); + bool isCheckable() { return checkable; } + + void addRow(OBSActionRow *actionRow); + +private: + void toggleVisibility(); + + QPixmap extendDown; + QPixmap extendUp; + + QVBoxLayout *layout; + OBSCollapsibleRowFrame *rowWidget; + QHBoxLayout *rowLayout; + + OBSActionRowWidget *actionRow; + QFrame *expandFrame; + QHBoxLayout *btnLayout; + OBSActionRowExpandButton *expandButton; + OBSPropertiesList *propertyList; + + OBSToggleSwitch *toggleSwitch = nullptr; + bool checkable = false; +}; diff --git a/frontend/components/idian/OBSCheckBox.cpp b/frontend/components/idian/OBSCheckBox.cpp new file mode 100644 index 000000000..e5c947771 --- /dev/null +++ b/frontend/components/idian/OBSCheckBox.cpp @@ -0,0 +1,20 @@ +/****************************************************************************** + Copyright (C) 2024 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 "OBSCheckBox.hpp" + +OBSCheckBox::OBSCheckBox(QWidget *parent) : QCheckBox(parent), OBSIdianUtils(this) {} diff --git a/frontend/components/idian/OBSCheckBox.hpp b/frontend/components/idian/OBSCheckBox.hpp new file mode 100644 index 000000000..54dab0ce6 --- /dev/null +++ b/frontend/components/idian/OBSCheckBox.hpp @@ -0,0 +1,42 @@ +/****************************************************************************** + Copyright (C) 2024 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 "OBSIdianWidget.hpp" + +class OBSCheckBox : public QCheckBox, public OBSIdianUtils { + Q_OBJECT; + +public: + OBSCheckBox(QWidget *parent = nullptr); + +protected: + void focusInEvent(QFocusEvent *e) override + { + OBSIdianUtils::showKeyFocused(e); + QAbstractButton::focusInEvent(e); + } + + void focusOutEvent(QFocusEvent *e) override + { + OBSIdianUtils::hideKeyFocused(e); + QAbstractButton::focusOutEvent(e); + } +}; diff --git a/frontend/components/idian/OBSComboBox.cpp b/frontend/components/idian/OBSComboBox.cpp new file mode 100644 index 000000000..224bea572 --- /dev/null +++ b/frontend/components/idian/OBSComboBox.cpp @@ -0,0 +1,65 @@ +/****************************************************************************** + Copyright (C) 2024 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 "OBSActionRow.hpp" +#include "OBSComboBox.hpp" +#include +#include + +#define UNUSED_PARAMETER(param) (void)param + +OBSComboBox::OBSComboBox(QWidget *parent) : QComboBox(parent), OBSIdianUtils(this) {} + +void OBSComboBox::showPopup() +{ + if (allowOpeningPopup) { + allowOpeningPopup = false; + QComboBox::showPopup(); + } +} + +void OBSComboBox::hidePopup() +{ + // It would be nice to find a better way to do this. + // + // When the dropdown is closed, block attempts to open it + // again for a short time. This is so clicking a GenericRow + // with the dropdown open doesn't immediately close and re-open it. + // I have tried all sorts of things involving handling mouse events + // and event filters on both GenericRow and ComboBox. + // + // All my efforts have failed so we get this instead. + allowOpeningPopup = false; + QTimer::singleShot(120, this, [=]() { allowOpeningPopup = true; }); + + QComboBox::hidePopup(); +} + +void OBSComboBox::mousePressEvent(QMouseEvent *event) +{ + blog(LOG_INFO, "OBSComboBox::mousePressEvent"); + QComboBox::mousePressEvent(event); +} + +void OBSComboBox::togglePopup() +{ + if (view()->isVisible()) { + OBSComboBox::hidePopup(); + } else { + OBSComboBox::showPopup(); + } +} diff --git a/frontend/components/idian/OBSComboBox.hpp b/frontend/components/idian/OBSComboBox.hpp new file mode 100644 index 000000000..c67acd866 --- /dev/null +++ b/frontend/components/idian/OBSComboBox.hpp @@ -0,0 +1,54 @@ +/****************************************************************************** + Copyright (C) 2024 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 "OBSIdianWidget.hpp" + +class OBSComboBox : public QComboBox, public OBSIdianUtils { + Q_OBJECT + +public: + OBSComboBox(QWidget *parent = nullptr); + +public Q_SLOTS: + void togglePopup(); + +private: + bool allowOpeningPopup = true; + +protected: + void showPopup() override; + void hidePopup() override; + + void mousePressEvent(QMouseEvent *event) override; + + void focusInEvent(QFocusEvent *e) override + { + OBSIdianUtils::showKeyFocused(e); + QComboBox::focusInEvent(e); + } + + void focusOutEvent(QFocusEvent *e) override + { + OBSIdianUtils::hideKeyFocused(e); + QComboBox::focusOutEvent(e); + } +}; diff --git a/frontend/components/idian/OBSDoubleSpinBox.cpp b/frontend/components/idian/OBSDoubleSpinBox.cpp new file mode 100644 index 000000000..b233cf114 --- /dev/null +++ b/frontend/components/idian/OBSDoubleSpinBox.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** + 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 "OBSDoubleSpinBox.hpp" + +OBSDoubleSpinBox::OBSDoubleSpinBox(QWidget *parent) : QFrame(parent) +{ + layout = new QHBoxLayout(); + setLayout(layout); + + layout->setContentsMargins(0, 0, 0, 0); + + decr = new QPushButton("-"); + decr->setObjectName("obsSpinBoxButton"); + layout->addWidget(decr); + + setFocusProxy(decr); + + sbox = new QDoubleSpinBox(); + sbox->setObjectName("obsSpinBox"); + sbox->setButtonSymbols(QAbstractSpinBox::NoButtons); + sbox->setAlignment(Qt::AlignCenter); + layout->addWidget(sbox); + + incr = new QPushButton("+"); + incr->setObjectName("obsSpinBoxButton"); + layout->addWidget(incr); + + connect(decr, &QPushButton::pressed, sbox, &QDoubleSpinBox::stepDown); + connect(incr, &QPushButton::pressed, sbox, &QDoubleSpinBox::stepUp); +} diff --git a/frontend/components/idian/OBSDoubleSpinBox.hpp b/frontend/components/idian/OBSDoubleSpinBox.hpp new file mode 100644 index 000000000..03b8c36ef --- /dev/null +++ b/frontend/components/idian/OBSDoubleSpinBox.hpp @@ -0,0 +1,38 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +class OBSDoubleSpinBox : public QFrame { + Q_OBJECT; + +public: + OBSDoubleSpinBox(QWidget *parent = nullptr); + + QDoubleSpinBox *spinBox() const { return sbox; } + +private: + QHBoxLayout *layout; + QPushButton *decr; + QPushButton *incr; + QDoubleSpinBox *sbox; +}; diff --git a/frontend/components/idian/OBSGroupBox.cpp b/frontend/components/idian/OBSGroupBox.cpp new file mode 100644 index 000000000..4df2c98d5 --- /dev/null +++ b/frontend/components/idian/OBSGroupBox.cpp @@ -0,0 +1,122 @@ +/****************************************************************************** + 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 "OBSIdianWidget.hpp" +#include "OBSGroupBox.hpp" + +OBSGroupBox::OBSGroupBox(QWidget *parent) : QFrame(parent), OBSIdianUtils(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); + OBSIdianUtils::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); + OBSIdianUtils::addClass(contentsContainer, "contents"); + + layout->addWidget(headerContainer); + layout->addWidget(contentsContainer); + + propertyList = new OBSPropertiesList(this); + + setLayout(layout); + + contentsLayout->addWidget(propertyList); + /*contentsContainer->setSizePolicy(policy);*/ + + nameLabel = new QLabel(); + OBSIdianUtils::addClass(nameLabel, "title"); + nameLabel->setVisible(false); + labelLayout->addWidget(nameLabel); + + descriptionLabel = new QLabel(); + OBSIdianUtils::addClass(descriptionLabel, "description"); + descriptionLabel->setVisible(false); + labelLayout->addWidget(descriptionLabel); +} + +void OBSGroupBox::addRow(OBSActionRow *actionRow) const +{ + propertyList->addRow(actionRow); +} + +void OBSGroupBox::setTitle(QString name) +{ + nameLabel->setText(name); + setAccessibleName(name); + showTitle(true); +} + +void OBSGroupBox::setDescription(QString desc) +{ + descriptionLabel->setText(desc); + setAccessibleDescription(desc); + showDescription(true); +} + +void OBSGroupBox::showTitle(bool visible) +{ + nameLabel->setVisible(visible); +} + +void OBSGroupBox::showDescription(bool visible) +{ + descriptionLabel->setVisible(visible); +} + +void OBSGroupBox::setCheckable(bool check) +{ + checkable = check; + + if (checkable && !toggleSwitch) { + toggleSwitch = new OBSToggleSwitch(true); + controlLayout->addWidget(toggleSwitch); + connect(toggleSwitch, &OBSToggleSwitch::toggled, this, + [=](bool checked) { propertyList->setEnabled(checked); }); + } + + if (!checkable && toggleSwitch) { + controlLayout->removeWidget(toggleSwitch); + toggleSwitch->deleteLater(); + } +} diff --git a/frontend/components/idian/OBSGroupBox.hpp b/frontend/components/idian/OBSGroupBox.hpp new file mode 100644 index 000000000..a2aa5e18d --- /dev/null +++ b/frontend/components/idian/OBSGroupBox.hpp @@ -0,0 +1,68 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#include "OBSActionRow.hpp" +#include "OBSPropertiesList.hpp" +#include "OBSToggleSwitch.hpp" + +class OBSGroupBox : public QFrame, public OBSIdianUtils { + Q_OBJECT + +public: + OBSGroupBox(QWidget *parent = nullptr); + + OBSPropertiesList *properties() const { return propertyList; } + + void addRow(OBSActionRow *actionRow) const; + + void setTitle(QString name); + void setDescription(QString desc); + + void showTitle(bool visible); + void showDescription(bool visible); + + void setCheckable(bool check); + 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; + + QWidget *contentsContainer = nullptr; + QVBoxLayout *contentsLayout = nullptr; + + QLabel *nameLabel = nullptr; + QLabel *descriptionLabel = nullptr; + + OBSPropertiesList *propertyList = nullptr; + + OBSToggleSwitch *toggleSwitch = nullptr; + bool checkable = false; +}; diff --git a/frontend/components/idian/OBSIdianWidget.hpp b/frontend/components/idian/OBSIdianWidget.hpp new file mode 100644 index 000000000..f6e249c1e --- /dev/null +++ b/frontend/components/idian/OBSIdianWidget.hpp @@ -0,0 +1,152 @@ +/****************************************************************************** + Copyright (C) 2024 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 + +/* + * Helpers for OBS Idian widgets + */ + +static const QRegularExpression classRegex = QRegularExpression("^[a-zA-Z][a-zA-Z0-9_-]*$"); + +class OBSIdianUtils { + + static bool classNameIsValid(const QString &name) + { + QRegularExpressionMatch match = classRegex.match(name); + return match.hasMatch(); + } + +public: + QWidget *parent = nullptr; + + OBSIdianUtils(QWidget *w) { parent = w; } + + /* + * Set a custom property whenever the widget has + * keyboard focus specifically + */ + void showKeyFocused(QFocusEvent *e) + { + if (e->reason() != Qt::MouseFocusReason && e->reason() != Qt::PopupFocusReason) { + addClass("keyFocus"); + } else { + removeClass("keyFocus"); + } + } + + void hideKeyFocused(QFocusEvent *e) + { + if (e->reason() != Qt::PopupFocusReason) { + removeClass("keyFocus"); + } + } + + /* + * Force all children widgets to repaint + */ + void polishChildren() { polishChildren(parent); } + + static void polishChildren(QWidget *widget) + { + for (QWidget *child : widget->findChildren()) { + repolish(child); + } + } + + void repolish() { repolish(parent); } + + static void repolish(QWidget *widget) + { + widget->style()->unpolish(widget); + widget->style()->polish(widget); + } + + /* + * Adds a style class to the widget + */ + void addClass(const QString &classname) { addClass(parent, classname); } + + static void addClass(QWidget *widget, const QString &classname) + { + if (!classNameIsValid(classname)) { + return; + } + + QVariant current = widget->property("class"); + + QStringList classList = current.toString().split(" "); + if (classList.contains(classname)) { + return; + } + + classList.removeDuplicates(); + classList.append(classname); + + widget->setProperty("class", classList.join(" ")); + + repolish(widget); + } + + /* + * Removes a style class from a widget + */ + void removeClass(const QString &classname) { removeClass(parent, classname); } + + static void removeClass(QWidget *widget, const QString &classname) + { + if (!classNameIsValid(classname)) { + return; + } + + QVariant current = widget->property("class"); + if (current.isNull()) { + return; + } + + QStringList classList = current.toString().split(" "); + if (!classList.contains(classname, Qt::CaseSensitive)) { + return; + } + + classList.removeDuplicates(); + classList.removeAll(classname); + + widget->setProperty("class", classList.join(" ")); + + repolish(widget); + } + + /* + * Forces the addition or removal of a style class from a widget + */ + void toggleClass(const QString &classname, bool toggle) { toggleClass(parent, classname, toggle); } + + static void toggleClass(QWidget *widget, const QString &classname, bool toggle) + { + if (toggle) { + addClass(widget, classname); + } else { + removeClass(widget, classname); + } + } +}; diff --git a/frontend/components/idian/OBSPropertiesList.cpp b/frontend/components/idian/OBSPropertiesList.cpp new file mode 100644 index 000000000..bd6646407 --- /dev/null +++ b/frontend/components/idian/OBSPropertiesList.cpp @@ -0,0 +1,80 @@ +/****************************************************************************** + 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 "OBSPropertiesList.hpp" +#include "OBSActionRow.hpp" + +OBSPropertiesList::OBSPropertiesList(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); +} + +/* 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 OBSPropertiesList::addRow(OBSActionRow *actionRow) +{ + // Add custom spacer once more than one element exists + if (layout->count() > 0) + layout->addWidget(new OBSPropertiesListSpacer(this)); + + // Custom properties to work around :first and :last not existing. + if (!first) { + OBSIdianUtils::addClass(actionRow, "first"); + first = actionRow; + } + + // Remove last property from existing last item + if (last) + OBSIdianUtils::removeClass(last, "last"); + + // Most recently added item is also always last + OBSIdianUtils::addClass(actionRow, "last"); + last = actionRow; + + actionRow->setParent(this); + rowsList.append(actionRow); + layout->addWidget(actionRow); + adjustSize(); +} + +void OBSPropertiesList::clear() +{ + rowsList.clear(); + first = nullptr; + last = nullptr; + QLayoutItem *item = layout->takeAt(0); + + while (item) { + if (item->widget()) + item->widget()->deleteLater(); + delete item; + + item = layout->takeAt(0); + } + + adjustSize(); +} diff --git a/frontend/components/idian/OBSPropertiesList.hpp b/frontend/components/idian/OBSPropertiesList.hpp new file mode 100644 index 000000000..9623a1832 --- /dev/null +++ b/frontend/components/idian/OBSPropertiesList.hpp @@ -0,0 +1,57 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include + +#include "OBSIdianWidget.hpp" + +class OBSActionRow; + +class OBSPropertiesList : public QFrame { + Q_OBJECT + +public: + OBSPropertiesList(QWidget *parent = nullptr); + + void addRow(OBSActionRow *actionRow); + void clear(); + + QList rows() const { return rowsList; } + +private: + OBSActionRow *first = nullptr; + OBSActionRow *last = nullptr; + + QVBoxLayout *layout; + QList rowsList; +}; + +/** +* Spacer with only cosmetic functionality +*/ +class OBSPropertiesListSpacer : public QFrame { + Q_OBJECT +public: + OBSPropertiesListSpacer(QWidget *parent = nullptr) : QFrame(parent) + { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + } +}; diff --git a/frontend/components/idian/OBSSpinBox.cpp b/frontend/components/idian/OBSSpinBox.cpp new file mode 100644 index 000000000..1defa848e --- /dev/null +++ b/frontend/components/idian/OBSSpinBox.cpp @@ -0,0 +1,42 @@ +/****************************************************************************** + 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 "OBSSpinBox.hpp" + +OBSSpinBox::OBSSpinBox(QWidget *parent) : QFrame(parent) +{ + layout = new QHBoxLayout(); + setLayout(layout); + + layout->setContentsMargins(0, 0, 0, 0); + + decr = new QPushButton("-"); + decr->setObjectName("obsSpinBoxButton"); + layout->addWidget(decr); + + sbox = new QSpinBox(); + sbox->setObjectName("obsSpinBox"); + sbox->setButtonSymbols(QAbstractSpinBox::NoButtons); + layout->addWidget(sbox); + + incr = new QPushButton("+"); + incr->setObjectName("obsSpinBoxButton"); + layout->addWidget(incr); + + connect(decr, &QPushButton::pressed, sbox, &QSpinBox::stepDown); + connect(incr, &QPushButton::pressed, sbox, &QSpinBox::stepUp); +} diff --git a/frontend/components/idian/OBSSpinBox.hpp b/frontend/components/idian/OBSSpinBox.hpp new file mode 100644 index 000000000..c8fe07673 --- /dev/null +++ b/frontend/components/idian/OBSSpinBox.hpp @@ -0,0 +1,38 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +class OBSSpinBox : public QFrame { + Q_OBJECT; + +public: + OBSSpinBox(QWidget *parent = nullptr); + + QSpinBox *spinBox() const { return sbox; } + +private: + QHBoxLayout *layout; + QPushButton *decr; + QPushButton *incr; + QSpinBox *sbox; +}; diff --git a/frontend/components/idian/OBSToggleSwitch.cpp b/frontend/components/idian/OBSToggleSwitch.cpp new file mode 100644 index 000000000..10fbd5fd9 --- /dev/null +++ b/frontend/components/idian/OBSToggleSwitch.cpp @@ -0,0 +1,247 @@ +/****************************************************************************** + 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 "OBSToggleSwitch.hpp" +#include + +#define UNUSED_PARAMETER(param) (void)param + +static QColor blendColors(const QColor &color1, const QColor &color2, float ratio) +{ + int r = color1.red() * (1 - ratio) + color2.red() * ratio; + int g = color1.green() * (1 - ratio) + color2.green() * ratio; + int b = color1.blue() * (1 - ratio) + color2.blue() * ratio; + + return QColor(r, g, b, 255); +} + +OBSToggleSwitch::OBSToggleSwitch(QWidget *parent) + : QAbstractButton(parent), + animHandle(new QPropertyAnimation(this, "xpos", this)), + animBgColor(new QPropertyAnimation(this, "blend", this)), + OBSIdianUtils(this) +{ + offPos = rect().width() / 2 - 18; + onPos = rect().width() / 2 + 18; + xPos = offPos; + margin = 3; + + setCheckable(true); + setAccessibleName("ToggleSwitch"); + + installEventFilter(this); + + connect(this, &OBSToggleSwitch::clicked, this, &OBSToggleSwitch::onClicked); + + connect(animHandle, &QVariantAnimation::valueChanged, this, &OBSToggleSwitch::updateBackgroundColor); + connect(animBgColor, &QVariantAnimation::valueChanged, this, &OBSToggleSwitch::updateBackgroundColor); +} + +OBSToggleSwitch::OBSToggleSwitch(bool defaultState, QWidget *parent) : OBSToggleSwitch(parent) +{ + setChecked(defaultState); + if (defaultState) { + xPos = onPos; + } +} + +void OBSToggleSwitch::animateHandlePosition() +{ + animHandle->setStartValue(xPos); + + int endPos = onPos; + + if ((!isDelayed() && !isChecked()) || (isDelayed() && !pendingStatus)) + endPos = offPos; + + animHandle->setEndValue(endPos); + + animHandle->setDuration(120); + animHandle->start(); +} + +void OBSToggleSwitch::updateBackgroundColor() +{ + QColor offColor = underMouse() ? backgroundInactiveHover : backgroundInactive; + QColor onColor = underMouse() ? backgroundActiveHover : backgroundActive; + + if (!isDelayed()) { + int offset = isChecked() ? 0 : offPos; + blend = (float)(xPos - offset) / (float)(onPos); + } + + QColor bg = blendColors(offColor, onColor, blend); + + if (!isEnabled()) + bg = backgroundInactive; + + setStyleSheet("background: " + bg.name()); +} + +void OBSToggleSwitch::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::EnabledChange) { + OBSIdianUtils::toggleClass("disabled", !isEnabled()); + updateBackgroundColor(); + } +} + +void OBSToggleSwitch::paintEvent(QPaintEvent *e) +{ + UNUSED_PARAMETER(e); + + QStyleOptionButton opt; + opt.initFrom(this); + QPainter p(this); + + bool showChecked = isChecked(); + if (isDelayed()) { + showChecked = pendingStatus; + } + + opt.state.setFlag(QStyle::State_On, showChecked); + opt.state.setFlag(QStyle::State_Off, !showChecked); + + opt.state.setFlag(QStyle::State_Sunken, true); + + style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this); + + p.setRenderHint(QPainter::Antialiasing, true); + + p.setBrush(handleColor); + p.drawEllipse(QRectF(xPos, margin, handleSize, handleSize)); +} + +void OBSToggleSwitch::showEvent(QShowEvent *e) +{ + margin = (rect().height() - handleSize) / 2; + + offPos = margin; + onPos = rect().width() - handleSize - margin; + + xPos = isChecked() ? onPos : offPos; + + updateBackgroundColor(); + style()->polish(this); + + QAbstractButton::showEvent(e); +} + +void OBSToggleSwitch::click() +{ + if (!isDelayed()) + QAbstractButton::click(); + + if (isChecked() == pendingStatus) + setPending(!isChecked()); +} + +void OBSToggleSwitch::onClicked(bool checked) +{ + if (delayed) + return; + + setPending(checked); +} + +void OBSToggleSwitch::setStatus(bool status) +{ + if (status == isChecked() && status == pendingStatus) + return; + + pendingStatus = status; + setChecked(status); + + if (isChecked()) { + animBgColor->setStartValue(0.0f); + animBgColor->setEndValue(1.0f); + } else { + animBgColor->setStartValue(1.0f); + animBgColor->setEndValue(0.0f); + } + + animBgColor->setEasingCurve(QEasingCurve::InOutCubic); + animBgColor->setDuration(240); + animBgColor->start(); +} + +void OBSToggleSwitch::setPending(bool pending) +{ + pendingStatus = pending; + animateHandlePosition(); + + if (!isDelayed()) + return; + + if (pending) { + emit pendingChecked(); + } else { + emit pendingUnchecked(); + } +} + +void OBSToggleSwitch::setDelayed(bool state) +{ + delayed = state; + pendingStatus = isChecked(); +} + +void OBSToggleSwitch::enterEvent(QEnterEvent *e) +{ + setCursor(Qt::PointingHandCursor); + updateBackgroundColor(); + QAbstractButton::enterEvent(e); +} + +void OBSToggleSwitch::leaveEvent(QEvent *e) +{ + updateBackgroundColor(); + QAbstractButton::leaveEvent(e); +} + +void OBSToggleSwitch::keyReleaseEvent(QKeyEvent *e) +{ + if (!isDelayed()) { + QAbstractButton::keyReleaseEvent(e); + return; + } + + if (e->key() != Qt::Key_Space) { + return; + } + + click(); +} + +void OBSToggleSwitch::mouseReleaseEvent(QMouseEvent *e) +{ + if (!isDelayed()) { + QAbstractButton::mouseReleaseEvent(e); + return; + } + + if (e->button() != Qt::LeftButton) { + return; + } + + click(); +} + +QSize OBSToggleSwitch::sizeHint() const +{ + return QSize(2 * handleSize, handleSize); +} diff --git a/frontend/components/idian/OBSToggleSwitch.hpp b/frontend/components/idian/OBSToggleSwitch.hpp new file mode 100644 index 000000000..481815db8 --- /dev/null +++ b/frontend/components/idian/OBSToggleSwitch.hpp @@ -0,0 +1,121 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "OBSIdianWidget.hpp" + +class OBSToggleSwitch : public QAbstractButton, public OBSIdianUtils { + Q_OBJECT + Q_PROPERTY(int xpos MEMBER xPos WRITE setPos) + Q_PROPERTY(QColor background MEMBER backgroundInactive DESIGNABLE true) + Q_PROPERTY(QColor background_hover MEMBER backgroundInactiveHover DESIGNABLE true) + Q_PROPERTY(QColor background_checked MEMBER backgroundActive DESIGNABLE true) + Q_PROPERTY(QColor background_checked_hover MEMBER backgroundActiveHover DESIGNABLE true) + Q_PROPERTY(QColor handleColor MEMBER handleColor DESIGNABLE true) + Q_PROPERTY(int handleSize MEMBER handleSize DESIGNABLE true) + Q_PROPERTY(float blend MEMBER blend WRITE setBlend DESIGNABLE false) + +public: + OBSToggleSwitch(QWidget *parent = nullptr); + OBSToggleSwitch(bool defaultState, QWidget *parent = nullptr); + + QSize sizeHint() const override; + + void setPos(int x) + { + xPos = x; + update(); + } + + void setBlend(float newBlend) + { + blend = newBlend; + update(); + } + + void setDelayed(bool state); + bool isDelayed() { return delayed; } + + void setStatus(bool status); + void setPending(bool pending); + +public slots: + void click(); + +signals: + void pendingChecked(); + void pendingUnchecked(); + +protected: + void changeEvent(QEvent *event) override; + void paintEvent(QPaintEvent *) override; + void showEvent(QShowEvent *) override; + void enterEvent(QEnterEvent *) override; + void leaveEvent(QEvent *) override; + void keyReleaseEvent(QKeyEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; + + void focusInEvent(QFocusEvent *e) override + { + OBSIdianUtils::showKeyFocused(e); + QAbstractButton::focusInEvent(e); + } + + void focusOutEvent(QFocusEvent *e) override + { + OBSIdianUtils::hideKeyFocused(e); + QAbstractButton::focusOutEvent(e); + } + +private slots: + void onClicked(bool checked); + +private: + int xPos; + int onPos; + int offPos; + int margin; + + float blend = 0.0f; + + bool delayed = false; + bool pendingStatus = false; + + void animateHandlePosition(); + + void updateBackgroundColor(); + QColor backgroundInactive; + QColor backgroundInactiveHover; + QColor backgroundActive; + QColor backgroundActiveHover; + QColor handleColor; + int handleSize = 18; + + QPropertyAnimation *animHandle = nullptr; + QPropertyAnimation *animBgColor = nullptr; +}; diff --git a/frontend/components/idian/obs-widgets.hpp b/frontend/components/idian/obs-widgets.hpp new file mode 100644 index 000000000..61bc13432 --- /dev/null +++ b/frontend/components/idian/obs-widgets.hpp @@ -0,0 +1,37 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +// Idian - A family of custom widgets for OBS implementing the "Yami" UI design. +// +// (OBS Idian, get it?) + +#include "OBSActionRow.hpp" +#include "OBSCheckBox.hpp" +#include "OBSComboBox.hpp" +#include "OBSDoubleSpinBox.hpp" +#include "OBSGroupBox.hpp" +#include "OBSPropertiesList.hpp" +#include "OBSSpinBox.hpp" +#include "OBSToggleSwitch.hpp" + +/// Note: This file serves as an all-in-one include for custom OBS widgets. +/// It is not intended to define any widgets by itself. + +/// Note 2: These widgets are still heavily work in progress. They should not +/// yet be used outside of the demo and scene collection dialogues. diff --git a/frontend/data/themes/System.obt b/frontend/data/themes/System.obt index 0a7f8636f..b019aed67 100644 --- a/frontend/data/themes/System.obt +++ b/frontend/data/themes/System.obt @@ -379,3 +379,108 @@ QCalendarWidget #qt_calendar_nextmonth { StatusBarWidget > QFrame { padding: 0px 12px 8px; } + +OBSToggleSwitch { + qproperty-handle: rgb(255, 255, 255); + qproperty-backgroundActive: #284cb8; + qproperty-backgroundInactive: #3c404b; +} + +OBSGroupBox { + border-radius: 4px; + font-weight: bold; +} + +OBSGroupBox > QLabel.title { + font-weight: bold; +} + +OBSGroupBox > QLabel.subtitle { + color: palette(button-text); +} + +OBSPropertiesList { + background: palette(base); + border-radius: 4px; + border-width: 0px; + padding: 0px; + margin: 0px; +} + +OBSActionRow { + margin: 0; + padding: 0; + border-left: 3px solid transparent; + min-height: 48px; + background: transparent; +} + +OBSActionRow:hover { + background: palette(highlight); + border-left: 0; +} + +OBSActionRow[last="true"]:hover { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +OBSActionRow[first="true"]:hover { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +OBSActionRow > QLabel { + font-weight: 500; +} + +OBSActionRow > QLabel.subtitle { + font-size: 9pt; + color: palette(button-text); +} + +OBSActionRow QComboBox, +OBSActionRow QPushButton { + margin: 0; +} + +OBSActionRow QComboBox, +OBSActionRow QComboBox:hover, +OBSActionRow QComboBox:selected, +OBSActionRow QComboBox::on { + background: transparent; +} + +OBSActionRow QComboBox::drop-down { + border: none; +} + +OBSActionRow QComboBox::down-arrow { + image: url(./Light/collapse.svg); +} + +OBSPropertiesListSpacer { + max-height: 1px; + min-height: 1px; + background-color: palette(midlight); +} + +OBSCollapsibleActionRow { + margin: 0; + padding: 0; + border: none; +} + +OBSCollapsibleActionRow OBSPropertiesList { + border-radius: 0; + background: palette(mid); +} + +IdianPlayground QVBoxLayout { + background: palette(midlight); + border-radius: 4px; + padding-top: 32px; + padding-bottom: 8px; + font-weight: bold; + margin-bottom: 6px; +} diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index 6cd597b1c..95924d82c 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -97,6 +97,9 @@ --padding_base_value: var(--obsPadding); --spacing_base_value: calc(2 + calc(var(--obsPadding) / 2)); + --highlight_width: 1px; + --highlight_color: var(--primary_lighter); + /* TODO: Better Accessibility focus state */ /* TODO: Move Accessibilty Colors to Theme config system */ --border_highlight: "transparent"; @@ -172,6 +175,7 @@ --list_item_bg_hover: var(--primary_light); --input_border: var(--grey1); + --input_border_width: 1px; --input_border_hover: var(--grey1); --input_border_focus: var(--primary); @@ -182,6 +186,7 @@ --button_bg_down: var(--grey7); --button_bg_disabled: var(--grey6); + --button_border_width: var(--input_border_width); --button_border: var(--button_bg); --button_border_hover: var(--grey1); --button_border_focus: var(--grey1); @@ -193,7 +198,7 @@ --tab_border: var(--border_color); --tab_border_hover: var(--button_border_hover); - --tab_border_focus: var(--button_border_focus); + --tab_border_focus: var(--primary_lighter); --tab_border_selected: var(--primary); --tab_padding_base: calc(5px + var(--padding_base)); @@ -203,8 +208,22 @@ --separator_hover: var(--white1); - --highlight: rgb(42, 130, 218); - --highlight_inactive: rgb(25, 28, 34); + --action_row_base: calc(var(--input_height_base) * 0.75); + --action_row_height: calc(var(--action_row_base) + calc(var(--action_row_padding) * 2)); + --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); + + --toggle_border: 1; + --toggle_margin: 3; + --toggle_width: calc(var(--action_row_base) * 1.8); + --toggle_height: calc(var(--action_row_base) * 0.9); + --toggle_handle: calc(calc(calc(var(--toggle_height) * 0.9) - calc(var(--toggle_border) * 2)) - var(--toggle_margin)); + --toggle_radius: calc(var(--toggle_height) / 2); /* Qt Palette variables can be set with the "palette_" prefix */ --palette_window: var(--bg_window); @@ -538,6 +557,7 @@ QListView, QListWidget, QMenu { padding: var(--spacing_base); + outline: none; } QMenu { @@ -588,6 +608,7 @@ QMenu::item:selected, QListView::item:selected, QListWidget::item:selected { background-color: var(--primary); + border-color: var(--primary); } QMenu::item:hover, @@ -597,7 +618,7 @@ QMenu::item:selected:hover, QListView::item:selected:hover, QListWidget::item:selected:hover { background-color: var(--primary_light); - color: var(--text); + border-color: var(--highlight_color); } QMenu::item:focus, @@ -622,7 +643,7 @@ QListWidget QLineEdit { padding: 0; padding-bottom: 1px; margin: 0; - border: 1px solid var(--white1); + border: var(--input_border_width) solid var(--white1); border-radius: var(--border_radius); } @@ -937,6 +958,10 @@ QTabWidget::pane { border-top: 4px solid var(--tab_bg); } +QTabBar { + outline: none; +} + QTabWidget::tab-bar { alignment: left; } @@ -1124,7 +1149,7 @@ QTextEdit:!editable:focus { QSpinBox, QDoubleSpinBox { background-color: var(--input_bg); - border: 1px solid var(--input_bg); + border: var(--input_border_width) solid var(--input_bg); border-radius: var(--border_radius); padding: var(--input_padding) var(--input_text_padding); height: var(--input_height); @@ -1446,7 +1471,7 @@ QSlider::handle:disabled { background-color: var(--button_bg); padding: var(--padding_base_border) var(--padding_base_border); margin: 0px; - border: 1px solid var(--button_border); + border: var(--highlight_width) solid var(--button_border); border-radius: var(--border_radius); icon-size: var(--icon_base); } @@ -1702,8 +1727,8 @@ QTableView::indicator:unchecked:disabled { max-height: var(--icon_base); padding: var(--padding_base); margin-right: var(--spacing_large); - border: 1px solid transparent; - border-radius: 4px; + border: var(--highlight_width) solid transparent; + border-radius: var(--border_radius); } .checkbox-icon::indicator { @@ -1753,7 +1778,7 @@ QTableView::indicator:unchecked:disabled { background-color: var(--button_bg); padding: var(--padding_base_border) var(--padding_base_border); margin: 0px; - border: 1px solid var(--button_border); + border: var(--highlight_width) solid var(--button_border); border-radius: var(--border_radius); icon-size: var(--icon_base); } @@ -1763,7 +1788,7 @@ QTableView::indicator:unchecked:disabled { background-color: var(--button_bg_hover); padding: var(--padding_base_border) var(--padding_base_border); margin: 0px; - border: 1px solid var(--button_border_hover); + border: var(--highlight_width) solid var(--button_border_hover); icon-size: var(--icon_base); } @@ -2153,3 +2178,282 @@ OBSBasicAdvAudio #scrollAreaWidgetContents { #previewZoomOutButton:focus { border: 1px solid var(--input_border_hover); } + +/* Idian Widgets */ +OBSGroupBox { + border-radius: var(--border_radius); + font-weight: bold; + margin: 0 0 var(--spacing_base); + min-width: 300px; + max-width: 600px; +} + +OBSGroupBox .header .title { + font-weight: bold; + padding: var(--padding_large) 0; +} + +OBSGroupBox .header .description { + color: var(--text_muted); + padding: var(--spacing_small) 0; +} + +OBSPropertiesList { + border-width: 0; + padding: 0; + margin: var(--spacing_base) 0; +} + +OBSActionRowWidget { + background: var(--grey5); + margin: 0; + padding: var(--action_row_padding) var(--action_row_padding_x); +} + +OBSActionRowWidget.keyFocus { + background: var(--grey4); + border: var(--highlight_width) solid var(--grey4); +} + +OBSActionRowWidget.cursor-pointer.hover { + background: var(--grey4); + border: var(--highlight_width) solid var(--grey1); +} + +OBSActionRowWidget.first { + border-top-left-radius: var(--border_radius); + border-top-right-radius: var(--border_radius); +} + +OBSActionRowWidget.last { + border-bottom-left-radius: var(--border_radius); + border-bottom-right-radius: var(--border_radius); +} + +OBSActionRowWidget > QWidget QLabel { + background: red; + margin: 4px; + font-weight: 500; + max-height: var(--input_height); +} + +OBSActionRowWidget > QLabel.description { + font-size: var(--font_small); + color: var(--text_muted); +} + +OBSToggleSwitch { + qproperty-background: var(--grey6); + qproperty-background_hover: var(--grey7); + qproperty-background_checked: var(--primary); + qproperty-background_checked_hover: var(--primary_light); + + min-width: var(--toggle_width); + min-height: var(--toggle_height); + + border-radius: var(--toggle_radius); + + qproperty-handleColor: var(--white1); + qproperty-handleSize: var(--toggle_handle); + + border: var(--highlight_width) solid transparent; +} + +OBSToggleSwitch:hover { + border-color: var(--grey4); +} + +OBSToggleSwitch:checked:hover { + border-color: var(--white1); +} + +OBSToggleSwitch.keyFocus { + border-color: var(--highlight_color); +} + +OBSActionRowWidget OBSToggleSwitch:hover, +OBSActionRowWidget.hover > OBSToggleSwitch.row-buddy { + border-color: var(--grey1); +} + +OBSActionRowWidget OBSToggleSwitch:checked:hover, +OBSActionRowWidget.hover OBSToggleSwitch.row-buddy:checked { + border-color: var(--white1); +} + +OBSActionRowWidget QComboBox { + background-color: transparent; + min-height: var(--action_row_base); + max-height: var(--action_row_base); + min-width: var(--action_row_input_width); + border: var(--highlight_width) solid transparent; + padding: 0; + padding-left: var(--padding_xlarge); + margin: 0; +} + +OBSActionRowWidget QComboBox:focus { + border-color: transparent; +} + +OBSActionRowWidget QComboBox:hover { + border-color: var(--grey1); +} + +OBSActionRowWidget QComboBox.keyFocus { + border-color: var(--highlight_color); +} + +OBSActionRowWidget QComboBox::drop-down { + border: none; +} + +OBSActionRowWidget QComboBox::down-arrow { + image: url(theme:Dark/collapse.svg); +} + +OBSActionRowWidget QComboBox QAbstractItemView { + outline: none; +} + +OBSActionRowWidget QComboBox QAbstractItemView::item { + background-color: var(--bg_base); + padding: var(--padding_base) var(--padding_large); +} + +OBSActionRowWidget QComboBox QAbstractItemView::item:hover, +OBSActionRowWidget QComboBox QAbstractItemView::item:selected { + background-color: var(--list_item_bg_selected); + padding: var(--padding_base) var(--padding_large); +} + +OBSActionRowWidget QPushButton, +OBSActionRowWidget QSpinBox, +OBSActionRowWidget QDoubleSpinBox { + margin: 0; + padding: var(--padding_base) var(--action_row_padding_x); +} + +OBSPropertiesListSpacer { + max-height: var(--spacing_small); + min-height: var(--spacing_small); + background-color: var(--bg_window); +} + +OBSActionRowWidget OBSCheckBox { + outline: none; +} + +OBSActionRowWidget OBSCheckBox::indicator, +OBSActionRowWidget OBSCheckBox::indicator:unchecked:hover { + border: var(--highlight_width) solid transparent; + border-radius: var(--border_radius); +} + +OBSActionRowWidget.hover > OBSCheckBox.row-buddy::indicator, +OBSActionRowWidget > OBSCheckBox::indicator:unchecked:hover, +OBSActionRowWidget > OBSCheckBox::indicator:hover { + border-color: var(--grey1); +} + +OBSActionRowWidget.hover > OBSCheckBox.row-buddy::indicator:unchecked, +OBSCheckBox.keyFocus::indicator:unchecked { + image: url(theme:Yami/checkbox_unchecked_focus.svg); +} + +OBSActionRowWidget OBSCheckBox.keyFocus::indicator, +OBSActionRowWidget.hover > OBSCheckBox::indicator { + image: url(theme:Yami/checkbox_checked_focus.svg); +} + +OBSActionRowWidget OBSCheckBox.keyFocus::indicator, +OBSActionRowWidget OBSCheckBox.keyFocus::indicator:unchecked, +OBSActionRowWidget OBSCheckBox.keyFocus::indicator:hover, +OBSActionRowWidget OBSCheckBox.keyFocus::indicator:unchecked:hover { + border-color: var(--highlight_color); +} + +OBSCollapsibleRowWidget { + margin: 0; + padding: 0; + border: none; +} + +OBSCollapsibleRowWidget.keyFocus { + border: var(--highlight_width) solid var(--highlight_color); +} + +OBSCollapsibleRowWidget OBSPropertiesList { + border-radius: 0; + border-left: 1px solid var(--grey5); + border-right: 1px solid var(--grey5); + border-bottom: 1px solid var(--grey5); + margin: var(--spacing_small) 0px 0px; +} + +OBSCollapsibleRowWidget OBSPropertiesList OBSActionRowWidget { + background-color: var(--grey6); + padding-left: var(--action_row_padding_nested); +} + +OBSCollapsibleRowWidget OBSActionRowWidget.first, +OBSCollapsibleRowWidget OBSActionRowWidget.last { + border-radius: 0; +} + +OBSCollapsibleRowWidget OBSPropertiesList OBSToggleSwitch { + qproperty-background: var(--grey7); + qproperty-background_hover: var(--grey6); +} + +OBSActionRowExpandButton { + background: transparent; + min-width: var(--action_row_collapse); + max-width: var(--action_row_collapse); + min-height: var(--action_row_collapse); + max-height: var(--action_row_collapse); + border: none; +} + +OBSActionRowExpandButton::indicator { + background: var(--grey5); + border-radius: var(--action_row_collapse_radius); + padding: var(--padding_large); + image: url(theme:Dark/down.svg); + border: var(--highlight_width) solid var(--grey5); +} + +OBSActionRowExpandButton::indicator:checked { + image: url(theme:Dark/up.svg); +} + +OBSActionRowExpandButton.keyFocus, +OBSActionRowExpandButton.keyFocus::indicator { + border-color: var(--highlight_color); +} + +OBSCollapsibleRowFrame .btn-frame { + background: var(--grey5); + padding: var(--action_row_padding) var(--action_row_padding_x); +} + +OBSCollapsibleRowFrame.hover .btn-frame { + background: var(--grey4); +} + +OBSCollapsibleRowFrame.hover OBSActionRowWidget, +OBSCollapsibleRowFrame.hover OBSActionRowWidget.hover { + background: var(--grey4); + border: 2px solid var(--grey1); + border-right: none; +} + +OBSCollapsibleRowFrame.hover .row-buddy { + background: var(--grey4); + border: 2px solid var(--grey1); + border-left: none; +} + +OBSCollapsibleRowFrame.hover OBSActionRowExpandButton::indicator { + border-color: var(--grey1); +} diff --git a/frontend/dialogs/OBSIdianPlayground.cpp b/frontend/dialogs/OBSIdianPlayground.cpp new file mode 100644 index 000000000..05228062a --- /dev/null +++ b/frontend/dialogs/OBSIdianPlayground.cpp @@ -0,0 +1,132 @@ +/****************************************************************************** + 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 "OBSIdianPlayground.hpp" + +#include "components/idian/obs-widgets.hpp" + +#include + +OBSIdianPlayground::OBSIdianPlayground(QWidget *parent) : QDialog(parent), ui(new Ui_OBSIdianPlayground) +{ + ui->setupUi(this); + + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); + + OBSGroupBox *test; + OBSActionRowWidget *tmp; + + OBSComboBox *cbox = new OBSComboBox; + cbox->addItem("Test 1"); + cbox->addItem("Test 2"); + + // Group box 1 + test = new OBSGroupBox(this); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Row with a dropdown"); + tmp->setSuffix(cbox); + test->properties()->addRow(tmp); + + cbox = new OBSComboBox; + cbox->addItem("Test 3"); + cbox->addItem("Test 4"); + tmp = new OBSActionRowWidget(); + tmp->setTitle("Row with a dropdown"); + tmp->setDescription("And a subtitle!"); + tmp->setSuffix(cbox); + test->properties()->addRow(tmp); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Toggle Switch"); + tmp->setSuffix(new OBSToggleSwitch()); + test->properties()->addRow(tmp); + ui->scrollAreaWidgetContents->layout()->addWidget(test); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Delayed toggle switch"); + tmp->setDescription("The state can be set separately"); + auto tswitch = new OBSToggleSwitch; + tswitch->setDelayed(true); + connect(tswitch, &OBSToggleSwitch::pendingChecked, this, [=]() { + // Do async enable stuff, then set toggle status when complete + QTimer::singleShot(1000, [=]() { tswitch->setStatus(true); }); + }); + connect(tswitch, &OBSToggleSwitch::pendingUnchecked, this, [=]() { + // Do async disable stuff, then set toggle status when complete + QTimer::singleShot(1000, [=]() { tswitch->setStatus(false); }); + }); + tmp->setSuffix(tswitch); + test->properties()->addRow(tmp); + + // Group box 2 + test = new OBSGroupBox(); + test->setTitle("Just a few checkboxes"); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Box 1"); + tmp->setPrefix(new OBSCheckBox); + test->properties()->addRow(tmp); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Box 2"); + tmp->setPrefix(new OBSCheckBox); + test->properties()->addRow(tmp); + + ui->scrollAreaWidgetContents->layout()->addWidget(test); + + // Group box 2 + test = new OBSGroupBox(); + test->setTitle("Another Group"); + test->setDescription("With a subtitle"); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Placeholder"); + tmp->setSuffix(new OBSToggleSwitch); + test->properties()->addRow(tmp); + + OBSCollapsibleRowWidget *tmp2 = new OBSCollapsibleRowWidget("A Collapsible row!", this); + tmp2->setCheckable(true); + test->addRow(tmp2); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Spin box demo"); + tmp->setSuffix(new OBSDoubleSpinBox()); + tmp2->addRow(tmp); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Just another placeholder"); + tmp->setSuffix(new OBSToggleSwitch(true)); + tmp2->addRow(tmp); + + tmp = new OBSActionRowWidget(); + tmp->setTitle("Placeholder 2"); + tmp->setSuffix(new OBSToggleSwitch); + test->properties()->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()->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + + // Test Checkable Group + OBSGroupBox *test2 = new OBSGroupBox(); + test2->setTitle("Checkable Group"); + test2->setDescription("Description goes here"); + test2->setCheckable(true); + ui->scrollAreaWidgetContents->layout()->addWidget(test2); +} diff --git a/frontend/dialogs/OBSIdianPlayground.hpp b/frontend/dialogs/OBSIdianPlayground.hpp new file mode 100644 index 000000000..da69ff28d --- /dev/null +++ b/frontend/dialogs/OBSIdianPlayground.hpp @@ -0,0 +1,36 @@ +/****************************************************************************** + 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 . +******************************************************************************/ + +#pragma once + +#include "components/idian/obs-widgets.hpp" + +#include + +#include + +#include + +// QDialog including a bunch of custom widgets for demoing +class OBSIdianPlayground : public QDialog { + Q_OBJECT + + std::unique_ptr ui; + +public: + OBSIdianPlayground(QWidget *parent); +}; diff --git a/frontend/forms/OBSBasic.ui b/frontend/forms/OBSBasic.ui index ad8482986..652b559b7 100644 --- a/frontend/forms/OBSBasic.ui +++ b/frontend/forms/OBSBasic.ui @@ -937,6 +937,7 @@ Basic.MainMenu.Tools + @@ -2165,6 +2166,11 @@ PasteDuplicate + + + Widget Playground + + Basic.AutoConfig diff --git a/frontend/forms/OBSIdianPlayground.ui b/frontend/forms/OBSIdianPlayground.ui new file mode 100644 index 000000000..7f282b1e1 --- /dev/null +++ b/frontend/forms/OBSIdianPlayground.ui @@ -0,0 +1,55 @@ + + + OBSIdianPlayground + + + + 0 + 0 + 700 + 700 + + + + Idian Playground + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 786 + 919 + + + + + 0 + + + 0 + + + 0 + + + 9 + + + + + + + + + + diff --git a/frontend/forms/images/hide.svg b/frontend/forms/images/hide.svg new file mode 100644 index 000000000..9bdca9a2c --- /dev/null +++ b/frontend/forms/images/hide.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/forms/obs.qrc b/frontend/forms/obs.qrc index ee649e56a..2faceae68 100644 --- a/frontend/forms/obs.qrc +++ b/frontend/forms/obs.qrc @@ -30,6 +30,7 @@ images/visible.svg images/help.svg images/help_light.svg + images/hide.svg images/revert.svg images/alert.svg images/warning.svg diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index 8d1fad371..14ebc6624 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -422,6 +422,10 @@ OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); #endif +#ifndef ENABLE_WIDGET_PLAYGROUND + ui->widgetPlayground->setVisible(false); +#endif + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { QAction *nudge = new QAction(ui->preview); nudge->setShortcut(seq); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 28210980e..4ef7ebbc2 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -630,6 +630,7 @@ private slots: void on_autoConfigure_triggered(); void on_stats_triggered(); + void on_widgetPlayground_triggered(); void on_resetUI_triggered(); diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp index b0418a842..4e584481d 100644 --- a/frontend/widgets/OBSBasic_MainControls.cpp +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -42,6 +42,10 @@ #endif #include +#ifdef ENABLE_WIDGET_PLAYGROUND +#include "dialogs/OBSIdianPlayground.hpp" +#endif + #include #include @@ -635,6 +639,16 @@ void OBSBasic::on_stats_triggered() stats = statsDlg; } +void OBSBasic::on_widgetPlayground_triggered() +{ +#ifdef ENABLE_WIDGET_PLAYGROUND + OBSIdianPlayground playground(this); + playground.setModal(true); + playground.show(); + playground.exec(); +#endif +} + void OBSBasic::on_actionShowAbout_triggered() { if (about)