mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-03-22 01:44:49 -05:00
466 lines
14 KiB
C++
466 lines
14 KiB
C++
#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 <obs-frontend-api.h>
|
|
#include <obs-data.h>
|
|
#include <util/config-file.h>
|
|
|
|
#include <QFrame>
|
|
#include <QHBoxLayout>
|
|
#include <QMessageBox>
|
|
#include <QRegularExpression>
|
|
#include <QVBoxLayout>
|
|
|
|
namespace advss {
|
|
|
|
static constexpr char kConditionIdWindow[] = "window";
|
|
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OBS global config helpers
|
|
// ---------------------------------------------------------------------------
|
|
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<Macro> ¯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": "<user input>",
|
|
// "windowRegexConfig": {
|
|
// "enable": true, // use regex-style partial matching
|
|
// "partial": true, // match anywhere in the title
|
|
// "options": 3 // case-insensitive (QRegularExpression flags)
|
|
// },
|
|
// "focus": true, // only trigger when window is focused
|
|
// "version": 1
|
|
// }
|
|
// ---------------------------------------------------------------
|
|
OBSDataAutoRelease condSegment = obs_data_create();
|
|
obs_data_set_bool(condSegment, "enabled", true);
|
|
obs_data_set_int(condSegment, "version", 1);
|
|
|
|
OBSDataAutoRelease condRegex = obs_data_create();
|
|
obs_data_set_bool(condRegex, "enable", true);
|
|
obs_data_set_bool(condRegex, "partial", true);
|
|
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
|
|
|
|
OBSDataAutoRelease condData = obs_data_create();
|
|
obs_data_set_obj(condData, "segmentSettings", condSegment);
|
|
obs_data_set_string(condData, "id", "window");
|
|
obs_data_set_bool(condData, "checkTitle", true);
|
|
obs_data_set_string(condData, "window", window.toUtf8().constData());
|
|
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
|
|
obs_data_set_bool(condData, "focus", true);
|
|
obs_data_set_int(condData, "version", 1);
|
|
|
|
// Build action data blob
|
|
// ---------------------------------------------------------------
|
|
// Action blob — mirrors MacroActionSwitchScene::Save() output:
|
|
//
|
|
// {
|
|
// "segmentSettings": { "enabled": true, "version": 1 },
|
|
// "id": "scene_switch",
|
|
// "action": 0, // 0 = switch scene
|
|
// "sceneSelection": {
|
|
// "type": 0, // 0 = scene by name
|
|
// "name": "<scene>",
|
|
// "canvasSelection": "Main"
|
|
// },
|
|
// "transitionType": 1, // 1 = use scene's default transition
|
|
// "blockUntilTransitionDone": false,
|
|
// "sceneType": 0
|
|
// }
|
|
// ---------------------------------------------------------------
|
|
OBSDataAutoRelease actionSegment = obs_data_create();
|
|
obs_data_set_bool(actionSegment, "enabled", true);
|
|
obs_data_set_int(actionSegment, "version", 1);
|
|
|
|
OBSDataAutoRelease sceneSelection = obs_data_create();
|
|
obs_data_set_int(sceneSelection, "type", 0);
|
|
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
|
|
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
|
|
|
|
OBSDataAutoRelease actionData = obs_data_create();
|
|
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
|
|
obs_data_set_string(actionData, "id", "scene_switch");
|
|
obs_data_set_int(actionData, "action", 0);
|
|
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
|
|
obs_data_set_int(actionData, "transitionType", 1);
|
|
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
|
|
obs_data_set_int(actionData, "sceneType", 0);
|
|
|
|
if (!FirstRunWizard::CreateMacro(_macro, name, kConditionIdWindow,
|
|
condData, kActionIdSceneSwitch,
|
|
actionData)) {
|
|
QMessageBox::warning(
|
|
this,
|
|
obs_module_text("FirstRunWizard.review.errorTitle"),
|
|
QString(obs_module_text(
|
|
"FirstRunWizard.review.errorBody"))
|
|
.arg(window, scene));
|
|
_macro.reset();
|
|
// Still advance so the user is not stuck.
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 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<Macro> FirstRunWizard::ShowWizard(QWidget *parent)
|
|
{
|
|
auto wizard = new FirstRunWizard(parent);
|
|
wizard->exec();
|
|
wizard->deleteLater();
|
|
return wizard->_macro;
|
|
}
|
|
|
|
// static
|
|
bool FirstRunWizard::CreateMacro(std::shared_ptr<Macro> ¯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<Macro>(macroName, GetGlobalMacroSettings());
|
|
if (!macro) {
|
|
blog(LOG_WARNING, "FirstRunWizard: Macro allocation failed");
|
|
return false;
|
|
}
|
|
|
|
// 2. Instantiate condition via factory, then hydrate via Load()
|
|
auto condition =
|
|
MacroConditionFactory::Create(conditionId, macro.get());
|
|
if (!condition) {
|
|
blog(LOG_WARNING,
|
|
"FirstRunWizard: condition factory returned null "
|
|
"for id '%s' — is the base plugin loaded?",
|
|
conditionId.c_str());
|
|
return false;
|
|
}
|
|
if (!condition->Load(conditionData)) {
|
|
blog(LOG_WARNING,
|
|
"FirstRunWizard: condition Load() failed for id '%s'",
|
|
conditionId.c_str());
|
|
return false;
|
|
}
|
|
macro->Conditions().emplace_back(condition);
|
|
|
|
// 3. Instantiate action via factory, then hydrate via Load()
|
|
auto action = MacroActionFactory::Create(actionId, macro.get());
|
|
if (!action) {
|
|
blog(LOG_WARNING,
|
|
"FirstRunWizard: action factory returned null "
|
|
"for id '%s' — is the base plugin loaded?",
|
|
actionId.c_str());
|
|
return false;
|
|
}
|
|
if (!action->Load(actionData)) {
|
|
blog(LOG_WARNING,
|
|
"FirstRunWizard: action Load() failed for id '%s'",
|
|
actionId.c_str());
|
|
return false;
|
|
}
|
|
macro->Actions().emplace_back(action);
|
|
|
|
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", macroName.c_str());
|
|
return true;
|
|
}
|
|
|
|
} // namespace advss
|