Add "Source interact" action

This commit is contained in:
WarmUpTill 2026-04-02 12:07:03 +02:00
parent 254da3b5f1
commit ba99c9e1bf
14 changed files with 1622 additions and 1 deletions

View File

@ -971,6 +971,49 @@ AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.source.refresh="Refresh"
AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value."
AdvSceneSwitcher.action.source.dialog.accept="Accept changes"
AdvSceneSwitcher.action.sourceInteraction="Source Interaction"
AdvSceneSwitcher.action.sourceInteraction.source="Source"
AdvSceneSwitcher.action.sourceInteraction.noSelection="Select a step to edit it"
AdvSceneSwitcher.action.sourceInteraction.record="Record interaction ..."
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove="Mouse move (%1, %2)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick="Mouse %1 %2 (%3, %4)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount=" x%1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel="Mouse wheel (%1, %2) dx=%3 dy=%4"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress="Key %1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText="Key %1 '%2'"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText="Type \"%1\""
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait="Wait %1 ms"
AdvSceneSwitcher.action.sourceInteraction.step.edit.type="Type"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove="Mouse move"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick="Mouse click"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel="Mouse wheel"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress="Key press"
AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText="Type text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wait="Wait"
AdvSceneSwitcher.action.sourceInteraction.step.edit.x="X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.y="Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.button="Button"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp="Mouse up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount="Click count"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx="Delta X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy="Delta Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey="Virtual key"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp="Key up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.text="Text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs="Duration (ms)"
AdvSceneSwitcher.action.sourceInteraction.button.left="Left"
AdvSceneSwitcher.action.sourceInteraction.button.middle="Middle"
AdvSceneSwitcher.action.sourceInteraction.button.right="Right"
AdvSceneSwitcher.action.sourceInteraction.record.title="Record Source Interactions"
AdvSceneSwitcher.action.sourceInteraction.record.start="Start recording"
AdvSceneSwitcher.action.sourceInteraction.record.stop="Stop recording"
AdvSceneSwitcher.action.sourceInteraction.record.accept="Accept"
AdvSceneSwitcher.action.sourceInteraction.record.placeholder="Start recording and interact with the source to capture steps"
AdvSceneSwitcher.action.sourceInteraction.record.invalidSource="No valid source is selected.\nPlease select a source that supports interaction before recording."
AdvSceneSwitcher.action.media="Media"
AdvSceneSwitcher.action.media.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause"

View File

@ -57,6 +57,8 @@ target_sources(
macro-action-sequence.hpp
macro-action-source.cpp
macro-action-source.hpp
macro-action-source-interaction.cpp
macro-action-source-interaction.hpp
macro-action-streaming.cpp
macro-action-streaming.hpp
macro-action-studio-mode.cpp
@ -144,7 +146,17 @@ target_sources(
target_sources(
${PROJECT_NAME}
PRIVATE utils/audio-helpers.cpp
PRIVATE utils/source-interaction-recorder.cpp
utils/source-interaction-recorder.hpp
utils/source-interaction-step.cpp
utils/source-interaction-step.hpp
utils/source-interaction-step-edit.cpp
utils/source-interaction-step-edit.hpp
utils/source-interaction-step-list.cpp
utils/source-interaction-step-list.hpp
utils/source-preview-widget.cpp
utils/source-preview-widget.hpp
utils/audio-helpers.cpp
utils/audio-helpers.hpp
utils/connection-manager.cpp
utils/connection-manager.hpp

View File

@ -0,0 +1,286 @@
#include "macro-action-source-interaction.hpp"
#include "source-interaction-recorder.hpp"
#include "layout-helpers.hpp"
#include "selection-helpers.hpp"
#include "sync-helpers.hpp"
#include "log-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout>
namespace advss {
const std::string MacroActionSourceInteraction::id = "source_interaction";
bool MacroActionSourceInteraction::_registered = MacroActionFactory::Register(
MacroActionSourceInteraction::id,
{MacroActionSourceInteraction::Create,
MacroActionSourceInteractionEdit::Create,
"AdvSceneSwitcher.action.sourceInteraction"});
bool MacroActionSourceInteraction::PerformAction()
{
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source.GetSource());
if (!source) {
blog(LOG_WARNING, "source interaction: source not found");
return true;
}
uint32_t flags = obs_source_get_output_flags(source);
if (!(flags & OBS_SOURCE_INTERACTION)) {
blog(LOG_WARNING,
"source interaction: source \"%s\" does not support interaction",
obs_source_get_name(source));
return true;
}
for (const auto &step : _steps) {
PerformInteractionStep(source, step);
}
return true;
}
void MacroActionSourceInteraction::LogAction() const
{
ablog(LOG_INFO, "performed source interaction on \"%s\" (%d steps)",
_source.ToString(true).c_str(), (int)_steps.size());
}
bool MacroActionSourceInteraction::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_source.Save(obj);
OBSDataArrayAutoRelease arr = obs_data_array_create();
for (const auto &step : _steps) {
OBSDataAutoRelease stepObj = obs_data_create();
step.Save(stepObj);
obs_data_array_push_back(arr, stepObj);
}
obs_data_set_array(obj, "steps", arr);
return true;
}
bool MacroActionSourceInteraction::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_source.Load(obj);
_steps.clear();
OBSDataArrayAutoRelease arr = obs_data_get_array(obj, "steps");
size_t count = obs_data_array_count(arr);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease stepObj = obs_data_array_item(arr, i);
SourceInteractionStep step;
step.Load(stepObj);
_steps.push_back(step);
}
return true;
}
std::string MacroActionSourceInteraction::GetShortDesc() const
{
return _source.ToString();
}
void MacroActionSourceInteraction::ResolveVariablesToFixedValues()
{
for (auto &step : _steps) {
step.x.ResolveVariables();
step.y.ResolveVariables();
step.clickCount.ResolveVariables();
step.wheelDeltaX.ResolveVariables();
step.wheelDeltaY.ResolveVariables();
step.nativeVkey.ResolveVariables();
step.text.ResolveVariables();
step.waitMs.ResolveVariables();
}
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Create(Macro *m)
{
return std::make_shared<MacroActionSourceInteraction>(m);
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Copy() const
{
return std::make_shared<MacroActionSourceInteraction>(*this);
}
static QStringList getInteractableSourceNames()
{
QStringList names;
obs_enum_sources(
[](void *param, obs_source_t *source) -> bool {
uint32_t flags = obs_source_get_output_flags(source);
if (flags & OBS_SOURCE_INTERACTION) {
auto list = static_cast<QStringList *>(param);
const char *name = obs_source_get_name(source);
if (name) {
list->append(QString(name));
}
}
return true;
},
&names);
names.sort();
return names;
}
MacroActionSourceInteractionEdit::MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData)
: QWidget(parent),
_sources(new SourceSelectionWidget(this, getInteractableSourceNames,
true)),
_stepList(new SourceInteractionStepList(this)),
_recordButton(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record"))),
_noSelectionLabel(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.noSelection")))
{
_stepList->AddControlWidget(_recordButton);
auto sourceRow = new QHBoxLayout;
sourceRow->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.source")));
sourceRow->addWidget(_sources);
sourceRow->addStretch();
_noSelectionLabel->setAlignment(Qt::AlignCenter);
_noSelectionLabel->hide();
auto mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(sourceRow);
mainLayout->addWidget(_stepList);
mainLayout->addWidget(_noSelectionLabel);
setLayout(mainLayout);
connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this,
SLOT(SourceChanged(const SourceSelection &)));
connect(_stepList, &SourceInteractionStepList::StepsChanged, this,
&MacroActionSourceInteractionEdit::OnStepsChanged);
connect(_stepList, &SourceInteractionStepList::RowSelected, this,
&MacroActionSourceInteractionEdit::SetCurrentStepEditor);
connect(_recordButton, &QPushButton::clicked, this,
&MacroActionSourceInteractionEdit::OpenRecorder);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionSourceInteractionEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_sources->SetSource(_entryData->_source);
_stepList->SetSteps(_entryData->_steps);
_noSelectionLabel->setVisible(_stepList->count() > 0);
}
void MacroActionSourceInteractionEdit::SourceChanged(
const SourceSelection &source)
{
GUARD_LOADING_AND_LOCK();
_entryData->_source = source;
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionSourceInteractionEdit::OnStepsChanged(
const std::vector<SourceInteractionStep> &steps)
{
GUARD_LOADING_AND_LOCK();
_entryData->_steps = steps;
}
void MacroActionSourceInteractionEdit::StepChanged(
const SourceInteractionStep &step)
{
GUARD_LOADING_AND_LOCK();
int row = _stepList->CurrentRow();
if (row < 0 || row >= (int)_entryData->_steps.size()) {
return;
}
_entryData->_steps[row] = step;
_stepList->UpdateStep(row, step);
}
void MacroActionSourceInteractionEdit::OpenRecorder()
{
if (!_entryData) {
return;
}
OBSSourceAutoRelease source =
obs_weak_source_get_source(_entryData->_source.GetSource());
if (!source) {
QMessageBox::warning(
this,
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"),
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.invalidSource"));
return;
}
auto dlg = new SourceInteractionRecorder(
window(), _entryData->_source.GetSource());
connect(dlg, &SourceInteractionRecorder::StepsRecorded, this,
&MacroActionSourceInteractionEdit::AcceptRecorded);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->show();
}
void MacroActionSourceInteractionEdit::AcceptRecorded(
const std::vector<SourceInteractionStep> &steps)
{
if (!_entryData) {
return;
}
{
auto lock = _entryData->Lock();
_entryData->_steps.insert(_entryData->_steps.end(),
steps.begin(), steps.end());
}
_stepList->SetSteps(_entryData->_steps);
}
void MacroActionSourceInteractionEdit::SetCurrentStepEditor(int row)
{
if (_stepEditor) {
delete _stepEditor;
_stepEditor = nullptr;
}
if (!_entryData || row < 0 || row >= (int)_entryData->_steps.size()) {
_noSelectionLabel->setVisible(_stepList->count() > 0);
return;
}
_noSelectionLabel->setVisible(false);
_stepEditor =
new SourceInteractionStepEdit(this, _entryData->_steps[row]);
static_cast<QVBoxLayout *>(layout())->addWidget(_stepEditor);
connect(_stepEditor, &SourceInteractionStepEdit::StepChanged, this,
&MacroActionSourceInteractionEdit::StepChanged);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@ -0,0 +1,78 @@
#pragma once
#include "macro-action-edit.hpp"
#include "source-selection.hpp"
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-interaction-step-edit.hpp"
#include <QLabel>
#include <QPushButton>
namespace advss {
class MacroActionSourceInteraction : public MacroAction {
public:
MacroActionSourceInteraction(Macro *m) : MacroAction(m) {}
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
bool PerformAction();
void LogAction() const;
void ResolveVariablesToFixedValues() override;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; }
std::string GetShortDesc() const;
SourceSelection _source;
std::vector<SourceInteractionStep> _steps;
private:
static bool _registered;
static const std::string id;
};
class MacroActionSourceInteractionEdit : public QWidget {
Q_OBJECT
public:
MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData =
nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionSourceInteractionEdit(
parent,
std::dynamic_pointer_cast<MacroActionSourceInteraction>(
action));
}
private slots:
void SourceChanged(const SourceSelection &);
void OnStepsChanged(const std::vector<SourceInteractionStep> &);
void StepChanged(const SourceInteractionStep &);
void OpenRecorder();
void AcceptRecorded(const std::vector<SourceInteractionStep> &);
signals:
void HeaderInfoChanged(const QString &);
private:
void SetCurrentStepEditor(int row);
SourceSelectionWidget *_sources;
SourceInteractionStepList *_stepList;
QPushButton *_recordButton;
SourceInteractionStepEdit *_stepEditor = nullptr;
QLabel *_noSelectionLabel;
std::shared_ptr<MacroActionSourceInteraction> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@ -0,0 +1,237 @@
#include "source-interaction-recorder.hpp"
#include "obs-module-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
namespace advss {
SourceInteractionRecorder::SourceInteractionRecorder(QWidget *parent,
obs_weak_source_t *source)
: QDialog(parent),
_source(source),
_preview(new SourcePreviewWidget(this, source)),
_stepList(new SourceInteractionStepList(this)),
_startStopButton(new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"),
this))
{
setWindowTitle(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"));
_preview->setMouseTracking(true);
_preview->setFocusPolicy(Qt::StrongFocus);
_preview->installEventFilter(this);
_stepList->HideControls();
_stepList->SetMinListHeight(50);
auto buttonRow = new QHBoxLayout;
buttonRow->addWidget(_startStopButton);
buttonRow->addStretch();
auto acceptButton = new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.accept"),
this);
buttonRow->addWidget(acceptButton);
auto layout = new QVBoxLayout(this);
layout->addWidget(_preview, 1);
layout->addWidget(_stepList);
layout->addLayout(buttonRow);
setLayout(layout);
connect(_startStopButton, &QPushButton::clicked, this,
&SourceInteractionRecorder::StartStop);
connect(acceptButton, &QPushButton::clicked, this, [this]() {
if (_recording) {
StartStop();
}
emit StepsRecorded(_steps);
accept();
});
}
SourceInteractionRecorder::~SourceInteractionRecorder() {}
void SourceInteractionRecorder::StartStop()
{
_recording = !_recording;
if (_recording) {
_steps.clear();
_stepList->Clear();
_firstEvent = true;
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.stop"));
} else {
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"));
}
}
void SourceInteractionRecorder::AppendStep(const SourceInteractionStep &step)
{
if (!_firstEvent) {
auto now = std::chrono::steady_clock::now();
int ms = static_cast<int>(
std::chrono::duration_cast<std::chrono::milliseconds>(
now - _lastEventTime)
.count());
if (ms > 10) {
SourceInteractionStep wait;
wait.type = SourceInteractionStep::Type::WAIT;
wait.waitMs.SetValue(ms);
_steps.push_back(wait);
_stepList->Insert(
QString::fromStdString(wait.ToString()));
}
}
_firstEvent = false;
_lastEventTime = std::chrono::steady_clock::now();
_steps.push_back(step);
_stepList->Insert(QString::fromStdString(step.ToString()));
_stepList->ScrollToBottom();
}
bool SourceInteractionRecorder::eventFilter(QObject *obj, QEvent *event)
{
if (obj != _preview || !_recording) {
return QDialog::eventFilter(obj, event);
}
switch (event->type()) {
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
return HandleMouseClick(static_cast<QMouseEvent *>(event));
case QEvent::MouseMove:
return HandleMouseMove(static_cast<QMouseEvent *>(event));
case QEvent::Wheel:
return HandleMouseWheel(static_cast<QWheelEvent *>(event));
case QEvent::KeyPress:
case QEvent::KeyRelease:
return HandleKeyEvent(static_cast<QKeyEvent *>(event));
case QEvent::FocusIn:
case QEvent::FocusOut: {
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_send_focus(source,
event->type() == QEvent::FocusIn);
}
return true;
}
default:
break;
}
return QDialog::eventFilter(obj, event);
}
bool SourceInteractionRecorder::HandleMouseClick(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_CLICK;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
step.mouseUp = (event->type() == QEvent::MouseButtonRelease);
step.clickCount.SetValue(
(event->type() == QEvent::MouseButtonDblClick) ? 2 : 1);
switch (event->button()) {
case Qt::LeftButton:
step.button = MOUSE_LEFT;
break;
case Qt::MiddleButton:
step.button = MOUSE_MIDDLE;
break;
case Qt::RightButton:
step.button = MOUSE_RIGHT;
break;
default:
return false;
}
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseMove(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_MOVE;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
// Always forward moves to the source so hover/cursor effects work,
// but only record them when a button is held (drag), to reduce noise.
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
if (event->buttons() != Qt::NoButton) {
AppendStep(step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseWheel(QWheelEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_WHEEL;
int srcX = 0, srcY = 0;
const QPointF pos = event->position();
_preview->GetSourceRelativeXY((int)pos.x(), (int)pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
const QPoint angle = event->angleDelta();
step.wheelDeltaX.SetValue(angle.x());
step.wheelDeltaY.SetValue(angle.y());
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleKeyEvent(QKeyEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::KEY_PRESS;
step.keyUp = (event->type() == QEvent::KeyRelease);
step.nativeVkey.SetValue(static_cast<int>(event->nativeVirtualKey()));
step.text = event->text().toStdString();
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
} // namespace advss

View File

@ -0,0 +1,54 @@
#pragma once
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-preview-widget.hpp"
#include <chrono>
#include <vector>
#include <QDialog>
#include <QPushButton>
class QKeyEvent;
class QMouseEvent;
class QWheelEvent;
namespace advss {
class SourceInteractionRecorder : public QDialog {
Q_OBJECT
public:
SourceInteractionRecorder(QWidget *parent, obs_weak_source_t *source);
~SourceInteractionRecorder();
const std::vector<SourceInteractionStep> &GetSteps() const
{
return _steps;
}
signals:
void StepsRecorded(const std::vector<SourceInteractionStep> &);
private slots:
void StartStop();
private:
bool eventFilter(QObject *obj, QEvent *event) override;
bool HandleMouseClick(QMouseEvent *);
bool HandleMouseMove(QMouseEvent *);
bool HandleMouseWheel(QWheelEvent *);
bool HandleKeyEvent(QKeyEvent *);
void AppendStep(const SourceInteractionStep &);
obs_weak_source_t *_source;
SourcePreviewWidget *_preview;
SourceInteractionStepList *_stepList;
QPushButton *_startStopButton;
bool _recording = false;
std::vector<SourceInteractionStep> _steps;
std::chrono::steady_clock::time_point _lastEventTime;
bool _firstEvent = true;
};
} // namespace advss

View File

@ -0,0 +1,315 @@
#include "source-interaction-step-edit.hpp"
#include "obs-module-helper.hpp"
#include "variable-line-edit.hpp"
#include "variable-spinbox.hpp"
#include <map>
#include <string>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QVBoxLayout>
namespace advss {
static const std::map<SourceInteractionStep::Type, std::string> stepTypeNames = {
{SourceInteractionStep::Type::MOUSE_MOVE,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove"},
{SourceInteractionStep::Type::MOUSE_CLICK,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick"},
{SourceInteractionStep::Type::MOUSE_WHEEL,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel"},
{SourceInteractionStep::Type::KEY_PRESS,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress"},
{SourceInteractionStep::Type::TYPE_TEXT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText"},
{SourceInteractionStep::Type::WAIT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wait"},
};
static const std::map<obs_mouse_button_type, std::string> mouseButtonNames = {
{MOUSE_LEFT, "AdvSceneSwitcher.action.sourceInteraction.button.left"},
{MOUSE_MIDDLE,
"AdvSceneSwitcher.action.sourceInteraction.button.middle"},
{MOUSE_RIGHT, "AdvSceneSwitcher.action.sourceInteraction.button.right"},
};
static void populateTypeCombo(QComboBox *combo)
{
for (const auto &[type, name] : stepTypeNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(type));
}
}
static void populateButtonCombo(QComboBox *combo)
{
for (const auto &[btn, name] : mouseButtonNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(btn));
}
}
static QHBoxLayout *labelledRow(const char *labelKey, QWidget *w)
{
auto row = new QHBoxLayout;
row->addWidget(new QLabel(obs_module_text(labelKey)));
row->addWidget(w);
row->addStretch();
return row;
}
SourceInteractionStepEdit::SourceInteractionStepEdit(
QWidget *parent, const SourceInteractionStep &step)
: QWidget(parent),
_typeCombo(new QComboBox(this)),
_fields(new QStackedWidget(this)),
_step(step)
{
populateTypeCombo(_typeCombo);
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
auto typeRow = labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.type",
_typeCombo);
mainLayout->addLayout(typeRow);
mainLayout->addWidget(_fields);
setLayout(mainLayout);
connect(_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SourceInteractionStepEdit::TypeChanged);
RebuildFields();
int idx = _typeCombo->findData(static_cast<int>(_step.type));
if (idx >= 0) {
_typeCombo->setCurrentIndex(idx);
}
}
void SourceInteractionStepEdit::TypeChanged(int)
{
_step.type = static_cast<SourceInteractionStep::Type>(
_typeCombo->currentData().toInt());
RebuildFields();
emit StepChanged(_step);
}
void SourceInteractionStepEdit::UpdateStep()
{
emit StepChanged(_step);
}
void SourceInteractionStepEdit::RebuildFields()
{
while (_fields->count() > 0) {
auto w = _fields->widget(0);
_fields->removeWidget(w);
delete w;
}
auto makeVarSpin = [](int min, int max,
const NumberVariable<int> &val) {
auto sb = new VariableSpinBox;
sb->setMinimum(min);
sb->setMaximum(max);
sb->SetValue(val);
return sb;
};
switch (_step.type) {
case SourceInteractionStep::Type::MOUSE_MOVE:
case SourceInteractionStep::Type::MOUSE_CLICK:
case SourceInteractionStep::Type::MOUSE_WHEEL: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto xSpin = makeVarSpin(-32768, 32767, _step.x);
auto ySpin = makeVarSpin(-32768, 32767, _step.y);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.x",
xSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.y",
ySpin));
connect(xSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, xSpin](const NumberVariable<int> &) {
_step.x = xSpin->Value();
UpdateStep();
});
connect(ySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, ySpin](const NumberVariable<int> &) {
_step.y = ySpin->Value();
UpdateStep();
});
if (_step.type == SourceInteractionStep::Type::MOUSE_CLICK) {
auto btnCombo = new QComboBox;
populateButtonCombo(btnCombo);
btnCombo->setCurrentIndex(btnCombo->findData(
static_cast<int>(_step.button)));
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp"));
upCheck->setChecked(_step.mouseUp);
auto countSpin = makeVarSpin(1, 3, _step.clickCount);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.button",
btnCombo));
layout->addWidget(upCheck);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount",
countSpin));
connect(btnCombo,
QOverload<int>::of(
&QComboBox::currentIndexChanged),
this, [this, btnCombo]() {
_step.button = static_cast<
obs_mouse_button_type>(
btnCombo->currentData().toInt());
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.mouseUp = upCheck->isChecked();
UpdateStep();
});
connect(countSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, countSpin](const NumberVariable<int> &) {
_step.clickCount = countSpin->Value();
UpdateStep();
});
} else if (_step.type ==
SourceInteractionStep::Type::MOUSE_WHEEL) {
auto dxSpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaX);
auto dySpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaY);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx",
dxSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy",
dySpin));
connect(dxSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dxSpin](const NumberVariable<int> &) {
_step.wheelDeltaX = dxSpin->Value();
UpdateStep();
});
connect(dySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dySpin](const NumberVariable<int> &) {
_step.wheelDeltaY = dySpin->Value();
UpdateStep();
});
}
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::KEY_PRESS: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto vkeySpin = makeVarSpin(0, 0xFFFF, _step.nativeVkey);
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp"));
upCheck->setChecked(_step.keyUp);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey",
vkeySpin));
layout->addWidget(upCheck);
connect(vkeySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, vkeySpin](const NumberVariable<int> &) {
_step.nativeVkey = vkeySpin->Value();
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.keyUp = upCheck->isChecked();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::TYPE_TEXT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto textEdit = new VariableLineEdit(page);
textEdit->setText(_step.text);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.text",
textEdit));
connect(textEdit, &QLineEdit::editingFinished, this,
[this, textEdit]() {
_step.text = textEdit->text().toStdString();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::WAIT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto msSpin = makeVarSpin(0, 60000, _step.waitMs);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs",
msSpin));
connect(msSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, msSpin](const NumberVariable<int> &) {
_step.waitMs = msSpin->Value();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
}
}
} // namespace advss

View File

@ -0,0 +1,32 @@
#pragma once
#include "source-interaction-step.hpp"
#include <QComboBox>
#include <QStackedWidget>
#include <QWidget>
namespace advss {
class SourceInteractionStepEdit : public QWidget {
Q_OBJECT
public:
SourceInteractionStepEdit(QWidget *parent,
const SourceInteractionStep &step);
SourceInteractionStep GetStep() const { return _step; }
signals:
void StepChanged(const SourceInteractionStep &);
private slots:
void TypeChanged(int);
void UpdateStep();
private:
void RebuildFields();
QComboBox *_typeCombo;
QStackedWidget *_fields;
SourceInteractionStep _step;
};
} // namespace advss

View File

@ -0,0 +1,99 @@
#include "source-interaction-step-list.hpp"
#include "obs-module-helper.hpp"
#include <QTimer>
namespace advss {
SourceInteractionStepList::SourceInteractionStepList(QWidget *parent)
: ListEditor(parent)
{
_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(_list, &QListWidget::currentRowChanged, this,
&SourceInteractionStepList::RowSelected);
SetPlaceholderText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.placeholder"));
SetMaxListHeight(350);
}
void SourceInteractionStepList::SetSteps(
const std::vector<SourceInteractionStep> &steps)
{
_steps = steps;
_list->clear();
for (const auto &step : _steps) {
_list->addItem(QString::fromStdString(step.ToString()));
}
UpdateListSize();
_list->setCurrentRow(-1);
}
void SourceInteractionStepList::UpdateStep(int row,
const SourceInteractionStep &step)
{
if (row < 0 || row >= (int)_steps.size()) {
return;
}
_steps[row] = step;
_list->item(row)->setText(QString::fromStdString(step.ToString()));
}
void SourceInteractionStepList::Add()
{
_steps.emplace_back();
_list->addItem(QString::fromStdString(_steps.back().ToString()));
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
emit StepsChanged(_steps);
_list->setCurrentRow((int)_steps.size() - 1);
}
void SourceInteractionStepList::Remove()
{
const QList<QListWidgetItem *> selected = _list->selectedItems();
if (selected.isEmpty()) {
return;
}
std::vector<int> rows;
rows.reserve(selected.size());
for (const auto *item : selected) {
rows.push_back(_list->row(item));
}
std::sort(rows.begin(), rows.end(), std::greater<int>());
for (int row : rows) {
if (row >= 0 && row < (int)_steps.size()) {
_steps.erase(_steps.begin() + row);
delete _list->takeItem(row);
}
}
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
emit StepsChanged(_steps);
}
void SourceInteractionStepList::Up()
{
int row = _list->currentRow();
if (row <= 0 || row >= (int)_steps.size()) {
return;
}
std::swap(_steps[row], _steps[row - 1]);
_list->insertItem(row - 1, _list->takeItem(row));
_list->setCurrentRow(row - 1);
emit StepsChanged(_steps);
}
void SourceInteractionStepList::Down()
{
int row = _list->currentRow();
if (row < 0 || row >= (int)_steps.size() - 1) {
return;
}
std::swap(_steps[row], _steps[row + 1]);
_list->insertItem(row + 1, _list->takeItem(row));
_list->setCurrentRow(row + 1);
emit StepsChanged(_steps);
}
} // namespace advss

View File

@ -0,0 +1,38 @@
#pragma once
#include "list-editor.hpp"
#include "source-interaction-step.hpp"
#include <vector>
namespace advss {
class SourceInteractionStepList : public ListEditor {
Q_OBJECT
public:
SourceInteractionStepList(QWidget *parent = nullptr);
void SetSteps(const std::vector<SourceInteractionStep> &steps);
void UpdateStep(int row, const SourceInteractionStep &step);
int CurrentRow() const { return _list->currentRow(); }
void SetCurrentRow(int row) { _list->setCurrentRow(row); }
void Clear() const { _list->clear(); }
void HideControls() const { _controls->hide(); }
void Insert(const QString &value) const { _list->addItem(value); }
void ScrollToBottom() const { _list->scrollToBottom(); }
void AddControlWidget(QWidget *widget) { _controls->AddWidget(widget); }
signals:
void StepsChanged(const std::vector<SourceInteractionStep> &);
void RowSelected(int row);
private slots:
void Add() override;
void Remove() override;
void Up() override;
void Down() override;
private:
std::vector<SourceInteractionStep> _steps;
};
} // namespace advss

View File

@ -0,0 +1,190 @@
#include "source-interaction-step.hpp"
#include "obs-module-helper.hpp"
#include "variable.hpp"
#include <obs-interaction.h>
#include <chrono>
#include <thread>
#include <QString>
namespace advss {
static QString varOrNum(const NumberVariable<int> &v)
{
if (!v.IsFixedType()) {
return QString::fromStdString(
GetWeakVariableName(v.GetVariable()));
}
return QString::number(v.GetFixedValue());
}
bool SourceInteractionStep::Save(obs_data_t *obj) const
{
obs_data_set_int(obj, "type", static_cast<int>(type));
x.Save(obj, "x");
y.Save(obj, "y");
obs_data_set_int(obj, "button", static_cast<int>(button));
obs_data_set_bool(obj, "mouseUp", mouseUp);
clickCount.Save(obj, "clickCount");
wheelDeltaX.Save(obj, "wheelDeltaX");
wheelDeltaY.Save(obj, "wheelDeltaY");
nativeVkey.Save(obj, "nativeVkey");
obs_data_set_int(obj, "modifiers", modifiers);
obs_data_set_bool(obj, "keyUp", keyUp);
text.Save(obj, "text");
waitMs.Save(obj, "waitMs");
return true;
}
bool SourceInteractionStep::Load(obs_data_t *obj)
{
type = static_cast<Type>(obs_data_get_int(obj, "type"));
x.Load(obj, "x");
y.Load(obj, "y");
button = static_cast<obs_mouse_button_type>(
obs_data_get_int(obj, "button"));
mouseUp = obs_data_get_bool(obj, "mouseUp");
clickCount.Load(obj, "clickCount");
wheelDeltaX.Load(obj, "wheelDeltaX");
wheelDeltaY.Load(obj, "wheelDeltaY");
nativeVkey.Load(obj, "nativeVkey");
modifiers = static_cast<uint32_t>(obs_data_get_int(obj, "modifiers"));
keyUp = obs_data_get_bool(obj, "keyUp");
text.Load(obj, "text");
waitMs.Load(obj, "waitMs");
return true;
}
std::string SourceInteractionStep::ToString() const
{
switch (type) {
case Type::MOUSE_MOVE:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove"))
.arg(varOrNum(x))
.arg(varOrNum(y))
.toStdString();
case Type::MOUSE_CLICK: {
const char *btnKey =
(button == MOUSE_LEFT)
? "AdvSceneSwitcher.action.sourceInteraction.button.left"
: (button == MOUSE_MIDDLE)
? "AdvSceneSwitcher.action.sourceInteraction.button.middle"
: "AdvSceneSwitcher.action.sourceInteraction.button.right";
const char *dirKey =
mouseUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp"
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown";
QString result =
QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick"))
.arg(obs_module_text(dirKey))
.arg(obs_module_text(btnKey))
.arg(varOrNum(x))
.arg(varOrNum(y));
if (!clickCount.IsFixedType() ||
clickCount.GetFixedValue() > 1) {
result +=
QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount"))
.arg(varOrNum(clickCount));
}
return result.toStdString();
}
case Type::MOUSE_WHEEL:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel"))
.arg(varOrNum(x))
.arg(varOrNum(y))
.arg(varOrNum(wheelDeltaX))
.arg(varOrNum(wheelDeltaY))
.toStdString();
case Type::KEY_PRESS: {
const char *dirKey =
keyUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp"
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown";
if (text.empty()) {
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress"))
.arg(obs_module_text(dirKey))
.toStdString();
}
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText"))
.arg(obs_module_text(dirKey))
.arg(QString::fromStdString(text))
.toStdString();
}
case Type::TYPE_TEXT:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText"))
.arg(QString::fromStdString(text))
.toStdString();
case Type::WAIT:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait"))
.arg(varOrNum(waitMs))
.toStdString();
}
return "Unknown";
}
void PerformInteractionStep(obs_source_t *source,
const SourceInteractionStep &step)
{
switch (step.type) {
case SourceInteractionStep::Type::MOUSE_MOVE: {
obs_mouse_event e{};
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_move(source, &e, false);
break;
}
case SourceInteractionStep::Type::MOUSE_CLICK: {
obs_mouse_event e{};
e.modifiers = step.modifiers;
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_click(source, &e, step.button,
step.mouseUp, step.clickCount);
break;
}
case SourceInteractionStep::Type::MOUSE_WHEEL: {
obs_mouse_event e{};
e.modifiers = step.modifiers;
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_wheel(source, &e, step.wheelDeltaX,
step.wheelDeltaY);
break;
}
case SourceInteractionStep::Type::KEY_PRESS: {
std::string textCopy = step.text;
obs_key_event e{};
e.modifiers = step.modifiers;
e.text = textCopy.data();
e.native_vkey =
static_cast<uint32_t>(step.nativeVkey.GetValue());
obs_source_send_key_click(source, &e, step.keyUp);
break;
}
case SourceInteractionStep::Type::TYPE_TEXT: {
const std::string resolvedText = step.text;
for (const char c : resolvedText) {
std::string ch(1, c);
obs_key_event e{};
e.text = ch.data();
obs_source_send_key_click(source, &e, false);
obs_source_send_key_click(source, &e, true);
}
break;
}
case SourceInteractionStep::Type::WAIT:
std::this_thread::sleep_for(
std::chrono::milliseconds(step.waitMs));
break;
}
}
} // namespace advss

View File

@ -0,0 +1,48 @@
#pragma once
#include "variable-number.hpp"
#include "variable-string.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
namespace advss {
struct SourceInteractionStep {
enum class Type {
MOUSE_MOVE,
MOUSE_CLICK,
MOUSE_WHEEL,
KEY_PRESS,
TYPE_TEXT,
WAIT,
};
Type type = Type::MOUSE_MOVE;
// Mouse fields
NumberVariable<int> x = 0;
NumberVariable<int> y = 0;
obs_mouse_button_type button = MOUSE_LEFT;
bool mouseUp = false;
NumberVariable<int> clickCount = 1;
NumberVariable<int> wheelDeltaX = 0;
NumberVariable<int> wheelDeltaY = 120;
// Key / text fields
NumberVariable<int> nativeVkey = 0;
uint32_t modifiers = 0; // INTERACT_*
bool keyUp = false;
StringVariable text; // for TYPE_TEXT or key text
// Wait field (ms)
NumberVariable<int> waitMs = 100;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string ToString() const;
};
void PerformInteractionStep(obs_source_t *source,
const SourceInteractionStep &step);
} // namespace advss

View File

@ -0,0 +1,149 @@
#include "source-preview-widget.hpp"
#include <obs.hpp>
#include <graphics/graphics.h>
#include <QResizeEvent>
namespace advss {
static int32_t clampToSource(int32_t v, uint32_t max)
{
if (v < 0) {
return 0;
}
if (v > (int32_t)max) {
return (int32_t)max;
}
return v;
}
SourcePreviewWidget::SourcePreviewWidget(QWidget *parent,
obs_weak_source_t *source)
: QWidget(parent),
_source(source)
{
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
setMinimumSize(320, 240);
}
SourcePreviewWidget::~SourcePreviewWidget()
{
if (_display) {
obs_display_remove_draw_callback(
_display, SourcePreviewWidget::DrawCallback, this);
obs_display_destroy(_display);
_display = nullptr;
}
if (_showing) {
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_dec_showing(source);
}
_showing = false;
}
}
void SourcePreviewWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (!_display) {
WId wid = winId();
if (!wid) {
return;
}
gs_init_data info{};
#if defined(_WIN32)
info.window.hwnd = reinterpret_cast<void *>(wid);
#elif defined(__APPLE__)
info.window.view = reinterpret_cast<id>(wid);
#else
info.window.id = static_cast<uint32_t>(wid);
info.window.display = nullptr;
#endif
info.format = GS_BGRA;
info.zsformat = GS_ZS_NONE;
info.cx = static_cast<uint32_t>(width());
info.cy = static_cast<uint32_t>(height());
_display = obs_display_create(&info, 0x000000);
if (_display) {
obs_display_add_draw_callback(
_display, SourcePreviewWidget::DrawCallback,
this);
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_inc_showing(source);
_showing = true;
}
}
} else {
obs_display_resize(_display, static_cast<uint32_t>(width()),
static_cast<uint32_t>(height()));
}
}
void SourcePreviewWidget::DrawCallback(void *param, uint32_t cx, uint32_t cy)
{
auto self = static_cast<SourcePreviewWidget *>(param);
OBSSourceAutoRelease source = obs_weak_source_get_source(self->_source);
if (!source) {
return;
}
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
float scaleX = static_cast<float>(cx) / srcW;
float scaleY = static_cast<float>(cy) / srcH;
float scale = std::min(scaleX, scaleY);
int newW = static_cast<int>(scale * srcW);
int newH = static_cast<int>(scale * srcH);
int offX = (static_cast<int>(cx) - newW) / 2;
int offY = (static_cast<int>(cy) - newH) / 2;
self->_offsetX = offX;
self->_offsetY = offY;
self->_scale = scale;
gs_viewport_push();
gs_projection_push();
gs_ortho(0.0f, static_cast<float>(srcW), 0.0f, static_cast<float>(srcH),
-100.0f, 100.0f);
gs_set_viewport(offX, offY, newW, newH);
obs_source_video_render(source);
gs_projection_pop();
gs_viewport_pop();
}
bool SourcePreviewWidget::GetSourceRelativeXY(int widgetX, int widgetY,
int &srcX, int &srcY) const
{
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (!source) {
return false;
}
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
srcX = static_cast<int>((widgetX - _offsetX) / _scale);
srcY = static_cast<int>((widgetY - _offsetY) / _scale);
srcX = static_cast<int>(
clampToSource(static_cast<int32_t>(srcX), srcW));
srcY = static_cast<int>(
clampToSource(static_cast<int32_t>(srcY), srcH));
return true;
}
} // namespace advss

View File

@ -0,0 +1,40 @@
#pragma once
#include <obs.h>
#include <graphics/graphics.h>
#include <QWidget>
class QResizeEvent;
namespace advss {
// A native window widget that renders an OBS source via obs_display.
// Stores letterbox geometry for coordinate mapping.
class SourcePreviewWidget : public QWidget {
Q_OBJECT
public:
SourcePreviewWidget(QWidget *parent, obs_weak_source_t *source);
~SourcePreviewWidget();
// Maps a widget-local point to source-space coordinates.
bool GetSourceRelativeXY(int widgetX, int widgetY, int &srcX,
int &srcY) const;
protected:
void resizeEvent(QResizeEvent *) override;
QPaintEngine *paintEngine() const override { return nullptr; }
private:
static void DrawCallback(void *param, uint32_t cx, uint32_t cy);
obs_weak_source_t *_source;
obs_display_t *_display = nullptr;
bool _showing = false;
int _offsetX = 0;
int _offsetY = 0;
float _scale = 1.0f;
};
} // namespace advss