This commit is contained in:
WarmUpTill 2026-07-01 19:46:08 +00:00 committed by GitHub
commit 03df1cb7b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1696 additions and 401 deletions

View File

@ -209,6 +209,13 @@ target_sources(
lib/utils/filter-combo-box.hpp
lib/utils/first-run-wizard.cpp
lib/utils/first-run-wizard.hpp
lib/utils/first-run-wizard-audio.cpp
lib/utils/first-run-wizard-audio.hpp
lib/utils/first-run-wizard-helpers.hpp
lib/utils/first-run-wizard-sequence.cpp
lib/utils/first-run-wizard-sequence.hpp
lib/utils/first-run-wizard-window.cpp
lib/utils/first-run-wizard-window.hpp
lib/utils/help-icon.hpp
lib/utils/help-icon.cpp
lib/utils/item-selection-helpers.cpp

View File

@ -2680,7 +2680,28 @@ FirstRunWizard.windowTitle="Advanced Scene Switcher - Setup Wizard"
FirstRunWizard.welcome.title="Welcome to Advanced Scene Switcher"
FirstRunWizard.welcome.subtitle="Let's create your first automation in a few easy steps."
FirstRunWizard.welcome.body="<p>Advanced Scene Switcher lets you build <b>Macros</b> - rules of the form:</p><blockquote><i>When [condition] -> perform [action]</i></blockquote><p>This wizard creates a macro that <b>switches to a chosen OBS scene whenever a specific window comes into focus</b>.</p><p>You can skip at any time. Re-open this wizard later from the <b>General</b> tab inside the Advanced Scene Switcher dialog.</p>"
FirstRunWizard.welcome.body="<p>Advanced Scene Switcher lets you build <b>Macros</b> - rules of the form:</p><blockquote><i>When [condition] -> perform [action]</i></blockquote><p>This wizard will guide you through creating your first automation. On the next page, choose the type of macro you want to set up.</p><p>You can skip at any time. Re-open this wizard later from the <b>General</b> tab inside the Advanced Scene Switcher dialog.</p>"
FirstRunWizard.template.title="Choose an Automation Type"
FirstRunWizard.template.subtitle="Select the kind of macro you would like to create."
FirstRunWizard.template.window="Switch to a scene when a window comes into focus"
FirstRunWizard.template.sequence="Run a timed sequence of scene switches"
FirstRunWizard.template.audio="Show or hide a source based on audio activity"
FirstRunWizard.audio.source.title="Choose an Audio Source"
FirstRunWizard.audio.source.subtitle="Select the audio source to monitor and configure when it should be considered active."
FirstRunWizard.audio.source.sourceRow="Audio source:{{source}}"
FirstRunWizard.audio.source.thresholdRow="Show when volume is above{{threshold}}for{{duration}}"
FirstRunWizard.audio.target.title="Choose a Target Source"
FirstRunWizard.audio.target.subtitle="Select the source to show when audio is active and hide when it is not."
FirstRunWizard.audio.target.sourceRow="Source to show/hide:{{source}}"
FirstRunWizard.audio.review.title="Review Your Macro"
FirstRunWizard.audio.review.subtitle="Click Back to make changes, or Finish to create the macro."
FirstRunWizard.audio.review.summary="<b>When</b> <i>%1</i> output volume is above <b>%2 dB</b> for <b>%3</b>:<br><br>&nbsp;&nbsp;<b>Show</b> <i>%4</i> on the current scene<br><br><b>Otherwise:</b><br><br>&nbsp;&nbsp;<b>Hide</b> <i>%4</i> on the current scene"
FirstRunWizard.audio.review.errorTitle="Macro Creation Failed"
FirstRunWizard.audio.review.errorBody="The audio macro could not be created. Please check the OBS log for details."
FirstRunWizard.scene.title="Choose a Target Scene"
FirstRunWizard.scene.subtitle="Which OBS scene should become active when your chosen window comes into focus?"
@ -2701,6 +2722,26 @@ FirstRunWizard.review.summary="<b>Macro name:</b> Window -> %1<br><br><b>Conditi
FirstRunWizard.review.errorTitle="Macro Creation Failed"
FirstRunWizard.review.errorBody="The macro could not be created automatically.\n\nYou can create it manually on the Macros tab:\n Condition: Window -> contains \"%1\"\n Action: Switch scene -> \"%2\""
FirstRunWizard.seqTrigger.title="Choose a Trigger Scene"
FirstRunWizard.seqTrigger.subtitle="The sequence will start when you have been on this scene for the specified duration."
FirstRunWizard.seqTrigger.scene="Trigger scene:{{scene}}"
FirstRunWizard.seqTrigger.delay="Start sequence after{{duration}}"
FirstRunWizard.seqScenes.title="Build Your Sequence"
FirstRunWizard.seqScenes.subtitle="Add at least two scenes. The plugin will switch through them in order, waiting the configured time between each switch."
FirstRunWizard.seqScenes.triggerInfo="When on \"%1\" for %2, switch to:"
FirstRunWizard.seqScenes.addScene="Add scene"
FirstRunWizard.seqScenes.removeTooltip="Remove this scene from the sequence"
FirstRunWizard.seqScenes.switchTo="Switch to{{scene}}"
FirstRunWizard.seqScenes.thenWait="then wait{{duration}}"
FirstRunWizard.seqReview.title="Review Your Sequence"
FirstRunWizard.seqReview.subtitle="Click Back to make changes, or Finish to create the macro."
FirstRunWizard.seqReview.trigger="When on <b>%1</b> for at least <b>%2</b>:"
FirstRunWizard.seqReview.step="After %1, switch to %2"
FirstRunWizard.seqReview.errorTitle="Macro Creation Failed"
FirstRunWizard.seqReview.errorBody="The sequence macro could not be created. Please check the OBS log for details."
FirstRunWizard.done.title="You're all set!"
FirstRunWizard.done.subtitle="Your first macro has been created and is now active."
FirstRunWizard.done.body="<p>You can view and edit it on the <b>Macros</b> tab of the Advanced Scene Switcher dialog.</p><p>To learn more:</p><ul><li><a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\">Plugin wiki</a></li><li><a href=\"https://obsproject.com/forum/resources/automatic-scene-switching.395/\">OBS forum thread</a></li></ul>"

View File

@ -452,7 +452,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
}
bool wasSkipped = false;
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
auto macro = wiz::FirstRunWizard::ShowWizard(this, &wasSkipped);
if (macro) {
renameMacroIfNecessary(macro);
QTimer::singleShot(0, this,
@ -466,7 +466,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
void AdvSceneSwitcher::on_openSetupWizard_clicked()
{
auto macro = FirstRunWizard::ShowWizard(this);
auto macro = wiz::FirstRunWizard::ShowWizard(this);
if (!macro) {
return;
}

View File

@ -0,0 +1,425 @@
#include "first-run-wizard-audio.hpp"
#include "first-run-wizard-helpers.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "macro-settings.hpp"
#include "selection-helpers.hpp"
#include <obs-data.h>
#include <obs.hpp>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout>
#include <QWidget>
namespace advss {
namespace wiz {
// Builds the obs_data blob for an audio volume condition,
// matching MacroConditionAudio::Save() output.
//
// Checks output volume (checkType 0) ABOVE (outputCondition 0) the given
// threshold in dB, held for at least `duration`.
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "audio",
// "logic": 0,
// "durationModifier": {
// "time_constraint": 1, // AT_LEAST
// "seconds": <Duration::Save output>
// },
// "audioSource": { "type": 0, "name": "<source>" },
// "monitor": 0,
// "volume": { "value": 0.0, "type": 0 },
// "syncOffset": { "value": 0, "type": 0 },
// "balance": { "value": 0.5, "type": 0 },
// "checkType": 0, // OUTPUT_VOLUME
// "outputCondition": 0, // ABOVE
// "volumeCondition": 0,
// "useDb": true,
// "volumeDB": { "value": <thresholdDb>, "type": 0 },
// "version": 3
// }
static OBSDataAutoRelease BuildAudioConditionData(const QString &sourceName,
double thresholdDb,
const Duration &duration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease durMod = obs_data_create();
obs_data_set_int(durMod, "time_constraint", 1);
duration.Save(durMod, "seconds");
OBSDataAutoRelease audioSrc = obs_data_create();
obs_data_set_int(audioSrc, "type", 0);
obs_data_set_string(audioSrc, "name", sourceName.toUtf8().constData());
OBSDataAutoRelease volume = obs_data_create();
obs_data_set_double(volume, "value", 0.0);
obs_data_set_int(volume, "type", 0);
OBSDataAutoRelease syncOffset = obs_data_create();
obs_data_set_int(syncOffset, "value", 0);
obs_data_set_int(syncOffset, "type", 0);
OBSDataAutoRelease balance = obs_data_create();
obs_data_set_double(balance, "value", 0.5);
obs_data_set_int(balance, "type", 0);
OBSDataAutoRelease volumeDB = obs_data_create();
obs_data_set_double(volumeDB, "value", thresholdDb);
obs_data_set_int(volumeDB, "type", 0);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "audio");
obs_data_set_int(data, "logic", 0);
obs_data_set_obj(data, "durationModifier", durMod);
obs_data_set_obj(data, "audioSource", audioSrc);
obs_data_set_int(data, "monitor", 0);
obs_data_set_obj(data, "volume", volume);
obs_data_set_obj(data, "syncOffset", syncOffset);
obs_data_set_obj(data, "balance", balance);
obs_data_set_int(data, "checkType", 0);
obs_data_set_int(data, "outputCondition", 0);
obs_data_set_int(data, "volumeCondition", 0);
obs_data_set_bool(data, "useDb", true);
obs_data_set_obj(data, "volumeDB", volumeDB);
obs_data_set_int(data, "version", 3);
return data;
}
// Builds the obs_data blob for a scene-visibility action on the current scene,
// matching MacroActionSceneVisibility::Save() output.
//
// sceneSelection type 3 = current scene.
// action 0 = SHOW, action 1 = HIDE.
static OBSDataAutoRelease BuildVisibilityActionData(const QString &sourceName,
int action)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 3); // current scene
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease itemSel = obs_data_create();
obs_data_set_int(itemSel, "type", 0);
obs_data_set_int(itemSel, "idxType", 0);
obs_data_set_int(itemSel, "idx", 0);
obs_data_set_string(itemSel, "item", sourceName.toUtf8().constData());
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene_visibility");
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_obj(data, "sceneItemSelection", itemSel);
obs_data_set_bool(data, "updateTransition", false);
obs_data_set_int(data, "transitionType", 0);
obs_data_set_string(data, "transition", "");
obs_data_set_bool(data, "updateDuration", false);
Duration().Save(data, "duration");
obs_data_set_int(data, "action", action);
return data;
}
// ===========================================================================
// AudioSourcePage
// ===========================================================================
AudioSourcePage::AudioSourcePage(QWidget *parent)
: QWizardPage(parent),
_sourceCombo(new QComboBox(this)),
_thresholdSpinbox(new QDoubleSpinBox(this)),
_durationSelection(new DurationSelection(this, true, 0.0)),
_volmeterLayout(new QVBoxLayout)
{
setTitle(obs_module_text("FirstRunWizard.audio.source.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.source.subtitle"));
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
_thresholdSpinbox->setRange(-100.0, 0.0);
_thresholdSpinbox->setValue(-24.0);
_thresholdSpinbox->setDecimals(1);
_thresholdSpinbox->setSingleStep(1.0);
_thresholdSpinbox->setSuffix(" dB");
_durationSelection->SetDuration(Duration(1.0));
registerField("audioSourceName*", _sourceCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&AudioSourcePage::UpdateVolmeter);
connect(_thresholdSpinbox,
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
&AudioSourcePage::SyncSliderFromSpinbox);
auto *sourceRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.audio.source.sourceRow"),
sourceRow, {{"{{source}}", _sourceCombo}}, false);
auto *thresholdRow = new QHBoxLayout;
PlaceWidgets(
obs_module_text("FirstRunWizard.audio.source.thresholdRow"),
thresholdRow,
{{"{{threshold}}", _thresholdSpinbox},
{"{{duration}}", _durationSelection}},
false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(sourceRow);
layout->addLayout(thresholdRow);
layout->addLayout(_volmeterLayout);
layout->addStretch();
}
void AudioSourcePage::initializePage()
{
_sourceCombo->clear();
PopulateAudioSelection(_sourceCombo, false);
}
void AudioSourcePage::UpdateVolmeter()
{
delete _volControl;
_volControl = nullptr;
OBSSourceAutoRelease source = obs_get_source_by_name(
_sourceCombo->currentText().toUtf8().constData());
if (!source) {
return;
}
_volControl = new VolControl(source.Get());
_volmeterLayout->addWidget(_volControl);
connect(_volControl->GetSlider(), &DoubleSlider::DoubleValChanged, this,
&AudioSourcePage::SyncSpinboxFromSlider);
SyncSliderFromSpinbox();
}
void AudioSourcePage::SyncSpinboxFromSlider()
{
if (!_volControl) {
return;
}
const QSignalBlocker blocker(_thresholdSpinbox);
_thresholdSpinbox->setValue(_volControl->GetSlider()->DoubleValue() -
100.0);
}
void AudioSourcePage::SyncSliderFromSpinbox()
{
if (!_volControl) {
return;
}
const QSignalBlocker blocker(_volControl->GetSlider());
_volControl->GetSlider()->SetDoubleVal(_thresholdSpinbox->value() +
100.0);
}
bool AudioSourcePage::isComplete() const
{
return _sourceCombo->count() > 0 &&
!_sourceCombo->currentText().isEmpty();
}
Duration AudioSourcePage::GetDuration() const
{
return _durationSelection->GetDuration();
}
// ===========================================================================
// AudioTargetPage
// ===========================================================================
AudioTargetPage::AudioTargetPage(QWidget *parent)
: QWizardPage(parent),
_sourceCombo(new QComboBox(this))
{
setTitle(obs_module_text("FirstRunWizard.audio.target.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.target.subtitle"));
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
registerField("audioTargetSource*", _sourceCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto *row = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.audio.target.sourceRow"),
row, {{"{{source}}", _sourceCombo}}, false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addStretch();
}
void AudioTargetPage::initializePage()
{
QStringList names;
auto enumScenes = [](void *param, obs_source_t *src) -> bool {
auto *list = reinterpret_cast<QStringList *>(param);
obs_scene_t *scene = obs_scene_from_source(src);
obs_scene_enum_items(
scene,
[](obs_scene_t *, obs_sceneitem_t *item,
void *p) -> bool {
auto *l = reinterpret_cast<QStringList *>(p);
OBSSource s = obs_sceneitem_get_source(item);
if (s) {
*l << obs_source_get_name(s);
}
return true;
},
list);
return true;
};
obs_enum_scenes(enumScenes, &names);
names.sort(Qt::CaseInsensitive);
names.removeDuplicates();
const QString prev = _sourceCombo->currentText();
_sourceCombo->clear();
_sourceCombo->addItems(names);
const int idx = _sourceCombo->findText(prev);
if (idx >= 0) {
_sourceCombo->setCurrentIndex(idx);
}
}
bool AudioTargetPage::isComplete() const
{
return _sourceCombo->count() > 0 &&
!_sourceCombo->currentText().isEmpty();
}
// ===========================================================================
// AudioReviewPage
// ===========================================================================
AudioReviewPage::AudioReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_summary(new QLabel(this)),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.audio.review.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.review.subtitle"));
detail::setupSummaryLabel(_summary);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void AudioReviewPage::initializePage()
{
const QString audioSource = field("audioSourceName").toString();
const QString targetSource = field("audioTargetSource").toString();
auto *sourcePage = qobject_cast<AudioSourcePage *>(
wizard()->page(PAGE_AUDIO_SOURCE));
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
: -24.0;
const Duration duration = sourcePage ? sourcePage->GetDuration()
: Duration(1.0);
_summary->setText(
QString(obs_module_text("FirstRunWizard.audio.review.summary"))
.arg(audioSource.toHtmlEscaped())
.arg(thresholdDb, 0, 'f', 1)
.arg(QString::fromStdString(duration.ToString()))
.arg(targetSource.toHtmlEscaped()));
}
bool AudioReviewPage::validatePage()
{
const QString audioSource = field("audioSourceName").toString();
const QString targetSource = field("audioTargetSource").toString();
const std::string name =
("Audio: " + audioSource + " -> " + targetSource).toStdString();
auto *sourcePage = qobject_cast<AudioSourcePage *>(
wizard()->page(PAGE_AUDIO_SOURCE));
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
: -24.0;
const Duration duration = sourcePage ? sourcePage->GetDuration()
: Duration(1.0);
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: audio macro allocation failed");
return true;
}
_macro->SetActionConditionSplitterPosition(
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
_macro->SetElseActionSplitterPosition(
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
OBSDataAutoRelease condData =
BuildAudioConditionData(audioSource, thresholdDb, duration);
if (!detail::addCondition(_macro.get(), "audio", condData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
OBSDataAutoRelease showData =
BuildVisibilityActionData(targetSource, 0);
if (!detail::addAction(_macro.get(), "scene_visibility", showData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
OBSDataAutoRelease hideData =
BuildVisibilityActionData(targetSource, 1);
if (!detail::addElseAction(_macro.get(), "scene_visibility",
hideData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
blog(LOG_INFO, "FirstRunWizard: created audio macro '%s'",
name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@ -0,0 +1,83 @@
#pragma once
#include "duration-control.hpp"
#include "duration.hpp"
#include "first-run-wizard.hpp"
#include "volume-control.hpp"
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QLabel>
#include <QVBoxLayout>
#include <memory>
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// AudioSourcePage
// Registers wizard field "audioSourceName" (QString).
// Volume threshold and duration are read back via getters.
// ---------------------------------------------------------------------------
class AudioSourcePage : public QWizardPage {
Q_OBJECT
public:
explicit AudioSourcePage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_AUDIO_TARGET; }
double GetThresholdDb() const { return _thresholdSpinbox->value(); }
Duration GetDuration() const;
private slots:
void UpdateVolmeter();
void SyncSpinboxFromSlider();
void SyncSliderFromSpinbox();
private:
QComboBox *_sourceCombo;
QDoubleSpinBox *_thresholdSpinbox;
DurationSelection *_durationSelection;
QVBoxLayout *_volmeterLayout;
VolControl *_volControl = nullptr;
};
// ---------------------------------------------------------------------------
// AudioTargetPage
// Registers wizard field "audioTargetSource" (QString).
// ---------------------------------------------------------------------------
class AudioTargetPage : public QWizardPage {
Q_OBJECT
public:
explicit AudioTargetPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_AUDIO_REVIEW; }
private:
QComboBox *_sourceCombo;
};
// ---------------------------------------------------------------------------
// AudioReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class AudioReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit AudioReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@ -0,0 +1,85 @@
#pragma once
#include "log-helper.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include <obs-data.h>
#include <QFrame>
#include <QLabel>
#include <string>
namespace advss::wiz::detail {
static bool addCondition(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto cond = MacroConditionFactory::Create(id, macro);
if (!cond) {
blog(LOG_WARNING,
"FirstRunWizard: condition factory returned null for '%s'",
id.c_str());
return false;
}
if (!cond->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: condition Load() failed for '%s'",
id.c_str());
return false;
}
macro->Conditions().emplace_back(cond);
return true;
}
static bool addAction(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto action = MacroActionFactory::Create(id, macro);
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: action factory returned null for '%s'",
id.c_str());
return false;
}
if (!action->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: action Load() failed for '%s'",
id.c_str());
return false;
}
macro->Actions().emplace_back(action);
return true;
}
static bool addElseAction(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto action = MacroActionFactory::Create(id, macro);
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: else-action factory returned null for '%s'",
id.c_str());
return false;
}
if (!action->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: else-action Load() failed for '%s'",
id.c_str());
return false;
}
macro->ElseActions().emplace_back(action);
return true;
}
static void setupSummaryLabel(QLabel *label)
{
label->setWordWrap(true);
label->setTextFormat(Qt::RichText);
label->setFrameShape(QFrame::StyledPanel);
label->setContentsMargins(12, 12, 12, 12);
}
} // namespace advss::wiz::detail

View File

@ -0,0 +1,499 @@
#include "first-run-wizard-sequence.hpp"
#include "first-run-wizard-helpers.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "macro-settings.hpp"
#include "selection-helpers.hpp"
#include <obs-data.h>
#include <QFrame>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QScrollArea>
#include <QVBoxLayout>
namespace advss {
namespace wiz {
// Builds the obs_data blob for a "current scene == sceneName for at least
// triggerDuration" condition, matching MacroConditionScene::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "scene",
// "logic": 0,
// "durationModifier": {
// "time_constraint": 1, // AT_LEAST
// "seconds": <Duration::Save output>
// },
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
// "type": 10, // CURRENT_SCENE
// "version": 1
// }
static OBSDataAutoRelease
BuildSceneConditionData(const QString &scene, const Duration &triggerDuration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease durMod = obs_data_create();
obs_data_set_int(durMod, "time_constraint", 1);
triggerDuration.Save(durMod, "seconds");
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 0);
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene");
obs_data_set_int(data, "logic", 0);
obs_data_set_obj(data, "durationModifier", durMod);
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_int(data, "type", 10);
obs_data_set_int(data, "version", 1);
return data;
}
// Builds the obs_data blob for a scene-switch action,
// matching MacroActionSwitchScene::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "scene_switch",
// "action": 0,
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
// "transitionType": 1, // scene's default transition
// "blockUntilTransitionDone": true,
// "sceneType": 0
// }
static OBSDataAutoRelease BuildSceneSwitchData(const QString &scene)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 0);
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene_switch");
obs_data_set_int(data, "action", 0);
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_int(data, "transitionType", 1);
obs_data_set_bool(data, "blockUntilTransitionDone", true);
obs_data_set_int(data, "sceneType", 0);
return data;
}
// Builds the obs_data blob for a wait action, matching MacroActionWait::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "wait",
// "duration": <Duration::Save output>,
// "waitType": 0,
// "version": 1
// }
static OBSDataAutoRelease BuildWaitData(const Duration &duration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "wait");
duration.Save(data, "duration");
obs_data_set_int(data, "waitType", 0);
obs_data_set_int(data, "version", 1);
return data;
}
// ===========================================================================
// SequenceTriggerPage
// ===========================================================================
SequenceTriggerPage::SequenceTriggerPage(QWidget *parent)
: QWizardPage(parent),
_sceneCombo(new QComboBox(this)),
_delaySelection(new DurationSelection(this, true, 0.0))
{
setTitle(obs_module_text("FirstRunWizard.seqTrigger.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqTrigger.subtitle"));
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
_delaySelection->SetDuration(Duration(5.0));
registerField("seqTriggerScene*", _sceneCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto *sceneRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.scene"),
sceneRow, {{"{{scene}}", _sceneCombo}}, false);
auto *delayRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.delay"),
delayRow, {{"{{duration}}", _delaySelection}}, false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(sceneRow);
layout->addLayout(delayRow);
layout->addStretch();
}
void SequenceTriggerPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames()) {
_sceneCombo->addItem(name);
}
}
bool SequenceTriggerPage::isComplete() const
{
return _sceneCombo->count() > 0 &&
!_sceneCombo->currentText().isEmpty();
}
Duration SequenceTriggerPage::GetTriggerDuration() const
{
return _delaySelection->GetDuration();
}
// ===========================================================================
// SequenceScenesPage
// ===========================================================================
SequenceScenesPage::SequenceScenesPage(QWidget *parent)
: QWizardPage(parent),
_triggerInfoLabel(new QLabel(this)),
_stepsContainer(new QWidget(this)),
_stepsLayout(new QVBoxLayout(_stepsContainer))
{
setTitle(obs_module_text("FirstRunWizard.seqScenes.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqScenes.subtitle"));
_triggerInfoLabel->setWordWrap(true);
_triggerInfoLabel->setFrameShape(QFrame::StyledPanel);
_triggerInfoLabel->setContentsMargins(8, 4, 8, 4);
_stepsLayout->setContentsMargins(0, 0, 0, 0);
_stepsLayout->setSpacing(4);
auto *scrollArea = new QScrollArea(this);
scrollArea->setWidget(_stepsContainer);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto *addBtn = new QPushButton(
obs_module_text("FirstRunWizard.seqScenes.addScene"), this);
connect(addBtn, &QPushButton::clicked, this,
&SequenceScenesPage::onAddStepClicked);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_triggerInfoLabel);
layout->addWidget(scrollArea, 1);
layout->addWidget(addBtn, 0, Qt::AlignLeft);
}
void SequenceScenesPage::initializePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
_triggerInfoLabel->setText(
QString(obs_module_text("FirstRunWizard.seqScenes.triggerInfo"))
.arg(triggerScene)
.arg(QString::fromStdString(
triggerDuration.ToString())));
if (_initialized) {
return;
}
_initialized = true;
// Pre-select the scenes after the trigger scene so the user doesn't
// have to start by deselecting it manually.
const QStringList scenes = GetSceneNames();
const int count = static_cast<int>(scenes.size());
const int triggerIdx = scenes.indexOf(triggerScene);
const int firstIdx = count > 0 ? (triggerIdx + 1) % count : 0;
const int secondIdx = count > 0 ? (triggerIdx + 2) % count : 0;
AddStep(firstIdx);
AddStep(secondIdx);
}
bool SequenceScenesPage::isComplete() const
{
return _rows.size() >= 2;
}
QVector<QPair<QString, Duration>> SequenceScenesPage::GetSteps() const
{
QVector<QPair<QString, Duration>> steps;
steps.reserve(_rows.size());
for (int i = 0; i < _rows.size(); ++i) {
const Duration delay = (i < _rows.size() - 1)
? _rows[i].delay->GetDuration()
: Duration();
steps.append({_rows[i].scene->currentText(), delay});
}
return steps;
}
void SequenceScenesPage::onAddStepClicked()
{
const QStringList scenes = GetSceneNames();
const int count = static_cast<int>(scenes.size());
int nextIdx = 0;
if (!_rows.isEmpty() && count > 0) {
const int lastIdx = _rows.last().scene->currentIndex();
nextIdx = (lastIdx + 1) % count;
}
AddStep(nextIdx);
emit completeChanged();
}
void SequenceScenesPage::AddStep(int defaultSceneIndex)
{
auto *row = new QWidget(_stepsContainer);
auto *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
auto *sceneCombo = new QComboBox(row);
sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
const QStringList scenes = GetSceneNames();
for (const QString &name : scenes) {
sceneCombo->addItem(name);
}
if (defaultSceneIndex >= 0 && defaultSceneIndex < scenes.size()) {
sceneCombo->setCurrentIndex(defaultSceneIndex);
}
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.switchTo"),
rowLayout, {{"{{scene}}", sceneCombo}}, false);
auto *delayWidget = new QWidget(row);
auto *delayLayout = new QHBoxLayout(delayWidget);
delayLayout->setContentsMargins(0, 0, 0, 0);
auto *durationSel = new DurationSelection(delayWidget, true, 0.1);
durationSel->SetDuration(Duration(5.0));
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.thenWait"),
delayLayout, {{"{{duration}}", durationSel}}, false);
rowLayout->addWidget(delayWidget);
auto *removeBtn = new QPushButton(row);
removeBtn->setProperty("themeID",
QVariant(QString::fromUtf8("removeIconSmall")));
removeBtn->setProperty("class",
QVariant(QString::fromUtf8("icon-trash")));
removeBtn->setToolTip(
obs_module_text("FirstRunWizard.seqScenes.removeTooltip"));
removeBtn->setMaximumSize(22, 22);
rowLayout->addWidget(removeBtn);
_rows.append({row, sceneCombo, delayWidget, durationSel, removeBtn});
_stepsLayout->addWidget(row);
UpdateDelayVisibility();
UpdateRemoveButtons();
connect(removeBtn, &QPushButton::clicked, this,
[this, row]() { RemoveStep(row); });
}
void SequenceScenesPage::RemoveStep(QWidget *rowWidget)
{
int idx = -1;
for (int i = 0; i < _rows.size(); ++i) {
if (_rows[i].row == rowWidget) {
idx = i;
break;
}
}
if (idx < 0) {
return;
}
_stepsLayout->removeWidget(_rows[idx].row);
_rows[idx].row->deleteLater();
_rows.removeAt(idx);
UpdateDelayVisibility();
UpdateRemoveButtons();
emit completeChanged();
}
void SequenceScenesPage::UpdateDelayVisibility()
{
for (int i = 0; i < _rows.size(); ++i) {
_rows[i].delayWidget->setVisible(i < _rows.size() - 1);
}
}
void SequenceScenesPage::UpdateRemoveButtons()
{
const bool canRemove = _rows.size() > 1;
for (auto &step : _rows) {
step.remove->setEnabled(canRemove);
}
}
// ===========================================================================
// SequenceReviewPage
// ===========================================================================
SequenceReviewPage::SequenceReviewPage(QWidget *parent,
std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_summary(new QLabel(this)),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.seqReview.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqReview.subtitle"));
detail::setupSummaryLabel(_summary);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void SequenceReviewPage::initializePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
auto *seqPage = qobject_cast<SequenceScenesPage *>(
wizard()->page(PAGE_SEQ_SCENES));
const auto steps = seqPage ? seqPage->GetSteps()
: QVector<QPair<QString, Duration>>{};
QString html =
QString("<p>%1</p>")
.arg(QString(obs_module_text(
"FirstRunWizard.seqReview.trigger"))
.arg(triggerScene.toHtmlEscaped())
.arg(QString::fromStdString(
triggerDuration.ToString())));
html += "<ol>";
for (int i = 0; i < steps.size(); ++i) {
const QString scene = steps[i].first.toHtmlEscaped();
if (i == 0) {
html += "<li>" + scene + "</li>";
} else {
const QString waitStr = QString::fromStdString(
steps[i - 1].second.ToString());
html += "<li>" +
QString(obs_module_text(
"FirstRunWizard.seqReview.step"))
.arg(waitStr)
.arg(scene) +
"</li>";
}
}
html += "</ol>";
_summary->setText(html);
}
bool SequenceReviewPage::validatePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
const std::string name = ("Sequence: " + triggerScene).toStdString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
auto *seqPage = qobject_cast<SequenceScenesPage *>(
wizard()->page(PAGE_SEQ_SCENES));
if (!seqPage) {
return true;
}
const auto steps = seqPage->GetSteps();
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: sequence macro allocation failed");
return true;
}
_macro->SetRunInParallel(true);
// --- Condition ---
OBSDataAutoRelease condData =
BuildSceneConditionData(triggerScene, triggerDuration);
if (!detail::addCondition(_macro.get(), "scene", condData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text("FirstRunWizard.seqReview.errorTitle"),
obs_module_text("FirstRunWizard.seqReview.errorBody"));
return true;
}
// --- Actions: scene_switch interleaved with wait ---
for (int i = 0; i < steps.size(); ++i) {
OBSDataAutoRelease switchData =
BuildSceneSwitchData(steps[i].first);
if (!detail::addAction(_macro.get(), "scene_switch",
switchData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.seqReview.errorTitle"),
obs_module_text(
"FirstRunWizard.seqReview.errorBody"));
return true;
}
const bool isLastStep = (i == steps.size() - 1);
if (!isLastStep) {
OBSDataAutoRelease waitData =
BuildWaitData(steps[i].second);
if (!detail::addAction(_macro.get(), "wait",
waitData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.seqReview.errorTitle"),
obs_module_text(
"FirstRunWizard.seqReview.errorBody"));
return true;
}
}
}
blog(LOG_INFO, "FirstRunWizard: created sequence macro '%s'",
name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@ -0,0 +1,101 @@
#pragma once
#include "duration-control.hpp"
#include "duration.hpp"
#include "first-run-wizard.hpp"
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QVector>
#include <QWidget>
#include <memory>
class QVBoxLayout;
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// SequenceTriggerPage
// Registers wizard field "seqTriggerScene" (QString).
// Trigger duration is read back via GetTriggerDuration().
// ---------------------------------------------------------------------------
class SequenceTriggerPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceTriggerPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_SEQ_SCENES; }
Duration GetTriggerDuration() const;
private:
QComboBox *_sceneCombo;
DurationSelection *_delaySelection;
};
// ---------------------------------------------------------------------------
// SequenceScenesPage
// Lets the user build an ordered list of scenes with delays between them.
// The delay after the last scene is ignored when building the macro.
// ---------------------------------------------------------------------------
struct SequenceStep {
QWidget *row;
QComboBox *scene;
QWidget *delayWidget; // hidden for the last row
DurationSelection *delay;
QPushButton *remove;
};
class SequenceScenesPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceScenesPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_SEQ_REVIEW; }
// Returns (sceneName, delayAfter) pairs. The last entry's delayAfter
// is always a default-constructed Duration and must be ignored by the caller.
QVector<QPair<QString, Duration>> GetSteps() const;
private slots:
void onAddStepClicked();
private:
void AddStep(int defaultSceneIndex = 0);
void RemoveStep(QWidget *rowWidget);
void UpdateDelayVisibility();
void UpdateRemoveButtons();
QLabel *_triggerInfoLabel;
QWidget *_stepsContainer;
QVBoxLayout *_stepsLayout;
QVector<SequenceStep> _rows;
bool _initialized = false;
};
// ---------------------------------------------------------------------------
// SequenceReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class SequenceReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@ -0,0 +1,308 @@
#include "first-run-wizard-window.hpp"
#include "first-run-wizard-helpers.hpp"
#include "macro-settings.hpp"
#include "obs-module-helper.hpp"
#include "platform-funcs.hpp"
#include "selection-helpers.hpp"
#include <obs.hpp>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QRegularExpression>
#include <QVBoxLayout>
namespace advss {
namespace wiz {
static constexpr char kConditionIdWindow[] = "window";
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
static QString DetectFocusedWindow()
{
return QString::fromStdString(GetCurrentWindowTitle());
}
static QString EscapeForRegex(const QString &input)
{
return QRegularExpression::escape(input);
}
// ===========================================================================
// WindowSceneSelectionPage
// ===========================================================================
WindowSceneSelectionPage::WindowSceneSelectionPage(QWidget *parent)
: QWizardPage(parent)
{
setTitle(obs_module_text("FirstRunWizard.scene.title"));
setSubTitle(obs_module_text("FirstRunWizard.scene.subtitle"));
auto label =
new QLabel(obs_module_text("FirstRunWizard.scene.label"), this);
_sceneCombo = new QComboBox(this);
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
// registerField with * suffix means the field is mandatory for Next
registerField("targetScene*", _sceneCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_sceneCombo, 1);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addStretch();
}
void WindowSceneSelectionPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames()) {
_sceneCombo->addItem(name);
}
}
bool WindowSceneSelectionPage::isComplete() const
{
return _sceneCombo->count() > 0 &&
!_sceneCombo->currentText().isEmpty();
}
// ===========================================================================
// WindowConditionPage
// ===========================================================================
WindowConditionPage::WindowConditionPage(QWidget *parent)
: QWizardPage(parent),
_detectTimer(new QTimer(this))
{
setTitle(obs_module_text("FirstRunWizard.window.title"));
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
this);
_windowEdit = new QLineEdit(this);
_windowEdit->setPlaceholderText(
obs_module_text("FirstRunWizard.window.placeholder"));
_autoDetect = new QPushButton(
obs_module_text("FirstRunWizard.window.autoDetect"), this);
_autoDetect->setToolTip(
obs_module_text("FirstRunWizard.window.autoDetectTooltip"));
registerField("windowTitle*", _windowEdit);
connect(_windowEdit, &QLineEdit::textChanged, this,
&QWizardPage::completeChanged);
connect(_autoDetect, &QPushButton::clicked, this,
&WindowConditionPage::onAutoDetectClicked);
connect(_detectTimer, &QTimer::timeout, this,
&WindowConditionPage::onCountdownTick);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_windowEdit, 1);
auto hint =
new QLabel(obs_module_text("FirstRunWizard.window.hint"), this);
hint->setTextFormat(Qt::RichText);
hint->setWordWrap(true);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addWidget(_autoDetect, 0, Qt::AlignLeft);
layout->addWidget(hint);
layout->addStretch();
}
void WindowConditionPage::initializePage()
{
if (_windowEdit->text().isEmpty()) {
QString detected = DetectFocusedWindow();
if (!detected.isEmpty()) {
_windowEdit->setText(detected);
}
}
}
bool WindowConditionPage::isComplete() const
{
return !_windowEdit->text().trimmed().isEmpty();
}
void WindowConditionPage::onAutoDetectClicked()
{
_countdown = 3;
_autoDetect->setEnabled(false);
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
_detectTimer->start(1000);
}
void WindowConditionPage::onCountdownTick()
{
--_countdown;
if (_countdown > 0) {
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
return;
}
_detectTimer->stop();
QString title = DetectFocusedWindow();
if (!title.isEmpty()) {
_windowEdit->setText(title);
}
_autoDetect->setEnabled(true);
_autoDetect->setText(
obs_module_text("FirstRunWizard.window.autoDetect"));
}
// ===========================================================================
// WindowReviewPage
// ===========================================================================
WindowReviewPage::WindowReviewPage(QWidget *parent,
std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.review.title"));
setSubTitle(obs_module_text("FirstRunWizard.review.subtitle"));
_summary = new QLabel(this);
detail::setupSummaryLabel(_summary);
auto layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void WindowReviewPage::initializePage()
{
const QString scene = field("targetScene").toString();
const QString window = field("windowTitle").toString();
_summary->setText(
QString(obs_module_text("FirstRunWizard.review.summary"))
.arg(scene.toHtmlEscaped(), window.toHtmlEscaped()));
}
bool WindowReviewPage::validatePage()
{
const QString scene = field("targetScene").toString();
const QString window = EscapeForRegex(field("windowTitle").toString());
const std::string name = ("Window -> " + scene).toStdString();
// Build condition data blob
// ---------------------------------------------------------------
// Condition blob — mirrors MacroConditionWindow::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "window",
// "checkTitle": true,
// "window": "<user input>",
// "windowRegexConfig": {
// "enable": true, // use regex-style partial matching
// "partial": true, // match anywhere in the title
// "options": 3 // case-insensitive (QRegularExpression flags)
// },
// "focus": true, // only trigger when window is focused
// "version": 1
// }
// ---------------------------------------------------------------
OBSDataAutoRelease condSegment = obs_data_create();
obs_data_set_bool(condSegment, "enabled", true);
obs_data_set_int(condSegment, "version", 1);
OBSDataAutoRelease condRegex = obs_data_create();
obs_data_set_bool(condRegex, "enable", true);
obs_data_set_bool(condRegex, "partial", true);
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
OBSDataAutoRelease condData = obs_data_create();
obs_data_set_obj(condData, "segmentSettings", condSegment);
obs_data_set_string(condData, "id", "window");
obs_data_set_bool(condData, "checkTitle", true);
obs_data_set_string(condData, "window", window.toUtf8().constData());
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
obs_data_set_bool(condData, "focus", true);
obs_data_set_int(condData, "version", 1);
// Build action data blob
// ---------------------------------------------------------------
// Action blob — mirrors MacroActionSwitchScene::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "scene_switch",
// "action": 0, // 0 = switch scene
// "sceneSelection": {
// "type": 0, // 0 = scene by name
// "name": "<scene>",
// "canvasSelection": "Main"
// },
// "transitionType": 1, // 1 = use scene's default transition
// "blockUntilTransitionDone": false,
// "sceneType": 0
// }
// ---------------------------------------------------------------
OBSDataAutoRelease actionSegment = obs_data_create();
obs_data_set_bool(actionSegment, "enabled", true);
obs_data_set_int(actionSegment, "version", 1);
OBSDataAutoRelease sceneSelection = obs_data_create();
obs_data_set_int(sceneSelection, "type", 0);
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
OBSDataAutoRelease actionData = obs_data_create();
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
obs_data_set_string(actionData, "id", "scene_switch");
obs_data_set_int(actionData, "action", 0);
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
obs_data_set_int(actionData, "transitionType", 1);
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
obs_data_set_int(actionData, "sceneType", 0);
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: window macro allocation failed");
return true;
}
if (!detail::addCondition(_macro.get(), kConditionIdWindow, condData) ||
!detail::addAction(_macro.get(), kActionIdSceneSwitch,
actionData)) {
QMessageBox::warning(
this,
obs_module_text("FirstRunWizard.review.errorTitle"),
QString(obs_module_text(
"FirstRunWizard.review.errorBody"))
.arg(window, scene));
_macro.reset();
// Still advance so the user is not stuck.
return true;
}
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@ -0,0 +1,76 @@
#pragma once
#include "first-run-wizard.hpp"
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <memory>
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// WindowSceneSelectionPage
// Registers wizard field "targetScene" (QString).
// ---------------------------------------------------------------------------
class WindowSceneSelectionPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowSceneSelectionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW_CONDITION; }
private:
QComboBox *_sceneCombo;
};
// ---------------------------------------------------------------------------
// WindowConditionPage
// Registers wizard field "windowTitle" (QString).
// Auto-detect button samples the focused window after a countdown.
// ---------------------------------------------------------------------------
class WindowConditionPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowConditionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW_REVIEW; }
private slots:
void onAutoDetectClicked();
void onCountdownTick();
private:
QLineEdit *_windowEdit;
QPushButton *_autoDetect;
QTimer *_detectTimer;
int _countdown = 3;
};
// ---------------------------------------------------------------------------
// WindowReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class WindowReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@ -1,28 +1,17 @@
#include "first-run-wizard.hpp"
#include "first-run-wizard-audio.hpp"
#include "first-run-wizard-sequence.hpp"
#include "first-run-wizard-window.hpp"
#include "log-helper.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include "macro-settings.hpp"
#include "platform-funcs.hpp"
#include "selection-helpers.hpp"
#include <obs-frontend-api.h>
#include <obs-data.h>
#include <util/config-file.h>
#include <QFrame>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QRegularExpression>
#include <QVBoxLayout>
namespace advss {
static constexpr char kConditionIdWindow[] = "window";
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
// ---------------------------------------------------------------------------
// OBS global config helpers
// ---------------------------------------------------------------------------
@ -53,10 +42,10 @@ static void WriteFirstRun(bool value)
config_save_safe(cfg, "tmp", nullptr);
}
static QString DetectFocusedWindow()
{
return QString::fromStdString(GetCurrentWindowTitle());
}
} // namespace advss
namespace advss {
namespace wiz {
// ===========================================================================
// WelcomePage
@ -78,271 +67,39 @@ WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent)
}
// ===========================================================================
// SceneSelectionPage
// TemplatePage
// ===========================================================================
SceneSelectionPage::SceneSelectionPage(QWidget *parent) : QWizardPage(parent)
{
setTitle(obs_module_text("FirstRunWizard.scene.title"));
setSubTitle(obs_module_text("FirstRunWizard.scene.subtitle"));
auto label =
new QLabel(obs_module_text("FirstRunWizard.scene.label"), this);
_sceneCombo = new QComboBox(this);
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
// registerField with * suffix means the field is mandatory for Next
registerField("targetScene*", _sceneCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_sceneCombo, 1);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addStretch();
}
void SceneSelectionPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames())
_sceneCombo->addItem(name);
}
bool SceneSelectionPage::isComplete() const
{
return _sceneCombo->count() > 0 &&
!_sceneCombo->currentText().isEmpty();
}
// ===========================================================================
// WindowConditionPage
// ===========================================================================
WindowConditionPage::WindowConditionPage(QWidget *parent)
TemplatePage::TemplatePage(QWidget *parent)
: QWizardPage(parent),
_detectTimer(new QTimer(this))
_windowRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.window"), this)),
_sequenceRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.sequence"), this)),
_audioRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.audio"), this))
{
setTitle(obs_module_text("FirstRunWizard.window.title"));
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
setTitle(obs_module_text("FirstRunWizard.template.title"));
setSubTitle(obs_module_text("FirstRunWizard.template.subtitle"));
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
this);
_windowEdit = new QLineEdit(this);
_windowEdit->setPlaceholderText(
obs_module_text("FirstRunWizard.window.placeholder"));
_windowRadio->setChecked(true);
_autoDetect = new QPushButton(
obs_module_text("FirstRunWizard.window.autoDetect"), this);
_autoDetect->setToolTip(
obs_module_text("FirstRunWizard.window.autoDetectTooltip"));
registerField("windowTitle*", _windowEdit);
connect(_windowEdit, &QLineEdit::textChanged, this,
&QWizardPage::completeChanged);
connect(_autoDetect, &QPushButton::clicked, this,
&WindowConditionPage::onAutoDetectClicked);
connect(_detectTimer, &QTimer::timeout, this,
&WindowConditionPage::onCountdownTick);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_windowEdit, 1);
auto hint =
new QLabel(obs_module_text("FirstRunWizard.window.hint"), this);
hint->setTextFormat(Qt::RichText);
hint->setWordWrap(true);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addWidget(_autoDetect, 0, Qt::AlignLeft);
layout->addWidget(hint);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_windowRadio);
layout->addWidget(_sequenceRadio);
layout->addWidget(_audioRadio);
layout->addStretch();
}
void WindowConditionPage::initializePage()
int TemplatePage::nextId() const
{
if (_windowEdit->text().isEmpty()) {
QString detected = DetectFocusedWindow();
if (!detected.isEmpty()) {
_windowEdit->setText(detected);
}
if (_sequenceRadio->isChecked()) {
return PAGE_SEQ_TRIGGER;
}
}
bool WindowConditionPage::isComplete() const
{
return !_windowEdit->text().trimmed().isEmpty();
}
void WindowConditionPage::onAutoDetectClicked()
{
_countdown = 3;
_autoDetect->setEnabled(false);
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
_detectTimer->start(1000);
}
void WindowConditionPage::onCountdownTick()
{
--_countdown;
if (_countdown > 0) {
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
return;
if (_audioRadio->isChecked()) {
return PAGE_AUDIO_SOURCE;
}
_detectTimer->stop();
QString title = DetectFocusedWindow();
if (!title.isEmpty()) {
_windowEdit->setText(title);
}
_autoDetect->setEnabled(true);
_autoDetect->setText(
obs_module_text("FirstRunWizard.window.autoDetect"));
}
// ===========================================================================
// ReviewPage
// ===========================================================================
ReviewPage::ReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.review.title"));
setSubTitle(obs_module_text("FirstRunWizard.review.subtitle"));
_summary = new QLabel(this);
_summary->setWordWrap(true);
_summary->setTextFormat(Qt::RichText);
_summary->setFrameShape(QFrame::StyledPanel);
_summary->setContentsMargins(12, 12, 12, 12);
auto layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void ReviewPage::initializePage()
{
const QString scene = field("targetScene").toString();
const QString window = field("windowTitle").toString();
_summary->setText(
QString(obs_module_text("FirstRunWizard.review.summary"))
.arg(scene.toHtmlEscaped(), window.toHtmlEscaped()));
}
static QString escapeForRegex(const QString &input)
{
return QRegularExpression::escape(input);
}
bool ReviewPage::validatePage()
{
const QString scene = field("targetScene").toString();
const QString window = escapeForRegex(field("windowTitle").toString());
const std::string name = ("Window -> " + scene).toStdString();
// Build condition data blob
// ---------------------------------------------------------------
// Condition blob — mirrors MacroConditionWindow::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "window",
// "checkTitle": true,
// "window": "<user input>",
// "windowRegexConfig": {
// "enable": true, // use regex-style partial matching
// "partial": true, // match anywhere in the title
// "options": 3 // case-insensitive (QRegularExpression flags)
// },
// "focus": true, // only trigger when window is focused
// "version": 1
// }
// ---------------------------------------------------------------
OBSDataAutoRelease condSegment = obs_data_create();
obs_data_set_bool(condSegment, "enabled", true);
obs_data_set_int(condSegment, "version", 1);
OBSDataAutoRelease condRegex = obs_data_create();
obs_data_set_bool(condRegex, "enable", true);
obs_data_set_bool(condRegex, "partial", true);
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
OBSDataAutoRelease condData = obs_data_create();
obs_data_set_obj(condData, "segmentSettings", condSegment);
obs_data_set_string(condData, "id", "window");
obs_data_set_bool(condData, "checkTitle", true);
obs_data_set_string(condData, "window", window.toUtf8().constData());
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
obs_data_set_bool(condData, "focus", true);
obs_data_set_int(condData, "version", 1);
// Build action data blob
// ---------------------------------------------------------------
// Action blob — mirrors MacroActionSwitchScene::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "scene_switch",
// "action": 0, // 0 = switch scene
// "sceneSelection": {
// "type": 0, // 0 = scene by name
// "name": "<scene>",
// "canvasSelection": "Main"
// },
// "transitionType": 1, // 1 = use scene's default transition
// "blockUntilTransitionDone": false,
// "sceneType": 0
// }
// ---------------------------------------------------------------
OBSDataAutoRelease actionSegment = obs_data_create();
obs_data_set_bool(actionSegment, "enabled", true);
obs_data_set_int(actionSegment, "version", 1);
OBSDataAutoRelease sceneSelection = obs_data_create();
obs_data_set_int(sceneSelection, "type", 0);
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
OBSDataAutoRelease actionData = obs_data_create();
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
obs_data_set_string(actionData, "id", "scene_switch");
obs_data_set_int(actionData, "action", 0);
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
obs_data_set_int(actionData, "transitionType", 1);
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
obs_data_set_int(actionData, "sceneType", 0);
if (!FirstRunWizard::CreateMacro(_macro, name, kConditionIdWindow,
condData, kActionIdSceneSwitch,
actionData)) {
QMessageBox::warning(
this,
obs_module_text("FirstRunWizard.review.errorTitle"),
QString(obs_module_text(
"FirstRunWizard.review.errorBody"))
.arg(window, scene));
_macro.reset();
// Still advance so the user is not stuck.
}
return true;
return PAGE_WINDOW_SCENE;
}
// ===========================================================================
@ -373,12 +130,19 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
{
setWindowTitle(obs_module_text("FirstRunWizard.windowTitle"));
setWizardStyle(QWizard::ModernStyle);
setMinimumSize(540, 420);
setMinimumSize(600, 420);
setPage(PAGE_WELCOME, new WelcomePage(this));
setPage(PAGE_SCENE, new SceneSelectionPage(this));
setPage(PAGE_WINDOW, new WindowConditionPage(this));
setPage(PAGE_REVIEW, new ReviewPage(this, _macro));
setPage(PAGE_TEMPLATE, new TemplatePage(this));
setPage(PAGE_WINDOW_SCENE, new WindowSceneSelectionPage(this));
setPage(PAGE_WINDOW_CONDITION, new WindowConditionPage(this));
setPage(PAGE_WINDOW_REVIEW, new WindowReviewPage(this, _macro));
setPage(PAGE_SEQ_TRIGGER, new SequenceTriggerPage(this));
setPage(PAGE_SEQ_SCENES, new SequenceScenesPage(this));
setPage(PAGE_SEQ_REVIEW, new SequenceReviewPage(this, _macro));
setPage(PAGE_AUDIO_SOURCE, new AudioSourcePage(this));
setPage(PAGE_AUDIO_TARGET, new AudioTargetPage(this));
setPage(PAGE_AUDIO_REVIEW, new AudioReviewPage(this, _macro));
setPage(PAGE_DONE, new DonePage(this));
setStartId(PAGE_WELCOME);
@ -410,58 +174,5 @@ std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent,
return wizard->_macro;
}
// static
bool FirstRunWizard::CreateMacro(std::shared_ptr<Macro> &macro,
const std::string &macroName,
const std::string &conditionId,
obs_data_t *conditionData,
const std::string &actionId,
obs_data_t *actionData)
{
// 1. Create and register the Macro
macro = std::make_shared<Macro>(macroName, GetGlobalMacroSettings());
if (!macro) {
blog(LOG_WARNING, "FirstRunWizard: Macro allocation failed");
return false;
}
// 2. Instantiate condition via factory, then hydrate via Load()
auto condition =
MacroConditionFactory::Create(conditionId, macro.get());
if (!condition) {
blog(LOG_WARNING,
"FirstRunWizard: condition factory returned null "
"for id '%s' — is the base plugin loaded?",
conditionId.c_str());
return false;
}
if (!condition->Load(conditionData)) {
blog(LOG_WARNING,
"FirstRunWizard: condition Load() failed for id '%s'",
conditionId.c_str());
return false;
}
macro->Conditions().emplace_back(condition);
// 3. Instantiate action via factory, then hydrate via Load()
auto action = MacroActionFactory::Create(actionId, macro.get());
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: action factory returned null "
"for id '%s' — is the base plugin loaded?",
actionId.c_str());
return false;
}
if (!action->Load(actionData)) {
blog(LOG_WARNING,
"FirstRunWizard: action Load() failed for id '%s'",
actionId.c_str());
return false;
}
macro->Actions().emplace_back(action);
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", macroName.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@ -1,31 +1,34 @@
#pragma once
#include <obs-data.h>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <QRadioButton>
#include <QWizard>
#include <QWizardPage>
#include <string>
#include <memory>
namespace advss {
bool IsFirstRun();
class Macro;
bool IsFirstRun();
namespace wiz {
// ---------------------------------------------------------------------------
// Page IDs
// ---------------------------------------------------------------------------
enum WizardPageId {
PAGE_WELCOME = 0,
PAGE_SCENE,
PAGE_WINDOW,
PAGE_REVIEW,
PAGE_TEMPLATE,
PAGE_WINDOW_SCENE,
PAGE_WINDOW_CONDITION,
PAGE_WINDOW_REVIEW,
PAGE_SEQ_TRIGGER,
PAGE_SEQ_SCENES,
PAGE_SEQ_REVIEW,
PAGE_AUDIO_SOURCE,
PAGE_AUDIO_TARGET,
PAGE_AUDIO_REVIEW,
PAGE_DONE,
};
@ -36,64 +39,23 @@ class WelcomePage : public QWizardPage {
Q_OBJECT
public:
explicit WelcomePage(QWidget *parent = nullptr);
int nextId() const override { return PAGE_SCENE; }
int nextId() const override { return PAGE_TEMPLATE; }
};
// ---------------------------------------------------------------------------
// SceneSelectionPage
// Registers wizard field "targetScene" (QString).
// TemplatePage
// Lets the user choose which kind of automation to create.
// ---------------------------------------------------------------------------
class SceneSelectionPage : public QWizardPage {
class TemplatePage : public QWizardPage {
Q_OBJECT
public:
explicit SceneSelectionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW; }
explicit TemplatePage(QWidget *parent = nullptr);
int nextId() const override;
private:
QComboBox *_sceneCombo;
};
// ---------------------------------------------------------------------------
// WindowConditionPage
// Registers wizard field "windowTitle" (QString).
// Auto-detect button samples the focused window after a countdown.
// ---------------------------------------------------------------------------
class WindowConditionPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowConditionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_REVIEW; }
private slots:
void onAutoDetectClicked();
void onCountdownTick();
private:
QLineEdit *_windowEdit;
QPushButton *_autoDetect;
QTimer *_detectTimer;
int _countdown = 3;
};
// ---------------------------------------------------------------------------
// ReviewPage
// Displays a summary and calls FirstRunWizard::CreateMacro() on Finish.
// ---------------------------------------------------------------------------
class ReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit ReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<Macro> &_macro;
QRadioButton *_windowRadio;
QRadioButton *_sequenceRadio;
QRadioButton *_audioRadio;
};
// ---------------------------------------------------------------------------
@ -113,17 +75,14 @@ class FirstRunWizard : public QWizard {
Q_OBJECT
public:
explicit FirstRunWizard(QWidget *parent = nullptr);
static std::shared_ptr<Macro> ShowWizard(QWidget *parent,
bool *wasSkipped = nullptr);
static bool
CreateMacro(std::shared_ptr<Macro> &macro, const std::string &macroName,
const std::string &conditionId, obs_data_t *conditionData,
const std::string &actionId, obs_data_t *actionData);
static std::shared_ptr<advss::Macro>
ShowWizard(QWidget *parent, bool *wasSkipped = nullptr);
private:
void markFirstRunComplete();
std::shared_ptr<Macro> _macro;
std::shared_ptr<advss::Macro> _macro;
};
} // namespace wiz
} // namespace advss