diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index 34f4c0e7fe3a1d..03296d1ec8d519 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -52,6 +52,8 @@ target_sources( components/MenuButton.hpp components/MenuCheckBox.cpp components/MenuCheckBox.hpp + components/MenuRadioButton.cpp + components/MenuRadioButton.hpp components/Multiview.cpp components/Multiview.hpp components/MuteCheckBox.hpp diff --git a/frontend/components/MenuCheckBox.cpp b/frontend/components/MenuCheckBox.cpp index ceb6adb37a1419..a7336496552f1c 100644 --- a/frontend/components/MenuCheckBox.cpp +++ b/frontend/components/MenuCheckBox.cpp @@ -28,24 +28,25 @@ MenuCheckBox::MenuCheckBox(const QString &text, QWidget *parent) : QCheckBox(tex setContentsMargins(0, 0, 0, 0); if (auto menu = qobject_cast(parent)) { - connect(menu, &QMenu::hovered, this, [this, menu](QAction *action) { - if (action != menuAction) { - setHovered(false); - update(); - } + connect(menu, &QMenu::aboutToShow, this, [this]() { + setHovered(false); + update(); + }); + connect(menu, &QMenu::aboutToHide, this, [this]() { + setHovered(false); + update(); }); - connect(menu, &QMenu::aboutToHide, this, [this]() { setHovered(false); }); } } -void MenuCheckBox::setAction(QAction *action) +void MenuCheckBox::focusInEvent(QFocusEvent *) { - menuAction = action; + setHovered(true); } -void MenuCheckBox::focusInEvent(QFocusEvent *) +void MenuCheckBox::focusOutEvent(QFocusEvent *) { - setHovered(true); + setHovered(false); } void MenuCheckBox::mousePressEvent(QMouseEvent *event) diff --git a/frontend/components/MenuCheckBox.hpp b/frontend/components/MenuCheckBox.hpp index 550dd654edd443..0c2dd65a3e1960 100644 --- a/frontend/components/MenuCheckBox.hpp +++ b/frontend/components/MenuCheckBox.hpp @@ -30,10 +30,9 @@ class MenuCheckBox : public QCheckBox { public: explicit MenuCheckBox(const QString &text, QWidget *parent = nullptr); - void setAction(QAction *action); - protected: void focusInEvent(QFocusEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; @@ -42,7 +41,6 @@ class MenuCheckBox : public QCheckBox { void paintEvent(QPaintEvent *event) override; private: - QPointer menuAction = nullptr; bool mousePressInside = false; bool isHovered = false; diff --git a/frontend/components/MenuRadioButton.cpp b/frontend/components/MenuRadioButton.cpp new file mode 100644 index 00000000000000..a9e1ee478986f4 --- /dev/null +++ b/frontend/components/MenuRadioButton.cpp @@ -0,0 +1,113 @@ +/****************************************************************************** + 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 "MenuRadioButton.hpp" + +#include +#include +#include +#include + +MenuRadioButton::MenuRadioButton(const QString &text, QWidget *parent) : QRadioButton(text, parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + setContentsMargins(0, 0, 0, 0); + + if (auto menu = qobject_cast(parent)) { + connect(menu, &QMenu::aboutToShow, this, [this]() { + setHovered(false); + update(); + }); + connect(menu, &QMenu::aboutToHide, this, [this]() { + setHovered(false); + update(); + }); + } +} + +void MenuRadioButton::focusInEvent(QFocusEvent *) +{ + setHovered(true); +} + +void MenuRadioButton::focusOutEvent(QFocusEvent *) +{ + setHovered(false); +} + +void MenuRadioButton::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + mousePressInside = true; + event->accept(); + } else { + QRadioButton::mousePressEvent(event); + } +} + +void MenuRadioButton::mouseMoveEvent(QMouseEvent *event) +{ + if (!rect().contains(event->pos())) { + mousePressInside = false; + } + event->accept(); +} + +void MenuRadioButton::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (mousePressInside && rect().contains(event->pos())) { + toggle(); + } + event->accept(); + } else { + QRadioButton::mouseReleaseEvent(event); + } + + mousePressInside = false; +} + +void MenuRadioButton::enterEvent(QEnterEvent *) +{ + setHovered(true); + update(); +} + +void MenuRadioButton::leaveEvent(QEvent *) +{ + setHovered(false); + update(); +} + +void MenuRadioButton::paintEvent(QPaintEvent *) +{ + QStylePainter p(this); + QStyleOptionButton opt; + initStyleOption(&opt); + + if (isHovered) { + opt.state |= QStyle::State_MouseOver; + opt.state |= QStyle::State_Selected; + } + + p.drawControl(QStyle::CE_RadioButton, opt); +} + +void MenuRadioButton::setHovered(bool hovered) +{ + isHovered = hovered; +} diff --git a/frontend/components/MenuRadioButton.hpp b/frontend/components/MenuRadioButton.hpp new file mode 100644 index 00000000000000..6e9288f1de59b3 --- /dev/null +++ b/frontend/components/MenuRadioButton.hpp @@ -0,0 +1,48 @@ +/****************************************************************************** + 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 QStyleOptionButton; +class QStylePainter; +class QMouseEvent; + +class MenuRadioButton : public QRadioButton { + Q_OBJECT + +public: + explicit MenuRadioButton(const QString &text, QWidget *parent = nullptr); + +protected: + void focusInEvent(QFocusEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void paintEvent(QPaintEvent *event) override; + + bool isHovered = false; + void setHovered(bool hovered); + +private: + bool mousePressInside = false; +}; diff --git a/frontend/data/themes/Light/radio_checked.svg b/frontend/data/themes/Light/radio_checked.svg new file mode 100644 index 00000000000000..bee3c9d5ec6c20 --- /dev/null +++ b/frontend/data/themes/Light/radio_checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Light/radio_checked_disabled.svg b/frontend/data/themes/Light/radio_checked_disabled.svg new file mode 100644 index 00000000000000..756779b1e8ddef --- /dev/null +++ b/frontend/data/themes/Light/radio_checked_disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Light/radio_checked_focus.svg b/frontend/data/themes/Light/radio_checked_focus.svg new file mode 100644 index 00000000000000..b877cf261116ab --- /dev/null +++ b/frontend/data/themes/Light/radio_checked_focus.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Light/radio_unchecked.svg b/frontend/data/themes/Light/radio_unchecked.svg new file mode 100644 index 00000000000000..d18cda7ae31333 --- /dev/null +++ b/frontend/data/themes/Light/radio_unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Light/radio_unchecked_disabled.svg b/frontend/data/themes/Light/radio_unchecked_disabled.svg new file mode 100644 index 00000000000000..2cd1d31f0b1c64 --- /dev/null +++ b/frontend/data/themes/Light/radio_unchecked_disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Light/radio_unchecked_focus.svg b/frontend/data/themes/Light/radio_unchecked_focus.svg new file mode 100644 index 00000000000000..17dd66ca016d6b --- /dev/null +++ b/frontend/data/themes/Light/radio_unchecked_focus.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index bbdf7642912661..40f3f24966faf6 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -695,7 +695,6 @@ SourceTreeItem { QMenu::item, QMenu > QWidget { padding: var(--padding_large) var(--padding_menu); - padding-right: 20px; } QMenu > QWidget { @@ -755,6 +754,7 @@ QListWidget::item:hover { } QMenu::item:focus, +QMenu > QWidget:focus, QListView::item:focus, QListWidget::item:focus, QMenu::item:selected:focus, @@ -793,6 +793,10 @@ QListWidget QLineEdit:focus { border: 1px solid var(--grey1); } +QMenu MenuRadioButton { + padding-left: var(--padding_large); +} + /* Settings QList */ OBSBasicSettings QScrollBar:vertical { @@ -1418,9 +1422,7 @@ QDoubleSpinBox::down-arrow { color: var(--text); } - /* Buttons */ - QPushButton { background-color: var(--button_bg); color: var(--text); @@ -1532,7 +1534,6 @@ QToolButton:disabled, } /* Sliders */ - QSlider::groove { background-color: var(--grey4); border: none; @@ -1999,6 +2000,7 @@ OBSBasicSettings { QCheckBox::indicator, QGroupBox::indicator, +QRadioButton::indicator, QTableView::indicator { width: var(--icon_base); height: var(--icon_base); @@ -2045,6 +2047,30 @@ QTableView::indicator:unchecked:disabled { image: url(theme:Yami/checkbox_unchecked_disabled.svg); } +QRadioButton::indicator:checked { + image: url(theme:Yami/radio_checked.svg); +} + +QRadioButton::indicator:unchecked { + image: url(theme:Yami/radio_unchecked.svg); +} + +QRadioButton::indicator:checked:hover { + image: url(theme:Yami/radio_checked_focus.svg); +} + +QRadioButton::indicator:unchecked:hover { + image: url(theme:Yami/radio_unchecked_focus.svg); +} + +QRadioButton::indicator:checked:disabled { + image: url(theme:Yami/radio_checked_disabled.svg); +} + +QRadioButton::indicator:unchecked:disabled { + image: url(theme:Yami/radio_unchecked_disabled.svg); +} + /* Icon Checkboxes */ .checkbox-icon { outline: none; diff --git a/frontend/data/themes/Yami/radio_checked.svg b/frontend/data/themes/Yami/radio_checked.svg new file mode 100644 index 00000000000000..b34311a030028a --- /dev/null +++ b/frontend/data/themes/Yami/radio_checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Yami/radio_checked_disabled.svg b/frontend/data/themes/Yami/radio_checked_disabled.svg new file mode 100644 index 00000000000000..3513fd4b52b37c --- /dev/null +++ b/frontend/data/themes/Yami/radio_checked_disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Yami/radio_checked_focus.svg b/frontend/data/themes/Yami/radio_checked_focus.svg new file mode 100644 index 00000000000000..c27a0143a21d70 --- /dev/null +++ b/frontend/data/themes/Yami/radio_checked_focus.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Yami/radio_unchecked.svg b/frontend/data/themes/Yami/radio_unchecked.svg new file mode 100644 index 00000000000000..2cd1d31f0b1c64 --- /dev/null +++ b/frontend/data/themes/Yami/radio_unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Yami/radio_unchecked_disabled.svg b/frontend/data/themes/Yami/radio_unchecked_disabled.svg new file mode 100644 index 00000000000000..8056702828ad60 --- /dev/null +++ b/frontend/data/themes/Yami/radio_unchecked_disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/data/themes/Yami/radio_unchecked_focus.svg b/frontend/data/themes/Yami/radio_unchecked_focus.svg new file mode 100644 index 00000000000000..5fcb6ed05e78df --- /dev/null +++ b/frontend/data/themes/Yami/radio_unchecked_focus.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/frontend/data/themes/Yami_Light.ovt b/frontend/data/themes/Yami_Light.ovt index 82ff22fccd16dc..89a1a87985c125 100644 --- a/frontend/data/themes/Yami_Light.ovt +++ b/frontend/data/themes/Yami_Light.ovt @@ -289,6 +289,30 @@ QTableView::indicator:unchecked:disabled { image: url(theme:Light/checkbox_unchecked_disabled.svg); } +QRadioButton::indicator:checked { + image: url(theme:Light/radio_checked.svg); +} + +QRadioButton::indicator:unchecked { + image: url(theme:Light/radio_unchecked.svg); +} + +QRadioButton::indicator:checked:hover { + image: url(theme:Light/radio_checked_focus.svg); +} + +QRadioButton::indicator:unchecked:hover { + image: url(theme:Light/radio_unchecked_focus.svg); +} + +QRadioButton::indicator:checked:disabled { + image: url(theme:Light/radio_checked_disabled.svg); +} + +QRadioButton::indicator:unchecked:disabled { + image: url(theme:Light/radio_unchecked_disabled.svg); +} + .indicator-lock::indicator:checked, .indicator-lock::indicator:checked:hover { image: url(theme:Light/locked.svg); diff --git a/frontend/widgets/AudioMixer.cpp b/frontend/widgets/AudioMixer.cpp index 67906bda8bd5de..ae66fb70966e0a 100644 --- a/frontend/widgets/AudioMixer.cpp +++ b/frontend/widgets/AudioMixer.cpp @@ -804,13 +804,11 @@ void AudioMixer::createMixerContextMenu() showHiddenCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowHidden"), mixerMenu); QWidgetAction *showHiddenAction = new QWidgetAction(mixerMenu); - showHiddenCheckBox->setAction(showHiddenAction); showHiddenCheckBox->setChecked(showHidden); showHiddenAction->setDefaultWidget(showHiddenCheckBox); QWidgetAction *showInactiveAction = new QWidgetAction(mixerMenu); MenuCheckBox *showInactiveCheckBox = new MenuCheckBox(QTStr("Basic.AudioMixer.ShowInactive"), mixerMenu); - showInactiveCheckBox->setAction(showInactiveAction); showInactiveCheckBox->setChecked(showInactive); showInactiveAction->setDefaultWidget(showInactiveCheckBox); @@ -818,7 +816,6 @@ void AudioMixer::createMixerContextMenu() const char *hiddenLastString = mixerVertical ? "Basic.AudioMixer.KeepHiddenRight" : "Basic.AudioMixer.KeepHiddenBottom"; MenuCheckBox *hiddenLastCheckBox = new MenuCheckBox(QTStr(hiddenLastString), mixerMenu); - hiddenLastCheckBox->setAction(hiddenLastAction); hiddenLastCheckBox->setChecked(keepHiddenLast); hiddenLastAction->setDefaultWidget(hiddenLastCheckBox); @@ -826,7 +823,6 @@ void AudioMixer::createMixerContextMenu() const char *inactiveLastString = mixerVertical ? "Basic.AudioMixer.KeepInactiveRight" : "Basic.AudioMixer.KeepInactiveBottom"; MenuCheckBox *inactiveLastCheckBox = new MenuCheckBox(QTStr(inactiveLastString), mixerMenu); - inactiveLastCheckBox->setAction(inactiveLastAction); inactiveLastCheckBox->setChecked(keepInactiveLast); inactiveLastAction->setDefaultWidget(inactiveLastCheckBox); diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index b6f5445305f301..15c820a0dd06b6 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -1442,7 +1442,6 @@ void OBSBasic::applicationShutdown() noexcept delete scaleFilteringMenu; delete blendingModeMenu; delete colorMenu; - delete colorWidgetAction; delete colorSelect; delete deinterlaceMenu; delete perSceneTransitionMenu; diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 4343e16054c4da..40024aae0a19af 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -1177,6 +1177,13 @@ public slots: QMenu *CreateVisibilityTransitionMenu(bool visible); void CenterSelectedSceneItems(const CenterType ¢erType); + void setScaleFilter(OBSSceneItem sceneItem, obs_scale_type type); + void setBlendingMethod(OBSSceneItem item, obs_blending_method method); + void setBlendingMode(OBSSceneItem item, obs_blending_type type); + + void setDeinterlacingMode(OBSSceneItem sceneItem, obs_deinterlace_mode type); + void setDeinterlacingOrder(OBSSceneItem sceneItem, obs_deinterlace_field_order order); + /* OBS Callbacks */ static void SourceCreated(void *data, calldata_t *params); static void SourceRemoved(void *data, calldata_t *params); @@ -1189,14 +1196,6 @@ private slots: void ReorderSources(OBSScene scene); void RefreshSources(OBSScene scene); - void SetDeinterlacingMode(); - void SetDeinterlacingOrder(); - - void SetScaleFilter(); - - void SetBlendingMethod(); - void SetBlendingMode(); - void on_actionRotate90CW_triggered(); void on_actionRotate90CCW_triggered(); void on_actionRotate180_triggered(); @@ -1239,10 +1238,10 @@ private slots: public: void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); - QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); - QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); - QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *createDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *createScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *createBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *createBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, obs_sceneitem_t *item); void CreateSourcePopupMenu(int idx, bool preview); diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp index e76f2bf93d9441..51a29986ba54fa 100644 --- a/frontend/widgets/OBSBasic_SceneItems.cpp +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -21,6 +21,7 @@ #include "ColorSelect.hpp" #include "OBSProjector.hpp" +#include #include #include #include @@ -259,11 +260,8 @@ void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, cons } } -void OBSBasic::SetDeinterlacingMode() +void OBSBasic::setDeinterlacingMode(OBSSceneItem sceneItem, obs_deinterlace_mode mode) { - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_source_t *source = obs_sceneitem_get_source(sceneItem); obs_deinterlace_mode oldMode = obs_source_get_deinterlace_mode(source); @@ -288,11 +286,8 @@ void OBSBasic::SetDeinterlacingMode() } } -void OBSBasic::SetDeinterlacingOrder() +void OBSBasic::setDeinterlacingOrder(OBSSceneItem sceneItem, obs_deinterlace_field_order order) { - QAction *action = reinterpret_cast(sender()); - obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); obs_source_t *source = obs_sceneitem_get_source(sceneItem); obs_deinterlace_field_order oldOrder = obs_source_get_deinterlace_field_order(source); @@ -317,52 +312,69 @@ void OBSBasic::SetDeinterlacingOrder() } } -QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +QMenu *OBSBasic::createDeinterlacingMenu(QMenu *menu, obs_source_t *source) { - obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); - obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); - QAction *action; + obs_deinterlace_mode currentDeinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order currentDeinterlaceOrder = obs_source_get_deinterlace_field_order(source); -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceMode == mode); - - ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); - ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); - ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); - ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); - ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); - ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); - ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); - ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); - ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); -#undef ADD_MODE + QButtonGroup *modeGroup = new QButtonGroup(menu); + auto createModeEntry = [this, menu, modeGroup, currentDeinterlaceMode](const char *name, + obs_deinterlace_mode mode) { + MenuRadioButton *widget = new MenuRadioButton(QTStr(name), menu); + modeGroup->addButton(widget); + + QWidgetAction *action = new QWidgetAction(menu); + widget->setChecked(currentDeinterlaceMode == mode); + action->setDefaultWidget(widget); + menu->addAction(action); + + connect(widget, &QRadioButton::toggled, this, [this, mode, widget](bool checked) { + if (checked) { + setDeinterlacingMode(GetCurrentSceneItem(), mode); + } + }); + }; + + createModeEntry("Disable", OBS_DEINTERLACE_MODE_DISABLE); + createModeEntry("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + createModeEntry("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + createModeEntry("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + createModeEntry("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + createModeEntry("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + createModeEntry("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + createModeEntry("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + createModeEntry("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); menu->addSeparator(); -#define ADD_ORDER(name, order) \ - action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ - action->setProperty("order", (int)order); \ - action->setCheckable(true); \ - action->setChecked(deinterlaceOrder == order); + QButtonGroup *orderGroup = new QButtonGroup(menu); + auto createOrderEntry = [this, menu, orderGroup, currentDeinterlaceOrder](const char *name, + obs_deinterlace_field_order order) { + MenuRadioButton *widget = new MenuRadioButton(QTStr(name), menu); + orderGroup->addButton(widget); + + QWidgetAction *action = new QWidgetAction(menu); + widget->setChecked(currentDeinterlaceOrder == order); + action->setDefaultWidget(widget); + menu->addAction(action); - ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); - ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); -#undef ADD_ORDER + connect(widget, &QRadioButton::toggled, this, [this, order, widget](bool checked) { + if (checked) { + setDeinterlacingOrder(GetCurrentSceneItem(), order); + } + }); + }; + + createOrderEntry("Deinterlacing.TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + createOrderEntry("Deinterlacing.BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); return menu; } -void OBSBasic::SetScaleFilter() +void OBSBasic::setScaleFilter(OBSSceneItem sceneItem, obs_scale_type type) { - QAction *action = reinterpret_cast(sender()); - obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - - obs_scale_type oldMode = obs_sceneitem_get_scale_filter(sceneItem); - obs_sceneitem_set_scale_filter(sceneItem, mode); + obs_scale_type oldType = obs_sceneitem_get_scale_filter(sceneItem); + obs_sceneitem_set_scale_filter(sceneItem, type); auto undo_redo = [](const std::string &uuid, int64_t id, obs_scale_type val) { OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); @@ -382,41 +394,44 @@ void OBSBasic::SetScaleFilter() if (uuid && *uuid) { QString actionString = QTStr("Undo.ScaleFiltering").arg(obs_source_get_name(source), name); - auto undoFunction = std::bind(undo_redo, std::placeholders::_1, id, oldMode); - auto redoFunction = std::bind(undo_redo, std::placeholders::_1, id, mode); + auto undoFunction = std::bind(undo_redo, std::placeholders::_1, id, oldType); + auto redoFunction = std::bind(undo_redo, std::placeholders::_1, id, type); undo_s.add_action(actionString, undoFunction, redoFunction, uuid, uuid); } } -QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +QMenu *OBSBasic::createScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) { - obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); - QAction *action; + obs_scale_type currentScaleFilter = obs_sceneitem_get_scale_filter(item); -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(scaleFilter == mode); + auto createMenuEntry = [this, menu, currentScaleFilter](const char *name, obs_scale_type type) { + MenuRadioButton *widget = new MenuRadioButton(QTStr(name), menu); - ADD_MODE("Disable", OBS_SCALE_DISABLE); - ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); - ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); - ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); - ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); - ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); -#undef ADD_MODE + QWidgetAction *action = new QWidgetAction(menu); + widget->setChecked(currentScaleFilter == type); + action->setDefaultWidget(widget); + menu->addAction(action); + + connect(widget, &QRadioButton::toggled, this, [this, type, widget](bool checked) { + if (checked) { + setScaleFilter(GetCurrentSceneItem(), type); + } + }); + }; + + createMenuEntry("Disable", OBS_SCALE_DISABLE); + createMenuEntry("ScaleFiltering.Point", OBS_SCALE_POINT); + createMenuEntry("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + createMenuEntry("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + createMenuEntry("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + createMenuEntry("ScaleFiltering.Area", OBS_SCALE_AREA); return menu; } -void OBSBasic::SetBlendingMethod() +void OBSBasic::setBlendingMethod(OBSSceneItem sceneItem, obs_blending_method method) { - QAction *action = reinterpret_cast(sender()); - obs_blending_method method = (obs_blending_method)action->property("method").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_blending_method oldMethod = obs_sceneitem_get_blending_method(sceneItem); obs_sceneitem_set_blending_method(sceneItem, method); @@ -445,32 +460,35 @@ void OBSBasic::SetBlendingMethod() } } -QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +QMenu *OBSBasic::createBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) { - obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); - QAction *action; + obs_blending_method currentBlendingMethod = obs_sceneitem_get_blending_method(item); -#define ADD_MODE(name, method) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ - action->setProperty("method", (int)method); \ - action->setCheckable(true); \ - action->setChecked(blendingMethod == method); + auto createMenuEntry = [this, menu, currentBlendingMethod](const char *name, obs_blending_method method) { + MenuRadioButton *widget = new MenuRadioButton(QTStr(name), menu); + + QWidgetAction *action = new QWidgetAction(menu); + widget->setChecked(currentBlendingMethod == method); + action->setDefaultWidget(widget); + menu->addAction(action); + + connect(widget, &QRadioButton::toggled, this, [this, method, widget](bool checked) { + if (checked) { + setBlendingMethod(GetCurrentSceneItem(), method); + } + }); + }; - ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); - ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); -#undef ADD_MODE + createMenuEntry("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + createMenuEntry("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); return menu; } -void OBSBasic::SetBlendingMode() +void OBSBasic::setBlendingMode(OBSSceneItem sceneItem, obs_blending_type type) { - QAction *action = reinterpret_cast(sender()); - obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); - OBSSceneItem sceneItem = GetCurrentSceneItem(); - obs_blending_type oldMode = obs_sceneitem_get_blending_mode(sceneItem); - obs_sceneitem_set_blending_mode(sceneItem, mode); + obs_sceneitem_set_blending_mode(sceneItem, type); auto undo_redo = [](const std::string &uuid, int64_t id, obs_blending_type val) { OBSSourceAutoRelease s = obs_get_source_by_uuid(uuid.c_str()); @@ -491,31 +509,38 @@ void OBSBasic::SetBlendingMode() QString actionString = QTStr("Undo.BlendingMode").arg(obs_source_get_name(source), name); auto undoFunction = std::bind(undo_redo, std::placeholders::_1, id, oldMode); - auto redoFunction = std::bind(undo_redo, std::placeholders::_1, id, mode); + auto redoFunction = std::bind(undo_redo, std::placeholders::_1, id, type); undo_s.add_action(actionString, undoFunction, redoFunction, uuid, uuid); } } -QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +QMenu *OBSBasic::createBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) { - obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); - QAction *action; + obs_blending_type currentBlendingMode = obs_sceneitem_get_blending_mode(item); + + auto createMenuEntry = [this, menu, currentBlendingMode](const char *name, obs_blending_type type) { + MenuRadioButton *widget = new MenuRadioButton(QTStr(name), menu); + + QWidgetAction *action = new QWidgetAction(menu); + widget->setChecked(currentBlendingMode == type); + action->setDefaultWidget(widget); + menu->addAction(action); + + connect(widget, &QRadioButton::toggled, this, [this, type, widget](bool checked) { + if (checked) { + setBlendingMode(GetCurrentSceneItem(), type); + } + }); + }; -#define ADD_MODE(name, mode) \ - action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ - action->setProperty("mode", (int)mode); \ - action->setCheckable(true); \ - action->setChecked(blendingMode == mode); - - ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); - ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); - ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); - ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); - ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); - ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); - ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); -#undef ADD_MODE + createMenuEntry("BlendingMode.Normal", OBS_BLEND_NORMAL); + createMenuEntry("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + createMenuEntry("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + createMenuEntry("BlendingMode.Screen", OBS_BLEND_SCREEN); + createMenuEntry("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + createMenuEntry("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + createMenuEntry("BlendingMode.Darken", OBS_BLEND_DARKEN); return menu; } @@ -688,14 +713,14 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) // Scene item menu entries if (hasVideo && source) { scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); - popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + popup.addMenu(createScaleFilteringMenu(scaleFilteringMenu, sceneItem)); blendingModeMenu = new QMenu(QTStr("BlendingMode")); - popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + popup.addMenu(createBlendingModeMenu(blendingModeMenu, sceneItem)); blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); - popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + popup.addMenu(createBlendingMethodMenu(blendingMethodMenu, sceneItem)); if (isAsyncVideo) { deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); - popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + popup.addMenu(createDeinterlacingMenu(deinterlaceMenu, source)); } popup.addMenu(CreateVisibilityTransitionMenu(true)); diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp index 65275ef6d0606b..124278588780d5 100644 --- a/frontend/widgets/OBSBasic_Transitions.cpp +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -996,10 +997,13 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) duration->setSingleStep(50); duration->setValue(curDuration); - auto setTransition = [this](QAction *action, bool visible) { + QAction *transitionPropertiesAction = new QAction(QTStr("Properties"), this); + connect(transitionPropertiesAction, &QAction::triggered, this, + visible ? &OBSBasic::ShowTransitionProperties : &OBSBasic::HideTransitionProperties); + + auto setTransition = [this, transitionPropertiesAction](QString id, bool visible) { OBSBasic *main = OBSBasic::Get(); - QString id = action->property("transition_id").toString(); OBSSceneItem sceneItem = main->GetCurrentSceneItem(); int64_t sceneItemId = obs_sceneitem_get_id(sceneItem); std::string sceneUUID = obs_source_get_uuid(obs_scene_get_source(obs_sceneitem_get_scene(sceneItem))); @@ -1017,6 +1021,8 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) if (id.isNull() || id.isEmpty()) { obs_sceneitem_set_transition(sceneItem, visible, nullptr); obs_sceneitem_set_transition_duration(sceneItem, visible, 0); + + transitionPropertiesAction->setEnabled(false); } else { OBSSource tr = obs_sceneitem_get_transition(sceneItem, visible); @@ -1034,9 +1040,8 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) obs_sceneitem_set_transition_duration(sceneItem, visible, duration); } } - if (obs_source_configurable(tr)) { - CreatePropertiesWindow(tr); - } + + transitionPropertiesAction->setEnabled(obs_source_configurable(tr)); } OBSDataAutoRelease newTransitionData = obs_sceneitem_transition_save(sceneItem, visible); std::string undo_data(obs_data_get_json(oldTransitionData)); @@ -1055,21 +1060,31 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) }; connect(duration, &QSpinBox::valueChanged, duration, setDuration); - action = menu->addAction(QT_UTF8(Str("None"))); - action->setProperty("transition_id", QT_UTF8("")); - action->setCheckable(true); - action->setChecked(!curId); - connect(action, &QAction::triggered, this, std::bind(setTransition, action, visible)); + auto createTransitionButton = [this, setTransition, menu](QString name, QString transitionId, bool checked, + bool visible) { + MenuRadioButton *widget = new MenuRadioButton(name, menu); + QWidgetAction *widgetAction = new QWidgetAction(menu); + widget->setChecked(checked); + widgetAction->setDefaultWidget(widget); + menu->addAction(widgetAction); + + connect(widget, &QRadioButton::toggled, this, + [this, setTransition, transitionId, visible](bool checked) { + if (checked) { + setTransition(transitionId, visible); + } + }); + }; + + createTransitionButton("None", "", !curId, visible); + size_t idx = 0; const char *id; while (obs_enum_transition_types(idx++, &id)) { const char *name = obs_source_get_display_name(id); const bool match = id && curId && strcmp(id, curId) == 0; - action = menu->addAction(QT_UTF8(name)); - action->setProperty("transition_id", QT_UTF8(id)); - action->setCheckable(true); - action->setChecked(match); - connect(action, &QAction::triggered, this, std::bind(setTransition, action, visible)); + + createTransitionButton(name, id, match, visible); } QWidgetAction *durationAction = new QWidgetAction(menu); @@ -1077,11 +1092,9 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) menu->addSeparator(); menu->addAction(durationAction); - if (curId && obs_is_source_configurable(curId)) { - menu->addSeparator(); - menu->addAction(QTStr("Properties"), this, - visible ? &OBSBasic::ShowTransitionProperties : &OBSBasic::HideTransitionProperties); - } + menu->addAction(transitionPropertiesAction); + + transitionPropertiesAction->setEnabled(curId && obs_is_source_configurable(curId)); auto copyTransition = [this](QAction *, bool visible) { OBSBasic *main = OBSBasic::Get(); @@ -1117,6 +1130,7 @@ QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible) action = menu->addAction(QT_UTF8(Str("Paste"))); action->setEnabled(!!OBSGetStrongRef(copySourceTransition)); connect(action, &QAction::triggered, this, std::bind(pasteTransition, action, visible)); + return menu; }