mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-05-09 04:32:13 -05:00
Add "Source interact" action
This commit is contained in:
parent
254da3b5f1
commit
ba99c9e1bf
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
286
plugins/base/macro-action-source-interaction.cpp
Normal file
286
plugins/base/macro-action-source-interaction.cpp
Normal 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
|
||||
78
plugins/base/macro-action-source-interaction.hpp
Normal file
78
plugins/base/macro-action-source-interaction.hpp
Normal 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
|
||||
237
plugins/base/utils/source-interaction-recorder.cpp
Normal file
237
plugins/base/utils/source-interaction-recorder.cpp
Normal 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
|
||||
54
plugins/base/utils/source-interaction-recorder.hpp
Normal file
54
plugins/base/utils/source-interaction-recorder.hpp
Normal 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
|
||||
315
plugins/base/utils/source-interaction-step-edit.cpp
Normal file
315
plugins/base/utils/source-interaction-step-edit.cpp
Normal 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
|
||||
32
plugins/base/utils/source-interaction-step-edit.hpp
Normal file
32
plugins/base/utils/source-interaction-step-edit.hpp
Normal 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
|
||||
99
plugins/base/utils/source-interaction-step-list.cpp
Normal file
99
plugins/base/utils/source-interaction-step-list.cpp
Normal 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
|
||||
38
plugins/base/utils/source-interaction-step-list.hpp
Normal file
38
plugins/base/utils/source-interaction-step-list.hpp
Normal 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
|
||||
190
plugins/base/utils/source-interaction-step.cpp
Normal file
190
plugins/base/utils/source-interaction-step.cpp
Normal 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
|
||||
48
plugins/base/utils/source-interaction-step.hpp
Normal file
48
plugins/base/utils/source-interaction-step.hpp
Normal 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
|
||||
149
plugins/base/utils/source-preview-widget.cpp
Normal file
149
plugins/base/utils/source-preview-widget.cpp
Normal 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
|
||||
40
plugins/base/utils/source-preview-widget.hpp
Normal file
40
plugins/base/utils/source-preview-widget.hpp
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user