From b3a69fdb3f8699ee2f6ac399fcc03f78d07e7b86 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:44:34 +0200 Subject: [PATCH 1/3] Split first run wizard into multiple files --- CMakeLists.txt | 3 + lib/general.cpp | 4 +- lib/utils/first-run-wizard-helpers.hpp | 85 ++++++ lib/utils/first-run-wizard-window.cpp | 308 ++++++++++++++++++++++ lib/utils/first-run-wizard-window.hpp | 76 ++++++ lib/utils/first-run-wizard.cpp | 352 +------------------------ lib/utils/first-run-wizard.hpp | 90 +------ 7 files changed, 496 insertions(+), 422 deletions(-) create mode 100644 lib/utils/first-run-wizard-helpers.hpp create mode 100644 lib/utils/first-run-wizard-window.cpp create mode 100644 lib/utils/first-run-wizard-window.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 887b53ae..3ef43b9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,6 +209,9 @@ 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-helpers.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 diff --git a/lib/general.cpp b/lib/general.cpp index e872f425..a93f5169 100644 --- a/lib/general.cpp +++ b/lib/general.cpp @@ -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; } diff --git a/lib/utils/first-run-wizard-helpers.hpp b/lib/utils/first-run-wizard-helpers.hpp new file mode 100644 index 00000000..d1c0d108 --- /dev/null +++ b/lib/utils/first-run-wizard-helpers.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include "log-helper.hpp" +#include "macro.hpp" +#include "macro-action-factory.hpp" +#include "macro-condition-factory.hpp" + +#include + +#include +#include + +#include + +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 diff --git a/lib/utils/first-run-wizard-window.cpp b/lib/utils/first-run-wizard-window.cpp new file mode 100644 index 00000000..76ebdeeb --- /dev/null +++ b/lib/utils/first-run-wizard-window.cpp @@ -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 + +#include +#include +#include +#include + +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 ¯o) + : 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": "", + // "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": "", + // "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(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 diff --git a/lib/utils/first-run-wizard-window.hpp b/lib/utils/first-run-wizard-window.hpp new file mode 100644 index 00000000..28d3d245 --- /dev/null +++ b/lib/utils/first-run-wizard-window.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "first-run-wizard.hpp" + +#include +#include +#include +#include +#include + +#include + +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 ¯o); + void initializePage() override; + bool validatePage() override; + int nextId() const override { return PAGE_DONE; } + +private: + QLabel *_summary; + std::shared_ptr &_macro; +}; + +} // namespace wiz +} // namespace advss diff --git a/lib/utils/first-run-wizard.cpp b/lib/utils/first-run-wizard.cpp index 30773d08..3386cde6 100644 --- a/lib/utils/first-run-wizard.cpp +++ b/lib/utils/first-run-wizard.cpp @@ -1,28 +1,15 @@ #include "first-run-wizard.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 -#include #include -#include -#include -#include -#include #include namespace advss { -static constexpr char kConditionIdWindow[] = "window"; -static constexpr char kActionIdSceneSwitch[] = "scene_switch"; - // --------------------------------------------------------------------------- // OBS global config helpers // --------------------------------------------------------------------------- @@ -53,10 +40,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 @@ -77,274 +64,6 @@ WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent) layout->addStretch(); } -// =========================================================================== -// SceneSelectionPage -// =========================================================================== - -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) - : 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")); -} - -// =========================================================================== -// ReviewPage -// =========================================================================== - -ReviewPage::ReviewPage(QWidget *parent, std::shared_ptr ¯o) - : 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": "", - // "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": "", - // "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; -} - // =========================================================================== // DonePage // =========================================================================== @@ -376,9 +95,9 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent) setMinimumSize(540, 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_WINDOW_SCENE, new WindowSceneSelectionPage(this)); + setPage(PAGE_WINDOW_CONDITION, new WindowConditionPage(this)); + setPage(PAGE_WINDOW_REVIEW, new WindowReviewPage(this, _macro)); setPage(PAGE_DONE, new DonePage(this)); setStartId(PAGE_WELCOME); @@ -410,58 +129,5 @@ std::shared_ptr FirstRunWizard::ShowWizard(QWidget *parent, return wizard->_macro; } -// static -bool FirstRunWizard::CreateMacro(std::shared_ptr ¯o, - const std::string ¯oName, - 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(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 diff --git a/lib/utils/first-run-wizard.hpp b/lib/utils/first-run-wizard.hpp index 842d9b58..457d33fc 100644 --- a/lib/utils/first-run-wizard.hpp +++ b/lib/utils/first-run-wizard.hpp @@ -1,31 +1,27 @@ #pragma once -#include - -#include -#include -#include -#include -#include +#include #include #include -#include +#include namespace advss { +bool IsFirstRun(); + class Macro; -bool IsFirstRun(); +namespace wiz { // --------------------------------------------------------------------------- // Page IDs // --------------------------------------------------------------------------- enum WizardPageId { PAGE_WELCOME = 0, - PAGE_SCENE, - PAGE_WINDOW, - PAGE_REVIEW, + PAGE_WINDOW_SCENE, + PAGE_WINDOW_CONDITION, + PAGE_WINDOW_REVIEW, PAGE_DONE, }; @@ -36,64 +32,7 @@ class WelcomePage : public QWizardPage { Q_OBJECT public: explicit WelcomePage(QWidget *parent = nullptr); - int nextId() const override { return PAGE_SCENE; } -}; - -// --------------------------------------------------------------------------- -// SceneSelectionPage -// Registers wizard field "targetScene" (QString). -// --------------------------------------------------------------------------- -class SceneSelectionPage : public QWizardPage { - Q_OBJECT -public: - explicit SceneSelectionPage(QWidget *parent = nullptr); - void initializePage() override; - bool isComplete() const override; - int nextId() const override { return PAGE_WINDOW; } - -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 ¯o); - void initializePage() override; - bool validatePage() override; - int nextId() const override { return PAGE_DONE; } - -private: - QLabel *_summary; - std::shared_ptr &_macro; + int nextId() const override { return PAGE_WINDOW_SCENE; } }; // --------------------------------------------------------------------------- @@ -113,17 +52,14 @@ class FirstRunWizard : public QWizard { Q_OBJECT public: explicit FirstRunWizard(QWidget *parent = nullptr); - static std::shared_ptr ShowWizard(QWidget *parent, - bool *wasSkipped = nullptr); - static bool - CreateMacro(std::shared_ptr ¯o, const std::string ¯oName, - const std::string &conditionId, obs_data_t *conditionData, - const std::string &actionId, obs_data_t *actionData); + static std::shared_ptr + ShowWizard(QWidget *parent, bool *wasSkipped = nullptr); private: void markFirstRunComplete(); - std::shared_ptr _macro; + std::shared_ptr _macro; }; +} // namespace wiz } // namespace advss From 19253ace4b55ddb832f397bc90c410f0f7ddec31 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:16:51 +0200 Subject: [PATCH 2/3] Add "Sequence" option to the first run wizard --- CMakeLists.txt | 2 + data/locale/en-US.ini | 27 +- lib/utils/first-run-wizard-sequence.cpp | 499 ++++++++++++++++++++++++ lib/utils/first-run-wizard-sequence.hpp | 101 +++++ lib/utils/first-run-wizard.cpp | 37 +- lib/utils/first-run-wizard.hpp | 21 +- 6 files changed, 684 insertions(+), 3 deletions(-) create mode 100644 lib/utils/first-run-wizard-sequence.cpp create mode 100644 lib/utils/first-run-wizard-sequence.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ef43b9c..ef29d3b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,8 @@ target_sources( lib/utils/first-run-wizard.cpp lib/utils/first-run-wizard.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 diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0e6761f6..e7e71792 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -2679,7 +2679,12 @@ 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="

Advanced Scene Switcher lets you build Macros - rules of the form:

When [condition] -> perform [action]

This wizard creates a macro that switches to a chosen OBS scene whenever a specific window comes into focus.

You can skip at any time. Re-open this wizard later from the General tab inside the Advanced Scene Switcher dialog.

" +FirstRunWizard.welcome.body="

Advanced Scene Switcher lets you build Macros - rules of the form:

When [condition] -> perform [action]

This wizard will guide you through creating your first automation. On the next page, choose the type of macro you want to set up.

You can skip at any time. Re-open this wizard later from the General tab inside the Advanced Scene Switcher dialog.

" + +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.scene.title="Choose a Target Scene" FirstRunWizard.scene.subtitle="Which OBS scene should become active when your chosen window comes into focus?" @@ -2700,6 +2705,26 @@ FirstRunWizard.review.summary="Macro name: Window -> %1

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 %1 for at least %2:" +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="

You can view and edit it on the Macros tab of the Advanced Scene Switcher dialog.

To learn more:

" diff --git a/lib/utils/first-run-wizard-sequence.cpp b/lib/utils/first-run-wizard-sequence.cpp new file mode 100644 index 00000000..0a7ed82c --- /dev/null +++ b/lib/utils/first-run-wizard-sequence.cpp @@ -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 + +#include +#include +#include +#include +#include + +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": +// }, +// "sceneSelection": { "type": 0, "name": "", "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": "", "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": , +// "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( + 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(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> SequenceScenesPage::GetSteps() const +{ + QVector> 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(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 ¯o) + : 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( + wizard()->page(PAGE_SEQ_TRIGGER)); + const Duration triggerDuration = + triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0); + + auto *seqPage = qobject_cast( + wizard()->page(PAGE_SEQ_SCENES)); + const auto steps = seqPage ? seqPage->GetSteps() + : QVector>{}; + + QString html = + QString("

%1

") + .arg(QString(obs_module_text( + "FirstRunWizard.seqReview.trigger")) + .arg(triggerScene.toHtmlEscaped()) + .arg(QString::fromStdString( + triggerDuration.ToString()))); + + html += "
    "; + for (int i = 0; i < steps.size(); ++i) { + const QString scene = steps[i].first.toHtmlEscaped(); + if (i == 0) { + html += "
  1. " + scene + "
  2. "; + } else { + const QString waitStr = QString::fromStdString( + steps[i - 1].second.ToString()); + html += "
  3. " + + QString(obs_module_text( + "FirstRunWizard.seqReview.step")) + .arg(waitStr) + .arg(scene) + + "
  4. "; + } + } + html += "
"; + + _summary->setText(html); +} + +bool SequenceReviewPage::validatePage() +{ + const QString triggerScene = field("seqTriggerScene").toString(); + const std::string name = ("Sequence: " + triggerScene).toStdString(); + + auto *triggerPage = qobject_cast( + wizard()->page(PAGE_SEQ_TRIGGER)); + const Duration triggerDuration = + triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0); + + auto *seqPage = qobject_cast( + wizard()->page(PAGE_SEQ_SCENES)); + if (!seqPage) { + return true; + } + const auto steps = seqPage->GetSteps(); + + _macro = std::make_shared(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 diff --git a/lib/utils/first-run-wizard-sequence.hpp b/lib/utils/first-run-wizard-sequence.hpp new file mode 100644 index 00000000..7789e741 --- /dev/null +++ b/lib/utils/first-run-wizard-sequence.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "duration-control.hpp" +#include "duration.hpp" +#include "first-run-wizard.hpp" + +#include +#include +#include +#include +#include + +#include + +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> 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 _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 ¯o); + void initializePage() override; + bool validatePage() override; + int nextId() const override { return PAGE_DONE; } + +private: + QLabel *_summary; + std::shared_ptr &_macro; +}; + +} // namespace wiz +} // namespace advss diff --git a/lib/utils/first-run-wizard.cpp b/lib/utils/first-run-wizard.cpp index 3386cde6..45310c62 100644 --- a/lib/utils/first-run-wizard.cpp +++ b/lib/utils/first-run-wizard.cpp @@ -1,4 +1,5 @@ #include "first-run-wizard.hpp" +#include "first-run-wizard-sequence.hpp" #include "first-run-wizard-window.hpp" #include "macro.hpp" @@ -64,6 +65,36 @@ WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent) layout->addStretch(); } +// =========================================================================== +// TemplatePage +// =========================================================================== + +TemplatePage::TemplatePage(QWidget *parent) + : QWizardPage(parent), + _windowRadio(new QRadioButton( + obs_module_text("FirstRunWizard.template.window"), this)), + _sequenceRadio(new QRadioButton( + obs_module_text("FirstRunWizard.template.sequence"), this)) +{ + setTitle(obs_module_text("FirstRunWizard.template.title")); + setSubTitle(obs_module_text("FirstRunWizard.template.subtitle")); + + _windowRadio->setChecked(true); + + auto *layout = new QVBoxLayout(this); + layout->addWidget(_windowRadio); + layout->addWidget(_sequenceRadio); + layout->addStretch(); +} + +int TemplatePage::nextId() const +{ + if (_sequenceRadio->isChecked()) { + return PAGE_SEQ_TRIGGER; + } + return PAGE_WINDOW_SCENE; +} + // =========================================================================== // DonePage // =========================================================================== @@ -92,12 +123,16 @@ 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_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_DONE, new DonePage(this)); setStartId(PAGE_WELCOME); diff --git a/lib/utils/first-run-wizard.hpp b/lib/utils/first-run-wizard.hpp index 457d33fc..4aba3d48 100644 --- a/lib/utils/first-run-wizard.hpp +++ b/lib/utils/first-run-wizard.hpp @@ -19,9 +19,13 @@ namespace wiz { // --------------------------------------------------------------------------- enum WizardPageId { PAGE_WELCOME = 0, + PAGE_TEMPLATE, PAGE_WINDOW_SCENE, PAGE_WINDOW_CONDITION, PAGE_WINDOW_REVIEW, + PAGE_SEQ_TRIGGER, + PAGE_SEQ_SCENES, + PAGE_SEQ_REVIEW, PAGE_DONE, }; @@ -32,7 +36,22 @@ class WelcomePage : public QWizardPage { Q_OBJECT public: explicit WelcomePage(QWidget *parent = nullptr); - int nextId() const override { return PAGE_WINDOW_SCENE; } + int nextId() const override { return PAGE_TEMPLATE; } +}; + +// --------------------------------------------------------------------------- +// TemplatePage +// Lets the user choose which kind of automation to create. +// --------------------------------------------------------------------------- +class TemplatePage : public QWizardPage { + Q_OBJECT +public: + explicit TemplatePage(QWidget *parent = nullptr); + int nextId() const override; + +private: + QRadioButton *_windowRadio; + QRadioButton *_sequenceRadio; }; // --------------------------------------------------------------------------- From 41ed2101e3247da99b8ebae6ab194ac6cc2d2d94 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:38:52 +0200 Subject: [PATCH 3/3] Add "Audio" option to the first run wizard --- CMakeLists.txt | 2 + data/locale/en-US.ini | 16 + lib/utils/first-run-wizard-audio.cpp | 425 +++++++++++++++++++++++++++ lib/utils/first-run-wizard-audio.hpp | 83 ++++++ lib/utils/first-run-wizard.cpp | 12 +- lib/utils/first-run-wizard.hpp | 4 + 6 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 lib/utils/first-run-wizard-audio.cpp create mode 100644 lib/utils/first-run-wizard-audio.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef29d3b1..aec82262 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,6 +209,8 @@ 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 diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e7e71792..d73b999a 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -2685,6 +2685,22 @@ 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="When %1 output volume is above %2 dB for %3:

  Show %4 on the current scene

Otherwise:

  Hide %4 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?" diff --git a/lib/utils/first-run-wizard-audio.cpp b/lib/utils/first-run-wizard-audio.cpp new file mode 100644 index 00000000..c13a1d35 --- /dev/null +++ b/lib/utils/first-run-wizard-audio.cpp @@ -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 +#include + +#include +#include +#include +#include + +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": +// }, +// "audioSource": { "type": 0, "name": "" }, +// "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": , "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::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(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(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 ¯o) + : 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( + 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( + 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(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 diff --git a/lib/utils/first-run-wizard-audio.hpp b/lib/utils/first-run-wizard-audio.hpp new file mode 100644 index 00000000..3d733def --- /dev/null +++ b/lib/utils/first-run-wizard-audio.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "duration-control.hpp" +#include "duration.hpp" +#include "first-run-wizard.hpp" +#include "volume-control.hpp" + +#include +#include +#include +#include + +#include + +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 ¯o); + void initializePage() override; + bool validatePage() override; + int nextId() const override { return PAGE_DONE; } + +private: + QLabel *_summary; + std::shared_ptr &_macro; +}; + +} // namespace wiz +} // namespace advss diff --git a/lib/utils/first-run-wizard.cpp b/lib/utils/first-run-wizard.cpp index 45310c62..9fe2edf8 100644 --- a/lib/utils/first-run-wizard.cpp +++ b/lib/utils/first-run-wizard.cpp @@ -1,4 +1,5 @@ #include "first-run-wizard.hpp" +#include "first-run-wizard-audio.hpp" #include "first-run-wizard-sequence.hpp" #include "first-run-wizard-window.hpp" @@ -74,7 +75,9 @@ TemplatePage::TemplatePage(QWidget *parent) _windowRadio(new QRadioButton( obs_module_text("FirstRunWizard.template.window"), this)), _sequenceRadio(new QRadioButton( - obs_module_text("FirstRunWizard.template.sequence"), this)) + obs_module_text("FirstRunWizard.template.sequence"), this)), + _audioRadio(new QRadioButton( + obs_module_text("FirstRunWizard.template.audio"), this)) { setTitle(obs_module_text("FirstRunWizard.template.title")); setSubTitle(obs_module_text("FirstRunWizard.template.subtitle")); @@ -84,6 +87,7 @@ TemplatePage::TemplatePage(QWidget *parent) auto *layout = new QVBoxLayout(this); layout->addWidget(_windowRadio); layout->addWidget(_sequenceRadio); + layout->addWidget(_audioRadio); layout->addStretch(); } @@ -92,6 +96,9 @@ int TemplatePage::nextId() const if (_sequenceRadio->isChecked()) { return PAGE_SEQ_TRIGGER; } + if (_audioRadio->isChecked()) { + return PAGE_AUDIO_SOURCE; + } return PAGE_WINDOW_SCENE; } @@ -133,6 +140,9 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent) 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); diff --git a/lib/utils/first-run-wizard.hpp b/lib/utils/first-run-wizard.hpp index 4aba3d48..4ddde3b7 100644 --- a/lib/utils/first-run-wizard.hpp +++ b/lib/utils/first-run-wizard.hpp @@ -26,6 +26,9 @@ enum WizardPageId { PAGE_SEQ_TRIGGER, PAGE_SEQ_SCENES, PAGE_SEQ_REVIEW, + PAGE_AUDIO_SOURCE, + PAGE_AUDIO_TARGET, + PAGE_AUDIO_REVIEW, PAGE_DONE, }; @@ -52,6 +55,7 @@ public: private: QRadioButton *_windowRadio; QRadioButton *_sequenceRadio; + QRadioButton *_audioRadio; }; // ---------------------------------------------------------------------------