Compare commits

...

19 Commits

Author SHA1 Message Date
WarmUpTill
5dfed8ac2a CI: Switch to OBS 27.1.3 2021-11-20 05:37:57 -08:00
WarmUpTill
d7102d395e Ensure contents of comboboxes are readable when changing scene/filter 2021-11-20 03:52:54 -08:00
WarmUpTill
82a41694a1 Allow interacting with OBS main window while plugin settings are opened 2021-11-20 03:52:54 -08:00
WarmUpTill
a1cae263c1 Add macro action "random" (#353)
This should be expanded upon by adding the ability to add weights to each macro.
2021-11-20 12:52:12 +01:00
WarmUpTill
cce6e240c2 Add option to fade in/out volume change 2021-11-17 12:30:10 -08:00
WarmUpTill
9d04b4d1a0 Move setLayoutVisible() to utility 2021-11-17 12:30:10 -08:00
WarmUpTill
fd7e48eff9 Add scene-visibility macro condition 2021-11-17 12:29:57 -08:00
WarmUpTill
0350a46004 Allow use of "previous scene" 2021-11-17 12:29:57 -08:00
WarmUpTill
00fec4b573 Fix macro hotkeys not working 2021-11-17 12:29:41 -08:00
WarmUpTill
c40a3af8ca Fix add, delete, reorder not working when changing macro segment type 2021-11-17 12:06:53 -08:00
WarmUpTill
e452d8cc8c Allow selecting source type for scene visibility action
This should allow to simplify some setups which required showing /
hiding all sources of a particular type.
2021-11-12 10:05:54 -08:00
WarmUpTill
fbc9818764 Fix macro count not updating correctly
The underlying issue are the potential deadlocks when triggering
frontent function calls while saving / loading is triggered in parallel
like it can happen when switching scene collections.

Hopefully the new frontend events allow for a cleaner solution.
2021-11-12 10:05:11 -08:00
WarmUpTill
5ef09f661f Show warning if macro is paused
And clean up timer handling for this condition type
2021-11-12 10:05:11 -08:00
WarmUpTill
2155a942f3 Do not check conditions of paused macros 2021-11-12 10:05:11 -08:00
WarmUpTill
5af206da60 Fix typo 2021-11-09 10:24:14 -08:00
WarmUpTill
924f68df64 Fix typo 2021-11-07 11:14:25 +01:00
WarmUpTill
2f3c4a6dae Cleanup 2021-11-06 22:30:26 +01:00
WarmUpTill
f955c07bda Fix warnings 2021-11-01 11:23:43 -07:00
WarmUpTill
6f1cd3cbcc Enable building without opencv 2021-11-01 11:23:43 -07:00
31 changed files with 1098 additions and 128 deletions

View File

@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
env:
PLUGIN_NAME: SceneSwitcher
LIB_NAME: advanced-scene-switcher
OBS_TAG: 27.1.1
OBS_TAG: 27.1.3
jobs:
macos64:

View File

@@ -147,7 +147,6 @@ if(UNIX AND NOT APPLE)
if(NOT PROCPS_LIBRARY)
message(FATAL_ERROR "procps lib not found - please set PROCPS_LIBRARY")
endif()
message (STATUS "WHAT THE FUCK IS GOING ON ${PROCPS_LIBRARY}")
link_libraries(${X11_LIBRARIES} ${procps_LIBRARIES})
include_directories("${X11_INCLUDE_DIR}" "${X11_Xtst_INCLUDE_PATH}"
"${X11_Xss_INCLUDE_PATH}" "${PROCPS_INCLUDE_DIR}")
@@ -176,10 +175,6 @@ if(WIN32)
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
endif()
# opencv
find_package(OpenCV REQUIRED)
include_directories("${OpenCV_INCLUDE_DIRS}")
# Setup QT tools
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
@@ -220,6 +215,7 @@ set(advanced-scene-switcher_HEADERS
src/headers/macro-action-media.hpp
src/headers/macro-action-plugin-state.hpp
src/headers/macro-action-preview-scene.hpp
src/headers/macro-action-random.hpp
src/headers/macro-action-recording.hpp
src/headers/macro-action-replay-buffer.hpp
src/headers/macro-action-run.hpp
@@ -250,6 +246,7 @@ set(advanced-scene-switcher_HEADERS
src/headers/macro-condition-replay-buffer.hpp
src/headers/macro-condition-scene-order.hpp
src/headers/macro-condition-scene-transform.hpp
src/headers/macro-condition-scene-visibility.hpp
src/headers/macro-condition-scene.hpp
src/headers/macro-condition-source.hpp
src/headers/macro-condition-streaming.hpp
@@ -311,6 +308,7 @@ set(advanced-scene-switcher_SOURCES
src/macro-action-media.cpp
src/macro-action-plugin-state.cpp
src/macro-action-preview-scene.cpp
src/macro-action-random.cpp
src/macro-action-recording.cpp
src/macro-action-replay-buffer.cpp
src/macro-action-run.cpp
@@ -341,6 +339,7 @@ set(advanced-scene-switcher_SOURCES
src/macro-condition-replay-buffer.cpp
src/macro-condition-scene-order.cpp
src/macro-condition-scene-transform.cpp
src/macro-condition-scene-visibility.cpp
src/macro-condition-scene.cpp
src/macro-condition-source.cpp
src/macro-condition-streaming.cpp
@@ -365,6 +364,22 @@ set(advanced-scene-switcher_SOURCES
src/volume-control.cpp
src/version.cpp)
# opencv
find_package(OpenCV)
if(OpenCV_FOUND)
include_directories("${OpenCV_INCLUDE_DIRS}")
else()
set(OpenCV_LIBRARIES "")
message(
WARNING
"OpenCV not found! Functionality relying on OpenCV will be disabled!\nOpenCV sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/deps/opencv"
)
list(REMOVE_ITEM advanced-scene-switcher_SOURCES
src/macro-condition-video.cpp)
list(REMOVE_ITEM advanced-scene-switcher_HEADERS
src/headers/macro-condition-video.hpp)
endif()
add_library(
advanced-scene-switcher MODULE
${advanced-scene-switcher_HEADERS}

View File

@@ -45,7 +45,7 @@ AdvSceneSwitcher.generalTab.priority.fileContent="Datei Inhalt"
AdvSceneSwitcher.generalTab.priority.sceneSequence="Szenenfolge"
AdvSceneSwitcher.generalTab.priority.idleDetection="Inaktivitätserkennung"
AdvSceneSwitcher.generalTab.priority.executable="Prozess"
AdvSceneSwitcher.generalTab.priority.screenRegion="Bildschrimbereich"
AdvSceneSwitcher.generalTab.priority.screenRegion="Bildschirmbereich"
AdvSceneSwitcher.generalTab.priority.windowTitle="Fenstername"
AdvSceneSwitcher.generalTab.priority.media="Medien"
AdvSceneSwitcher.generalTab.priority.time="Zeit"
@@ -98,7 +98,7 @@ AdvSceneSwitcher.executableTab.entry="Wenn {{processes}} läuft wechsle zu {{sce
AdvSceneSwitcher.executableTab.help="Dieser Tab erlaubt es Szenen zu wechseln, wenn bestimmte Prozesse laufen.\nDas kann nützlich sein, wenn sich der Name eines Fensters ändern kann oder dieser nicht bekannt ist.\n\nKlicke auf das markierte Plus Symbol, um einen neuen Eintrag hinzuzufügen."
; Screen Region Tab
AdvSceneSwitcher.screenRegionTab.title="Bildschrimbereich"
AdvSceneSwitcher.screenRegionTab.title="Bildschirmbereich"
AdvSceneSwitcher.screenRegionTab.currentPosition="Mauszeiger ist bei:"
AdvSceneSwitcher.screenRegionTab.showGuideFrames="Hilfsrahmen einblenden"
AdvSceneSwitcher.screenRegionTab.hideGuideFrames="Hilfsrahmen ausblenden"

View File

@@ -96,7 +96,7 @@ AdvSceneSwitcher.condition.cursor.entry.line2="Cursor is currently at {{xPos}} x
AdvSceneSwitcher.condition.scene="Scene"
AdvSceneSwitcher.condition.scene.type.current="Current scene is"
AdvSceneSwitcher.condition.scene.type.previous="Previous scene is"
AdvSceneSwitcher.condition.scene.type.changed="Scene chagned"
AdvSceneSwitcher.condition.scene.type.changed="Scene changed"
AdvSceneSwitcher.condition.scene.type.notChanged="Scene has not changed"
AdvSceneSwitcher.condition.scene.waitForTransition="Wait for transition to complete"
AdvSceneSwitcher.condition.scene.entry="{{sceneType}} {{scenes}} {{waitForTransition}}"
@@ -172,6 +172,7 @@ AdvSceneSwitcher.condition.macro.count.type.below="Less than"
AdvSceneSwitcher.condition.macro.count.type.above="More than"
AdvSceneSwitcher.condition.macro.count.type.equal="Exactly"
AdvSceneSwitcher.condition.macro.count.reset="Reset"
AdvSceneSwitcher.condition.macro.pausedWarning="Selected macro is currently paused!"
AdvSceneSwitcher.condition.macro.state.entry="Conditions of {{macros}} are true"
AdvSceneSwitcher.condition.macro.count.entry.line1="{{macros}} was executed {{conditions}} {{count}} times"
AdvSceneSwitcher.condition.macro.count.entry.line2="Current count: {{currentCount}} {{resetCount}}"
@@ -232,6 +233,10 @@ AdvSceneSwitcher.condition.transition.type.started="Transition active"
AdvSceneSwitcher.condition.transition.type.ended="Transition ended"
AdvSceneSwitcher.condition.transition.durationSuffix="seconds"
AdvSceneSwitcher.condition.transition.entry="{{conditions}}{{transitions}}{{duration}}{{durationSuffix}}"
AdvSceneSwitcher.condition.sceneVisibility="Scene item visibility"
AdvSceneSwitcher.condition.sceneVisibility.type.shown="Shown"
AdvSceneSwitcher.condition.sceneVisibility.type.hidden="Hidden"
AdvSceneSwitcher.condition.sceneVisibility.entry="On {{scenes}} {{sources}} is {{conditions}} "
; Macro Actions
AdvSceneSwitcher.action.switchScene="Switch scene"
@@ -246,6 +251,7 @@ AdvSceneSwitcher.action.audio.type.mute="Mute"
AdvSceneSwitcher.action.audio.type.unmute="Unmute"
AdvSceneSwitcher.action.audio.type.sourceVolume="Set source volume"
AdvSceneSwitcher.action.audio.type.masterVolume="Set master volume"
AdvSceneSwitcher.action.audio.fade="{{fade}}Fade over {{duration}} seconds"
AdvSceneSwitcher.action.audio.entry="{{actions}} {{audioSources}} {{volume}}"
AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
@@ -271,7 +277,9 @@ AdvSceneSwitcher.action.run.entry="Run {{filePath}}"
AdvSceneSwitcher.action.sceneVisibility="Scene item visibility"
AdvSceneSwitcher.action.sceneVisibility.type.show="Show"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Hide"
AdvSceneSwitcher.action.sceneVisibility.entry="On {{scenes}} {{actions}} {{sources}}"
AdvSceneSwitcher.action.sceneVisibility.type.source="Source"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Any"
AdvSceneSwitcher.action.sceneVisibility.entry="On {{scenes}} {{actions}} {{sourceTypes}} {{sources}}"
AdvSceneSwitcher.action.filter="Filter"
AdvSceneSwitcher.action.filter.type.enable="Enable"
AdvSceneSwitcher.action.filter.type.disable="Disable"
@@ -345,6 +353,10 @@ AdvSceneSwitcher.action.timer.type.continue="Continue"
AdvSceneSwitcher.action.timer.type.reset="Reset"
AdvSceneSwitcher.action.timer.type.setTimeRemaining="Set time remaining of"
AdvSceneSwitcher.action.timer.entry="{{timerAction}} timers on {{macros}} {{duration}}"
AdvSceneSwitcher.action.random="Random"
AdvSceneSwitcher.action.random.arguments="Macros:"
AdvSceneSwitcher.action.random.addArgument="Add Macro"
AdvSceneSwitcher.action.random.entry="Run {{macroSelection}}"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"

View File

@@ -20,6 +20,7 @@ SwitcherData *switcher = nullptr;
AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
: QDialog(parent), ui(new Ui_AdvSceneSwitcher)
{
switcher->settingsWindowOpened = true;
ui->setupUi(this);
std::lock_guard<std::mutex> lock(switcher->m);
@@ -28,6 +29,13 @@ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
loadUI();
}
AdvSceneSwitcher::~AdvSceneSwitcher()
{
if (switcher) {
switcher->settingsWindowOpened = false;
}
}
bool translationAvailable()
{
return !!strcmp(obs_module_text("AdvSceneSwitcher.pluginName"),
@@ -422,7 +430,7 @@ void SwitcherData::Start()
void ResetMacroCounters()
{
for (auto &m : switcher->macros) {
m.ResetCount();
m->ResetCount();
}
}
@@ -444,6 +452,13 @@ void SwitcherData::Stop()
server.stop();
client.disconnect();
for (auto &t : audioHelperThreads) {
if (t.joinable()) {
t.join();
}
}
audioHelperThreads.clear();
if (showSystemTrayNotifications) {
DisplayTrayMessage(
obs_module_text("AdvSceneSwitcher.pluginName"),
@@ -628,6 +643,8 @@ static void OBSEvent(enum obs_frontend_event event, void *switcher)
}
}
AdvSceneSwitcher *ssWindow;
extern "C" void InitSceneSwitcher()
{
blog(LOG_INFO, "version: %s", g_GIT_TAG);
@@ -645,11 +662,16 @@ extern "C" void InitSceneSwitcher()
PlatformInit();
auto cb = []() {
QMainWindow *window =
(QMainWindow *)obs_frontend_get_main_window();
AdvSceneSwitcher ss(window);
ss.exec();
if (switcher->settingsWindowOpened) {
ssWindow->show();
ssWindow->raise();
ssWindow->activateWindow();
} else {
ssWindow = new AdvSceneSwitcher(
(QMainWindow *)obs_frontend_get_main_window());
ssWindow->setAttribute(Qt::WA_DeleteOnClose);
ssWindow->show();
}
};
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);

View File

@@ -5,7 +5,7 @@
#include <QFileDialog>
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent)
: _type(type), QWidget(parent)
: QWidget(parent), _type(type)
{
_filePath = new QLineEdit();
_browseButton =

View File

@@ -186,6 +186,9 @@ void AdvSceneSwitcher::on_toggleStartButton_clicked()
void AdvSceneSwitcher::closeEvent(QCloseEvent *)
{
if (!switcher) {
return;
}
switcher->windowPos = this->pos();
switcher->windowSize = this->size();

View File

@@ -29,6 +29,7 @@ public:
bool currentStatusActive = false;
AdvSceneSwitcher(QWidget *parent);
~AdvSceneSwitcher();
void reject() override;
void closeEvent(QCloseEvent *event) override;
@@ -52,8 +53,8 @@ public:
void SetEditMacro(Macro &m);
void HighlightAction(int idx);
void HighlightCondition(int idx);
void PopulateMacroActions(Macro &m, int afterIdx = 0);
void PopulateMacroConditions(Macro &m, int afterIdx = 0);
void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0);
void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0);
void ConnectControlSignals(MacroActionEdit *);
void ConnectControlSignals(MacroConditionEdit *);
void SwapActions(Macro *m, int pos1, int pos2);

View File

@@ -1,7 +1,10 @@
#pragma once
#include <QSpinBox>
#include <QHBoxLayout>
#include "macro-action-edit.hpp"
#include "duration-control.hpp"
#include <QSpinBox>
#include <QCheckBox>
#include <QHBoxLayout>
enum class AudioAction {
MUTE,
@@ -26,6 +29,8 @@ public:
OBSWeakSource _audioSource;
AudioAction _action = AudioAction::MUTE;
int _volume = 0;
bool _fade = false;
Duration _duration;
private:
static bool _registered;
@@ -53,6 +58,8 @@ private slots:
void SourceChanged(const QString &text);
void ActionChanged(int value);
void VolumeChanged(int value);
void FadeChanged(int value);
void DurationChanged(double seconds);
signals:
void HeaderInfoChanged(const QString &);
@@ -60,9 +67,11 @@ protected:
QComboBox *_audioSources;
QComboBox *_actions;
QSpinBox *_volumePercent;
QCheckBox *_fade;
DurationSelection *_duration;
QHBoxLayout *_fadeLayout;
std::shared_ptr<MacroActionAudio> _entryData;
private:
QHBoxLayout *_mainLayout;
bool _loading = true;
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include "macro-action-edit.hpp"
#include "macro-selection.hpp"
#include <QPushButton>
#include <QListWidget>
#include <unordered_map>
class MacroActionRandom : public MultiMacroRefAction {
public:
bool PerformAction();
void LogAction();
bool Save(obs_data_t *obj);
bool Load(obs_data_t *obj);
std::string GetId() { return id; };
static std::shared_ptr<MacroAction> Create()
{
return std::make_shared<MacroActionRandom>();
}
// TODO: add weights to each macro ...
// std::unordered_map<MacroRef, int> _weights;
private:
MacroRef lastRandomMacro;
static bool _registered;
static const std::string id;
};
class MacroActionRandomEdit : public QWidget {
Q_OBJECT
public:
MacroActionRandomEdit(
QWidget *parent,
std::shared_ptr<MacroActionRandom> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionRandomEdit(
parent,
std::dynamic_pointer_cast<MacroActionRandom>(action));
}
private slots:
void MacroRemove(const QString &name);
void MacroRename(const QString &oldName, const QString &newName);
void AddMacro();
void RemoveMacro();
int FindEntry(const std::string &macro);
void MacroSelectionChanged(int idx);
protected:
std::shared_ptr<MacroActionRandom> _entryData;
private:
MacroSelection *_macroSelection;
QListWidget *_macroList;
QPushButton *_add;
QPushButton *_remove;
bool _loading = true;
};

View File

@@ -8,6 +8,11 @@ enum class SceneVisibilityAction {
HIDE,
};
enum class SceneItemSourceType {
SOURCE,
SOURCE_GROUP,
};
class MacroActionSceneVisibility : public MacroAction {
public:
bool PerformAction();
@@ -22,7 +27,9 @@ public:
}
SceneSelection _scene;
SceneItemSourceType _sourceType = SceneItemSourceType::SOURCE;
OBSWeakSource _source;
std::string _sourceGroup = "";
SceneVisibilityAction _action = SceneVisibilityAction::SHOW;
private:
@@ -49,6 +56,7 @@ public:
private slots:
void SceneChanged(const SceneSelection &);
void SourceTypeChanged(int value);
void SourceChanged(const QString &text);
void ActionChanged(int value);
signals:
@@ -56,6 +64,7 @@ signals:
protected:
SceneSelectionWidget *_scenes;
QComboBox *_sourceTypes;
QComboBox *_sources;
QComboBox *_actions;
std::shared_ptr<MacroActionSceneVisibility> _entryData;

View File

@@ -67,6 +67,7 @@ private slots:
void ConditionChanged(int cond);
void ResetClicked();
void UpdateCount();
void UpdatePaused();
signals:
void HeaderInfoChanged(const QString &);
@@ -76,16 +77,17 @@ protected:
QComboBox *_counterConditions;
QSpinBox *_count;
QLabel *_currentCount;
QLabel *_pausedWarning;
QPushButton *_resetCount;
QHBoxLayout *_settingsLine1;
QHBoxLayout *_settingsLine2;
std::unique_ptr<QTimer> _timer;
QTimer _countTimer;
QTimer _pausedTimer;
std::shared_ptr<MacroConditionMacro> _entryData;
private:
void ClearLayouts();
void SetupStateWidgets();
void SetupCountWidgets();
void ResetTimer();
bool _loading = true;
};

View File

@@ -0,0 +1,66 @@
#pragma once
#include "macro.hpp"
#include "scene-selection.hpp"
#include <QComboBox>
enum class SceneVisibilityCondition {
SHOWN,
HIDDEN,
};
class MacroConditionSceneVisibility : public MacroCondition {
public:
bool CheckCondition();
bool Save(obs_data_t *obj);
bool Load(obs_data_t *obj);
std::string GetShortDesc();
std::string GetId() { return id; };
static std::shared_ptr<MacroCondition> Create()
{
return std::make_shared<MacroConditionSceneVisibility>();
}
SceneSelection _scene;
OBSWeakSource _source;
SceneVisibilityCondition _condition = SceneVisibilityCondition::SHOWN;
private:
static bool _registered;
static const std::string id;
};
class MacroConditionSceneVisibilityEdit : public QWidget {
Q_OBJECT
public:
MacroConditionSceneVisibilityEdit(
QWidget *parent,
std::shared_ptr<MacroConditionSceneVisibility> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionSceneVisibilityEdit(
parent,
std::dynamic_pointer_cast<MacroConditionSceneVisibility>(
cond));
}
private slots:
void SceneChanged(const SceneSelection &);
void SourceChanged(const QString &text);
void ConditionChanged(int cond);
signals:
void HeaderInfoChanged(const QString &);
protected:
SceneSelectionWidget *_scenes;
QComboBox *_sources;
QComboBox *_conditions;
std::shared_ptr<MacroConditionSceneVisibility> _entryData;
private:
bool _loading = true;
};

View File

@@ -77,7 +77,7 @@ public:
bool Matched() { return _matched; }
std::string Name() { return _name; }
void SetName(const std::string &name);
void SetPaused(bool pause = true) { _paused = pause; }
void SetPaused(bool pause = true);
bool Paused() { return _paused; }
int GetCount() { return _count; };
void ResetCount() { _count = 0; };
@@ -102,6 +102,7 @@ private:
void SetupHotkeys();
void ClearHotkeys();
void SetHotkeysDesc();
void ResetTimers();
std::string _name = "";
std::deque<std::shared_ptr<MacroCondition>> _conditions;
@@ -144,3 +145,9 @@ public:
void ResolveMacroRef();
MacroRef _macro;
};
class MultiMacroRefAction : public MacroAction {
public:
void ResolveMacroRef();
std::vector<MacroRef> _macros;
};

View File

@@ -6,6 +6,7 @@
#include <QDateTime>
#include <QThread>
#include <curl/curl.h>
#include <unordered_map>
#include "scene-group.hpp"
#include "scene-trigger.hpp"
@@ -63,6 +64,8 @@ class SwitcherThread;
struct SwitcherData {
SwitcherThread *th = nullptr;
bool settingsWindowOpened = false;
std::condition_variable cv;
std::mutex m;
bool transitionActive = false;
@@ -92,10 +95,14 @@ struct SwitcherData {
StartupBehavior startupBehavior = PERSIST;
AutoStartEvent autoStartEvent = AutoStartEvent::NEVER;
std::vector<std::thread> audioHelperThreads;
std::atomic_bool masterAudioFadeActive = {false};
std::unordered_map<std::string, std::atomic_bool> activeAudioFades;
Duration cooldown;
std::chrono::high_resolution_clock::time_point lastMatchTime;
std::deque<Macro> macros;
std::deque<std::shared_ptr<Macro>> macros;
std::condition_variable macroWaitCv;
std::atomic_bool abortMacroWait = {false};
bool macroSceneSwitched = false;

View File

@@ -45,6 +45,7 @@ void placeWidgets(std::string text, QBoxLayout *layout,
bool addStretch = true);
void deleteLayoutItem(QLayoutItem *item);
void clearLayout(QLayout *layout, int afterIdx = 0);
void setLayoutVisible(QLayout *layout, bool visible);
QMetaObject::Connection PulseWidget(QWidget *widget, QColor startColor,
QColor = QColor(0, 0, 0, 0),
QString specifier = "QLabel ",
@@ -78,3 +79,4 @@ void populateFilterSelection(QComboBox *list,
void populateSceneItemSelection(QComboBox *list,
OBSWeakSource sceneWeakSource = nullptr);
void populateSceneItemSelection(QComboBox *list, SceneSelection &s);
void populateSourceTypeSelection(QComboBox *list);

View File

@@ -18,6 +18,94 @@ const static std::map<AudioAction, std::string> actionTypes = {
"AdvSceneSwitcher.action.audio.type.masterVolume"},
};
constexpr auto fadeInterval = std::chrono::milliseconds(100);
constexpr float minFade = 0.000001f;
void fadeSourceVolume(Duration duration, float vol, OBSWeakSource audioSource)
{
auto s = obs_weak_source_get_source(audioSource);
if (!s) {
return;
}
float curVol = obs_source_get_volume(s);
obs_source_release(s);
bool volIncrease = curVol <= vol;
int nrSteps = duration.seconds * 1000 / fadeInterval.count();
float volDiff = (volIncrease) ? vol - curVol : curVol - vol;
float volStep = volDiff / nrSteps;
if (volStep < minFade) {
switcher->activeAudioFades[GetWeakSourceName(audioSource)] =
false;
return;
}
for (int step = 0; step < nrSteps && !switcher->stop; ++step) {
auto s = obs_weak_source_get_source(audioSource);
if (!s) {
return;
}
curVol = (volIncrease) ? curVol + volStep : curVol - volStep;
obs_source_set_volume(s, curVol);
std::this_thread::sleep_for(fadeInterval);
obs_source_release(s);
}
switcher->activeAudioFades[GetWeakSourceName(audioSource)] = false;
}
void fadeMasterVolume(Duration duration, float vol)
{
float curVol = obs_get_master_volume();
bool volIncrease = curVol <= vol;
int nrSteps = duration.seconds * 1000 / fadeInterval.count();
float volDiff = (volIncrease) ? vol - curVol : curVol - vol;
float volStep = volDiff / nrSteps;
if (volStep < minFade) {
switcher->masterAudioFadeActive = false;
return;
}
for (int step = 0; step < nrSteps && !switcher->stop; ++step) {
curVol = (volIncrease) ? curVol + volStep : curVol - volStep;
obs_set_master_volume(curVol);
std::this_thread::sleep_for(fadeInterval);
}
switcher->masterAudioFadeActive = false;
}
void startSourceFade(Duration &duration, float vol, OBSWeakSource audioSource)
{
if (!audioSource) {
return;
}
auto it =
switcher->activeAudioFades.find(GetWeakSourceName(audioSource));
if (it != switcher->activeAudioFades.end() && it->second == true) {
blog(LOG_WARNING,
"Audio fade for volume of %s already active! New fade request will be ignored!",
GetWeakSourceName(audioSource).c_str());
return;
}
switcher->activeAudioFades[GetWeakSourceName(audioSource)] = true;
switcher->audioHelperThreads.emplace_back(fadeSourceVolume, duration,
vol, audioSource);
}
void startMasterFade(Duration &duration, float vol)
{
if (switcher->masterAudioFadeActive) {
blog(LOG_WARNING,
"Audio fade for master volume already active! New fade request will be ignored!");
return;
}
switcher->masterAudioFadeActive = true;
switcher->audioHelperThreads.emplace_back(fadeMasterVolume, duration,
vol);
}
bool MacroActionAudio::PerformAction()
{
auto s = obs_weak_source_get_source(_audioSource);
@@ -29,10 +117,19 @@ bool MacroActionAudio::PerformAction()
obs_source_set_muted(s, false);
break;
case AudioAction::SOURCE_VOLUME:
obs_source_set_volume(s, (float)_volume / 100.0f);
if (_fade) {
startSourceFade(_duration, (float)_volume / 100.0f,
_audioSource);
} else {
obs_source_set_volume(s, (float)_volume / 100.0f);
}
break;
case AudioAction::MASTER_VOLUME:
obs_set_master_volume((float)_volume / 100.0f);
if (_fade) {
startMasterFade(_duration, (float)_volume / 100.0f);
} else {
obs_set_master_volume((float)_volume / 100.0f);
}
break;
default:
break;
@@ -46,9 +143,10 @@ void MacroActionAudio::LogAction()
auto it = actionTypes.find(_action);
if (it != actionTypes.end()) {
vblog(LOG_INFO,
"performed action \"%s\" for source \"%s\" with volume %d",
"performed action \"%s\" for source \"%s\" with volume %d with fade %d %f",
it->second.c_str(),
GetWeakSourceName(_audioSource).c_str(), _volume);
GetWeakSourceName(_audioSource).c_str(), _volume, _fade,
_duration.seconds);
} else {
blog(LOG_WARNING, "ignored unknown audio action %d",
static_cast<int>(_action));
@@ -58,20 +156,24 @@ void MacroActionAudio::LogAction()
bool MacroActionAudio::Save(obs_data_t *obj)
{
MacroAction::Save(obj);
_duration.Save(obj);
obs_data_set_string(obj, "audioSource",
GetWeakSourceName(_audioSource).c_str());
obs_data_set_int(obj, "action", static_cast<int>(_action));
obs_data_set_int(obj, "volume", _volume);
obs_data_set_bool(obj, "fade", _fade);
return true;
}
bool MacroActionAudio::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_duration.Load(obj);
const char *audioSourceName = obs_data_get_string(obj, "audioSource");
_audioSource = GetWeakSourceByName(audioSourceName);
_action = static_cast<AudioAction>(obs_data_get_int(obj, "action"));
_volume = obs_data_get_int(obj, "volume");
_fade = obs_data_get_bool(obj, "fade");
return true;
}
@@ -100,6 +202,8 @@ MacroActionAudioEdit::MacroActionAudioEdit(
_volumePercent->setMinimum(0);
_volumePercent->setMaximum(2000);
_volumePercent->setSuffix("%");
_fade = new QCheckBox();
_duration = new DurationSelection(parent, false);
populateActionSelection(_actions);
populateAudioSelection(_audioSources);
@@ -111,15 +215,25 @@ MacroActionAudioEdit::MacroActionAudioEdit(
SLOT(SourceChanged(const QString &)));
QWidget::connect(_volumePercent, SIGNAL(valueChanged(int)), this,
SLOT(VolumeChanged(int)));
QWidget::connect(_fade, SIGNAL(stateChanged(int)), this,
SLOT(FadeChanged(int)));
QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this,
SLOT(DurationChanged(double)));
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{audioSources}}", _audioSources},
{"{{actions}}", _actions},
{"{{volume}}", _volumePercent},
{"{{audioSources}}", _audioSources}, {"{{actions}}", _actions},
{"{{volume}}", _volumePercent}, {"{{fade}}", _fade},
{"{{duration}}", _duration},
};
QHBoxLayout *entryLayout = new QHBoxLayout;
placeWidgets(obs_module_text("AdvSceneSwitcher.action.audio.entry"),
mainLayout, widgetPlaceholders);
entryLayout, widgetPlaceholders);
_fadeLayout = new QHBoxLayout;
placeWidgets(obs_module_text("AdvSceneSwitcher.action.audio.fade"),
_fadeLayout, widgetPlaceholders);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addLayout(_fadeLayout);
setLayout(mainLayout);
_entryData = entryData;
@@ -140,19 +254,11 @@ bool hasSourceControl(AudioAction action)
void MacroActionAudioEdit::SetWidgetVisibility()
{
if (hasVolumeControl(_entryData->_action)) {
_volumePercent->show();
} else {
_volumePercent->hide();
}
if (hasSourceControl(_entryData->_action)) {
_audioSources->show();
} else {
_audioSources->hide();
}
_volumePercent->setVisible(hasVolumeControl(_entryData->_action));
_audioSources->setVisible(hasSourceControl(_entryData->_action));
setLayoutVisible(_fadeLayout, hasVolumeControl(_entryData->_action));
adjustSize();
}
void MacroActionAudioEdit::UpdateEntryData()
{
if (!_entryData) {
@@ -163,7 +269,8 @@ void MacroActionAudioEdit::UpdateEntryData()
GetWeakSourceName(_entryData->_audioSource).c_str());
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_volumePercent->setValue(_entryData->_volume);
_fade->setChecked(_entryData->_fade);
_duration->SetDuration(_entryData->_duration);
SetWidgetVisibility();
}
@@ -199,3 +306,23 @@ void MacroActionAudioEdit::VolumeChanged(int value)
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_volume = value;
}
void MacroActionAudioEdit::FadeChanged(int value)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_fade = value;
}
void MacroActionAudioEdit::DurationChanged(double seconds)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_duration.seconds = seconds;
}

View File

@@ -92,12 +92,14 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text)
return;
}
auto idx = _entryData->get()->GetIndex();
std::string id = MacroActionFactory::GetIdByName(text);
HeaderInfoChanged("");
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->reset();
*_entryData = MacroActionFactory::Create(id);
(*_entryData)->SetIndex(idx);
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
this, SLOT(HeaderInfoChanged(const QString &)));

View File

@@ -94,6 +94,7 @@ MacroActionFilterEdit::MacroActionFilterEdit(
{
_sources = new QComboBox();
_filters = new QComboBox();
_filters->setSizeAdjustPolicy(QComboBox::AdjustToContents);
_actions = new QComboBox();
_getSettings = new QPushButton(
obs_module_text("AdvSceneSwitcher.action.filter.getSettings"));
@@ -165,6 +166,7 @@ void MacroActionFilterEdit::SourceChanged(const QString &text)
}
_filters->clear();
populateFilterSelection(_filters, _entryData->_source);
_filters->adjustSize();
}
void MacroActionFilterEdit::FilterChanged(const QString &text)

View File

@@ -141,7 +141,7 @@ obs_key_combination keysToOBSKeycombo(const std::vector<HotkeyType> &keys)
return combo;
}
for (int i = 0; i < keys.size() - 1; i++) {
for (uint32_t i = 0; i < keys.size() - 1; i++) {
switch (keys[i]) {
case HotkeyType::Key_Shift_L:
combo.modifiers |= INTERACT_SHIFT_KEY;

263
src/macro-action-random.cpp Normal file
View File

@@ -0,0 +1,263 @@
#include "headers/macro-action-random.hpp"
#include "headers/advanced-scene-switcher.hpp"
#include "headers/name-dialog.hpp"
#include "headers/utility.hpp"
#include <cstdlib>
const std::string MacroActionRandom::id = "random";
bool MacroActionRandom::_registered = MacroActionFactory::Register(
MacroActionRandom::id,
{MacroActionRandom::Create, MacroActionRandomEdit::Create,
"AdvSceneSwitcher.action.random"});
std::vector<MacroRef> getPossibleMacros(std::vector<MacroRef> &macros,
MacroRef &lastRandomMacro)
{
std::vector<MacroRef> res;
if (macros.size() == 1) {
if (macros[0]->Paused()) {
return res;
} else {
return macros;
}
}
for (auto &m : macros) {
if (m.get() && !m->Paused() &&
!(lastRandomMacro.get() == m.get())) {
res.push_back(m);
}
}
return res;
}
bool MacroActionRandom::PerformAction()
{
if (_macros.size() == 0) {
return true;
}
auto macros = getPossibleMacros(_macros, lastRandomMacro);
if (macros.size() == 0) {
return true;
}
if (macros.size() == 1) {
lastRandomMacro = macros[0];
return macros[0]->PerformAction();
}
size_t idx = std::rand() % (macros.size());
lastRandomMacro = macros[idx];
return macros[idx]->PerformAction();
}
void MacroActionRandom::LogAction()
{
vblog(LOG_INFO, "running random macro");
}
bool MacroActionRandom::Save(obs_data_t *obj)
{
MacroAction::Save(obj);
obs_data_array_t *args = obs_data_array_create();
for (auto &m : _macros) {
obs_data_t *array_obj = obs_data_create();
m.Save(array_obj);
obs_data_array_push_back(args, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, "macros", args);
obs_data_array_release(args);
return true;
}
bool MacroActionRandom::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
obs_data_array_t *args = obs_data_get_array(obj, "macros");
size_t count = obs_data_array_count(args);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(args, i);
MacroRef ref;
ref.Load(array_obj);
_macros.push_back(ref);
obs_data_release(array_obj);
}
obs_data_array_release(args);
return true;
}
MacroActionRandomEdit::MacroActionRandomEdit(
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
: QWidget(parent)
{
_macroSelection = new MacroSelection(window());
_macroList = new QListWidget();
_macroList->setSortingEnabled(true);
_add = new QPushButton();
_add->setMaximumSize(QSize(22, 22));
_add->setProperty("themeID",
QVariant(QString::fromUtf8("addIconSmall")));
_add->setFlat(true);
_remove = new QPushButton();
_remove->setMaximumSize(QSize(22, 22));
_remove->setProperty("themeID",
QVariant(QString::fromUtf8("removeIconSmall")));
_remove->setFlat(true);
QWidget::connect(_add, SIGNAL(clicked()), this, SLOT(AddMacro()));
QWidget::connect(_remove, SIGNAL(clicked()), this, SLOT(RemoveMacro()));
QWidget::connect(_macroList, SIGNAL(currentRowChanged(int)), this,
SLOT(MacroSelectionChanged(int)));
QWidget::connect(window(),
SIGNAL(MacroRenamed(const QString &, const QString &)),
this,
SLOT(MacroRename(const QString &, const QString &)));
auto *entryLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{macroSelection}}", _macroSelection},
};
placeWidgets(obs_module_text("AdvSceneSwitcher.action.random.entry"),
entryLayout, widgetPlaceholders);
auto *argButtonLayout = new QHBoxLayout;
argButtonLayout->addWidget(_add);
argButtonLayout->addWidget(_remove);
argButtonLayout->addStretch();
auto *mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.random.arguments")));
mainLayout->addWidget(_macroList);
mainLayout->addLayout(argButtonLayout);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionRandomEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
for (auto &m : _entryData->_macros) {
if (!m.get()) {
continue;
}
auto name = QString::fromStdString(m->Name());
QListWidgetItem *item = new QListWidgetItem(name, _macroList);
item->setData(Qt::UserRole, name);
}
}
void MacroActionRandomEdit::MacroRemove(const QString &name)
{
if (_entryData) {
auto it = _entryData->_macros.begin();
while (it != _entryData->_macros.end()) {
if (it->get()->Name() == name.toStdString()) {
it = _entryData->_macros.erase(it);
} else {
++it;
}
}
}
}
void MacroActionRandomEdit::MacroRename(const QString &oldName,
const QString &newName)
{
auto count = _macroList->count();
for (int idx = 0; idx < count; ++idx) {
QListWidgetItem *item = _macroList->item(idx);
QString itemString = item->data(Qt::UserRole).toString();
if (oldName == itemString) {
item->setData(Qt::UserRole, newName);
item->setText(newName);
break;
}
}
}
void MacroActionRandomEdit::AddMacro()
{
if (_loading || !_entryData) {
return;
}
auto macroName = _macroSelection->currentText();
MacroRef macro(macroName.toStdString());
if (!macro.get()) {
return;
}
if (FindEntry(macro->Name()) != -1) {
return;
}
QVariant v = QVariant::fromValue(macroName);
QListWidgetItem *item = new QListWidgetItem(macroName, _macroList);
item->setData(Qt::UserRole, macroName);
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_macros.push_back(macro);
}
void MacroActionRandomEdit::RemoveMacro()
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
auto item = _macroList->currentItem();
if (!item) {
return;
}
std::string name = item->data(Qt::UserRole).toString().toStdString();
for (auto it = _entryData->_macros.begin();
it != _entryData->_macros.end(); ++it) {
auto m = *it;
if (m.get() && m->Name() == name) {
_entryData->_macros.erase(it);
break;
}
}
delete item;
}
int MacroActionRandomEdit::FindEntry(const std::string &macro)
{
int count = _macroList->count();
int idx = -1;
for (int i = 0; i < count; i++) {
QListWidgetItem *item = _macroList->item(i);
QString itemString = item->data(Qt::UserRole).toString();
if (QString::fromStdString(macro) == itemString) {
idx = i;
break;
}
}
return idx;
}
void MacroActionRandomEdit::MacroSelectionChanged(int idx)
{
if (_loading || !_entryData || idx == -1) {
return;
}
QListWidgetItem *item = _macroList->item(idx);
QString name = item->text();
_macroSelection->SetCurrentMacro(GetMacroByName(name.toUtf8().data()));
}

View File

@@ -17,14 +17,22 @@ const static std::map<SceneVisibilityAction, std::string> actionTypes = {
"AdvSceneSwitcher.action.sceneVisibility.type.hide"},
};
struct VisInfo {
const static std::map<SceneItemSourceType, std::string> sourceItemSourceTypes = {
{SceneItemSourceType::SOURCE,
"AdvSceneSwitcher.action.sceneVisibility.type.source"},
{SceneItemSourceType::SOURCE_GROUP,
"AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup"},
};
struct VisibilityData {
std::string name;
bool visible;
};
static bool visibilityEnum(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
static bool visibilitySourceEnum(obs_scene_t *, obs_sceneitem_t *item,
void *ptr)
{
VisInfo *vInfo = reinterpret_cast<VisInfo *>(ptr);
VisibilityData *vInfo = reinterpret_cast<VisibilityData *>(ptr);
auto sourceName = obs_source_get_name(obs_sceneitem_get_source(item));
if (vInfo->name == sourceName) {
obs_sceneitem_set_visible(item, vInfo->visible);
@@ -32,7 +40,26 @@ static bool visibilityEnum(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
if (obs_sceneitem_is_group(item)) {
obs_scene_t *scene = obs_sceneitem_group_get_scene(item);
obs_scene_enum_items(scene, visibilityEnum, ptr);
obs_scene_enum_items(scene, visibilitySourceEnum, ptr);
}
return true;
}
static bool visibilitySourceTypeEnum(obs_scene_t *, obs_sceneitem_t *item,
void *ptr)
{
VisibilityData *vInfo = reinterpret_cast<VisibilityData *>(ptr);
auto sourceTypeName = obs_source_get_display_name(
obs_source_get_id(obs_sceneitem_get_source(item)));
if (vInfo->name == sourceTypeName) {
obs_sceneitem_set_visible(item, vInfo->visible);
}
if (obs_sceneitem_is_group(item)) {
obs_scene_t *scene = obs_sceneitem_group_get_scene(item);
obs_scene_enum_items(scene, visibilitySourceTypeEnum, ptr);
}
return true;
@@ -43,18 +70,21 @@ bool MacroActionSceneVisibility::PerformAction()
auto s = obs_weak_source_get_source(_scene.GetScene());
auto scene = obs_scene_from_source(s);
auto sourceName = GetWeakSourceName(_source);
VisInfo vInfo = {sourceName, _action == SceneVisibilityAction::SHOW};
VisibilityData vInfo = {"", _action == SceneVisibilityAction::SHOW};
switch (_action) {
case SceneVisibilityAction::SHOW:
obs_scene_enum_items(scene, visibilityEnum, &vInfo);
switch (_sourceType) {
case SceneItemSourceType::SOURCE:
vInfo.name = sourceName;
obs_scene_enum_items(scene, visibilitySourceEnum, &vInfo);
break;
case SceneVisibilityAction::HIDE:
obs_scene_enum_items(scene, visibilityEnum, &vInfo);
case SceneItemSourceType::SOURCE_GROUP:
vInfo.name = _sourceGroup;
obs_scene_enum_items(scene, visibilitySourceTypeEnum, &vInfo);
break;
default:
break;
}
obs_source_release(s);
return true;
}
@@ -63,10 +93,18 @@ void MacroActionSceneVisibility::LogAction()
{
auto it = actionTypes.find(_action);
if (it != actionTypes.end()) {
vblog(LOG_INFO,
"performed visibility action \"%s\" for source \"%s\" on scene \"%s\"",
it->second.c_str(), GetWeakSourceName(_source).c_str(),
_scene.ToString().c_str());
if (_sourceType == SceneItemSourceType::SOURCE) {
vblog(LOG_INFO,
"performed visibility action \"%s\" for source \"%s\" on scene \"%s\"",
it->second.c_str(),
GetWeakSourceName(_source).c_str(),
_scene.ToString().c_str());
} else {
vblog(LOG_INFO,
"performed visibility action \"%s\" for any source type \"%s\" on scene \"%s\"",
it->second.c_str(), _sourceGroup.c_str(),
_scene.ToString().c_str());
}
} else {
blog(LOG_WARNING, "ignored unknown SceneVisibility action %d",
static_cast<int>(_action));
@@ -77,8 +115,14 @@ bool MacroActionSceneVisibility::Save(obs_data_t *obj)
{
MacroAction::Save(obj);
_scene.Save(obj);
obs_data_set_string(obj, "source", GetWeakSourceName(_source).c_str());
if (_sourceType == SceneItemSourceType::SOURCE) {
obs_data_set_string(obj, "source",
GetWeakSourceName(_source).c_str());
} else {
obs_data_set_string(obj, "source", _sourceGroup.c_str());
}
obs_data_set_int(obj, "action", static_cast<int>(_action));
obs_data_set_int(obj, "sourceType", static_cast<int>(_sourceType));
return true;
}
@@ -86,8 +130,11 @@ bool MacroActionSceneVisibility::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_scene.Load(obj);
_sourceType = static_cast<SceneItemSourceType>(
obs_data_get_int(obj, "sourceType"));
const char *sourceName = obs_data_get_string(obj, "source");
_source = GetWeakSourceByName(sourceName);
_sourceGroup = sourceName;
_action = static_cast<SceneVisibilityAction>(
obs_data_get_int(obj, "action"));
return true;
@@ -95,9 +142,16 @@ bool MacroActionSceneVisibility::Load(obs_data_t *obj)
std::string MacroActionSceneVisibility::GetShortDesc()
{
if (_source) {
if (_sourceType == SceneItemSourceType::SOURCE && _source) {
return _scene.ToString() + " - " + GetWeakSourceName(_source);
}
if (_sourceType == SceneItemSourceType::SOURCE_GROUP &&
!_sourceGroup.empty()) {
return _scene.ToString() + " - " +
obs_module_text(
"AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup") +
" " + _sourceGroup;
}
return "";
}
@@ -108,31 +162,44 @@ static inline void populateActionSelection(QComboBox *list)
}
}
static inline void populateSourceItemTypeSelection(QComboBox *list)
{
for (auto entry : sourceItemSourceTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
}
}
MacroActionSceneVisibilityEdit::MacroActionSceneVisibilityEdit(
QWidget *parent, std::shared_ptr<MacroActionSceneVisibility> entryData)
: QWidget(parent)
{
_scenes = new SceneSelectionWidget(window(), false, false, true);
_scenes = new SceneSelectionWidget(window(), false, true, true);
_sourceTypes = new QComboBox();
_sources = new QComboBox();
_sources->setSizeAdjustPolicy(QComboBox::AdjustToContents);
_actions = new QComboBox();
populateSourceItemTypeSelection(_sourceTypes);
populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
QWidget::connect(_sourceTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(SourceTypeChanged(int)));
QWidget::connect(_sources, SIGNAL(currentTextChanged(const QString &)),
this, SLOT(SourceChanged(const QString &)));
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", _scenes},
{"{{sourceTypes}}", _sourceTypes},
{"{{sources}}", _sources},
{"{{actions}}", _actions},
};
placeWidgets(obs_module_text(
"AdvSceneSwitcher.action.SceneVisibility.entry"),
"AdvSceneSwitcher.action.sceneVisibility.entry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
@@ -148,10 +215,17 @@ void MacroActionSceneVisibilityEdit::UpdateEntryData()
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_sourceTypes->setCurrentIndex(
static_cast<int>(_entryData->_sourceType));
_scenes->SetScene(_entryData->_scene);
populateSceneItemSelection(_sources, _entryData->_scene);
_sources->setCurrentText(
GetWeakSourceName(_entryData->_source).c_str());
if (_entryData->_sourceType == SceneItemSourceType::SOURCE) {
populateSceneItemSelection(_sources, _entryData->_scene);
_sources->setCurrentText(
GetWeakSourceName(_entryData->_source).c_str());
} else {
populateSourceTypeSelection(_sources);
_sources->setCurrentText(_entryData->_sourceGroup.c_str());
}
}
void MacroActionSceneVisibilityEdit::SceneChanged(const SceneSelection &s)
@@ -165,6 +239,26 @@ void MacroActionSceneVisibilityEdit::SceneChanged(const SceneSelection &s)
}
_sources->clear();
populateSceneItemSelection(_sources, _entryData->_scene);
_sources->adjustSize();
}
void MacroActionSceneVisibilityEdit::SourceTypeChanged(int value)
{
if (_loading || !_entryData) {
return;
}
{
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_sourceType =
static_cast<SceneItemSourceType>(value);
}
_sources->clear();
if (_entryData->_sourceType == SceneItemSourceType::SOURCE) {
populateSceneItemSelection(_sources, _entryData->_scene);
} else {
populateSourceTypeSelection(_sources);
}
}
void MacroActionSceneVisibilityEdit::SourceChanged(const QString &text)
@@ -174,7 +268,15 @@ void MacroActionSceneVisibilityEdit::SourceChanged(const QString &text)
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_source = GetWeakSourceByQString(text);
if (_entryData->_sourceType == SceneItemSourceType::SOURCE) {
_entryData->_source = GetWeakSourceByQString(text);
} else {
if (text == obs_module_text("AdvSceneSwitcher.selectItem")) {
_entryData->_sourceGroup = "";
} else {
_entryData->_sourceGroup = text.toStdString();
}
}
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}

View File

@@ -188,6 +188,7 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
return;
}
auto idx = _entryData->get()->GetIndex();
std::string id = MacroConditionFactory::GetIdByName(text);
auto temp = DurationConstraint();
@@ -198,6 +199,7 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
auto logic = (*_entryData)->GetLogicType();
_entryData->reset();
*_entryData = MacroConditionFactory::Create(id);
(*_entryData)->SetIndex(idx);
(*_entryData)->SetLogicType(logic);
auto widget =
MacroConditionFactory::CreateWidget(id, this, *_entryData);

View File

@@ -96,6 +96,7 @@ MacroConditionFilterEdit::MacroConditionFilterEdit(
{
_sources = new QComboBox();
_filters = new QComboBox();
_filters->setSizeAdjustPolicy(QComboBox::AdjustToContents);
_conditions = new QComboBox();
_getSettings = new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.filter.getSettings"));
@@ -159,6 +160,7 @@ void MacroConditionFilterEdit::SourceChanged(const QString &text)
}
_filters->clear();
populateFilterSelection(_filters, _entryData->_source);
_filters->adjustSize();
}
void MacroConditionFilterEdit::FilterChanged(const QString &text)

View File

@@ -125,6 +125,8 @@ MacroConditionMacroEdit::MacroConditionMacroEdit(
_currentCount = new QLabel(parent);
_resetCount = new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.macro.count.reset"));
_pausedWarning = new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.macro.pausedWarning"));
_count->setMaximum(10000000);
populateTypeSelection(_types);
@@ -156,9 +158,18 @@ MacroConditionMacroEdit::MacroConditionMacroEdit(
mainLayout->addLayout(typesLayout);
mainLayout->addLayout(_settingsLine1);
mainLayout->addLayout(_settingsLine2);
mainLayout->addWidget(_pausedWarning);
setLayout(mainLayout);
_entryData = entryData;
connect(&_countTimer, SIGNAL(timeout()), this, SLOT(UpdateCount()));
_countTimer.start(1000);
_pausedWarning->setVisible(false);
connect(&_pausedTimer, SIGNAL(timeout()), this, SLOT(UpdatePaused()));
_pausedTimer.start(1000);
UpdateEntryData();
_loading = false;
}
@@ -237,7 +248,6 @@ void MacroConditionMacroEdit::UpdateEntryData()
_counterConditions->setCurrentIndex(
static_cast<int>(_entryData->_counterCondition));
_count->setValue(_entryData->_count);
ResetTimer();
}
void MacroConditionMacroEdit::MacroChanged(const QString &text)
@@ -248,7 +258,6 @@ void MacroConditionMacroEdit::MacroChanged(const QString &text)
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_macro.UpdateRef(text);
ResetTimer();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
@@ -303,7 +312,6 @@ void MacroConditionMacroEdit::ResetClicked()
}
_entryData->_macro->ResetCount();
ResetTimer();
}
void MacroConditionMacroEdit::UpdateCount()
@@ -316,9 +324,9 @@ void MacroConditionMacroEdit::UpdateCount()
}
}
void MacroConditionMacroEdit::ResetTimer()
void MacroConditionMacroEdit::UpdatePaused()
{
_timer.reset(new QTimer(this));
connect(_timer.get(), SIGNAL(timeout()), this, SLOT(UpdateCount()));
_timer->start(1000);
_pausedWarning->setVisible(_entryData && _entryData->_macro.get() &&
_entryData->_macro->Paused());
adjustSize();
}

View File

@@ -0,0 +1,184 @@
#include "headers/macro-condition-edit.hpp"
#include "headers/macro-condition-scene-visibility.hpp"
#include "headers/utility.hpp"
#include "headers/advanced-scene-switcher.hpp"
#include <regex>
const std::string MacroConditionSceneVisibility::id = "scene_visibility";
bool MacroConditionSceneVisibility::_registered =
MacroConditionFactory::Register(
MacroConditionSceneVisibility::id,
{MacroConditionSceneVisibility::Create,
MacroConditionSceneVisibilityEdit::Create,
"AdvSceneSwitcher.condition.sceneVisibility"});
static std::map<SceneVisibilityCondition, std::string>
SceneVisibilityConditionTypes = {
{SceneVisibilityCondition::SHOWN,
"AdvSceneSwitcher.condition.sceneVisibility.type.shown"},
{SceneVisibilityCondition::HIDDEN,
"AdvSceneSwitcher.condition.sceneVisibility.type.hidden"},
};
struct VisibilityData {
std::string name;
bool visible;
bool result;
};
static bool visibilityEnum(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
{
VisibilityData *vInfo = reinterpret_cast<VisibilityData *>(ptr);
auto sourceName = obs_source_get_name(obs_sceneitem_get_source(item));
if (vInfo->name == sourceName &&
obs_sceneitem_visible(item) == vInfo->visible) {
vInfo->result = true;
return false;
}
if (obs_sceneitem_is_group(item)) {
obs_scene_t *scene = obs_sceneitem_group_get_scene(item);
obs_scene_enum_items(scene, visibilityEnum, ptr);
}
return true;
}
bool MacroConditionSceneVisibility::CheckCondition()
{
if (!_source) {
return false;
}
auto s = obs_weak_source_get_source(_scene.GetScene());
auto scene = obs_scene_from_source(s);
auto sourceName = GetWeakSourceName(_source);
VisibilityData data = {sourceName,
_condition == SceneVisibilityCondition::SHOWN,
false};
obs_scene_enum_items(scene, visibilityEnum, &data);
obs_source_release(s);
return data.result;
}
bool MacroConditionSceneVisibility::Save(obs_data_t *obj)
{
MacroCondition::Save(obj);
_scene.Save(obj);
obs_data_set_string(obj, "source", GetWeakSourceName(_source).c_str());
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
return true;
}
bool MacroConditionSceneVisibility::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_scene.Load(obj);
const char *sourceName = obs_data_get_string(obj, "source");
_source = GetWeakSourceByName(sourceName);
_condition = static_cast<SceneVisibilityCondition>(
obs_data_get_int(obj, "condition"));
return true;
}
std::string MacroConditionSceneVisibility::GetShortDesc()
{
if (_source) {
return _scene.ToString() + " - " + GetWeakSourceName(_source);
}
return "";
}
static inline void populateConditionSelection(QComboBox *list)
{
for (auto entry : SceneVisibilityConditionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
}
}
MacroConditionSceneVisibilityEdit::MacroConditionSceneVisibilityEdit(
QWidget *parent,
std::shared_ptr<MacroConditionSceneVisibility> entryData)
: QWidget(parent)
{
_scenes = new SceneSelectionWidget(window(), false, true, true);
_sources = new QComboBox();
_sources->setSizeAdjustPolicy(QComboBox::AdjustToContents);
_conditions = new QComboBox();
populateConditionSelection(_conditions);
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
QWidget::connect(_sources, SIGNAL(currentTextChanged(const QString &)),
this, SLOT(SourceChanged(const QString &)));
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{sources}}", _sources},
{"{{scenes}}", _scenes},
{"{{conditions}}", _conditions},
};
QHBoxLayout *mainLayout = new QHBoxLayout;
placeWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.sceneVisibility.entry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionSceneVisibilityEdit::SourceChanged(const QString &text)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_source = GetWeakSourceByQString(text);
}
void MacroConditionSceneVisibilityEdit::SceneChanged(const SceneSelection &s)
{
if (_loading || !_entryData) {
return;
}
{
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_scene = s;
}
_sources->clear();
populateSceneItemSelection(_sources, _entryData->_scene);
_sources->adjustSize();
}
void MacroConditionSceneVisibilityEdit::ConditionChanged(int index)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_condition = static_cast<SceneVisibilityCondition>(index);
}
void MacroConditionSceneVisibilityEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_conditions->setCurrentIndex(static_cast<int>(_entryData->_condition));
_scenes->SetScene(_entryData->_scene);
populateSceneItemSelection(_sources, _entryData->_scene);
_sources->setCurrentText(
GetWeakSourceName(_entryData->_source).c_str());
}

View File

@@ -947,16 +947,6 @@ bool patternControlIsOptional(VideoCondition cond)
cond == VideoCondition::HAS_NOT_CHANGED;
}
void setLayoutVisible(QLayout *layout, bool visible)
{
for (int i = 0; i < layout->count(); ++i) {
QWidget *widget = layout->itemAt(i)->widget();
if (widget != NULL) {
widget->setVisible(visible);
}
}
}
void MacroConditionVideoEdit::SetWidgetVisibility()
{
_imagePath->setVisible(requiresFileInput(_entryData->_condition));

View File

@@ -16,7 +16,7 @@ MacroSelection::MacroSelection(QWidget *parent) : QComboBox(parent)
firstItem->setEnabled(false);
for (auto &m : switcher->macros) {
addItem(QString::fromStdString(m.Name()));
addItem(QString::fromStdString(m->Name()));
}
QWidget::connect(parent, SIGNAL(MacroAdded(const QString &)), this,

View File

@@ -47,7 +47,7 @@ bool AdvSceneSwitcher::addNewMacro(std::string &name)
{
std::lock_guard<std::mutex> lock(switcher->m);
switcher->macros.emplace_back(name);
switcher->macros.emplace_back(std::make_shared<Macro>(name));
}
return true;
}
@@ -84,8 +84,11 @@ void AdvSceneSwitcher::on_macroRemove_clicked()
switcher->abortMacroWait = true;
switcher->macroWaitCv.notify_one();
int idx = ui->macros->currentRow();
QString::fromStdString(switcher->macros[idx].Name());
QString::fromStdString(switcher->macros[idx]->Name());
switcher->macros.erase(switcher->macros.begin() + idx);
for (auto &m : switcher->macros) {
m->ResolveMacroRef();
}
}
delete item;
@@ -108,7 +111,7 @@ void AdvSceneSwitcher::on_macroUp_clicked()
switcher->macros.begin() + index - 1);
for (auto &m : switcher->macros) {
m.ResolveMacroRef();
m->ResolveMacroRef();
}
}
}
@@ -125,7 +128,7 @@ void AdvSceneSwitcher::on_macroDown_clicked()
switcher->macros.begin() + index + 1);
for (auto &m : switcher->macros) {
m.ResolveMacroRef();
m->ResolveMacroRef();
}
}
}
@@ -170,9 +173,8 @@ void AdvSceneSwitcher::on_macroName_editingFinished()
emit MacroRenamed(oldName, newName);
}
void AdvSceneSwitcher::PopulateMacroActions(Macro &m, int afterIdx)
void AdvSceneSwitcher::PopulateMacroActions(Macro &m, uint32_t afterIdx)
{
bool root = afterIdx == 0;
auto &actions = m.Actions();
for (; afterIdx < actions.size(); afterIdx++) {
auto newEntry = new MacroActionEdit(this, &actions[afterIdx],
@@ -180,12 +182,11 @@ void AdvSceneSwitcher::PopulateMacroActions(Macro &m, int afterIdx)
ConnectControlSignals(newEntry);
ui->macroEditActionLayout->addWidget(newEntry);
ui->macroEditActionHelp->setVisible(false);
root = false;
}
ui->macroEditActionHelp->setVisible(actions.size() == 0);
}
void AdvSceneSwitcher::PopulateMacroConditions(Macro &m, int afterIdx)
void AdvSceneSwitcher::PopulateMacroConditions(Macro &m, uint32_t afterIdx)
{
bool root = afterIdx == 0;
auto &conditions = m.Conditions();
@@ -314,12 +315,12 @@ void AdvSceneSwitcher::on_macros_itemChanged(QListWidgetItem *item)
void AdvSceneSwitcher::setupMacroTab()
{
for (auto &m : switcher->macros) {
QString text = QString::fromStdString(m.Name());
QString text = QString::fromStdString(m->Name());
QListWidgetItem *item = new QListWidgetItem(text, ui->macros);
item->setData(Qt::UserRole, text);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
if (m.Paused()) {
if (m->Paused()) {
item->setCheckState(Qt::Unchecked);
} else {
item->setCheckState(Qt::Checked);
@@ -392,8 +393,8 @@ void AdvSceneSwitcher::CopyMacro()
return;
}
switcher->macros.back().Load(data);
switcher->macros.back().SetName(name);
switcher->macros.back()->Load(data);
switcher->macros.back()->SetName(name);
obs_data_release(data);
QString text = QString::fromStdString(name);

View File

@@ -36,6 +36,11 @@ bool Macro::CeckMatch()
{
_matched = false;
for (auto &c : _conditions) {
if (_paused) {
vblog(LOG_INFO, "Macro %s is paused", _name.c_str());
return false;
}
auto startTime = std::chrono::high_resolution_clock::now();
bool cond = c->CheckCondition();
auto endTime = std::chrono::high_resolution_clock::now();
@@ -89,15 +94,11 @@ bool Macro::CeckMatch()
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
// Condition checks shall still be run even if macro is paused.
// Otherwise conditions might behave in unexpected ways when resuming.
//
// For example, audio could immediately match after unpause, when it
// matched before it was paused due to timers not being updated.
if (_paused) {
vblog(LOG_INFO, "Macro %s is paused", _name.c_str());
_matched = false;
return false;
// TODO: Move back to PerformAction() once new scene collection frontend
// events are available - see:
// https://github.com/obsproject/obs-studio/commit/feda1aaa283e8a99f6ba1159cfe6b9c1f2934a61
if (_matched && _count != std::numeric_limits<int>::max()) {
_count++;
}
return _matched;
@@ -113,9 +114,6 @@ bool Macro::PerformAction()
return false;
}
}
if (_count != std::numeric_limits<int>::max()) {
_count++;
}
return ret;
}
@@ -125,6 +123,21 @@ void Macro::SetName(const std::string &name)
SetHotkeysDesc();
}
void Macro::ResetTimers()
{
for (auto &c : _conditions) {
c->ResetDuration();
}
}
void Macro::SetPaused(bool pause)
{
if (_paused && !pause) {
ResetTimers();
}
_paused = pause;
}
void Macro::UpdateActionIndices()
{
int idx = 0;
@@ -290,13 +303,18 @@ void Macro::ResolveMacroRef()
MacroRefCondition *ref =
dynamic_cast<MacroRefCondition *>(c.get());
if (ref) {
ref->_macro.UpdateRef();
ref->ResolveMacroRef();
}
}
for (auto &a : _actions) {
MacroRefAction *ref = dynamic_cast<MacroRefAction *>(a.get());
if (ref) {
ref->_macro.UpdateRef();
ref->ResolveMacroRef();
}
MultiMacroRefAction *ref2 =
dynamic_cast<MultiMacroRefAction *>(a.get());
if (ref2) {
ref2->ResolveMacroRef();
}
}
}
@@ -445,7 +463,7 @@ void SwitcherData::saveMacros(obs_data_t *obj)
for (auto &m : macros) {
obs_data_t *array_obj = obs_data_create();
m.Save(array_obj);
m->Save(array_obj);
obs_data_array_push_back(macroArray, array_obj);
obs_data_release(array_obj);
@@ -463,14 +481,14 @@ void SwitcherData::loadMacros(obs_data_t *obj)
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(macroArray, i);
macros.emplace_back();
macros.back().Load(array_obj);
macros.emplace_back(std::make_shared<Macro>());
macros.back()->Load(array_obj);
obs_data_release(array_obj);
}
obs_data_array_release(macroArray);
for (auto &m : macros) {
m.ResolveMacroRef();
m->ResolveMacroRef();
}
}
@@ -478,11 +496,11 @@ bool SwitcherData::checkMacros()
{
bool ret = false;
for (auto &m : macros) {
if (m.CeckMatch()) {
if (m->CeckMatch()) {
ret = true;
// This has to be performed here for now as actions are
// not performed immediately after checking conditions.
if (m.SwitchesScene()) {
if (m->SwitchesScene()) {
switcher->macroSceneSwitched = true;
}
}
@@ -492,12 +510,15 @@ bool SwitcherData::checkMacros()
bool SwitcherData::runMacros()
{
// TODO: Don't rely on creating a copy of each macro once new frontend
// events are available - see:
// https://github.com/obsproject/obs-studio/commit/feda1aaa283e8a99f6ba1159cfe6b9c1f2934a61
for (auto m : macros) {
if (m.Matched()) {
vblog(LOG_INFO, "running macro: %s", m.Name().c_str());
if (!m.PerformAction()) {
if (m->Matched()) {
vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
if (!m->PerformAction()) {
blog(LOG_WARNING, "abort macro: %s",
m.Name().c_str());
m->Name().c_str());
return false;
}
}
@@ -508,8 +529,8 @@ bool SwitcherData::runMacros()
Macro *GetMacroByName(const char *name)
{
for (auto &m : switcher->macros) {
if (m.Name() == name) {
return &m;
if (m->Name() == name) {
return m.get();
}
}
@@ -570,3 +591,10 @@ void MacroRefAction::ResolveMacroRef()
{
_macro.UpdateRef();
}
void MultiMacroRefAction::ResolveMacroRef()
{
for (auto &m : _macros) {
m.UpdateRef();
}
}

View File

@@ -229,6 +229,16 @@ void clearLayout(QLayout *layout, int afterIdx)
}
}
void setLayoutVisible(QLayout *layout, bool visible)
{
for (int i = 0; i < layout->count(); ++i) {
QWidget *widget = layout->itemAt(i)->widget();
if (widget != NULL) {
widget->setVisible(visible);
}
}
}
bool compareIgnoringLineEnding(QString &s1, QString &s2)
{
// Let QT deal with different types of lineendings
@@ -800,7 +810,8 @@ void populateSceneItemSelection(QComboBox *list, SceneSelection &s)
{
std::set<QString> names;
if (s.GetType() == SceneSelectionType::CURRENT) {
if (s.GetType() == SceneSelectionType::CURRENT ||
s.GetType() == SceneSelectionType::PREVIOUS) {
auto enumScenes = [](void *param, obs_source_t *source) {
if (!source) {
return true;
@@ -828,6 +839,36 @@ void populateSceneItemSelection(QComboBox *list, SceneSelection &s)
list->setCurrentIndex(0);
}
void populateSourceTypeSelection(QComboBox *list)
{
std::set<QString> sourceTypeNames;
auto enumSourcesTypes = [](void *param, obs_source_t *source) {
if (!source) {
return true;
}
std::set<QString> *names =
reinterpret_cast<std::set<QString> *>(param);
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT) {
return true;
}
names->insert(
obs_source_get_display_name(obs_source_get_id(source)));
return true;
};
obs_enum_sources(enumSourcesTypes, &sourceTypeNames);
for (auto &name : sourceTypeNames) {
if (!name.isEmpty()) {
list->addItem(name);
}
}
list->model()->sort(0);
addSelectionEntry(list, obs_module_text("AdvSceneSwitcher.selectItem"));
list->setCurrentIndex(0);
}
QMetaObject::Connection PulseWidget(QWidget *widget, QColor startColor,
QColor endColor, QString specifier,
bool once)