#include "first-run-wizard.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 // --------------------------------------------------------------------------- static constexpr char kConfigSection[] = "AdvancedSceneSwitcher"; static constexpr char kFirstRunKey[] = "firstRun"; bool IsFirstRun() { #if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0) config_t *cfg = obs_frontend_get_user_config(); #else config_t *cfg = obs_frontend_get_global_config(); #endif if (!config_has_user_value(cfg, kConfigSection, kFirstRunKey)) { return true; } return config_get_bool(cfg, kConfigSection, kFirstRunKey); } static void WriteFirstRun(bool value) { #if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0) config_t *cfg = obs_frontend_get_user_config(); #else config_t *cfg = obs_frontend_get_global_config(); #endif config_set_bool(cfg, kConfigSection, kFirstRunKey, value); config_save_safe(cfg, "tmp", nullptr); } static QString DetectFocusedWindow() { std::string title; GetCurrentWindowTitle(title); return QString::fromStdString(title); } // =========================================================================== // WelcomePage // =========================================================================== WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent) { setTitle(obs_module_text("FirstRunWizard.welcome.title")); setSubTitle(obs_module_text("FirstRunWizard.welcome.subtitle")); auto body = new QLabel(obs_module_text("FirstRunWizard.welcome.body"), this); body->setWordWrap(true); body->setTextFormat(Qt::RichText); auto layout = new QVBoxLayout(this); layout->addWidget(body); 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 // =========================================================================== DonePage::DonePage(QWidget *parent) : QWizardPage(parent) { setTitle(obs_module_text("FirstRunWizard.done.title")); setSubTitle(obs_module_text("FirstRunWizard.done.subtitle")); auto body = new QLabel(obs_module_text("FirstRunWizard.done.body"), this); body->setWordWrap(true); body->setTextFormat(Qt::RichText); body->setOpenExternalLinks(true); auto layout = new QVBoxLayout(this); layout->addWidget(body); layout->addStretch(); } // =========================================================================== // FirstRunWizard // =========================================================================== FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent) { setWindowTitle(obs_module_text("FirstRunWizard.windowTitle")); setWizardStyle(QWizard::ModernStyle); 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_DONE, new DonePage(this)); setStartId(PAGE_WELCOME); setOption(QWizard::NoBackButtonOnLastPage, true); setOption(QWizard::NoCancelButtonOnLastPage, true); // Mark done on both Accept (Finish) and Reject (Cancel / close) connect(this, &QWizard::accepted, this, &FirstRunWizard::markFirstRunComplete); connect(this, &QWizard::rejected, this, &FirstRunWizard::markFirstRunComplete); } void FirstRunWizard::markFirstRunComplete() { WriteFirstRun(false); } // static std::shared_ptr FirstRunWizard::ShowWizard(QWidget *parent) { auto wizard = new FirstRunWizard(parent); wizard->exec(); wizard->deleteLater(); 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 advss