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 audioConditionTypes = { + {AudioCondition::ABOVE, "AdvSceneSwitcher.ondition.audio.state.above"}, + {AudioCondition::BELOW, "AdvSceneSwitcher.ondition.audio.state.below"}, +}; + +MacroConditionAudio::~MacroConditionAudio() +{ + obs_volmeter_remove_callback(_volmeter, SetVolumeLevel, this); + obs_volmeter_destroy(_volmeter); +} + +bool MacroConditionAudio::CheckCondition() +{ + // peak will have a value from -60 db to 0 db + bool volumeThresholdreached = false; + + if (_condition == AudioCondition::ABOVE) { + volumeThresholdreached = ((double)_peak + 60) * 1.7 > _volume; + } else { + volumeThresholdreached = ((double)_peak + 60) * 1.7 < _volume; + } + + // Reset for next check + _peak = -std::numeric_limits::infinity(); + + if (!volumeThresholdreached) { + _duration.Reset(); + return false; + } + return _duration.DurationReached(); +} + +bool MacroConditionAudio::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "audioSource", + GetWeakSourceName(_audioSource).c_str()); + obs_data_set_int(obj, "volume", _volume); + obs_data_set_int(obj, "condition", static_cast(_condition)); + _duration.Save(obj); + return true; +} + +obs_volmeter_t *AddVolmeterToSource(MacroConditionAudio *entry, + obs_weak_source *source) +{ + obs_volmeter_t *volmeter = obs_volmeter_create(OBS_FADER_LOG); + obs_volmeter_add_callback(volmeter, MacroConditionAudio::SetVolumeLevel, + entry); + obs_source_t *as = obs_weak_source_get_source(source); + if (!obs_volmeter_attach_source(volmeter, as)) { + const char *name = obs_source_get_name(as); + blog(LOG_WARNING, "failed to attach volmeter to source %s", + name); + } + obs_source_release(as); + + return volmeter; +} + +bool MacroConditionAudio::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + const char *audioSourceName = obs_data_get_string(obj, "audioSource"); + _audioSource = GetWeakSourceByName(audioSourceName); + + _volume = obs_data_get_int(obj, "volume"); + _condition = + static_cast(obs_data_get_int(obj, "condition")); + _duration.Load(obj); + + _volmeter = AddVolmeterToSource(this, _audioSource); + return true; +} + +void MacroConditionAudio::SetVolumeLevel( + void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + UNUSED_PARAMETER(magnitude); + UNUSED_PARAMETER(inputPeak); + MacroConditionAudio *c = static_cast(data); + + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + if (peak[i] > c->_peak) { + c->_peak = peak[i]; + } + } +} + +void MacroConditionAudio::ResetVolmeter() +{ + obs_volmeter_remove_callback(_volmeter, SetVolumeLevel, this); + obs_volmeter_destroy(_volmeter); + + _volmeter = AddVolmeterToSource(this, _audioSource); +} + +static inline void populateConditionSelection(QComboBox *list) +{ + for (auto entry : audioConditionTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionAudioEdit::MacroConditionAudioEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _audioSources = new QComboBox(); + _condition = new QComboBox(); + _volume = new QSpinBox(); + _duration = new DurationSelection(parent, false); + + _volume->setSuffix("%"); + _volume->setMaximum(100); + _volume->setMinimum(0); + + QWidget::connect(_volume, SIGNAL(valueChanged(int)), this, + SLOT(VolumeThresholdChanged(int))); + QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); + QWidget::connect(_audioSources, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(SourceChanged(const QString &))); + + AdvSceneSwitcher::populateAudioSelection(_audioSources); + populateConditionSelection(_condition); + + QHBoxLayout *switchLayout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{audioSources}}", _audioSources}, + {"{{volume}}", _volume}, + {"{{condition}}", _condition}, + {"{{duration}}", _duration}}; + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.audio.entry"), + switchLayout, widgetPlaceholders); + + QVBoxLayout *mainLayout = new QVBoxLayout; + + mainLayout->addLayout(switchLayout); + + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionAudioEdit::UpdateVolmeterSource() +{ + delete _volMeter; + obs_source_t *soruce = + obs_weak_source_get_source(_entryData->_audioSource); + _volMeter = new VolControl(soruce); + obs_source_release(soruce); + + QLayout *layout = this->layout(); + layout->addWidget(_volMeter); + + QWidget::connect(_volMeter->GetSlider(), SIGNAL(valueChanged(int)), + _volume, SLOT(setValue(int))); + QWidget::connect(_volume, SIGNAL(valueChanged(int)), + _volMeter->GetSlider(), SLOT(setValue(int))); + + // Slider will default to 0 so set it manually once + _volMeter->GetSlider()->setValue(_entryData->_volume); +} + +void MacroConditionAudioEdit::SourceChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_audioSource = GetWeakSourceByQString(text); + _entryData->ResetVolmeter(); + UpdateVolmeterSource(); +} + +void MacroConditionAudioEdit::VolumeThresholdChanged(int vol) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_volume = vol; +} + +void MacroConditionAudioEdit::ConditionChanged(int cond) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_condition = static_cast(cond); +} + +void MacroConditionAudioEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +} + +void MacroConditionAudioEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _audioSources->setCurrentText( + GetWeakSourceName(_entryData->_audioSource).c_str()); + _volume->setValue(_entryData->_volume); + _condition->setCurrentIndex(static_cast(_entryData->_condition)); + _duration->SetDuration(_entryData->_duration); + UpdateVolmeterSource(); +} diff --git a/src/macro-condition-edit.cpp b/src/macro-condition-edit.cpp new file mode 100644 index 00000000..31b839b5 --- /dev/null +++ b/src/macro-condition-edit.cpp @@ -0,0 +1,194 @@ +#include "headers/macro-condition-edit.hpp" + +std::map MacroConditionFactory::_methods; + +bool MacroConditionFactory::Register(int id, MacroConditionInfo info) +{ + if (auto it = _methods.find(id); it == _methods.end()) { + _methods[id] = info; + return true; + } + return false; +} + +std::shared_ptr MacroConditionFactory::Create(const int id) +{ + if (auto it = _methods.find(id); it != _methods.end()) + return it->second._createFunc(); + + return nullptr; +} + +QWidget * +MacroConditionFactory::CreateWidget(const int id, QWidget *parent, + std::shared_ptr cond) +{ + if (auto it = _methods.find(id); it != _methods.end()) + return it->second._createWidgetFunc(parent, cond); + + return nullptr; +} + +static inline void populateLogicSelection(QComboBox *list, bool root = false) +{ + if (root) { + for (auto entry : MacroCondition::logicTypes) { + if (static_cast(entry.first) < logic_root_offset) { + list->addItem(obs_module_text( + entry.second._name.c_str())); + } + } + } else { + for (auto entry : MacroCondition::logicTypes) { + if (static_cast(entry.first) >= + logic_root_offset) { + list->addItem(obs_module_text( + entry.second._name.c_str())); + } + } + } +} + +static inline void populateConditionSelection(QComboBox *list) +{ + for (auto entry : MacroConditionFactory::GetConditionTypes()) { + list->addItem(obs_module_text(entry.second._name.c_str())); + } +} + +MacroConditionEdit::MacroConditionEdit( + QWidget *parent, std::shared_ptr *entryData, int type, + bool root, bool startCollapsed) + : QWidget(parent) +{ + _logicSelection = new QComboBox(); + _conditionSelection = new QComboBox(); + _section = new Section(300); + + QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)), + this, SLOT(LogicSelectionChanged(int))); + QWidget::connect(_conditionSelection, SIGNAL(currentIndexChanged(int)), + this, SLOT(ConditionSelectionChanged(int))); + + populateLogicSelection(_logicSelection, root); + populateConditionSelection(_conditionSelection); + + _section->AddHeaderWidget(_logicSelection); + _section->AddHeaderWidget(_conditionSelection); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(_section); + setLayout(mainLayout); + + _entryData = entryData; + _isRoot = root; + UpdateEntryData(type); + _loading = false; + _section->Collapse(startCollapsed); +} + +void MacroConditionEdit::LogicSelectionChanged(int idx) +{ + if (_loading || !_entryData) { + return; + } + + LogicType type; + if (IsRootNode()) { + type = static_cast(idx); + } else { + type = static_cast(idx + logic_root_offset); + } + + std::lock_guard lock(switcher->m); + (*_entryData)->SetLogicType(type); +} + +bool MacroConditionEdit::IsRootNode() +{ + return _isRoot; +} + +void MacroConditionEdit::UpdateEntryData(int type) +{ + _conditionSelection->setCurrentIndex(type); + auto widget = MacroConditionFactory::CreateWidget(type, window(), + *_entryData); + auto logic = (*_entryData)->GetLogicType(); + if (IsRootNode()) { + _logicSelection->setCurrentIndex(static_cast(logic)); + } else { + _logicSelection->setCurrentIndex(static_cast(logic) - + logic_root_offset); + } + _section->SetContent(widget); +} + +void MacroConditionEdit::Collapse(bool collapsed) +{ + _section->Collapse(collapsed); +} + +void MacroConditionEdit::ConditionSelectionChanged(int idx) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + auto logic = (*_entryData)->GetLogicType(); + _entryData->reset(); + *_entryData = MacroConditionFactory::Create(idx); + (*_entryData)->SetLogicType(logic); + auto widget = + MacroConditionFactory::CreateWidget(idx, window(), *_entryData); + _section->SetContent(widget); + _section->Collapse(false); +} + +void AdvSceneSwitcher::on_conditionAdd_clicked() +{ + auto macro = getSelectedMacro(); + if (!macro) { + return; + } + + std::lock_guard lock(switcher->m); + bool root = macro->Conditions().size() == 0; + macro->Conditions().emplace_back(MacroConditionFactory::Create(0)); + auto logic = root ? LogicType::ROOT_NONE : LogicType::NONE; + macro->Conditions().back()->SetLogicType(logic); + auto newEntry = new MacroConditionEdit( + this, ¯o->Conditions().back(), 0, root); + ui->macroEditConditionLayout->addWidget(newEntry); + ui->macroEditConditionHelp->setVisible(false); + newEntry->Collapse(false); +} + +void AdvSceneSwitcher::on_conditionRemove_clicked() +{ + auto macro = getSelectedMacro(); + if (!macro) { + return; + } + std::lock_guard lock(switcher->m); + if (macro->Conditions().empty()) { + return; + } + macro->Conditions().pop_back(); + + int count = ui->macroEditConditionLayout->count(); + auto item = ui->macroEditConditionLayout->takeAt(count - 1); + + if (item) { + auto widget = item->widget(); + if (widget) { + widget->setVisible(false); + } + delete item; + } + + if (count == 1) { + ui->macroEditConditionHelp->setVisible(true); + } +} diff --git a/src/macro-condition-file.cpp b/src/macro-condition-file.cpp new file mode 100644 index 00000000..234c4a5d --- /dev/null +++ b/src/macro-condition-file.cpp @@ -0,0 +1,293 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-file.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" +#include "headers/curl-helper.hpp" + +#include +#include + +const int MacroConditionFile::id = 4; + +bool MacroConditionFile::_registered = MacroConditionFactory::Register( + MacroConditionFile::id, + {MacroConditionFile::Create, MacroConditionFileEdit::Create, + "AdvSceneSwitcher.condition.file"}); + +static std::hash strHash; + +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, + void *userp) +{ + ((std::string *)userp)->append((char *)contents, size * nmemb); + return size * nmemb; +} + +static std::string getRemoteData(std::string &url) +{ + std::string readBuffer; + + if (switcher->curl && f_curl_setopt && f_curl_perform) { + f_curl_setopt(switcher->curl, CURLOPT_URL, url.c_str()); + f_curl_setopt(switcher->curl, CURLOPT_WRITEFUNCTION, + WriteCallback); + f_curl_setopt(switcher->curl, CURLOPT_WRITEDATA, &readBuffer); + f_curl_perform(switcher->curl); + } + return readBuffer; +} + +bool MacroConditionFile::matchFileContent(QString &filedata) +{ + if (_onlyMatchIfChanged) { + size_t newHash = strHash(filedata.toUtf8().constData()); + if (newHash == _lastHash) { + return false; + } + _lastHash = newHash; + } + + if (_useRegex) { + QRegExp rx(QString::fromStdString(_text)); + return rx.exactMatch(filedata); + } + + QString text = QString::fromStdString(_text); + return compareIgnoringLineEnding(text, filedata); +} + +bool MacroConditionFile::checkRemoteFileContent() +{ + std::string data = getRemoteData(_file); + QString qdata = QString::fromStdString(data); + return matchFileContent(qdata); +} + +bool MacroConditionFile::checkLocalFileContent() +{ + QString t = QString::fromStdString(_text); + QFile file(QString::fromStdString(_file)); + if (_file.empty() || !file.open(QIODevice::ReadOnly)) { + return false; + } + + if (_useTime) { + QDateTime newLastMod = QFileInfo(file).lastModified(); + if (_lastMod == newLastMod) { + return false; + } + _lastMod = newLastMod; + } + + QString filedata = QTextStream(&file).readAll(); + bool match = matchFileContent(filedata); + + file.close(); + return match; +} + +bool MacroConditionFile::CheckCondition() +{ + if (_fileType == FileType::REMOTE) { + return checkRemoteFileContent(); + } else { + return checkLocalFileContent(); + } +} + +bool MacroConditionFile::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "file", _file.c_str()); + obs_data_set_string(obj, "text", _text.c_str()); + obs_data_set_int(obj, "fileType", static_cast(_fileType)); + obs_data_set_bool(obj, "useRegex", _useRegex); + obs_data_set_bool(obj, "useTime", _useTime); + obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged); + return true; +} + +bool MacroConditionFile::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _file = obs_data_get_string(obj, "file"); + _text = obs_data_get_string(obj, "text"); + _fileType = static_cast(obs_data_get_int(obj, "fileType")); + _useRegex = obs_data_get_bool(obj, "useRegex"); + _useTime = obs_data_get_bool(obj, "useTime"); + _onlyMatchIfChanged = obs_data_get_bool(obj, "onlyMatchIfChanged"); + return true; +} + +MacroConditionFileEdit::MacroConditionFileEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _fileType = new QComboBox(); + _filePath = new QLineEdit(); + _browseButton = + new QPushButton(obs_module_text("AdvSceneSwitcher.browse")); + _browseButton->setStyleSheet("border:1px solid gray;"); + _matchText = new QPlainTextEdit(); + _useRegex = new QCheckBox( + obs_module_text("AdvSceneSwitcher.fileTab.useRegExp")); + _checkModificationDate = new QCheckBox(obs_module_text( + "AdvSceneSwitcher.fileTab.checkfileContentTime")); + _checkFileContent = new QCheckBox( + obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")); + + QWidget::connect(_fileType, SIGNAL(currentIndexChanged(int)), this, + SLOT(FileTypeChanged(int))); + QWidget::connect(_filePath, SIGNAL(editingFinished()), this, + SLOT(FilePathChanged())); + QWidget::connect(_browseButton, SIGNAL(clicked()), this, + SLOT(BrowseButtonClicked())); + QWidget::connect(_matchText, SIGNAL(textChanged()), this, + SLOT(MatchTextChanged())); + QWidget::connect(_useRegex, SIGNAL(stateChanged(int)), this, + SLOT(UseRegexChanged(int))); + QWidget::connect(_checkModificationDate, SIGNAL(stateChanged(int)), + this, SLOT(CheckModificationDateChanged(int))); + QWidget::connect(_checkFileContent, SIGNAL(stateChanged(int)), this, + SLOT(OnlyMatchIfChangedChanged(int))); + + _fileType->addItem(obs_module_text("AdvSceneSwitcher.fileTab.local")); + _fileType->addItem(obs_module_text("AdvSceneSwitcher.fileTab.remote")); + + std::unordered_map widgetPlaceholders = { + {"{{fileType}}", _fileType}, + {"{{filePath}}", _filePath}, + {"{{browseButton}}", _browseButton}, + {"{{matchText}}", _matchText}, + {"{{useRegex}}", _useRegex}, + {"{{checkModificationDate}}", _checkModificationDate}, + {"{{checkFileContent}}", _checkFileContent}, + }; + + QVBoxLayout *mainLayout = new QVBoxLayout; + QHBoxLayout *line1Layout = new QHBoxLayout; + QHBoxLayout *line2Layout = new QHBoxLayout; + QHBoxLayout *line3Layout = new QHBoxLayout; + placeWidgets( + obs_module_text("AdvSceneSwitcher.condition.file.entry.line1"), + line1Layout, widgetPlaceholders); + placeWidgets( + obs_module_text("AdvSceneSwitcher.condition.file.entry.line2"), + line2Layout, widgetPlaceholders, false); + placeWidgets( + obs_module_text("AdvSceneSwitcher.condition.file.entry.line3"), + line3Layout, widgetPlaceholders); + mainLayout->addLayout(line1Layout); + mainLayout->addLayout(line2Layout); + mainLayout->addLayout(line3Layout); + + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionFileEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _fileType->setCurrentIndex(static_cast(_entryData->_fileType)); + + _filePath->setText(QString::fromStdString(_entryData->_file)); + _matchText->setPlainText(QString::fromStdString(_entryData->_text)); + _useRegex->setChecked(_entryData->_useRegex); + _checkModificationDate->setChecked(_entryData->_useTime); + _checkFileContent->setChecked(_entryData->_onlyMatchIfChanged); +} + +void MacroConditionFileEdit::FileTypeChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + FileType type = static_cast(index); + + if (type == FileType::LOCAL) { + _browseButton->setDisabled(false); + _checkModificationDate->setDisabled(false); + } else { + _browseButton->setDisabled(true); + _checkModificationDate->setDisabled(true); + } + + std::lock_guard lock(switcher->m); + _entryData->_fileType = type; +} + +void MacroConditionFileEdit::FilePathChanged() +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_file = _filePath->text().toUtf8().constData(); +} + +void MacroConditionFileEdit::BrowseButtonClicked() +{ + if (_loading || !_entryData) { + return; + } + + QString path = QFileDialog::getOpenFileName( + this, + tr(obs_module_text("AdvSceneSwitcher.fileTab.selectRead")), + QDir::currentPath(), + tr(obs_module_text("AdvSceneSwitcher.fileTab.anyFileType"))); + if (path.isEmpty()) { + return; + } + + _filePath->setText(path); + FilePathChanged(); +} + +void MacroConditionFileEdit::MatchTextChanged() +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_text = _matchText->toPlainText().toUtf8().constData(); +} + +void MacroConditionFileEdit::UseRegexChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_useRegex = state; +} + +void MacroConditionFileEdit::CheckModificationDateChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_useTime = state; +} + +void MacroConditionFileEdit::OnlyMatchIfChangedChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_onlyMatchIfChanged = state; +} diff --git a/src/macro-condition-media.cpp b/src/macro-condition-media.cpp new file mode 100644 index 00000000..fbcf44e4 --- /dev/null +++ b/src/macro-condition-media.cpp @@ -0,0 +1,377 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-media.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionMedia::id = 5; + +bool MacroConditionMedia::_registered = MacroConditionFactory::Register( + MacroConditionMedia::id, + {MacroConditionMedia::Create, MacroConditionMediaEdit::Create, + "AdvSceneSwitcher.condition.media"}); + +static std::unordered_map + mediaTimeRestrictions = { + {MediaTimeRestriction::TIME_RESTRICTION_NONE, + "AdvSceneSwitcher.mediaTab.timeRestriction.none"}, + {MediaTimeRestriction::TIME_RESTRICTION_SHORTER, + "AdvSceneSwitcher.mediaTab.timeRestriction.shorter"}, + {MediaTimeRestriction::TIME_RESTRICTION_LONGER, + "AdvSceneSwitcher.mediaTab.timeRestriction.longer"}, + {MediaTimeRestriction::TIME_RESTRICTION_REMAINING_SHORTER, + "AdvSceneSwitcher.mediaTab.timeRestriction.remainShorter"}, + {MediaTimeRestriction::TIME_RESTRICTION_REMAINING_LONGER, + "AdvSceneSwitcher.mediaTab.timeRestriction.remainLonger"}, +}; + +static std::unordered_map mediaStates = { + {MediaState::OBS_MEDIA_STATE_NONE, + "AdvSceneSwitcher.mediaTab.states.none"}, + {MediaState::OBS_MEDIA_STATE_PLAYING, + "AdvSceneSwitcher.mediaTab.states.playing"}, + {MediaState::OBS_MEDIA_STATE_OPENING, + "AdvSceneSwitcher.mediaTab.states.opening"}, + {MediaState::OBS_MEDIA_STATE_BUFFERING, + "AdvSceneSwitcher.mediaTab.states.buffering"}, + {MediaState::OBS_MEDIA_STATE_PAUSED, + "AdvSceneSwitcher.mediaTab.states.paused"}, + {MediaState::OBS_MEDIA_STATE_STOPPED, + "AdvSceneSwitcher.mediaTab.states.stopped"}, + {MediaState::OBS_MEDIA_STATE_ENDED, + "AdvSceneSwitcher.mediaTab.states.ended"}, + {MediaState::OBS_MEDIA_STATE_ERROR, + "AdvSceneSwitcher.mediaTab.states.error"}, + {MediaState::PLAYED_TO_END, + "AdvSceneSwitcher.mediaTab.states.playedToEnd"}, + {MediaState::ANY, "AdvSceneSwitcher.mediaTab.states.any"}, +}; + +MacroConditionMedia::~MacroConditionMedia() +{ + obs_source_t *mediasource = obs_weak_source_get_source(_source); + signal_handler_t *sh = obs_source_get_signal_handler(mediasource); + signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); + signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + obs_source_release(mediasource); +} + +bool matchTime(const int64_t currentTime, const int64_t duration, + const MediaTimeRestriction restriction, const int64_t time) +{ + bool matchedTimeNone = + (restriction == MediaTimeRestriction::TIME_RESTRICTION_NONE); + bool matchedTimeLonger = + (restriction == + MediaTimeRestriction::TIME_RESTRICTION_LONGER) && + (currentTime > time); + bool matchedTimeShorter = + (restriction == + MediaTimeRestriction::TIME_RESTRICTION_SHORTER) && + (currentTime < time); + bool matchedTimeRemainLonger = + (restriction == + MediaTimeRestriction::TIME_RESTRICTION_REMAINING_LONGER) && + (duration > currentTime && duration - currentTime > time); + bool matchedTimeRemainShorter = + (restriction == + MediaTimeRestriction::TIME_RESTRICTION_REMAINING_SHORTER) && + (duration > currentTime && duration - currentTime < time); + + return matchedTimeNone || matchedTimeLonger || matchedTimeShorter || + matchedTimeRemainLonger || matchedTimeRemainShorter; +} + +bool MacroConditionMedia::CheckCondition() +{ + if (!_source) { + return false; + } + bool match = false; + + obs_source_t *source = obs_weak_source_get_source(_source); + auto duration = obs_source_media_get_duration(source); + auto time = obs_source_media_get_time(source); + obs_media_state currentState = obs_source_media_get_state(source); + obs_source_release(source); + + // To be able to compare to obs_media_state more easily + int expectedState = static_cast(_state); + + bool matchedStopped = expectedState == OBS_MEDIA_STATE_STOPPED && + _stopped; + + // The signal for the state ended is intentionally not used here + // so matchedEnded can be used to specify the end of a VLC playlist + // by two consequtive matches of OBS_MEDIA_STATE_ENDED + // + // This was done to reduce the likelyhood of interpreting a single + // OBS_MEDIA_STATE_ENDED caught by obs_source_media_get_state() + // as the end of the playlist of the VLC source, while actually being + // in the middle of switching to the next item of the playlist + // + // If there is a separate obs_media_sate in the future for the + // "end of playlist reached" signal the line below can be used + // and an additional check for this new singal can be introduced + // + // bool matchedEnded = _state == OBS_MEDIA_STATE_ENDED && _ended; + + bool ended = false; + + if (currentState == OBS_MEDIA_STATE_ENDED) { + ended = _previousStateEnded; + _previousStateEnded = true; + } else { + _previousStateEnded = false; + } + + bool matchedEnded = ended && (expectedState == OBS_MEDIA_STATE_ENDED); + + // match if playedToEnd was true in last interval + // and playback is currently ended + bool matchedPlayedToEnd = _state == MediaState::PLAYED_TO_END && + _playedToEnd && ended; + + // interval * 2 to make sure not to miss any state changes + // which happened during check of the conditions + _playedToEnd = _playedToEnd || + (duration - time <= switcher->interval * 2); + + // reset for next check + if (ended) { + _playedToEnd = false; + } + _stopped = false; + _ended = false; + + bool matchedState = + (currentState == expectedState || _state == MediaState::ANY) || + matchedStopped || matchedEnded || matchedPlayedToEnd; + + bool matchedTime = + matchTime(time, duration, _restriction, _time.seconds * 1000); + bool matched = matchedState && matchedTime; + + if (matched && !_alreadyMatched) { + match = true; + } + + _alreadyMatched = matched; + + return match; +} + +bool MacroConditionMedia::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "source", GetWeakSourceName(_source).c_str()); + + obs_data_set_int(obj, "state", static_cast(_state)); + obs_data_set_int(obj, "restriction", static_cast(_restriction)); + _time.Save(obj); + return true; +} + +bool MacroConditionMedia::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + const char *sourceName = obs_data_get_string(obj, "source"); + _source = GetWeakSourceByName(sourceName); + + _state = static_cast(obs_data_get_int(obj, "state")); + _restriction = static_cast( + obs_data_get_int(obj, "restriction")); + _time.Load(obj); + + obs_source_t *mediasource = obs_weak_source_get_source(_source); + signal_handler_t *sh = obs_source_get_signal_handler(mediasource); + signal_handler_connect(sh, "media_stopped", MediaStopped, this); + signal_handler_connect(sh, "media_ended", MediaEnded, this); + obs_source_release(mediasource); + return true; +} + +void MacroConditionMedia::ClearSignalHandler() +{ + obs_source_t *mediasource = obs_weak_source_get_source(_source); + signal_handler_t *sh = obs_source_get_signal_handler(mediasource); + signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); + signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + obs_source_release(mediasource); +} + +void MacroConditionMedia::ResetSignalHandler() +{ + obs_source_t *mediasource = obs_weak_source_get_source(_source); + signal_handler_t *sh = obs_source_get_signal_handler(mediasource); + signal_handler_disconnect(sh, "media_stopped", MediaStopped, this); + signal_handler_disconnect(sh, "media_ended", MediaEnded, this); + signal_handler_connect(sh, "media_stopped", MediaStopped, this); + signal_handler_connect(sh, "media_ended", MediaEnded, this); + obs_source_release(mediasource); +} + +void MacroConditionMedia::MediaStopped(void *data, calldata_t *) +{ + MacroConditionMedia *media = static_cast(data); + media->_stopped = true; +} + +void MacroConditionMedia::MediaEnded(void *data, calldata_t *) +{ + MacroConditionMedia *media = static_cast(data); + media->_ended = true; +} + +static void populateMediaTimeRestrictions(QComboBox *list) +{ + for (auto entry : mediaTimeRestrictions) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +static void populateMediaStates(QComboBox *list) +{ + for (auto entry : mediaStates) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionMediaEdit::MacroConditionMediaEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _mediaSources = new QComboBox(); + _states = new QComboBox(); + _timeRestrictions = new QComboBox(); + _time = new DurationSelection(); + + QWidget::connect(_mediaSources, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(SourceChanged(const QString &))); + QWidget::connect(_states, SIGNAL(currentIndexChanged(int)), this, + SLOT(StateChanged(int))); + QWidget::connect(_timeRestrictions, SIGNAL(currentIndexChanged(int)), + this, SLOT(TimeRestrictionChanged(int))); + QWidget::connect(_time, SIGNAL(DurationChanged(double)), this, + SLOT(TimeChanged(double))); + QWidget::connect(_time, SIGNAL(UnitChanged(DurationUnit)), this, + SLOT(TimeUnitChanged(DurationUnit))); + + AdvSceneSwitcher::populateMediaSelection(_mediaSources); + populateMediaStates(_states); + populateMediaTimeRestrictions(_timeRestrictions); + + QHBoxLayout *mainLayout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{mediaSources}}", _mediaSources}, + {"{{states}}", _states}, + {"{{timeRestrictions}}", _timeRestrictions}, + {"{{time}}", _time}, + }; + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.media.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionMediaEdit::SourceChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->ClearSignalHandler(); + _entryData->_source = GetWeakSourceByQString(text); + _entryData->ResetSignalHandler(); +} + +MediaState getMediaStateFromIdx(int idx) +{ + if (idx < static_cast(MediaState::LAST_OBS_MEDIA_STATE)) { + return static_cast(idx); + } else { + return static_cast( + idx - + static_cast(MediaState::LAST_OBS_MEDIA_STATE) + + custom_media_states_offset); + } +} + +void MacroConditionMediaEdit::StateChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_state = getMediaStateFromIdx(index); +} + +void MacroConditionMediaEdit::TimeRestrictionChanged(int index) +{ + if (_loading || !_entryData) { + return; + } + + if (static_cast(index) == + MediaTimeRestriction::TIME_RESTRICTION_NONE) { + _time->setDisabled(true); + } else { + _time->setDisabled(false); + } + + std::lock_guard lock(switcher->m); + _entryData->_restriction = static_cast(index); +} + +void MacroConditionMediaEdit::TimeChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_time.seconds = seconds; +} + +void MacroConditionMediaEdit::TimeUnitChanged(DurationUnit unit) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_time.displayUnit = unit; +} + +int getIdxFromMediaState(MediaState state) +{ + if (state < MediaState::LAST_OBS_MEDIA_STATE) { + return static_cast(state); + } else { + return static_cast(state) - custom_media_states_offset + + static_cast(MediaState::LAST_OBS_MEDIA_STATE); + } +} + +void MacroConditionMediaEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _mediaSources->setCurrentText( + GetWeakSourceName(_entryData->_source).c_str()); + _states->setCurrentIndex(getIdxFromMediaState(_entryData->_state)); + _timeRestrictions->setCurrentIndex( + static_cast(_entryData->_restriction)); + _time->SetDuration(_entryData->_time); + if (_entryData->_restriction == + MediaTimeRestriction::TIME_RESTRICTION_NONE) { + _time->setDisabled(true); + } +} diff --git a/src/macro-condition-plugin-state.cpp b/src/macro-condition-plugin-state.cpp new file mode 100644 index 00000000..83a06268 --- /dev/null +++ b/src/macro-condition-plugin-state.cpp @@ -0,0 +1,93 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-plugin-state.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionPluginState::id = 9; + +bool MacroConditionPluginState::_registered = MacroConditionFactory::Register( + MacroConditionPluginState::id, + {MacroConditionPluginState::Create, + MacroConditionPluginStateEdit::Create, + "AdvSceneSwitcher.condition.pluginState"}); + +static std::unordered_map + pluginStateConditionTypes = { + {PluginStateCondition::SCENESWITCHED, + "AdvSceneSwitcher.condition.pluginState.state.sceneSwitched"}, +}; + +bool MacroConditionPluginState::CheckCondition() +{ + return switcher->macroSceneSwitched; +} + +bool MacroConditionPluginState::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "condition", static_cast(_condition)); + return true; +} + +bool MacroConditionPluginState::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _condition = static_cast( + obs_data_get_int(obj, "condition")); + return true; +} + +static inline void populateConditionSelection(QComboBox *list) +{ + for (auto entry : pluginStateConditionTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionPluginStateEdit::MacroConditionPluginStateEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _condition = new QComboBox(); + + QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + populateConditionSelection(_condition); + + QHBoxLayout *switchLayout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{condition}}", _condition}, + }; + placeWidgets( + obs_module_text("AdvSceneSwitcher.condition.pluginState.entry"), + switchLayout, widgetPlaceholders); + + QVBoxLayout *mainLayout = new QVBoxLayout; + + mainLayout->addLayout(switchLayout); + + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionPluginStateEdit::ConditionChanged(int cond) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_condition = static_cast(cond); +} + +void MacroConditionPluginStateEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _condition->setCurrentIndex(static_cast(_entryData->_condition)); +} diff --git a/src/macro-condition-recording.cpp b/src/macro-condition-recording.cpp new file mode 100644 index 00000000..f18ef38b --- /dev/null +++ b/src/macro-condition-recording.cpp @@ -0,0 +1,138 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-recording.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionRecord::id = 8; + +bool MacroConditionRecord::_registered = MacroConditionFactory::Register( + MacroConditionRecord::id, + {MacroConditionRecord::Create, MacroConditionRecordEdit::Create, + "AdvSceneSwitcher.condition.record"}); + +static std::unordered_map recordStates = { + {RecordState::STOP, "AdvSceneSwitcher.condition.record.state.stop"}, + {RecordState::PAUSE, "AdvSceneSwitcher.condition.record.state.pause"}, + {RecordState::START, "AdvSceneSwitcher.condition.record.state.start"}, +}; + +bool MacroConditionRecord::CheckCondition() +{ + bool stateMatch = false; + switch (_recordState) { + case RecordState::STOP: + stateMatch = !obs_frontend_recording_active(); + break; + case RecordState::PAUSE: + stateMatch = obs_frontend_recording_paused(); + break; + case RecordState::START: + stateMatch = obs_frontend_recording_active(); + break; + default: + break; + } + + if (!stateMatch) { + _duration.Reset(); + return false; + } + + return _duration.DurationReached(); +} + +bool MacroConditionRecord::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "state", static_cast(_recordState)); + _duration.Save(obj); + return true; +} + +bool MacroConditionRecord::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _recordState = static_cast(obs_data_get_int(obj, "state")); + _duration.Load(obj); + return true; +} + +static inline void populateStateSelection(QComboBox *list) +{ + for (auto entry : recordStates) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionRecordEdit::MacroConditionRecordEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _recordState = new QComboBox(); + _duration = new DurationSelection(); + + QWidget::connect(_recordState, SIGNAL(currentIndexChanged(int)), this, + SLOT(StateChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); + QWidget::connect(_duration, SIGNAL(UnitChanged(DurationUnit)), this, + SLOT(DurationUnitChanged(DurationUnit))); + + populateStateSelection(_recordState); + + QHBoxLayout *mainLayout = new QHBoxLayout; + + std::unordered_map widgetPlaceholders = { + {"{{recordState}}", _recordState}, + {"{{duration}}", _duration}, + }; + + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.record.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionRecordEdit::StateChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_recordState = static_cast(value); +} + +void MacroConditionRecordEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +} + +void MacroConditionRecordEdit::DurationUnitChanged(DurationUnit unit) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.displayUnit = unit; +} + +void MacroConditionRecordEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _recordState->setCurrentIndex( + static_cast(_entryData->_recordState)); + _duration->SetDuration(_entryData->_duration); +} diff --git a/src/macro-condition-region.cpp b/src/macro-condition-region.cpp new file mode 100644 index 00000000..fdb331c8 --- /dev/null +++ b/src/macro-condition-region.cpp @@ -0,0 +1,140 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-region.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionRegion::id = 2; + +bool MacroConditionRegion::_registered = MacroConditionFactory::Register( + MacroConditionRegion::id, + {MacroConditionRegion::Create, MacroConditionRegionEdit::Create, + "AdvSceneSwitcher.condition.region"}); + +bool MacroConditionRegion::CheckCondition() +{ + std::pair cursorPos = getCursorPos(); + return cursorPos.first >= _minX && cursorPos.second >= _minY && + cursorPos.first <= _maxX && cursorPos.second <= _maxY; +} + +bool MacroConditionRegion::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "minX", _minX); + obs_data_set_int(obj, "minY", _minY); + obs_data_set_int(obj, "maxX", _maxX); + obs_data_set_int(obj, "maxY", _maxY); + return true; +} + +bool MacroConditionRegion::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _minX = obs_data_get_int(obj, "minX"); + _minY = obs_data_get_int(obj, "minY"); + _maxX = obs_data_get_int(obj, "maxX"); + _maxY = obs_data_get_int(obj, "maxY"); + return true; +} + +MacroConditionRegionEdit::MacroConditionRegionEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _minX = new QSpinBox(); + _minY = new QSpinBox(); + _maxX = new QSpinBox(); + _maxY = new QSpinBox(); + + _minX->setPrefix("Min X: "); + _minY->setPrefix("Min Y: "); + _maxX->setPrefix("Max X: "); + _maxY->setPrefix("Max Y: "); + + _minX->setMinimum(-1000000); + _minY->setMinimum(-1000000); + _maxX->setMinimum(-1000000); + _maxY->setMinimum(-1000000); + + _minX->setMaximum(1000000); + _minY->setMaximum(1000000); + _maxX->setMaximum(1000000); + _maxY->setMaximum(1000000); + + QWidget::connect(_minX, SIGNAL(valueChanged(int)), this, + SLOT(MinXChanged(int))); + QWidget::connect(_minY, SIGNAL(valueChanged(int)), this, + SLOT(MinYChanged(int))); + QWidget::connect(_maxX, SIGNAL(valueChanged(int)), this, + SLOT(MaxXChanged(int))); + QWidget::connect(_maxY, SIGNAL(valueChanged(int)), this, + SLOT(MaxYChanged(int))); + + QHBoxLayout *mainLayout = new QHBoxLayout; + + std::unordered_map widgetPlaceholders = { + {"{{minX}}", _minX}, + {"{{minY}}", _minY}, + {"{{maxX}}", _maxX}, + {"{{maxY}}", _maxY}}; + + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.region.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionRegionEdit::MinXChanged(int pos) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_minX = pos; +} + +void MacroConditionRegionEdit::MinYChanged(int pos) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_minY = pos; +} + +void MacroConditionRegionEdit::MaxXChanged(int pos) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_maxX = pos; +} + +void MacroConditionRegionEdit::MaxYChanged(int pos) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_maxY = pos; +} + +void MacroConditionRegionEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _minX->setValue(_entryData->_minX); + _minY->setValue(_entryData->_minY); + _maxX->setValue(_entryData->_maxX); + _maxY->setValue(_entryData->_maxY); +} diff --git a/src/macro-condition-scene.cpp b/src/macro-condition-scene.cpp new file mode 100644 index 00000000..4e19f8fa --- /dev/null +++ b/src/macro-condition-scene.cpp @@ -0,0 +1,150 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-scene.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionScene::id = 0; + +bool MacroConditionScene::_registered = MacroConditionFactory::Register( + MacroConditionScene::id, + {MacroConditionScene::Create, MacroConditionSceneEdit::Create, + "AdvSceneSwitcher.condition.scene"}); + +static std::unordered_map sceneTypes = { + {SceneType::CURRENT, "AdvSceneSwitcher.condition.scene.type.current"}, + {SceneType::PREVIOUS, "AdvSceneSwitcher.condition.scene.type.previous"}, +}; + +bool MacroConditionScene::CheckCondition() +{ + bool sceneMatch = false; + if (_type == SceneType::CURRENT) { + obs_source_t *rawScene = obs_frontend_get_current_scene(); + OBSWeakSource currentScene = + obs_source_get_weak_source(rawScene); + sceneMatch = currentScene == _scene; + obs_weak_source_release(currentScene); + obs_source_release(rawScene); + } else { + sceneMatch = switcher->previousScene == _scene; + } + + if (!sceneMatch) { + _duration.Reset(); + return false; + } + return _duration.DurationReached(); +} + +bool MacroConditionScene::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "scene", GetWeakSourceName(_scene).c_str()); + obs_data_set_int(obj, "type", static_cast(_type)); + _duration.Save(obj); + return true; +} + +bool MacroConditionScene::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _scene = GetWeakSourceByName(obs_data_get_string(obj, "scene")); + _type = static_cast(obs_data_get_int(obj, "type")); + _duration.Load(obj); + return true; +} + +static inline void populateTypeSelection(QComboBox *list) +{ + for (auto entry : sceneTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionSceneEdit::MacroConditionSceneEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _sceneSelection = new QComboBox(); + _sceneType = new QComboBox(); + _duration = new DurationSelection(); + + QWidget::connect(_sceneSelection, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(SceneChanged(const QString &))); + QWidget::connect(_sceneType, SIGNAL(currentIndexChanged(int)), this, + SLOT(TypeChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); + QWidget::connect(_duration, SIGNAL(UnitChanged(DurationUnit)), this, + SLOT(DurationUnitChanged(DurationUnit))); + + AdvSceneSwitcher::populateSceneSelection(_sceneSelection); + populateTypeSelection(_sceneType); + + QHBoxLayout *mainLayout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{scenes}}", _sceneSelection}, + {"{{sceneType}}", _sceneType}, + {"{{duration}}", _duration}, + }; + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.scene.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionSceneEdit::SceneChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_scene = GetWeakSourceByQString(text); +} + +void MacroConditionSceneEdit::TypeChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_type = static_cast(value); +} + +void MacroConditionSceneEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +} + +void MacroConditionSceneEdit::DurationUnitChanged(DurationUnit unit) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.displayUnit = unit; +} + +void MacroConditionSceneEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _sceneSelection->setCurrentText( + GetWeakSourceName(_entryData->_scene).c_str()); + _sceneType->setCurrentIndex(static_cast(_entryData->_type)); + _duration->SetDuration(_entryData->_duration); +} diff --git a/src/macro-condition-streaming.cpp b/src/macro-condition-streaming.cpp new file mode 100644 index 00000000..e31116c2 --- /dev/null +++ b/src/macro-condition-streaming.cpp @@ -0,0 +1,134 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-streaming.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +const int MacroConditionStream::id = 7; + +bool MacroConditionStream::_registered = MacroConditionFactory::Register( + MacroConditionStream::id, + {MacroConditionStream::Create, MacroConditionStreamEdit::Create, + "AdvSceneSwitcher.condition.stream"}); + +static std::unordered_map streamStates = { + {StreamState::STOP, "AdvSceneSwitcher.condition.stream.state.stop"}, + {StreamState::START, "AdvSceneSwitcher.condition.stream.state.start"}, +}; + +bool MacroConditionStream::CheckCondition() +{ + bool stateMatch = false; + switch (_streamState) { + case StreamState::STOP: + stateMatch = !obs_frontend_streaming_active(); + break; + case StreamState::START: + stateMatch = obs_frontend_streaming_active(); + break; + default: + break; + } + + if (!stateMatch) { + _duration.Reset(); + return false; + } + + return _duration.DurationReached(); +} + +bool MacroConditionStream::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_int(obj, "state", static_cast(_streamState)); + _duration.Save(obj); + return true; +} + +bool MacroConditionStream::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _streamState = static_cast(obs_data_get_int(obj, "state")); + _duration.Load(obj); + return true; +} + +static inline void populateStateSelection(QComboBox *list) +{ + for (auto entry : streamStates) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionStreamEdit::MacroConditionStreamEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _streamState = new QComboBox(); + _duration = new DurationSelection(); + + QWidget::connect(_streamState, SIGNAL(currentIndexChanged(int)), this, + SLOT(StateChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); + QWidget::connect(_duration, SIGNAL(UnitChanged(DurationUnit)), this, + SLOT(DurationUnitChanged(DurationUnit))); + + populateStateSelection(_streamState); + + QHBoxLayout *mainLayout = new QHBoxLayout; + + std::unordered_map widgetPlaceholders = { + {"{{streamState}}", _streamState}, + {"{{duration}}", _duration}, + }; + + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.stream.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionStreamEdit::StateChanged(int value) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_streamState = static_cast(value); +} + +void MacroConditionStreamEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +} + +void MacroConditionStreamEdit::DurationUnitChanged(DurationUnit unit) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.displayUnit = unit; +} + +void MacroConditionStreamEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _streamState->setCurrentIndex( + static_cast(_entryData->_streamState)); + _duration->SetDuration(_entryData->_duration); +} diff --git a/src/macro-condition-video.cpp b/src/macro-condition-video.cpp new file mode 100644 index 00000000..9fa76aae --- /dev/null +++ b/src/macro-condition-video.cpp @@ -0,0 +1,363 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-video.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +#include +#include +#include +#include + +const int MacroConditionVideo::id = 6; + +bool MacroConditionVideo::_registered = MacroConditionFactory::Register( + MacroConditionVideo::id, + {MacroConditionVideo::Create, MacroConditionVideoEdit::Create, + "AdvSceneSwitcher.condition.video"}); + +static std::unordered_map conditionTypes = { + {VideoCondition::MATCH, + "AdvSceneSwitcher.condition.video.condition.match"}, + {VideoCondition::DIFFER, + "AdvSceneSwitcher.condition.video.condition.differ"}, + {VideoCondition::HAS_NOT_CHANGED, + "AdvSceneSwitcher.condition.video.condition.hasNotChanged"}, + {VideoCondition::HAS_CHANGED, + "AdvSceneSwitcher.condition.video.condition.hasChanged"}, + {VideoCondition::NO_IMAGE, + "AdvSceneSwitcher.condition.video.condition.noImage"}, +}; + +bool requiresFileInput(VideoCondition t) +{ + return t == VideoCondition::MATCH || t == VideoCondition::DIFFER; +} + +bool MacroConditionVideo::CheckCondition() +{ + bool match = false; + + if (_screenshotData) { + if (_screenshotData->done) { + bool imageMatch = Compare(); + + if (!imageMatch) { + _duration.Reset(); + } + + if (imageMatch && _duration.DurationReached()) { + match = true; + } + + if (!requiresFileInput(_condition)) { + _matchImage = std::move(_screenshotData->image); + } + _screenshotData.reset(nullptr); + } + } + + GetScreenshot(); + return match; +} + +bool MacroConditionVideo::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "videoSource", + GetWeakSourceName(_videoSource).c_str()); + obs_data_set_int(obj, "condition", static_cast(_condition)); + _duration.Save(obj); + obs_data_set_string(obj, "filePath", _file.c_str()); + return true; +} + +bool MacroConditionVideo::Load(obs_data_t *obj) +{ + const char *videoSourceName = obs_data_get_string(obj, "videoSource"); + _videoSource = GetWeakSourceByName(videoSourceName); + _condition = + static_cast(obs_data_get_int(obj, "condition")); + _duration.Load(obj); + _file = obs_data_get_string(obj, "filePath"); + + if (requiresFileInput(_condition)) { + (void)LoadImageFromFile(); + } + return true; +} + +void MacroConditionVideo::GetScreenshot() +{ + auto source = obs_weak_source_get_source(_videoSource); + _screenshotData = std::make_unique(source); + obs_source_release(source); +} + +bool MacroConditionVideo::LoadImageFromFile() +{ + if (!_matchImage.load(QString::fromStdString(_file))) { + blog(LOG_WARNING, "Cannot load image data from file '%s'", + _file.c_str()); + return false; + } + _matchImage = + _matchImage.convertToFormat(QImage::Format::Format_RGBX8888); + return true; +} + +bool MacroConditionVideo::Compare() +{ + switch (_condition) { + case VideoCondition::MATCH: + return _screenshotData->image == _matchImage; + case VideoCondition::DIFFER: + return _screenshotData->image != _matchImage; + case VideoCondition::HAS_CHANGED: + return _screenshotData->image != _matchImage; + case VideoCondition::HAS_NOT_CHANGED: + return _screenshotData->image == _matchImage; + case VideoCondition::NO_IMAGE: + return _screenshotData->image.isNull(); + default: + break; + } + return false; +} + +static inline void populateConditionSelection(QComboBox *list) +{ + for (auto entry : conditionTypes) { + list->addItem(obs_module_text(entry.second.c_str())); + } +} + +MacroConditionVideoEdit::MacroConditionVideoEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _videoSelection = new QComboBox(); + _condition = new QComboBox(); + _duration = new DurationSelection(); + _filePath = new QLineEdit(); + _browseButton = + new QPushButton(obs_module_text("AdvSceneSwitcher.browse")); + + _filePath->setFixedWidth(100); + + _browseButton->setStyleSheet("border:1px solid gray;"); + + QWidget::connect(_videoSelection, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(SourceChanged(const QString &))); + QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, + SLOT(ConditionChanged(int))); + QWidget::connect(_duration, SIGNAL(DurationChanged(double)), this, + SLOT(DurationChanged(double))); + QWidget::connect(_duration, SIGNAL(UnitChanged(DurationUnit)), this, + SLOT(DurationUnitChanged(DurationUnit))); + QWidget::connect(_filePath, SIGNAL(editingFinished()), this, + SLOT(FilePathChanged())); + QWidget::connect(_browseButton, SIGNAL(clicked()), this, + SLOT(BrowseButtonClicked())); + + AdvSceneSwitcher::populateVideoSelection(_videoSelection, false); + populateConditionSelection(_condition); + + QHBoxLayout *mainLayout = new QHBoxLayout; + std::unordered_map widgetPlaceholders = { + {"{{videoSources}}", _videoSelection}, + {"{{condition}}", _condition}, + {"{{duration}}", _duration}, + {"{{filePath}}", _filePath}, + {"{{browseButton}}", _browseButton}, + }; + placeWidgets(obs_module_text("AdvSceneSwitcher.condition.video.entry"), + mainLayout, widgetPlaceholders); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionVideoEdit::UpdatePreviewTooltip() +{ + if (!_entryData) { + return; + } + + if (!requiresFileInput(_entryData->_condition)) { + this->setToolTip(""); + return; + } + + QImage preview = _entryData->GetMatchImage().scaled( + {300, 300}, Qt::KeepAspectRatio); + + QByteArray data; + QBuffer buffer(&data); + if (!preview.save(&buffer, "PNG")) { + return; + } + + QString html = + QString("") + .arg(QString(data.toBase64())); + this->setToolTip(html); +} + +void MacroConditionVideoEdit::SetFilePath(const QString &text) +{ + _filePath->setText(text); + FilePathChanged(); +} + +void MacroConditionVideoEdit::SourceChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_videoSource = GetWeakSourceByQString(text); +} + +void MacroConditionVideoEdit::ConditionChanged(int cond) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_condition = static_cast(cond); + + if (requiresFileInput(_entryData->_condition)) { + _filePath->show(); + _browseButton->show(); + } else { + _filePath->hide(); + _browseButton->hide(); + } + + // Reload image data to avoid incorrect matches. + // + // Condition type HAS_NOT_CHANGED will use matchImage to store previous + // frame of video source, which will differ from the image stored at + // specified file location. + if (_entryData->LoadImageFromFile()) { + UpdatePreviewTooltip(); + } +} + +void MacroConditionVideoEdit::FilePathChanged() +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_file = _filePath->text().toUtf8().constData(); + if (_entryData->LoadImageFromFile()) { + UpdatePreviewTooltip(); + } +} + +void MacroConditionVideoEdit::BrowseButtonClicked() +{ + if (_loading || !_entryData) { + return; + } + + QString path; + bool useExistingFile = false; + // Ask whether to create screenshot or to select existing file + if (_entryData->_videoSource) { + QMessageBox msgBox( + QMessageBox::Question, + obs_module_text("AdvSceneSwitcher.windowTitle"), + obs_module_text( + "AdvSceneSwitcher.condition.video.askFileAction"), + QMessageBox::Yes | QMessageBox::No); + msgBox.setWindowFlags(Qt::Window | Qt::WindowTitleHint | + Qt::CustomizeWindowHint); + msgBox.setButtonText( + QMessageBox::Yes, + obs_module_text( + "AdvSceneSwitcher.condition.video.askFileAction.file")); + msgBox.setButtonText( + QMessageBox::No, + obs_module_text( + "AdvSceneSwitcher.condition.video.askFileAction.screenshot")); + useExistingFile = msgBox.exec() == QMessageBox::Yes; + } + + if (useExistingFile) { + path = QFileDialog::getOpenFileName(this); + if (path.isEmpty()) { + return; + } + + } else { + auto source = + obs_weak_source_get_source(_entryData->_videoSource); + auto screenshot = std::make_unique(source); + obs_source_release(source); + + path = QFileDialog::getSaveFileName(this); + if (path.isEmpty()) { + return; + } + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + return; + } + if (!screenshot->done) { // Screenshot usually completed by now + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + if (!screenshot->done) { + DisplayMessage("Failed to get screenshot of source!"); + return; + } + + screenshot->image.save(path); + } + SetFilePath(path); +} + +void MacroConditionVideoEdit::DurationChanged(double seconds) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.seconds = seconds; +} + +void MacroConditionVideoEdit::DurationUnitChanged(DurationUnit unit) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_duration.displayUnit = unit; +} + +void MacroConditionVideoEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _videoSelection->setCurrentText( + GetWeakSourceName(_entryData->_videoSource).c_str()); + _condition->setCurrentIndex(static_cast(_entryData->_condition)); + _duration->SetDuration(_entryData->_duration); + _filePath->setText(QString::fromStdString(_entryData->_file)); + + if (!requiresFileInput(_entryData->_condition)) { + _filePath->hide(); + _browseButton->hide(); + } +} diff --git a/src/macro-condition-window.cpp b/src/macro-condition-window.cpp new file mode 100644 index 00000000..ed0923fc --- /dev/null +++ b/src/macro-condition-window.cpp @@ -0,0 +1,195 @@ +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-condition-window.hpp" +#include "headers/utility.hpp" +#include "headers/advanced-scene-switcher.hpp" + +#include + +const int MacroConditionWindow::id = 1; + +bool MacroConditionWindow::_registered = MacroConditionFactory::Register( + MacroConditionWindow::id, + {MacroConditionWindow::Create, MacroConditionWindowEdit::Create, + "AdvSceneSwitcher.condition.window"}); + +bool MacroConditionWindow::CheckWindowTitleSwitchDirect( + std::string ¤tWindowTitle) +{ + bool focus = (!_focus || _window == currentWindowTitle); + bool fullscreen = (!_fullscreen || isFullscreen(_window)); + bool max = (!_maximized || isMaximized(_window)); + + return focus && fullscreen && max; +} + +bool MacroConditionWindow::CheckWindowTitleSwitchRegex( + std::string ¤tWindowTitle, std::vector &windowList) +{ + bool match = false; + for (auto &window : windowList) { + try { + std::regex expr(_window); + if (!std::regex_match(window, expr)) { + continue; + } + } catch (const std::regex_error &) { + } + + bool focus = (!_focus || window == currentWindowTitle); + bool fullscreen = (!_fullscreen || isFullscreen(window)); + bool max = (!_maximized || isMaximized(window)); + + if (focus && fullscreen && max) { + match = true; + break; + } + } + return match; +} + +bool MacroConditionWindow::CheckCondition() +{ + std::string currentWindowTitle; + GetCurrentWindowTitle(currentWindowTitle); + + std::vector windowList; + GetWindowList(windowList); + + bool match = false; + + if (std::find(windowList.begin(), windowList.end(), _window) != + windowList.end()) { + match = CheckWindowTitleSwitchDirect(currentWindowTitle); + } else { + match = CheckWindowTitleSwitchRegex(currentWindowTitle, + windowList); + } + + return match; +} + +bool MacroConditionWindow::Save(obs_data_t *obj) +{ + MacroCondition::Save(obj); + obs_data_set_string(obj, "window", _window.c_str()); + obs_data_set_bool(obj, "fullscreen", _fullscreen); + obs_data_set_bool(obj, "maximized", _maximized); + obs_data_set_bool(obj, "focus", _focus); + return true; +} + +bool MacroConditionWindow::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _window = obs_data_get_string(obj, "window"); + _fullscreen = obs_data_get_bool(obj, "fullscreen"); +#if __APPLE__ + // TODO: Implement maximized check on MacOS + _maximized = false; +#else + _maximized = obs_data_get_bool(obj, "maximized"); +#endif + _focus = obs_data_get_bool(obj, "focus"); + return true; +} + +MacroConditionWindowEdit::MacroConditionWindowEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent) +{ + _windowSelection = new QComboBox(); + _windowSelection->setEditable(true); + _windowSelection->setMaxVisibleItems(20); + + _fullscreen = new QCheckBox(); + _maximized = new QCheckBox(); + _focused = new QCheckBox(); + + QWidget::connect(_windowSelection, + SIGNAL(currentTextChanged(const QString &)), this, + SLOT(WindowChanged(const QString &))); + QWidget::connect(_fullscreen, SIGNAL(stateChanged(int)), this, + SLOT(FullscreenChanged(int))); + QWidget::connect(_maximized, SIGNAL(stateChanged(int)), this, + SLOT(MaximizedChanged(int))); + QWidget::connect(_focused, SIGNAL(stateChanged(int)), this, + SLOT(FocusChanged(int))); + + AdvSceneSwitcher::populateWindowSelection(_windowSelection); + + std::unordered_map widgetPlaceholders = { + {"{{windows}}", _windowSelection}, + {"{{fullscreen}}", _fullscreen}, + {"{{maximized}}", _maximized}, + {"{{focused}}", _focused}, + }; + + QVBoxLayout *mainLayout = new QVBoxLayout; + QHBoxLayout *line1Layout = new QHBoxLayout; + QHBoxLayout *line2Layout = new QHBoxLayout; + placeWidgets(obs_module_text( + "AdvSceneSwitcher.condition.window.entry.line1"), + line1Layout, widgetPlaceholders); + placeWidgets(obs_module_text( + "AdvSceneSwitcher.condition.window.entry.line2"), + line2Layout, widgetPlaceholders); + mainLayout->addLayout(line1Layout); + mainLayout->addLayout(line2Layout); + setLayout(mainLayout); + + _entryData = entryData; + UpdateEntryData(); + _loading = false; +} + +void MacroConditionWindowEdit::WindowChanged(const QString &text) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_window = text.toStdString(); +} + +void MacroConditionWindowEdit::FullscreenChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_fullscreen = state; +} + +void MacroConditionWindowEdit::MaximizedChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_maximized = state; +} + +void MacroConditionWindowEdit::FocusChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + std::lock_guard lock(switcher->m); + _entryData->_focus = state; +} + +void MacroConditionWindowEdit::UpdateEntryData() +{ + if (!_entryData) { + return; + } + + _windowSelection->setCurrentText(_entryData->_window.c_str()); + _fullscreen->setChecked(_entryData->_fullscreen); + _maximized->setChecked(_entryData->_maximized); + _focused->setChecked(_entryData->_focus); +} diff --git a/src/macro-tab.cpp b/src/macro-tab.cpp new file mode 100644 index 00000000..67feda0f --- /dev/null +++ b/src/macro-tab.cpp @@ -0,0 +1,290 @@ +#include "headers/macro.hpp" +#include "headers/macro-action-edit.hpp" +#include "headers/macro-condition-edit.hpp" +#include "headers/advanced-scene-switcher.hpp" +#include "headers/name-dialog.hpp" + +#include + +static QMetaObject::Connection addPulse; + +const auto conditionsCollapseThreshold = 4; +const auto actionsCollapseThreshold = 2; + +bool macroNameExists(std::string name) +{ + for (auto &m : switcher->macros) { + if (m.Name() == name) { + return true; + } + } + + return false; +} + +bool AdvSceneSwitcher::addNewMacro(std::string &name) +{ + QString format{ + obs_module_text("AdvSceneSwitcher.macroTab.defaultname")}; + + int i = 1; + QString placeHolderText = format.arg(i); + while ((macroNameExists(placeHolderText.toUtf8().constData()))) { + placeHolderText = format.arg(++i); + } + + bool accepted = AdvSSNameDialog::AskForName( + this, obs_module_text("AdvSceneSwitcher.macroTab.add"), + obs_module_text("AdvSceneSwitcher.macroTab.name"), name, + placeHolderText); + + if (!accepted) { + return false; + } + + if (name.empty()) { + return false; + } + + if (macroNameExists(name)) { + DisplayMessage( + obs_module_text("AdvSceneSwitcher.macroTab.exists")); + return false; + } + + { + std::lock_guard lock(switcher->m); + switcher->macros.emplace_back(name); + } + return true; +} + +void AdvSceneSwitcher::on_macroAdd_clicked() +{ + std::string name; + if (!addNewMacro(name)) { + return; + } + QString text = QString::fromStdString(name); + + QListWidgetItem *item = new QListWidgetItem(text, ui->macros); + item->setData(Qt::UserRole, text); + ui->macros->setCurrentItem(item); + + ui->macroAdd->disconnect(addPulse); + ui->macroHelp->setVisible(false); +} + +void AdvSceneSwitcher::on_macroRemove_clicked() +{ + QListWidgetItem *item = ui->macros->currentItem(); + if (!item) { + return; + } + { + std::lock_guard lock(switcher->m); + int idx = ui->macros->currentRow(); + switcher->macros.erase(switcher->macros.begin() + idx); + } + + delete item; + + if (ui->macros->count() == 0) { + ui->macroHelp->setVisible(true); + } +} + +void AdvSceneSwitcher::on_macroUp_clicked() +{ + std::lock_guard lock(switcher->m); + int index = ui->macros->currentRow(); + if (index != -1 && index != 0) { + ui->macros->insertItem(index - 1, ui->macros->takeItem(index)); + ui->macros->setCurrentRow(index - 1); + + iter_swap(switcher->macros.begin() + index, + switcher->macros.begin() + index - 1); + } +} + +void AdvSceneSwitcher::on_macroDown_clicked() +{ + std::lock_guard lock(switcher->m); + int index = ui->macros->currentRow(); + if (index != -1 && index != ui->macros->count() - 1) { + ui->macros->insertItem(index + 1, ui->macros->takeItem(index)); + ui->macros->setCurrentRow(index + 1); + + iter_swap(switcher->macros.begin() + index, + switcher->macros.begin() + index + 1); + } +} + +void AdvSceneSwitcher::on_macroName_editingFinished() +{ + bool nameValid = true; + + Macro *macro = getSelectedMacro(); + if (!macro) { + return; + } + + QString newName = ui->macroName->text(); + QString oldName = QString::fromStdString(macro->Name()); + + if (newName.isEmpty() || newName == oldName) { + nameValid = false; + } + + if (nameValid && macroNameExists(newName.toUtf8().constData())) { + DisplayMessage( + obs_module_text("AdvSceneSwitcher.macroTab.exists")); + nameValid = false; + } + + { + std::lock_guard lock(switcher->m); + if (nameValid) { + macro->SetName(newName.toUtf8().constData()); + QListWidgetItem *item = ui->macros->currentItem(); + item->setData(Qt::UserRole, newName); + item->setText(newName); + } else { + ui->macroName->setText(oldName); + } + } +} + +void AdvSceneSwitcher::SetEditMacro(Macro &m) +{ + ui->macroName->setText(m.Name().c_str()); + clearLayout(ui->macroEditConditionLayout); + clearLayout(ui->macroEditActionLayout); + + bool collapse = m.Conditions().size() > conditionsCollapseThreshold; + bool root = true; + for (auto &c : m.Conditions()) { + auto newEntry = new MacroConditionEdit(this, &c, c->GetId(), + root, collapse); + ui->macroEditConditionLayout->addWidget(newEntry); + ui->macroEditConditionHelp->setVisible(false); + root = false; + } + + collapse = m.Actions().size() > actionsCollapseThreshold; + for (auto &a : m.Actions()) { + auto newEntry = + new MacroActionEdit(this, &a, a->GetId(), collapse); + ui->macroEditActionLayout->addWidget(newEntry); + ui->macroEditActionHelp->setVisible(false); + } + + ui->macroEdit->setDisabled(false); + + if (m.Conditions().size() == 0) { + ui->macroEditConditionHelp->setVisible(true); + } else { + ui->macroEditConditionHelp->setVisible(false); + } + + if (m.Actions().size() == 0) { + ui->macroEditActionHelp->setVisible(true); + } else { + ui->macroEditActionHelp->setVisible(false); + } +} + +Macro *AdvSceneSwitcher::getSelectedMacro() +{ + Macro *macro = nullptr; + QListWidgetItem *item = ui->macros->currentItem(); + + if (!item) { + return macro; + } + + QString name = item->data(Qt::UserRole).toString(); + for (auto &m : switcher->macros) { + if (name.compare(m.Name().c_str()) == 0) { + macro = &m; + break; + } + } + + return macro; +} + +void AdvSceneSwitcher::on_macros_currentRowChanged(int idx) +{ + if (loading) { + return; + } + + if (idx == -1) { + ui->macroEdit->setDisabled(true); + return; + } + + QListWidgetItem *item = ui->macros->item(idx); + QString macroName = item->data(Qt::UserRole).toString(); + + for (auto &m : switcher->macros) { + if (macroName.compare(m.Name().c_str()) == 0) { + SetEditMacro(m); + break; + } + } +} + +void AdvSceneSwitcher::setupMacroTab() +{ + for (auto &m : switcher->macros) { + QString text = QString::fromStdString(m.Name()); + + QListWidgetItem *item = new QListWidgetItem(text, ui->macros); + item->setData(Qt::UserRole, text); + } + + if (switcher->macros.size() == 0) { + addPulse = PulseWidget(ui->macroAdd, QColor(Qt::green)); + ui->macroHelp->setVisible(true); + } else { + ui->macroHelp->setVisible(false); + } + + ui->macros->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->macros, SIGNAL(customContextMenuRequested(QPoint)), this, + SLOT(showMacroContextMenu(QPoint))); + + ui->macroEdit->setDisabled(true); +} + +void AdvSceneSwitcher::showMacroContextMenu(const QPoint &pos) +{ + QPoint globalPos = ui->macros->mapToGlobal(pos); + QMenu myMenu; + myMenu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.copy"), + this, SLOT(copyMacro())); + myMenu.exec(globalPos); +} + +void AdvSceneSwitcher::copyMacro() +{ + obs_data_t *data = obs_data_create(); + getSelectedMacro()->Save(data); + + std::string name; + if (!addNewMacro(name)) { + obs_data_release(data); + return; + } + + switcher->macros.back().Load(data); + switcher->macros.back().SetName(name); + obs_data_release(data); + + QString text = QString::fromStdString(name); + QListWidgetItem *item = new QListWidgetItem(text, ui->macros); + item->setData(Qt::UserRole, text); + ui->macros->setCurrentItem(item); +} diff --git a/src/macro.cpp b/src/macro.cpp new file mode 100644 index 00000000..a333f14f --- /dev/null +++ b/src/macro.cpp @@ -0,0 +1,298 @@ +#include "headers/macro.hpp" + +#include "headers/macro-action-edit.hpp" +#include "headers/macro-condition-edit.hpp" +#include "headers/macro-action-switch-scene.hpp" + +const std::unordered_map MacroCondition::logicTypes = { + {LogicType::NONE, {"AdvSceneSwitcher.logic.none"}}, + {LogicType::AND, {"AdvSceneSwitcher.logic.and"}}, + {LogicType::OR, {"AdvSceneSwitcher.logic.or"}}, + {LogicType::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}}, + {LogicType::OR_NOT, {"AdvSceneSwitcher.logic.orNot"}}, + {LogicType::ROOT_NONE, {"AdvSceneSwitcher.logic.rootNone"}}, + {LogicType::ROOT_NOT, {"AdvSceneSwitcher.logic.not"}}, +}; + +Macro::Macro(std::string name) : _name(name) {} + +Macro::~Macro() {} + +bool Macro::CeckMatch() +{ + _matched = false; + for (auto &c : _conditions) { + bool cond = c->CheckCondition(); + + switch (c->GetLogicType()) { + case LogicType::NONE: + vblog(LOG_INFO, + "ignoring condition check 'none' for '%s'", + _name.c_str()); + continue; + break; + case LogicType::AND: + _matched = _matched && cond; + break; + case LogicType::OR: + _matched = _matched || cond; + break; + case LogicType::AND_NOT: + _matched = _matched && !cond; + break; + case LogicType::OR_NOT: + _matched = _matched || !cond; + break; + case LogicType::ROOT_NONE: + _matched = cond; + break; + case LogicType::ROOT_NOT: + _matched = !cond; + break; + default: + blog(LOG_WARNING, + "ignoring unkown condition check for '%s'", + _name.c_str()); + break; + } + } + + return _matched; +} + +bool Macro::PerformAction() +{ + bool ret = true; + for (auto &a : _actions) { + ret = ret && a->PerformAction(); + a->LogAction(); + if (!ret) { + return false; + } + } + + return ret; +} + +bool Macro::Save(obs_data_t *obj) +{ + obs_data_set_string(obj, "name", _name.c_str()); + + obs_data_array_t *conditions = obs_data_array_create(); + for (auto &c : _conditions) { + obs_data_t *array_obj = obs_data_create(); + + c->Save(array_obj); + obs_data_array_push_back(conditions, array_obj); + + obs_data_release(array_obj); + } + obs_data_set_array(obj, "conditions", conditions); + obs_data_array_release(conditions); + + obs_data_array_t *actions = obs_data_array_create(); + for (auto &a : _actions) { + obs_data_t *array_obj = obs_data_create(); + + a->Save(array_obj); + obs_data_array_push_back(actions, array_obj); + + obs_data_release(array_obj); + } + obs_data_set_array(obj, "actions", actions); + obs_data_array_release(actions); + + return true; +} + +bool isValidLogic(LogicType t, bool root) +{ + bool isRoot = isRootLogicType(t); + if (!isRoot == root) { + return false; + } + if (isRoot) { + if (t >= LogicType::ROOT_LAST) { + return false; + } + } else if (t >= LogicType::LAST) { + return false; + } + return true; +} + +void setValidLogic(MacroCondition *c, bool root, std::string name) +{ + if (isValidLogic(c->GetLogicType(), root)) { + return; + } + if (root) { + c->SetLogicType(LogicType::ROOT_NONE); + blog(LOG_WARNING, + "setting invalid logic selection to 'if' for macro %s", + name.c_str()); + } else { + c->SetLogicType(LogicType::NONE); + blog(LOG_WARNING, + "setting invalid logic selection to 'ignore' for macro %s", + name.c_str()); + } +} + +bool Macro::Load(obs_data_t *obj) +{ + _name = obs_data_get_string(obj, "name"); + bool root = true; + + obs_data_array_t *conditions = obs_data_get_array(obj, "conditions"); + size_t count = obs_data_array_count(conditions); + + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(conditions, i); + + int id = obs_data_get_int(array_obj, "id"); + + auto newEntry = MacroConditionFactory::Create(id); + if (newEntry) { + _conditions.emplace_back(newEntry); + auto c = _conditions.back().get(); + c->Load(array_obj); + setValidLogic(c, root, _name); + } else { + blog(LOG_WARNING, + "discarding condition entry with unkown id (%d) for macro %s", + id, _name.c_str()); + } + + obs_data_release(array_obj); + root = false; + } + obs_data_array_release(conditions); + + obs_data_array_t *actions = obs_data_get_array(obj, "actions"); + count = obs_data_array_count(actions); + + for (size_t i = 0; i < count; i++) { + obs_data_t *array_obj = obs_data_array_item(actions, i); + + int id = obs_data_get_int(array_obj, "id"); + + auto newEntry = MacroActionFactory::Create(id); + if (newEntry) { + _actions.emplace_back(newEntry); + _actions.back()->Load(array_obj); + } else { + blog(LOG_WARNING, + "discarding action entry with unkown id (%d) for macro %s", + id, _name.c_str()); + } + + obs_data_release(array_obj); + } + obs_data_array_release(actions); + + return true; +} + +bool Macro::SwitchesScene() +{ + MacroActionSwitchScene temp; + for (auto &a : _actions) { + if (a->GetId() == temp.GetId()) { + return true; + } + } + return false; +} + +bool MacroCondition::Save(obs_data_t *obj) +{ + obs_data_set_int(obj, "id", GetId()); + obs_data_set_int(obj, "logic", static_cast(_logic)); + return true; +} + +bool MacroCondition::Load(obs_data_t *obj) +{ + _logic = static_cast(obs_data_get_int(obj, "logic")); + return true; +} + +bool MacroAction::Save(obs_data_t *obj) +{ + obs_data_set_int(obj, "id", GetId()); + return true; +} + +bool MacroAction::Load(obs_data_t *obj) +{ + UNUSED_PARAMETER(obj); + return true; +} + +void MacroAction::LogAction() +{ + blog(LOG_INFO, "performed action %d", GetId()); +} + +void SwitcherData::saveMacros(obs_data_t *obj) +{ + obs_data_array_t *macroArray = 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(macroArray, array_obj); + + obs_data_release(array_obj); + } + obs_data_set_array(obj, "macros", macroArray); + obs_data_array_release(macroArray); +} + +void SwitcherData::loadMacros(obs_data_t *obj) +{ + macros.clear(); + + obs_data_array_t *macroArray = obs_data_get_array(obj, "macros"); + size_t count = obs_data_array_count(macroArray); + + 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); + obs_data_release(array_obj); + } + obs_data_array_release(macroArray); +} + +bool SwitcherData::checkMacros() +{ + bool ret = false; + for (auto &m : macros) { + 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()) { + switcher->macroSceneSwitched = true; + } + } + } + return ret; +} + +bool SwitcherData::runMacros() +{ + for (auto &m : macros) { + if (m.Matched()) { + blog(LOG_INFO, "running macro: %s", m.Name().c_str()); + if (!m.PerformAction()) { + blog(LOG_WARNING, "abort macro: %s", + m.Name().c_str()); + return false; + } + } + } + return true; +} diff --git a/src/name-dialog.cpp b/src/name-dialog.cpp new file mode 100644 index 00000000..a4db0ed4 --- /dev/null +++ b/src/name-dialog.cpp @@ -0,0 +1,66 @@ +#include +#include +#include "headers/name-dialog.hpp" + +AdvSSNameDialog::AdvSSNameDialog(QWidget *parent) : QDialog(parent) +{ + setModal(true); + setWindowModality(Qt::WindowModality::WindowModal); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setFixedWidth(555); + setMinimumHeight(100); + QVBoxLayout *layout = new QVBoxLayout; + setLayout(layout); + + label = new QLabel(this); + layout->addWidget(label); + label->setText("Set Text"); + + userText = new QLineEdit(this); + layout->addWidget(userText); + + QDialogButtonBox *buttonbox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonbox); + buttonbox->setCenterButtons(true); + connect(buttonbox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonbox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +static bool IsWhitespace(char ch) +{ + return ch == ' ' || ch == '\t'; +} + +static void CleanWhitespace(std::string &str) +{ + while (str.size() && IsWhitespace(str.back())) + str.erase(str.end() - 1); + while (str.size() && IsWhitespace(str.front())) + str.erase(str.begin()); +} + +bool AdvSSNameDialog::AskForName(QWidget *parent, const QString &title, + const QString &text, + std::string &userTextInput, + const QString &placeHolder, int maxSize) +{ + if (maxSize <= 0 || maxSize > 32767) { + maxSize = 170; + } + + AdvSSNameDialog dialog(parent); + dialog.setWindowTitle(title); + + dialog.label->setText(text); + dialog.userText->setMaxLength(maxSize); + dialog.userText->setText(placeHolder); + dialog.userText->selectAll(); + + if (dialog.exec() != DialogCode::Accepted) { + return false; + } + userTextInput = dialog.userText->text().toUtf8().constData(); + CleanWhitespace(userTextInput); + return true; +} diff --git a/src/scene-group.cpp b/src/scene-group.cpp index ff5405ed..b1c3cb9b 100644 --- a/src/scene-group.cpp +++ b/src/scene-group.cpp @@ -1,7 +1,9 @@ #include #include #include + #include "headers/advanced-scene-switcher.hpp" +#include "headers/name-dialog.hpp" #include "headers/utility.hpp" static QMetaObject::Connection addPulse; @@ -127,7 +129,7 @@ void AdvSceneSwitcher::on_sceneGroupAdd_clicked() placeHolderText = format.arg(++i); } - bool accepted = SGNameDialog::AskForName( + bool accepted = AdvSSNameDialog::AskForName( this, obs_module_text("AdvSceneSwitcher.sceneGroupTab.add"), obs_module_text("AdvSceneSwitcher.sceneGroupTab.add"), name, placeHolderText); @@ -509,68 +511,6 @@ void AdvSceneSwitcher::setupSceneGroupTab() ui->sceneGroupEdit->setDisabled(true); } -SGNameDialog::SGNameDialog(QWidget *parent) : QDialog(parent) -{ - setModal(true); - setWindowModality(Qt::WindowModality::WindowModal); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - setFixedWidth(555); - setMinimumHeight(100); - QVBoxLayout *layout = new QVBoxLayout; - setLayout(layout); - - label = new QLabel(this); - layout->addWidget(label); - label->setText("Set Text"); - - userText = new QLineEdit(this); - layout->addWidget(userText); - - QDialogButtonBox *buttonbox = new QDialogButtonBox( - QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - layout->addWidget(buttonbox); - buttonbox->setCenterButtons(true); - connect(buttonbox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonbox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -static bool IsWhitespace(char ch) -{ - return ch == ' ' || ch == '\t'; -} - -static void CleanWhitespace(std::string &str) -{ - while (str.size() && IsWhitespace(str.back())) - str.erase(str.end() - 1); - while (str.size() && IsWhitespace(str.front())) - str.erase(str.begin()); -} - -bool SGNameDialog::AskForName(QWidget *parent, const QString &title, - const QString &text, std::string &userTextInput, - const QString &placeHolder, int maxSize) -{ - if (maxSize <= 0 || maxSize > 32767) { - maxSize = 170; - } - - SGNameDialog dialog(parent); - dialog.setWindowTitle(title); - - dialog.label->setText(text); - dialog.userText->setMaxLength(maxSize); - dialog.userText->setText(placeHolder); - dialog.userText->selectAll(); - - if (dialog.exec() != DialogCode::Accepted) { - return false; - } - userTextInput = dialog.userText->text().toUtf8().constData(); - CleanWhitespace(userTextInput); - return true; -} - void populateTypeSelection(QComboBox *list) { list->addItem( diff --git a/src/section.cpp b/src/section.cpp new file mode 100644 index 00000000..9713950c --- /dev/null +++ b/src/section.cpp @@ -0,0 +1,112 @@ +#include +#include "headers/section.hpp" +#include "headers/utility.hpp" + +Section::Section(const int animationDuration, QWidget *parent) + : QWidget(parent), _animationDuration(animationDuration) +{ + _toggleButton = new QToolButton(this); + _headerLine = new QFrame(this); + _mainLayout = new QGridLayout(this); + _headerWidgetLayout = new QHBoxLayout(); + + _toggleButton->setStyleSheet("QToolButton {border: none;}"); + _toggleButton->setToolButtonStyle(Qt::ToolButtonIconOnly); + _toggleButton->setArrowType(Qt::ArrowType::RightArrow); + _toggleButton->setCheckable(true); + _toggleButton->setChecked(true); + + _headerLine->setFrameShape(QFrame::HLine); + _headerLine->setFrameShadow(QFrame::Sunken); + _headerLine->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Maximum); + + // Don't waste space + _mainLayout->setVerticalSpacing(0); + _mainLayout->setContentsMargins(0, 0, 0, 0); + + // But add some spacing for widgets in header + _headerWidgetLayout->setSpacing(11); + _headerWidgetLayout->addWidget(_toggleButton); + + int row = 0; + _mainLayout->addLayout(_headerWidgetLayout, row, 0, 1, 1, + Qt::AlignLeft); + _mainLayout->addWidget(_headerLine, row++, 2, 1, 1); + setLayout(_mainLayout); + + connect(_toggleButton, &QToolButton::toggled, this, &Section::Collapse); +} + +void Section::Collapse(bool collapsed) +{ + _toggleButton->setChecked(collapsed); + _toggleButton->setArrowType(!collapsed ? Qt::ArrowType::DownArrow + : Qt::ArrowType::RightArrow); + _toggleAnimation->setDirection(!collapsed + ? QAbstractAnimation::Forward + : QAbstractAnimation::Backward); + _toggleAnimation->start(); +} + +void Section::SetContent(QWidget *w) +{ + // Clean up previous content + if (_contentArea) { + auto oldLayout = _contentArea->layout(); + if (oldLayout) { + clearLayout(oldLayout); + delete oldLayout; + } + } + + delete _contentArea; + delete _toggleAnimation; + + // Setup contentArea + _contentArea = new QScrollArea(this); + _contentArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + // Start out collapsed + _contentArea->setMaximumHeight(0); + _contentArea->setMinimumHeight(0); + + auto newLayout = new QVBoxLayout(); + newLayout->addWidget(w); + _contentArea->setLayout(newLayout); + _mainLayout->addWidget(_contentArea, 1, 0, 1, 3); + + // Animation Setup + _toggleAnimation = new QParallelAnimationGroup(this); + _toggleAnimation->addAnimation( + new QPropertyAnimation(this, "minimumHeight")); + _toggleAnimation->addAnimation( + new QPropertyAnimation(this, "maximumHeight")); + _toggleAnimation->addAnimation( + new QPropertyAnimation(_contentArea, "maximumHeight")); + + const auto collapsedHeight = + sizeHint().height() - _contentArea->maximumHeight(); + auto contentHeight = newLayout->sizeHint().height(); + + for (int i = 0; i < _toggleAnimation->animationCount() - 1; ++i) { + QPropertyAnimation *SectionAnimation = + static_cast( + _toggleAnimation->animationAt(i)); + SectionAnimation->setDuration(_animationDuration); + SectionAnimation->setStartValue(collapsedHeight); + SectionAnimation->setEndValue(collapsedHeight + contentHeight); + } + + QPropertyAnimation *contentAnimation = + static_cast(_toggleAnimation->animationAt( + _toggleAnimation->animationCount() - 1)); + contentAnimation->setDuration(_animationDuration); + contentAnimation->setStartValue(0); + contentAnimation->setEndValue(contentHeight); +} + +void Section::AddHeaderWidget(QWidget *w) +{ + _headerWidgetLayout->addWidget(w); +} diff --git a/src/switch-file.cpp b/src/switch-file.cpp index 17ca0197..da050bb2 100644 --- a/src/switch-file.cpp +++ b/src/switch-file.cpp @@ -161,7 +161,7 @@ static size_t WriteCallback(void *contents, size_t size, size_t nmemb, return size * nmemb; } -std::string getRemoteData(std::string &url) +static std::string getRemoteData(std::string &url) { std::string readBuffer; diff --git a/src/switch-generic.cpp b/src/switch-generic.cpp index 5490a4a6..f8cb8c5e 100644 --- a/src/switch-generic.cpp +++ b/src/switch-generic.cpp @@ -1,6 +1,34 @@ #include "headers/advanced-scene-switcher.hpp" #include "headers/utility.hpp" +static inline bool SceneGroupValid(SceneGroup *group) +{ + if (group) { + return group->name != invalid_scene_group_name; + } + return false; +} + +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()); +} + bool SceneSwitcherEntry::initialized() { return (usePreviousScene || WeakSourceValid(scene) || diff --git a/src/switch-media.cpp b/src/switch-media.cpp index 576d058a..c83c1627 100644 --- a/src/switch-media.cpp +++ b/src/switch-media.cpp @@ -400,7 +400,7 @@ void swap(MediaSwitch &first, MediaSwitch &second) second.resetSignalHandler(); } -void populateMediaStates(QComboBox *list) +static void populateMediaStates(QComboBox *list) { list->addItem(obs_module_text("AdvSceneSwitcher.mediaTab.states.none")); list->addItem( @@ -422,7 +422,7 @@ void populateMediaStates(QComboBox *list) list->addItem(obs_module_text("AdvSceneSwitcher.mediaTab.states.any")); } -void populateTimeRestrictions(QComboBox *list) +static void populateTimeRestrictions(QComboBox *list) { list->addItem(obs_module_text( "AdvSceneSwitcher.mediaTab.timeRestriction.none")); diff --git a/src/switch-priority.cpp b/src/switch-priority.cpp index c7c16629..2e720925 100644 --- a/src/switch-priority.cpp +++ b/src/switch-priority.cpp @@ -66,7 +66,7 @@ bool SwitcherData::prioFuncsValid() } for (int p : functionNamesByPriority) { - if (p < 0 || p > 9) { + if (p < 0 || p > 10) { return false; } }