From ba99c9e1bf6a3b3cbafeec4c183d2b440f9bc879 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:07:03 +0200 Subject: [PATCH] Add "Source interact" action --- data/locale/en-US.ini | 43 +++ plugins/base/CMakeLists.txt | 14 +- .../base/macro-action-source-interaction.cpp | 286 ++++++++++++++++ .../base/macro-action-source-interaction.hpp | 78 +++++ .../utils/source-interaction-recorder.cpp | 237 +++++++++++++ .../utils/source-interaction-recorder.hpp | 54 +++ .../utils/source-interaction-step-edit.cpp | 315 ++++++++++++++++++ .../utils/source-interaction-step-edit.hpp | 32 ++ .../utils/source-interaction-step-list.cpp | 99 ++++++ .../utils/source-interaction-step-list.hpp | 38 +++ .../base/utils/source-interaction-step.cpp | 190 +++++++++++ .../base/utils/source-interaction-step.hpp | 48 +++ plugins/base/utils/source-preview-widget.cpp | 149 +++++++++ plugins/base/utils/source-preview-widget.hpp | 40 +++ 14 files changed, 1622 insertions(+), 1 deletion(-) create mode 100644 plugins/base/macro-action-source-interaction.cpp create mode 100644 plugins/base/macro-action-source-interaction.hpp create mode 100644 plugins/base/utils/source-interaction-recorder.cpp create mode 100644 plugins/base/utils/source-interaction-recorder.hpp create mode 100644 plugins/base/utils/source-interaction-step-edit.cpp create mode 100644 plugins/base/utils/source-interaction-step-edit.hpp create mode 100644 plugins/base/utils/source-interaction-step-list.cpp create mode 100644 plugins/base/utils/source-interaction-step-list.hpp create mode 100644 plugins/base/utils/source-interaction-step.cpp create mode 100644 plugins/base/utils/source-interaction-step.hpp create mode 100644 plugins/base/utils/source-preview-widget.cpp create mode 100644 plugins/base/utils/source-preview-widget.hpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d8a3b2a0..98293383 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -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" diff --git a/plugins/base/CMakeLists.txt b/plugins/base/CMakeLists.txt index e9eb66a9..03952a0e 100644 --- a/plugins/base/CMakeLists.txt +++ b/plugins/base/CMakeLists.txt @@ -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 diff --git a/plugins/base/macro-action-source-interaction.cpp b/plugins/base/macro-action-source-interaction.cpp new file mode 100644 index 00000000..5b6ae960 --- /dev/null +++ b/plugins/base/macro-action-source-interaction.cpp @@ -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 +#include + +#include +#include +#include + +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 MacroActionSourceInteraction::Create(Macro *m) +{ + return std::make_shared(m); +} + +std::shared_ptr MacroActionSourceInteraction::Copy() const +{ + return std::make_shared(*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(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 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 &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 &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(layout())->addWidget(_stepEditor); + connect(_stepEditor, &SourceInteractionStepEdit::StepChanged, this, + &MacroActionSourceInteractionEdit::StepChanged); + + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/plugins/base/macro-action-source-interaction.hpp b/plugins/base/macro-action-source-interaction.hpp new file mode 100644 index 00000000..cdfc12d4 --- /dev/null +++ b/plugins/base/macro-action-source-interaction.hpp @@ -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 +#include + +namespace advss { + +class MacroActionSourceInteraction : public MacroAction { +public: + MacroActionSourceInteraction(Macro *m) : MacroAction(m) {} + + static std::shared_ptr Create(Macro *m); + std::shared_ptr 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 _steps; + +private: + static bool _registered; + static const std::string id; +}; + +class MacroActionSourceInteractionEdit : public QWidget { + Q_OBJECT +public: + MacroActionSourceInteractionEdit( + QWidget *parent, + std::shared_ptr entryData = + nullptr); + void UpdateEntryData(); + static QWidget *Create(QWidget *parent, + std::shared_ptr action) + { + return new MacroActionSourceInteractionEdit( + parent, + std::dynamic_pointer_cast( + action)); + } + +private slots: + void SourceChanged(const SourceSelection &); + void OnStepsChanged(const std::vector &); + void StepChanged(const SourceInteractionStep &); + void OpenRecorder(); + void AcceptRecorded(const std::vector &); + +signals: + void HeaderInfoChanged(const QString &); + +private: + void SetCurrentStepEditor(int row); + + SourceSelectionWidget *_sources; + SourceInteractionStepList *_stepList; + QPushButton *_recordButton; + SourceInteractionStepEdit *_stepEditor = nullptr; + QLabel *_noSelectionLabel; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss diff --git a/plugins/base/utils/source-interaction-recorder.cpp b/plugins/base/utils/source-interaction-recorder.cpp new file mode 100644 index 00000000..0a9302a0 --- /dev/null +++ b/plugins/base/utils/source-interaction-recorder.cpp @@ -0,0 +1,237 @@ +#include "source-interaction-recorder.hpp" +#include "obs-module-helper.hpp" + +#include +#include + +#include +#include +#include +#include + +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( + std::chrono::duration_cast( + 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(event)); + case QEvent::MouseMove: + return HandleMouseMove(static_cast(event)); + case QEvent::Wheel: + return HandleMouseWheel(static_cast(event)); + case QEvent::KeyPress: + case QEvent::KeyRelease: + return HandleKeyEvent(static_cast(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(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 diff --git a/plugins/base/utils/source-interaction-recorder.hpp b/plugins/base/utils/source-interaction-recorder.hpp new file mode 100644 index 00000000..d6ef8912 --- /dev/null +++ b/plugins/base/utils/source-interaction-recorder.hpp @@ -0,0 +1,54 @@ +#pragma once +#include "source-interaction-step.hpp" +#include "source-interaction-step-list.hpp" +#include "source-preview-widget.hpp" + +#include +#include + +#include +#include + +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 &GetSteps() const + { + return _steps; + } + +signals: + void StepsRecorded(const std::vector &); + +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 _steps; + std::chrono::steady_clock::time_point _lastEventTime; + bool _firstEvent = true; +}; + +} // namespace advss diff --git a/plugins/base/utils/source-interaction-step-edit.cpp b/plugins/base/utils/source-interaction-step-edit.cpp new file mode 100644 index 00000000..95dbfb01 --- /dev/null +++ b/plugins/base/utils/source-interaction-step-edit.cpp @@ -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 +#include + +#include +#include +#include +#include + +namespace advss { + +static const std::map 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 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(type)); + } +} + +static void populateButtonCombo(QComboBox *combo) +{ + for (const auto &[btn, name] : mouseButtonNames) { + combo->addItem(obs_module_text(name.c_str()), + static_cast(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::of(&QComboBox::currentIndexChanged), + this, &SourceInteractionStepEdit::TypeChanged); + + RebuildFields(); + + int idx = _typeCombo->findData(static_cast(_step.type)); + if (idx >= 0) { + _typeCombo->setCurrentIndex(idx); + } +} + +void SourceInteractionStepEdit::TypeChanged(int) +{ + _step.type = static_cast( + _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 &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 &>::of( + &GenericVariableSpinbox::NumberVariableChanged), + this, [this, xSpin](const NumberVariable &) { + _step.x = xSpin->Value(); + UpdateStep(); + }); + connect(ySpin, + QOverload &>::of( + &GenericVariableSpinbox::NumberVariableChanged), + this, [this, ySpin](const NumberVariable &) { + _step.y = ySpin->Value(); + UpdateStep(); + }); + + if (_step.type == SourceInteractionStep::Type::MOUSE_CLICK) { + auto btnCombo = new QComboBox; + populateButtonCombo(btnCombo); + btnCombo->setCurrentIndex(btnCombo->findData( + static_cast(_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::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 &>::of( + &GenericVariableSpinbox:: + NumberVariableChanged), + this, + [this, countSpin](const NumberVariable &) { + _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 &>::of( + &GenericVariableSpinbox:: + NumberVariableChanged), + this, + [this, dxSpin](const NumberVariable &) { + _step.wheelDeltaX = dxSpin->Value(); + UpdateStep(); + }); + connect(dySpin, + QOverload &>::of( + &GenericVariableSpinbox:: + NumberVariableChanged), + this, + [this, dySpin](const NumberVariable &) { + _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 &>::of( + &GenericVariableSpinbox::NumberVariableChanged), + this, [this, vkeySpin](const NumberVariable &) { + _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 &>::of( + &GenericVariableSpinbox::NumberVariableChanged), + this, [this, msSpin](const NumberVariable &) { + _step.waitMs = msSpin->Value(); + UpdateStep(); + }); + + _fields->addWidget(page); + _fields->setCurrentWidget(page); + break; + } + } +} + +} // namespace advss diff --git a/plugins/base/utils/source-interaction-step-edit.hpp b/plugins/base/utils/source-interaction-step-edit.hpp new file mode 100644 index 00000000..440bf646 --- /dev/null +++ b/plugins/base/utils/source-interaction-step-edit.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "source-interaction-step.hpp" + +#include +#include +#include + +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 diff --git a/plugins/base/utils/source-interaction-step-list.cpp b/plugins/base/utils/source-interaction-step-list.cpp new file mode 100644 index 00000000..fea59dca --- /dev/null +++ b/plugins/base/utils/source-interaction-step-list.cpp @@ -0,0 +1,99 @@ +#include "source-interaction-step-list.hpp" +#include "obs-module-helper.hpp" + +#include + +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 &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 selected = _list->selectedItems(); + if (selected.isEmpty()) { + return; + } + std::vector rows; + rows.reserve(selected.size()); + for (const auto *item : selected) { + rows.push_back(_list->row(item)); + } + std::sort(rows.begin(), rows.end(), std::greater()); + 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 diff --git a/plugins/base/utils/source-interaction-step-list.hpp b/plugins/base/utils/source-interaction-step-list.hpp new file mode 100644 index 00000000..501fe2f8 --- /dev/null +++ b/plugins/base/utils/source-interaction-step-list.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "list-editor.hpp" +#include "source-interaction-step.hpp" + +#include + +namespace advss { + +class SourceInteractionStepList : public ListEditor { + Q_OBJECT +public: + SourceInteractionStepList(QWidget *parent = nullptr); + + void SetSteps(const std::vector &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 &); + void RowSelected(int row); + +private slots: + void Add() override; + void Remove() override; + void Up() override; + void Down() override; + +private: + std::vector _steps; +}; + +} // namespace advss diff --git a/plugins/base/utils/source-interaction-step.cpp b/plugins/base/utils/source-interaction-step.cpp new file mode 100644 index 00000000..aba0a970 --- /dev/null +++ b/plugins/base/utils/source-interaction-step.cpp @@ -0,0 +1,190 @@ +#include "source-interaction-step.hpp" +#include "obs-module-helper.hpp" +#include "variable.hpp" + +#include + +#include +#include + +#include + +namespace advss { + +static QString varOrNum(const NumberVariable &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(type)); + x.Save(obj, "x"); + y.Save(obj, "y"); + obs_data_set_int(obj, "button", static_cast(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(obs_data_get_int(obj, "type")); + x.Load(obj, "x"); + y.Load(obj, "y"); + button = static_cast( + 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(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(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 diff --git a/plugins/base/utils/source-interaction-step.hpp b/plugins/base/utils/source-interaction-step.hpp new file mode 100644 index 00000000..56bdf05b --- /dev/null +++ b/plugins/base/utils/source-interaction-step.hpp @@ -0,0 +1,48 @@ +#pragma once +#include "variable-number.hpp" +#include "variable-string.hpp" + +#include +#include + +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 x = 0; + NumberVariable y = 0; + obs_mouse_button_type button = MOUSE_LEFT; + bool mouseUp = false; + NumberVariable clickCount = 1; + NumberVariable wheelDeltaX = 0; + NumberVariable wheelDeltaY = 120; + + // Key / text fields + NumberVariable nativeVkey = 0; + uint32_t modifiers = 0; // INTERACT_* + bool keyUp = false; + StringVariable text; // for TYPE_TEXT or key text + + // Wait field (ms) + NumberVariable 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 diff --git a/plugins/base/utils/source-preview-widget.cpp b/plugins/base/utils/source-preview-widget.cpp new file mode 100644 index 00000000..85168cc4 --- /dev/null +++ b/plugins/base/utils/source-preview-widget.cpp @@ -0,0 +1,149 @@ +#include "source-preview-widget.hpp" + +#include +#include + +#include + +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(wid); +#elif defined(__APPLE__) + info.window.view = reinterpret_cast(wid); +#else + info.window.id = static_cast(wid); + info.window.display = nullptr; +#endif + info.format = GS_BGRA; + info.zsformat = GS_ZS_NONE; + info.cx = static_cast(width()); + info.cy = static_cast(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(width()), + static_cast(height())); + } +} + +void SourcePreviewWidget::DrawCallback(void *param, uint32_t cx, uint32_t cy) +{ + auto self = static_cast(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(cx) / srcW; + float scaleY = static_cast(cy) / srcH; + float scale = std::min(scaleX, scaleY); + + int newW = static_cast(scale * srcW); + int newH = static_cast(scale * srcH); + int offX = (static_cast(cx) - newW) / 2; + int offY = (static_cast(cy) - newH) / 2; + + self->_offsetX = offX; + self->_offsetY = offY; + self->_scale = scale; + + gs_viewport_push(); + gs_projection_push(); + gs_ortho(0.0f, static_cast(srcW), 0.0f, static_cast(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((widgetX - _offsetX) / _scale); + srcY = static_cast((widgetY - _offsetY) / _scale); + srcX = static_cast( + clampToSource(static_cast(srcX), srcW)); + srcY = static_cast( + clampToSource(static_cast(srcY), srcH)); + return true; +} + +} // namespace advss diff --git a/plugins/base/utils/source-preview-widget.hpp b/plugins/base/utils/source-preview-widget.hpp new file mode 100644 index 00000000..2a236cc9 --- /dev/null +++ b/plugins/base/utils/source-preview-widget.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +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