Compare commits

...

28 Commits
1.33.1 ... ui

Author SHA1 Message Date
WarmUpTill
df358de5fb Fix double free 2026-05-11 17:22:36 +02:00
WarmUpTill
6d7e23c88f locale: Clarify "reevaluate condition state" option for run macro action 2026-05-08 21:08:30 +02:00
WarmUpTill
9fff05c57c Fix "Run Macro" help being visible when option is not selected 2026-05-08 21:08:30 +02:00
WarmUpTill
6da151f7d7 Fix variable tab not listing variables on startup 2026-05-07 20:16:42 +02:00
WarmUpTill
61ee58ba70 Enable resolving plugin symbols in OBS crash reports 2026-05-06 20:40:15 +02:00
WarmUpTill
312fe1d648 Don't highlight paused macro in macro list 2026-05-06 20:40:15 +02:00
WarmUpTill
5d3c83c292 Don't log "on change" when macro is paused 2026-05-06 20:40:15 +02:00
WarmUpTill
964b6c6c71 Don't highlight action trigger mode when macro is paused 2026-05-06 20:40:15 +02:00
WarmUpTill
27116c0cd3 Ensure plugin is stopped before running cleanup steps 2026-05-06 20:40:15 +02:00
WarmUpTill
06c29bced5 Replace "Ignore Entry" condition logic with SwitchButton toggle 2026-05-06 20:40:10 +02:00
WarmUpTill
4f3d9e3a00 Use static self-registration for action queue and variable setup
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Removes explicit setup calls from the plugin core. Each module now
registers its own save/load/cleanup steps via a static initializer,
decoupling them from InitSceneSwitcher and SwitcherData.
2026-04-30 22:39:06 +02:00
WarmUpTill
c02896ea17 Clear websocket and mqtt connections on shutdown
This was previously implicitly done by a settings reload on shutdown,
but this has since been removed, and we need to clear the connections
explicitly.
2026-04-30 22:39:06 +02:00
WarmUpTill
4bfd40219b Deregister all inline scripts on SCRIPTING_SHUTDOWN 2026-04-30 22:39:06 +02:00
WarmUpTill
6a3de18069 Clear macros on shutdown
This was done implicitly in the shutdown handler by clearing the
plugin's settings.
Due to unwanted side effects this behavior has been disabled for the
shutdown and thus macros have to be cleared explicitly on shutdown.
2026-04-30 22:39:06 +02:00
WarmUpTill
8c51b44fbb Fix potential event sub freeze on disconnect 2026-04-30 22:39:06 +02:00
WarmUpTill
10daa38921 Cleanup 2026-04-30 22:39:06 +02:00
WarmUpTill
6e0f7ab38f Default to assuming OBS is shutting down
This reverts commit eea91b79b38b7c33561dc6c639d01dea8a802cdd.
2026-04-30 22:39:06 +02:00
WarmUpTill
86061b3cf1 Ignore OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP on shutdown
This prevents the plugin being restarted, connections being reset, and
potentially delaying the shutdown of OBS.
2026-04-30 22:39:06 +02:00
WarmUpTill
c3669cae3e Tests: Make building tests optional, enable for CI 2026-04-30 20:11:52 +02:00
WarmUpTill
a116360f87 Tests: Handle QFile::open() return value 2026-04-30 20:11:52 +02:00
WarmUpTill
713f695aa4 Show variable mapping for "Source" and "Twitch" action
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-27 19:20:51 +02:00
WarmUpTill
dec5c2d763 Add option to directly map temp var values to variables 2026-04-27 19:20:51 +02:00
WarmUpTill
f090dea136 Add "Run macro" option
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-21 17:48:05 +02:00
WarmUpTill
d00986df03 Add button to get current values to "Streaming" action 2026-04-21 17:45:35 +02:00
WarmUpTill
03e5397b7e CI: remove openssl workaround
Some checks are pending
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-04-20 20:47:46 +02:00
WarmUpTill
94d49db196 Add tests for process condition
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-15 14:02:59 +02:00
WarmUpTill
1483f9d9dc Cleanup / style changes 2026-04-15 14:02:59 +02:00
WarmUpTill
99629e8c66 Add process path check 2026-04-15 14:02:59 +02:00
71 changed files with 1700 additions and 302 deletions

View File

@@ -255,6 +255,10 @@ ${_usage_host:-}"
cmake_args+=(
-DCMAKE_PREFIX_PATH="${advss_deps_path}"
-DOPENSSL_ROOT_DIR="${advss_deps_path}"
-DOPENSSL_INCLUDE_DIR="${advss_deps_path}/include"
-DOPENSSL_CRYPTO_LIBRARY="${advss_deps_path}/lib/libcrypto.a"
-DOPENSSL_SSL_LIBRARY="${advss_deps_path}/lib/libssl.a"
--preset ${_preset}
)

View File

@@ -18,7 +18,4 @@ if (( ! ${+commands[brew]} )) {
}
brew bundle --file ${SCRIPT_HOME}/.Brewfile
# Workaround to make sure locally built openssl is picked up by cmake
brew uninstall --ignore-dependencies openssl@3 || true
rehash || true
log_group

View File

@@ -6,7 +6,7 @@ on:
description: "Project name detected by parsing build spec file"
value: ${{ jobs.check-event.outputs.pluginName }}
env:
DEP_DIR: .deps/advss-build-dependencies-3
DEP_DIR: .deps/advss-build-dependencies-4
jobs:
check-event:
name: Check GitHub Event Data 🔎

View File

@@ -504,7 +504,11 @@ endif()
# --- End of section ---
add_subdirectory(plugins)
add_subdirectory(tests)
option(ADVSS_ENABLE_TESTS "Build advanced-scene-switcher unit tests" OFF)
if(ADVSS_ENABLE_TESTS)
add_subdirectory(tests)
endif()
# --- Install ---

View File

@@ -41,7 +41,8 @@
"description": "Build for macOS 11.0+ (Universal binary) for CI",
"generator": "Xcode",
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"ADVSS_ENABLE_TESTS": true
}
},
{
@@ -69,7 +70,8 @@
"displayName": "Windows x64 CI build",
"description": "Build for Windows x64 on CI",
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"ADVSS_ENABLE_TESTS": true
}
},
{
@@ -97,7 +99,8 @@
"description": "Build for Linux x86_64 on CI",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_COMPILE_WARNING_AS_ERROR": true
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"ADVSS_ENABLE_TESTS": true
}
},
{
@@ -125,7 +128,8 @@
"description": "Build for Linux aarch64 on CI",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_COMPILE_WARNING_AS_ERROR": true
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"ADVSS_ENABLE_TESTS": true
}
}
],

View File

@@ -99,7 +99,6 @@ AdvSceneSwitcher.macroList.deleted="gelöscht"
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
; Macro Logic
AdvSceneSwitcher.logic.none="Eintrag ignorieren"
AdvSceneSwitcher.logic.and="Und"
AdvSceneSwitcher.logic.or="Oder"
AdvSceneSwitcher.logic.andNot="Und nicht"
@@ -211,8 +210,8 @@ AdvSceneSwitcher.condition.record.state.start="Aufnahme läuft"
AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert"
AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt"
AdvSceneSwitcher.condition.process="Prozess"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.entry.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.layout.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="Leerlauf"
AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin-Status"
@@ -416,7 +415,6 @@ AdvSceneSwitcher.action.replay.type.save="Replay Buffer speichern"
AdvSceneSwitcher.action.streaming="Stream"
AdvSceneSwitcher.action.streaming.type.stop="Stream stoppen"
AdvSceneSwitcher.action.streaming.type.start="Stream starten"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ausführen"
AdvSceneSwitcher.action.sceneVisibility="Sichtbarkeit von Szenenelementen"
AdvSceneSwitcher.action.sceneVisibility.type.show="Anzeigen"
@@ -452,7 +450,7 @@ AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Pausieren"
AdvSceneSwitcher.action.macro.type.unpause="Nicht mehr pausieren"
AdvSceneSwitcher.action.macro.type.resetCounter="Zähler zurücksetzen"
AdvSceneSwitcher.action.macro.type.run="Aktionen ausführen"
AdvSceneSwitcher.action.macro.type.runActions="Aktionen ausführen"
AdvSceneSwitcher.action.macro.type.stop="Aktionen stoppen"
AdvSceneSwitcher.action.pluginState="Plugin-Status"
AdvSceneSwitcher.action.pluginState.type.stop="Erweiterten Automatischen Szenenwechsler stoppen"

View File

@@ -306,7 +306,6 @@ AdvSceneSwitcher.macroList.deleted="deleted"
AdvSceneSwitcher.macroList.duplicate="\"%1\" is alreay selected!"
# Macro Logic
AdvSceneSwitcher.logic.none="Ignore entry"
AdvSceneSwitcher.logic.and="And"
AdvSceneSwitcher.logic.or="Or"
AdvSceneSwitcher.logic.andNot="And not"
@@ -484,8 +483,9 @@ AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
AdvSceneSwitcher.condition.record.state.duration="Recording duration is longer than"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.layout.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout.path="{{checkPath}}Match binary path:{{path}}{{pathRegex}}"
AdvSceneSwitcher.condition.idle="Idle"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin state"
@@ -941,7 +941,8 @@ AdvSceneSwitcher.action.streaming.type.server="Set server URL"
AdvSceneSwitcher.action.streaming.type.streamKey="Set stream key"
AdvSceneSwitcher.action.streaming.type.username="Set username"
AdvSceneSwitcher.action.streaming.type.password="Set password"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.streaming.getCurrentValue="Get current value"
AdvSceneSwitcher.action.streaming.layout="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}{{getCurrentValue}}"
AdvSceneSwitcher.action.run="Run"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}Wait for process exit or at most {{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="Note that macro properties won't work if you leave this unticked, as the process spawns detached from the rest of logic and there's no control over it."
@@ -1070,12 +1071,12 @@ AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Unpause"
AdvSceneSwitcher.action.macro.type.togglePause="Toggle pause"
AdvSceneSwitcher.action.macro.type.resetCounter="Reset counter"
AdvSceneSwitcher.action.macro.type.run="Run macro"
AdvSceneSwitcher.action.macro.type.runActions="Run macro actions"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="Do not consider condition state"
AdvSceneSwitcher.action.macro.type.run.conditions.true="Only if conditions evaluate to true"
AdvSceneSwitcher.action.macro.type.run.conditions.false="Only if conditions evaluate to false"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Reevaluate the condition state before executing this action"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nCheck this option if you want those side effects to be taken into account when evaluating the condition state."
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Check conditions now, even if macro is paused"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nAdditionally, paused macros are assumed to not match their conditions.\nCheck this option to force a fresh condition evaluation before running this action, regardless of whether the macro is paused."
AdvSceneSwitcher.action.macro.type.run.actionType.regular="actions"
AdvSceneSwitcher.action.macro.type.run.actionType.else="else-actions"
AdvSceneSwitcher.action.macro.type.run.skipWhenPaused="Skip execution when macro is paused"
@@ -1086,6 +1087,9 @@ AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
AdvSceneSwitcher.action.macro.type.getInfo="Get macro info"
AdvSceneSwitcher.action.macro.type.runMacro="Run macro"
AdvSceneSwitcher.action.macro.type.runMacro.noConditionsWarning="Warning: The selected macro has no conditions!"
AdvSceneSwitcher.action.macro.type.runMacro.help="Unlike \"Run macro actions\", this option will immediately re-evaluate the conditions of the selected macro and then run either its actions or else-actions depending on the result."
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
@@ -1096,6 +1100,7 @@ AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allo
AdvSceneSwitcher.action.macro.layout.run="{{actions}}{{actionSections}}of{{macros}}"
AdvSceneSwitcher.action.macro.layout.run.condition="{{conditionBehaviors}}of{{conditionMacros}}"
AdvSceneSwitcher.action.macro.layout.actionState="{{actions}}{{actionSelectionType}}{{actionIndex}}{{label}}{{regex}}{{actionTypes}}in{{actionSections}}section of{{macros}}"
AdvSceneSwitcher.action.macro.layout.runMacro="{{actions}}{{runMacroHelp}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.other="{{actions}}{{macros}}"
AdvSceneSwitcher.action.pluginState="Plugin state"
AdvSceneSwitcher.action.pluginState.type.stop="Stop the Advanced Scene Switcher plugin"
@@ -1789,6 +1794,10 @@ AdvSceneSwitcher.script.file.layout="Script file:{{path}}{{open}}"
AdvSceneSwitcher.tempVar.select="--select value--"
AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:"
AdvSceneSwitcher.tempVar.outputMappings="Save outputs to variables:"
AdvSceneSwitcher.tempVar.outputMappings.add="+ Add output mapping"
AdvSceneSwitcher.tempVar.outputMappings.toggle="Assign outputs to variables"
AdvSceneSwitcher.tempVar.outputMappings.remove="Remove output mapping"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id="Twitch broadcaster user ID"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id.description="The numerical Twitch user ID of the broadcaster."
@@ -2312,6 +2321,7 @@ AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun="Seconds since last run"
AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description="The number of seconds elapsed since the macro was last executed. Value is -1 if the macro has never been executed."
AdvSceneSwitcher.tempVar.process.name="Process name"
AdvSceneSwitcher.tempVar.process.path="Process path"
AdvSceneSwitcher.tempVar.run.process.id="Process ID"
AdvSceneSwitcher.tempVar.run.process.id.description="PID of the process assigned by the system."

View File

@@ -83,7 +83,6 @@ AdvSceneSwitcher.macroTab.highlightTrueConditions="Resaltar condiciones de la ma
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
; Lógica de macros
AdvSceneSwitcher.logic.none="Omitir entrada"
AdvSceneSwitcher.logic.and="Y"
AdvSceneSwitcher.logic.or="O"
AdvSceneSwitcher.logic.andNot="Y no"
@@ -173,7 +172,7 @@ AdvSceneSwitcher.condition.record.state.start="Grabación en ejecución"
AdvSceneSwitcher.condition.record.state.pause="Grabación en pausa"
AdvSceneSwitcher.condition.record.state.stop="Grabación detenida"
AdvSceneSwitcher.condition.process="Proceso"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}se está ejecutando{{focused}}y está enfocado"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}se está ejecutando{{focused}}y está enfocado"
AdvSceneSwitcher.condition.idle="Inactivo"
AdvSceneSwitcher.condition.idle.entry="No hay entradas de teclado o ratón durante {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado del complemento"
@@ -340,7 +339,6 @@ AdvSceneSwitcher.action.replay.type.save="Guardar búfer de reproducción"
AdvSceneSwitcher.action.streaming="Transmisión"
AdvSceneSwitcher.action.streaming.type.stop="Detener transmisión"
AdvSceneSwitcher.action.streaming.type.start="Iniciar transmisión"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ejecutar"
AdvSceneSwitcher.action.sceneVisibility="Visibilidad del elemento de escena"
AdvSceneSwitcher.action.sceneVisibility.type.show="Mostrar"
@@ -372,7 +370,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pausa"
AdvSceneSwitcher.action.macro.type.unpause="Reanudar"
AdvSceneSwitcher.action.macro.type.resetCounter="Reiniciar contador"
AdvSceneSwitcher.action.macro.type.run="Ejecutar"
AdvSceneSwitcher.action.macro.type.runActions="Ejecutar"
AdvSceneSwitcher.action.macro.type.stop="Detener"
AdvSceneSwitcher.action.pluginState="Estado del complemento"
AdvSceneSwitcher.action.pluginState.type.stop="Detener el complemento Advanced Scene Switcher"

View File

@@ -131,7 +131,6 @@ AdvSceneSwitcher.macroList.deleted="supprimé"
AdvSceneSwitcher.macroList.duplicate="\"%1\" est déjà sélectionné !"
; Macro Logic
AdvSceneSwitcher.logic.none="Ignorer l'entrée"
AdvSceneSwitcher.logic.and="Et"
AdvSceneSwitcher.logic.or="Ou"
AdvSceneSwitcher.logic.andNot="Et pas"
@@ -279,8 +278,8 @@ AdvSceneSwitcher.condition.record.state.start="Enregistrement en cours"
AdvSceneSwitcher.condition.record.state.pause="Enregistrement en pause"
AdvSceneSwitcher.condition.record.state.stop="Arrêt de l'enregistrement"
AdvSceneSwitcher.condition.process="Processus"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.entry.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.layout.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inactif"
AdvSceneSwitcher.condition.idle.entry="Aucune entrée de clavier ou de souris pendant{{duration}}"
AdvSceneSwitcher.condition.pluginState="État du plugin"
@@ -551,7 +550,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Reprendre"
AdvSceneSwitcher.action.macro.type.resetCounter="Réinitialiser le compteur"
AdvSceneSwitcher.action.macro.type.run="Exécuter les actions"
AdvSceneSwitcher.action.macro.type.runActions="Exécuter les actions"
AdvSceneSwitcher.action.macro.type.stop="Arrêter les actions"
AdvSceneSwitcher.action.macro.type.disableAction="Désactiver l'action"
AdvSceneSwitcher.action.macro.type.enableAction="Activer l'action"

View File

@@ -275,7 +275,6 @@ AdvSceneSwitcher.macroList.deleted="削除"
AdvSceneSwitcher.macroList.duplicate="\"%1\" はすでに選択されています!"
# Macro Logic
AdvSceneSwitcher.logic.none="入力無視"
; AdvSceneSwitcher.logic.and="And"
; AdvSceneSwitcher.logic.or="Or"
; AdvSceneSwitcher.logic.andNot="And not"
@@ -445,8 +444,8 @@ AdvSceneSwitcher.condition.stream.service.tooltip="現在のサービス名: %1"
AdvSceneSwitcher.condition.record.state.duration="録音時間が長くなっています。"
; AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="プロセス"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}が実行中{{focused}}に集中しています"
AdvSceneSwitcher.condition.process.entry.focus="現在のフォアグラウンドプロセス:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}が実行中{{focused}}に集中しています"
AdvSceneSwitcher.condition.process.layout.focus="現在のフォアグラウンドプロセス:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="アイドル"
AdvSceneSwitcher.condition.idle.entry="{{duration}}の間、キーボードまたはマウスの入力がありません"
AdvSceneSwitcher.condition.pluginState="プラグインの状態"
@@ -923,7 +922,7 @@ AdvSceneSwitcher.action.macro.type.pause="一時停止"
AdvSceneSwitcher.action.macro.type.unpause="一時停止解除"
AdvSceneSwitcher.action.macro.type.togglePause="一時停止を切り替え"
AdvSceneSwitcher.action.macro.type.resetCounter="カウンターリセット"
AdvSceneSwitcher.action.macro.type.run="マクロを実行"
AdvSceneSwitcher.action.macro.type.runActions="マクロを実行"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="条件の状態を考慮しない"
AdvSceneSwitcher.action.macro.type.run.conditions.true="条件が true と評価された場合のみ"
AdvSceneSwitcher.action.macro.type.run.conditions.false="条件が false と評価された場合のみ"

View File

@@ -239,7 +239,6 @@ AdvSceneSwitcher.macroList.deleted="excluída"
AdvSceneSwitcher.macroList.duplicate="\"%1\" já está selecionada!"
; Macro Logic
AdvSceneSwitcher.logic.none="Ignorar entrada"
AdvSceneSwitcher.logic.and="E"
AdvSceneSwitcher.logic.or="Ou"
AdvSceneSwitcher.logic.andNot="E não"
@@ -395,8 +394,8 @@ AdvSceneSwitcher.condition.record.state.stop="Gravação parada"
AdvSceneSwitcher.condition.record.state.duration="Duração da gravação é maior que"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Processo"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}está em execução{{focused}}e está em foco"
AdvSceneSwitcher.condition.process.entry.focus="Processo atual em primeiro plano:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}está em execução{{focused}}e está em foco"
AdvSceneSwitcher.condition.process.layout.focus="Processo atual em primeiro plano:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inativo"
AdvSceneSwitcher.condition.idle.entry="Sem entradas de teclado ou mouse por {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado do plugin"
@@ -765,7 +764,6 @@ AdvSceneSwitcher.action.streaming.type.server="Definir URL do servidor"
AdvSceneSwitcher.action.streaming.type.streamKey="Definir chave de transmissão"
AdvSceneSwitcher.action.streaming.type.username="Definir nome de usuário"
AdvSceneSwitcher.action.streaming.type.password="Definir senha"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Executar"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}Aguardar a saída do processo ou no máximo {{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="Observe que as propriedades da macro não funcionarão se você deixar isso desmarcado, pois o processo é iniciado de forma independente do restante da lógica e não há controle sobre ele."
@@ -840,7 +838,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pausar"
AdvSceneSwitcher.action.macro.type.unpause="Retomar"
AdvSceneSwitcher.action.macro.type.resetCounter="Reiniciar contador"
AdvSceneSwitcher.action.macro.type.run="Executar macro"
AdvSceneSwitcher.action.macro.type.runActions="Executar macro"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="Não considerar o estado da condição"
AdvSceneSwitcher.action.macro.type.run.conditions.true="Apenas se as condições forem verdadeiras"
AdvSceneSwitcher.action.macro.type.run.conditions.false="Apenas se as condições forem falsas"

View File

@@ -70,7 +70,6 @@ AdvSceneSwitcher.macroTab.name="Имя:"
AdvSceneSwitcher.macroTab.defaultname="Макрос %1"
AdvSceneSwitcher.macroTab.copy="Создать копию"
; Macro Logic
AdvSceneSwitcher.logic.none="Игнорировать вход"
AdvSceneSwitcher.logic.and="И"
AdvSceneSwitcher.logic.or="Или"
AdvSceneSwitcher.logic.andNot="И не"
@@ -103,7 +102,7 @@ AdvSceneSwitcher.condition.record.state.start="Запись запущена"
AdvSceneSwitcher.condition.record.state.pause="Запись приостановлена"
AdvSceneSwitcher.condition.record.state.stop="Запись остановлена"
AdvSceneSwitcher.condition.process="Процесс"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.idle="Простой"
AdvSceneSwitcher.condition.idle.entry="Не было ни клавиатуры, ни мыши в течении{{duration}}"
AdvSceneSwitcher.condition.pluginState="Состояние плагина"
@@ -135,7 +134,6 @@ AdvSceneSwitcher.action.replay.type.save="Сохранить буфер восп
AdvSceneSwitcher.action.streaming="Потоковое вещание"
AdvSceneSwitcher.action.streaming.type.stop="Остановить потоковое вещание"
AdvSceneSwitcher.action.streaming.type.start="Начать потоковое вещание"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Запустить"

View File

@@ -78,7 +78,6 @@ AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet"
AdvSceneSwitcher.macroTab.collapseAll="Hepsini Küçült"
; Macro Logic
AdvSceneSwitcher.logic.none="Girişi yoksay"
AdvSceneSwitcher.logic.and="Ve"
AdvSceneSwitcher.logic.or="Ya da"
AdvSceneSwitcher.logic.andNot="ve değil"
@@ -150,7 +149,7 @@ AdvSceneSwitcher.condition.record.state.start="Kayıt Çalışıyor"
AdvSceneSwitcher.condition.record.state.pause="Kayıt durakladı"
AdvSceneSwitcher.condition.record.state.stop="Kayıt durdu"
AdvSceneSwitcher.condition.process="İşlem"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}çalışıyor{{focused}}ve odaklandı"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}çalışıyor{{focused}}ve odaklandı"
AdvSceneSwitcher.condition.idle="Boşta"
AdvSceneSwitcher.condition.idle.entry="...için klavye veya fare girişi yok {{duration}}"
AdvSceneSwitcher.condition.pluginState="Eklenti durumu"
@@ -270,7 +269,6 @@ AdvSceneSwitcher.action.replay.type.save="Tekrar arabelleğini kaydet"
AdvSceneSwitcher.action.streaming="Yayın"
AdvSceneSwitcher.action.streaming.type.stop="Yayın durdur"
AdvSceneSwitcher.action.streaming.type.start="Yayın başlat"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Çalıştır"
AdvSceneSwitcher.action.sceneVisibility="Sahne öğesi görünürlüğü"
AdvSceneSwitcher.action.sceneVisibility.type.show="Göster"
@@ -301,7 +299,7 @@ AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Duraklat"
AdvSceneSwitcher.action.macro.type.unpause="Duraklatma"
AdvSceneSwitcher.action.macro.type.resetCounter="Sayacı sıfırla"
AdvSceneSwitcher.action.macro.type.run="Çalıştır"
AdvSceneSwitcher.action.macro.type.runActions="Çalıştır"
AdvSceneSwitcher.action.pluginState="Eklenti durumu"
AdvSceneSwitcher.action.pluginState.type.stop="Advanced Scene Switcher eklentisini durdurun"
AdvSceneSwitcher.action.pluginState.type.noMatch="Eşleşmeme davranışını değiştirin:"

View File

@@ -259,7 +259,6 @@ AdvSceneSwitcher.macroList.deleted="删除"
AdvSceneSwitcher.macroList.duplicate="\"%1\" 已选择!"
; Macro Logic
AdvSceneSwitcher.logic.none="忽略条件"
AdvSceneSwitcher.logic.and="且"
AdvSceneSwitcher.logic.or="或"
AdvSceneSwitcher.logic.andNot="且不"
@@ -424,8 +423,8 @@ AdvSceneSwitcher.condition.record.state.stop="录制停止"
AdvSceneSwitcher.condition.record.state.duration="录制时间长于"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="进程"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点"
AdvSceneSwitcher.condition.process.entry.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点"
AdvSceneSwitcher.condition.process.layout.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="闲置检测"
AdvSceneSwitcher.condition.idle.entry="{{duration}}内没有键盘或鼠标输入"
AdvSceneSwitcher.condition.pluginState="插件状态"
@@ -825,7 +824,6 @@ AdvSceneSwitcher.action.streaming.type.server="设置服务器"
AdvSceneSwitcher.action.streaming.type.streamKey="设置推流码"
AdvSceneSwitcher.action.streaming.type.username="设置用户名"
AdvSceneSwitcher.action.streaming.type.password="设置密码"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="运行"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}进程退出或最多等待{{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="请注意,如果不勾选此选项,宏属性将不起作用,因为进程的启动会脱离逻辑的其他部分,因此无法对其进行控制."
@@ -900,7 +898,7 @@ AdvSceneSwitcher.action.macro="宏"
AdvSceneSwitcher.action.macro.type.pause="停用"
AdvSceneSwitcher.action.macro.type.unpause="启用"
AdvSceneSwitcher.action.macro.type.resetCounter="重置计数器"
AdvSceneSwitcher.action.macro.type.run="运行宏"
AdvSceneSwitcher.action.macro.type.runActions="运行宏"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="忽略条件结果"
AdvSceneSwitcher.action.macro.type.run.conditions.true="仅当条件结果为真时"
AdvSceneSwitcher.action.macro.type.run.conditions.false="仅当条件结果为假时"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#fefefe" xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<path d="M42.6,17.8c2.4,0,7.2-2,7.2-8.4c0-6.4-4.6-6.8-6.1-6.8c-2.8,0-5.6,2-8.1,6.3c-2.5,4.4-5.3,9.1-5.3,9.1
l-0.1,0c-0.6-3.1-1.1-5.6-1.3-6.7c-0.5-2.7-3.6-8.4-9.9-8.4c-6.4,0-12.2,3.7-12.2,3.7l0,0C5.8,7.3,5.1,8.5,5.1,9.9
c0,2.1,1.7,3.9,3.9,3.9c0.6,0,1.2-0.2,1.7-0.4l0,0c0,0,4.8-2.7,5.9,0c0.3,0.8,0.6,1.7,0.9,2.7c1.2,4.2,2.4,9.1,3.3,13.5l-4.2,6
c0,0-4.7-1.7-7.1-1.7s-7.2,2-7.2,8.4s4.6,6.8,6.1,6.8c2.8,0,5.6-2,8.1-6.3c2.5-4.4,5.3-9.1,5.3-9.1c0.8,4,1.5,7.1,1.9,8.5
c1.6,4.5,5.3,7.2,10.1,7.2c0,0,5,0,10.9-3.3c1.4-0.6,2.4-2,2.4-3.6c0-2.1-1.7-3.9-3.9-3.9c-0.6,0-1.2,0.2-1.7,0.4l0,0
c0,0-4.2,2.4-5.6,0.5c-1-2-1.9-4.6-2.6-7.8c-0.6-2.8-1.3-6.2-2-9.5l4.3-6.2C35.5,16.1,40.2,17.8,42.6,17.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#202020" xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<path d="M42.6,17.8c2.4,0,7.2-2,7.2-8.4c0-6.4-4.6-6.8-6.1-6.8c-2.8,0-5.6,2-8.1,6.3c-2.5,4.4-5.3,9.1-5.3,9.1
l-0.1,0c-0.6-3.1-1.1-5.6-1.3-6.7c-0.5-2.7-3.6-8.4-9.9-8.4c-6.4,0-12.2,3.7-12.2,3.7l0,0C5.8,7.3,5.1,8.5,5.1,9.9
c0,2.1,1.7,3.9,3.9,3.9c0.6,0,1.2-0.2,1.7-0.4l0,0c0,0,4.8-2.7,5.9,0c0.3,0.8,0.6,1.7,0.9,2.7c1.2,4.2,2.4,9.1,3.3,13.5l-4.2,6
c0,0-4.7-1.7-7.1-1.7s-7.2,2-7.2,8.4s4.6,6.8,6.1,6.8c2.8,0,5.6-2,8.1-6.3c2.5-4.4,5.3-9.1,5.3-9.1c0.8,4,1.5,7.1,1.9,8.5
c1.6,4.5,5.3,7.2,10.1,7.2c0,0,5,0,10.9-3.3c1.4-0.6,2.4-2,2.4-3.6c0-2.1-1.7-3.9-3.9-3.9c-0.6,0-1.2,0.2-1.7,0.4l0,0
c0,0-4.2,2.4-5.6,0.5c-1-2-1.9-4.6-2.6-7.8c-0.6-2.8-1.3-6.2-2-9.5l4.3-6.2C35.5,16.1,40.2,17.8,42.6,17.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -322,8 +322,7 @@ void SwitcherData::SetPreconditions()
{
// Window title
lastTitle = currentTitle;
std::string title;
GetCurrentWindowTitle(title);
auto title = GetCurrentWindowTitle();
for (auto &window : ignoreWindowsSwitches) {
bool equals = (title == window);
bool matches = false;
@@ -343,7 +342,7 @@ void SwitcherData::SetPreconditions()
currentTitle = title;
// Process name
GetForegroundProcessName(currentForegroundProcess);
currentForegroundProcess = GetForegroundProcessName();
// Macro
InvalidateMacroTempVarValues();
@@ -507,6 +506,8 @@ bool SwitcherData::AnySceneTransitionStarted()
******************************************************************************/
extern "C" EXPORT void FreeSceneSwitcher()
{
switcher->Stop();
PlatformCleanup();
RunPluginCleanupSteps();
@@ -628,13 +629,20 @@ static void handleSceneCollectionCleanup()
return;
}
// OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP is also called on
// shutdown.
// Here we also don't want to clear the settings.
if (switcher->obsIsShuttingDown) {
return;
}
SaveSceneSwitcher(nullptr, false, nullptr);
}
// Note to future self:
// be careful using switcher->m here as there is potential for deadlocks when using
// frontend functions such as obs_frontend_set_current_scene()
static void OBSEvent(enum obs_frontend_event event, void *switcher)
static void OBSEvent(enum obs_frontend_event event, void *)
{
if (!switcher) {
return;
@@ -768,8 +776,6 @@ void HighlightMacroSettingsButton(bool enable)
enable);
}
void SetupActionQueues();
extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
translateFunc translate)
{
@@ -781,12 +787,11 @@ extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
PlatformInit();
LoadPlugins();
SetupDock();
SetupActionQueues();
RunPluginInitSteps();
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
obs_frontend_add_event_callback(OBSEvent, switcher);
obs_frontend_add_event_callback(OBSEvent, nullptr);
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
obs_module_text("AdvSceneSwitcher.pluginName"));

View File

@@ -468,7 +468,6 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
// Needs to be loaded before any entries which might rely on scene group
// selections to be available.
loadSceneGroups(obj);
LoadVariables(obj);
RunLoadSteps(obj);
@@ -507,7 +506,6 @@ void SwitcherData::SaveSettings(obs_data_t *obj)
saveSceneGroups(obj);
SaveMacros(obj);
SaveGlobalMacroSettings(obj);
SaveVariables(obj);
saveWindowTitleSwitches(obj);
saveScreenRegionSwitches(obj);
savePauseSwitches(obj);

View File

@@ -92,12 +92,11 @@ bool SwitcherData::checkExeSwitch(OBSWeakSource &scene,
}
std::string title = switcher->currentTitle;
QStringList runningProcesses;
bool ignored = false;
bool match = false;
// Check for match
GetProcessList(runningProcesses);
const auto runningProcesses = GetProcessList();
for (ExecutableSwitch &s : executableSwitches) {
if (!s.initialized()) {
continue;

View File

@@ -230,8 +230,7 @@ bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
std::string currentWindowTitle = switcher->currentTitle;
bool match = false;
std::vector<std::string> windowList;
GetWindowList(windowList);
const auto windowList = GetWindowList();
for (WindowSwitch &s : windowSwitches) {
if (!s.initialized()) {

View File

@@ -32,6 +32,9 @@
#endif
#include <fstream>
#include <sstream>
#include <climits>
#include <dirent.h>
#include <unistd.h>
#include "kwin-helpers.h"
namespace advss {
@@ -233,9 +236,9 @@ std::string getWindowName(Window window)
return windowTitle;
}
void GetWindowList(std::vector<std::string> &windows)
std::vector<std::string> GetWindowList()
{
windows.resize(0);
std::vector<std::string> windows;
for (auto window : getTopLevelWindows()) {
auto name = getWindowName(window);
if (name.empty()) {
@@ -243,18 +246,7 @@ void GetWindowList(std::vector<std::string> &windows)
}
windows.emplace_back(name);
}
}
void GetWindowList(QStringList &windows)
{
windows.clear();
for (auto window : getTopLevelWindows()) {
auto name = getWindowName(window);
if (name.empty()) {
continue;
}
windows << QString::fromStdString(name);
}
return windows;
}
int getActiveWindow(Window *&window)
@@ -278,29 +270,24 @@ int getActiveWindow(Window *&window)
&bytes, (uint8_t **)&window);
}
void GetCurrentWindowTitle(std::string &title)
std::string GetCurrentWindowTitle()
{
if (KWin) {
title = FocusNotifier::getActiveWindowTitle();
return;
return FocusNotifier::getActiveWindowTitle();
}
Window *data = 0;
if (getActiveWindow(data) != Success || !data) {
return;
return {};
}
if (!data[0]) {
XFree(data);
return;
return {};
}
auto name = getWindowName(data[0]);
XFree(data);
if (name.empty()) {
return;
}
title = name;
return name;
}
bool windowStatesAreSet(const std::string &windowTitle,
@@ -409,16 +396,17 @@ static void getProcessListProcps2(QStringList &processes)
#endif
}
void GetProcessList(QStringList &processes)
QStringList GetProcessList()
{
processes.clear();
QStringList processes;
if (libprocpsSupported) {
getProcessListProcps(processes);
return;
return processes;
}
if (libprocps2Supported) {
getProcessListProcps2(processes);
}
return processes;
}
long getForegroundProcessPid()
@@ -469,24 +457,83 @@ std::string getProcNameFromPid(long pid)
return name;
}
void GetForegroundProcessName(QString &proc)
std::string GetForegroundProcessName()
{
std::string temp;
GetForegroundProcessName(temp);
proc = QString::fromStdString(temp);
auto pid = getForegroundProcessPid();
return getProcNameFromPid(pid);
}
void GetForegroundProcessName(std::string &proc)
static std::string getProcessPathFromPid(long pid)
{
std::string linkPath = "/proc/" + std::to_string(pid) + "/exe";
char buf[PATH_MAX];
ssize_t len = readlink(linkPath.c_str(), buf, sizeof(buf) - 1);
if (len <= 0) {
return {};
}
buf[len] = '\0';
return buf;
}
std::string GetForegroundProcessPath()
{
proc.resize(0);
auto pid = getForegroundProcessPid();
proc = getProcNameFromPid(pid);
if (pid <= 0) {
return {};
}
return getProcessPathFromPid(pid);
}
QStringList GetProcessPathsFromName(const QString &name)
{
QStringList paths;
const std::string nameStr = name.toStdString();
DIR *procDir = opendir("/proc");
if (!procDir) {
return paths;
}
struct dirent *entry;
while ((entry = readdir(procDir)) != nullptr) {
bool isPid = (entry->d_name[0] != '\0');
for (const char *c = entry->d_name; *c; c++) {
if (!isdigit(*c)) {
isPid = false;
break;
}
}
if (!isPid) {
continue;
}
std::string pid = entry->d_name;
std::string commPath = "/proc/" + pid + "/comm";
std::ifstream commFile(commPath);
if (!commFile) {
continue;
}
std::string comm;
std::getline(commFile, comm);
if (comm != nameStr) {
continue;
}
std::string path = getProcessPathFromPid(std::stol(pid));
if (path.empty()) {
continue;
}
QString qPath = QString::fromStdString(path);
if (!paths.contains(qPath)) {
paths.append(qPath);
}
}
closedir(procDir);
return paths;
}
bool IsInFocus(const QString &executable)
{
std::string current;
GetForegroundProcessName(current);
const auto current = GetForegroundProcessName();
// True if executable switch equals current window
bool equals = (executable.toStdString() == current);

View File

@@ -49,10 +49,12 @@ MacroActionEdit::MacroActionEdit(QWidget *parent,
_section->AddHeaderWidget(_enable);
_section->AddHeaderWidget(_actionSelection);
_section->AddHeaderWidget(_headerInfo);
_section->AddHeaderWidget(_varMappingToggle);
auto actionLayout = new QVBoxLayout;
actionLayout->setContentsMargins({7, 7, 7, 7});
actionLayout->addWidget(_section);
actionLayout->addWidget(_outputMappings);
_contentLayout->addLayout(actionLayout);
auto mainLayout = new QHBoxLayout;
@@ -61,7 +63,6 @@ MacroActionEdit::MacroActionEdit(QWidget *parent,
mainLayout->addWidget(_frame);
setLayout(mainLayout);
_entryData = entryData;
SetupWidgets(true);
actionStateTimer->start(300);
@@ -90,7 +91,10 @@ void MacroActionEdit::SetupWidgets(bool basicSetup)
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
this, SLOT(HeaderInfoChanged(const QString &)));
QWidget::connect(widget, SIGNAL(ShowVariableMappings(bool)), this,
SLOT(ShowVariableMappings(bool)));
_section->SetContent(widget, (*_entryData)->GetCollapsed());
SetupVarMappings((*_entryData).get());
SetFocusPolicyOfWidgets();
_allWidgetsAreSetup = true;
@@ -121,7 +125,10 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text)
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
this, SLOT(HeaderInfoChanged(const QString &)));
QWidget::connect(widget, SIGNAL(ShowVariableMappings(bool)), this,
SLOT(ShowVariableMappings(bool)));
_section->SetContent(widget);
SetupVarMappings((*_entryData).get());
SetFocusPolicyOfWidgets();
}

View File

@@ -120,6 +120,15 @@ bool MacroActionMacro::PerformAction()
case Action::TOGGLE_ACTION:
AdjustActionState(macro);
break;
case Action::RUN_MACRO: {
if (_runOptions.skipWhenPaused && macro->Paused()) {
break;
}
const bool conditionsMatched =
macro->CheckConditions(true);
macro->PerformActions(conditionsMatched, false, true);
break;
}
case Action::GET_INFO: {
SetTempVarValue(
"conditionCount",
@@ -210,6 +219,9 @@ void MacroActionMacro::LogAction() const
case Action::GET_INFO:
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
break;
case Action::RUN_MACRO:
ablog(LOG_INFO, "run macro \"%s\"", macro->Name().c_str());
break;
default:
break;
}
@@ -403,7 +415,9 @@ static void populateActionSelection(QComboBox *list)
{MacroActionMacro::Action::NESTED_MACRO,
"AdvSceneSwitcher.action.macro.type.nestedMacro"},
{MacroActionMacro::Action::RUN_ACTIONS,
"AdvSceneSwitcher.action.macro.type.run"},
"AdvSceneSwitcher.action.macro.type.runActions"},
{MacroActionMacro::Action::RUN_MACRO,
"AdvSceneSwitcher.action.macro.type.runMacro"},
{MacroActionMacro::Action::STOP,
"AdvSceneSwitcher.action.macro.type.stop"},
{MacroActionMacro::Action::DISABLE_ACTION,
@@ -483,6 +497,10 @@ MacroActionMacroEdit::MacroActionMacroEdit(
_actionSections(new QComboBox(this)),
_skipWhenPaused(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.skipWhenPaused"))),
_noConditionsWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.macro.type.runMacro.noConditionsWarning"))),
_runMacroHelp(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.action.macro.type.runMacro.help"))),
_setInputs(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.setInputs"))),
_inputs(new MacroInputEdit()),
@@ -556,6 +574,7 @@ MacroActionMacroEdit::MacroActionMacroEdit(
layout->addLayout(_setInputsLayout);
layout->addWidget(_inputs);
layout->addWidget(_skipWhenPaused);
layout->addWidget(_noConditionsWarning);
layout->addWidget(_nestedMacro);
setLayout(layout);
_entryData = entryData;
@@ -725,6 +744,7 @@ void MacroActionMacroEdit::InputsChanged(const StringList &inputs)
void MacroActionMacroEdit::SetWidgetVisibility()
{
_entryLayout->removeWidget(_actions);
_entryLayout->removeWidget(_runMacroHelp);
_entryLayout->removeWidget(_actionIndex);
_entryLayout->removeWidget(_macros);
_entryLayout->removeWidget(_actionSections);
@@ -740,6 +760,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
const std::unordered_map<std::string, QWidget *> placeholders = {
{"{{actions}}", _actions},
{"{{runMacroHelp}}", _runMacroHelp},
{"{{actionIndex}}", _actionIndex},
{"{{macros}}", _macros},
{"{{actionSections}}", _actionSections},
@@ -764,6 +785,9 @@ void MacroActionMacroEdit::SetWidgetVisibility()
case MacroActionMacro::Action::GET_INFO:
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
break;
case MacroActionMacro::Action::RUN_MACRO:
layoutText = "AdvSceneSwitcher.action.macro.layout.runMacro";
break;
case MacroActionMacro::Action::RUN_ACTIONS:
layoutText = "AdvSceneSwitcher.action.macro.layout.run";
break;
@@ -790,7 +814,8 @@ void MacroActionMacroEdit::SetWidgetVisibility()
}
if (action == MacroActionMacro::Action::RUN_ACTIONS ||
action == MacroActionMacro::Action::STOP) {
action == MacroActionMacro::Action::STOP ||
action == MacroActionMacro::Action::RUN_MACRO) {
_macros->HideSelectedMacro();
} else {
_macros->ShowAllMacros();
@@ -835,8 +860,19 @@ void MacroActionMacroEdit::SetWidgetVisibility()
_actionSections->setVisible(
action == MacroActionMacro::Action::RUN_ACTIONS ||
isModifyingActionState);
_skipWhenPaused->setVisible(action ==
MacroActionMacro::Action::RUN_ACTIONS);
_skipWhenPaused->setVisible(
action == MacroActionMacro::Action::RUN_ACTIONS ||
action == MacroActionMacro::Action::RUN_MACRO);
if (action == MacroActionMacro::Action::RUN_MACRO) {
auto macro = _entryData->_macro.GetMacro();
_noConditionsWarning->setVisible(!macro ||
macro->Conditions().empty());
} else {
_noConditionsWarning->setVisible(false);
}
_runMacroHelp->setVisible(action ==
MacroActionMacro::Action::RUN_MACRO);
_nestedMacro->setVisible(action ==
MacroActionMacro::Action::NESTED_MACRO);

View File

@@ -11,6 +11,7 @@
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
namespace advss {
@@ -57,6 +58,7 @@ public:
TOGGLE_PAUSE,
NESTED_MACRO,
GET_INFO,
RUN_MACRO,
};
void SetAction(Action);
@@ -131,6 +133,8 @@ private:
QCheckBox *_reevaluateConditionState;
QComboBox *_actionSections;
QCheckBox *_skipWhenPaused;
QLabel *_noConditionsWarning;
HelpIcon *_runMacroHelp;
QCheckBox *_setInputs;
MacroInputEdit *_inputs;
QHBoxLayout *_entryLayout;

View File

@@ -19,10 +19,11 @@ const std::string MacroActionVariable::id = "variable";
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
auto refs = MacroSegment::GetTempVarRefs();
if (_tempVar.HasValidID()) {
refs.push_back(_tempVar);
}
return {_tempVar};
return refs;
}
bool MacroActionVariable::_registered = MacroActionFactory::Register(

View File

@@ -5,6 +5,7 @@
#include "path-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "section.hpp"
#include "switch-button.hpp"
#include "ui-helpers.hpp"
#include "utility.hpp"
@@ -102,12 +103,15 @@ MacroConditionEdit::MacroConditionEdit(
QWidget *parent, std::shared_ptr<MacroCondition> *entryData,
bool isRootCondition)
: MacroSegmentEdit(parent),
_enable(new SwitchButton()),
_logicSelection(new QComboBox()),
_conditionSelection(new FilterComboBox()),
_dur(new DurationModifierEdit()),
_entryData(entryData),
_isRoot(isRootCondition)
{
QWidget::connect(_enable, SIGNAL(checked(bool)), this,
SLOT(ConditionEnableChanged(bool)));
QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)),
this, SLOT(LogicSelectionChanged(int)));
QWidget::connect(_conditionSelection,
@@ -122,14 +126,17 @@ MacroConditionEdit::MacroConditionEdit(
Logic::PopulateLogicTypeSelection(_logicSelection, isRootCondition);
populateConditionSelection(_conditionSelection);
_section->AddHeaderWidget(_enable);
_section->AddHeaderWidget(_logicSelection);
_section->AddHeaderWidget(_conditionSelection);
_section->AddHeaderWidget(_headerInfo);
_section->AddHeaderWidget(_dur);
_section->AddHeaderWidget(_varMappingToggle);
QVBoxLayout *conditionLayout = new QVBoxLayout;
conditionLayout->setContentsMargins({7, 7, 7, 7});
conditionLayout->addWidget(_section);
conditionLayout->addWidget(_outputMappings);
_contentLayout->addLayout(conditionLayout);
QHBoxLayout *mainLayout = new QHBoxLayout;
@@ -152,8 +159,17 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
const auto logic = static_cast<Logic::Type>(
_logicSelection->itemData(idx).toInt());
(*_entryData)->SetLogicType(logic);
}
SetEnableAppearance(logic != Logic::Type::NONE);
void MacroConditionEdit::ConditionEnableChanged(bool value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
(*_entryData)->SetEnabled(value);
SetDisableEffect(!value);
}
bool MacroConditionEdit::IsRootNode() const
@@ -166,7 +182,9 @@ void MacroConditionEdit::SetLogicSelection()
const auto logic = (*_entryData)->GetLogicType();
_logicSelection->setCurrentIndex(
_logicSelection->findData(static_cast<int>(logic)));
SetEnableAppearance(logic != Logic::Type::NONE);
const bool enabled = (*_entryData)->Enabled();
_enable->setChecked(enabled);
SetEnableAppearance(enabled);
}
void MacroConditionEdit::SetRootNode(bool root)
@@ -204,7 +222,10 @@ void MacroConditionEdit::SetupWidgets(bool basicSetup)
MacroConditionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
this, SLOT(HeaderInfoChanged(const QString &)));
QWidget::connect(widget, SIGNAL(ShowVariableMappings(bool)), this,
SLOT(ShowVariableMappings(bool)));
_section->SetContent(widget, (*_entryData)->GetCollapsed());
SetupVarMappings((*_entryData).get());
SetFocusPolicyOfWidgets();
_allWidgetsAreSetup = true;
@@ -234,10 +255,12 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
{
auto lock = LockContext();
auto logic = (*_entryData)->GetLogicType();
const bool enabled = (*_entryData)->Enabled();
_entryData->reset();
*_entryData = MacroConditionFactory::Create(id, macro);
(*_entryData)->SetIndex(idx);
(*_entryData)->SetLogicType(logic);
(*_entryData)->SetEnabled(enabled);
(*_entryData)->PostLoad();
RunAndClearPostLoadSteps();
}
@@ -245,7 +268,10 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
MacroConditionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
this, SLOT(HeaderInfoChanged(const QString &)));
QWidget::connect(widget, SIGNAL(ShowVariableMappings(bool)), this,
SLOT(ShowVariableMappings(bool)));
_section->SetContent(widget);
SetupVarMappings((*_entryData).get());
_dur->setVisible(MacroConditionFactory::UsesDurationModifier(id));
SetFocusPolicyOfWidgets();
}

View File

@@ -8,6 +8,8 @@
namespace advss {
class SwitchButton;
class DurationModifierEdit : public QWidget {
Q_OBJECT
public:
@@ -47,11 +49,13 @@ private slots:
void ConditionSelectionChanged(const QString &text);
void DurationChanged(const Duration &value);
void DurationModifierChanged(DurationModifier::Type m);
void ConditionEnableChanged(bool);
private:
void SetLogicSelection();
std::shared_ptr<MacroSegment> Data() const;
SwitchButton *_enable;
QComboBox *_logicSelection;
FilterComboBox *_conditionSelection;
DurationModifierEdit *_dur;

View File

@@ -8,10 +8,11 @@ const std::string MacroConditionTempVar::id = "temp_var";
std::vector<TempVariableRef> MacroConditionTempVar::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
auto refs = MacroSegment::GetTempVarRefs();
if (_tempVar.HasValidID()) {
refs.push_back(_tempVar);
}
return {_tempVar};
return refs;
}
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(

View File

@@ -31,6 +31,10 @@ bool MacroCondition::Load(obs_data_t *obj)
{
MacroSegment::Load(obj);
_logic.Load(obj, "logic");
if (_logic.GetType() == Logic::Type::NONE) {
SetEnabled(false);
_logic.SetType(Logic::Type::AND);
}
_durationModifier.Load(obj);
return true;
}
@@ -50,7 +54,8 @@ void MacroCondition::ValidateLogicSelection(bool isRootCondition,
return;
}
_logic.SetType(Logic::Type::NONE);
_logic.SetType(Logic::Type::AND);
SetEnabled(false);
blog(LOG_WARNING,
"setting invalid logic selection to 'ignore' for macro %s",
context);

View File

@@ -1,8 +1,10 @@
#include "macro-segment.hpp"
#include "macro.hpp"
#include "mouse-wheel-guard.hpp"
#include "path-helpers.hpp"
#include "section.hpp"
#include "ui-helpers.hpp"
#include "variable.hpp"
#include <QApplication>
#include <QEvent>
@@ -15,7 +17,48 @@ namespace advss {
std::vector<TempVariableRef> MacroSegment::GetTempVarRefs() const
{
return {};
std::vector<TempVariableRef> refs;
for (const auto &mapping : _varMappings) {
if (mapping.tempVar.HasValidID()) {
refs.push_back(mapping.tempVar);
}
}
return refs;
}
void MacroSegment::ApplyVarMappings()
{
auto macro = GetMacro();
for (const auto &mapping : _varMappings) {
auto var = mapping.variable.lock();
if (!var) {
continue;
}
const auto tempVar = mapping.tempVar.GetTempVariable(macro);
if (!tempVar) {
continue;
}
const auto value = tempVar->Value();
if (!value) {
continue;
}
var->SetValue(*value);
}
}
const std::vector<VarMapping> &MacroSegment::GetVarMappings() const
{
return _varMappings;
}
void MacroSegment::SetVarMappings(const std::vector<VarMapping> &mappings)
{
_varMappings = mappings;
}
std::vector<TempVariable> MacroSegment::GetOwnTempVars() const
{
return _tempVariables;
}
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
@@ -24,14 +67,32 @@ MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
{
}
void MacroSegment::SetVarMappingExpanded(bool expanded)
{
_varMappingExpanded = expanded;
}
bool MacroSegment::Save(obs_data_t *obj) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "collapsed", _collapsed);
obs_data_set_bool(data, "varMappingExpanded", _varMappingExpanded);
obs_data_set_bool(data, "useCustomLabel", _useCustomLabel);
obs_data_set_string(data, "customLabel", _customLabel.c_str());
obs_data_set_bool(data, "enabled", _enabled);
obs_data_set_int(data, "version", 1);
obs_data_set_int(data, "version", 2);
OBSDataArrayAutoRelease mappingsArray = obs_data_array_create();
for (const auto &mapping : _varMappings) {
OBSDataAutoRelease item = obs_data_create();
mapping.tempVar.Save(item, GetMacro(), "tempVar");
obs_data_set_string(
item, "variable",
GetWeakVariableName(mapping.variable).c_str());
obs_data_array_push_back(mappingsArray, item);
}
obs_data_set_array(data, "varMappings", mappingsArray);
obs_data_set_obj(obj, "segmentSettings", data);
return true;
}
@@ -40,6 +101,7 @@ bool MacroSegment::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "segmentSettings");
_collapsed = obs_data_get_bool(data, "collapsed");
_varMappingExpanded = obs_data_get_bool(data, "varMappingExpanded");
_useCustomLabel = obs_data_get_bool(data, "useCustomLabel");
_customLabel = obs_data_get_string(data, "customLabel");
obs_data_set_default_bool(data, "enabled", true);
@@ -50,6 +112,26 @@ bool MacroSegment::Load(obs_data_t *obj)
_enabled = obs_data_get_bool(obj, "enabled");
}
// Reset the previously unused "enabled" value for conditions to "true"
if (obs_data_get_int(data, "version") < 2 &&
obs_data_has_user_value(obj, "logic")) {
_enabled = true;
}
_varMappings.clear();
OBSDataArrayAutoRelease mappingsArray =
obs_data_get_array(data, "varMappings");
const size_t count = obs_data_array_count(mappingsArray);
_varMappings.reserve(count);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease item = obs_data_array_item(mappingsArray, i);
VarMapping mapping;
mapping.variable = GetWeakVariableByName(
obs_data_get_string(item, "variable"));
_varMappings.push_back(std::move(mapping));
_varMappings.back().tempVar.Load(item, GetMacro(), "tempVar");
}
ClearAvailableTempvars();
return true;
}
@@ -226,11 +308,38 @@ MacroSegmentEdit::MacroSegmentEdit(QWidget *parent)
_headerInfo(new QLabel()),
_frame(new QWidget),
_contentLayout(new QVBoxLayout),
_outputMappings(new TempVarOutputMappingsWidget(this)),
_varMappingToggle(new QPushButton(this)),
_noBorderframe(new QFrame),
_borderFrame(new QFrame),
_dropLineAbove(new QFrame),
_dropLineBelow(new QFrame)
{
const auto iconPath = QString::fromStdString(GetDataFilePath(
"res/images/" + GetThemeTypeName() + "Variable.svg"));
_varMappingToggle->setIcon(QIcon(iconPath));
_varMappingToggle->setMaximumWidth(22);
_varMappingToggle->setFlat(true);
_varMappingToggle->setCheckable(true);
_varMappingToggle->setVisible(false);
_varMappingToggle->setToolTip(obs_module_text(
"AdvSceneSwitcher.tempVar.outputMappings.toggle"));
QWidget::connect(_varMappingToggle, &QPushButton::toggled,
_outputMappings,
&TempVarOutputMappingsWidget::SetPanelExpanded);
QWidget::connect(_varMappingToggle, &QPushButton::toggled, this,
[this](bool checked) {
if (Data()) {
Data()->SetVarMappingExpanded(checked);
}
});
QWidget::connect(_outputMappings,
&TempVarOutputMappingsWidget::ExpandableChanged,
_varMappingToggle, &QPushButton::setVisible);
QWidget::connect(_section, &Section::Collapsed, _outputMappings,
&TempVarOutputMappingsWidget::SetSectionCollapsed);
_dropLineAbove->setLineWidth(3);
_dropLineAbove->setFixedHeight(11);
_dropLineBelow->setLineWidth(3);
@@ -292,6 +401,21 @@ MacroSegmentEdit::MacroSegmentEdit(QWidget *parent)
_headerInfo->installEventFilter(this);
}
void MacroSegmentEdit::SetupVarMappings(MacroSegment *segment)
{
_outputMappings->SetSegment(segment);
_varMappingToggle->setChecked(segment &&
segment->GetVarMappingExpanded());
}
void MacroSegmentEdit::ShowVariableMappings(bool show)
{
_varMappingToggle->setChecked(show);
if (Data()) {
Data()->SetVarMappingExpanded(show);
}
}
bool MacroSegmentEdit::eventFilter(QObject *obj, QEvent *ev)
{
if (obj == _headerInfo && ev->type() == QEvent::MouseMove) {

View File

@@ -9,15 +9,23 @@
#include <QWidget>
#include <QFrame>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTimer>
#include <obs-data.h>
#include <memory>
class QLabel;
namespace advss {
class Macro;
class Variable;
struct VarMapping {
TempVariableRef tempVar;
std::weak_ptr<Variable> variable;
};
class EXPORT MacroSegment : public Lockable {
public:
@@ -28,6 +36,8 @@ public:
int GetIndex() const { return _idx; }
void SetCollapsed(bool collapsed) { _collapsed = collapsed; }
bool GetCollapsed() const { return _collapsed; }
void SetVarMappingExpanded(bool expanded);
bool GetVarMappingExpanded() const { return _varMappingExpanded; }
void SetUseCustomLabel(bool enable) { _useCustomLabel = enable; }
bool GetUseCustomLabel() const { return _useCustomLabel; }
void SetCustomLabel(const std::string &label) { _customLabel = label; }
@@ -43,6 +53,10 @@ public:
void SetEnabled(bool);
bool Enabled() const;
virtual std::string GetVariableValue() const;
void ApplyVarMappings();
const std::vector<VarMapping> &GetVarMappings() const;
void SetVarMappings(const std::vector<VarMapping> &mappings);
std::vector<TempVariable> GetOwnTempVars() const;
protected:
friend bool SupportsVariableValue(MacroSegment *);
@@ -87,6 +101,7 @@ private:
// UI helper
bool _highlight = false;
bool _collapsed = false;
bool _varMappingExpanded = false;
bool _enabled = true;
// Custom header labels
@@ -99,6 +114,7 @@ private:
int _variableRefs = 0;
std::string _variableValue;
std::vector<TempVariable> _tempVariables;
std::vector<VarMapping> _varMappings;
friend class Macro;
};
@@ -118,8 +134,11 @@ public:
virtual std::shared_ptr<MacroSegment> Data() const = 0;
virtual void SetupWidgets(bool basicSetup = false) = 0;
void SetupVarMappings(MacroSegment *segment);
public slots:
void HeaderInfoChanged(const QString &);
void ShowVariableMappings(bool show);
protected slots:
void Collapsed(bool) const;
@@ -134,6 +153,8 @@ protected:
QLabel *_headerInfo;
QWidget *_frame;
QVBoxLayout *_contentLayout;
TempVarOutputMappingsWidget *_outputMappings;
QPushButton *_varMappingToggle;
bool _allWidgetsAreSetup = false;
private:

View File

@@ -557,6 +557,10 @@ void AdvSceneSwitcher::HighlightOnChange() const
return;
}
if (macro->Paused()) {
return;
}
if (macro->ActionTriggerModePreventedActionsSince(
lastOnChangeHighlightCheckTime)) {
HighlightWidget(ui->actionTriggerMode, Qt::yellow,

View File

@@ -17,6 +17,17 @@
namespace advss {
static bool setup()
{
AddPluginCleanupStep([]() {
GetTopLevelMacros().clear();
GetTemporaryMacros().clear();
});
return true;
}
static bool setupDone = setup();
Macro::Macro(const std::string &name) : _dockSettings(this)
{
SetName(name);
@@ -113,6 +124,7 @@ static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
condition->WithLock([&condition, &conditionMatched]() {
conditionMatched = condition->EvaluateCondition();
});
condition->ApplyVarMappings();
const auto endTime = std::chrono::high_resolution_clock::now();
const auto timeSpent = endTime - startTime;
@@ -145,6 +157,15 @@ bool Macro::CheckConditionHelper(
return conditionMatched;
};
if (!condition->Enabled()) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
condition->GetId().c_str(), _name.c_str());
if (!_useShortCircuitEvaluation) {
(void)evaluateCondition();
}
return _matched;
}
const auto logicType = condition->GetLogicType();
if (logicType == Logic::Type::NONE) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
@@ -269,7 +290,7 @@ bool Macro::CheckConditions(bool ignorePause)
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
: (_elseActions.size() > 0);
if (!_actionModeMatch && hasActionsToExecute) {
if (!_actionModeMatch && hasActionsToExecute && !_paused) {
_lastActionRunModePreventTime =
std::chrono::high_resolution_clock::now();
}
@@ -368,7 +389,7 @@ bool Macro::ShouldRunActions() const
!_paused && (_matched || _elseActions.size() > 0) &&
_actionModeMatch;
if (VerboseLoggingEnabled() && !_actionModeMatch) {
if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
if (_matched && _actions.size() > 0) {
blog(LOG_INFO, "skip actions for Macro %s (on change)",
_name.c_str());
@@ -432,6 +453,7 @@ bool Macro::RunActionsHelper(
action->WithLock([&action, &actionResult]() {
actionResult = action->PerformAction();
});
action->ApplyVarMappings();
actionsExecutedSuccessfully =
actionsExecutedSuccessfully && actionResult;
} else {

View File

@@ -14,10 +14,9 @@
namespace advss {
void GetWindowList(std::vector<std::string> &windows)
std::vector<std::string> GetWindowList()
{
windows.resize(0);
std::vector<std::string> windows;
@autoreleasepool {
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
@@ -49,21 +48,12 @@ void GetWindowList(std::vector<std::string> &windows)
apps = nil;
CFRelease(cfApps);
}
return windows;
}
void GetWindowList(QStringList &windows)
std::string GetCurrentWindowTitle()
{
windows.clear();
std::vector<std::string> temp;
GetWindowList(temp);
for (auto &w : temp) {
windows << QString::fromStdString(w);
}
}
void GetCurrentWindowTitle(std::string &title)
{
title.resize(0);
std::string title;
@autoreleasepool {
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
@@ -107,6 +97,7 @@ void GetCurrentWindowTitle(std::string &title)
apps = nil;
CFRelease(cfApps);
}
return title;
}
bool isWindowOriginOnScreen(NSDictionary *app, NSScreen *screen,
@@ -273,9 +264,9 @@ int SecondsSinceLastInput()
return (int)time;
}
void GetProcessList(QStringList &list)
QStringList GetProcessList()
{
list.clear();
QStringList list;
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
NSArray *array = [ws runningApplications];
@@ -291,11 +282,11 @@ void GetProcessList(QStringList &list)
}
}
}
return list;
}
void GetForegroundProcessName(std::string &proc)
std::string GetForegroundProcessName()
{
proc.resize(0);
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
NSArray *array = [ws runningApplications];
@@ -308,23 +299,73 @@ void GetForegroundProcessName(std::string &proc)
break;
}
const char *str = name.UTF8String;
proc = std::string(str);
if (str) {
return str;
}
break;
}
}
return {};
}
void GetForegroundProcessName(QString &proc)
std::string GetForegroundProcessPath()
{
std::string temp;
GetForegroundProcessName(temp);
proc = QString::fromStdString(temp);
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
for (NSRunningApplication *app in [ws runningApplications]) {
if (!app.isActive) {
continue;
}
NSURL *url = app.executableURL;
if (!url) {
break;
}
const char *str = url.path.UTF8String;
if (str) {
return str;
}
break;
}
}
return {};
}
QStringList GetProcessPathsFromName(const QString &name)
{
QStringList paths;
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
for (NSRunningApplication *app in [ws runningApplications]) {
NSString *appName = app.localizedName;
if (!appName) {
continue;
}
const char *nameStr = appName.UTF8String;
if (!nameStr ||
name != QString::fromUtf8(nameStr)) {
continue;
}
NSURL *url = app.executableURL;
if (!url) {
continue;
}
const char *pathStr = url.path.UTF8String;
if (!pathStr) {
continue;
}
QString path = QString::fromUtf8(pathStr);
if (!paths.contains(path)) {
paths.append(path);
}
}
}
return paths;
}
bool IsInFocus(const QString &executable)
{
std::string current;
GetForegroundProcessName(current);
const auto current = GetForegroundProcessName();
// True if executable switch equals current window
bool equals = (executable.toStdString() == current);

View File

@@ -11,15 +11,16 @@ namespace advss {
enum class HotkeyType;
EXPORT void GetWindowList(std::vector<std::string> &windows);
EXPORT void GetWindowList(QStringList &windows);
EXPORT void GetCurrentWindowTitle(std::string &title);
EXPORT std::vector<std::string> GetWindowList();
EXPORT std::string GetCurrentWindowTitle();
EXPORT bool IsFullscreen(const std::string &title);
EXPORT bool IsMaximized(const std::string &title);
EXPORT std::optional<std::string> GetTextInWindow(const std::string &window);
EXPORT int SecondsSinceLastInput();
EXPORT void GetProcessList(QStringList &processes);
EXPORT void GetForegroundProcessName(std::string &name);
EXPORT QStringList GetProcessList();
EXPORT std::string GetForegroundProcessName();
EXPORT std::string GetForegroundProcessPath();
EXPORT QStringList GetProcessPathsFromName(const QString &name);
EXPORT bool IsInFocus(const QString &executable);
void PlatformInit();
void PlatformCleanup();

View File

@@ -16,14 +16,17 @@ static void setupTab(QTabWidget *);
static ActionQueueTable *tabWidget = nullptr;
void RegisterActionQueueTab()
static bool setup()
{
AddPluginInitStep([]() {
AddPluginInitStep([] {
AddSetupTabCallback("actionQueueTab", ActionQueueTable::Create,
setupTab);
});
return true;
}
static bool setupDone = setup();
static void setTabVisible(QTabWidget *tabWidget, bool visible)
{
SetTabVisibleByName(

View File

@@ -12,20 +12,16 @@ std::deque<std::shared_ptr<Item>> &GetActionQueues()
return queues;
}
void RegisterActionQueueTab();
void SetupActionQueues()
static bool setup()
{
static bool done = false;
if (done) {
return;
}
AddSaveStep(SaveActionQueues);
AddLoadStep(LoadActionQueues);
RegisterActionQueueTab();
done = true;
AddPluginCleanupStep([]() { queues.clear(); });
return true;
}
static bool setupDone = setup();
ActionQueue::ActionQueue() : Item()
{
_lastEmpty = std::chrono::high_resolution_clock::now();

View File

@@ -101,7 +101,6 @@ signals:
};
std::deque<std::shared_ptr<Item>> &GetActionQueues();
void SetupActionQueues();
void SaveActionQueues(obs_data_t *);
void LoadActionQueues(obs_data_t *);
void ImportQueues(obs_data_t *);

View File

@@ -8,7 +8,6 @@
namespace advss {
const std::map<Logic::Type, const char *> Logic::localeMap = {
{Logic::Type::NONE, {"AdvSceneSwitcher.logic.none"}},
{Logic::Type::AND, {"AdvSceneSwitcher.logic.and"}},
{Logic::Type::OR, {"AdvSceneSwitcher.logic.or"}},
{Logic::Type::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
@@ -67,7 +66,7 @@ void Logic::PopulateLogicTypeSelection(QComboBox *list, bool isRootCondition)
return typeValue < rootOffset;
}}
: std::function<bool(int)>{[](int typeValue) {
return typeValue >= rootOffset;
return typeValue > rootOffset;
}};
for (const auto &[type, name] : localeMap) {
const int typeValue = static_cast<int>(type);

View File

@@ -28,6 +28,8 @@ static constexpr bool handleUncleanShutdown = true;
static bool wasCleanShutdown = false;
static bool suppressCrashDialog = false;
static char *sentinelFile = nullptr;
bool GetSuppressCrashDialog()
{
return suppressCrashDialog;
@@ -58,7 +60,6 @@ static void handleShutdown(enum obs_frontend_event event, void *)
return;
}
char *sentinelFile = obs_module_config_path(sentinel.data());
if (!sentinelFile) {
return;
}
@@ -78,7 +79,8 @@ static void handleShutdown(enum obs_frontend_event event, void *)
static void setup()
{
char *sentinelFile = obs_module_config_path(sentinel.data());
// Freed in handleShutdown()
sentinelFile = obs_module_config_path(sentinel.data());
if (!sentinelFile) {
return;
}
@@ -106,7 +108,6 @@ static void setup()
file.write("running");
file.close();
bfree(sentinelFile);
obs_frontend_add_event_callback(handleShutdown, nullptr);
return;

View File

@@ -55,9 +55,7 @@ static void WriteFirstRun(bool value)
static QString DetectFocusedWindow()
{
std::string title;
GetCurrentWindowTitle(title);
return QString::fromStdString(title);
return QString::fromStdString(GetCurrentWindowTitle());
}
// ===========================================================================

View File

@@ -75,6 +75,18 @@ static std::vector<std::function<void()>> &getStopSteps()
return steps;
}
static std::vector<std::function<void(obs_data_t *)>> &getEarlySaveSteps()
{
static std::vector<std::function<void(obs_data_t *)>> steps;
return steps;
}
static std::vector<std::function<void(obs_data_t *)>> &getEarlyLoadSteps()
{
static std::vector<std::function<void(obs_data_t *)>> steps;
return steps;
}
static std::vector<std::function<void(obs_data_t *)>> &getSaveSteps()
{
static std::vector<std::function<void(obs_data_t *)>> steps;
@@ -109,6 +121,18 @@ void LoadPluginSettings(obs_data_t *obj)
GetSwitcher()->LoadSettings(obj);
}
void AddEarlySaveStep(std::function<void(obs_data_t *)> step)
{
std::lock_guard<std::mutex> lock(mutex);
getEarlySaveSteps().emplace_back(step);
}
void AddEarlyLoadStep(std::function<void(obs_data_t *)> step)
{
std::lock_guard<std::mutex> lock(mutex);
getEarlyLoadSteps().emplace_back(step);
}
void AddSaveStep(std::function<void(obs_data_t *)> step)
{
std::lock_guard<std::mutex> lock(mutex);
@@ -136,6 +160,9 @@ void AddIntervalResetStep(std::function<void()> step)
void RunSaveSteps(obs_data_t *obj)
{
std::lock_guard<std::mutex> lock(mutex);
for (const auto &func : getEarlySaveSteps()) {
func(obj);
}
for (const auto &func : getSaveSteps()) {
func(obj);
}
@@ -144,6 +171,9 @@ void RunSaveSteps(obs_data_t *obj)
void RunLoadSteps(obs_data_t *obj)
{
std::lock_guard<std::mutex> lock(mutex);
for (const auto &func : getEarlyLoadSteps()) {
func(obj);
}
for (const auto &func : getLoadSteps()) {
func(obj);
}
@@ -310,7 +340,7 @@ bool HighlightUIElementsEnabled()
bool OBSIsShuttingDown()
{
return GetSwitcher() && GetSwitcher()->obsIsShuttingDown;
return !GetSwitcher() || GetSwitcher()->obsIsShuttingDown;
}
bool InitialLoadIsComplete()

View File

@@ -8,6 +8,8 @@ namespace advss {
void SavePluginSettings(obs_data_t *);
EXPORT void LoadPluginSettings(obs_data_t *);
void AddEarlySaveStep(std::function<void(obs_data_t *)>);
void AddEarlyLoadStep(std::function<void(obs_data_t *)>);
EXPORT void AddSaveStep(std::function<void(obs_data_t *)>);
EXPORT void AddLoadStep(std::function<void(obs_data_t *)>);
EXPORT void AddPostLoadStep(std::function<void()>);

View File

@@ -173,10 +173,9 @@ void PopulateTransitionSelection(QComboBox *sel, bool addCurrent, bool addAny,
void PopulateWindowSelection(QComboBox *sel, bool addSelect)
{
std::vector<std::string> windows;
GetWindowList(windows);
const auto windows = GetWindowList();
for (std::string &window : windows) {
for (const std::string &window : windows) {
sel->addItem(window.c_str());
}
@@ -257,8 +256,7 @@ void PopulateMediaSelection(QComboBox *sel, bool addSelect)
void PopulateProcessSelection(QComboBox *sel, bool addSelect)
{
QStringList processes;
GetProcessList(processes);
auto processes = GetProcessList();
processes.sort();
for (QString &process : processes) {
sel->addItem(process);

View File

@@ -5,11 +5,20 @@
#include "macro-edit.hpp"
#include "macro-segment.hpp"
#include "plugin-state-helpers.hpp"
#include "sync-helpers.hpp"
#include "ui-helpers.hpp"
#include "utility.hpp"
#include "variable.hpp"
#include <QVariant>
#include <QAbstractItemView>
#include <QHBoxLayout>
#include <QLabel>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QToolButton>
#include <QVariant>
#include <QVBoxLayout>
#include <atomic>
Q_DECLARE_METATYPE(advss::TempVariableRef);
@@ -731,4 +740,293 @@ void NotifyUIAboutTempVarChange(MacroSegment *segment)
segment);
}
TempVarOutputMappingsWidget::TempVarOutputMappingsWidget(QWidget *parent)
: QWidget(parent),
_rowsLayout(new QVBoxLayout()),
_addButton(new QPushButton(
obs_module_text("AdvSceneSwitcher.tempVar.outputMappings.add"),
this)),
_animation(new QPropertyAnimation(this, "maximumHeight", this))
{
_rowsLayout->setContentsMargins(0, 0, 0, 0);
_rowsLayout->setSpacing(2);
auto rowsContainer = new QWidget(this);
rowsContainer->setLayout(_rowsLayout);
auto label = new QLabel(
obs_module_text("AdvSceneSwitcher.tempVar.outputMappings"),
this);
auto mainLayout = new QVBoxLayout();
mainLayout->addWidget(label);
mainLayout->addWidget(rowsContainer);
mainLayout->addWidget(_addButton);
setLayout(mainLayout);
_animation->setDuration(300);
_animation->setEasingCurve(QEasingCurve::InOutQuad);
connect(_animation, &QPropertyAnimation::finished, this,
&TempVarOutputMappingsWidget::AnimationFinished);
hide();
connect(_addButton, &QPushButton::clicked, this,
&TempVarOutputMappingsWidget::Add);
connect(TempVarSignalManager::Instance(),
SIGNAL(SegmentTempVarsChanged(MacroSegment *)), this,
SLOT(SegmentTempVarsChanged(MacroSegment *)));
}
void TempVarOutputMappingsWidget::SetSegment(MacroSegment *segment)
{
_segment = segment;
_panelExpanded = false;
Rebuild();
}
void TempVarOutputMappingsWidget::Add()
{
if (!_segment) {
return;
}
auto mappings = _segment->GetVarMappings();
mappings.push_back(VarMapping{});
{
auto lock = LockContext();
_segment->SetVarMappings(mappings);
}
Rebuild();
}
void TempVarOutputMappingsWidget::Remove(int rowIdx)
{
if (!_segment) {
return;
}
auto mappings = _segment->GetVarMappings();
if (rowIdx < 0 || rowIdx >= (int)mappings.size()) {
return;
}
mappings.erase(mappings.begin() + rowIdx);
{
auto lock = LockContext();
_segment->SetVarMappings(mappings);
}
Rebuild();
}
void TempVarOutputMappingsWidget::SegmentTempVarsChanged(MacroSegment *segment)
{
if (segment != _segment) {
return;
}
// Re-populate each temp var combobox in case temp vars were added/removed
_loading = true;
const auto &mappings = _segment->GetVarMappings();
for (int i = 0; i < (int)_rows.size(); i++) {
auto combo = _rows[i].tempVarCombo;
const QSignalBlocker blocker(combo);
combo->clear();
PopulateTempVarCombo(combo);
if (i < (int)mappings.size() &&
mappings[i].tempVar.HasValidID()) {
QVariant v;
v.setValue(mappings[i].tempVar);
combo->setCurrentIndex(combo->findData(v));
}
}
_loading = false;
UpdateVisibility();
}
void TempVarOutputMappingsWidget::WriteBackMappings()
{
if (_loading || !_segment) {
return;
}
std::vector<VarMapping> mappings;
for (const auto &row : _rows) {
VarMapping m;
const int idx = row.tempVarCombo->currentIndex();
if (idx >= 0) {
m.tempVar = row.tempVarCombo->itemData(idx)
.value<TempVariableRef>();
}
auto item = row.varSelection->GetCurrentItem();
if (!item) {
continue;
}
m.variable = GetWeakVariableByName(item->Name());
mappings.push_back(std::move(m));
}
auto lock = LockContext();
_segment->SetVarMappings(mappings);
}
void TempVarOutputMappingsWidget::Rebuild()
{
_loading = true;
// Remove all existing row widgets from layout and delete them
for (auto &row : _rows) {
_rowsLayout->removeWidget(row.container);
delete row.container;
}
_rows.clear();
if (!_segment) {
_loading = false;
UpdateVisibility();
return;
}
const auto &mappings = _segment->GetVarMappings();
for (int i = 0; i < (int)mappings.size(); i++) {
const auto &mapping = mappings[i];
auto container = new QWidget(this);
auto rowLayout = new QHBoxLayout();
rowLayout->setContentsMargins(0, 0, 0, 0);
auto tempVarCombo = new FilterComboBox(
container,
obs_module_text("AdvSceneSwitcher.tempVar.select"));
tempVarCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
tempVarCombo->setMaximumWidth(350);
tempVarCombo->setDuplicatesEnabled(true);
PopulateTempVarCombo(tempVarCombo);
if (mapping.tempVar.HasValidID()) {
QVariant v;
v.setValue(mapping.tempVar);
tempVarCombo->setCurrentIndex(
tempVarCombo->findData(v));
}
auto arrowLabel = new QLabel(container);
{
QIcon icon;
const auto path = (GetThemeTypeName() == "Light")
? "theme:Light/right.svg"
: "theme:Dark/right.svg";
icon.addFile(QString::fromUtf8(path), QSize(),
QIcon::Normal, QIcon::Off);
arrowLabel->setPixmap(icon.pixmap(16, 16));
}
auto varSel = new VariableSelection(container);
varSel->SetVariable(mapping.variable);
auto removeBtn = new QToolButton(container);
removeBtn->setProperty("themeID", QVariant(QString::fromUtf8(
"removeIconSmall")));
removeBtn->setProperty(
"class", QVariant(QString::fromUtf8("icon-trash")));
removeBtn->setToolTip(obs_module_text(
"AdvSceneSwitcher.tempVar.outputMappings.remove"));
rowLayout->addWidget(tempVarCombo, 1);
rowLayout->addWidget(arrowLabel);
rowLayout->addWidget(varSel, 1);
rowLayout->addWidget(removeBtn);
container->setLayout(rowLayout);
_rowsLayout->addWidget(container);
_rows.push_back({container, tempVarCombo, varSel});
const int capturedIdx = i;
connect(removeBtn, &QPushButton::clicked, this,
[this, capturedIdx]() { Remove(capturedIdx); });
connect(tempVarCombo,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &TempVarOutputMappingsWidget::WriteBackMappings);
connect(varSel, &VariableSelection::SelectionChanged, this,
[this](const QString &) { WriteBackMappings(); });
}
_loading = false;
UpdateVisibility();
}
bool TempVarOutputMappingsWidget::IsExpandable() const
{
return _segment && !_segment->GetOwnTempVars().empty();
}
void TempVarOutputMappingsWidget::SetPanelExpanded(bool expanded)
{
_panelExpanded = expanded;
UpdateHeight();
}
void TempVarOutputMappingsWidget::SetSectionCollapsed(bool collapsed)
{
_sectionCollapsed = collapsed;
UpdateHeight();
}
void TempVarOutputMappingsWidget::AnimateTo(int targetHeight)
{
if (_animation->state() == QAbstractAnimation::Running) {
_animation->stop();
}
if (targetHeight > 0 && !isVisible()) {
setMinimumHeight(0);
setMaximumHeight(0);
show();
}
const int currentMax = maximumHeight();
const int currentHeight = (currentMax >= QWIDGETSIZE_MAX) ? height()
: currentMax;
_animationTargetHeight = targetHeight;
_animation->setStartValue(currentHeight);
_animation->setEndValue(targetHeight);
_animation->start();
}
void TempVarOutputMappingsWidget::AnimationFinished()
{
if (_animationTargetHeight > 0) {
setMaximumHeight(QWIDGETSIZE_MAX);
} else {
hide();
setMaximumHeight(QWIDGETSIZE_MAX);
}
}
void TempVarOutputMappingsWidget::UpdateHeight()
{
const bool shouldShow = _panelExpanded && !_sectionCollapsed &&
IsExpandable();
if (shouldShow) {
AnimateTo(sizeHint().height());
} else {
AnimateTo(0);
}
}
void TempVarOutputMappingsWidget::UpdateVisibility()
{
const bool expandable = IsExpandable();
emit ExpandableChanged(expandable);
UpdateHeight();
}
void TempVarOutputMappingsWidget::PopulateTempVarCombo(
FilterComboBox *combo) const
{
if (!_segment) {
return;
}
for (const auto &var : _segment->GetOwnTempVars()) {
QVariant v;
v.setValue(var.GetRef());
combo->addItem(QString::fromStdString(var.Name()), v);
}
}
} // namespace advss

View File

@@ -5,11 +5,12 @@
#include <obs-data.h>
#include <mutex>
#include <optional>
#include <QEnterEvent>
#include <QEvent>
#include <QStringList>
#include <string>
class QPropertyAnimation;
class QPushButton;
class QVBoxLayout;
namespace advss {
class Macro;
@@ -17,6 +18,9 @@ class MacroEdit;
class MacroSegment;
class TempVariableRef;
class TempVariableSelection;
class TempVarOutputMappingsWidget;
class VariableSelection;
struct VarMapping;
// TempVariables are variables that are local to a given macro.
// They can be created and used by macro segments.
@@ -127,4 +131,48 @@ private:
void NotifyUIAboutTempVarChange(MacroSegment *);
EXPORT void IncrementTempVarInUseGeneration();
class ADVSS_EXPORT TempVarOutputMappingsWidget : public QWidget {
Q_OBJECT
public:
TempVarOutputMappingsWidget(QWidget *parent);
void SetSegment(MacroSegment *segment);
void SetPanelExpanded(bool expanded);
void SetSectionCollapsed(bool collapsed);
bool IsExpandable() const;
signals:
void ExpandableChanged(bool);
private slots:
void Add();
void Remove(int rowIdx);
void SegmentTempVarsChanged(MacroSegment *segment);
void WriteBackMappings();
void AnimationFinished();
private:
void Rebuild();
void UpdateVisibility();
void UpdateHeight();
void AnimateTo(int targetHeight);
void PopulateTempVarCombo(FilterComboBox *combo) const;
struct MappingRow {
QWidget *container;
FilterComboBox *tempVarCombo;
VariableSelection *varSelection;
};
MacroSegment *_segment = nullptr;
QVBoxLayout *_rowsLayout;
QPushButton *_addButton;
std::vector<MappingRow> _rows;
bool _loading = false;
bool _panelExpanded = false;
bool _sectionCollapsed = false;
int _animationTargetHeight = 0;
QPropertyAnimation *_animation;
};
} // namespace advss

View File

@@ -63,7 +63,6 @@ static void load(obs_data_t *data)
{
tabSettings.Load(data, "tabSettings");
dockSettings.Load(data, "dockSettings");
enableDock(obs_data_get_bool(data, "addVariablesDock"));
}

View File

@@ -1,9 +1,12 @@
#include "variable.hpp"
#include "math-helpers.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include "utility.hpp"
#include <obs.hpp>
#include <QGridLayout>
namespace advss {
@@ -15,6 +18,15 @@ static std::deque<std::shared_ptr<Item>> variables;
static std::mutex lastVariableChangeMutex;
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
static bool setup()
{
AddEarlySaveStep(SaveVariables);
AddEarlyLoadStep(LoadVariables);
AddPluginCleanupStep([]() { variables.clear(); });
return true;
}
static bool setupDone = setup();
static void setLastVariableChangeTime()
{
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
@@ -418,34 +430,31 @@ static bool variableWithNameExists(const std::string &name)
void SaveVariables(obs_data_t *obj)
{
obs_data_array_t *variablesArray = obs_data_array_create();
OBSDataArrayAutoRelease variablesArray = obs_data_array_create();
for (const auto &v : variables) {
obs_data_t *array_obj = obs_data_create();
OBSDataAutoRelease array_obj = obs_data_create();
v->Save(array_obj);
obs_data_array_push_back(variablesArray, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, "variables", variablesArray);
obs_data_array_release(variablesArray);
}
void LoadVariables(obs_data_t *obj)
{
variables.clear();
obs_data_array_t *variablesArray = obs_data_get_array(obj, "variables");
OBSDataArrayAutoRelease variablesArray =
obs_data_get_array(obj, "variables");
size_t count = obs_data_array_count(variablesArray);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(variablesArray, i);
OBSDataAutoRelease array_obj =
obs_data_array_item(variablesArray, i);
auto var = Variable::Create();
variables.emplace_back(var);
variables.back()->Load(array_obj);
obs_data_release(array_obj);
}
obs_data_array_release(variablesArray);
}
static void signalImportedVariables(void *varsPtr)
@@ -460,16 +469,15 @@ static void signalImportedVariables(void *varsPtr)
void ImportVariables(obs_data_t *data)
{
obs_data_array_t *array = obs_data_get_array(data, "variables");
OBSDataArrayAutoRelease array = obs_data_get_array(data, "variables");
size_t count = obs_data_array_count(array);
auto importedVars = new std::vector<std::shared_ptr<Item>>;
for (size_t i = 0; i < count; i++) {
obs_data_t *arrayElement = obs_data_array_item(array, i);
OBSDataAutoRelease arrayElement = obs_data_array_item(array, i);
auto var = Variable::Create();
var->Load(arrayElement);
obs_data_release(arrayElement);
if (variableWithNameExists(var->Name())) {
continue;
@@ -479,8 +487,6 @@ void ImportVariables(obs_data_t *data)
importedVars->emplace_back(var);
}
obs_data_array_release(array);
QueueUITask(signalImportedVariables, importedVars);
}

View File

@@ -1,10 +1,12 @@
#include "platform-funcs.hpp"
#include "plugin-state-helpers.hpp"
#include <windows.h>
#include <UIAutomation.h>
#include <util/platform.h>
#include <TlHelp32.h>
#include <Psapi.h>
#include <memory>
#include <locale>
#include <codecvt>
#include <string>
@@ -16,6 +18,9 @@
#include <QWidget>
#include <mutex>
#define ADVSS_WIDEN_(x) L##x
#define ADVSS_WIDEN(x) ADVSS_WIDEN_(x)
namespace advss {
#define MAX_SEARCH 1000
@@ -133,9 +138,9 @@ const std::vector<std::string> getOBSWindows()
return lastDoneHelper->windows;
}
void GetWindowList(std::vector<std::string> &windows)
std::vector<std::string> GetWindowList()
{
windows.resize(0);
std::vector<std::string> windows;
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
// Also add OBS windows
@@ -147,20 +152,10 @@ void GetWindowList(std::vector<std::string> &windows)
// Add entry for OBS Studio itself - see GetCurrentWindowTitle()
windows.emplace_back("OBS");
return windows;
}
void GetWindowList(QStringList &windows)
{
windows.clear();
std::vector<std::string> w;
GetWindowList(w);
for (auto window : w) {
windows << QString::fromStdString(window);
}
}
void GetCurrentWindowTitle(std::string &title)
std::string GetCurrentWindowTitle()
{
HWND window = GetForegroundWindow();
DWORD pid;
@@ -178,15 +173,15 @@ void GetCurrentWindowTitle(std::string &title)
//
// So instead rely on Qt to get the title of the active window.
if (GetCurrentProcessId() == pid) {
auto window = QApplication::activeWindow();
if (window) {
title = window->windowTitle().toStdString();
} else {
title = "OBS";
auto obsWindow = QApplication::activeWindow();
if (obsWindow) {
return obsWindow->windowTitle().toStdString();
}
return;
return "OBS";
}
std::string title;
GetWindowTitle(window, title);
return title;
}
static HWND getHWNDfromTitle(const std::string &title)
@@ -350,22 +345,22 @@ bool IsFullscreen(const std::string &title)
return false;
}
void GetProcessList(QStringList &processes)
QStringList GetProcessList()
{
QStringList processes;
HANDLE procSnapshot;
PROCESSENTRY32 procEntry;
procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (procSnapshot == INVALID_HANDLE_VALUE) {
return;
return processes;
}
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(procSnapshot, &procEntry)) {
CloseHandle(procSnapshot);
return;
return processes;
}
do {
@@ -383,9 +378,10 @@ void GetProcessList(QStringList &processes)
} while (Process32Next(procSnapshot, &procEntry));
CloseHandle(procSnapshot);
return processes;
}
static void GetForegroundProcessName(QString &proc)
static QString getForegroundProcessNameStr()
{
// only checks if the current foreground window is from the same executable,
// may return true for any window from a program
@@ -396,31 +392,90 @@ static void GetForegroundProcessName(QString &proc)
HANDLE process = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (process == NULL) {
return;
return {};
}
WCHAR executablePath[600];
GetModuleFileNameEx(process, 0, executablePath, 600);
CloseHandle(process);
proc = QString::fromWCharArray(executablePath)
.split(QRegularExpression("(/|\\\\)"))
.back();
return QString::fromWCharArray(executablePath)
.split(QRegularExpression("(/|\\\\)"))
.back();
}
void GetForegroundProcessName(std::string &proc)
std::string GetForegroundProcessName()
{
QString temp;
GetForegroundProcessName(temp);
proc = temp.toStdString();
return getForegroundProcessNameStr().toStdString();
}
std::string GetForegroundProcessPath()
{
HWND foregroundWindow = GetForegroundWindow();
DWORD processId = 0;
GetWindowThreadProcessId(foregroundWindow, &processId);
HANDLE process = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (process == NULL) {
return {};
}
WCHAR executablePath[600];
GetModuleFileNameEx(process, 0, executablePath, 600);
CloseHandle(process);
return QString::fromWCharArray(executablePath).toStdString();
}
QStringList GetProcessPathsFromName(const QString &name)
{
QStringList paths;
HANDLE procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (procSnapshot == INVALID_HANDLE_VALUE) {
return paths;
}
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(procSnapshot, &procEntry)) {
CloseHandle(procSnapshot);
return paths;
}
do {
QString exeName = QString::fromWCharArray(procEntry.szExeFile);
if (exeName != name) {
continue;
}
HANDLE process =
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, procEntry.th32ProcessID);
if (process == NULL) {
continue;
}
WCHAR executablePath[600];
if (GetModuleFileNameEx(process, 0, executablePath, 600)) {
QString path = QString::fromWCharArray(executablePath);
if (!paths.contains(path)) {
paths.append(path);
}
}
CloseHandle(process);
} while (Process32Next(procSnapshot, &procEntry));
CloseHandle(procSnapshot);
return paths;
}
bool IsInFocus(const QString &executable)
{
// only checks if the current foreground window is from the same executable,
// may return true for any window from a program
QString foregroundProc;
GetForegroundProcessName(foregroundProc);
const auto foregroundProc = getForegroundProcessNameStr();
// True if executable switch equals current window
bool equals = (executable == foregroundProc);
@@ -450,9 +505,86 @@ int SecondsSinceLastInput()
return (getTime() - getLastInputTime()) / 1000;
}
static void addPluginFolderToSymbolPath()
{
// This runs after OBS_FRONTEND_EVENT_FINISHED_LOADING, which fires after
// obs_load_all_modules() completes. By that point OBS has already called
// reset_win32_symbol_paths() -> SymInitializeW(), so DbgHelp is
// initialized and we can append our plugins subfolder (where the PDB
// files live) to the existing search path.
HMODULE dbghelp = LoadLibraryW(L"DbgHelp");
if (!dbghelp) {
return;
}
typedef BOOL(WINAPI * SymGetSearchPathW_t)(HANDLE, PWSTR, DWORD);
typedef BOOL(WINAPI * SymSetSearchPathW_t)(HANDLE, PCWSTR);
typedef BOOL(WINAPI * SymRefreshModuleList_t)(HANDLE);
auto symGetSearchPathW = reinterpret_cast<SymGetSearchPathW_t>(
GetProcAddress(dbghelp, "SymGetSearchPathW"));
auto symSetSearchPathW = reinterpret_cast<SymSetSearchPathW_t>(
GetProcAddress(dbghelp, "SymSetSearchPathW"));
auto symRefreshModuleList = reinterpret_cast<SymRefreshModuleList_t>(
GetProcAddress(dbghelp, "SymRefreshModuleList"));
if (!symGetSearchPathW || !symSetSearchPathW || !symRefreshModuleList) {
FreeLibrary(dbghelp);
return;
}
HMODULE hModule = NULL;
if (!GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(addPluginFolderToSymbolPath),
&hModule)) {
FreeLibrary(dbghelp);
return;
}
wchar_t dllDir[MAX_PATH];
if (!GetModuleFileNameW(hModule, dllDir, MAX_PATH)) {
FreeLibrary(dbghelp);
return;
}
wchar_t *lastSep = wcsrchr(dllDir, L'\\');
if (!lastSep) {
FreeLibrary(dbghelp);
return;
}
*lastSep = L'\0';
wchar_t pluginsPath[MAX_PATH];
wcsncpy_s(pluginsPath, MAX_PATH, dllDir, _TRUNCATE);
wcsncat_s(pluginsPath, MAX_PATH, L"\\" ADVSS_WIDEN(ADVSS_PLUGIN_FOLDER),
_TRUNCATE);
constexpr DWORD currentPathLen = 4096;
constexpr DWORD newPathLen = 8192;
auto currentPath = std::make_unique<wchar_t[]>(currentPathLen);
auto newPath = std::make_unique<wchar_t[]>(newPathLen);
symGetSearchPathW(GetCurrentProcess(), currentPath.get(),
currentPathLen);
if (currentPath[0] != L'\0') {
_snwprintf_s(newPath.get(), newPathLen, _TRUNCATE, L"%s;%s",
currentPath.get(), pluginsPath);
} else {
wcsncpy_s(newPath.get(), newPathLen, pluginsPath, _TRUNCATE);
}
symSetSearchPathW(GetCurrentProcess(), newPath.get());
symRefreshModuleList(GetCurrentProcess());
FreeLibrary(dbghelp);
}
void PlatformInit()
{
CoInitialize(NULL);
AddFinishedLoadingStep(addPluginFolderToSymbolPath);
}
void PlatformCleanup()

View File

@@ -11,10 +11,11 @@ const std::string MacroActionFilter::id = "filter";
std::vector<TempVariableRef> MacroActionFilter::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
auto refs = MacroSegment::GetTempVarRefs();
if (_tempVar.HasValidID()) {
refs.push_back(_tempVar);
}
return {_tempVar};
return refs;
}
bool MacroActionFilter::_registered = MacroActionFactory::Register(

View File

@@ -15,10 +15,11 @@ const std::string MacroActionSource::id = "source";
std::vector<TempVariableRef> MacroActionSource::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
auto refs = MacroSegment::GetTempVarRefs();
if (_tempVar.HasValidID()) {
refs.push_back(_tempVar);
}
return {_tempVar};
return refs;
}
bool MacroActionSource::_registered = MacroActionFactory::Register(
@@ -793,6 +794,8 @@ void MacroActionSourceEdit::SetWidgetVisibility()
action == MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
action == MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
emit ShowVariableMappings(isGetSetting || isGetSettings);
adjustSize();
updateGeometry();
}

View File

@@ -113,6 +113,7 @@ private slots:
signals:
void HeaderInfoChanged(const QString &);
void ShowVariableMappings(bool show);
private:
void SetWidgetVisibility();

View File

@@ -4,6 +4,7 @@
#include "ui-helpers.hpp"
#include <obs-frontend-api.h>
#include <obs.hpp>
#include <util/config-file.h>
namespace advss {
@@ -183,6 +184,8 @@ MacroActionStreamEdit::MacroActionStreamEdit(
_keyFrameInterval(new VariableSpinBox()),
_stringValue(new VariableLineEdit(this)),
_showPassword(new QPushButton()),
_getCurrentValue(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.streaming.getCurrentValue"))),
_layout(new QHBoxLayout())
{
_keyFrameInterval->setMinimum(0);
@@ -208,13 +211,17 @@ MacroActionStreamEdit::MacroActionStreamEdit(
SLOT(ShowPassword()));
QWidget::connect(_showPassword, SIGNAL(released()), this,
SLOT(HidePassword()));
QWidget::connect(_getCurrentValue, SIGNAL(clicked()), this,
SLOT(GetCurrentValueClicked()));
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.streaming.entry"),
_layout,
{{"{{actions}}", _actions},
{"{{keyFrameInterval}}", _keyFrameInterval},
{"{{stringValue}}", _stringValue},
{"{{showPassword}}", _showPassword}});
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.streaming.layout"),
_layout,
{{"{{actions}}", _actions},
{"{{keyFrameInterval}}", _keyFrameInterval},
{"{{stringValue}}", _stringValue},
{"{{showPassword}}", _showPassword},
{"{{getCurrentValue}}", _getCurrentValue}});
setLayout(_layout);
_entryData = entryData;
@@ -291,6 +298,62 @@ void MacroActionStreamEdit::SetWidgetVisibility()
_stringValue->setEchoMode(QLineEdit::Normal);
_showPassword->hide();
}
_getCurrentValue->setVisible(
action == MacroActionStream::Action::KEYFRAME_INTERVAL ||
action == MacroActionStream::Action::SERVER ||
action == MacroActionStream::Action::STREAM_KEY ||
action == MacroActionStream::Action::USERNAME ||
action == MacroActionStream::Action::PASSWORD);
}
void MacroActionStreamEdit::GetCurrentValueClicked()
{
if (!_entryData) {
return;
}
switch (_entryData->_action) {
case MacroActionStream::Action::KEYFRAME_INTERVAL: {
const auto configPath =
GetPathInProfileDir("streamEncoder.json");
OBSDataAutoRelease settings =
obs_data_create_from_json_file_safe(configPath.c_str(),
"bak");
if (!settings) {
break;
}
_keyFrameInterval->SetFixedValue(
(int)obs_data_get_int(settings, "keyint_sec"));
break;
}
case MacroActionStream::Action::SERVER:
case MacroActionStream::Action::STREAM_KEY:
case MacroActionStream::Action::USERNAME:
case MacroActionStream::Action::PASSWORD: {
static const std::map<MacroActionStream::Action, const char *>
settingsKeys = {
{MacroActionStream::Action::SERVER, "server"},
{MacroActionStream::Action::STREAM_KEY, "key"},
{MacroActionStream::Action::USERNAME,
"username"},
{MacroActionStream::Action::PASSWORD,
"password"},
};
auto service = obs_frontend_get_streaming_service();
OBSDataAutoRelease settings = obs_service_get_settings(service);
if (!settings) {
obs_service_release(service);
break;
}
const char *val = obs_data_get_string(
settings, settingsKeys.at(_entryData->_action));
if (val) {
_stringValue->setText(QString(val));
}
break;
}
default:
break;
}
}
void MacroActionStreamEdit::ActionChanged(int value)

View File

@@ -67,12 +67,14 @@ private slots:
void StringValueChanged();
void ShowPassword();
void HidePassword();
void GetCurrentValueClicked();
protected:
QComboBox *_actions;
VariableSpinBox *_keyFrameInterval;
VariableLineEdit *_stringValue;
QPushButton *_showPassword;
QPushButton *_getCurrentValue;
std::shared_ptr<MacroActionStream> _entryData;
private:

View File

@@ -42,8 +42,7 @@ void CloseWindow(const std::string &) {}
std::optional<std::string> MacroActionWindow::GetMatchingWindow() const
{
std::vector<std::string> windowList;
GetWindowList(windowList);
const auto windowList = GetWindowList();
if (!_regex.Enabled()) {
if (std::find(windowList.begin(), windowList.end(),

View File

@@ -16,54 +16,100 @@ bool MacroConditionProcess::_registered = MacroConditionFactory::Register(
bool MacroConditionProcess::CheckCondition()
{
QStringList runningProcesses;
QString proc = QString::fromStdString(_process);
GetProcessList(runningProcesses);
std::string foregroundProcessName;
GetForegroundProcessName(foregroundProcessName);
const auto foregroundProcessName = GetForegroundProcessName();
SetVariableValue(foregroundProcessName);
if (!_regex.Enabled()) {
if (runningProcesses.contains(proc) &&
(!_checkFocus || IsInFocus(proc))) {
SetTempVarValue("name", proc.toStdString());
return true;
}
return false;
}
const QString proc = QString::fromStdString(_process);
int matchIndex = -1;
bool foundMatch = false;
for (const auto &process : runningProcesses) {
matchIndex++;
if (_regex.Matches(process, proc)) {
foundMatch = true;
break;
if (_checkFocus) {
// Check name and path against the same foreground process
// instance to avoid false positives when multiple processes
// share the same name
const auto foregroundPath = GetForegroundProcessPath();
const QString foregroundName =
QString::fromStdString(foregroundProcessName);
SetTempVarValue("name", foregroundProcessName);
SetTempVarValue("path", foregroundPath);
bool nameMatches =
_regex.Enabled() ? _regex.Matches(foregroundName, proc)
: (foregroundName == proc);
if (!nameMatches) {
return false;
}
}
if (!foundMatch) {
return false;
}
if (!_checkFocus) {
SetTempVarValue("name",
runningProcesses.at(matchIndex).toStdString());
if (_checkPath) {
const QString pathPattern =
QString::fromStdString(_processPath);
const QString qForegroundPath =
QString::fromStdString(foregroundPath);
bool pathMatches =
_pathRegex.Enabled()
? _pathRegex.Matches(qForegroundPath,
pathPattern)
: qForegroundPath == pathPattern;
if (!pathMatches) {
return false;
}
}
return true;
}
if (!IsInFocus(proc)) {
return false;
const auto runningProcesses = GetProcessList();
for (const auto &process : runningProcesses) {
bool nameMatches = _regex.Enabled()
? _regex.Matches(process, proc)
: (process == proc);
if (!nameMatches) {
continue;
}
if (!_checkPath) {
SetTempVarValue("name", process.toStdString());
return true;
}
const auto paths = GetProcessPathsFromName(process);
const QString pathPattern =
QString::fromStdString(_processPath);
bool foundMatchingPath = false;
for (const auto &path : paths) {
bool pathMatches =
_pathRegex.Enabled()
? _pathRegex.Matches(path, pathPattern)
: path == pathPattern;
if (pathMatches) {
SetTempVarValue("path", path.toStdString());
foundMatchingPath = true;
break;
}
}
if (!foundMatchingPath) {
continue;
}
SetTempVarValue("name", process.toStdString());
return true;
}
SetTempVarValue("name", foregroundProcessName);
return true;
return false;
}
bool MacroConditionProcess::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
_process.Save(obj, "process");
obs_data_set_bool(obj, "focus", _checkFocus);
_regex.Save(obj);
obs_data_set_int(obj, "version", 1);
obs_data_set_bool(obj, "focus", _checkFocus);
obs_data_set_bool(obj, "checkPath", _checkPath);
_processPath.Save(obj, "processPath");
_pathRegex.Save(obj, "pathRegex");
obs_data_set_int(obj, "version", 2);
return true;
}
@@ -78,6 +124,9 @@ bool MacroConditionProcess::Load(obs_data_t *obj)
} else {
_regex.Load(obj);
}
_checkPath = obs_data_get_bool(obj, "checkPath");
_processPath.Load(obj, "processPath");
_pathRegex.Load(obj, "pathRegex");
return true;
}
@@ -91,6 +140,8 @@ void MacroConditionProcess::SetupTempVars()
MacroCondition::SetupTempVars();
AddTempvar("name",
obs_module_text("AdvSceneSwitcher.tempVar.process.name"));
AddTempvar("path",
obs_module_text("AdvSceneSwitcher.tempVar.process.path"));
}
MacroConditionProcessEdit::MacroConditionProcessEdit(
@@ -100,13 +151,20 @@ MacroConditionProcessEdit::MacroConditionProcessEdit(
_regex(new RegexConfigWidget(this)),
_focused(new QCheckBox()),
_focusProcess(new QLabel()),
_focusLayout(new QHBoxLayout())
_focusLayout(new QHBoxLayout()),
_checkPath(new QCheckBox()),
_processPath(new VariableLineEdit(this)),
_pathRegex(new RegexConfigWidget(this)),
_pathLayout(new QHBoxLayout())
{
_processSelection->setEditable(true);
_processSelection->setMaxVisibleItems(20);
_processSelection->setToolTip(
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
_processPath->setToolTip(
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
QWidget::connect(_processSelection,
SIGNAL(currentTextChanged(const QString &)), this,
SLOT(ProcessChanged(const QString &)));
@@ -115,6 +173,13 @@ MacroConditionProcessEdit::MacroConditionProcessEdit(
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_focused, SIGNAL(stateChanged(int)), this,
SLOT(FocusChanged(int)));
QWidget::connect(_checkPath, SIGNAL(stateChanged(int)), this,
SLOT(CheckPathChanged(int)));
QWidget::connect(_processPath, SIGNAL(textChanged(const QString &)),
this, SLOT(ProcessPathChanged(const QString &)));
QWidget::connect(_pathRegex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(PathRegexChanged(const RegexConfig &)));
QWidget::connect(&_timer, SIGNAL(timeout()), this,
SLOT(UpdateFocusProcess()));
@@ -125,18 +190,25 @@ MacroConditionProcessEdit::MacroConditionProcessEdit(
{"{{regex}}", _regex},
{"{{focused}}", _focused},
{"{{focusProcess}}", _focusProcess},
{"{{checkPath}}", _checkPath},
{"{{path}}", _processPath},
{"{{pathRegex}}", _pathRegex},
};
auto entryLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.process.entry"),
obs_module_text("AdvSceneSwitcher.condition.process.layout"),
entryLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.process.entry.focus"),
"AdvSceneSwitcher.condition.process.layout.focus"),
_focusLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.process.layout.path"),
_pathLayout, widgetPlaceholders);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addLayout(_focusLayout);
mainLayout->addLayout(_pathLayout);
setLayout(mainLayout);
_entryData = entryData;
@@ -178,11 +250,31 @@ void MacroConditionProcessEdit::FocusChanged(int state)
SetWidgetVisibility();
}
void MacroConditionProcessEdit::CheckPathChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_checkPath = state;
SetWidgetVisibility();
}
void MacroConditionProcessEdit::ProcessPathChanged(const QString &text)
{
GUARD_LOADING_AND_LOCK();
_entryData->_processPath = text.toStdString();
}
void MacroConditionProcessEdit::PathRegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pathRegex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionProcessEdit::UpdateFocusProcess()
{
std::string name;
GetForegroundProcessName(name);
_focusProcess->setText(QString::fromStdString(name));
_focusProcess->setText(
QString::fromStdString(GetForegroundProcessName()));
}
void MacroConditionProcessEdit::SetWidgetVisibility()
@@ -191,6 +283,13 @@ void MacroConditionProcessEdit::SetWidgetVisibility()
return;
}
SetLayoutVisible(_focusLayout, _entryData->_checkFocus);
_processPath->setVisible(_entryData->_checkPath);
_pathRegex->setVisible(_entryData->_checkPath);
if (_entryData->_checkPath) {
RemoveStretchIfPresent(_pathLayout);
} else {
AddStretchIfNecessary(_pathLayout);
}
adjustSize();
updateGeometry();
}
@@ -205,6 +304,9 @@ void MacroConditionProcessEdit::UpdateEntryData()
_entryData->_process.UnresolvedValue().c_str());
_regex->SetRegexConfig(_entryData->_regex);
_focused->setChecked(_entryData->_checkFocus);
_checkPath->setChecked(_entryData->_checkPath);
_processPath->setText(_entryData->_processPath);
_pathRegex->SetRegexConfig(_entryData->_pathRegex);
SetWidgetVisibility();
}

View File

@@ -1,7 +1,7 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "regex-config.hpp"
#include "variable-string.hpp"
#include "variable-line-edit.hpp"
#include <QCheckBox>
@@ -21,8 +21,11 @@ public:
}
StringVariable _process;
bool _checkFocus = true;
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig();
bool _checkFocus = true;
bool _checkPath = false;
StringVariable _processPath;
RegexConfig _pathRegex = RegexConfig::PartialMatchRegexConfig();
private:
void SetupTempVars();
@@ -54,6 +57,9 @@ private slots:
void ProcessChanged(const QString &text);
void RegexChanged(const RegexConfig &);
void FocusChanged(int state);
void CheckPathChanged(int state);
void ProcessPathChanged(const QString &text);
void PathRegexChanged(const RegexConfig &);
void UpdateFocusProcess();
signals:
void HeaderInfoChanged(const QString &);
@@ -66,6 +72,10 @@ private:
QCheckBox *_focused;
QLabel *_focusProcess;
QHBoxLayout *_focusLayout;
QCheckBox *_checkPath;
VariableLineEdit *_processPath;
RegexConfigWidget *_pathRegex;
QHBoxLayout *_pathLayout;
QTimer _timer;
std::shared_ptr<MacroConditionProcess> _entryData;
bool _loading = true;

View File

@@ -135,8 +135,7 @@ static bool foregroundWindowChanged()
bool MacroConditionWindow::CheckCondition()
{
std::vector<std::string> windowList;
GetWindowList(windowList);
const auto windowList = GetWindowList();
bool match = false;
if (_windowRegex.Enabled()) {
match = WindowRegexMatches(windowList);

View File

@@ -23,6 +23,7 @@ bool setup()
{
AddSaveStep(saveConnections);
AddLoadStep(loadConnections);
AddPluginCleanupStep([]() { connections.clear(); });
return true;
}

View File

@@ -718,6 +718,7 @@ static bool setup()
{
AddSaveStep(SaveMqttConnections);
AddLoadStep(LoadMqttConnections);
AddPluginCleanupStep([]() { GetMqttConnections().clear(); });
return true;
}

View File

@@ -2,9 +2,13 @@
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include <obs-frontend-api.h>
#include <obs-module.h>
#include <obs.hpp>
#include <mutex>
#include <unordered_set>
#include <QDir>
#include <QFileInfo>
@@ -28,6 +32,17 @@ const std::string_view InlineScript::_defaultLUAScript =
"end";
;
static std::mutex instancesMutex;
static std::unordered_set<InlineScript *> instances;
static void handleScriptingShutdown(enum obs_frontend_event event, void *)
{
if (event != OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN) {
return;
}
InlineScript::DeregisterAll();
}
static bool setup()
{
auto sh = obs_get_signal_handler();
@@ -35,6 +50,8 @@ static bool setup()
std::string("void ") + signalName.data() + "(string id)";
signal_handler_add(sh, signalDecl.c_str());
obs_frontend_add_event_callback(handleScriptingShutdown, nullptr);
return true;
}
static bool setupDone = setup();
@@ -98,6 +115,10 @@ static bool createScriptFile(const char *settingsFile, const char *content)
InlineScript::InlineScript() : _instanceId(_instanceIdCounter++)
{
{
std::lock_guard<std::mutex> lock(instancesMutex);
instances.insert(this);
}
Setup();
}
@@ -107,9 +128,27 @@ InlineScript::InlineScript(const InlineScript &other)
_textLUA(other._textLUA),
_instanceId(_instanceIdCounter++)
{
{
std::lock_guard<std::mutex> lock(instancesMutex);
instances.insert(this);
}
Setup();
}
InlineScript::~InlineScript()
{
std::lock_guard<std::mutex> lock(instancesMutex);
instances.erase(this);
}
void InlineScript::DeregisterAll()
{
std::lock_guard<std::mutex> lock(instancesMutex);
for (auto *instance : instances) {
instance->_script.reset();
}
}
void InlineScript::Save(obs_data_t *data) const
{
OBSDataAutoRelease obj = obs_data_create();

View File

@@ -15,6 +15,7 @@ class InlineScript {
public:
InlineScript();
InlineScript(const InlineScript &);
~InlineScript();
enum Type { INLINE, FILE };
@@ -34,6 +35,8 @@ public:
void ResolveVariablesToFixedValues();
static void DeregisterAll();
private:
void Setup();
void SetupFile();

View File

@@ -8,6 +8,8 @@
#include "date/tz.h"
#endif
using namespace std::chrono_literals;
namespace advss {
using websocketpp::lib::placeholders::_1;
@@ -28,7 +30,7 @@ static constexpr std::string_view registerSubscriptionURL =
static constexpr std::string_view registerSubscriptionPath =
"/helix/eventsub/subscriptions";
#endif
static const int reconnectDelay = 15;
static const auto reconnectDelay = 15s;
#undef DispatchMessage
@@ -81,6 +83,10 @@ void EventSub::ConnectThread()
} else {
_client->connect(con);
_connection = connection_hdl(con);
if (_disconnect) {
_client->close(con, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
}
_client->run();
}
@@ -98,9 +104,9 @@ void EventSub::WaitAndReconnect()
auto thread = std::thread([this]() {
std::unique_lock<std::mutex> lock(_waitMtx);
blog(LOG_INFO,
"Twitch EventSub trying to reconnect to in %d seconds.",
reconnectDelay);
_cv.wait_for(lock, std::chrono::seconds(reconnectDelay));
"Twitch EventSub trying to reconnect to in %lld seconds.",
(long long)reconnectDelay.count());
_cv.wait_for(lock, reconnectDelay);
_reconnecting = false;
if (_disconnect) {
@@ -155,12 +161,6 @@ void EventSub::Disconnect()
_cv.notify_all();
}
while (_connected) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_client->close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
}
if (_thread.joinable()) {
_thread.join();
}
@@ -342,8 +342,7 @@ static bool isValidTimestamp(const std::string &timestamp)
auto duration = now - parsedTime;
// Clocks might be off by a bit, so allow negative values also
return duration <= std::chrono::minutes(10) &&
duration >= std::chrono::minutes(-1);
return duration <= 10min && duration >= -1min;
} catch (const std::exception &e) {
blog(LOG_WARNING, "%s: %s", __func__, e.what());
return false;

View File

@@ -1840,7 +1840,10 @@ void MacroActionTwitchEdit::SetWidgetLayout()
const char *layoutText;
const char *layout2Text = nullptr;
switch (_entryData->GetAction()) {
const auto action = _entryData->GetAction();
switch (action) {
case MacroActionTwitch::Action::SEND_CHAT_MESSAGE:
layoutText = obs_module_text(
"AdvSceneSwitcher.action.twitch.layout.chat");
@@ -1892,6 +1895,12 @@ void MacroActionTwitchEdit::SetWidgetLayout()
_layout->setContentsMargins(0, 0, 0, 0);
_layout2->setContentsMargins(0, 0, 0, 0);
const bool showVariableMapping =
action == MacroActionTwitch::Action::CHANNEL_GET_INFO ||
action == MacroActionTwitch::Action::USER_GET_INFO ||
action == MacroActionTwitch::Action::POINTS_REWARD_GET_INFO;
emit ShowVariableMappings(showVariableMapping);
}
void MacroActionTwitchEdit::UpdateEntryData()

View File

@@ -267,6 +267,7 @@ private slots:
signals:
void HeaderInfoChanged(const QString &);
void ShowVariableMappings(bool show);
protected:
std::shared_ptr<MacroActionTwitch> _entryData;

View File

@@ -87,6 +87,7 @@ static bool setupTwitchTokenSupport()
{
AddSaveStep(saveConnections);
AddLoadStep(loadConnections);
AddPluginCleanupStep([]() { twitchTokens.clear(); });
return true;
}

View File

@@ -187,6 +187,13 @@ target_sources(
${ADVSS_SOURCE_DIR}/lib/utils/file-selection.cpp
${ADVSS_SOURCE_DIR}/lib/variables/variable-text-edit.cpp)
# --- macro-condition-process --- #
target_sources(
${PROJECT_NAME}
PRIVATE test-macro-condition-process.cpp stubs/platform-funcs.cpp
${ADVSS_SOURCE_DIR}/plugins/base/macro-condition-process.cpp)
# --- Testing --- #
enable_testing()

View File

@@ -0,0 +1,101 @@
#include "platform-funcs.hpp"
#include "selection-helpers.hpp"
#include <QComboBox>
#include <optional>
#include <string>
#include <vector>
namespace advss {
namespace {
std::string g_foregroundProcessName;
std::string g_foregroundProcessPath;
QStringList g_processList;
QStringList g_processPathsFromName;
} // namespace
void SetStubForegroundProcessName(const std::string &name)
{
g_foregroundProcessName = name;
}
void SetStubForegroundProcessPath(const std::string &path)
{
g_foregroundProcessPath = path;
}
void SetStubProcessList(const QStringList &list)
{
g_processList = list;
}
void SetStubProcessPaths(const QStringList &paths)
{
g_processPathsFromName = paths;
}
// --- platform-funcs.hpp stubs ---
std::vector<std::string> GetWindowList()
{
return {};
}
std::string GetCurrentWindowTitle()
{
return {};
}
bool IsFullscreen(const std::string &)
{
return false;
}
bool IsMaximized(const std::string &)
{
return false;
}
std::optional<std::string> GetTextInWindow(const std::string &)
{
return {};
}
int SecondsSinceLastInput()
{
return 0;
}
QStringList GetProcessList()
{
return g_processList;
}
std::string GetForegroundProcessName()
{
return g_foregroundProcessName;
}
std::string GetForegroundProcessPath()
{
return g_foregroundProcessPath;
}
QStringList GetProcessPathsFromName(const QString &)
{
return g_processPathsFromName;
}
bool IsInFocus(const QString &)
{
return false;
}
// --- selection-helpers.hpp stubs ---
void PopulateProcessSelection(QComboBox *, bool) {}
} // namespace advss

View File

@@ -4,6 +4,8 @@ namespace advss {
void SavePluginSettings(obs_data_t *) {}
void LoadPluginSettings(obs_data_t *) {}
void AddEarlySaveStep(std::function<void(obs_data_t *)>) {}
void AddEarlyLoadStep(std::function<void(obs_data_t *)>) {}
void AddSaveStep(std::function<void(obs_data_t *)>) {}
void AddLoadStep(std::function<void(obs_data_t *)>) {}
void AddPostLoadStep(std::function<void()>) {}

View File

@@ -13,7 +13,10 @@ using advss::MacroConditionFile;
static void writeFile(const QString &path, const QString &content)
{
QFile f(path);
f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text |
QIODevice::Truncate)) {
return;
}
QTextStream(&f) << content;
}
@@ -155,7 +158,9 @@ TEST_CASE("DATE_CHANGE: triggers when modification date changes",
// Explicitly set the modification time to a known future value so the
// test is not sensitive to filesystem mtime resolution.
QFile f(path);
f.open(QIODevice::ReadWrite);
if (!f.open(QIODevice::ReadWrite)) {
return;
}
f.setFileTime(QDateTime::currentDateTime().addSecs(10),
QFileDevice::FileModificationTime);
f.close();

View File

@@ -0,0 +1,170 @@
#include "catch.hpp"
#include "macro-condition-process.hpp"
namespace advss {
void SetStubForegroundProcessName(const std::string &);
void SetStubForegroundProcessPath(const std::string &);
void SetStubProcessList(const QStringList &);
void SetStubProcessPaths(const QStringList &);
} // namespace advss
using advss::MacroConditionProcess;
// ---------------------------------------------------------------------------
// Name matching — no focus, no path
// ---------------------------------------------------------------------------
TEST_CASE("no focus: exact name match returns true",
"[macro-condition-process]")
{
advss::SetStubProcessList({"game.exe", "obs64.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = false;
REQUIRE(cond.CheckCondition());
}
TEST_CASE("no focus: exact name mismatch returns false",
"[macro-condition-process]")
{
advss::SetStubProcessList({"obs64.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = false;
REQUIRE_FALSE(cond.CheckCondition());
}
TEST_CASE("no focus: regex name match returns true",
"[macro-condition-process]")
{
advss::SetStubProcessList({"game.exe", "obs64.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.*";
cond._checkFocus = false;
cond._regex.SetEnabled(true);
REQUIRE(cond.CheckCondition());
}
TEST_CASE("no focus: regex name mismatch returns false",
"[macro-condition-process]")
{
advss::SetStubProcessList({"obs64.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.*";
cond._checkFocus = false;
cond._regex.SetEnabled(true);
REQUIRE_FALSE(cond.CheckCondition());
}
// ---------------------------------------------------------------------------
// Focus — name only
// ---------------------------------------------------------------------------
TEST_CASE("focus: foreground name matches returns true",
"[macro-condition-process]")
{
advss::SetStubForegroundProcessName("game.exe");
advss::SetStubForegroundProcessPath("C:/Games/Game/game.exe");
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = true;
REQUIRE(cond.CheckCondition());
}
TEST_CASE("focus: foreground name does not match returns false",
"[macro-condition-process]")
{
advss::SetStubForegroundProcessName("obs64.exe");
advss::SetStubForegroundProcessPath("C:/OBS/obs64.exe");
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = true;
REQUIRE_FALSE(cond.CheckCondition());
}
// ---------------------------------------------------------------------------
// Path matching — no focus
// ---------------------------------------------------------------------------
TEST_CASE("no focus with path: name and path both match returns true",
"[macro-condition-process]")
{
advss::SetStubProcessList({"game.exe"});
advss::SetStubProcessPaths({"C:/Steam/steamapps/common/Game/game.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = false;
cond._checkPath = true;
cond._processPath = "C:/Steam/steamapps/common/Game/game.exe";
REQUIRE(cond.CheckCondition());
}
TEST_CASE("no focus with path: name matches but path does not returns false",
"[macro-condition-process]")
{
advss::SetStubProcessList({"game.exe"});
advss::SetStubProcessPaths({"C:/Epic/Games/Game/game.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = false;
cond._checkPath = true;
cond._processPath = "Steam";
REQUIRE_FALSE(cond.CheckCondition());
}
// ---------------------------------------------------------------------------
// Focus + path
//
// When both focus and path are checked, both must be satisfied by the *same*
// foreground process instance. A background process that matches the path
// but is not in focus must not cause a false positive.
// ---------------------------------------------------------------------------
TEST_CASE("focus with path: foreground matches both name and path returns true",
"[macro-condition-process]")
{
advss::SetStubForegroundProcessName("game.exe");
advss::SetStubForegroundProcessPath(
"C:/Steam/steamapps/common/Game/game.exe");
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = true;
cond._checkPath = true;
cond._processPath = "C:/Steam/steamapps/common/Game/game.exe";
REQUIRE(cond.CheckCondition());
}
TEST_CASE(
"focus with path: foreground name matches but path does not returns false",
"[macro-condition-process]")
{
advss::SetStubForegroundProcessName("game.exe");
advss::SetStubForegroundProcessPath("C:/Epic/Games/Game/game.exe");
advss::SetStubProcessPaths({"C:/Steam/steamapps/common/Game/game.exe"});
MacroConditionProcess cond(nullptr);
cond._process = "game.exe";
cond._checkFocus = true;
cond._checkPath = true;
cond._processPath = "C:/Steam/steamapps/common/Game/game.exe";
REQUIRE_FALSE(cond.CheckCondition());
}