diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index b14aa2f569bbf5..7e378c8f62f396 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 ad0506a0858ff2..c15dba6f7d068e 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 00000000000000..c44ec5c06e5224 --- /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 00000000000000..7e374beb5d0d44 --- /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 00000000000000..8abb4b06afabbb --- /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 00000000000000..87659fbc1f4a96 --- /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 00000000000000..85a403a49b7342 --- /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 00000000000000..decee934102961 --- /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/locale/en-US.ini b/frontend/data/locale/en-US.ini index e1a4efa32068d0..536e6926e875f5 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -63,6 +63,8 @@ Left="Left" Right="Right" Top="Top" Bottom="Bottom" +Width="Width" +Height="Height" Reset="Reset" Hours="Hours" Minutes="Minutes" @@ -699,24 +701,26 @@ Basic.Filters.AddFilter.Text="Please specify the name of the filter" # transform window Basic.TransformWindow="Scene Item Transform" Basic.TransformWindow.Position="Position" -Basic.TransformWindow.PositionX="Position X" -Basic.TransformWindow.PositionY="Position Y" +Basic.TransformWindow.PositionX="X" +Basic.TransformWindow.PositionY="Y" Basic.TransformWindow.Rotation="Rotation" Basic.TransformWindow.Size="Size" Basic.TransformWindow.Width="Width" Basic.TransformWindow.Height="Height" -Basic.TransformWindow.Alignment="Positional Alignment" -Basic.TransformWindow.BoundsType="Bounding Box Type" -Basic.TransformWindow.BoundsAlignment="Alignment in Bounding Box" -Basic.TransformWindow.Bounds="Bounding Box Size" +Basic.TransformWindow.Alignment="Alignment" +Basic.TransformWindow.BoundsType="Bounds Type" +Basic.TransformWindow.BoundsAlignment="Bounds Alignment" +Basic.TransformWindow.Bounds="Bounds" Basic.TransformWindow.BoundsWidth="Bounding Box Width" Basic.TransformWindow.BoundsHeight="Bounding Box Height" -Basic.TransformWindow.CropToBounds="Crop to Bounding Box" +Basic.TransformWindow.CropToBounds="Crop Bounds" Basic.TransformWindow.Crop="Crop" Basic.TransformWindow.CropLeft="Crop Left" Basic.TransformWindow.CropRight="Crop Right" Basic.TransformWindow.CropTop="Crop Top" Basic.TransformWindow.CropBottom="Crop Bottom" +Basic.TransformWindow.Accessible.PositionX="X Position" +Basic.TransformWindow.Accessible.PositionY="Y Position" Basic.TransformWindow.Alignment.TopLeft="Top Left" Basic.TransformWindow.Alignment.TopCenter="Top Center" @@ -728,13 +732,13 @@ Basic.TransformWindow.Alignment.BottomLeft="Bottom Left" Basic.TransformWindow.Alignment.BottomCenter="Bottom Center" Basic.TransformWindow.Alignment.BottomRight="Bottom Right" -Basic.TransformWindow.BoundsType.None="No bounds" +Basic.TransformWindow.BoundsType.None="Automatic" Basic.TransformWindow.BoundsType.MaxOnly="Maximum size only" -Basic.TransformWindow.BoundsType.ScaleInner="Scale to inner bounds" -Basic.TransformWindow.BoundsType.ScaleOuter="Scale to outer bounds" -Basic.TransformWindow.BoundsType.ScaleToWidth="Scale to width of bounds" -Basic.TransformWindow.BoundsType.ScaleToHeight="Scale to height of bounds" -Basic.TransformWindow.BoundsType.Stretch="Stretch to bounds" +Basic.TransformWindow.BoundsType.ScaleInner="Fit" +Basic.TransformWindow.BoundsType.ScaleOuter="Cover" +Basic.TransformWindow.BoundsType.ScaleToWidth="Fill Width" +Basic.TransformWindow.BoundsType.ScaleToHeight="Fill Height" +Basic.TransformWindow.BoundsType.Stretch="Stretch" Basic.TransformWindow.Title="Edit Transform for '%1'" Basic.TransformWindow.NoSelectedSource="No source selected" @@ -1635,3 +1639,6 @@ PluginManager.Section.Discover="Browse" PluginManager.Section.Manage="Installed" PluginManager.Section.Updates="Updates" PluginManager.Section.Manage.Title="Manage Enabled Plugins" + +# Custom Widget Localization +Accessible.Widget.Name.AlignmentSelector="Alignment Selector" diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index d00b26708fc4a1..752dc069ee55ec 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -335,6 +335,12 @@ margin: var(--spacing_base) 0; } +.subtitle { + font-size: var(--font_small); + font-weight: bold; + color: var(--text_muted); +} + .button-primary { background-color: var(--primary_dark); border-color: var(--primary); @@ -2513,3 +2519,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/dialogs/OBSBasicTransform.cpp b/frontend/dialogs/OBSBasicTransform.cpp index 04407a8e08a880..828aa9acc6b0db 100644 --- a/frontend/dialogs/OBSBasicTransform.cpp +++ b/frontend/dialogs/OBSBasicTransform.cpp @@ -4,6 +4,7 @@ #include "moc_OBSBasicTransform.cpp" +namespace { static bool find_sel(obs_scene_t *, obs_sceneitem_t *item, void *param) { OBSSceneItem &dst = *static_cast(param); @@ -28,8 +29,28 @@ static OBSSceneItem FindASelectedItem(obs_scene_t *scene) obs_scene_enum_items(scene, find_sel, &item); return item; } +static vec2 getAlignmentConversion(uint32_t alignment) +{ + vec2 ratio = {0.5f, 0.5f}; + if (alignment & OBS_ALIGN_RIGHT) { + ratio.x = 1.0f; + } + if (alignment & OBS_ALIGN_LEFT) { + ratio.x = 0.0f; + } + if (alignment & OBS_ALIGN_BOTTOM) { + ratio.y = 1.0f; + } + if (alignment & OBS_ALIGN_TOP) { + ratio.y = 0.0f; + } + + return ratio; +} +} // namespace #define COMBO_CHANGED &QComboBox::currentIndexChanged +#define ALIGN_CHANGED &AlignmentSelector::currentIndexChanged #define ISCROLL_CHANGED &QSpinBox::valueChanged #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged @@ -42,24 +63,38 @@ OBSBasicTransform::OBSBasicTransform(OBSSceneItem item, OBSBasic *parent) ui->setupUi(this); - HookWidget(ui->positionX, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->positionY, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->rotation, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->sizeX, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->sizeY, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->align, COMBO_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->boundsType, COMBO_CHANGED, &OBSBasicTransform::OnBoundsType); - HookWidget(ui->boundsAlign, COMBO_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->boundsWidth, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->boundsHeight, DSCROLL_CHANGED, &OBSBasicTransform::OnControlChanged); - HookWidget(ui->cropLeft, ISCROLL_CHANGED, &OBSBasicTransform::OnCropChanged); - HookWidget(ui->cropRight, ISCROLL_CHANGED, &OBSBasicTransform::OnCropChanged); - HookWidget(ui->cropTop, ISCROLL_CHANGED, &OBSBasicTransform::OnCropChanged); - HookWidget(ui->cropBottom, ISCROLL_CHANGED, &OBSBasicTransform::OnCropChanged); + positionAlignment = new AlignmentSelector(this); + positionAlignment->setAccessibleName(QTStr("Basic.TransformWindow.Alignment")); + ui->alignmentLayout->addWidget(positionAlignment); + positionAlignment->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + boundsAlignment = new AlignmentSelector(this); + boundsAlignment->setAccessibleName(QTStr("Basic.TransformWindow.BoundsAlignment")); + boundsAlignment->setEnabled(false); + ui->boundsAlignmentLayout->addWidget(boundsAlignment); + boundsAlignment->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + setTabOrder(ui->rotation, positionAlignment); + setTabOrder(ui->boundsType, boundsAlignment); + + hookWidget(ui->positionX, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->positionY, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->rotation, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->sizeX, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->sizeY, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(positionAlignment.get(), ALIGN_CHANGED, &OBSBasicTransform::onAlignChanged); + hookWidget(ui->boundsType, COMBO_CHANGED, &OBSBasicTransform::onBoundsType); + hookWidget(boundsAlignment.get(), ALIGN_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->boundsWidth, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->boundsHeight, DSCROLL_CHANGED, &OBSBasicTransform::onControlChanged); + hookWidget(ui->cropLeft, ISCROLL_CHANGED, &OBSBasicTransform::onCropChanged); + hookWidget(ui->cropRight, ISCROLL_CHANGED, &OBSBasicTransform::onCropChanged); + hookWidget(ui->cropTop, ISCROLL_CHANGED, &OBSBasicTransform::onCropChanged); + hookWidget(ui->cropBottom, ISCROLL_CHANGED, &OBSBasicTransform::onCropChanged); #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - HookWidget(ui->cropToBounds, &QCheckBox::checkStateChanged, &OBSBasicTransform::OnControlChanged); + hookWidget(ui->cropToBounds, &QCheckBox::checkStateChanged, &OBSBasicTransform::onControlChanged); #else - HookWidget(ui->cropToBounds, &QCheckBox::stateChanged, &OBSBasicTransform::OnControlChanged); + hookWidget(ui->cropToBounds, &QCheckBox::stateChanged, &OBSBasicTransform::onControlChanged); #endif ui->buttonBox->button(QDialogButtonBox::Close)->setDefault(true); @@ -69,14 +104,18 @@ OBSBasicTransform::OBSBasicTransform(OBSSceneItem item, OBSBasic *parent) installEventFilter(CreateShortcutFilter()); OBSScene scene = obs_sceneitem_get_scene(item); - SetScene(scene); - SetItem(item); + setScene(scene); + setItem(item); std::string name = obs_source_get_name(obs_sceneitem_get_source(item)); setWindowTitle(QTStr("Basic.TransformWindow.Title").arg(name.c_str())); OBSDataAutoRelease wrapper = obs_scene_save_transform_states(main->GetCurrentScene(), false); undo_data = std::string(obs_data_get_json(wrapper)); + + adjustSize(); + setMinimumSize(size()); + setMaximumSize(size()); } OBSBasicTransform::~OBSBasicTransform() @@ -97,7 +136,7 @@ OBSBasicTransform::~OBSBasicTransform() undo_redo, undo_redo, undo_data, redo_data); } -void OBSBasicTransform::SetScene(OBSScene scene) +void OBSBasicTransform::setScene(OBSScene scene) { sigs.clear(); @@ -113,25 +152,27 @@ void OBSBasicTransform::SetScene(OBSScene scene) } } -void OBSBasicTransform::SetItem(OBSSceneItem newItem) +void OBSBasicTransform::setItem(OBSSceneItem newItem) { - QMetaObject::invokeMethod(this, "SetItemQt", Q_ARG(OBSSceneItem, OBSSceneItem(newItem))); + QMetaObject::invokeMethod(this, "setItemQt", Q_ARG(OBSSceneItem, OBSSceneItem(newItem))); } -void OBSBasicTransform::SetEnabled(bool enable) +void OBSBasicTransform::setEnabled(bool enable) { - ui->container->setEnabled(enable); + ui->transformSettings->setEnabled(enable); + ui->boundsSettings->setEnabled(enable); + ui->cropSettings->setEnabled(enable); ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(enable); } -void OBSBasicTransform::SetItemQt(OBSSceneItem newItem) +void OBSBasicTransform::setItemQt(OBSSceneItem newItem) { item = newItem; if (item) - RefreshControls(); + refreshControls(); bool enable = !!item && !obs_sceneitem_locked(item); - SetEnabled(enable); + setEnabled(enable); } void OBSBasicTransform::OBSSceneItemTransform(void *param, calldata_t *data) @@ -140,7 +181,7 @@ void OBSBasicTransform::OBSSceneItemTransform(void *param, calldata_t *data) OBSSceneItem item = (obs_sceneitem_t *)calldata_ptr(data, "item"); if (item == window->item && !window->ignoreTransformSignal) - QMetaObject::invokeMethod(window, "RefreshControls"); + QMetaObject::invokeMethod(window, "refreshControls"); } void OBSBasicTransform::OBSSceneItemRemoved(void *param, calldata_t *data) @@ -150,7 +191,7 @@ void OBSBasicTransform::OBSSceneItemRemoved(void *param, calldata_t *data) obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(data, "item"); if (item == window->item) - window->SetItem(FindASelectedItem(scene)); + window->setItem(FindASelectedItem(scene)); } void OBSBasicTransform::OBSSceneItemSelect(void *param, calldata_t *data) @@ -159,7 +200,7 @@ void OBSBasicTransform::OBSSceneItemSelect(void *param, calldata_t *data) OBSSceneItem item = (obs_sceneitem_t *)calldata_ptr(data, "item"); if (item != window->item) - window->SetItem(item); + window->setItem(item); } void OBSBasicTransform::OBSSceneItemDeselect(void *param, calldata_t *data) @@ -170,7 +211,7 @@ void OBSBasicTransform::OBSSceneItemDeselect(void *param, calldata_t *data) if (item == window->item) { window->setWindowTitle(QTStr("Basic.TransformWindow.NoSelectedSource")); - window->SetItem(FindASelectedItem(scene)); + window->setItem(FindASelectedItem(scene)); } } @@ -179,23 +220,23 @@ void OBSBasicTransform::OBSSceneItemLocked(void *param, calldata_t *data) OBSBasicTransform *window = static_cast(param); bool locked = calldata_bool(data, "locked"); - QMetaObject::invokeMethod(window, "SetEnabled", Q_ARG(bool, !locked)); + QMetaObject::invokeMethod(window, "setEnabled", Q_ARG(bool, !locked)); } -static const uint32_t listToAlign[] = {OBS_ALIGN_TOP | OBS_ALIGN_LEFT, - OBS_ALIGN_TOP, - OBS_ALIGN_TOP | OBS_ALIGN_RIGHT, - OBS_ALIGN_LEFT, - OBS_ALIGN_CENTER, - OBS_ALIGN_RIGHT, - OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT, - OBS_ALIGN_BOTTOM, - OBS_ALIGN_BOTTOM | OBS_ALIGN_RIGHT}; - -static int AlignToList(uint32_t align) +static const uint32_t indexToAlign[] = {OBS_ALIGN_TOP | OBS_ALIGN_LEFT, + OBS_ALIGN_TOP, + OBS_ALIGN_TOP | OBS_ALIGN_RIGHT, + OBS_ALIGN_LEFT, + OBS_ALIGN_CENTER, + OBS_ALIGN_RIGHT, + OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT, + OBS_ALIGN_BOTTOM, + OBS_ALIGN_BOTTOM | OBS_ALIGN_RIGHT}; + +static int alignToIndex(uint32_t align) { int index = 0; - for (uint32_t curAlign : listToAlign) { + for (uint32_t curAlign : indexToAlign) { if (curAlign == align) return index; @@ -205,14 +246,14 @@ static int AlignToList(uint32_t align) return 0; } -void OBSBasicTransform::RefreshControls() +void OBSBasicTransform::refreshControls() { if (!item) return; - obs_transform_info osi; + obs_transform_info oti; obs_sceneitem_crop crop; - obs_sceneitem_get_info2(item, &osi); + obs_sceneitem_get_info2(item, &oti); obs_sceneitem_get_crop(item, &crop); obs_source_t *source = obs_sceneitem_get_source(item); @@ -221,26 +262,26 @@ void OBSBasicTransform::RefreshControls() float width = float(source_cx); float height = float(source_cy); - int alignIndex = AlignToList(osi.alignment); - int boundsAlignIndex = AlignToList(osi.bounds_alignment); + int alignIndex = alignToIndex(oti.alignment); + int boundsAlignIndex = alignToIndex(oti.bounds_alignment); ignoreItemChange = true; - ui->positionX->setValue(osi.pos.x); - ui->positionY->setValue(osi.pos.y); - ui->rotation->setValue(osi.rot); - ui->sizeX->setValue(osi.scale.x * width); - ui->sizeY->setValue(osi.scale.y * height); - ui->align->setCurrentIndex(alignIndex); + ui->positionX->setValue(oti.pos.x); + ui->positionY->setValue(oti.pos.y); + ui->rotation->setValue(oti.rot); + ui->sizeX->setValue(oti.scale.x * width); + ui->sizeY->setValue(oti.scale.y * height); + positionAlignment->setCurrentIndex(alignIndex); bool valid_size = source_cx != 0 && source_cy != 0; ui->sizeX->setEnabled(valid_size); ui->sizeY->setEnabled(valid_size); - ui->boundsType->setCurrentIndex(int(osi.bounds_type)); - ui->boundsAlign->setCurrentIndex(boundsAlignIndex); - ui->boundsWidth->setValue(osi.bounds.x); - ui->boundsHeight->setValue(osi.bounds.y); - ui->cropToBounds->setChecked(osi.crop_to_bounds); + ui->boundsType->setCurrentIndex(int(oti.bounds_type)); + boundsAlignment->setCurrentIndex(boundsAlignIndex); + ui->boundsWidth->setValue(oti.bounds.x); + ui->boundsHeight->setValue(oti.bounds.y); + ui->cropToBounds->setChecked(oti.crop_to_bounds); ui->cropLeft->setValue(int(crop.left)); ui->cropRight->setValue(int(crop.right)); @@ -252,7 +293,45 @@ void OBSBasicTransform::RefreshControls() setWindowTitle(QTStr("Basic.TransformWindow.Title").arg(name.c_str())); } -void OBSBasicTransform::OnBoundsType(int index) +void OBSBasicTransform::onAlignChanged(int index) +{ + uint32_t alignment = indexToAlign[index]; + + vec2 flipRatio = getAlignmentConversion(alignment); + + obs_transform_info oti; + obs_sceneitem_crop crop; + obs_sceneitem_get_info2(item, &oti); + obs_sceneitem_get_crop(item, &crop); + + obs_source_t *source = obs_sceneitem_get_source(item); + uint32_t sourceWidth = obs_source_get_width(source); + uint32_t sourceHeight = obs_source_get_height(source); + + uint32_t widthForFlip = sourceWidth - crop.left - crop.right; + uint32_t heightForFlip = sourceHeight - crop.top - crop.bottom; + + if (oti.bounds_type != OBS_BOUNDS_NONE) { + widthForFlip = oti.bounds.x; + heightForFlip = oti.bounds.y; + } + + vec2 currentRatio = getAlignmentConversion(oti.alignment); + + float shiftX = (currentRatio.x - flipRatio.x) * widthForFlip * oti.scale.x; + float shiftY = (currentRatio.y - flipRatio.y) * heightForFlip * oti.scale.y; + + bool previousIgnoreState = ignoreItemChange; + + ignoreItemChange = true; + ui->positionX->setValue(oti.pos.x - shiftX); + ui->positionY->setValue(oti.pos.y - shiftY); + ignoreItemChange = previousIgnoreState; + + onControlChanged(); +} + +void OBSBasicTransform::onBoundsType(int index) { if (index == -1) return; @@ -260,10 +339,13 @@ void OBSBasicTransform::OnBoundsType(int index) obs_bounds_type type = (obs_bounds_type)index; bool enable = (type != OBS_BOUNDS_NONE); - ui->boundsAlign->setEnabled(enable); + boundsAlignment->setEnabled(enable && type != OBS_BOUNDS_STRETCH); ui->boundsWidth->setEnabled(enable); ui->boundsHeight->setEnabled(enable); - ui->cropToBounds->setEnabled(enable); + + bool isCoverBounds = type == OBS_BOUNDS_SCALE_OUTER || type == OBS_BOUNDS_SCALE_TO_WIDTH || + type == OBS_BOUNDS_SCALE_TO_HEIGHT; + ui->cropToBounds->setEnabled(isCoverBounds); if (!ignoreItemChange) { obs_bounds_type lastType = obs_sceneitem_get_bounds_type(item); @@ -272,15 +354,56 @@ void OBSBasicTransform::OnBoundsType(int index) int width = (int)obs_source_get_width(source); int height = (int)obs_source_get_height(source); - ui->boundsWidth->setValue(width); - ui->boundsHeight->setValue(height); + vec2 scale; + obs_sceneitem_get_scale(item, &scale); + + obs_sceneitem_crop crop; + obs_sceneitem_get_crop(item, &crop); + + ui->sizeX->setValue(width); + ui->sizeY->setValue(height); + + ui->boundsWidth->setValue((width - crop.left - crop.right) * scale.x); + ui->boundsHeight->setValue((height - crop.top - crop.bottom) * scale.y); + } else if (type == OBS_BOUNDS_NONE) { + OBSSource source = obs_sceneitem_get_source(item); + int width = (int)obs_source_get_width(source); + int height = (int)obs_source_get_height(source); + + matrix4 draw; + obs_sceneitem_get_draw_transform(item, &draw); + + ui->sizeX->setValue(width * draw.x.x); + ui->sizeY->setValue(height * draw.y.y); + + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + // We use the draw transform values here which is always a top left coordinate origin. + vec2 currentRatio = getAlignmentConversion(OBS_ALIGN_TOP | OBS_ALIGN_LEFT); + vec2 flipRatio = getAlignmentConversion(oti.alignment); + + float drawX = draw.t.x; + float drawY = draw.t.y; + + obs_sceneitem_crop crop; + obs_sceneitem_get_crop(item, &crop); + + uint32_t widthForFlip = width - crop.left - crop.right; + uint32_t heightForFlip = height - crop.top - crop.bottom; + + float shiftX = (currentRatio.x - flipRatio.x) * (widthForFlip * draw.x.x); + float shiftY = (currentRatio.y - flipRatio.y) * (heightForFlip * draw.y.y); + + ui->positionX->setValue(oti.pos.x - (oti.pos.x - drawX) - shiftX); + ui->positionY->setValue(oti.pos.y - (oti.pos.y - drawY) - shiftY); } } - OnControlChanged(); + onControlChanged(); } -void OBSBasicTransform::OnControlChanged() +void OBSBasicTransform::onControlChanged() { if (ignoreItemChange) return; @@ -303,10 +426,10 @@ void OBSBasicTransform::OnControlChanged() oti.pos.x = float(ui->positionX->value()); oti.pos.y = float(ui->positionY->value()); oti.rot = float(ui->rotation->value()); - oti.alignment = listToAlign[ui->align->currentIndex()]; + oti.alignment = indexToAlign[positionAlignment->currentIndex()]; oti.bounds_type = (obs_bounds_type)ui->boundsType->currentIndex(); - oti.bounds_alignment = listToAlign[ui->boundsAlign->currentIndex()]; + oti.bounds_alignment = indexToAlign[boundsAlignment->currentIndex()]; oti.bounds.x = float(ui->boundsWidth->value()); oti.bounds.y = float(ui->boundsHeight->value()); oti.crop_to_bounds = ui->cropToBounds->isChecked(); @@ -316,7 +439,7 @@ void OBSBasicTransform::OnControlChanged() ignoreTransformSignal = false; } -void OBSBasicTransform::OnCropChanged() +void OBSBasicTransform::onCropChanged() { if (ignoreItemChange) return; @@ -332,11 +455,11 @@ void OBSBasicTransform::OnCropChanged() ignoreTransformSignal = false; } -void OBSBasicTransform::OnSceneChanged(QListWidgetItem *current, QListWidgetItem *) +void OBSBasicTransform::onSceneChanged(QListWidgetItem *current, QListWidgetItem *) { if (!current) return; OBSScene scene = GetOBSRef(current); - this->SetScene(scene); + this->setScene(scene); } diff --git a/frontend/dialogs/OBSBasicTransform.hpp b/frontend/dialogs/OBSBasicTransform.hpp index 22acfe8c5fb834..3d6778a8c165d5 100644 --- a/frontend/dialogs/OBSBasicTransform.hpp +++ b/frontend/dialogs/OBSBasicTransform.hpp @@ -3,8 +3,10 @@ #include "ui_OBSBasicTransform.h" #include +#include #include +#include class OBSBasic; class QListWidgetItem; @@ -15,6 +17,9 @@ class OBSBasicTransform : public QDialog { private: std::unique_ptr ui; + QPointer positionAlignment; + QPointer boundsAlignment; + OBSBasic *main; OBSSceneItem item; std::vector sigs; @@ -25,14 +30,14 @@ class OBSBasicTransform : public QDialog { bool ignoreItemChange = false; template - void HookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), + void hookWidget(Widget *widget, void (WidgetParent::*signal)(SignalArgs...), void (OBSBasicTransform::*slot)(SlotArgs...)) { QObject::connect(widget, signal, this, slot); } - void SetScene(OBSScene scene); - void SetItem(OBSSceneItem newItem); + void setScene(OBSScene scene); + void setItem(OBSSceneItem newItem); static void OBSSceneItemTransform(void *param, calldata_t *data); static void OBSSceneItemRemoved(void *param, calldata_t *data); @@ -41,17 +46,18 @@ class OBSBasicTransform : public QDialog { static void OBSSceneItemLocked(void *param, calldata_t *data); private slots: - void RefreshControls(); - void SetItemQt(OBSSceneItem newItem); - void OnBoundsType(int index); - void OnControlChanged(); - void OnCropChanged(); - void SetEnabled(bool enable); + void refreshControls(); + void setItemQt(OBSSceneItem newItem); + void onAlignChanged(int index); + void onBoundsType(int index); + void onControlChanged(); + void onCropChanged(); + void setEnabled(bool enable); public: OBSBasicTransform(OBSSceneItem item, OBSBasic *parent); ~OBSBasicTransform(); public slots: - void OnSceneChanged(QListWidgetItem *current, QListWidgetItem *prev); + void onSceneChanged(QListWidgetItem *current, QListWidgetItem *prev); }; diff --git a/frontend/forms/OBSBasicTransform.ui b/frontend/forms/OBSBasicTransform.ui index 6fb029bbbfa1d7..9f27bc5d9c21c8 100644 --- a/frontend/forms/OBSBasicTransform.ui +++ b/frontend/forms/OBSBasicTransform.ui @@ -6,28 +6,49 @@ 0 0 - 564 - 320 + 900 + 280 + + + 0 + 0 + + Basic.TransformWindow - + + + 0 + + + 0 + + + 0 + + + 0 + - - - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 4 - - - 4 + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + dialog-container + + + + 0 0 @@ -41,40 +62,21 @@ 0 - - - - - 0 - 0 - - - - - 170 - 0 - + + + + QFrame::NoFrame - - Basic.TransformWindow.Position + + QFrame::Plain - - Basic.TransformWindow.Position + + 0 - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - + + dialog-container dialog-frame - + 0 @@ -87,8 +89,40 @@ 0 - - + + 8 + + + 2 + + + + + Width + + + Basic.TransformWindow.Width + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Basic.TransformWindow.Accessible.PositionX + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + 120 @@ -97,18 +131,18 @@ - 100 + 120 16777215 - Basic.TransformWindow.PositionX + Width px - 4 + 2 -90001.000000000000000 @@ -116,10 +150,26 @@ 90001.000000000000000 + + 1.000000000000000 + - - + + + + Basic.TransformWindow.Rotation + + + Basic.TransformWindow.Rotation + + + subtitle + + + + + 120 @@ -128,18 +178,18 @@ - 100 + 120 16777215 - Basic.TransformWindow.PositionY + Basic.TransformWindow.PositionX px - 4 + 2 -90001.000000000000000 @@ -149,89 +199,14 @@ - - - - - - - Basic.TransformWindow.Rotation - - - Basic.TransformWindow.Rotation - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 100 - 16777215 - - - - Basic.TransformWindow.Rotation - - - ° - - - -360.000000000000000 - - - 360.000000000000000 - - - 0.100000000000000 - - - - - - - Basic.TransformWindow.Size - - - Basic.TransformWindow.Size - - - - - - - - 0 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - + + + + + 0 + 0 + + 120 @@ -240,18 +215,107 @@ - 100 + 120 16777215 - Basic.TransformWindow.Width + Basic.TransformWindow.Rotation + + + ° + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.100000000000000 + + + + + + + Basic.TransformWindow.Size + + + Basic.TransformWindow.Size + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + subtitle + + + + + + + Basic.TransformWindow.Position + + + Basic.TransformWindow.Position + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + subtitle + + + + + + + Qt::Horizontal + + + + 0 + 10 + + + + + + + + Basic.TransformWindow.Accessible.PositionY + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Basic.TransformWindow.PositionY px - 4 + 2 -90001.000000000000000 @@ -259,12 +323,9 @@ 90001.000000000000000 - - 1.000000000000000 - - + @@ -274,7 +335,7 @@ - 100 + 120 16777215 @@ -285,7 +346,7 @@ px - 4 + 2 -90001.000000000000000 @@ -298,226 +359,80 @@ + + + + Height + + + Basic.TransformWindow.Height + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Basic.TransformWindow.Alignment + + + Basic.TransformWindow.Alignment + + + subtitle + + + - - - - Basic.TransformWindow.Alignment - - - Basic.TransformWindow.Alignment - - - - - - - Basic.TransformWindow.Alignment - - - Basic.TransformWindow.Alignment.TopLeft - - - - Basic.TransformWindow.Alignment.TopLeft - - - - - Basic.TransformWindow.Alignment.TopCenter - - - - - Basic.TransformWindow.Alignment.TopRight - - - - - Basic.TransformWindow.Alignment.CenterLeft - - - - - Basic.TransformWindow.Alignment.Center - - - - - Basic.TransformWindow.Alignment.CenterRight - - - - - Basic.TransformWindow.Alignment.BottomLeft - - - - - Basic.TransformWindow.Alignment.BottomCenter - - - - - Basic.TransformWindow.Alignment.BottomRight - - - - - - - - Qt::Vertical - - - - 10 - 10 - - - - - - - - Basic.TransformWindow.BoundsType - - - Basic.TransformWindow.BoundsType - - - - - - - Basic.TransformWindow.BoundsType + + + + QFrame::NoFrame - - - Basic.TransformWindow.BoundsType.None - - - - - Basic.TransformWindow.BoundsType.Stretch - - - - - Basic.TransformWindow.BoundsType.ScaleInner - - - - - Basic.TransformWindow.BoundsType.ScaleOuter - - - - - Basic.TransformWindow.BoundsType.ScaleToWidth - - - - - Basic.TransformWindow.BoundsType.ScaleToHeight - - - - - Basic.TransformWindow.BoundsType.MaxOnly - - - - - - - - Basic.TransformWindow.BoundsAlignment - - - Basic.TransformWindow.BoundsAlignment - - - - - - - false - - - Basic.TransformWindow.BoundsAlignment - - - Basic.TransformWindow.Alignment.TopLeft - - - - Basic.TransformWindow.Alignment.TopLeft - - - - - Basic.TransformWindow.Alignment.TopCenter - - - - - Basic.TransformWindow.Alignment.TopRight - - - - - Basic.TransformWindow.Alignment.CenterLeft - - - - - Basic.TransformWindow.Alignment.Center - - - - - Basic.TransformWindow.Alignment.CenterRight - - - - - Basic.TransformWindow.Alignment.BottomLeft - - - - - Basic.TransformWindow.Alignment.BottomCenter - - - - - Basic.TransformWindow.Alignment.BottomRight - - - - - - - - Basic.TransformWindow.Bounds + + QFrame::Plain - - Basic.TransformWindow.Bounds + + 0 - - - - - - - 0 - 0 - + + dialog-container dialog-frame - + 0 @@ -530,7 +445,147 @@ 0 - + + 8 + + + 2 + + + + + Basic.TransformWindow.BoundsType + + + + Basic.TransformWindow.BoundsType.None + + + + + Basic.TransformWindow.BoundsType.Stretch + + + + + Basic.TransformWindow.BoundsType.ScaleInner + + + + + Basic.TransformWindow.BoundsType.ScaleOuter + + + + + Basic.TransformWindow.BoundsType.ScaleToWidth + + + + + Basic.TransformWindow.BoundsType.ScaleToHeight + + + + + Basic.TransformWindow.BoundsType.MaxOnly + + + + + + + + Qt::Horizontal + + + + 0 + 10 + + + + + + + + Basic.TransformWindow.Bounds + + + Basic.TransformWindow.Bounds + + + subtitle + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Basic.TransformWindow.Alignment + + + Basic.TransformWindow.Alignment + + + subtitle + + + + + + + Basic.TransformWindow.BoundsWidth + + + Basic.TransformWindow.Width + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Basic.TransformWindow.BoundsHeight + + + Basic.TransformWindow.Height + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + false @@ -543,7 +598,7 @@ - 100 + 120 16777215 @@ -554,7 +609,7 @@ px - 4 + 2 1.000000000000000 @@ -564,7 +619,7 @@ - + false @@ -577,7 +632,7 @@ - 100 + 120 16777215 @@ -588,7 +643,7 @@ px - 4 + 2 1.000000000000000 @@ -598,260 +653,281 @@ + + + + false + + + + 100 + 16777215 + + + + Basic.TransformWindow.BoundsAlignment + + + Basic.TransformWindow.CropToBounds + + + + + + - - - - Basic.TransformWindow.BoundsAlignment + + + + 0 - - Basic.TransformWindow.CropToBounds + + dialog-container dialog-frame + + + 0 + + + 0 + + + 0 + + + 0 + + + 8 + + + 2 + + + + + Basic.TransformWindow.CropLeft + + + Left + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Right + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Basic.TransformWindow.CropBottom + + + px + + + 100000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Basic.TransformWindow.CropRight + + + px + + + 100000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Basic.TransformWindow.CropLeft + + + px + + + 100000 + + + + + + + Top + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Basic.TransformWindow.CropTop + + + px + + + 100000 + + + + + + + Basic.TransformWindow.Crop + + + Basic.TransformWindow.Crop + + + subtitle + + + + + + + Bottom + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + - - - - Qt::Vertical - - - - 10 - 10 - - - - - - - - Basic.TransformWindow.Crop - - - Basic.TransformWindow.Crop + + + + QDialogButtonBox::Close|QDialogButtonBox::Reset - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Basic.TransformWindow.CropLeft - - - px - - - 100000 - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Basic.TransformWindow.CropRight - - - px - - - 100000 - - - - - - - Left - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - cropLeft - - - - - - - Bottom - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - cropBottom - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Basic.TransformWindow.CropTop - - - px - - - 100000 - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 100 - 16777215 - - - - Basic.TransformWindow.CropBottom - - - px - - - 100000 - - - - - - - Top - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - cropTop - - - - - - - Right - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - cropRight - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - - - - - Qt::Vertical - - - - 0 - 10 - - - - - - - - QDialogButtonBox::Close|QDialogButtonBox::Reset - - - + + positionX + positionY + sizeX + sizeY + rotation + boundsType + boundsWidth + boundsHeight + cropToBounds + cropLeft + cropRight + cropTop + cropBottom + @@ -862,7 +938,7 @@ 20 - 20 + 358 20 diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index f366205263cd64..0061bb47572464 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -21,6 +21,7 @@ #include "OBSMainWindow.hpp" #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include +#include #include #include diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp index a4db6d5ddc62db..1d08784796b7c3 100644 --- a/frontend/widgets/OBSBasic_MainControls.cpp +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -488,7 +488,7 @@ void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) if (transformWindow) transformWindow->close(); transformWindow = new OBSBasicTransform(item, this); - connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::onSceneChanged); transformWindow->show(); transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); }