frontend: Create AlignmentSelector widget

This commit is contained in:
Warchamp7
2025-07-18 15:17:16 -04:00
committed by Ryan Foster
parent 8c946e792a
commit c01a9bea49
10 changed files with 749 additions and 0 deletions

View File

@@ -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<AlignmentSelector *>(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())

View File

@@ -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

View File

@@ -0,0 +1,71 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "AccessibleAlignmentCell.hpp"
#include <OBSApp.hpp>
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;
}

View File

@@ -0,0 +1,51 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <components/AlignmentSelector.hpp>
#include <QAccessibleInterface>
#include <QRect>
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 {}
};

View File

@@ -0,0 +1,126 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "AccessibleAlignmentSelector.hpp"
#include <OBSApp.hpp>
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<QAccessibleInterface *>(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");
}

View File

@@ -0,0 +1,47 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <components/AccessibleAlignmentCell.hpp>
#include <QAccessible>
#include <QAccessibleInterface>
#include <QAccessibleWidget>
class AlignmentSelector;
class AccessibleAlignmentSelector : public QAccessibleWidget {
mutable QHash<int, QAccessible::Id> 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;
};

View File

@@ -0,0 +1,324 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "AlignmentSelector.hpp"
#include <util/base.h>
#include <QAccessible>
#include <QMouseEvent>
#include <QPainter>
#include <QStyleOptionButton>
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<AccessibleAlignmentSelector *>(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<AccessibleAlignmentSelector *>(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();
}

View File

@@ -0,0 +1,71 @@
/******************************************************************************
Copyright (C) 2025 by Taylor Giampaolo <warchamp7@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <components/AccessibleAlignmentSelector.hpp>
#include <QFocusEvent>
#include <QKeyEvent>
#include <QWidget>
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;
};

View File

@@ -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);
}

View File

@@ -21,6 +21,7 @@
#include "OBSMainWindow.hpp"
#include <OBSApp.hpp>
#include <components/AccessibleAlignmentSelector.hpp>
#include <oauth/Auth.hpp>
#include <utility/BasicOutputHandler.hpp>
#include <utility/OBSCanvas.hpp>
@@ -38,6 +39,7 @@
#include <util/threading.h>
#include <util/util.hpp>
#include <QAccessible>
#include <QSystemTrayIcon>
#include <deque>