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