#include "macro-action-scene-visibility.hpp" #include "layout-helpers.hpp" #include "macro-helpers.hpp" #include "plugin-state-helpers.hpp" #include "sync-helpers.hpp" #include "transition-helpers.hpp" #include namespace advss { const std::string MacroActionSceneVisibility::id = "scene_visibility"; bool MacroActionSceneVisibility::_registered = MacroActionFactory::Register( MacroActionSceneVisibility::id, {MacroActionSceneVisibility::Create, MacroActionSceneVisibilityEdit::Create, "AdvSceneSwitcher.action.sceneVisibility"}); const static std::map actionTypes = { {MacroActionSceneVisibility::Action::SHOW, "AdvSceneSwitcher.action.sceneVisibility.type.show"}, {MacroActionSceneVisibility::Action::HIDE, "AdvSceneSwitcher.action.sceneVisibility.type.hide"}, {MacroActionSceneVisibility::Action::TOGGLE, "AdvSceneSwitcher.action.sceneVisibility.type.toggle"}, }; namespace { struct TransitionRestoreContext; static std::unordered_map restoreContexts; static std::mutex restoreMutex; struct TransitionRestoreContext { public: obs_sceneitem_t *item; bool wasVisible; OBSSource originalTransition = nullptr; uint32_t originalDuration; obs_source_t *transition = nullptr; signal_handler_t *sh = nullptr; }; } // namespace static void handleShutdown(enum obs_frontend_event event, void *) { if (event != OBS_FRONTEND_EVENT_EXIT) { return; } std::lock_guard lock(restoreMutex); for (const auto &[_, ctx] : restoreContexts) { obs_source_release(ctx->transition); } restoreContexts.clear(); } static bool setup() { obs_frontend_add_event_callback(handleShutdown, nullptr); return true; } static bool setupDone = setup(); static void handleSourceDestroyed(void *param, calldata_t *) { if (OBSIsShuttingDown()) { return; } auto ctx = static_cast(param); ctx->sh = nullptr; ctx->originalTransition = nullptr; delete ctx; } static void resetSceneItemTransition(void *param, calldata_t *) { if (OBSIsShuttingDown()) { return; } auto ctx = static_cast(param); if (!ctx) { return; } SetSceneItemTransition(ctx->item, ctx->originalTransition, !ctx->wasVisible); obs_sceneitem_set_transition_duration(ctx->item, !ctx->wasVisible, ctx->originalDuration); signal_handler_disconnect(ctx->sh, "transition_stop", resetSceneItemTransition, ctx); signal_handler_disconnect(ctx->sh, "destroy", handleSourceDestroyed, ctx); { std::lock_guard lock(restoreMutex); restoreContexts.erase(ctx->item); } obs_source_release(ctx->transition); delete ctx; } static void attachRestoreContext(obs_sceneitem_t *item, obs_source_t *transition, bool itemWasVisible, OBSSource originalTransition, uint32_t originalDuration) { signal_handler_t *sh = obs_source_get_signal_handler(transition); if (!sh) { return; } std::lock_guard lock(restoreMutex); auto ctx = new TransitionRestoreContext{ item, itemWasVisible, originalTransition, originalDuration, obs_source_get_ref(transition), sh, }; auto it = restoreContexts.find(item); if (it != restoreContexts.end()) { auto *oldCtx = it->second; signal_handler_disconnect(oldCtx->sh, "transition_stop", resetSceneItemTransition, oldCtx); signal_handler_disconnect(oldCtx->sh, "destroy", handleSourceDestroyed, oldCtx); ctx->originalTransition = oldCtx->originalTransition; ctx->originalDuration = oldCtx->originalDuration; obs_source_release(oldCtx->transition); delete oldCtx; restoreContexts.erase(it); } restoreContexts[item] = ctx; signal_handler_connect(sh, "transition_stop", resetSceneItemTransition, ctx); signal_handler_connect(sh, "destroy", handleSourceDestroyed, ctx); } static obs_source_t * setSceneItemVisibility(obs_sceneitem_t *item, const bool setTransition, const OBSWeakSource &transitionWeak, const bool setDuration, const Duration &duration, MacroActionSceneVisibility::Action action) { const OBSSourceAutoRelease transition = OBSGetStrongRef(transitionWeak); const bool itemIsVisible = obs_sceneitem_visible(item); const OBSSource currentTransition = obs_sceneitem_get_transition(item, !itemIsVisible); const uint32_t currentTransitionDuration = obs_sceneitem_get_transition_duration(item, !itemIsVisible); OBSSource privateTransitionSource = nullptr; if (setTransition) { privateTransitionSource = SetSceneItemTransition( item, transition, !itemIsVisible); } else { privateTransitionSource = obs_sceneitem_get_transition(item, !itemIsVisible); } if (setDuration) { obs_sceneitem_set_transition_duration(item, !itemIsVisible, duration.Milliseconds()); } switch (action) { case MacroActionSceneVisibility::Action::SHOW: obs_sceneitem_set_visible(item, true); break; case MacroActionSceneVisibility::Action::HIDE: obs_sceneitem_set_visible(item, false); break; case MacroActionSceneVisibility::Action::TOGGLE: obs_sceneitem_set_visible(item, !itemIsVisible); break; } if (!setTransition && !setDuration) { return privateTransitionSource; } if (!privateTransitionSource) { if (setTransition) { SetSceneItemTransition(item, currentTransition, !itemIsVisible); } if (setDuration) { obs_sceneitem_set_transition_duration( item, !itemIsVisible, currentTransitionDuration); } return nullptr; } auto sh = obs_source_get_signal_handler(privateTransitionSource); if (!sh) { return nullptr; } attachRestoreContext(item, privateTransitionSource, itemIsVisible, currentTransition, currentTransitionDuration); return privateTransitionSource; } namespace { struct TransitionWaitItem { obs_sceneitem_t *sceneItem; obs_source_t *expectedSource; bool show; std::atomic *pendingCount; std::atomic done{false}; void markDone() { bool expected = false; if (done.compare_exchange_strong(expected, true)) { (*pendingCount)--; GetMacroTransitionCV().notify_all(); } } }; } // namespace static void onTransitionDone(void *data, calldata_t *) { static_cast(data)->markDone(); } static void waitForSceneItemTransitions( std::atomic &pendingCount, const std::vector> &waitItems, Macro *macro) { using namespace std::chrono_literals; SetMacroAbortWait(false); std::unique_lock lock(*GetMutex()); while (pendingCount > 0 && !MacroWaitShouldAbort() && !MacroIsStopped(macro) && !OBSIsShuttingDown()) { GetMacroTransitionCV().wait_for(lock, 100ms); // Detect transitions replaced mid-wait (e.g. user sets // transition to "None" via OBS context menu). In that // case transition_stop never fires, so we poll. for (const auto &wi : waitItems) { if (wi->done) { continue; } obs_source_t *current = obs_sceneitem_get_transition( wi->sceneItem, wi->show); if (current != wi->expectedSource) { wi->markDone(); } } } } bool MacroActionSceneVisibility::PerformAction() { auto items = _source.GetSceneItems(_scene); if (!_blockUntilTransitionDone) { for (const auto &item : items) { setSceneItemVisibility(item, _updateTransition, _transition.GetTransition(), _updateDuration, _duration, _action); } return true; } std::atomic pendingCount(0); std::vector transitionSources; std::vector> waitItems; std::vector signalConnections; for (const auto &item : items) { const bool itemWasVisible = obs_sceneitem_visible(item); bool targetVisible; switch (_action) { case Action::SHOW: targetVisible = true; break; case Action::HIDE: targetVisible = false; break; case Action::TOGGLE: targetVisible = !itemWasVisible; break; default: continue; } if (itemWasVisible == targetVisible) { // No-op: visibility won't change, no transition will play setSceneItemVisibility(item, _updateTransition, _transition.GetTransition(), _updateDuration, _duration, _action); continue; } auto *source = setSceneItemVisibility( item, _updateTransition, _transition.GetTransition(), _updateDuration, _duration, _action); if (!source) { continue; } auto *sh = obs_source_get_signal_handler(source); if (!sh) { continue; } transitionSources.emplace_back(obs_source_get_ref(source)); ++pendingCount; auto wi = std::make_unique(); wi->sceneItem = item; wi->expectedSource = source; wi->show = !itemWasVisible; wi->pendingCount = &pendingCount; signalConnections.emplace_back(sh, "transition_stop", onTransitionDone, wi.get()); waitItems.push_back(std::move(wi)); } if (pendingCount == 0) { return true; } waitForSceneItemTransitions(pendingCount, waitItems, GetMacro()); return !MacroWaitShouldAbort(); } void MacroActionSceneVisibility::LogAction() const { auto it = actionTypes.find(_action); if (it != actionTypes.end()) { ablog(LOG_INFO, "performed visibility action \"%s\" for source \"%s\" on scene \"%s\"", it->second.c_str(), _source.ToString(true).c_str(), _scene.ToString(true).c_str()); } else { blog(LOG_WARNING, "ignored unknown SceneVisibility action %d", static_cast(_action)); } } bool MacroActionSceneVisibility::Save(obs_data_t *obj) const { MacroAction::Save(obj); _scene.Save(obj); _source.Save(obj); obs_data_set_bool(obj, "updateTransition", _updateTransition); _transition.Save(obj); obs_data_set_bool(obj, "updateDuration", _updateDuration); _duration.Save(obj); obs_data_set_int(obj, "action", static_cast(_action)); obs_data_set_bool(obj, "blockUntilTransitionDone", _blockUntilTransitionDone); return true; } bool MacroActionSceneVisibility::Load(obs_data_t *obj) { // Convert old data format // TODO: Remove in future version if (obs_data_has_user_value(obj, "source")) { auto sourceName = obs_data_get_string(obj, "source"); obs_data_set_string(obj, "sceneItem", sourceName); } MacroAction::Load(obj); _scene.Load(obj); _source.Load(obj); _updateTransition = obs_data_get_bool(obj, "updateTransition"); _transition.Load(obj); _updateDuration = obs_data_get_bool(obj, "updateDuration"); _duration.Load(obj); _action = static_cast( obs_data_get_int(obj, "action")); _blockUntilTransitionDone = obs_data_get_bool(obj, "blockUntilTransitionDone"); // TODO: Remove in future version if (obs_data_get_int(obj, "sourceType") != 0) { auto sourceGroup = obs_data_get_string(obj, "sourceGroup"); _source.SetSourceTypeSelection(sourceGroup); } return true; } std::string MacroActionSceneVisibility::GetShortDesc() const { if (!_source.ToString().empty()) { return _scene.ToString() + " - " + _source.ToString(); } return ""; } std::shared_ptr MacroActionSceneVisibility::Create(Macro *m) { return std::make_shared(m); } std::shared_ptr MacroActionSceneVisibility::Copy() const { return std::make_shared(*this); } void MacroActionSceneVisibility::ResolveVariablesToFixedValues() { _scene.ResolveVariables(); _source.ResolveVariables(); } static inline void populateActionSelection(QComboBox *list) { for (const auto &entry : actionTypes) { list->addItem(obs_module_text(entry.second.c_str())); } } MacroActionSceneVisibilityEdit::MacroActionSceneVisibilityEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _scenes(new SceneSelectionWidget(this, true, false, true, true)), _sources(new SceneItemSelectionWidget( parent, { SceneItemSelection::Type::SOURCE_NAME, SceneItemSelection::Type::VARIABLE_NAME, SceneItemSelection::Type::SOURCE_NAME_PATTERN, SceneItemSelection::Type::SOURCE_GROUP, SceneItemSelection::Type::SOURCE_TYPE, SceneItemSelection::Type::INDEX, SceneItemSelection::Type::INDEX_RANGE, SceneItemSelection::Type::ALL, }, SceneItemSelectionWidget::NameClashMode::ALL)), _updateTransition(new QCheckBox(this)), _transitions(new TransitionSelectionWidget(this, false, false)), _updateDuration(new QCheckBox(this)), _duration(new DurationSelection(this, false)), _durationLayout(new QHBoxLayout), _blockUntilTransitionDone(new QCheckBox( obs_module_text( "AdvSceneSwitcher.action.sceneVisibility.blockUntilTransitionDone"), this)), _actions(new QComboBox()) { populateActionSelection(_actions); _duration->SpinBox()->setSpecialValueText("-"); QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this, SLOT(ActionChanged(int))); QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)), this, SLOT(SceneChanged(const SceneSelection &))); QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)), _sources, SLOT(SceneChanged(const SceneSelection &))); QWidget::connect(_sources, SIGNAL(SceneItemChanged(const SceneItemSelection &)), this, SLOT(SourceChanged(const SceneItemSelection &))); QWidget::connect(_updateTransition, SIGNAL(stateChanged(int)), this, SLOT(UpdateTransitionChanged(int))); QWidget::connect(_transitions, SIGNAL(TransitionChanged(const TransitionSelection &)), this, SLOT(TransitionChanged(const TransitionSelection &))); QWidget::connect(_updateDuration, SIGNAL(stateChanged(int)), this, SLOT(UpdateDurationChanged(int))); QWidget::connect(_duration, SIGNAL(DurationChanged(const Duration &)), this, SLOT(DurationChanged(const Duration &))); QWidget::connect(_blockUntilTransitionDone, SIGNAL(stateChanged(int)), this, SLOT(BlockUntilTransitionDoneChanged(int))); auto sceneItemLayout = new QHBoxLayout; PlaceWidgets(obs_module_text( "AdvSceneSwitcher.action.sceneVisibility.layout"), sceneItemLayout, {{"{{scenes}}", _scenes}, {"{{sources}}", _sources}, {"{{actions}}", _actions}}); auto transitionLayout = new QHBoxLayout; PlaceWidgets( obs_module_text( "AdvSceneSwitcher.action.sceneVisibility.layout.transition"), transitionLayout, {{"{{updateTransition}}", _updateTransition}, {"{{transitions}}", _transitions}}); PlaceWidgets( obs_module_text( "AdvSceneSwitcher.action.sceneVisibility.layout.duration"), _durationLayout, {{"{{updateDuration}}", _updateDuration}, {"{{duration}}", _duration}}); auto layout = new QVBoxLayout; layout->addLayout(sceneItemLayout); layout->addLayout(transitionLayout); layout->addLayout(_durationLayout); layout->addWidget(_blockUntilTransitionDone); setLayout(layout); _entryData = entryData; UpdateEntryData(); SetWidgetVisibility(); _loading = false; } void MacroActionSceneVisibilityEdit::UpdateEntryData() { if (!_entryData) { return; } _actions->setCurrentIndex(static_cast(_entryData->_action)); _scenes->SetScene(_entryData->_scene); _sources->SetSceneItem(_entryData->_source); _updateTransition->setChecked(_entryData->_updateTransition); _transitions->SetTransition(_entryData->_transition); _updateDuration->setChecked(_entryData->_updateDuration); _duration->SetDuration(_entryData->_duration); _blockUntilTransitionDone->setChecked( _entryData->_blockUntilTransitionDone); } void MacroActionSceneVisibilityEdit::SceneChanged(const SceneSelection &s) { GUARD_LOADING_AND_LOCK(); _entryData->_scene = s; } void MacroActionSceneVisibilityEdit::SourceChanged( const SceneItemSelection &item) { GUARD_LOADING_AND_LOCK(); _entryData->_source = item; emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); adjustSize(); updateGeometry(); } void MacroActionSceneVisibilityEdit::UpdateTransitionChanged(int state) { GUARD_LOADING_AND_LOCK(); _entryData->_updateTransition = state; SetWidgetVisibility(); } void MacroActionSceneVisibilityEdit::TransitionChanged( const TransitionSelection &t) { GUARD_LOADING_AND_LOCK(); _entryData->_transition = t; SetWidgetVisibility(); } void MacroActionSceneVisibilityEdit::UpdateDurationChanged(int state) { GUARD_LOADING_AND_LOCK(); _entryData->_updateDuration = state; SetWidgetVisibility(); } void MacroActionSceneVisibilityEdit::DurationChanged(const Duration &dur) { GUARD_LOADING_AND_LOCK(); _entryData->_duration = dur; } void MacroActionSceneVisibilityEdit::ActionChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_action = static_cast(value); } void MacroActionSceneVisibilityEdit::BlockUntilTransitionDoneChanged(int state) { GUARD_LOADING_AND_LOCK(); _entryData->_blockUntilTransitionDone = state; } void MacroActionSceneVisibilityEdit::SetWidgetVisibility() { const bool hideDurationSelection = _entryData->_updateTransition && IsFixedLengthTransition( _entryData->_transition.GetTransition()); SetLayoutVisible(_durationLayout, !hideDurationSelection); _transitions->setEnabled(_entryData->_updateTransition); _duration->setEnabled(_entryData->_updateDuration); adjustSize(); updateGeometry(); } } // namespace advss