From c01a9bea49bb30db06322ef1845431890ff69cb0 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Fri, 18 Jul 2025 15:17:16 -0400 Subject: [PATCH] frontend: Create AlignmentSelector widget --- frontend/OBSApp.cpp | 11 + frontend/cmake/ui-components.cmake | 6 + .../components/AccessibleAlignmentCell.cpp | 71 ++++ .../components/AccessibleAlignmentCell.hpp | 51 +++ .../AccessibleAlignmentSelector.cpp | 126 +++++++ .../AccessibleAlignmentSelector.hpp | 47 +++ frontend/components/AlignmentSelector.cpp | 324 ++++++++++++++++++ frontend/components/AlignmentSelector.hpp | 71 ++++ frontend/data/themes/Yami.obt | 40 +++ frontend/widgets/OBSBasic.hpp | 2 + 10 files changed, 749 insertions(+) create mode 100644 frontend/components/AccessibleAlignmentCell.cpp create mode 100644 frontend/components/AccessibleAlignmentCell.hpp create mode 100644 frontend/components/AccessibleAlignmentSelector.cpp create mode 100644 frontend/components/AccessibleAlignmentSelector.hpp create mode 100644 frontend/components/AlignmentSelector.cpp create mode 100644 frontend/components/AlignmentSelector.hpp diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 1bf3f80ea..b8181a67c 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -144,6 +144,15 @@ UncleanLaunchAction handleUncleanShutdown(bool enableCrashUpload) return launchAction; } + +QAccessibleInterface *alignmentSelectorFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("AlignmentSelector")) { + if (auto *w = qobject_cast(object)) + return new AccessibleAlignmentSelector(w); + } + return nullptr; +} } // namespace QObject *CreateShortcutFilter() @@ -1022,6 +1031,8 @@ void OBSApp::AppInit() { ProfileScope("OBSApp::AppInit"); + QAccessible::installFactory(alignmentSelectorFactory); + if (!MakeUserDirs()) throw "Failed to create required user directories"; if (!InitGlobalConfig()) diff --git a/frontend/cmake/ui-components.cmake b/frontend/cmake/ui-components.cmake index ad0506a08..c15dba6f7 100644 --- a/frontend/cmake/ui-components.cmake +++ b/frontend/cmake/ui-components.cmake @@ -12,6 +12,12 @@ target_sources( PRIVATE components/AbsoluteSlider.cpp components/AbsoluteSlider.hpp + components/AccessibleAlignmentCell.cpp + components/AccessibleAlignmentCell.hpp + components/AccessibleAlignmentSelector.cpp + components/AccessibleAlignmentSelector.hpp + components/AlignmentSelector.cpp + components/AlignmentSelector.hpp components/ApplicationAudioCaptureToolbar.cpp components/ApplicationAudioCaptureToolbar.hpp components/AudioCaptureToolbar.cpp diff --git a/frontend/components/AccessibleAlignmentCell.cpp b/frontend/components/AccessibleAlignmentCell.cpp new file mode 100644 index 000000000..c44ec5c06 --- /dev/null +++ b/frontend/components/AccessibleAlignmentCell.cpp @@ -0,0 +1,71 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "AccessibleAlignmentCell.hpp" + +#include + +using namespace std::string_view_literals; +constexpr std::array indexToStrings = { + "Basic.TransformWindow.Alignment.TopLeft"sv, "Basic.TransformWindow.Alignment.TopCenter"sv, + "Basic.TransformWindow.Alignment.TopRight"sv, "Basic.TransformWindow.Alignment.CenterLeft"sv, + "Basic.TransformWindow.Alignment.Center"sv, "Basic.TransformWindow.Alignment.CenterRight"sv, + "Basic.TransformWindow.Alignment.BottomLeft"sv, "Basic.TransformWindow.Alignment.BottomCenter"sv, + "Basic.TransformWindow.Alignment.BottomRight"sv}; + +AccessibleAlignmentCell::AccessibleAlignmentCell(QAccessibleInterface *parent, AlignmentSelector *widget, int index) + : parent_(parent), + widget(widget), + index_(index) +{ +} + +QRect AccessibleAlignmentCell::rect() const +{ + return widget->cellRect(index_); +} + +QString AccessibleAlignmentCell::text(QAccessible::Text text) const +{ + if (text == QAccessible::Name || text == QAccessible::Value) { + return QString(indexToStrings[index_].data()); + } + return QString(); +} + +QAccessible::State AccessibleAlignmentCell::state() const +{ + QAccessible::State state; + bool enabled = widget->isEnabled(); + + bool isSelectedCell = widget->currentIndex() == index_; + bool isFocusedCell = widget->focusedCell == index_; + + state.disabled = !enabled; + state.focusable = enabled; + state.focused = widget->hasFocus() && isFocusedCell; + state.checkable = true; + state.checked = isSelectedCell; + state.selected = isSelectedCell; + + return state; +} + +QAccessible::Role AccessibleAlignmentCell::role() const +{ + return QAccessible::CheckBox; +} diff --git a/frontend/components/AccessibleAlignmentCell.hpp b/frontend/components/AccessibleAlignmentCell.hpp new file mode 100644 index 000000000..7e374beb5 --- /dev/null +++ b/frontend/components/AccessibleAlignmentCell.hpp @@ -0,0 +1,51 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#include +#include + +class AlignmentSelector; + +class AccessibleAlignmentCell : public QAccessibleInterface { + QAccessibleInterface *parent_; + AlignmentSelector *widget; + int index_; + +public: + AccessibleAlignmentCell(QAccessibleInterface *parent, AlignmentSelector *widget, int index); + + int index() const { return index_; } + + QRect rect() const override; + QString text(QAccessible::Text t) const override; + QAccessible::State state() const override; + QAccessible::Role role() const override; + + QObject *object() const override { return nullptr; } + QAccessibleInterface *child(int) const override { return nullptr; } + QAccessibleInterface *childAt(int, int) const override { return nullptr; } + int childCount() const override { return 0; } + int indexOfChild(const QAccessibleInterface *) const override { return -1; } + QAccessibleInterface *parent() const override { return parent_; } + QAccessibleInterface *focusChild() const override { return nullptr; } + bool isValid() const override { return widget != nullptr; } + void setText(QAccessible::Text, const QString &) override {} +}; diff --git a/frontend/components/AccessibleAlignmentSelector.cpp b/frontend/components/AccessibleAlignmentSelector.cpp new file mode 100644 index 000000000..8abb4b06a --- /dev/null +++ b/frontend/components/AccessibleAlignmentSelector.cpp @@ -0,0 +1,126 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "AccessibleAlignmentSelector.hpp" + +#include + +AccessibleAlignmentSelector::AccessibleAlignmentSelector(AlignmentSelector *widget_) + : QAccessibleWidget(widget_, QAccessible::Grouping) +{ + for (int i = 0; i < cellCount; ++i) { + AccessibleAlignmentCell *cell = new AccessibleAlignmentCell(this, widget_, i); + QAccessible::registerAccessibleInterface(cell); + cellInterfaces.insert(i, QAccessible::uniqueId(cell)); + } +} + +AccessibleAlignmentSelector::~AccessibleAlignmentSelector() +{ + for (QAccessible::Id id : std::as_const(cellInterfaces)) { + QAccessible::deleteAccessibleInterface(id); + } +} + +int AccessibleAlignmentSelector::childCount() const +{ + return cellCount; +} + +QAccessibleInterface *AccessibleAlignmentSelector::child(int index) const +{ + if (QAccessible::Id id = cellInterfaces.value(index)) { + return QAccessible::accessibleInterface(id); + } + + return nullptr; +} + +int AccessibleAlignmentSelector::indexOfChild(const QAccessibleInterface *child) const +{ + if (!child) { + return -1; + } + + QAccessible::Id id = QAccessible::uniqueId(const_cast(child)); + return cellInterfaces.key(id, -1); +} + +bool AccessibleAlignmentSelector::isValid() const +{ + return widget() != nullptr; +} + +QAccessibleInterface *AccessibleAlignmentSelector::focusChild() const +{ + for (int i = 0; i < childCount(); ++i) { + if (child(i)->state().focused) { + return child(i); + } + } + return nullptr; +} + +QRect AccessibleAlignmentSelector::rect() const +{ + return widget()->rect(); +} + +QString AccessibleAlignmentSelector::text(QAccessible::Text textType) const +{ + if (textType == QAccessible::Name) { + QString str = widget()->accessibleName(); + if (str.isEmpty()) { + str = QTStr("Accessible.Widget.Name.AlignmentSelector"); + } + return str; + } + + if (textType == QAccessible::Value) { + return value().toString(); + } + + return QAccessibleWidget::text(textType); +} + +QAccessible::Role AccessibleAlignmentSelector::role() const +{ + return QAccessible::Grouping; +} + +QAccessible::State AccessibleAlignmentSelector::state() const +{ + QAccessible::State state; + + state.focusable = true; + state.focused = widget()->hasFocus(); + state.disabled = !widget()->isEnabled(); + state.readOnly = false; + + return state; +} + +QVariant AccessibleAlignmentSelector::value() const +{ + for (int i = 0; i < childCount(); ++i) { + if (child(i)->state().checked) { + return child(i)->text(QAccessible::Name); + } + } + + return QTStr("None"); +} diff --git a/frontend/components/AccessibleAlignmentSelector.hpp b/frontend/components/AccessibleAlignmentSelector.hpp new file mode 100644 index 000000000..87659fbc1 --- /dev/null +++ b/frontend/components/AccessibleAlignmentSelector.hpp @@ -0,0 +1,47 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#include +#include +#include + +class AlignmentSelector; + +class AccessibleAlignmentSelector : public QAccessibleWidget { + mutable QHash cellInterfaces{}; + static constexpr int cellCount = 9; + +public: + explicit AccessibleAlignmentSelector(AlignmentSelector *widget); + ~AccessibleAlignmentSelector(); + + QRect rect() const override; + QAccessible::Role role() const override; + QAccessible::State state() const override; + QString text(QAccessible::Text t) const override; + QAccessibleInterface *child(int index) const override; + int childCount() const override; + int indexOfChild(const QAccessibleInterface *child) const override; + bool isValid() const override; + QAccessibleInterface *focusChild() const override; + + QVariant value() const; +}; diff --git a/frontend/components/AlignmentSelector.cpp b/frontend/components/AlignmentSelector.cpp new file mode 100644 index 000000000..85a403a49 --- /dev/null +++ b/frontend/components/AlignmentSelector.cpp @@ -0,0 +1,324 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "AlignmentSelector.hpp" + +#include + +#include +#include +#include +#include + +AlignmentSelector::AlignmentSelector(QWidget *parent) : QWidget(parent) +{ + setFocusPolicy(Qt::StrongFocus); + setMouseTracking(true); + setAttribute(Qt::WA_Hover); +} + +QSize AlignmentSelector::sizeHint() const +{ + int base = fontMetrics().height() * 2; + return QSize(base, base); +} + +QSize AlignmentSelector::minimumSizeHint() const +{ + return QSize(16, 16); +} + +Qt::Alignment AlignmentSelector::value() const +{ + return cellAlignment(selectedCell); +} + +int AlignmentSelector::currentIndex() const +{ + return selectedCell; +} + +void AlignmentSelector::setAlignment(Qt::Alignment value) +{ + alignment = value; +} + +void AlignmentSelector::setCurrentIndex(int index) +{ + selectCell(index); +} + +void AlignmentSelector::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + QStyle *style = this->style(); + + int cellW = gridRect().width() / 3; + int cellH = gridRect().height() / 3; + + for (int i = 0; i < 9; ++i) { + QRect rect = cellRect(i); + rect = rect.adjusted(0, 0, -1, -1); + + QStyleOptionFrame frameOpt; + frameOpt.rect = rect; + frameOpt.state = isEnabled() ? QStyle::State_Enabled : QStyle::State_None; + frameOpt.lineWidth = 1; + frameOpt.midLineWidth = 0; + if (i == hoveredCell) { + frameOpt.state |= QStyle::State_MouseOver; + } + if (i == selectedCell) { + frameOpt.state |= QStyle::State_On; + } + if (i == focusedCell && hasFocus()) { + frameOpt.state |= QStyle::State_HasFocus; + } + + QStyleOptionButton radioOpt; + radioOpt.state = isEnabled() ? QStyle::State_Enabled : QStyle::State_None; + radioOpt.rect = rect.adjusted(cellW / 6, cellH / 6, -cellW / 6, -cellH / 6); + if (i == hoveredCell) { + radioOpt.state |= QStyle::State_MouseOver; + } + if (i == selectedCell) { + radioOpt.state |= QStyle::State_On; + } + + if (i == focusedCell && hasFocus()) { + radioOpt.state |= QStyle::State_HasFocus; + } + style->drawPrimitive(QStyle::PE_IndicatorRadioButton, &radioOpt, &painter, this); + + style->drawPrimitive(QStyle::PE_Frame, &frameOpt, &painter, this); + + if (i == focusedCell && hasFocus()) { + QStyleOptionFocusRect focusOpt; + focusOpt.initFrom(this); + focusOpt.rect = rect.adjusted(1, 1, -1, -1); + focusOpt.state = isEnabled() ? QStyle::State_Enabled : QStyle::State_None; + focusOpt.state |= QStyle::State_HasFocus; + style->drawPrimitive(QStyle::PE_FrameFocusRect, &focusOpt, &painter, this); + } + } +} + +QRect AlignmentSelector::cellRect(int index) const +{ + int col = index % 3; + int row = index / 3; + + QRect gridRect = this->gridRect(); + int cellW = gridRect.width() / 3; + int cellH = gridRect.height() / 3; + + return QRect(col * cellW + gridRect.left(), row * cellH + gridRect.top(), cellW, cellH); +} + +Qt::Alignment AlignmentSelector::cellAlignment(int index) const +{ + Qt::Alignment hAlign; + Qt::Alignment vAlign; + + switch (index % 3) { + case 0: + hAlign = Qt::AlignLeft; + break; + case 1: + hAlign = Qt::AlignHCenter; + break; + case 2: + hAlign = Qt::AlignRight; + break; + } + + switch (index / 3) { + case 0: + vAlign = Qt::AlignTop; + break; + case 1: + vAlign = Qt::AlignVCenter; + break; + case 2: + vAlign = Qt::AlignBottom; + break; + } + + return hAlign | vAlign; +} + +void AlignmentSelector::leaveEvent(QEvent *) +{ + hoveredCell = -1; + update(); +} + +void AlignmentSelector::mouseMoveEvent(QMouseEvent *event) +{ + QRect grid = gridRect(); + int cellW = grid.width() / 3; + int cellH = grid.height() / 3; + + QPoint pos = event->position().toPoint(); + if (!grid.contains(pos)) { + hoveredCell = -1; + return; + } + + int col = (pos.x() - grid.left()) / cellW; + int row = (pos.y() - grid.top()) / cellH; + int cell = row * 3 + col; + + if (hoveredCell != cell) { + hoveredCell = cell; + update(); + } +} + +void AlignmentSelector::mousePressEvent(QMouseEvent *event) +{ + QRect grid = gridRect(); + int cellW = grid.width() / 3; + int cellH = grid.height() / 3; + + QPoint pos = event->position().toPoint(); + if (!grid.contains(pos)) { + return; + } + + int col = (pos.x() - grid.left()) / cellW; + int row = (pos.y() - grid.top()) / cellH; + int cell = row * 3 + col; + + selectCell(cell); +} + +void AlignmentSelector::keyPressEvent(QKeyEvent *event) +{ + int moveX = 0; + int moveY = 0; + + switch (event->key()) { + case Qt::Key_Left: + moveX = -1; + break; + case Qt::Key_Right: + moveX = 1; + break; + case Qt::Key_Up: + moveY = -1; + break; + case Qt::Key_Down: + moveY = 1; + break; + case Qt::Key_Space: + case Qt::Key_Return: + case Qt::Key_Enter: + selectCell(focusedCell); + return; + default: + QWidget::keyPressEvent(event); + return; + } + + moveFocusedCell(moveX, moveY); +} + +QRect AlignmentSelector::gridRect() const +{ + int side = std::min(width(), height()); + int x = 0; + int y = 0; + + if (alignment & Qt::AlignHCenter) { + x = (width() - side) / 2; + } else if (alignment & Qt::AlignRight) { + x = width() - side; + } + + if (alignment & Qt::AlignVCenter) { + y = (height() - side) / 2; + } else if (alignment & Qt::AlignBottom) { + y = height() - side; + } + + return QRect(x, y, side, side); +} + +void AlignmentSelector::moveFocusedCell(int moveX, int moveY) +{ + int row = focusedCell / 3; + int col = focusedCell % 3; + + row = std::clamp(row + moveY, 0, 2); + col = std::clamp(col + moveX, 0, 2); + + int newCell = row * 3 + col; + setFocusedCell(newCell); +} + +void AlignmentSelector::setFocusedCell(int cell) +{ + if (cell != focusedCell) { + focusedCell = cell; + update(); + + if (AccessibleAlignmentSelector *interface = + dynamic_cast(QAccessible::queryAccessibleInterface(this))) { + if (QAccessibleInterface *child = interface->child(cell)) { + QAccessibleEvent event(child, QAccessible::Focus); + QAccessible::updateAccessibility(&event); + } + } + } +} + +void AlignmentSelector::selectCell(int cell) +{ + setFocusedCell(cell); + if (cell != selectedCell) { + selectedCell = cell; + + emit valueChanged(cellAlignment(cell)); + emit currentIndexChanged(cell); + } + update(); + + if (AccessibleAlignmentSelector *interface = + dynamic_cast(QAccessible::queryAccessibleInterface(this))) { + if (QAccessibleInterface *child = interface->child(cell)) { + QAccessible::State state; + state.checked = true; + + QAccessibleStateChangeEvent event(child, state); + QAccessible::updateAccessibility(&event); + } + } +} + +void AlignmentSelector::focusInEvent(QFocusEvent *) +{ + setFocusedCell(selectedCell); + update(); +} + +void AlignmentSelector::focusOutEvent(QFocusEvent *) +{ + hoveredCell = -1; + setFocusedCell(-1); + update(); +} diff --git a/frontend/components/AlignmentSelector.hpp b/frontend/components/AlignmentSelector.hpp new file mode 100644 index 000000000..decee9341 --- /dev/null +++ b/frontend/components/AlignmentSelector.hpp @@ -0,0 +1,71 @@ +/****************************************************************************** + Copyright (C) 2025 by Taylor Giampaolo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#include +#include +#include + +class AlignmentSelector : public QWidget { + Q_OBJECT + + friend class AccessibleAlignmentSelector; + friend class AccessibleAlignmentCell; + +public: + explicit AlignmentSelector(QWidget *parent = nullptr); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + Qt::Alignment value() const; + int currentIndex() const; + + void setAlignment(Qt::Alignment alignment); + void setCurrentIndex(int index); + +signals: + void valueChanged(Qt::Alignment value); + void currentIndexChanged(int value); + +protected: + QRect cellRect(int index) const; + + void paintEvent(QPaintEvent *event) override; + void leaveEvent(QEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void focusInEvent(QFocusEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; + +private: + Qt::Alignment alignment = Qt::AlignTop | Qt::AlignLeft; + + int hoveredCell = -1; + int focusedCell = 4; + int selectedCell = 4; + + QRect gridRect() const; + + void moveFocusedCell(int moveX, int moveY); + void setFocusedCell(int cell); + void selectCell(int cell); + Qt::Alignment cellAlignment(int index) const; +}; diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index d00b26708..06fbe968b 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -2513,3 +2513,43 @@ idian--RowFrame.hover .row-buddy { idian--RowFrame.hover idian--ExpandButton::indicator { border-color: var(--grey1); } + +AlignmentSelector { + border: 1px solid var(--input_border); + margin: 0px; + border-radius: 2px; +} + +AlignmentSelector:focus, +AlignmentSelector:hover { + border: 1px solid var(--white3); +} + +AlignmentSelector:checked:hover { + border: 1px solid var(--primary_lighter); +} + +AlignmentSelector::indicator { + margin: 0px; + background: transparent; +} + +AlignmentSelector::indicator:checked { + background: var(--primary); +} + +AlignmentSelector::indicator:checked:focus { + background: var(--primary_light); +} + +AlignmentSelector:disabled { + border: 1px solid var(--grey3); +} + +AlignmentSelector::indicator:disabled { + background: var(--grey5); +} + +AlignmentSelector::indicator:checked:disabled { + background: var(--grey3); +} diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 8f6222dba..e817a60ac 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -21,6 +21,7 @@ #include "OBSMainWindow.hpp" #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include +#include #include #include