Compare commits

..

12 Commits
1.33.0 ... test

Author SHA1 Message Date
WarmUpTill
9d38d22270 Tests: Make building tests optional, enable for CI 2026-04-29 22:58:45 +02:00
WarmUpTill
de007176d5 Tests: Handle QFile::open() return value 2026-04-29 22:58:41 +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
WarmUpTill
ba38b8bf27 Don't block UI while executing long runnig actions
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
The previous approach had the problem of losing any action internal
state changes in the created copy.

Revert "Fix temp var values of actions not being accessible"
This reverts commit df42538319.
Revert "Don't block UI while running actions"
This reverts commit a01d26e25d.
2026-04-04 21:14:05 +02:00
WarmUpTill
38513735dd Refresh available interactable soruces on show 2026-04-04 21:14:05 +02:00
63 changed files with 1525 additions and 301 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

@@ -211,8 +211,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 +416,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 +451,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

@@ -484,8 +484,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 +942,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,7 +1072,7 @@ 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"
@@ -1086,6 +1088,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 +1101,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 +1795,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 +2322,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

@@ -173,7 +173,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 +340,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 +371,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

@@ -279,8 +279,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 +551,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

@@ -445,8 +445,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 +923,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

@@ -395,8 +395,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 +765,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 +839,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

@@ -103,7 +103,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 +135,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

@@ -150,7 +150,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 +270,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 +300,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

@@ -424,8 +424,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 +825,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 +899,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();

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,17 @@ 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);
}
_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

@@ -126,10 +126,12 @@ MacroConditionEdit::MacroConditionEdit(
_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;
@@ -204,7 +206,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;
@@ -245,7 +250,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,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

@@ -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);
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,20 @@ bool MacroSegment::Load(obs_data_t *obj)
_enabled = obs_data_get_bool(obj, "enabled");
}
_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;
}
@@ -167,24 +243,6 @@ bool MacroSegment::IsTempVarInUse(const std::string &id) const
return false;
}
void MacroSegment::CopyTempVarValuesFrom(const MacroSegment &other)
{
for (const auto &src : other._tempVariables) {
for (auto &dst : _tempVariables) {
if (dst.ID() != src.ID()) {
continue;
}
auto value = src.Value();
if (value) {
dst.SetValue(*value);
} else {
dst.InvalidateValue();
}
break;
}
}
}
void MacroSegment::SetTempVarValue(const std::string &id,
const std::string &value)
{
@@ -244,11 +302,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);
@@ -310,6 +395,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 *);
@@ -57,7 +71,6 @@ protected:
bool IsTempVarInUse(const std::string &id) const;
void SetTempVarValue(const std::string &id, const std::string &value);
void CopyTempVarValuesFrom(const MacroSegment &other);
template<typename T, typename = std::enable_if_t<
std::is_same<std::decay_t<T>, bool>::value>>
@@ -88,6 +101,7 @@ private:
// UI helper
bool _highlight = false;
bool _collapsed = false;
bool _varMappingExpanded = false;
bool _enabled = true;
// Custom header labels
@@ -100,6 +114,7 @@ private:
int _variableRefs = 0;
std::string _variableValue;
std::vector<TempVariable> _tempVariables;
std::vector<VarMapping> _varMappings;
friend class Macro;
};
@@ -119,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;
@@ -135,6 +153,8 @@ protected:
QLabel *_headerInfo;
QWidget *_frame;
QVBoxLayout *_contentLayout;
TempVarOutputMappingsWidget *_outputMappings;
QPushButton *_varMappingToggle;
bool _allWidgetsAreSetup = false;
private:

View File

@@ -113,6 +113,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;
@@ -428,14 +429,11 @@ bool Macro::RunActionsHelper(
}
if (action->Enabled()) {
action->LogAction();
std::shared_ptr<MacroAction> actionCopy;
action->WithLock([&action, &actionCopy]() {
actionCopy = action->Copy();
});
bool actionResult = actionCopy->PerformAction();
action->WithLock([&action, &actionCopy]() {
action->CopyTempVarValuesFrom(*actionCopy);
bool actionResult = false;
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

@@ -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

@@ -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

@@ -66,4 +66,15 @@ void Lockable::WithLock(const std::function<void()> &func)
func();
}
SuspendLock::SuspendLock(Lockable &lockable)
: _mtx(static_cast<std::mutex &>(lockable._mtx))
{
_mtx.unlock();
}
SuspendLock::~SuspendLock()
{
_mtx.lock();
}
} // namespace advss

View File

@@ -51,6 +51,24 @@ public:
private:
PerInstanceMutex _mtx;
friend class SuspendLock;
};
// RAII guard that temporarily releases a Lockable's per-segment lock.
// Use this inside PerformAction() / CheckCondition() to unblock the UI
// during long-running operations while still running on the original segment.
// The caller MUST be holding the lock (i.e. be inside WithLock) when
// constructing this object; the lock is re-acquired on destruction.
class EXPORT SuspendLock {
public:
SuspendLock(Lockable &lockable);
~SuspendLock();
SuspendLock(const SuspendLock &) = delete;
SuspendLock &operator=(const SuspendLock &) = delete;
private:
std::mutex &_mtx;
};
} // namespace advss

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

@@ -133,9 +133,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 +147,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 +168,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 +340,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 +373,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 +387,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);

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

@@ -95,7 +95,7 @@ static void waitHelper(std::unique_lock<std::mutex> *lock, Macro *macro,
}
}
void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
void MacroActionMedia::PerformActionHelper(obs_source_t *source)
{
obs_media_state state = obs_source_media_get_state(source);
@@ -130,6 +130,7 @@ void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
SeekToPercentage(source);
break;
case Action::WAIT_FOR_PLAYBACK_STOP: {
SuspendLock suspendLock(*this);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), source);
break;

View File

@@ -45,7 +45,7 @@ public:
SceneSelection _scene;
private:
void PerformActionHelper(obs_source_t *) const;
void PerformActionHelper(obs_source_t *);
void SeekToPercentage(obs_source_t *source) const;
static bool _registered;

View File

@@ -159,7 +159,10 @@ bool MacroActionPlayAudio::PerformAction()
if (_waitForCompletion) {
SetMacroAbortWait(false);
waitForPlaybackToEnd(GetMacro(), source, maxMs);
{
SuspendLock suspendLock(*this);
waitForPlaybackToEnd(GetMacro(), source, maxMs);
}
deactivatePlayback(source, wantsOutput);
return true;
}

View File

@@ -15,9 +15,20 @@ bool MacroActionRun::_registered = MacroActionFactory::Register(
bool MacroActionRun::PerformAction()
{
if (_wait) {
_procConfig.StartProcessAndWait(_timeout.Milliseconds());
SetTempVarValues();
// Snapshot config before releasing the lock for the blocking wait
auto procConfig = _procConfig;
const auto timeout = _timeout.Milliseconds();
{
SuspendLock suspendLock(*this);
procConfig.StartProcessAndWait(timeout);
}
SetTempVarValue("process.id", procConfig.GetProcessId());
SetTempVarValue("process.exitCode",
procConfig.GetProcessExitCode());
SetTempVarValue("process.stream.output",
procConfig.GetProcessOutputStream());
SetTempVarValue("process.stream.error",
procConfig.GetProcessErrorStream());
return true;
}

View File

@@ -259,6 +259,12 @@ void MacroActionSourceInteractionEdit::AcceptRecorded(
_stepList->SetSteps(_entryData->_steps);
}
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
{
const QSignalBlocker b(this);
UpdateEntryData();
}
void MacroActionSourceInteractionEdit::SetCurrentStepEditor(int row)
{
if (_stepEditor) {

View File

@@ -62,6 +62,9 @@ private slots:
signals:
void HeaderInfoChanged(const QString &);
protected:
void showEvent(QShowEvent *event) override;
private:
void SetCurrentStepEditor(int row);

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

@@ -57,8 +57,11 @@ bool MacroActionWait::PerformAction()
std::chrono::milliseconds((int)(sleepDuration * 1000));
SetMacroAbortWait(false);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), time);
{
SuspendLock suspendLock(*this);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), time);
}
return !MacroWaitShouldAbort();
}

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

@@ -112,48 +112,56 @@ static URLInfo getURLInfo(const std::string &input, bool keepParams)
bool MacroActionHttp::PerformAction()
{
// Capture all config while holding the segment lock
const auto [host, path] = getURLInfo(_url, !_setParams);
httplib::Client cli(host);
setTimeout(cli, _timeout);
const auto params = _setParams ? getParams(_params) : httplib::Params();
const auto headers = _setHeaders ? getHeaders(_headers)
: httplib::Headers();
const auto method = _method;
const std::string body = _body;
const std::string contentType = _contentType;
// Release the segment lock for the blocking network call
httplib::Result response;
switch (_method) {
case MacroActionHttp::Method::GET:
response = cli.Get(path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Post(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Put(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Patch(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Delete(pathWithParam, headers, _body, _contentType);
break;
}
default:
break;
{
SuspendLock suspendLock(*this);
switch (method) {
case MacroActionHttp::Method::GET:
response = cli.Get(path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Post(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Put(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Patch(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Delete(pathWithParam, headers, body,
contentType);
break;
}
default:
break;
}
}
if (VerboseLoggingEnabled() && !response) {

View File

@@ -35,6 +35,7 @@ bool MacroActionScript::PerformAction()
return true;
}
SuspendLock suspendLock(static_cast<MacroAction &>(*this));
(void)SendTriggerSignal();
return true;
}

View File

@@ -36,6 +36,7 @@ bool MacroConditionScript::CheckCondition()
return false;
}
SuspendLock suspendLock(static_cast<MacroCondition &>(*this));
return SendTriggerSignal();
}

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

@@ -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

@@ -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());
}