diff --git a/CMakeLists.txt b/CMakeLists.txt
index eba2f56a..1ee2ac31 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -80,9 +80,31 @@ set(advanced-scene-switcher_HEADERS
src/headers/switch-sequence.hpp
src/headers/switch-video.hpp
src/headers/switch-generic.hpp
+ src/headers/macro-action-edit.hpp
+ src/headers/macro-action-audio.hpp
+ src/headers/macro-action-recording.hpp
+ src/headers/macro-action-replay-buffer.hpp
+ src/headers/macro-action-run.hpp
+ src/headers/macro-action-streaming.hpp
+ src/headers/macro-action-switch-scene.hpp
+ src/headers/macro-action-wait.hpp
+ src/headers/macro-condition-edit.hpp
+ src/headers/macro-condition-audio.hpp
+ src/headers/macro-condition-file.hpp
+ src/headers/macro-condition-media.hpp
+ src/headers/macro-condition-plugin-state.hpp
+ src/headers/macro-condition-recording.hpp
+ src/headers/macro-condition-region.hpp
+ src/headers/macro-condition-scene.hpp
+ src/headers/macro-condition-streaming.hpp
+ src/headers/macro-condition-video.hpp
+ src/headers/macro-condition-window.hpp
+ src/headers/macro.hpp
src/headers/curl-helper.hpp
src/headers/screenshot-helper.hpp
+ src/headers/name-dialog.hpp
src/headers/duration-control.hpp
+ src/headers/section.hpp
src/headers/volume-control.hpp
src/headers/version.h
)
@@ -112,9 +134,32 @@ set(advanced-scene-switcher_SOURCES
src/switch-audio.cpp
src/switch-video.cpp
src/switch-generic.cpp
+ src/macro-action-edit.cpp
+ src/macro-action-audio.cpp
+ src/macro-action-recording.cpp
+ src/macro-action-replay-buffer.cpp
+ src/macro-action-run.cpp
+ src/macro-action-streaming.cpp
+ src/macro-action-switch-scene.cpp
+ src/macro-action-wait.cpp
+ src/macro-condition-edit.cpp
+ src/macro-condition-audio.cpp
+ src/macro-condition-file.cpp
+ src/macro-condition-media.cpp
+ src/macro-condition-plugin-state.cpp
+ src/macro-condition-recording.cpp
+ src/macro-condition-region.cpp
+ src/macro-condition-scene.cpp
+ src/macro-condition-streaming.cpp
+ src/macro-condition-video.cpp
+ src/macro-condition-window.cpp
+ src/macro.cpp
+ src/macro-tab.cpp
src/curl-helper.cpp
src/screenshot-helper.cpp
+ src/name-dialog.cpp
src/duration-control.cpp
+ src/section.cpp
src/volume-control.cpp
src/version.cpp
)
diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index c21692d2..03b103f3 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -51,6 +51,105 @@ AdvSceneSwitcher.generalTab.priority.media="Media"
AdvSceneSwitcher.generalTab.priority.time="Time"
AdvSceneSwitcher.generalTab.priority.audio="Audio"
AdvSceneSwitcher.generalTab.priority.video="Video"
+AdvSceneSwitcher.generalTab.priority.macro="Macro"
+
+; Macro Tab
+AdvSceneSwitcher.macroTab.title="Macro"
+AdvSceneSwitcher.macroTab.macros="Macros"
+AdvSceneSwitcher.macroTab.help="Macros allow you to execute a string of actions depending on multiple conditions.\n\nClick on the highlighted plus symbol to add a new Macro."
+AdvSceneSwitcher.macroTab.editConditionHelp="This section allows you to define Macro conditions.\n\nSelect an existing or add a new Macro on the left."
+AdvSceneSwitcher.macroTab.editActionHelp="This section allows you to define Macro actions.\n\nSelect an existing or add a new Macro on the left."
+AdvSceneSwitcher.macroTab.edit="Edit macro"
+AdvSceneSwitcher.macroTab.edit.logic="Logic type:"
+AdvSceneSwitcher.macroTab.edit.condition="Condition type:"
+AdvSceneSwitcher.macroTab.edit.action="Action type:"
+AdvSceneSwitcher.macroTab.add="Add new macro"
+AdvSceneSwitcher.macroTab.name="Name:"
+AdvSceneSwitcher.macroTab.defaultname="Macro %1"
+AdvSceneSwitcher.macroTab.exists="Macro name exists already"
+AdvSceneSwitcher.macroTab.copy="Create copy"
+; Macro Logic
+AdvSceneSwitcher.logic.none="Ignore entry"
+AdvSceneSwitcher.logic.and="And"
+AdvSceneSwitcher.logic.or="Or"
+AdvSceneSwitcher.logic.andNot="And not"
+AdvSceneSwitcher.logic.orNot="Or not"
+AdvSceneSwitcher.logic.rootNone="If"
+AdvSceneSwitcher.logic.not="If not"
+; Macro Conditions
+AdvSceneSwitcher.condition.audio="Audio"
+AdvSceneSwitcher.ondition.audio.state.below="Below"
+AdvSceneSwitcher.ondition.audio.state.above="Above"
+AdvSceneSwitcher.condition.audio.entry="Volume of {{audioSources}} is {{condition}} {{volume}} for {{duration}} seconds"
+AdvSceneSwitcher.condition.region="Screen region"
+AdvSceneSwitcher.condition.region.entry="Cursor is in {{minX}} {{minY}} x {{maxX}} {{maxY}}"
+AdvSceneSwitcher.condition.scene="Scene"
+AdvSceneSwitcher.condition.scene.type.current="Current"
+AdvSceneSwitcher.condition.scene.type.previous="Previous"
+AdvSceneSwitcher.condition.scene.entry="{{sceneType}} scene is {{scenes}} for {{duration}}"
+AdvSceneSwitcher.condition.window="Window"
+AdvSceneSwitcher.condition.window.entry.line1="{{windows}} exist and ..."
+AdvSceneSwitcher.condition.window.entry.line2="... is {{fullscreen}} fullscreen {{maximized}} maximized {{focused}} focused"
+AdvSceneSwitcher.condition.file="File"
+AdvSceneSwitcher.condition.file.entry.line1="Content of {{fileType}} {{filePath}} {{browseButton}} matches:"
+AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
+AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
+AdvSceneSwitcher.condition.media="Media"
+AdvSceneSwitcher.condition.media.entry="{{mediaSources}} state is {{states}} and {{timeRestrictions}} {{time}}"
+AdvSceneSwitcher.condition.video="Video"
+AdvSceneSwitcher.condition.video.condition.match="exactly matches"
+AdvSceneSwitcher.condition.video.condition.differ="does not match"
+AdvSceneSwitcher.condition.video.condition.hasChanged="has changed"
+AdvSceneSwitcher.condition.video.condition.hasNotChanged="has not changed"
+AdvSceneSwitcher.condition.video.condition.noImage="has no output"
+AdvSceneSwitcher.condition.video.askFileAction="Do you want to use an existing file or create a screenshot of the currently selected source?"
+AdvSceneSwitcher.condition.video.askFileAction.file="Use existing file"
+AdvSceneSwitcher.condition.video.askFileAction.screenshot="Create screenshot"
+AdvSceneSwitcher.condition.video.entry="{{videoSources}} {{condition}} {{filePath}} {{browseButton}} for {{duration}}"
+AdvSceneSwitcher.condition.stream="Streaming"
+AdvSceneSwitcher.condition.stream.state.start="Stream running"
+AdvSceneSwitcher.condition.stream.state.stop="Stream stopped"
+AdvSceneSwitcher.condition.stream.entry="{{streamState}} for {{duration}}"
+AdvSceneSwitcher.condition.record="Recording"
+AdvSceneSwitcher.condition.record.state.start="Recording running"
+AdvSceneSwitcher.condition.record.state.pause="Recording paused"
+AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
+AdvSceneSwitcher.condition.record.entry="{{recordState}} for {{duration}}"
+AdvSceneSwitcher.condition.pluginState="Plugin state"
+AdvSceneSwitcher.condition.pluginState.state.sceneSwitched="Automated scene change was triggered in this interval"
+AdvSceneSwitcher.condition.pluginState.entry="{{condition}}"
+
+; Macro Actions
+AdvSceneSwitcher.action.switchScene="Switch scene"
+AdvSceneSwitcher.action.scene.entry="Switch to scene {{scenes}} using {{transitions}} with a duration of {{duration}} seconds"
+AdvSceneSwitcher.action.wait="Wait"
+AdvSceneSwitcher.action.wait.type.fixed="fixed"
+AdvSceneSwitcher.action.wait.type.random="random"
+AdvSceneSwitcher.action.wait.entry.fixed="Wait for {{waitType}} duration of {{duration}}"
+AdvSceneSwitcher.action.wait.entry.random="Wait for {{waitType}} duration from {{duration}} to {{duration2}}"
+AdvSceneSwitcher.action.audio="Audio"
+AdvSceneSwitcher.action.audio.type.mute="Mute"
+AdvSceneSwitcher.action.audio.type.unmute="Unmute"
+AdvSceneSwitcher.action.audio.entry="{{actions}} {{audioSources}}"
+AdvSceneSwitcher.action.recording="Recording"
+AdvSceneSwitcher.action.recording.type.stop="Stop recording"
+AdvSceneSwitcher.action.recording.type.start="Start recording"
+AdvSceneSwitcher.action.recording.type.pause="Pause recording"
+AdvSceneSwitcher.action.recording.type.unpause="Unpause recording"
+AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording"
+AdvSceneSwitcher.action.recording.entry="{{actions}}{{pauseHint}}"
+AdvSceneSwitcher.action.replay="Replay buffer"
+AdvSceneSwitcher.action.replay.type.stop="Stop replay buffer"
+AdvSceneSwitcher.action.replay.type.start="Start replay buffer"
+AdvSceneSwitcher.action.replay.type.save="Save replay buffer"
+AdvSceneSwitcher.action.replay.entry="{{actions}}"
+AdvSceneSwitcher.action.streaming="Streaming"
+AdvSceneSwitcher.action.streaming.type.stop="Stop streaming"
+AdvSceneSwitcher.action.streaming.type.start="Start streaming"
+AdvSceneSwitcher.action.streaming.entry="{{actions}}"
+AdvSceneSwitcher.action.run="Run"
+AdvSceneSwitcher.action.run.entry="Run {{filePath}} {{browseButton}}"
+
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
diff --git a/forms/advanced-scene-switcher.ui b/forms/advanced-scene-switcher.ui
index 9507c85d..f21ba378 100644
--- a/forms/advanced-scene-switcher.ui
+++ b/forms/advanced-scene-switcher.ui
@@ -530,6 +530,414 @@
+
+
+ AdvSceneSwitcher.macroTab.title
+
+
+ -
+
+
+ AdvSceneSwitcher.macroTab.macros
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ -
+
+
+ false
+
+
+ AdvSceneSwitcher.macroTab.help
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ addIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ removeIconSmall
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 20
+
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+ ../../../forms/images/up.svg../../../forms/images/up.svg
+
+
+ true
+
+
+ upArrowIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+
+
+
+
+ ../../../forms/images/down.svg../../../forms/images/down.svg
+
+
+ true
+
+
+ downArrowIconSmall
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+ -
+
+
+ AdvSceneSwitcher.macroTab.edit
+
+
+
-
+
+
-
+
+
+ AdvSceneSwitcher.macroTab.name
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 767
+ 293
+
+
+
+
-
+
+
-
+
+
+ false
+
+
+ AdvSceneSwitcher.macroTab.editConditionHelp
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ addIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ removeIconSmall
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 767
+ 145
+
+
+
+
-
+
+
-
+
+
+ false
+
+
+ AdvSceneSwitcher.macroTab.editActionHelp
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ addIconSmall
+
+
+
+ -
+
+
+
+ 22
+ 22
+
+
+
+ true
+
+
+ removeIconSmall
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
AdvSceneSwitcher.transitionTab.title
diff --git a/src/advanced-scene-switcher.cpp b/src/advanced-scene-switcher.cpp
index c4640c90..96f7b640 100644
--- a/src/advanced-scene-switcher.cpp
+++ b/src/advanced-scene-switcher.cpp
@@ -64,6 +64,7 @@ void AdvSceneSwitcher::loadUI()
setupNetworkTab();
setupSceneGroupTab();
setupTriggerTab();
+ setupMacroTab();
setTabOrder();
restoreWindowGeo();
@@ -461,6 +462,8 @@ void SwitcherData::Thread()
// The previous scene might have changed during the linger duration,
// if a longer transition is used than the configured check interval
bool setPrevSceneAfterLinger = false;
+ bool macroMatch = false;
+ macroSceneSwitched = false;
endTime = std::chrono::high_resolution_clock::now();
@@ -495,7 +498,7 @@ void SwitcherData::Thread()
continue;
}
match = checkForMatch(scene, transition, linger,
- setPrevSceneAfterLinger);
+ setPrevSceneAfterLinger, macroMatch);
if (stop) {
break;
}
@@ -533,7 +536,11 @@ void SwitcherData::Thread()
lock.unlock();
if (match) {
- switchScene({scene, transition, 0});
+ if (macroMatch) {
+ runMacros();
+ } else {
+ switchScene({scene, transition, 0});
+ }
}
writeSceneInfoToFile();
@@ -544,7 +551,8 @@ void SwitcherData::Thread()
bool SwitcherData::checkForMatch(OBSWeakSource &scene,
OBSWeakSource &transition, int &linger,
- bool &setPrevSceneAfterLinger)
+ bool &setPrevSceneAfterLinger,
+ bool ¯oMatch)
{
bool match = false;
@@ -590,6 +598,12 @@ bool SwitcherData::checkForMatch(OBSWeakSource &scene,
case video_func:
match = checkVideoSwitch(scene, transition);
break;
+ case macro_func:
+ if (checkMacros()) {
+ match = true;
+ macroMatch = true;
+ }
+ break;
}
if (stop) {
diff --git a/src/duration-control.cpp b/src/duration-control.cpp
index 7fd34278..72f28b32 100644
--- a/src/duration-control.cpp
+++ b/src/duration-control.cpp
@@ -51,6 +51,7 @@ int durationUnitToMultiplier(DurationUnit u)
return 0;
}
+
std::string durationUnitToString(DurationUnit u)
{
switch (u) {
diff --git a/src/general.cpp b/src/general.cpp
index 3383d697..161c8519 100644
--- a/src/general.cpp
+++ b/src/general.cpp
@@ -320,51 +320,54 @@ int findTabIndex(QTabWidget *tabWidget, int pos)
tabName = "generalTab";
break;
case 1:
- tabName = "transitionsTab";
+ tabName = "macroTab";
break;
case 2:
- tabName = "pauseTab";
+ tabName = "transitionsTab";
break;
case 3:
- tabName = "windowTitleTab";
+ tabName = "pauseTab";
break;
case 4:
- tabName = "executableTab";
+ tabName = "windowTitleTab";
break;
case 5:
- tabName = "screenRegionTab";
+ tabName = "executableTab";
break;
case 6:
- tabName = "mediaTab";
+ tabName = "screenRegionTab";
break;
case 7:
- tabName = "fileTab";
+ tabName = "mediaTab";
break;
case 8:
- tabName = "randomTab";
+ tabName = "fileTab";
break;
case 9:
- tabName = "timeTab";
+ tabName = "randomTab";
break;
case 10:
- tabName = "idleTab";
+ tabName = "timeTab";
break;
case 11:
- tabName = "sceneSequenceTab";
+ tabName = "idleTab";
break;
case 12:
- tabName = "audioTab";
+ tabName = "sceneSequenceTab";
break;
case 13:
- tabName = "videoTab";
+ tabName = "audioTab";
break;
case 14:
- tabName = "networkTab";
+ tabName = "videoTab";
break;
case 15:
- tabName = "sceneGroupTab";
+ tabName = "networkTab";
break;
case 16:
+ tabName = "sceneGroupTab";
+ break;
+ case 17:
tabName = "sceneTriggerTab";
break;
}
@@ -432,6 +435,7 @@ void SwitcherData::loadSettings(obs_data_t *obj)
}
loadSceneGroups(obj);
+ loadMacros(obj);
loadWindowTitleSwitches(obj);
loadScreenRegionSwitches(obj);
loadPauseSwitches(obj);
@@ -461,6 +465,7 @@ void SwitcherData::saveSettings(obs_data_t *obj)
// selections to be available.
saveSceneGroups(obj);
+ saveMacros(obj);
saveWindowTitleSwitches(obj);
saveScreenRegionSwitches(obj);
savePauseSwitches(obj);
@@ -512,26 +517,28 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
obs_data_set_int(obj, "priority7", functionNamesByPriority[7]);
obs_data_set_int(obj, "priority8", functionNamesByPriority[8]);
obs_data_set_int(obj, "priority9", functionNamesByPriority[9]);
+ obs_data_set_int(obj, "priority10", functionNamesByPriority[10]);
obs_data_set_int(obj, "threadPriority", threadPriority);
obs_data_set_int(obj, "generalTabPos", tabOrder[0]);
- obs_data_set_int(obj, "transitionTabPos", tabOrder[1]);
- obs_data_set_int(obj, "pauseTabPos", tabOrder[2]);
- obs_data_set_int(obj, "titleTabPos", tabOrder[3]);
- obs_data_set_int(obj, "exeTabPos", tabOrder[4]);
- obs_data_set_int(obj, "regionTabPos", tabOrder[5]);
- obs_data_set_int(obj, "mediaTabPos", tabOrder[6]);
- obs_data_set_int(obj, "fileTabPos", tabOrder[7]);
- obs_data_set_int(obj, "randomTabPos", tabOrder[8]);
- obs_data_set_int(obj, "timeTabPos", tabOrder[9]);
- obs_data_set_int(obj, "idleTabPos", tabOrder[10]);
- obs_data_set_int(obj, "sequenceTabPos", tabOrder[11]);
- obs_data_set_int(obj, "audioTabPos", tabOrder[12]);
- obs_data_set_int(obj, "videoTabPos", tabOrder[13]);
- obs_data_set_int(obj, "networkTabPos", tabOrder[14]);
- obs_data_set_int(obj, "sceneGroupTabPos", tabOrder[15]);
- obs_data_set_int(obj, "triggerTabPos", tabOrder[16]);
+ obs_data_set_int(obj, "macroTabPos", tabOrder[1]);
+ obs_data_set_int(obj, "transitionTabPos", tabOrder[2]);
+ obs_data_set_int(obj, "pauseTabPos", tabOrder[3]);
+ obs_data_set_int(obj, "titleTabPos", tabOrder[4]);
+ obs_data_set_int(obj, "exeTabPos", tabOrder[5]);
+ obs_data_set_int(obj, "regionTabPos", tabOrder[6]);
+ obs_data_set_int(obj, "mediaTabPos", tabOrder[7]);
+ obs_data_set_int(obj, "fileTabPos", tabOrder[8]);
+ obs_data_set_int(obj, "randomTabPos", tabOrder[9]);
+ obs_data_set_int(obj, "timeTabPos", tabOrder[10]);
+ obs_data_set_int(obj, "idleTabPos", tabOrder[11]);
+ obs_data_set_int(obj, "sequenceTabPos", tabOrder[12]);
+ obs_data_set_int(obj, "audioTabPos", tabOrder[13]);
+ obs_data_set_int(obj, "videoTabPos", tabOrder[14]);
+ obs_data_set_int(obj, "networkTabPos", tabOrder[15]);
+ obs_data_set_int(obj, "sceneGroupTabPos", tabOrder[16]);
+ obs_data_set_int(obj, "triggerTabPos", tabOrder[17]);
obs_data_set_bool(obj, "saveWindowGeo", saveWindowGeo);
obs_data_set_int(obj, "windowPosX", windowPos.x());
@@ -591,6 +598,7 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
functionNamesByPriority[7] = (obs_data_get_int(obj, "priority7"));
functionNamesByPriority[8] = (obs_data_get_int(obj, "priority8"));
functionNamesByPriority[9] = (obs_data_get_int(obj, "priority9"));
+ functionNamesByPriority[10] = (obs_data_get_int(obj, "priority10"));
if (!prioFuncsValid()) {
functionNamesByPriority[0] = (default_priority_0);
functionNamesByPriority[1] = (default_priority_1);
@@ -602,6 +610,7 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
functionNamesByPriority[7] = (default_priority_7);
functionNamesByPriority[8] = (default_priority_8);
functionNamesByPriority[9] = (default_priority_9);
+ functionNamesByPriority[10] = (default_priority_10);
}
obs_data_set_default_int(obj, "threadPriority",
@@ -609,25 +618,27 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
threadPriority = obs_data_get_int(obj, "threadPriority");
obs_data_set_default_int(obj, "generalTabPos", 0);
- obs_data_set_default_int(obj, "transitionTabPos", 1);
- obs_data_set_default_int(obj, "pauseTabPos", 2);
- obs_data_set_default_int(obj, "titleTabPos", 3);
- obs_data_set_default_int(obj, "exeTabPos", 4);
- obs_data_set_default_int(obj, "regionTabPos", 5);
- obs_data_set_default_int(obj, "mediaTabPos", 6);
- obs_data_set_default_int(obj, "fileTabPos", 7);
- obs_data_set_default_int(obj, "randomTabPos", 8);
- obs_data_set_default_int(obj, "timeTabPos", 9);
- obs_data_set_default_int(obj, "idleTabPos", 10);
- obs_data_set_default_int(obj, "sequenceTabPos", 11);
- obs_data_set_default_int(obj, "audioTabPos", 12);
- obs_data_set_default_int(obj, "videoTabPos", 13);
- obs_data_set_default_int(obj, "networkTabPos", 14);
- obs_data_set_default_int(obj, "sceneGroupTabPos", 15);
- obs_data_set_default_int(obj, "triggerTabPos", 16);
+ obs_data_set_default_int(obj, "macroTabPos", 1);
+ obs_data_set_default_int(obj, "transitionTabPos", 2);
+ obs_data_set_default_int(obj, "pauseTabPos", 3);
+ obs_data_set_default_int(obj, "titleTabPos", 4);
+ obs_data_set_default_int(obj, "exeTabPos", 5);
+ obs_data_set_default_int(obj, "regionTabPos", 6);
+ obs_data_set_default_int(obj, "mediaTabPos", 7);
+ obs_data_set_default_int(obj, "fileTabPos", 8);
+ obs_data_set_default_int(obj, "randomTabPos", 9);
+ obs_data_set_default_int(obj, "timeTabPos", 10);
+ obs_data_set_default_int(obj, "idleTabPos", 11);
+ obs_data_set_default_int(obj, "sequenceTabPos", 12);
+ obs_data_set_default_int(obj, "audioTabPos", 13);
+ obs_data_set_default_int(obj, "videoTabPos", 14);
+ obs_data_set_default_int(obj, "networkTabPos", 15);
+ obs_data_set_default_int(obj, "sceneGroupTabPos", 16);
+ obs_data_set_default_int(obj, "triggerTabPos", 17);
tabOrder.clear();
tabOrder.emplace_back((int)(obs_data_get_int(obj, "generalTabPos")));
+ tabOrder.emplace_back((int)(obs_data_get_int(obj, "macroTabPos")));
tabOrder.emplace_back((int)(obs_data_get_int(obj, "transitionTabPos")));
tabOrder.emplace_back((int)(obs_data_get_int(obj, "pauseTabPos")));
tabOrder.emplace_back((int)(obs_data_get_int(obj, "titleTabPos")));
@@ -836,6 +847,10 @@ void AdvSceneSwitcher::setupGeneralTab()
s = obs_module_text(
"AdvSceneSwitcher.generalTab.priority.video");
break;
+ case macro_func:
+ s = obs_module_text(
+ "AdvSceneSwitcher.generalTab.priority.macro");
+ break;
}
QString text(s.c_str());
QListWidgetItem *item =
diff --git a/src/headers/advanced-scene-switcher.hpp b/src/headers/advanced-scene-switcher.hpp
index e1a0dc5e..737cd3b5 100644
--- a/src/headers/advanced-scene-switcher.hpp
+++ b/src/headers/advanced-scene-switcher.hpp
@@ -8,7 +8,7 @@
#define blog(level, msg, ...) blog(level, "[adv-ss] " msg, ##__VA_ARGS__)
#define vblog(level, msg, ...) \
- if (verbose) { \
+ if (switcher->verbose) { \
blog(level, msg, ##__VA_ARGS__); \
}
@@ -43,6 +43,10 @@ public:
void OpenSequenceExtendEdit(SequenceWidget *sw);
void SetEditSceneGroup(SceneGroup &sg);
+ bool addNewMacro(std::string &name);
+ Macro *getSelectedMacro();
+ void SetEditMacro(Macro &m);
+
void loadUI();
void setupGeneralTab();
void setupTitleTab();
@@ -61,6 +65,7 @@ public:
void setupTriggerTab();
void setupVideoTab();
void setupNetworkTab();
+ void setupMacroTab();
void setTabOrder();
void restoreWindowGeo();
@@ -126,6 +131,19 @@ public slots:
void on_tabMoved(int from, int to);
void on_tabWidget_currentChanged(int index);
+ void on_macroAdd_clicked();
+ void on_macroRemove_clicked();
+ void on_macroUp_clicked();
+ void on_macroDown_clicked();
+ void on_macroName_editingFinished();
+ void on_macros_currentRowChanged(int idx);
+ void on_conditionAdd_clicked();
+ void on_conditionRemove_clicked();
+ void on_actionAdd_clicked();
+ void on_actionRemove_clicked();
+ void showMacroContextMenu(const QPoint &);
+ void copyMacro();
+
void on_screenRegionSwitches_currentRowChanged(int idx);
void on_showFrame_clicked();
void on_screenRegionAdd_clicked();
diff --git a/src/headers/macro-action-audio.hpp b/src/headers/macro-action-audio.hpp
new file mode 100644
index 00000000..4049e868
--- /dev/null
+++ b/src/headers/macro-action-audio.hpp
@@ -0,0 +1,58 @@
+#pragma once
+#include
+#include "macro-action-edit.hpp"
+
+enum class AudioAction {
+ MUTE,
+ UNMUTE,
+};
+
+class MacroActionAudio : public MacroAction {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ OBSWeakSource _audioSource;
+ AudioAction _action = AudioAction::MUTE;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionAudioEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionAudioEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionAudioEdit(
+ parent,
+ std::dynamic_pointer_cast(action));
+ }
+
+private slots:
+ void SourceChanged(const QString &text);
+ void ActionChanged(int value);
+
+protected:
+ QComboBox *_audioSources;
+ QComboBox *_actions;
+ std::shared_ptr _entryData;
+
+private:
+ QHBoxLayout *_mainLayout;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-edit.hpp b/src/headers/macro-action-edit.hpp
new file mode 100644
index 00000000..0556cafb
--- /dev/null
+++ b/src/headers/macro-action-edit.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include "macro.hpp"
+#include "section.hpp"
+
+struct MacroActionInfo {
+ using TCreateMethod = std::shared_ptr (*)();
+ using TCreateWidgetMethod = QWidget *(*)(QWidget *parent,
+ std::shared_ptr);
+ TCreateMethod _createFunc;
+ TCreateWidgetMethod _createWidgetFunc;
+ std::string _name;
+};
+
+class MacroActionFactory {
+public:
+ MacroActionFactory() = delete;
+ static bool Register(int id, MacroActionInfo);
+ static std::shared_ptr Create(const int id);
+ static QWidget *CreateWidget(const int id, QWidget *parent,
+ std::shared_ptr action);
+ static auto GetActionTypes() { return _methods; }
+
+private:
+ static std::map _methods;
+};
+
+class MacroActionEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionEdit(QWidget *parent = nullptr,
+ std::shared_ptr * = nullptr, int type = 0,
+ bool startCollapsed = false);
+ void UpdateEntryData(int type);
+ void Collapse(bool collapsed);
+
+private slots:
+ void ActionSelectionChanged(int idx);
+
+protected:
+ QComboBox *_actionSelection;
+ Section *_section;
+
+ std::shared_ptr *_entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-recording.hpp b/src/headers/macro-action-recording.hpp
new file mode 100644
index 00000000..05c26669
--- /dev/null
+++ b/src/headers/macro-action-recording.hpp
@@ -0,0 +1,59 @@
+#pragma once
+#include
+#include
+#include "macro-action-edit.hpp"
+
+enum class RecordAction {
+ STOP,
+ START,
+ PAUSE,
+ UNPAUSE,
+};
+
+class MacroActionRecord : public MacroAction {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ RecordAction _action = RecordAction::STOP;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionRecordEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionRecordEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionRecordEdit(
+ parent,
+ std::dynamic_pointer_cast(action));
+ }
+
+private slots:
+ void ActionChanged(int value);
+
+protected:
+ QComboBox *_actions;
+ QLabel *_pauseHint;
+ std::shared_ptr _entryData;
+
+private:
+ QHBoxLayout *_mainLayout;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-replay-buffer.hpp b/src/headers/macro-action-replay-buffer.hpp
new file mode 100644
index 00000000..26dc7fc7
--- /dev/null
+++ b/src/headers/macro-action-replay-buffer.hpp
@@ -0,0 +1,66 @@
+#pragma once
+#include
+#include "macro-action-edit.hpp"
+#include "duration-control.hpp"
+
+enum class ReplayBufferAction {
+ STOP,
+ START,
+ SAVE,
+};
+
+class MacroActionReplayBuffer : public MacroAction {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ ReplayBufferAction _action = ReplayBufferAction::STOP;
+
+private:
+ // Add artifical delay before trying to save the replay buffer again.
+ //
+ // Continiously calling obs_frontend_replay_buffer_save() does not
+ // result in any output actually being written.
+ // OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED also seems to be sent before
+ // any data is written.
+ Duration _duration;
+
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionReplayBufferEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionReplayBufferEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionReplayBufferEdit(
+ parent,
+ std::dynamic_pointer_cast(
+ action));
+ }
+
+private slots:
+ void ActionChanged(int value);
+
+protected:
+ QComboBox *_actions;
+ std::shared_ptr _entryData;
+
+private:
+ QHBoxLayout *_mainLayout;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-run.hpp b/src/headers/macro-action-run.hpp
new file mode 100644
index 00000000..cf276057
--- /dev/null
+++ b/src/headers/macro-action-run.hpp
@@ -0,0 +1,52 @@
+#pragma once
+#include "macro-action-edit.hpp"
+
+#include
+#include
+
+class MacroActionRun : public MacroAction {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ std::string _path = obs_module_text("AdvSceneSwitcher.enterPath");
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionRunEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionRunEdit(QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionRunEdit(
+ parent,
+ std::dynamic_pointer_cast(action));
+ }
+
+private slots:
+ void FilePathChanged();
+ void BrowseButtonClicked();
+
+protected:
+ std::shared_ptr _entryData;
+
+private:
+ QLineEdit *_filePath;
+ QPushButton *_browseButton;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-streaming.hpp b/src/headers/macro-action-streaming.hpp
new file mode 100644
index 00000000..e0d71003
--- /dev/null
+++ b/src/headers/macro-action-streaming.hpp
@@ -0,0 +1,60 @@
+#pragma once
+#include
+#include "macro-action-edit.hpp"
+#include "duration-control.hpp"
+
+enum class StreamAction {
+ STOP,
+ START,
+};
+
+class MacroActionStream : public MacroAction {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ StreamAction _action = StreamAction::STOP;
+
+private:
+ // Acts as a safeguard for misconfigured streaming setups leading to an
+ // endless error spam.
+ Duration _retryCooldown;
+
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionStreamEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionStreamEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionStreamEdit(
+ parent,
+ std::dynamic_pointer_cast(action));
+ }
+
+private slots:
+ void ActionChanged(int value);
+
+protected:
+ QComboBox *_actions;
+ std::shared_ptr _entryData;
+
+private:
+ QHBoxLayout *_mainLayout;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-action-switch-scene.hpp b/src/headers/macro-action-switch-scene.hpp
new file mode 100644
index 00000000..36b8c5ff
--- /dev/null
+++ b/src/headers/macro-action-switch-scene.hpp
@@ -0,0 +1,49 @@
+#pragma once
+#include "macro-action-edit.hpp"
+#include "switch-generic.hpp"
+#include "duration-control.hpp"
+
+class MacroActionSwitchScene : public MacroAction, public SceneSwitcherEntry {
+public:
+ bool PerformAction();
+ void LogAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+ Duration _duration;
+
+private:
+ const char *getType() { return "MacroActionSwitchScene"; }
+
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionSwitchSceneEdit : public SwitchWidget {
+ Q_OBJECT
+
+public:
+ MacroActionSwitchSceneEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionSwitchSceneEdit(
+ parent,
+ std::dynamic_pointer_cast(
+ action));
+ }
+
+private slots:
+ void DurationChanged(double seconds);
+
+protected:
+ DurationSelection *_duration;
+ std::shared_ptr _entryData;
+};
diff --git a/src/headers/macro-action-wait.hpp b/src/headers/macro-action-wait.hpp
new file mode 100644
index 00000000..50b555e4
--- /dev/null
+++ b/src/headers/macro-action-wait.hpp
@@ -0,0 +1,64 @@
+#pragma once
+#include
+#include "macro-action-edit.hpp"
+#include "duration-control.hpp"
+
+enum class WaitType {
+ FIXED,
+ RANDOM,
+};
+
+class MacroActionWait : public MacroAction {
+public:
+ bool PerformAction();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+ Duration _duration;
+ Duration _duration2;
+ WaitType _waitType = WaitType::FIXED;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroActionWaitEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroActionWaitEdit(
+ QWidget *parent,
+ std::shared_ptr entryData = nullptr);
+ void UpdateEntryData();
+ void SetupFixedDurationEdit();
+ void SetupRandomDurationEdit();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr action)
+ {
+ return new MacroActionWaitEdit(
+ parent,
+ std::dynamic_pointer_cast(action));
+ }
+
+private slots:
+ void DurationChanged(double value);
+ void DurationUnitChanged(DurationUnit unit);
+ void Duration2Changed(double value);
+ void Duration2UnitChanged(DurationUnit unit);
+ void TypeChanged(int value);
+
+protected:
+ DurationSelection *_duration;
+ DurationSelection *_duration2;
+ QComboBox *_waitType;
+ std::shared_ptr _entryData;
+
+private:
+ QHBoxLayout *_mainLayout;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-audio.hpp b/src/headers/macro-condition-audio.hpp
new file mode 100644
index 00000000..f6843ef2
--- /dev/null
+++ b/src/headers/macro-condition-audio.hpp
@@ -0,0 +1,78 @@
+#pragma once
+#include "macro.hpp"
+#include "volume-control.hpp"
+#include
+#include
+#include
+#include
+
+#include "duration-control.hpp"
+
+enum class AudioCondition {
+ ABOVE,
+ BELOW,
+};
+
+class MacroConditionAudio : public MacroCondition {
+public:
+ ~MacroConditionAudio();
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+ static void SetVolumeLevel(void *data,
+ const float magnitude[MAX_AUDIO_CHANNELS],
+ const float peak[MAX_AUDIO_CHANNELS],
+ const float inputPeak[MAX_AUDIO_CHANNELS]);
+ void ResetVolmeter();
+
+ OBSWeakSource _audioSource;
+ int _volume = 0;
+ AudioCondition _condition = AudioCondition::ABOVE;
+ Duration _duration;
+ obs_volmeter_t *_volmeter = nullptr;
+
+private:
+ float _peak = -std::numeric_limits::infinity();
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionAudioEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionAudioEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionAudioEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+ void UpdateVolmeterSource();
+
+private slots:
+ void SourceChanged(const QString &text);
+ void VolumeThresholdChanged(int vol);
+ void ConditionChanged(int cond);
+ void DurationChanged(double seconds);
+
+protected:
+ QComboBox *_audioSources;
+ QComboBox *_condition;
+ QSpinBox *_volume;
+ DurationSelection *_duration;
+ VolControl *_volMeter = nullptr;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-edit.hpp b/src/headers/macro-condition-edit.hpp
new file mode 100644
index 00000000..3e935ee4
--- /dev/null
+++ b/src/headers/macro-condition-edit.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "advanced-scene-switcher.hpp"
+#include "macro.hpp"
+#include "macro-condition-scene.hpp"
+#include "section.hpp"
+#include "utility.hpp"
+
+#include
+
+struct MacroConditionInfo {
+ using TCreateMethod = std::shared_ptr (*)();
+ using TCreateWidgetMethod =
+ QWidget *(*)(QWidget *parent, std::shared_ptr);
+ TCreateMethod _createFunc;
+ TCreateWidgetMethod _createWidgetFunc;
+ std::string _name;
+};
+
+class MacroConditionFactory {
+public:
+ MacroConditionFactory() = delete;
+ static bool Register(int id, MacroConditionInfo);
+ static std::shared_ptr Create(const int id);
+ static QWidget *CreateWidget(const int id, QWidget *parent,
+ std::shared_ptr);
+ static auto GetConditionTypes() { return _methods; }
+
+private:
+ static std::map _methods;
+};
+
+class MacroConditionEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionEdit(QWidget *parent = nullptr,
+ std::shared_ptr * = nullptr,
+ int type = 0, bool root = true,
+ bool startCollapsed = false);
+ bool IsRootNode();
+ void UpdateEntryData(int type);
+ void Collapse(bool collapsed);
+
+private slots:
+ void LogicSelectionChanged(int idx);
+ void ConditionSelectionChanged(int idx);
+
+protected:
+ QComboBox *_logicSelection;
+ QComboBox *_conditionSelection;
+ Section *_section;
+
+ std::shared_ptr *_entryData;
+
+private:
+ bool _isRoot = true;
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-file.hpp b/src/headers/macro-condition-file.hpp
new file mode 100644
index 00000000..f5fd34c4
--- /dev/null
+++ b/src/headers/macro-condition-file.hpp
@@ -0,0 +1,82 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum class FileType {
+ LOCAL,
+ REMOTE,
+};
+
+class MacroConditionFile : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ std::string _file = obs_module_text("AdvSceneSwitcher.enterPath");
+ std::string _text = obs_module_text("AdvSceneSwitcher.enterText");
+ FileType _fileType = FileType::LOCAL;
+ bool _useRegex = false;
+ bool _useTime = false;
+ bool _onlyMatchIfChanged = false;
+
+private:
+ bool matchFileContent(QString &filedata);
+ bool checkRemoteFileContent();
+ bool checkLocalFileContent();
+
+ QDateTime _lastMod;
+ size_t _lastHash = 0;
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionFileEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionFileEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionFileEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void FileTypeChanged(int index);
+ void FilePathChanged();
+ void BrowseButtonClicked();
+ void MatchTextChanged();
+ void UseRegexChanged(int state);
+ void CheckModificationDateChanged(int state);
+ void OnlyMatchIfChangedChanged(int state);
+
+protected:
+ QComboBox *_fileType;
+ QLineEdit *_filePath;
+ QPushButton *_browseButton;
+ QPlainTextEdit *_matchText;
+ QCheckBox *_useRegex;
+ QCheckBox *_checkModificationDate;
+ QCheckBox *_checkFileContent;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-media.hpp b/src/headers/macro-condition-media.hpp
new file mode 100644
index 00000000..fcf516b4
--- /dev/null
+++ b/src/headers/macro-condition-media.hpp
@@ -0,0 +1,103 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include
+
+#include "duration-control.hpp"
+
+enum class MediaTimeRestriction {
+ TIME_RESTRICTION_NONE,
+ TIME_RESTRICTION_SHORTER,
+ TIME_RESTRICTION_LONGER,
+ TIME_RESTRICTION_REMAINING_SHORTER,
+ TIME_RESTRICTION_REMAINING_LONGER,
+};
+
+constexpr auto custom_media_states_offset = 100;
+
+enum class MediaState {
+ // OBS's internal states
+ OBS_MEDIA_STATE_NONE,
+ OBS_MEDIA_STATE_PLAYING,
+ OBS_MEDIA_STATE_OPENING,
+ OBS_MEDIA_STATE_BUFFERING,
+ OBS_MEDIA_STATE_PAUSED,
+ OBS_MEDIA_STATE_STOPPED,
+ OBS_MEDIA_STATE_ENDED,
+ OBS_MEDIA_STATE_ERROR,
+ // Just a marker
+ LAST_OBS_MEDIA_STATE,
+ // states added for use in the plugin
+ PLAYED_TO_END = custom_media_states_offset,
+ ANY,
+};
+
+class MacroConditionMedia : public MacroCondition {
+public:
+ ~MacroConditionMedia();
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+ void ClearSignalHandler();
+ void ResetSignalHandler();
+ static void MediaStopped(void *data, calldata_t *);
+ static void MediaEnded(void *data, calldata_t *);
+
+ OBSWeakSource _source = nullptr;
+ MediaState _state = MediaState::OBS_MEDIA_STATE_NONE;
+ MediaTimeRestriction _restriction =
+ MediaTimeRestriction::TIME_RESTRICTION_NONE;
+ Duration _time;
+
+private:
+ std::atomic_bool _stopped = {false};
+ std::atomic_bool _ended = {false};
+ // Trigger scene change only once even if media state might trigger repeatedly
+ bool _alreadyMatched = false;
+ // Workaround to enable use of "ended" to specify end of VLC playlist
+ bool _previousStateEnded = false;
+ bool _playedToEnd = false;
+
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionMediaEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionMediaEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionMediaEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void SourceChanged(const QString &text);
+ void StateChanged(int index);
+ void TimeRestrictionChanged(int index);
+ void TimeChanged(double seconds);
+ void TimeUnitChanged(DurationUnit unit);
+
+protected:
+ QComboBox *_mediaSources;
+ QComboBox *_states;
+ QComboBox *_timeRestrictions;
+ DurationSelection *_time;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-plugin-state.hpp b/src/headers/macro-condition-plugin-state.hpp
new file mode 100644
index 00000000..1a71df82
--- /dev/null
+++ b/src/headers/macro-condition-plugin-state.hpp
@@ -0,0 +1,54 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+
+enum class PluginStateCondition {
+ SCENESWITCHED,
+};
+
+class MacroConditionPluginState : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ PluginStateCondition _condition = PluginStateCondition::SCENESWITCHED;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionPluginStateEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionPluginStateEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionPluginStateEdit(
+ parent,
+ std::dynamic_pointer_cast(
+ cond));
+ }
+
+private slots:
+ void ConditionChanged(int cond);
+
+protected:
+ QComboBox *_condition;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-recording.hpp b/src/headers/macro-condition-recording.hpp
new file mode 100644
index 00000000..c66acdb1
--- /dev/null
+++ b/src/headers/macro-condition-recording.hpp
@@ -0,0 +1,60 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include "duration-control.hpp"
+
+enum class RecordState {
+ STOP,
+ PAUSE,
+ START,
+};
+
+class MacroConditionRecord : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ RecordState _recordState;
+ Duration _duration;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionRecordEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionRecordEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionRecordEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void StateChanged(int value);
+ void DurationChanged(double seconds);
+ void DurationUnitChanged(DurationUnit unit);
+
+protected:
+ QComboBox *_recordState;
+ DurationSelection *_duration;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-region.hpp b/src/headers/macro-condition-region.hpp
new file mode 100644
index 00000000..c998512d
--- /dev/null
+++ b/src/headers/macro-condition-region.hpp
@@ -0,0 +1,55 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+
+class MacroConditionRegion : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ int _minX = 0, _minY = 0, _maxX = 0, _maxY = 0;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionRegionEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionRegionEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionRegionEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void MinXChanged(int pos);
+ void MinYChanged(int pos);
+ void MaxXChanged(int pos);
+ void MaxYChanged(int pos);
+
+protected:
+ QSpinBox *_minX;
+ QSpinBox *_minY;
+ QSpinBox *_maxX;
+ QSpinBox *_maxY;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-scene.hpp b/src/headers/macro-condition-scene.hpp
new file mode 100644
index 00000000..8d4b56d4
--- /dev/null
+++ b/src/headers/macro-condition-scene.hpp
@@ -0,0 +1,62 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include "duration-control.hpp"
+
+enum class SceneType {
+ CURRENT,
+ PREVIOUS,
+};
+
+class MacroConditionScene : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ OBSWeakSource _scene;
+ SceneType _type;
+ Duration _duration;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionSceneEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionSceneEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionSceneEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void SceneChanged(const QString &text);
+ void TypeChanged(int value);
+ void DurationChanged(double seconds);
+ void DurationUnitChanged(DurationUnit unit);
+
+protected:
+ QComboBox *_sceneSelection;
+ QComboBox *_sceneType;
+ DurationSelection *_duration;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-streaming.hpp b/src/headers/macro-condition-streaming.hpp
new file mode 100644
index 00000000..4d959a03
--- /dev/null
+++ b/src/headers/macro-condition-streaming.hpp
@@ -0,0 +1,59 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include "duration-control.hpp"
+
+enum class StreamState {
+ STOP,
+ START,
+};
+
+class MacroConditionStream : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+ StreamState _streamState;
+ Duration _duration;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionStreamEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionStreamEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionStreamEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void StateChanged(int value);
+ void DurationChanged(double seconds);
+ void DurationUnitChanged(DurationUnit unit);
+
+protected:
+ QComboBox *_streamState;
+ DurationSelection *_duration;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-video.hpp b/src/headers/macro-condition-video.hpp
new file mode 100644
index 00000000..122e1b18
--- /dev/null
+++ b/src/headers/macro-condition-video.hpp
@@ -0,0 +1,82 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+#include
+#include "duration-control.hpp"
+#include "screenshot-helper.hpp"
+
+enum class VideoCondition {
+ MATCH,
+ DIFFER,
+ HAS_NOT_CHANGED,
+ HAS_CHANGED,
+ NO_IMAGE,
+};
+
+class MacroConditionVideo : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ QImage GetMatchImage() { return _matchImage; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+ void GetScreenshot();
+ bool LoadImageFromFile();
+
+ OBSWeakSource _videoSource;
+ VideoCondition _condition = VideoCondition::MATCH;
+ Duration _duration;
+ std::string _file = obs_module_text("AdvSceneSwitcher.enterPath");
+
+private:
+ bool Compare();
+
+ std::unique_ptr _screenshotData = nullptr;
+ QImage _matchImage;
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionVideoEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionVideoEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionVideoEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+ void UpdatePreviewTooltip();
+ void SetFilePath(const QString &text);
+
+private slots:
+ void SourceChanged(const QString &text);
+ void ConditionChanged(int cond);
+ void FilePathChanged();
+ void BrowseButtonClicked();
+ void DurationChanged(double seconds);
+ void DurationUnitChanged(DurationUnit unit);
+
+protected:
+ QComboBox *_videoSelection;
+ DurationSelection *_duration;
+ QComboBox *_condition;
+ QLineEdit *_filePath;
+ QPushButton *_browseButton;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro-condition-window.hpp b/src/headers/macro-condition-window.hpp
new file mode 100644
index 00000000..e90921f9
--- /dev/null
+++ b/src/headers/macro-condition-window.hpp
@@ -0,0 +1,64 @@
+#pragma once
+#include "macro.hpp"
+#include
+#include
+
+class MacroConditionWindow : public MacroCondition {
+public:
+ bool CheckCondition();
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+ int GetId() { return id; };
+ static std::shared_ptr Create()
+ {
+ return std::make_shared();
+ }
+
+private:
+ bool CheckWindowTitleSwitchDirect(std::string ¤tWindowTitle);
+ bool CheckWindowTitleSwitchRegex(std::string ¤tWindowTitle,
+ std::vector &windowList);
+
+public:
+ std::string _window;
+ bool _fullscreen = false;
+ bool _maximized = false;
+ bool _focus = true;
+
+private:
+ static bool _registered;
+ static const int id;
+};
+
+class MacroConditionWindowEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionWindowEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionWindowEdit(
+ parent,
+ std::dynamic_pointer_cast(cond));
+ }
+
+private slots:
+ void WindowChanged(const QString &text);
+ void FullscreenChanged(int state);
+ void MaximizedChanged(int state);
+ void FocusChanged(int state);
+
+protected:
+ QComboBox *_windowSelection;
+ QCheckBox *_fullscreen;
+ QCheckBox *_maximized;
+ QCheckBox *_focused;
+ std::shared_ptr _entryData;
+
+private:
+ bool _loading = true;
+};
diff --git a/src/headers/macro.hpp b/src/headers/macro.hpp
new file mode 100644
index 00000000..ebd23b3a
--- /dev/null
+++ b/src/headers/macro.hpp
@@ -0,0 +1,88 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+
+constexpr auto macro_func = 10;
+constexpr auto default_priority_10 = macro_func;
+
+constexpr auto logic_root_offset = 100;
+
+enum class LogicType {
+ ROOT_NONE = 0,
+ ROOT_NOT,
+ ROOT_LAST,
+ // leave some space for potential expansion
+ NONE = 100,
+ AND,
+ OR,
+ AND_NOT,
+ OR_NOT,
+ LAST,
+};
+
+static inline bool isRootLogicType(LogicType l)
+{
+ return static_cast(l) < logic_root_offset;
+}
+
+struct LogicTypeInfo {
+ std::string _name;
+};
+
+class MacroCondition {
+public:
+ virtual bool CheckCondition() = 0;
+ virtual bool Save(obs_data_t *obj) = 0;
+ virtual bool Load(obs_data_t *obj) = 0;
+ virtual int GetId() = 0;
+ LogicType GetLogicType() { return _logic; }
+ void SetLogicType(LogicType logic) { _logic = logic; }
+
+ static const std::unordered_map logicTypes;
+
+private:
+ LogicType _logic;
+};
+
+class MacroAction {
+public:
+ virtual bool PerformAction() = 0;
+ virtual bool Save(obs_data_t *obj) = 0;
+ virtual bool Load(obs_data_t *obj) = 0;
+ virtual int GetId() = 0;
+ virtual void LogAction();
+};
+
+class Macro {
+
+public:
+ Macro(std::string name = "");
+ virtual ~Macro();
+
+ bool CeckMatch();
+ bool PerformAction();
+ bool Matched() { return _matched; }
+ std::string Name() { return _name; }
+ void SetName(std::string name) { _name = name; }
+ std::deque> &Conditions()
+ {
+ return _conditions;
+ }
+ std::deque> &Actions() { return _actions; }
+
+ bool Save(obs_data_t *obj);
+ bool Load(obs_data_t *obj);
+
+ // Helper function for plugin state condition regarding scene change
+ bool SwitchesScene();
+
+private:
+ std::string _name = "";
+ std::deque> _conditions;
+ std::deque> _actions;
+ bool _matched = false;
+};
diff --git a/src/headers/name-dialog.hpp b/src/headers/name-dialog.hpp
new file mode 100644
index 00000000..8c37263d
--- /dev/null
+++ b/src/headers/name-dialog.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+#include
+#include
+
+// Based on OBS's NameDialog
+class AdvSSNameDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ AdvSSNameDialog(QWidget *parent);
+
+ // Returns true if user clicks OK, false otherwise
+ // userTextInput returns string that user typed into dialog
+ static bool AskForName(QWidget *parent, const QString &title,
+ const QString &text, std::string &userTextInput,
+ const QString &placeHolder = QString(""),
+ int maxSize = 170);
+
+private:
+ QLabel *label;
+ QLineEdit *userText;
+};
diff --git a/src/headers/scene-group.hpp b/src/headers/scene-group.hpp
index 887dfbb6..b29dc1ae 100644
--- a/src/headers/scene-group.hpp
+++ b/src/headers/scene-group.hpp
@@ -82,22 +82,3 @@ private:
SceneGroup *sceneGroup = nullptr;
};
-
-// Based on OBS's NameDialog
-class SGNameDialog : public QDialog {
- Q_OBJECT
-
-public:
- SGNameDialog(QWidget *parent);
-
- // Returns true if user clicks OK, false otherwise
- // userTextInput returns string that user typed into dialog
- static bool AskForName(QWidget *parent, const QString &title,
- const QString &text, std::string &userTextInput,
- const QString &placeHolder = QString(""),
- int maxSize = 170);
-
-private:
- QLabel *label;
- QLineEdit *userText;
-};
diff --git a/src/headers/section.hpp b/src/headers/section.hpp
new file mode 100644
index 00000000..8e3154a2
--- /dev/null
+++ b/src/headers/section.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class Section : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit Section(const int animationDuration = 300,
+ QWidget *parent = 0);
+
+ void SetContent(QWidget *w);
+ void AddHeaderWidget(QWidget *);
+
+public slots:
+ void Collapse(bool collapsed);
+
+private:
+ QGridLayout *_mainLayout;
+ QHBoxLayout *_headerWidgetLayout;
+ QToolButton *_toggleButton;
+ QFrame *_headerLine;
+ QParallelAnimationGroup *_toggleAnimation = nullptr;
+ QScrollArea *_contentArea = nullptr;
+ int _animationDuration;
+};
diff --git a/src/headers/switcher-data-structs.hpp b/src/headers/switcher-data-structs.hpp
index 6c5e833e..7fa4864a 100644
--- a/src/headers/switcher-data-structs.hpp
+++ b/src/headers/switcher-data-structs.hpp
@@ -24,12 +24,13 @@
#include "switch-video.hpp"
#include "switch-network.hpp"
+#include "macro.hpp"
#include "duration-control.hpp"
constexpr auto default_interval = 300;
constexpr auto previous_scene_name = "Previous Scene";
constexpr auto current_transition_name = "Current Transition";
-constexpr auto tab_count = 17;
+constexpr auto tab_count = 18;
typedef enum { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 } NoMatch;
typedef enum { PERSIST = 0, START = 1, STOP = 2 } StartupBehavior;
@@ -90,6 +91,11 @@ struct SwitcherData {
Duration cooldown;
std::chrono::high_resolution_clock::time_point lastMatchTime;
+ std::deque macros;
+ // Allow only one macro scene change per scene switcher interval
+ // with the top macro having the highest priority
+ bool macroSceneSwitched = false;
+
std::deque windowSwitches;
std::vector ignoreIdleWindows;
std::string lastTitle;
@@ -140,7 +146,7 @@ struct SwitcherData {
default_priority_0, default_priority_1, default_priority_2,
default_priority_3, default_priority_4, default_priority_5,
default_priority_6, default_priority_7, default_priority_8,
- default_priority_9};
+ default_priority_9, default_priority_10};
struct ThreadPrio {
std::string name;
@@ -197,7 +203,10 @@ struct SwitcherData {
void writeToStatusFile(QString status);
bool checkForMatch(OBSWeakSource &scene, OBSWeakSource &transition,
- int &linger, bool &setPreviousSceneAsMatch);
+ int &linger, bool &setPreviousSceneAsMatch,
+ bool ¯oMatch);
+ bool checkMacros();
+ bool runMacros();
bool checkSceneSequence(OBSWeakSource &scene, OBSWeakSource &transition,
int &linger, bool &setPrevSceneAfterLinger);
bool checkIdleSwitch(OBSWeakSource &scene, OBSWeakSource &transition);
@@ -225,6 +234,7 @@ struct SwitcherData {
void checkDefaultSceneTransitions();
void saveSettings(obs_data_t *obj);
+ void saveMacros(obs_data_t *obj);
void saveWindowTitleSwitches(obs_data_t *obj);
void saveScreenRegionSwitches(obs_data_t *obj);
void savePauseSwitches(obs_data_t *obj);
@@ -246,6 +256,7 @@ struct SwitcherData {
void saveVersion(obs_data_t *obj, std::string currentVersion);
void loadSettings(obs_data_t *obj);
+ void loadMacros(obs_data_t *obj);
void loadWindowTitleSwitches(obs_data_t *obj);
void loadScreenRegionSwitches(obs_data_t *obj);
void loadPauseSwitches(obs_data_t *obj);
diff --git a/src/headers/utility.hpp b/src/headers/utility.hpp
index 80823752..726f5c9c 100644
--- a/src/headers/utility.hpp
+++ b/src/headers/utility.hpp
@@ -11,14 +11,6 @@
#include
#include
-static inline bool SceneGroupValid(SceneGroup *group)
-{
- if (group) {
- return group->name != invalid_scene_group_name;
- }
- return false;
-}
-
static inline bool WeakSourceValid(obs_weak_source_t *ws)
{
obs_source_t *source = obs_weak_source_get_source(ws);
@@ -41,26 +33,6 @@ static inline std::string GetWeakSourceName(obs_weak_source_t *weak_source)
return name;
}
-static inline SceneGroup *GetSceneGroupByName(const char *name)
-{
- if (!switcher) {
- return nullptr;
- }
-
- for (SceneGroup &sg : switcher->sceneGroups) {
- if (sg.name == name) {
- return &sg;
- }
- }
-
- return nullptr;
-}
-
-static inline SceneGroup *GetSceneGroupByQString(const QString &name)
-{
- return GetSceneGroupByName(name.toUtf8().constData());
-}
-
static inline OBSWeakSource GetWeakSourceByName(const char *name)
{
OBSWeakSource weak;
@@ -185,6 +157,21 @@ placeWidgets(std::string text, QBoxLayout *layout,
}
}
+static inline void clearLayout(QLayout *layout)
+{
+ QLayoutItem *item;
+ while ((item = layout->takeAt(0))) {
+ if (item->layout()) {
+ clearLayout(item->layout());
+ delete item->layout();
+ }
+ if (item->widget()) {
+ delete item->widget();
+ }
+ delete item;
+ }
+}
+
static inline bool compareIgnoringLineEnding(QString &s1, QString &s2)
{
// Let QT deal with different types of lineendings
diff --git a/src/macro-action-audio.cpp b/src/macro-action-audio.cpp
new file mode 100644
index 00000000..9d7d2b53
--- /dev/null
+++ b/src/macro-action-audio.cpp
@@ -0,0 +1,130 @@
+#include "headers/macro-action-audio.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+const int MacroActionAudio::id = 2;
+
+bool MacroActionAudio::_registered = MacroActionFactory::Register(
+ MacroActionAudio::id,
+ {MacroActionAudio::Create, MacroActionAudioEdit::Create,
+ "AdvSceneSwitcher.action.audio"});
+
+const static std::unordered_map actionTypes = {
+ {AudioAction::MUTE, "AdvSceneSwitcher.action.audio.type.mute"},
+ {AudioAction::UNMUTE, "AdvSceneSwitcher.action.audio.type.unmute"},
+};
+
+bool MacroActionAudio::PerformAction()
+{
+ auto s = obs_weak_source_get_source(_audioSource);
+ switch (_action) {
+ case AudioAction::MUTE:
+ obs_source_set_muted(s, true);
+ break;
+ case AudioAction::UNMUTE:
+ obs_source_set_muted(s, false);
+ break;
+ default:
+ break;
+ }
+ obs_source_release(s);
+ return true;
+}
+
+void MacroActionAudio::LogAction()
+{
+ auto it = actionTypes.find(_action);
+ if (it != actionTypes.end()) {
+ vblog(LOG_INFO, "performed action \"%s\" for source \"%s\"",
+ it->second.c_str(),
+ GetWeakSourceName(_audioSource).c_str());
+ } else {
+ blog(LOG_WARNING, "ignored unknown audio action %d", _action);
+ }
+}
+
+bool MacroActionAudio::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ obs_data_set_string(obj, "audioSource",
+ GetWeakSourceName(_audioSource).c_str());
+ obs_data_set_int(obj, "action", static_cast(_action));
+ return true;
+}
+
+bool MacroActionAudio::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ const char *audioSourceName = obs_data_get_string(obj, "audioSource");
+ _audioSource = GetWeakSourceByName(audioSourceName);
+ _action = static_cast(obs_data_get_int(obj, "action"));
+ return true;
+}
+
+static inline void populateActionSelection(QComboBox *list)
+{
+ for (auto entry : actionTypes) {
+ list->addItem(obs_module_text(entry.second.c_str()));
+ }
+}
+
+MacroActionAudioEdit::MacroActionAudioEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _audioSources = new QComboBox();
+ _actions = new QComboBox();
+
+ populateActionSelection(_actions);
+ AdvSceneSwitcher::populateAudioSelection(_audioSources);
+
+ QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(ActionChanged(int)));
+ QWidget::connect(_audioSources,
+ SIGNAL(currentTextChanged(const QString &)), this,
+ SLOT(SourceChanged(const QString &)));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{audioSources}}", _audioSources},
+ {"{{actions}}", _actions},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.audio.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroActionAudioEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+
+ _audioSources->setCurrentText(
+ GetWeakSourceName(_entryData->_audioSource).c_str());
+ _actions->setCurrentIndex(static_cast(_entryData->_action));
+}
+
+void MacroActionAudioEdit::SourceChanged(const QString &text)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_audioSource = GetWeakSourceByQString(text);
+}
+
+void MacroActionAudioEdit::ActionChanged(int value)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_action = static_cast(value);
+}
diff --git a/src/macro-action-edit.cpp b/src/macro-action-edit.cpp
new file mode 100644
index 00000000..8cf6eb99
--- /dev/null
+++ b/src/macro-action-edit.cpp
@@ -0,0 +1,135 @@
+#include "headers/macro-action-edit.hpp"
+#include "headers/macro-action-switch-scene.hpp"
+#include "headers/utility.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+
+std::map MacroActionFactory::_methods;
+
+bool MacroActionFactory::Register(int id, MacroActionInfo info)
+{
+ if (auto it = _methods.find(id); it == _methods.end()) {
+ _methods[id] = info;
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr MacroActionFactory::Create(const int id)
+{
+ if (auto it = _methods.find(id); it != _methods.end())
+ return it->second._createFunc();
+
+ return nullptr;
+}
+
+QWidget *MacroActionFactory::CreateWidget(const int id, QWidget *parent,
+ std::shared_ptr action)
+{
+ if (auto it = _methods.find(id); it != _methods.end())
+ return it->second._createWidgetFunc(parent, action);
+
+ return nullptr;
+}
+
+static inline void populateActionSelection(QComboBox *list)
+{
+ for (auto entry : MacroActionFactory::GetActionTypes()) {
+ list->addItem(obs_module_text(entry.second._name.c_str()));
+ }
+}
+
+MacroActionEdit::MacroActionEdit(QWidget *parent,
+ std::shared_ptr *entryData,
+ int type, bool startCollapsed)
+ : QWidget(parent)
+{
+ _actionSelection = new QComboBox();
+ _section = new Section(300);
+
+ QWidget::connect(_actionSelection, SIGNAL(currentIndexChanged(int)),
+ this, SLOT(ActionSelectionChanged(int)));
+
+ populateActionSelection(_actionSelection);
+
+ _section->AddHeaderWidget(_actionSelection);
+
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->addWidget(_section);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData(type);
+
+ _loading = false;
+ _section->Collapse(startCollapsed);
+}
+
+void MacroActionEdit::ActionSelectionChanged(int idx)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->reset();
+ *_entryData = MacroActionFactory::Create(idx);
+ auto widget =
+ MacroActionFactory::CreateWidget(idx, window(), *_entryData);
+ _section->SetContent(widget);
+ _section->Collapse(false);
+}
+
+void MacroActionEdit::UpdateEntryData(int type)
+{
+ _actionSelection->setCurrentIndex(type);
+ auto widget =
+ MacroActionFactory::CreateWidget(type, window(), *_entryData);
+ _section->SetContent(widget);
+}
+
+void MacroActionEdit::Collapse(bool collapsed)
+{
+ _section->Collapse(collapsed);
+}
+
+void AdvSceneSwitcher::on_actionAdd_clicked()
+{
+ auto macro = getSelectedMacro();
+ if (!macro) {
+ return;
+ }
+ std::lock_guard lock(switcher->m);
+ macro->Actions().emplace_back(MacroActionFactory::Create(0));
+ auto newEntry = new MacroActionEdit(this, ¯o->Actions().back(), 0);
+ ui->macroEditActionLayout->addWidget(newEntry);
+ ui->macroEditActionHelp->setVisible(false);
+ newEntry->Collapse(false);
+}
+
+void AdvSceneSwitcher::on_actionRemove_clicked()
+{
+ auto macro = getSelectedMacro();
+ if (!macro) {
+ return;
+ }
+ std::lock_guard lock(switcher->m);
+ if (macro->Actions().empty()) {
+ return;
+ }
+ macro->Actions().pop_back();
+
+ int count = ui->macroEditActionLayout->count();
+ auto item = ui->macroEditActionLayout->takeAt(count - 1);
+
+ if (item) {
+ auto widget = item->widget();
+ if (widget) {
+ widget->setVisible(false);
+ }
+ delete item;
+ }
+
+ if (count == 1) {
+ ui->macroEditActionHelp->setVisible(true);
+ }
+}
diff --git a/src/macro-action-recording.cpp b/src/macro-action-recording.cpp
new file mode 100644
index 00000000..9aaccf6a
--- /dev/null
+++ b/src/macro-action-recording.cpp
@@ -0,0 +1,133 @@
+#include "headers/macro-action-recording.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+const int MacroActionRecord::id = 4;
+
+bool MacroActionRecord::_registered = MacroActionFactory::Register(
+ MacroActionRecord::id,
+ {MacroActionRecord::Create, MacroActionRecordEdit::Create,
+ "AdvSceneSwitcher.action.recording"});
+
+const static std::unordered_map actionTypes = {
+ {RecordAction::STOP, "AdvSceneSwitcher.action.recording.type.stop"},
+ {RecordAction::START, "AdvSceneSwitcher.action.recording.type.start"},
+ {RecordAction::PAUSE, "AdvSceneSwitcher.action.recording.type.pause"},
+ {RecordAction::UNPAUSE,
+ "AdvSceneSwitcher.action.recording.type.unpause"},
+};
+
+bool MacroActionRecord::PerformAction()
+{
+ switch (_action) {
+ case RecordAction::STOP:
+ if (obs_frontend_recording_active()) {
+ obs_frontend_recording_stop();
+ }
+ break;
+ case RecordAction::START:
+ if (!obs_frontend_recording_active()) {
+ obs_frontend_recording_start();
+ }
+ break;
+ case RecordAction::PAUSE:
+ if (obs_frontend_recording_active() &&
+ !obs_frontend_recording_paused()) {
+ obs_frontend_recording_pause(true);
+ }
+ break;
+ case RecordAction::UNPAUSE:
+ if (obs_frontend_recording_active() &&
+ obs_frontend_recording_paused()) {
+ obs_frontend_recording_pause(false);
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+}
+
+void MacroActionRecord::LogAction()
+{
+ auto it = actionTypes.find(_action);
+ if (it != actionTypes.end()) {
+ vblog(LOG_INFO, "performed action \"%s\"", it->second.c_str());
+ } else {
+ blog(LOG_WARNING, "ignored unknown recording action %d",
+ _action);
+ }
+}
+
+bool MacroActionRecord::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ obs_data_set_int(obj, "action", static_cast(_action));
+ return true;
+}
+
+bool MacroActionRecord::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ _action = static_cast(obs_data_get_int(obj, "action"));
+ return true;
+}
+
+static inline void populateActionSelection(QComboBox *list)
+{
+ for (auto entry : actionTypes) {
+ list->addItem(obs_module_text(entry.second.c_str()));
+ }
+}
+
+MacroActionRecordEdit::MacroActionRecordEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _actions = new QComboBox();
+ _pauseHint = new QLabel(obs_module_text(
+ "AdvSceneSwitcher.action.recording.pause.hint"));
+
+ populateActionSelection(_actions);
+
+ QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(ActionChanged(int)));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{actions}}", _actions},
+ {"{{pauseHint}}", _pauseHint},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+bool isPauseAction(RecordAction a)
+{
+ return a == RecordAction::PAUSE || a == RecordAction::UNPAUSE;
+}
+
+void MacroActionRecordEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+ _actions->setCurrentIndex(static_cast(_entryData->_action));
+ _pauseHint->setVisible(isPauseAction(_entryData->_action));
+}
+
+void MacroActionRecordEdit::ActionChanged(int value)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_action = static_cast(value);
+ _pauseHint->setVisible(isPauseAction(_entryData->_action));
+}
diff --git a/src/macro-action-replay-buffer.cpp b/src/macro-action-replay-buffer.cpp
new file mode 100644
index 00000000..d028c90a
--- /dev/null
+++ b/src/macro-action-replay-buffer.cpp
@@ -0,0 +1,121 @@
+#include "headers/macro-action-replay-buffer.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+const int MacroActionReplayBuffer::id = 5;
+
+bool MacroActionReplayBuffer::_registered = MacroActionFactory::Register(
+ MacroActionReplayBuffer::id,
+ {MacroActionReplayBuffer::Create, MacroActionReplayBufferEdit::Create,
+ "AdvSceneSwitcher.action.replay"});
+
+const static std::unordered_map actionTypes = {
+ {ReplayBufferAction::STOP, "AdvSceneSwitcher.action.replay.type.stop"},
+ {ReplayBufferAction::START,
+ "AdvSceneSwitcher.action.replay.type.start"},
+ {ReplayBufferAction::SAVE, "AdvSceneSwitcher.action.replay.type.save"},
+};
+
+bool MacroActionReplayBuffer::PerformAction()
+{
+ switch (_action) {
+ case ReplayBufferAction::STOP:
+ if (obs_frontend_replay_buffer_active()) {
+ obs_frontend_replay_buffer_stop();
+ }
+ break;
+ case ReplayBufferAction::START:
+ if (!obs_frontend_replay_buffer_active()) {
+ obs_frontend_replay_buffer_start();
+ }
+ break;
+ case ReplayBufferAction::SAVE:
+ if (obs_frontend_replay_buffer_active() &&
+ _duration.DurationReached()) {
+ obs_frontend_replay_buffer_save();
+ // Default buffer size is 20s so waiting for 10s before
+ // trying to save again seems reasonable
+ _duration.seconds = 10;
+ _duration.Reset();
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+}
+
+void MacroActionReplayBuffer::LogAction()
+{
+ auto it = actionTypes.find(_action);
+ if (it != actionTypes.end()) {
+ vblog(LOG_INFO, "performed action \"%s\"", it->second.c_str());
+ } else {
+ blog(LOG_WARNING, "ignored unknown replay buffer action %d",
+ _action);
+ }
+}
+
+bool MacroActionReplayBuffer::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ obs_data_set_int(obj, "action", static_cast(_action));
+ return true;
+}
+
+bool MacroActionReplayBuffer::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ _action = static_cast(
+ obs_data_get_int(obj, "action"));
+ return true;
+}
+
+static inline void populateActionSelection(QComboBox *list)
+{
+ for (auto entry : actionTypes) {
+ list->addItem(obs_module_text(entry.second.c_str()));
+ }
+}
+
+MacroActionReplayBufferEdit::MacroActionReplayBufferEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _actions = new QComboBox();
+
+ populateActionSelection(_actions);
+
+ QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(ActionChanged(int)));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{actions}}", _actions},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.replay.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroActionReplayBufferEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+ _actions->setCurrentIndex(static_cast(_entryData->_action));
+}
+
+void MacroActionReplayBufferEdit::ActionChanged(int value)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_action = static_cast(value);
+}
diff --git a/src/macro-action-run.cpp b/src/macro-action-run.cpp
new file mode 100644
index 00000000..9923e34e
--- /dev/null
+++ b/src/macro-action-run.cpp
@@ -0,0 +1,98 @@
+#include "headers/macro-action-run.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+#include
+#include
+
+const int MacroActionRun::id = 6;
+
+bool MacroActionRun::_registered = MacroActionFactory::Register(
+ MacroActionRun::id, {MacroActionRun::Create, MacroActionRunEdit::Create,
+ "AdvSceneSwitcher.action.run"});
+
+bool MacroActionRun::PerformAction()
+{
+ QProcess::startDetached(QString::fromStdString(_path), QStringList());
+ return true;
+}
+
+void MacroActionRun::LogAction()
+{
+ vblog(LOG_INFO, "run \"%s\"", _path.c_str());
+}
+
+bool MacroActionRun::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ obs_data_set_string(obj, "path", _path.c_str());
+ return true;
+}
+
+bool MacroActionRun::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ _path = obs_data_get_string(obj, "path");
+ return true;
+}
+
+MacroActionRunEdit::MacroActionRunEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _filePath = new QLineEdit();
+ _browseButton =
+ new QPushButton(obs_module_text("AdvSceneSwitcher.browse"));
+ _browseButton->setStyleSheet("border:1px solid gray;");
+
+ QWidget::connect(_filePath, SIGNAL(editingFinished()), this,
+ SLOT(FilePathChanged()));
+ QWidget::connect(_browseButton, SIGNAL(clicked()), this,
+ SLOT(BrowseButtonClicked()));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{filePath}}", _filePath},
+ {"{{browseButton}}", _browseButton},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.run.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroActionRunEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+ _filePath->setText(QString::fromStdString(_entryData->_path));
+}
+
+void MacroActionRunEdit::FilePathChanged()
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_path = _filePath->text().toUtf8().constData();
+}
+
+void MacroActionRunEdit::BrowseButtonClicked()
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ QString path = QFileDialog::getOpenFileName(this);
+ if (path.isEmpty()) {
+ return;
+ }
+
+ _filePath->setText(path);
+ FilePathChanged();
+}
diff --git a/src/macro-action-streaming.cpp b/src/macro-action-streaming.cpp
new file mode 100644
index 00000000..e1617ad5
--- /dev/null
+++ b/src/macro-action-streaming.cpp
@@ -0,0 +1,111 @@
+#include "headers/macro-action-streaming.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+const int MacroActionStream::id = 3;
+
+bool MacroActionStream::_registered = MacroActionFactory::Register(
+ MacroActionStream::id,
+ {MacroActionStream::Create, MacroActionStreamEdit::Create,
+ "AdvSceneSwitcher.action.streaming"});
+
+const static std::unordered_map actionTypes = {
+ {StreamAction::STOP, "AdvSceneSwitcher.action.streaming.type.stop"},
+ {StreamAction::START, "AdvSceneSwitcher.action.streaming.type.start"},
+};
+
+bool MacroActionStream::PerformAction()
+{
+ switch (_action) {
+ case StreamAction::STOP:
+ if (obs_frontend_streaming_active()) {
+ obs_frontend_streaming_stop();
+ }
+ break;
+ case StreamAction::START:
+ if (!obs_frontend_streaming_active() &&
+ _retryCooldown.DurationReached()) {
+ obs_frontend_streaming_start();
+ _retryCooldown.seconds++;
+ _retryCooldown.Reset();
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+}
+
+void MacroActionStream::LogAction()
+{
+ auto it = actionTypes.find(_action);
+ if (it != actionTypes.end()) {
+ vblog(LOG_INFO, "performed action \"%s\"", it->second.c_str());
+ } else {
+ blog(LOG_WARNING, "ignored unknown streaming action %d",
+ _action);
+ }
+}
+
+bool MacroActionStream::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ obs_data_set_int(obj, "action", static_cast(_action));
+ return true;
+}
+
+bool MacroActionStream::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ _action = static_cast(obs_data_get_int(obj, "action"));
+ return true;
+}
+
+static inline void populateActionSelection(QComboBox *list)
+{
+ for (auto entry : actionTypes) {
+ list->addItem(obs_module_text(entry.second.c_str()));
+ }
+}
+
+MacroActionStreamEdit::MacroActionStreamEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _actions = new QComboBox();
+
+ populateActionSelection(_actions);
+
+ QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(ActionChanged(int)));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{actions}}", _actions},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.streaming.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroActionStreamEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+ _actions->setCurrentIndex(static_cast(_entryData->_action));
+}
+
+void MacroActionStreamEdit::ActionChanged(int value)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_action = static_cast(value);
+}
diff --git a/src/macro-action-switch-scene.cpp b/src/macro-action-switch-scene.cpp
new file mode 100644
index 00000000..2c34bb79
--- /dev/null
+++ b/src/macro-action-switch-scene.cpp
@@ -0,0 +1,73 @@
+#include "headers/macro-action-switch-scene.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+const int MacroActionSwitchScene::id = 0;
+
+bool MacroActionSwitchScene::_registered = MacroActionFactory::Register(
+ MacroActionSwitchScene::id,
+ {MacroActionSwitchScene::Create, MacroActionSwitchSceneEdit::Create,
+ "AdvSceneSwitcher.action.switchScene"});
+
+bool MacroActionSwitchScene::PerformAction()
+{
+ OBSWeakSource scene = getScene();
+ switchScene({scene, transition, (int)(_duration.seconds * 1000)});
+ return true;
+}
+
+void MacroActionSwitchScene::LogAction()
+{
+ if (switcher->verbose) {
+ logMatch();
+ }
+}
+
+bool MacroActionSwitchScene::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ SceneSwitcherEntry::save(obj);
+ _duration.Save(obj);
+ return true;
+}
+
+bool MacroActionSwitchScene::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ SceneSwitcherEntry::load(obj);
+ _duration.Load(obj);
+ return true;
+}
+
+MacroActionSwitchSceneEdit::MacroActionSwitchSceneEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : SwitchWidget(parent, entryData.get(), true, true)
+{
+ _duration = new DurationSelection(parent, false);
+ QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this,
+ SLOT(DurationChanged(double)));
+
+ QHBoxLayout *mainLayout = new QHBoxLayout;
+ std::unordered_map widgetPlaceholders = {
+ {"{{scenes}}", scenes},
+ {"{{transitions}}", transitions},
+ {"{{duration}}", _duration},
+ };
+ placeWidgets(obs_module_text("AdvSceneSwitcher.action.scene.entry"),
+ mainLayout, widgetPlaceholders);
+ setLayout(mainLayout);
+
+ _entryData = entryData;
+ _duration->SetDuration(_entryData->_duration);
+ SwitchWidget::loading = false;
+}
+
+void MacroActionSwitchSceneEdit::DurationChanged(double seconds)
+{
+ if (SwitchWidget::loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_duration.seconds = seconds;
+}
diff --git a/src/macro-action-wait.cpp b/src/macro-action-wait.cpp
new file mode 100644
index 00000000..536cd238
--- /dev/null
+++ b/src/macro-action-wait.cpp
@@ -0,0 +1,206 @@
+#include "headers/macro-action-wait.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+#include "headers/utility.hpp"
+
+#include
+
+const int MacroActionWait::id = 1;
+
+bool MacroActionWait::_registered = MacroActionFactory::Register(
+ MacroActionWait::id,
+ {MacroActionWait::Create, MacroActionWaitEdit::Create,
+ "AdvSceneSwitcher.action.wait"});
+
+static std::unordered_map waitTypes = {
+ {WaitType::FIXED, "AdvSceneSwitcher.action.wait.type.fixed"},
+ {WaitType::RANDOM, "AdvSceneSwitcher.action.wait.type.random"},
+};
+
+static std::random_device rd;
+static std::default_random_engine re(rd());
+
+bool MacroActionWait::PerformAction()
+{
+ double sleep_duration;
+ if (_waitType == WaitType::FIXED) {
+ sleep_duration = _duration.seconds;
+ } else {
+ double min = (_duration.seconds < _duration2.seconds)
+ ? _duration.seconds
+ : _duration2.seconds;
+ double max = (_duration.seconds < _duration2.seconds)
+ ? _duration2.seconds
+ : _duration.seconds;
+ std::uniform_real_distribution unif(min, max);
+ sleep_duration = unif(re);
+ }
+ vblog(LOG_INFO, "perform action wait with duration of %f",
+ sleep_duration);
+
+ std::unique_lock lock(switcher->m);
+ switcher->cv.wait_for(lock, std::chrono::milliseconds((
+ long long)(sleep_duration * 1000)));
+ return !switcher->stop;
+}
+
+bool MacroActionWait::Save(obs_data_t *obj)
+{
+ MacroAction::Save(obj);
+ _duration.Save(obj);
+ _duration2.Save(obj, "seconds2", "displayUnit2");
+ obs_data_set_int(obj, "waitType", static_cast(_waitType));
+ return true;
+}
+
+bool MacroActionWait::Load(obs_data_t *obj)
+{
+ MacroAction::Load(obj);
+ _duration.Load(obj);
+ _duration2.Load(obj, "seconds2", "displayUnit2");
+ _waitType = static_cast(obs_data_get_int(obj, "waitType"));
+ return true;
+}
+
+static inline void populateTypeSelection(QComboBox *list)
+{
+ for (auto entry : waitTypes) {
+ list->addItem(obs_module_text(entry.second.c_str()));
+ }
+}
+
+MacroActionWaitEdit::MacroActionWaitEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent)
+{
+ _duration = new DurationSelection();
+ _duration2 = new DurationSelection();
+ _waitType = new QComboBox();
+
+ populateTypeSelection(_waitType);
+
+ QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this,
+ SLOT(DurationChanged(double)));
+ QWidget::connect(_duration, SIGNAL(UnitChanged(DurationUnit)), this,
+ SLOT(DurationUnitChanged(DurationUnit)));
+ QWidget::connect(_duration2, SIGNAL(DurationChanged(double)), this,
+ SLOT(Duration2Changed(double)));
+ QWidget::connect(_duration2, SIGNAL(UnitChanged(DurationUnit)), this,
+ SLOT(Duration2UnitChanged(DurationUnit)));
+ QWidget::connect(_waitType, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(TypeChanged(int)));
+
+ _mainLayout = new QHBoxLayout;
+ setLayout(_mainLayout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroActionWaitEdit::UpdateEntryData()
+{
+ if (!_entryData) {
+ return;
+ }
+
+ if (_entryData->_waitType == WaitType::FIXED) {
+ SetupFixedDurationEdit();
+ } else {
+ SetupRandomDurationEdit();
+ }
+
+ _duration->SetDuration(_entryData->_duration);
+ _duration2->SetDuration(_entryData->_duration2);
+ _waitType->setCurrentIndex(static_cast(_entryData->_waitType));
+}
+
+void MacroActionWaitEdit::SetupFixedDurationEdit()
+{
+ _mainLayout->removeWidget(_duration);
+ _mainLayout->removeWidget(_duration2);
+ _mainLayout->removeWidget(_waitType);
+ clearLayout(_mainLayout);
+ std::unordered_map widgetPlaceholders = {
+ {"{{duration}}", _duration},
+ {"{{waitType}}", _waitType},
+ };
+ placeWidgets(
+ obs_module_text("AdvSceneSwitcher.action.wait.entry.fixed"),
+ _mainLayout, widgetPlaceholders);
+ _duration2->hide();
+}
+
+void MacroActionWaitEdit::SetupRandomDurationEdit()
+{
+ _mainLayout->removeWidget(_duration);
+ _mainLayout->removeWidget(_duration2);
+ _mainLayout->removeWidget(_waitType);
+ clearLayout(_mainLayout);
+ std::unordered_map widgetPlaceholders = {
+ {"{{duration}}", _duration},
+ {"{{duration2}}", _duration2},
+ {"{{waitType}}", _waitType},
+ };
+ placeWidgets(
+ obs_module_text("AdvSceneSwitcher.action.wait.entry.random"),
+ _mainLayout, widgetPlaceholders);
+ _duration2->show();
+}
+
+void MacroActionWaitEdit::TypeChanged(int value)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ WaitType type = static_cast(value);
+
+ if (type == WaitType::FIXED) {
+ SetupFixedDurationEdit();
+ } else {
+ SetupRandomDurationEdit();
+ }
+
+ _entryData->_waitType = type;
+}
+
+void MacroActionWaitEdit::DurationChanged(double seconds)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_duration.seconds = seconds;
+}
+
+void MacroActionWaitEdit::DurationUnitChanged(DurationUnit unit)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_duration.displayUnit = unit;
+}
+
+void MacroActionWaitEdit::Duration2Changed(double seconds)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_duration2.seconds = seconds;
+}
+
+void MacroActionWaitEdit::Duration2UnitChanged(DurationUnit unit)
+{
+ if (_loading || !_entryData) {
+ return;
+ }
+
+ std::lock_guard lock(switcher->m);
+ _entryData->_duration2.displayUnit = unit;
+}
diff --git a/src/macro-condition-audio.cpp b/src/macro-condition-audio.cpp
new file mode 100644
index 00000000..d1251ceb
--- /dev/null
+++ b/src/macro-condition-audio.cpp
@@ -0,0 +1,239 @@
+#include "headers/macro-condition-edit.hpp"
+#include "headers/macro-condition-audio.hpp"
+#include "headers/utility.hpp"
+#include "headers/advanced-scene-switcher.hpp"
+
+const int MacroConditionAudio::id = 3;
+
+bool MacroConditionAudio::_registered = MacroConditionFactory::Register(
+ MacroConditionAudio::id,
+ {MacroConditionAudio::Create, MacroConditionAudioEdit::Create,
+ "AdvSceneSwitcher.condition.audio"});
+
+static std::unordered_map