Compare commits

...

53 Commits

Author SHA1 Message Date
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
WarmUpTill
acfc7d605b Add "Get macro info" 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-03 13:44:34 +02:00
WarmUpTill
df42538319 Fix temp var values of actions not being accessible
This a bug introduced with ec3052c823.
2026-04-03 13:44:34 +02:00
WarmUpTill
b02259e926 Add "Source interact" action 2026-04-03 09:41:09 +02:00
WarmUpTill
2b6bd9fb51 Add helpers to add widgets and separators to ListControls 2026-04-03 09:41:09 +02:00
WarmUpTill
aadedbb610 Enable setting min and max list size 2026-04-03 09:41:09 +02:00
WarmUpTill
d15a324137 Add placeholder text support 2026-04-03 09:41:09 +02:00
WarmUpTill
119fac17bf Cleanup
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
* Member access
* Refresh settings on show to ensure "repeat" changes are visible
2026-04-02 22:39:12 +02:00
WarmUpTill
088881b674 Add HTTP condition
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-02 17:26:16 +02:00
WarmUpTill
f008d5367b Adjust layout of HTTP action 2026-04-02 17:26:16 +02:00
WarmUpTill
1ace185a57 Add action to play sound 2026-04-02 17:25:41 +02:00
WarmUpTill
a01d26e25d Don't block UI while running actions 2026-04-02 17:25:41 +02:00
WarmUpTill
7e2ef9aacc Add tests for "File" 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-01 11:13:01 +02:00
WarmUpTill
4a98d064f7 Enable testing 2026-04-01 11:13:01 +02:00
WarmUpTill
b0d6cea52a Remove deprecated functionality from "File" condition 2026-04-01 11:13:01 +02:00
WarmUpTill
d6bf9ed101 Fix date / content changed check triggering on plugin start 2026-04-01 11:13:01 +02:00
WarmUpTill
7e940e515b Add option to suppress crash dialog
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-03-31 23:04:35 +02:00
WarmUpTill
29e1ff0754 Adjust video condition temp vars
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-03-30 21:20:00 +02:00
WarmUpTill
6309100cc4 Rework pattern count temp var 2026-03-30 21:20:00 +02:00
WarmUpTill
ec3052c823 Enable checking if temp var is in use 2026-03-30 21:20:00 +02:00
WarmUpTill
a28b800228
Remove Snap installation instructions from README
Some checks failed
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
Removed Snap package installation instructions for OBS Studio.
2026-03-27 18:36:35 +01:00
WarmUpTill
713a64ab45 Fix crash when using math helpers concurrently
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-03-25 19:38:22 +01:00
WarmUpTill
4d5ffe38fc Fix race conditions when accessing variables 2026-03-25 19:38:22 +01:00
WarmUpTill
85460157b1 Fix title of script open dialog 2026-03-25 19:38:22 +01:00
WarmUpTill
35e5b7620e Add option to "release the T-Bar"
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-03-23 19:05:49 +01:00
WarmUpTill
e4c06b14a7 Cleanup 2026-03-23 19:05:49 +01:00
WarmUpTill
7fe3258269 Add support for changing T-bar value 2026-03-23 19:05:49 +01:00
WarmUpTill
ec19b4d4fb Add option to add new recording chapter
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-03-22 21:24:59 +01:00
WarmUpTill
9301ead060 Add chat settings toggles
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-03-21 22:52:27 +01:00
WarmUpTill
bc29ece526 Cleanup and layout adjustments 2026-03-21 22:52:27 +01:00
WarmUpTill
4158c7a363 Add Twitch actions
* Send shoutout
* Cancel raid
* Enable/disable shield mode
* Enable/disable branded content
* Snooze next ad
2026-03-21 22:52:27 +01:00
WarmUpTill
944d1059da Add user moderation actions 2026-03-21 22:52:27 +01:00
WarmUpTill
bf18d8e106 Validate that user id matches token 2026-03-21 22:52:27 +01:00
WarmUpTill
bf216e0917 Fix subscription handling and improve logging
* Revocations were not handled properly
* Active subscriptions were not cleared on reconnect
* Migration client handling could cause crash
* Add error logging
2026-03-21 22:52:27 +01:00
WarmUpTill
2405b6dbbf Add "Next Macro" temp var to Sequence actions when setting index
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (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-03-21 00:05:58 +01:00
WarmUpTill
e64f2d195e Add temp vars to "Sequence" and "Random" action for executed macro 2026-03-21 00:05:58 +01:00
WarmUpTill
f66bec8caf Add option to expose current settings of source as temp var 2026-03-20 23:40:05 +01:00
WarmUpTill
3eb79e3adb Scroll to new macro segments
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
2026-03-19 20:21:08 +01:00
WarmUpTill
07e2ac3ca0 Fix macro list / macro edit area splitter resizing
When horizontally large widgets (e.g. the Window condition) were part of
the currently selected macro moving the splitter would result in it
either fully hiding the macro list or the macro edit area.

Now the list and edit area can be resized smoothly.
2026-03-19 20:21:08 +01:00
WarmUpTill
8e2c466c2d Rework libproc2 API version check
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 version was not behaving as expected for flatpak builds
2026-03-15 16:27:01 +01:00
WarmUpTill
cc68e2366c Fix file selection displaying resolved variable values
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-03-15 13:17:06 +01:00
WarmUpTill
7a0e08b0d8 Adapt to enable testing and add more tests
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-03-14 13:34:50 +01:00
WarmUpTill
be8744f0d0 Add action trigger modes
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
* always -> same as old behavior, if "on change" was disabled
* results changes -> same as old behavior, if "on change" was enabled
* any condition changes
* any condition changes and evaluates to true
2026-03-12 20:45:57 +01:00
WarmUpTill
d4425df694 Add FirstRunWizard 2026-03-12 20:45:57 +01:00
WarmUpTill
70e5f6002d Cleanup 2026-03-12 20:45:57 +01:00
WarmUpTill
ff98ec36d6 Show dialogs after OBS_FRONTEND_EVENT_FINISHED_LOADING is fired
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
2026-02-28 13:09:27 +01:00
WarmUpTill
4966802f14 Fix first action of paused macros being executed
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
2026-02-26 21:20:37 +01:00
kak hil imup
b23a90557f Fixed get_filename_component command call
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-02-22 20:51:48 +01:00
WarmUpTill
d6ea815b85 Expose "Similarity Rating" as temp var when pattern matching 2026-02-22 13:27:20 +01:00
153 changed files with 8659 additions and 1861 deletions

View File

@ -204,6 +204,8 @@ target_sources(
lib/utils/file-selection.hpp lib/utils/file-selection.hpp
lib/utils/filter-combo-box.cpp lib/utils/filter-combo-box.cpp
lib/utils/filter-combo-box.hpp lib/utils/filter-combo-box.hpp
lib/utils/first-run-wizard.cpp
lib/utils/first-run-wizard.hpp
lib/utils/help-icon.hpp lib/utils/help-icon.hpp
lib/utils/help-icon.cpp lib/utils/help-icon.cpp
lib/utils/item-selection-helpers.cpp lib/utils/item-selection-helpers.cpp
@ -454,14 +456,29 @@ else()
endif() endif()
if(PROCPS2_INCLUDE_DIR) if(PROCPS2_INCLUDE_DIR)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(libproc2 IMPORTED_TARGET libproc2<4.0.5 QUIET)
if(libproc2_FOUND)
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_USE_INFO)
endif()
pkg_check_modules(libproc2 REQUIRED IMPORTED_TARGET libproc2) pkg_check_modules(libproc2 REQUIRED IMPORTED_TARGET libproc2)
set(PROC_INCLUDE_DIR "${PROCPS2_INCLUDE_DIR}") set(PROC_INCLUDE_DIR "${PROCPS2_INCLUDE_DIR}")
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_AVAILABLE) target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_AVAILABLE)
set(PROCESS_CONDITION_SUPPORTED 1) set(PROCESS_CONDITION_SUPPORTED 1)
# Check if PIDS_VAL takes 4 arguments (old API, pre-4.0.5) or 3 (new API)
include(CheckCSourceCompiles)
set(CMAKE_REQUIRED_INCLUDES "${PROCPS2_INCLUDE_DIR}")
set(CMAKE_REQUIRED_LIBRARIES proc2)
check_c_source_compiles(
"
#include <libproc2/pids.h>
int main(void) {
struct pids_stack *s = 0;
struct pids_info *i = 0;
(void)PIDS_VAL(0, str, s, i);
return 0;
}
"
PROCPS2_PIDS_VAL_TAKES_INFO)
if(PROCPS2_PIDS_VAL_TAKES_INFO)
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_USE_INFO)
endif()
endif() endif()
if(NOT DEFINED PROCESS_CONDITION_SUPPORTED) if(NOT DEFINED PROCESS_CONDITION_SUPPORTED)
message( message(

View File

@ -14,11 +14,6 @@ On Linux the plugin is available via the **Flatpak** package manager for users w
flatpak install com.obsproject.Studio.Plugin.SceneSwitcher flatpak install com.obsproject.Studio.Plugin.SceneSwitcher
``` ```
The **Snap** package manager offers an OBS Studio installation which is bundled with the plugin:
```
sudo snap install obs-studio
```
More information can be found [here](https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation). More information can be found [here](https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation).
## Contributing ## Contributing

View File

@ -64,7 +64,7 @@ function(_git_find_closest_git_dir _start_dir _git_dir_var)
while(NOT EXISTS "${git_dir}") while(NOT EXISTS "${git_dir}")
# .git dir not found, search parent directories # .git dir not found, search parent directories
set(git_previous_parent "${cur_dir}") set(git_previous_parent "${cur_dir}")
get_filename_component(cur_dir ${cur_dir} DIRECTORY) get_filename_component(cur_dir "${cur_dir}" DIRECTORY)
if(cur_dir STREQUAL git_previous_parent) if(cur_dir STREQUAL git_previous_parent)
# We have reached the root directory, we are not in git # We have reached the root directory, we are not in git
set(${_git_dir_var} set(${_git_dir_var}

View File

@ -76,7 +76,6 @@ AdvSceneSwitcher.macroTab.name="Name:"
AdvSceneSwitcher.macroTab.run="Makro ausführen" AdvSceneSwitcher.macroTab.run="Makro ausführen"
AdvSceneSwitcher.macroTab.runFail="Ausführen von \"%1\" fehlgeschlagen!\nEntweder ist eine der Aktionen fehlgeschlagen oder das Makro wird bereits ausgeführt.\nSoll die aktuelle Ausführung gestoppt werden?" AdvSceneSwitcher.macroTab.runFail="Ausführen von \"%1\" fehlgeschlagen!\nEntweder ist eine der Aktionen fehlgeschlagen oder das Makro wird bereits ausgeführt.\nSoll die aktuelle Ausführung gestoppt werden?"
AdvSceneSwitcher.macroTab.runInParallel="Parallel zu anderen Makros ausführen" AdvSceneSwitcher.macroTab.runInParallel="Parallel zu anderen Makros ausführen"
AdvSceneSwitcher.macroTab.onChange="Nur bei Änderung ausführen"
AdvSceneSwitcher.macroTab.defaultname="Makro %1" AdvSceneSwitcher.macroTab.defaultname="Makro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Gruppe %1" AdvSceneSwitcher.macroTab.defaultGroupName="Gruppe %1"
AdvSceneSwitcher.macroTab.removeGroupPopup.text="Sicher, dass \"%1\" und alle zugehörigen Elemente gelöscht werden?" AdvSceneSwitcher.macroTab.removeGroupPopup.text="Sicher, dass \"%1\" und alle zugehörigen Elemente gelöscht werden?"
@ -146,11 +145,7 @@ AdvSceneSwitcher.condition.file="Datei"
AdvSceneSwitcher.condition.file.type.match="entspricht" AdvSceneSwitcher.condition.file.type.match="entspricht"
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert" AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert" AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
AdvSceneSwitcher.condition.file.remote="Entfernte Datei" AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.file.local="Lokale Datei"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medien" AdvSceneSwitcher.condition.media="Medien"
AdvSceneSwitcher.condition.media.source="Quelle" AdvSceneSwitcher.condition.media.source="Quelle"
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in" AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
@ -216,8 +211,8 @@ AdvSceneSwitcher.condition.record.state.start="Aufnahme läuft"
AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert" AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert"
AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt" AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt"
AdvSceneSwitcher.condition.process="Prozess" AdvSceneSwitcher.condition.process="Prozess"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert" AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.entry.focus="Aktueller Vordergrundprozess: {{focusProcess}}" AdvSceneSwitcher.condition.process.layout.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="Leerlauf" AdvSceneSwitcher.condition.idle="Leerlauf"
AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}" AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin-Status" AdvSceneSwitcher.condition.pluginState="Plugin-Status"
@ -413,7 +408,6 @@ AdvSceneSwitcher.action.recording.type.unpause="Aufnahme nicht mehr pausieren"
AdvSceneSwitcher.action.recording.type.split="Aufnahme-Datei teilen" AdvSceneSwitcher.action.recording.type.split="Aufnahme-Datei teilen"
AdvSceneSwitcher.action.recording.pause.hint="Bitte beachten, dass je nach Aufnahmeeinstellung die Aufnahme möglicherweise nicht unterbrochen werden kann" AdvSceneSwitcher.action.recording.pause.hint="Bitte beachten, dass je nach Aufnahmeeinstellung die Aufnahme möglicherweise nicht unterbrochen werden kann"
AdvSceneSwitcher.action.recording.split.hint="Vergewissern, dass die automatische Dateiaufteilung in den OBS-Einstellungen aktiviert ist!" AdvSceneSwitcher.action.recording.split.hint="Vergewissern, dass die automatische Dateiaufteilung in den OBS-Einstellungen aktiviert ist!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Replay Buffer" AdvSceneSwitcher.action.replay="Replay Buffer"
AdvSceneSwitcher.action.replay.saveWarn="Warnung: Ein zu häufiges Speichern kann dazu führen, dass der Replay Buffer nicht gespeichert wird!" AdvSceneSwitcher.action.replay.saveWarn="Warnung: Ein zu häufiges Speichern kann dazu führen, dass der Replay Buffer nicht gespeichert wird!"
AdvSceneSwitcher.action.replay.type.stop="Replay Buffer stoppen" AdvSceneSwitcher.action.replay.type.stop="Replay Buffer stoppen"
@ -506,9 +500,9 @@ AdvSceneSwitcher.action.transition.type.scene="Szenenübergang"
AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben" AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben"
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen" AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken" AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.layout.type="Anpassen {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
AdvSceneSwitcher.action.timer="Timer" AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pausieren" AdvSceneSwitcher.action.timer.type.pause="Pausieren"
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen" AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"
@ -835,8 +829,6 @@ AdvSceneSwitcher.status.inactive="Inaktiv"
AdvSceneSwitcher.running="Plugin läuft" AdvSceneSwitcher.running="Plugin läuft"
AdvSceneSwitcher.stopped="Plugin gestoppt" AdvSceneSwitcher.stopped="Plugin gestoppt"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Dies scheint das erste Mal zu sein, dass der Erweiterte Szenenwechsler gestartet wurde.<br>Bitte schaue ins <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> für eine Liste von Anleitungen und Beispielen.<br>Nicht zögern und Fragen im <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">Thread</span></a> des Plugins im OBS-Forum stellen!!</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="Die Entwicklung für dieses Tab wurde gestoppt!\nBitte stattdessen zur Verwendung des Makros-Tab übergehen.\nDieser Hinweis kann im Allgemein-Tab deaktiviert werden." AdvSceneSwitcher.deprecatedTabWarning="Die Entwicklung für dieses Tab wurde gestoppt!\nBitte stattdessen zur Verwendung des Makros-Tab übergehen.\nDieser Hinweis kann im Allgemein-Tab deaktiviert werden."
AdvSceneSwitcher.unit.milliseconds="Millisekunden" AdvSceneSwitcher.unit.milliseconds="Millisekunden"

View File

@ -40,6 +40,7 @@ AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Show system t
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints" AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints"
AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus" AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailure="Display warning if plugins cannot be loaded" AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailure="Display warning if plugins cannot be loaded"
AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog="Do not ask to keep the plugin stopped after an unclean shutdown"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage="<html><body>Loading of the following plugin libraries was unsuccessful, which could result in some Advanced Scene Switcher functions not being available:%1Check the OBS logs for details.<br>This message can be disabled on the General tab.</body></html>" AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage="<html><body>Loading of the following plugin libraries was unsuccessful, which could result in some Advanced Scene Switcher functions not being available:%1Check the OBS logs for details.<br>This message can be disabled on the General tab.</body></html>"
AdvSceneSwitcher.generalTab.generalBehavior.warnCorruptedInstallMessage="The plugin installation seems to be corrupted and might crash!\nPlease make sure the plugin was installed correctly!" AdvSceneSwitcher.generalTab.generalBehavior.warnCorruptedInstallMessage="The plugin installation seems to be corrupted and might crash!\nPlease make sure the plugin was installed correctly!"
AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can be represented via macros" AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can be represented via macros"
@ -177,7 +178,12 @@ AdvSceneSwitcher.macroTab.run.tooltip="Run all macro actions regardless of condi
AdvSceneSwitcher.macroTab.runElse="Run macro (else)" AdvSceneSwitcher.macroTab.runElse="Run macro (else)"
AdvSceneSwitcher.macroTab.runFail="Running \"%1\" failed!\nEither one of the actions failed or the macro is running already.\nDo you want to stop it?" AdvSceneSwitcher.macroTab.runFail="Running \"%1\" failed!\nEither one of the actions failed or the macro is running already.\nDo you want to stop it?"
AdvSceneSwitcher.macroTab.runInParallel="Run macro in parallel to other macros" AdvSceneSwitcher.macroTab.runInParallel="Run macro in parallel to other macros"
AdvSceneSwitcher.macroTab.onChange="Perform actions only on condition change" AdvSceneSwitcher.macroTab.actionTriggerMode.label="Perform actions:"
AdvSceneSwitcher.macroTab.actionTriggerMode.tooltip="Controls when the actions of this macro are performed.\n\n\"Always\" - actions are performed every time the conditions are met.\n\"Macro result changed\" - actions are only performed when the macro transitions between matching and not matching.\n\"Any condition changed\" - actions are only performed when any individual condition changes its result.\n\"Any condition triggered\" - actions are only performed when any individual condition changes from false to true."
AdvSceneSwitcher.macroTab.actionTriggerMode.always="always"
AdvSceneSwitcher.macroTab.actionTriggerMode.onOverallChange="only when result changes"
AdvSceneSwitcher.macroTab.actionTriggerMode.onAnyConditionChange="only when any condition changes"
AdvSceneSwitcher.macroTab.actionTriggerMode.onAnyConditionTriggered="only when any condition becomes true"
AdvSceneSwitcher.macroTab.defaultname="Macro %1" AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Group %1" AdvSceneSwitcher.macroTab.defaultGroupName="Group %1"
AdvSceneSwitcher.macroTab.macroNameExists="The name \"%1\" is already used by a macro." AdvSceneSwitcher.macroTab.macroNameExists="The name \"%1\" is already used by a macro."
@ -363,11 +369,7 @@ AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
AdvSceneSwitcher.condition.file.type.exists="exists" AdvSceneSwitcher.condition.file.type.exists="exists"
AdvSceneSwitcher.condition.file.type.isFile="is a file" AdvSceneSwitcher.condition.file.type.isFile="is a file"
AdvSceneSwitcher.condition.file.type.isFolder="is a folder" AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
AdvSceneSwitcher.condition.file.remote="Remote file" AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.file.local="Local file"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Media" AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.checkType.state="State matches" AdvSceneSwitcher.condition.media.checkType.state="State matches"
AdvSceneSwitcher.condition.media.checkType.time="Time restriction matches" AdvSceneSwitcher.condition.media.checkType.time="Time restriction matches"
@ -482,8 +484,9 @@ AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
AdvSceneSwitcher.condition.record.state.duration="Recording duration is longer than" AdvSceneSwitcher.condition.record.state.duration="Recording duration is longer than"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}" AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Process" AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}is running{{focused}}and is focused" AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process:{{focusProcess}}" 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="Idle"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}" AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin state" AdvSceneSwitcher.condition.pluginState="Plugin state"
@ -671,6 +674,34 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions" AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:" AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:" AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
AdvSceneSwitcher.condition.http="HTTP"
AdvSceneSwitcher.condition.http.layout="Receive{{method}}request on{{server}}"
AdvSceneSwitcher.condition.http.layout.path="Path:{{path}}{{regex}}"
AdvSceneSwitcher.condition.http.layout.body="Body:{{body}}{{regex}}"
AdvSceneSwitcher.condition.http.method.any="any"
AdvSceneSwitcher.condition.http.method.get="GET"
AdvSceneSwitcher.condition.http.method.post="POST"
AdvSceneSwitcher.condition.http.method.put="PUT"
AdvSceneSwitcher.condition.http.method.patch="PATCH"
AdvSceneSwitcher.condition.http.method.delete="DELETE"
AdvSceneSwitcher.httpServer.select="Select HTTP server"
AdvSceneSwitcher.httpServer.add="Add HTTP server"
AdvSceneSwitcher.httpServer.configure="Configure HTTP server"
AdvSceneSwitcher.httpServer.invalid="Invalid HTTP server"
AdvSceneSwitcher.httpServer.name="Name:"
AdvSceneSwitcher.httpServer.port="Port:"
AdvSceneSwitcher.httpServer.startOnLoad="Start on load"
AdvSceneSwitcher.httpServer.status.listening="Listening"
AdvSceneSwitcher.httpServer.status.stopped="Stopped"
AdvSceneSwitcher.httpServerTab.title="HTTP Servers"
AdvSceneSwitcher.httpServerTab.help="No HTTP servers configured.\nAdd one to receive incoming HTTP requests in conditions."
AdvSceneSwitcher.httpServerTab.addButton.tooltip="Add HTTP server"
AdvSceneSwitcher.httpServerTab.removeButton.tooltip="Remove HTTP server"
AdvSceneSwitcher.httpServerTab.name.header="Name"
AdvSceneSwitcher.httpServerTab.port.header="Port"
AdvSceneSwitcher.httpServerTab.status.header="Status"
AdvSceneSwitcher.httpServerTab.removeSingle.text="Are you sure you want to remove \"%1\"?"
AdvSceneSwitcher.httpServerTab.removeMultiple.text="Are you sure you want to remove %1 HTTP servers?"
AdvSceneSwitcher.condition.temporaryVariable="Macro property" AdvSceneSwitcher.condition.temporaryVariable="Macro property"
AdvSceneSwitcher.condition.variable="Variable" AdvSceneSwitcher.condition.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="equals" AdvSceneSwitcher.condition.variable.type.compare="equals"
@ -874,6 +905,15 @@ AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Fade{{fadeTypes}}{{rate}}per se
AdvSceneSwitcher.action.audio.fade.wait="Wait for fade to complete." AdvSceneSwitcher.action.audio.fade.wait="Wait for fade to complete."
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade." AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}{{track}}" AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}{{track}}"
AdvSceneSwitcher.action.playAudio="Play Audio File"
AdvSceneSwitcher.action.playAudio.file.browse="Browse for audio file"
AdvSceneSwitcher.action.playAudio.layout.volume="Volume{{volumeDB}}"
AdvSceneSwitcher.action.playAudio.layout.monitor="Monitoring{{monitorTypes}}"
AdvSceneSwitcher.action.playAudio.wait="Wait for playback to complete"
AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not available"
AdvSceneSwitcher.action.playAudio.tracks="Tracks"
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
AdvSceneSwitcher.action.recording="Recording" AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording" AdvSceneSwitcher.action.recording.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording" AdvSceneSwitcher.action.recording.type.start="Start recording"
@ -882,9 +922,10 @@ AdvSceneSwitcher.action.recording.type.unpause="Unpause recording"
AdvSceneSwitcher.action.recording.type.split="Split recording file" AdvSceneSwitcher.action.recording.type.split="Split recording file"
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder" AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename formatting" AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename formatting"
AdvSceneSwitcher.action.recording.type.addChapter="Add chapter"
AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording" AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording"
AdvSceneSwitcher.action.recording.split.hint="Make sure to enable automatic file splitting in the OBS settings first!" AdvSceneSwitcher.action.recording.split.hint="Make sure to enable automatic file splitting in the OBS settings first!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}" AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{chapterName}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Replay buffer" AdvSceneSwitcher.action.replay="Replay buffer"
AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!" AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!"
AdvSceneSwitcher.action.replay.durationWarn="Warning: Changing the maximum replay time will only apply the next time the replay buffer is started!" AdvSceneSwitcher.action.replay.durationWarn="Warning: Changing the maximum replay time will only apply the next time the replay buffer is started!"
@ -944,6 +985,8 @@ AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog
AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog" AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog"
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog" AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties dialog" AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties dialog"
AdvSceneSwitcher.action.source.type.getSetting="Get setting value"
AdvSceneSwitcher.action.source.type.getSettings="Get settings JSON"
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}" AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}"
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}" AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\"" AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
@ -966,6 +1009,49 @@ AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.source.refresh="Refresh" AdvSceneSwitcher.action.source.refresh="Refresh"
AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value." AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value."
AdvSceneSwitcher.action.source.dialog.accept="Accept changes" AdvSceneSwitcher.action.source.dialog.accept="Accept changes"
AdvSceneSwitcher.action.sourceInteraction="Source Interaction"
AdvSceneSwitcher.action.sourceInteraction.source="Source"
AdvSceneSwitcher.action.sourceInteraction.noSelection="Select a step to edit it"
AdvSceneSwitcher.action.sourceInteraction.record="Record interaction ..."
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove="Mouse move (%1, %2)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick="Mouse %1 %2 (%3, %4)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount=" x%1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel="Mouse wheel (%1, %2) dx=%3 dy=%4"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress="Key %1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText="Key %1 '%2'"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText="Type \"%1\""
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait="Wait %1 ms"
AdvSceneSwitcher.action.sourceInteraction.step.edit.type="Type"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove="Mouse move"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick="Mouse click"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel="Mouse wheel"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress="Key press"
AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText="Type text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wait="Wait"
AdvSceneSwitcher.action.sourceInteraction.step.edit.x="X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.y="Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.button="Button"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp="Mouse up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount="Click count"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx="Delta X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy="Delta Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey="Virtual key"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp="Key up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.text="Text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs="Duration (ms)"
AdvSceneSwitcher.action.sourceInteraction.button.left="Left"
AdvSceneSwitcher.action.sourceInteraction.button.middle="Middle"
AdvSceneSwitcher.action.sourceInteraction.button.right="Right"
AdvSceneSwitcher.action.sourceInteraction.record.title="Record Source Interactions"
AdvSceneSwitcher.action.sourceInteraction.record.start="Start recording"
AdvSceneSwitcher.action.sourceInteraction.record.stop="Stop recording"
AdvSceneSwitcher.action.sourceInteraction.record.accept="Accept"
AdvSceneSwitcher.action.sourceInteraction.record.placeholder="Start recording and interact with the source to capture steps"
AdvSceneSwitcher.action.sourceInteraction.record.invalidSource="No valid source is selected.\nPlease select a source that supports interaction before recording."
AdvSceneSwitcher.action.media="Media" AdvSceneSwitcher.action.media="Media"
AdvSceneSwitcher.action.media.type.play="Play" AdvSceneSwitcher.action.media.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause" AdvSceneSwitcher.action.media.type.pause="Pause"
@ -1000,6 +1086,7 @@ AdvSceneSwitcher.action.macro.type.stop="Stop actions"
AdvSceneSwitcher.action.macro.type.disableAction="Disable action" AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
AdvSceneSwitcher.action.macro.type.enableAction="Enable action" AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action" AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
AdvSceneSwitcher.action.macro.type.getInfo="Get macro info"
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro" AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index" AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label" AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
@ -1082,13 +1169,16 @@ AdvSceneSwitcher.action.studioMode.type.enable="Enable studio mode"
AdvSceneSwitcher.action.studioMode.type.disable="Disable studio mode" AdvSceneSwitcher.action.studioMode.type.disable="Disable studio mode"
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}" AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
AdvSceneSwitcher.action.transition="Transition" AdvSceneSwitcher.action.transition="Transition"
AdvSceneSwitcher.action.transition.type.scene="scene transition" AdvSceneSwitcher.action.transition.type.scene="Modify scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override" AdvSceneSwitcher.action.transition.type.sceneOverride="Modify scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition" AdvSceneSwitcher.action.transition.type.sourceShow="Modify source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition" AdvSceneSwitcher.action.transition.type.sourceHide="Modify source hide transition"
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.type.tbar="Modify T-Bar position"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}" AdvSceneSwitcher.action.transition.type.releaseTbar="Release T-Bar"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds" AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Set transition type to{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.transition.layout.tbar="Set T-Bar position to{{tbarPosition}}"
AdvSceneSwitcher.action.timer="Timer" AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pause" AdvSceneSwitcher.action.timer.type.pause="Pause"
AdvSceneSwitcher.action.timer.type.continue="Continue" AdvSceneSwitcher.action.timer.type.continue="Continue"
@ -1248,23 +1338,52 @@ AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Set stream catego
AdvSceneSwitcher.action.twitch.type.channel.info.tags.set="Set stream tags" AdvSceneSwitcher.action.twitch.type.channel.info.tags.set="Set stream tags"
AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language" AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language"
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid" AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
AdvSceneSwitcher.action.twitch.type.raid.end="Cancel raid"
AdvSceneSwitcher.action.twitch.type.shoutout.send="Send shoutout"
AdvSceneSwitcher.action.twitch.type.shieldMode.start="Enable shield mode"
AdvSceneSwitcher.action.twitch.type.shieldMode.end="Disable shield mode"
AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial" AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial"
AdvSceneSwitcher.action.twitch.type.commercial.snooze="Snooze next ad"
AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker" AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip" AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement" AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-only mode" AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode" AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.enable="Enable chat's follower-only mode"
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.disable="Disable chat's follower-only mode"
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.enable="Enable chat's subscriber-only mode"
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.disable="Disable chat's subscriber-only mode"
AdvSceneSwitcher.action.twitch.type.chat.slowMode.enable="Enable chat's slow mode"
AdvSceneSwitcher.action.twitch.type.chat.slowMode.disable="Disable chat's slow mode"
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.enable="Enable chat's non-moderator message delay"
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.disable="Disable chat's non-moderator message delay"
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.enable="Enable chat's unique message mode"
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.disable="Disable chat's unique message mode"
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Send chat message" AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Send chat message"
AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information" AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information"
AdvSceneSwitcher.action.twitch.type.user.ban="Ban user"
AdvSceneSwitcher.action.twitch.type.user.unban="Unban user"
AdvSceneSwitcher.action.twitch.type.user.block="Block user"
AdvSceneSwitcher.action.twitch.type.user.unblock="Unblock user"
AdvSceneSwitcher.action.twitch.type.user.moderator.add="Add user as moderator"
AdvSceneSwitcher.action.twitch.type.user.moderator.delete="Remove user as moderator"
AdvSceneSwitcher.action.twitch.type.user.vip.add="Add user as VIP"
AdvSceneSwitcher.action.twitch.type.user.vip.delete="Remove user as VIP"
AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information" AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information"
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information" AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control" AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}" AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{nonModDelayDuration}}{{channel}}{{pointsReward}}"
AdvSceneSwitcher.action.twitch.layout.chat="Using account{{account}}{{actions}}on{{channel}}" AdvSceneSwitcher.action.twitch.layout.chat="Using account{{account}}{{actions}}on{{channel}}"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="Using account{{account}}{{actions}}of{{channel}}" AdvSceneSwitcher.action.twitch.layout.channel.getInfo="Using account{{account}}{{actions}}of{{channel}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="Using account{{account}}{{actions}}for{{userInfoQueryType}}{{userLogin}}{{userId}}" AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}" AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="for{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="Using account{{account}}{{actions}}timeout{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}reason{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="Using account{{account}}{{actions}}for channel{{channel}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="Enter title" AdvSceneSwitcher.action.twitch.title.title="Enter title"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker" AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip" AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
@ -1285,6 +1404,8 @@ AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the l
AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached" AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached"
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags." AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels" AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels"
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.enable="Enable branded content"
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.disable="Disable branded content"
AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights." AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights."
AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs." AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs."
AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences." AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences."
@ -1390,6 +1511,7 @@ AdvSceneSwitcher.askBackup="Detected a new version of the Advanced Scene Switche
AdvSceneSwitcher.askForMacro="Select macro{{macroSelection}}" AdvSceneSwitcher.askForMacro="Select macro{{macroSelection}}"
AdvSceneSwitcher.crashDetected="OBS did not shut down cleanly (for example, due to a crash or freeze).\n\nThe Advanced Scene Switcher plugin would normally start automatically.\nWould you like to keep it stopped for now?" AdvSceneSwitcher.crashDetected="OBS did not shut down cleanly (for example, due to a crash or freeze).\n\nThe Advanced Scene Switcher plugin would normally start automatically.\nWould you like to keep it stopped for now?"
AdvSceneSwitcher.crashDetected.suppressCheckbox="Do not show this dialog again"
AdvSceneSwitcher.close="Close" AdvSceneSwitcher.close="Close"
AdvSceneSwitcher.browse="Browse" AdvSceneSwitcher.browse="Browse"
@ -1659,6 +1781,7 @@ AdvSceneSwitcher.script.language.lua="LUA"
AdvSceneSwitcher.script.language.select="--select language--" AdvSceneSwitcher.script.language.select="--select language--"
AdvSceneSwitcher.script.language.layout="Script language:{{language}}" AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
AdvSceneSwitcher.script.file.open="Open" AdvSceneSwitcher.script.file.open="Open"
AdvSceneSwitcher.script.file.select="Select Script File"
AdvSceneSwitcher.script.file.open.failed="Could not open script file!" AdvSceneSwitcher.script.file.open.failed="Could not open script file!"
AdvSceneSwitcher.script.file.warning.notFound="Warning: The given file path was not found!" AdvSceneSwitcher.script.file.warning.notFound="Warning: The given file path was not found!"
AdvSceneSwitcher.script.file.warning.openFail="Warning: Could not open the script file!" AdvSceneSwitcher.script.file.warning.openFail="Warning: Could not open the script file!"
@ -2176,8 +2299,21 @@ AdvSceneSwitcher.tempVar.macro.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed." AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed."
AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count" AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count"
AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of which the condition state was true." AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of which the condition state was true."
AdvSceneSwitcher.tempVar.macro.info.conditionCount="Condition count"
AdvSceneSwitcher.tempVar.macro.info.conditionCount.description="The number of conditions in the macro."
AdvSceneSwitcher.tempVar.macro.info.actionCount="Action count"
AdvSceneSwitcher.tempVar.macro.info.actionCount.description="The number of actions in the macro."
AdvSceneSwitcher.tempVar.macro.info.elseActionCount="Else-action count"
AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description="The number of else-actions in the macro."
AdvSceneSwitcher.tempVar.macro.info.paused="Paused"
AdvSceneSwitcher.tempVar.macro.info.paused.description="Whether the macro is currently paused. Value is \"true\" or \"false\"."
AdvSceneSwitcher.tempVar.macro.info.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.info.runCount.description="The number of times the macro has been executed."
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.name="Process name"
AdvSceneSwitcher.tempVar.process.path="Process path"
AdvSceneSwitcher.tempVar.run.process.id="Process ID" AdvSceneSwitcher.tempVar.run.process.id="Process ID"
AdvSceneSwitcher.tempVar.run.process.id.description="PID of the process assigned by the system." AdvSceneSwitcher.tempVar.run.process.id.description="PID of the process assigned by the system."
@ -2196,6 +2332,8 @@ AdvSceneSwitcher.tempVar.recording.durationSeconds.description="Recording durati
AdvSceneSwitcher.tempVar.recording.lastSavePath="Last save path" AdvSceneSwitcher.tempVar.recording.lastSavePath="Last save path"
AdvSceneSwitcher.tempVar.recording.lastSavePath.description="The file path of the last saved recording." AdvSceneSwitcher.tempVar.recording.lastSavePath.description="The file path of the last saved recording."
AdvSceneSwitcher.tempVar.video.similarity="Similarity Rating"
AdvSceneSwitcher.tempVar.video.similarity.description="Values range from 0 to 1, where 0 means dissimilar and 1 means highly similar."
AdvSceneSwitcher.tempVar.video.patternCount="Pattern count" AdvSceneSwitcher.tempVar.video.patternCount="Pattern count"
AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame." AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame."
AdvSceneSwitcher.tempVar.video.objectCount="Object count" AdvSceneSwitcher.tempVar.video.objectCount="Object count"
@ -2206,6 +2344,8 @@ AdvSceneSwitcher.tempVar.video.text="OCR text"
AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame." AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame."
AdvSceneSwitcher.tempVar.video.color="Average color" AdvSceneSwitcher.tempVar.video.color="Average color"
AdvSceneSwitcher.tempVar.video.color.description="The average RGB color in a given video input frame in HexArgb format." AdvSceneSwitcher.tempVar.video.color.description="The average RGB color in a given video input frame in HexArgb format."
AdvSceneSwitcher.tempVar.video.dominantColor="Dominant color"
AdvSceneSwitcher.tempVar.video.dominantColor.description="The most dominant RGB color in a given video input frame in HexArgb format.\nDetermined using k-means clustering, which groups similar colors and returns the center of the largest group.\nDue to the nature of the algorithm the result may vary slightly between evaluations."
AdvSceneSwitcher.tempVar.websocket.message="Received websocket message" AdvSceneSwitcher.tempVar.websocket.message="Received websocket message"
AdvSceneSwitcher.tempVar.websocket.message.description="The received websocket message, which matched the given pattern" AdvSceneSwitcher.tempVar.websocket.message.description="The received websocket message, which matched the given pattern"
@ -2335,9 +2475,14 @@ AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source." AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.http.status="Status code" AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.error="Error" AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown" AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
AdvSceneSwitcher.tempVar.http.method="Method"
AdvSceneSwitcher.tempVar.http.method.description="Received HTTP request method"
AdvSceneSwitcher.tempVar.http.path="Request path"
AdvSceneSwitcher.tempVar.http.path.description="Received HTTP request path"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.body.description="Received HTTP request body"
AdvSceneSwitcher.tempVar.mqtt.message="Message" AdvSceneSwitcher.tempVar.mqtt.message="Message"
@ -2347,6 +2492,14 @@ AdvSceneSwitcher.tempVar.cursor.y="Cursor position (Y)"
AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path" AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path"
AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved." AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved."
AdvSceneSwitcher.tempVar.sequence.macro="Macro"
AdvSceneSwitcher.tempVar.sequence.macro.description="The name of the macro executed by the \"Sequence\" action.\nIf no macro was executed the returned value is the empty string."
AdvSceneSwitcher.tempVar.sequence.nextMacro="Next Macro"
AdvSceneSwitcher.tempVar.sequence.nextMacro.description="The name of the macro which will be executed next after the index was set.\nIf no macro will be executed the returned value is the empty string."
AdvSceneSwitcher.tempVar.random.macro="Macro"
AdvSceneSwitcher.tempVar.random.macro.description="The name of the macro executed by the \"Random\" action.\nIf no macro was executed the returned value is the empty string."
AdvSceneSwitcher.selectScene="--select scene--" AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectCanvas="--select canvas--" AdvSceneSwitcher.selectCanvas="--select canvas--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene" AdvSceneSwitcher.selectPreviousScene="Previous Scene"
@ -2436,8 +2589,6 @@ AdvSceneSwitcher.status.inactive="Inactive"
AdvSceneSwitcher.running="Plugin running" AdvSceneSwitcher.running="Plugin running"
AdvSceneSwitcher.stopped="Plugin stopped" AdvSceneSwitcher.stopped="Plugin stopped"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>This seems to be the first time the Advanced Scene Switcher was started.<br>Please have a look at the <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> for a list of guides and examples.<br>Do not hesitate to ask questions in the plugin's <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">thread</span></a> on the OBS forums!</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="Development for this tab stopped!\nPlease consider transitioning to using Macros instead.\nThis hint can be disabled on the General tab." AdvSceneSwitcher.deprecatedTabWarning="Development for this tab stopped!\nPlease consider transitioning to using Macros instead.\nThis hint can be disabled on the General tab."
AdvSceneSwitcher.unit.milliseconds="milliseconds" AdvSceneSwitcher.unit.milliseconds="milliseconds"
@ -2477,6 +2628,37 @@ AdvSceneSwitcher.day.sunday="Sunday"
AdvSceneSwitcher.day.none="No day" AdvSceneSwitcher.day.none="No day"
AdvSceneSwitcher.day.any="Any day" AdvSceneSwitcher.day.any="Any day"
# First run wizard
FirstRunWizard.windowTitle="Advanced Scene Switcher - Setup Wizard"
FirstRunWizard.welcome.title="Welcome to Advanced Scene Switcher"
FirstRunWizard.welcome.subtitle="Let's create your first automation in a few easy steps."
FirstRunWizard.welcome.body="<p>Advanced Scene Switcher lets you build <b>Macros</b> - rules of the form:</p><blockquote><i>When [condition] -> perform [action]</i></blockquote><p>This wizard creates a macro that <b>switches to a chosen OBS scene whenever a specific window comes into focus</b>.</p><p>You can skip at any time. Re-open this wizard later from the <b>General</b> tab inside the Advanced Scene Switcher dialog.</p>"
FirstRunWizard.scene.title="Choose a Target Scene"
FirstRunWizard.scene.subtitle="Which OBS scene should become active when your chosen window comes into focus?"
FirstRunWizard.scene.label="Switch to scene:"
FirstRunWizard.window.title="Choose a Trigger Window"
FirstRunWizard.window.subtitle="Enter the title of the window that should trigger the scene switch. Partial matches are fine - \"Firefox\" will match any Firefox window."
FirstRunWizard.window.label="Window title contains:"
FirstRunWizard.window.placeholder="e.g. \"Firefox\", \"Visual Studio Code\""
FirstRunWizard.window.autoDetect="Auto-detect focused window"
FirstRunWizard.window.autoDetectTooltip="Click, then switch to the target window. The title will be captured automatically after 3 seconds."
FirstRunWizard.window.autoDetectCountdown="Detecting in %1 s - focus your window now..."
FirstRunWizard.window.hint="<small>Tip: a short partial title is more robust than the full title, which often changes when you switch tabs.</small>"
FirstRunWizard.review.title="Review Your Macro"
FirstRunWizard.review.subtitle="Click Back to make changes, or Finish to create the macro."
FirstRunWizard.review.summary="<b>Macro name:</b> Window -> %1<br><br><b>Condition:</b> Focused window title contains <i>\"%2\"</i> (case-insensitive)<br><br><b>Action:</b> Switch to scene <i>\"%1\"</i>"
FirstRunWizard.review.errorTitle="Macro Creation Failed"
FirstRunWizard.review.errorBody="The macro could not be created automatically.\n\nYou can create it manually on the Macros tab:\n Condition: Window -> contains \"%1\"\n Action: Switch scene -> \"%2\""
FirstRunWizard.done.title="You're all set!"
FirstRunWizard.done.subtitle="Your first macro has been created and is now active."
FirstRunWizard.done.body="<p>You can view and edit it on the <b>Macros</b> tab of the Advanced Scene Switcher dialog.</p><p>To learn more:</p><ul><li><a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\">Plugin wiki</a></li><li><a href=\"https://obsproject.com/forum/resources/automatic-scene-switching.395/\">OBS forum thread</a></li></ul>"
FirstRunWizard.openButton="Open Setup Wizard..."
# This secion is copied from the OBS locale files # This secion is copied from the OBS locale files
# OBS commonly shared locale # OBS commonly shared locale

View File

@ -72,7 +72,6 @@ AdvSceneSwitcher.macroTab.add="Agregar nueva macro"
AdvSceneSwitcher.macroTab.name="Nombre:" AdvSceneSwitcher.macroTab.name="Nombre:"
AdvSceneSwitcher.macroTab.run="Ejecutar macro" AdvSceneSwitcher.macroTab.run="Ejecutar macro"
AdvSceneSwitcher.macroTab.runInParallel="Ejecutar macro en paralelo a otras macros" AdvSceneSwitcher.macroTab.runInParallel="Ejecutar macro en paralelo a otras macros"
AdvSceneSwitcher.macroTab.onChange="Realizar acciones solo en el cambio de condición"
AdvSceneSwitcher.macroTab.defaultname="Macro %1" AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.copy="Crear copia" AdvSceneSwitcher.macroTab.copy="Crear copia"
AdvSceneSwitcher.macroTab.expandAll="Expandir todo" AdvSceneSwitcher.macroTab.expandAll="Expandir todo"
@ -118,9 +117,7 @@ AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Durante la tra
AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Durante la transición, verifique la escena de origen de la transición" AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Durante la transición, verifique la escena de origen de la transición"
AdvSceneSwitcher.condition.window="Ventana" AdvSceneSwitcher.condition.window="Ventana"
AdvSceneSwitcher.condition.file="Archivo" AdvSceneSwitcher.condition.file="Archivo"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}" AdvSceneSwitcher.condition.file.layout="Contenido de{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medios" AdvSceneSwitcher.condition.media="Medios"
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada" AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas" AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
@ -176,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.pause="Grabación en pausa"
AdvSceneSwitcher.condition.record.state.stop="Grabación detenida" AdvSceneSwitcher.condition.record.state.stop="Grabación detenida"
AdvSceneSwitcher.condition.process="Proceso" 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="Inactivo"
AdvSceneSwitcher.condition.idle.entry="No hay entradas de teclado o ratón durante {{duration}}" AdvSceneSwitcher.condition.idle.entry="No hay entradas de teclado o ratón durante {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado del complemento" AdvSceneSwitcher.condition.pluginState="Estado del complemento"
@ -335,7 +332,6 @@ AdvSceneSwitcher.action.recording.type.start="Iniciar grabación"
AdvSceneSwitcher.action.recording.type.pause="Pausar grabación" AdvSceneSwitcher.action.recording.type.pause="Pausar grabación"
AdvSceneSwitcher.action.recording.type.unpause="Reanudar grabación" AdvSceneSwitcher.action.recording.type.unpause="Reanudar grabación"
AdvSceneSwitcher.action.recording.pause.hint="Tenga en cuenta que, dependiendo de la configuración de grabación, es posible que no pueda pausar la grabación" AdvSceneSwitcher.action.recording.pause.hint="Tenga en cuenta que, dependiendo de la configuración de grabación, es posible que no pueda pausar la grabación"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Búfer de reproducción" AdvSceneSwitcher.action.replay="Búfer de reproducción"
AdvSceneSwitcher.action.replay.saveWarn="Advertencia: ¡Guardar con demasiada frecuencia puede hacer que el búfer de reproducción no se guarde realmente!" AdvSceneSwitcher.action.replay.saveWarn="Advertencia: ¡Guardar con demasiada frecuencia puede hacer que el búfer de reproducción no se guarde realmente!"
AdvSceneSwitcher.action.replay.type.stop="Detener el búfer de reproducción" AdvSceneSwitcher.action.replay.type.stop="Detener el búfer de reproducción"
@ -424,9 +420,9 @@ AdvSceneSwitcher.action.transition.type.scene="transición de escena"
AdvSceneSwitcher.action.transition.type.sceneOverride="anulación de transición de escena" AdvSceneSwitcher.action.transition.type.sceneOverride="anulación de transición de escena"
AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen" AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen"
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición" AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
AdvSceneSwitcher.action.transition.entry.line1="Modificar {{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.layout.type="Modificar {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Establecer el tipo de transición en {{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador" AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausa" AdvSceneSwitcher.action.timer.type.pause="Pausa"
AdvSceneSwitcher.action.timer.type.continue="Continuar" AdvSceneSwitcher.action.timer.type.continue="Continuar"
@ -681,8 +677,6 @@ AdvSceneSwitcher.status.inactive="Inactivo"
AdvSceneSwitcher.running="Iniciar complemento" AdvSceneSwitcher.running="Iniciar complemento"
AdvSceneSwitcher.stopped="Complemento de detención" AdvSceneSwitcher.stopped="Complemento de detención"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Esta parece ser la primera vez que se inicia el conmutador de escena avanzado.<br>Por favor, eche un vistazo a <a href=\"https:/ /github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> para obtener una lista de guías y ejemplos.<br>No dude en hacer preguntas en el complemento <a href=\"https://obsproject.com /forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">hilo</span></a> en los foros de OBS!</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="¡Se detuvo el desarrollo de esta pestaña!\nConsidere cambiar a macros en su lugar.\nEsta sugerencia se puede desactivar en la pestaña General". AdvSceneSwitcher.deprecatedTabWarning="¡Se detuvo el desarrollo de esta pestaña!\nConsidere cambiar a macros en su lugar.\nEsta sugerencia se puede desactivar en la pestaña General".
AdvSceneSwitcher.unit.milliseconds="milisegundos" AdvSceneSwitcher.unit.milliseconds="milisegundos"

View File

@ -78,7 +78,6 @@ AdvSceneSwitcher.macroTab.add="Ajouter une nouvelle macro"
AdvSceneSwitcher.macroTab.name="Nom :" AdvSceneSwitcher.macroTab.name="Nom :"
AdvSceneSwitcher.macroTab.run="Exécuter la macro" AdvSceneSwitcher.macroTab.run="Exécuter la macro"
AdvSceneSwitcher.macroTab.runInParallel="Exécuter la macro en parallèle avec d'autres macros" AdvSceneSwitcher.macroTab.runInParallel="Exécuter la macro en parallèle avec d'autres macros"
AdvSceneSwitcher.macroTab.onChange="Exécuter des actions uniquement en cas de changement de condition"
AdvSceneSwitcher.macroTab.defaultname="Macro %1" AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Groupe %1" AdvSceneSwitcher.macroTab.defaultGroupName="Groupe %1"
AdvSceneSwitcher.macroTab.removeSingleMacroPopup.text="Êtes-vous sûr de vouloir supprimer \"%1\" ?" AdvSceneSwitcher.macroTab.removeSingleMacroPopup.text="Êtes-vous sûr de vouloir supprimer \"%1\" ?"
@ -188,8 +187,6 @@ AdvSceneSwitcher.condition.file="Fichier"
AdvSceneSwitcher.condition.file.type.match="correspond à" AdvSceneSwitcher.condition.file.type.match="correspond à"
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu" AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
AdvSceneSwitcher.condition.file.type.dateChange="a changé de date de modification" AdvSceneSwitcher.condition.file.type.dateChange="a changé de date de modification"
AdvSceneSwitcher.condition.file.remote="Fichier distant"
AdvSceneSwitcher.condition.file.local="Fichier local"
AdvSceneSwitcher.condition.media="Média" AdvSceneSwitcher.condition.media="Média"
AdvSceneSwitcher.condition.media.source="Source" AdvSceneSwitcher.condition.media.source="Source"
AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène" AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène"
@ -282,8 +279,8 @@ AdvSceneSwitcher.condition.record.state.start="Enregistrement en cours"
AdvSceneSwitcher.condition.record.state.pause="Enregistrement en pause" AdvSceneSwitcher.condition.record.state.pause="Enregistrement en pause"
AdvSceneSwitcher.condition.record.state.stop="Arrêt de l'enregistrement" AdvSceneSwitcher.condition.record.state.stop="Arrêt de l'enregistrement"
AdvSceneSwitcher.condition.process="Processus" AdvSceneSwitcher.condition.process="Processus"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan" AdvSceneSwitcher.condition.process.layout="{{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.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inactif" AdvSceneSwitcher.condition.idle="Inactif"
AdvSceneSwitcher.condition.idle.entry="Aucune entrée de clavier ou de souris pendant{{duration}}" AdvSceneSwitcher.condition.idle.entry="Aucune entrée de clavier ou de souris pendant{{duration}}"
AdvSceneSwitcher.condition.pluginState="État du plugin" AdvSceneSwitcher.condition.pluginState="État du plugin"
@ -623,9 +620,9 @@ AdvSceneSwitcher.action.transition.type.scene="Transition de scène"
AdvSceneSwitcher.action.transition.type.sceneOverride="Remplacement de la transition de scène" AdvSceneSwitcher.action.transition.type.sceneOverride="Remplacement de la transition de scène"
AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source" AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source"
AdvSceneSwitcher.action.transition.type.sourceHide="Transition de masquage de la source" AdvSceneSwitcher.action.transition.type.sourceHide="Transition de masquage de la source"
AdvSceneSwitcher.action.transition.entry.line1="Modifier{{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.layout.type="Modifier{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Définir le type de transition sur{{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Définir le type de transition sur{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Définir la durée de la transition à{{duration}}secondes" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
AdvSceneSwitcher.action.timer="Minuterie" AdvSceneSwitcher.action.timer="Minuterie"
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause" AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
AdvSceneSwitcher.action.timer.type.continue="Continuer" AdvSceneSwitcher.action.timer.type.continue="Continuer"
@ -1123,8 +1120,6 @@ AdvSceneSwitcher.status.inactive="Inactif"
AdvSceneSwitcher.running="Plugin en cours d'exécution" AdvSceneSwitcher.running="Plugin en cours d'exécution"
AdvSceneSwitcher.stopped="Plugin arrêté" AdvSceneSwitcher.stopped="Plugin arrêté"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Il semble que ce soit la première fois que l'Advanced Scene Switcher est démarré.<br>Veuillez consulter le <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> pour obtenir une liste de guides et d'exemples.<br>N'hésitez pas à poser des questions dans le <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">fil de discussion</span></a> du plugin sur les forums OBS !</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="Le développement de cet onglet est terminé !\nVeuillez envisager de passer à l'utilisation des Macros à la place.\nCette astuce peut être désactivée dans l'onglet Général." AdvSceneSwitcher.deprecatedTabWarning="Le développement de cet onglet est terminé !\nVeuillez envisager de passer à l'utilisation des Macros à la place.\nCette astuce peut être désactivée dans l'onglet Général."
AdvSceneSwitcher.unit.milliseconds="millisecondes" AdvSceneSwitcher.unit.milliseconds="millisecondes"

View File

@ -164,7 +164,6 @@ AdvSceneSwitcher.macroTab.run.tooltip="条件に関係なくすべてのマク
AdvSceneSwitcher.macroTab.runElse="マクロ実行else" AdvSceneSwitcher.macroTab.runElse="マクロ実行else"
AdvSceneSwitcher.macroTab.runFail="\"%1\" の実行に失敗しました!\nいずれかのアクションが失敗したか、マクロがすでに実行されています。\n停止しますか" AdvSceneSwitcher.macroTab.runFail="\"%1\" の実行に失敗しました!\nいずれかのアクションが失敗したか、マクロがすでに実行されています。\n停止しますか"
AdvSceneSwitcher.macroTab.runInParallel="他のマクロと並行してマクロを実行する" AdvSceneSwitcher.macroTab.runInParallel="他のマクロと並行してマクロを実行する"
AdvSceneSwitcher.macroTab.onChange="条件変更時のみアクションを実行"
AdvSceneSwitcher.macroTab.defaultname="マクロ %1" AdvSceneSwitcher.macroTab.defaultname="マクロ %1"
AdvSceneSwitcher.macroTab.defaultGroupName="グループ %1" AdvSceneSwitcher.macroTab.defaultGroupName="グループ %1"
AdvSceneSwitcher.macroTab.macroNameExists="名前 \"%1\" は既にマクロで使用されています。" AdvSceneSwitcher.macroTab.macroNameExists="名前 \"%1\" は既にマクロで使用されています。"
@ -332,14 +331,6 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}ウィンドウにテ
AdvSceneSwitcher.condition.window.entry.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。" AdvSceneSwitcher.condition.window.entry.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。"
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}" AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
AdvSceneSwitcher.condition.file="ファイル" AdvSceneSwitcher.condition.file="ファイル"
; AdvSceneSwitcher.condition.file.type.match="matches"
AdvSceneSwitcher.condition.file.type.contentChange="内容変更しました"
AdvSceneSwitcher.condition.file.type.dateChange="更新日変更"
AdvSceneSwitcher.condition.file.remote="リモートファイル"
AdvSceneSwitcher.condition.file.local="ローカルファイル"
; AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
; AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
; AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
; AdvSceneSwitcher.condition.media="Media" ; AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.checkType.state="状態が一致" AdvSceneSwitcher.condition.media.checkType.state="状態が一致"
AdvSceneSwitcher.condition.media.checkType.time="時間が一致" AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
@ -454,8 +445,8 @@ AdvSceneSwitcher.condition.stream.service.tooltip="現在のサービス名: %1"
AdvSceneSwitcher.condition.record.state.duration="録音時間が長くなっています。" AdvSceneSwitcher.condition.record.state.duration="録音時間が長くなっています。"
; AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}" ; AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="プロセス" AdvSceneSwitcher.condition.process="プロセス"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}が実行中{{focused}}に集中しています" AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}が実行中{{focused}}に集中しています"
AdvSceneSwitcher.condition.process.entry.focus="現在のフォアグラウンドプロセス:{{focusProcess}}" AdvSceneSwitcher.condition.process.layout.focus="現在のフォアグラウンドプロセス:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="アイドル" AdvSceneSwitcher.condition.idle="アイドル"
AdvSceneSwitcher.condition.idle.entry="{{duration}}の間、キーボードまたはマウスの入力がありません" AdvSceneSwitcher.condition.idle.entry="{{duration}}の間、キーボードまたはマウスの入力がありません"
AdvSceneSwitcher.condition.pluginState="プラグインの状態" AdvSceneSwitcher.condition.pluginState="プラグインの状態"
@ -1021,9 +1012,9 @@ AdvSceneSwitcher.action.transition="トランジション"
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド" ; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション" ; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション" ; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
; AdvSceneSwitcher.action.transition.entry.line1="{{type}}{{scenes}}{{sources}} の変更" ; AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}} の変更"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}トランジションタイプを{{transitions}}に設定します" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}移行時間を {{duration}} 秒に設定します" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}移行時間を {{duration}} 秒に設定します"
AdvSceneSwitcher.action.timer="タイマー" AdvSceneSwitcher.action.timer="タイマー"
AdvSceneSwitcher.action.timer.type.pause="一時停止" AdvSceneSwitcher.action.timer.type.pause="一時停止"
AdvSceneSwitcher.action.timer.type.continue="続行" AdvSceneSwitcher.action.timer.type.continue="続行"
@ -1191,8 +1182,15 @@ AdvSceneSwitcher.action.twitch.reward.toggleControl="リワード名/変数選
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません"
; AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}" ; AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
AdvSceneSwitcher.action.twitch.layout.chat="{{channel}}でアカウント{{account}}{{actions}}を使用しています" AdvSceneSwitcher.action.twitch.layout.chat="{{channel}}でアカウント{{account}}{{actions}}を使用しています"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="{{userInfoQueryType}}{{userLogin}}{{userId}}でアカウント{{account}}{{actions}}を使用" AdvSceneSwitcher.action.twitch.layout.channel.getInfo="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="チャンネル{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}でアカウント{{account}}{{actions}}を使用" AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="アカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="アカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="アカウント{{account}}{{actions}}を使用 タイムアウト{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}} 理由{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="タイトルを入力" AdvSceneSwitcher.action.twitch.title.title="タイトルを入力"
AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明" AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明"
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します" AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"
@ -2252,8 +2250,6 @@ AdvSceneSwitcher.status.inactive="停止中"
AdvSceneSwitcher.running="プラグイン実行中" AdvSceneSwitcher.running="プラグイン実行中"
AdvSceneSwitcher.stopped="プラグイン停止しました" AdvSceneSwitcher.stopped="プラグイン停止しました"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>高機能シーンスイッチャーが初めて起動したようです。<br>ガイドと使用例については <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> をご覧ください。<br>質問は <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">OBSフォーラムのプラグインのスレッド</span></a> で遠慮なくどうぞ!</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="このタブの開発は停止しました!\n代わりにマクロの使用に移行することを検討してください。\nこのヒントは [全般] タブで無効にすることができます。" AdvSceneSwitcher.deprecatedTabWarning="このタブの開発は停止しました!\n代わりにマクロの使用に移行することを検討してください。\nこのヒントは [全般] タブで無効にすることができます。"
AdvSceneSwitcher.unit.milliseconds="ミリ秒" AdvSceneSwitcher.unit.milliseconds="ミリ秒"

View File

@ -147,7 +147,6 @@ AdvSceneSwitcher.macroTab.run.tooltip="Execute todas as ações da macro indepen
AdvSceneSwitcher.macroTab.runElse="Executar macro (alternativa)" AdvSceneSwitcher.macroTab.runElse="Executar macro (alternativa)"
AdvSceneSwitcher.macroTab.runFail="Falha ao executar \"%1\"!\nUma das ações falhou ou a macro já está em execução.\nDeseja interrompê-la?" AdvSceneSwitcher.macroTab.runFail="Falha ao executar \"%1\"!\nUma das ações falhou ou a macro já está em execução.\nDeseja interrompê-la?"
AdvSceneSwitcher.macroTab.runInParallel="Executar macro em paralelo com outras macros" AdvSceneSwitcher.macroTab.runInParallel="Executar macro em paralelo com outras macros"
AdvSceneSwitcher.macroTab.onChange="Executar ações apenas quando houver mudança na condição"
AdvSceneSwitcher.macroTab.defaultname="Macro %1" AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Grupo %1" AdvSceneSwitcher.macroTab.defaultGroupName="Grupo %1"
AdvSceneSwitcher.macroTab.macroNameExists="O nome \"%1\" já está em uso por uma macro." AdvSceneSwitcher.macroTab.macroNameExists="O nome \"%1\" já está em uso por uma macro."
@ -296,11 +295,6 @@ AdvSceneSwitcher.condition.file="Arquivo"
AdvSceneSwitcher.condition.file.type.match="corresponde" AdvSceneSwitcher.condition.file.type.match="corresponde"
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou" AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou"
AdvSceneSwitcher.condition.file.type.dateChange="data de modificação mudou" AdvSceneSwitcher.condition.file.type.dateChange="data de modificação mudou"
AdvSceneSwitcher.condition.file.remote="Arquivo remoto"
AdvSceneSwitcher.condition.file.local="Arquivo local"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Mídia" AdvSceneSwitcher.condition.media="Mídia"
AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde" AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde"
AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo corresponde" AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo corresponde"
@ -401,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.state.duration="Duração da gravação é maior que"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}" AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Processo" AdvSceneSwitcher.condition.process="Processo"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}está em execução{{focused}}e está em foco" AdvSceneSwitcher.condition.process.layout="{{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.focus="Processo atual em primeiro plano:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inativo" AdvSceneSwitcher.condition.idle="Inativo"
AdvSceneSwitcher.condition.idle.entry="Sem entradas de teclado ou mouse por {{duration}}" AdvSceneSwitcher.condition.idle.entry="Sem entradas de teclado ou mouse por {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado do plugin" AdvSceneSwitcher.condition.pluginState="Estado do plugin"
@ -755,7 +749,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="Alterar pasta de saí
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Alterar formatação do nome do arquivo" AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Alterar formatação do nome do arquivo"
AdvSceneSwitcher.action.recording.pause.hint="Observe que dependendo das suas configurações de gravação, você pode não conseguir pausar a gravação" AdvSceneSwitcher.action.recording.pause.hint="Observe que dependendo das suas configurações de gravação, você pode não conseguir pausar a gravação"
AdvSceneSwitcher.action.recording.split.hint="Certifique-se de habilitar a divisão automática de arquivos nas configurações do OBS primeiro!" AdvSceneSwitcher.action.recording.split.hint="Certifique-se de habilitar a divisão automática de arquivos nas configurações do OBS primeiro!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Buffer de replay" AdvSceneSwitcher.action.replay="Buffer de replay"
AdvSceneSwitcher.action.replay.saveWarn="Aviso: Salvar com muita frequência pode resultar em o buffer de replay não ser realmente salvo!" AdvSceneSwitcher.action.replay.saveWarn="Aviso: Salvar com muita frequência pode resultar em o buffer de replay não ser realmente salvo!"
AdvSceneSwitcher.action.replay.durationWarn="Aviso: Alterar o tempo máximo de replay só se aplicará na próxima vez que o buffer de replay for iniciado!" AdvSceneSwitcher.action.replay.durationWarn="Aviso: Alterar o tempo máximo de replay só se aplicará na próxima vez que o buffer de replay for iniciado!"
@ -934,9 +927,9 @@ AdvSceneSwitcher.action.transition.type.scene="transição de cena"
AdvSceneSwitcher.action.transition.type.sceneOverride="substituição de transição de cena" AdvSceneSwitcher.action.transition.type.sceneOverride="substituição de transição de cena"
AdvSceneSwitcher.action.transition.type.sourceShow="transição de exibição de fonte" AdvSceneSwitcher.action.transition.type.sourceShow="transição de exibição de fonte"
AdvSceneSwitcher.action.transition.type.sourceHide="transição de ocultação de fonte" AdvSceneSwitcher.action.transition.type.sourceHide="transição de ocultação de fonte"
AdvSceneSwitcher.action.transition.entry.line1="Modificar{{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.layout.type="Modificar{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Definir tipo de transição para{{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Definir tipo de transição para{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Definir duração da transição para{{duration}}segundos" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Definir duração da transição para{{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador" AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausar" AdvSceneSwitcher.action.timer.type.pause="Pausar"
AdvSceneSwitcher.action.timer.type.continue="Continuar" AdvSceneSwitcher.action.timer.type.continue="Continuar"
@ -1066,7 +1059,7 @@ AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Habilitar modo apenas
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Desabilitar modo apenas emotes no chat" AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Desabilitar modo apenas emotes no chat"
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de chat" AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de chat"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Não é possível selecionar categoria sem selecionar uma conta Twitch primeiro!" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Não é possível selecionar categoria sem selecionar uma conta Twitch primeiro!"
AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}" AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
AdvSceneSwitcher.action.twitch.layout.chat="Usando conta{{account}}{{actions}}em{{channel}}" AdvSceneSwitcher.action.twitch.layout.chat="Usando conta{{account}}{{actions}}em{{channel}}"
AdvSceneSwitcher.action.twitch.title.title="Digite o título" AdvSceneSwitcher.action.twitch.title.title="Digite o título"
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador" AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"
@ -1872,8 +1865,6 @@ AdvSceneSwitcher.status.inactive="Inativo"
AdvSceneSwitcher.running="Plugin em execução" AdvSceneSwitcher.running="Plugin em execução"
AdvSceneSwitcher.stopped="Plugin parado" AdvSceneSwitcher.stopped="Plugin parado"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Parece que esta é a primeira vez que o Advanced Scene Switcher está sendo iniciado.<br>Por favor, consulte o <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> para uma lista de guias e exemplos.<br>Não hesite em fazer perguntas no <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">tópico</span></a> do plugin nos fóruns do OBS!</p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="Desenvolvimento para esta aba interrompido!\nPor favor, considere a transição para o uso de Macros em vez disso.\nEsta dica pode ser desativada na aba Geral." AdvSceneSwitcher.deprecatedTabWarning="Desenvolvimento para esta aba interrompido!\nPor favor, considere a transição para o uso de Macros em vez disso.\nEsta dica pode ser desativada na aba Geral."
AdvSceneSwitcher.unit.milliseconds="milissegundos" AdvSceneSwitcher.unit.milliseconds="milissegundos"

View File

@ -85,8 +85,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий" AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
AdvSceneSwitcher.condition.window="Окно" AdvSceneSwitcher.condition.window="Окно"
AdvSceneSwitcher.condition.file="Файл" AdvSceneSwitcher.condition.file="Файл"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Медиа" AdvSceneSwitcher.condition.media="Медиа"
AdvSceneSwitcher.condition.video="Видео" AdvSceneSwitcher.condition.video="Видео"
AdvSceneSwitcher.condition.video.condition.match="точно соответствует" AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
@ -105,7 +103,7 @@ AdvSceneSwitcher.condition.record.state.start="Запись запущена"
AdvSceneSwitcher.condition.record.state.pause="Запись приостановлена" AdvSceneSwitcher.condition.record.state.pause="Запись приостановлена"
AdvSceneSwitcher.condition.record.state.stop="Запись остановлена" AdvSceneSwitcher.condition.record.state.stop="Запись остановлена"
AdvSceneSwitcher.condition.process="Процесс" AdvSceneSwitcher.condition.process="Процесс"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}запущен{{focused}}и сфокусирован" AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.idle="Простой" AdvSceneSwitcher.condition.idle="Простой"
AdvSceneSwitcher.condition.idle.entry="Не было ни клавиатуры, ни мыши в течении{{duration}}" AdvSceneSwitcher.condition.idle.entry="Не было ни клавиатуры, ни мыши в течении{{duration}}"
AdvSceneSwitcher.condition.pluginState="Состояние плагина" AdvSceneSwitcher.condition.pluginState="Состояние плагина"
@ -130,7 +128,6 @@ AdvSceneSwitcher.action.recording.type.start="Начать запись"
AdvSceneSwitcher.action.recording.type.pause="Пауза записи" AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы" AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись" AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Буфер воспроизведения" AdvSceneSwitcher.action.replay="Буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения" AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера" AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера"

View File

@ -72,7 +72,6 @@ AdvSceneSwitcher.macroTab.add="Yeni Makro ekle"
AdvSceneSwitcher.macroTab.name="İsim:" AdvSceneSwitcher.macroTab.name="İsim:"
AdvSceneSwitcher.macroTab.run="Makro Çalıştırma" AdvSceneSwitcher.macroTab.run="Makro Çalıştırma"
AdvSceneSwitcher.macroTab.runInParallel="Makroyu diğer makrolara paralel olarak çalıştırın" AdvSceneSwitcher.macroTab.runInParallel="Makroyu diğer makrolara paralel olarak çalıştırın"
AdvSceneSwitcher.macroTab.onChange="Eylemleri yalnızca koşul değişikliğinde gerçekleştirin"
AdvSceneSwitcher.macroTab.defaultname="Makro %1" AdvSceneSwitcher.macroTab.defaultname="Makro %1"
AdvSceneSwitcher.macroTab.copy="Kopya oluştur" AdvSceneSwitcher.macroTab.copy="Kopya oluştur"
AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet" AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet"
@ -108,9 +107,7 @@ AdvSceneSwitcher.condition.scene.type.notChanged="Sahne değişmedi"
AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Geçiş hedefi sahnesi için geçiş kontrolü sırasında" AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Geçiş hedefi sahnesi için geçiş kontrolü sırasında"
AdvSceneSwitcher.condition.window="Pencere" AdvSceneSwitcher.condition.window="Pencere"
AdvSceneSwitcher.condition.file="Dosya" AdvSceneSwitcher.condition.file="Dosya"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}" AdvSceneSwitcher.condition.file.layout="İçerik{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medya" AdvSceneSwitcher.condition.media="Medya"
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı" AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları " AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
@ -153,7 +150,7 @@ AdvSceneSwitcher.condition.record.state.start="Kayıt Çalışıyor"
AdvSceneSwitcher.condition.record.state.pause="Kayıt durakladı" AdvSceneSwitcher.condition.record.state.pause="Kayıt durakladı"
AdvSceneSwitcher.condition.record.state.stop="Kayıt durdu" AdvSceneSwitcher.condition.record.state.stop="Kayıt durdu"
AdvSceneSwitcher.condition.process="İşlem" 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="Boşta"
AdvSceneSwitcher.condition.idle.entry="...için klavye veya fare girişi yok {{duration}}" AdvSceneSwitcher.condition.idle.entry="...için klavye veya fare girişi yok {{duration}}"
AdvSceneSwitcher.condition.pluginState="Eklenti durumu" AdvSceneSwitcher.condition.pluginState="Eklenti durumu"
@ -266,7 +263,6 @@ AdvSceneSwitcher.action.recording.type.start="Kayıt Başlat"
AdvSceneSwitcher.action.recording.type.pause="Kayıt Duraklat" AdvSceneSwitcher.action.recording.type.pause="Kayıt Duraklat"
AdvSceneSwitcher.action.recording.type.unpause="Kayıt Duraklatma" AdvSceneSwitcher.action.recording.type.unpause="Kayıt Duraklatma"
AdvSceneSwitcher.action.recording.pause.hint="Kayıt ayarlarınıza bağlı olarak kaydı duraklatamayabileceğinizi unutmayın." AdvSceneSwitcher.action.recording.pause.hint="Kayıt ayarlarınıza bağlı olarak kaydı duraklatamayabileceğinizi unutmayın."
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Tekrar arabelleği" AdvSceneSwitcher.action.replay="Tekrar arabelleği"
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur" AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat" AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat"
@ -342,8 +338,8 @@ AdvSceneSwitcher.action.file.type.write="Yaz"
AdvSceneSwitcher.action.file.type.append="Ekle" AdvSceneSwitcher.action.file.type.append="Ekle"
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:" AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
AdvSceneSwitcher.action.transition="Geçiş" AdvSceneSwitcher.action.transition="Geçiş"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Geçiş türünü ayarla {{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
AdvSceneSwitcher.action.timer="Zamanlayıcı" AdvSceneSwitcher.action.timer="Zamanlayıcı"
AdvSceneSwitcher.action.timer.type.pause="Duraklat" AdvSceneSwitcher.action.timer.type.pause="Duraklat"
AdvSceneSwitcher.action.timer.type.continue="Devam et" AdvSceneSwitcher.action.timer.type.continue="Devam et"
@ -587,8 +583,6 @@ AdvSceneSwitcher.status.inactive="İnaktif"
AdvSceneSwitcher.running="Eklenti çalışıyor" AdvSceneSwitcher.running="Eklenti çalışıyor"
AdvSceneSwitcher.stopped="Eklenti durdu" AdvSceneSwitcher.stopped="Eklenti durdu"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>Gelişmiş Sahne Değiştirici ilk kez başlatılıyor gibi görünüyor.<br>Lütfen <a href=\"https://github.com/ adresine bir göz atın. WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> için kılavuzlar ve örnekler listesi.<br>Yapmayın. eklentinin <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:# sayfasında soru sormaktan çekinmeyin OBS forumlarında 268bd2;\">konu</span></a>!</p></body></html>"
AdvSceneSwitcher.unit.milliseconds="millisaniye" AdvSceneSwitcher.unit.milliseconds="millisaniye"
AdvSceneSwitcher.unit.seconds="saniye" AdvSceneSwitcher.unit.seconds="saniye"
AdvSceneSwitcher.unit.minutes="dakika" AdvSceneSwitcher.unit.minutes="dakika"

View File

@ -151,7 +151,6 @@ AdvSceneSwitcher.macroTab.run.tooltip="无论条件如何,都运行所有宏
AdvSceneSwitcher.macroTab.runElse="运行宏(不满足条件)" AdvSceneSwitcher.macroTab.runElse="运行宏(不满足条件)"
AdvSceneSwitcher.macroTab.runFail="运行 \"%1\" 失败!\n要么其中一个操作失败要么宏已在运行中.\n你想停止它吗?" AdvSceneSwitcher.macroTab.runFail="运行 \"%1\" 失败!\n要么其中一个操作失败要么宏已在运行中.\n你想停止它吗?"
AdvSceneSwitcher.macroTab.runInParallel="与其他宏并行运行宏" AdvSceneSwitcher.macroTab.runInParallel="与其他宏并行运行宏"
AdvSceneSwitcher.macroTab.onChange="仅在条件结果发生变化时执行操作(条件结果不变时只执行一次操作)"
AdvSceneSwitcher.macroTab.defaultname="宏 %1" AdvSceneSwitcher.macroTab.defaultname="宏 %1"
AdvSceneSwitcher.macroTab.defaultGroupName="分组 %1" AdvSceneSwitcher.macroTab.defaultGroupName="分组 %1"
AdvSceneSwitcher.macroTab.macroNameExists="名称 \"%1\" 已被宏使用." AdvSceneSwitcher.macroTab.macroNameExists="名称 \"%1\" 已被宏使用."
@ -319,11 +318,7 @@ AdvSceneSwitcher.condition.file="文件"
AdvSceneSwitcher.condition.file.type.match="内容匹配" AdvSceneSwitcher.condition.file.type.match="内容匹配"
AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变" AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变"
AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变" AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变"
AdvSceneSwitcher.condition.file.remote="网络文件" AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.file.local="本地文件"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="媒体" AdvSceneSwitcher.condition.media="媒体"
AdvSceneSwitcher.condition.media.checkType.state="状态匹配" AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配" AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
@ -429,8 +424,8 @@ AdvSceneSwitcher.condition.record.state.stop="录制停止"
AdvSceneSwitcher.condition.record.state.duration="录制时间长于" AdvSceneSwitcher.condition.record.state.duration="录制时间长于"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}" AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="进程" AdvSceneSwitcher.condition.process="进程"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点" AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点"
AdvSceneSwitcher.condition.process.entry.focus="当前焦点进程: {{focusProcess}}" AdvSceneSwitcher.condition.process.layout.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="闲置检测" AdvSceneSwitcher.condition.idle="闲置检测"
AdvSceneSwitcher.condition.idle.entry="{{duration}}内没有键盘或鼠标输入" AdvSceneSwitcher.condition.idle.entry="{{duration}}内没有键盘或鼠标输入"
AdvSceneSwitcher.condition.pluginState="插件状态" AdvSceneSwitcher.condition.pluginState="插件状态"
@ -814,7 +809,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="更改输出文件夹
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式" AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制" AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
AdvSceneSwitcher.action.recording.split.hint="注意请先确保在OBS设置中启用自动分割文件!" AdvSceneSwitcher.action.recording.split.hint="注意请先确保在OBS设置中启用自动分割文件!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="回放缓存" AdvSceneSwitcher.action.replay="回放缓存"
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!" AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!" AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
@ -995,9 +989,9 @@ AdvSceneSwitcher.action.transition.type.scene="场景转场动画"
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画" AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画" AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画" AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
AdvSceneSwitcher.action.transition.entry.line1="更改 {{type}}{{scenes}}{{sources}}" AdvSceneSwitcher.action.transition.layout.type="更改 {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}将转场动画类型设置为 {{transitions}}" AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}将转场动画类型设置为 {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}将转场动画时长设置为 {{duration}}秒" AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
AdvSceneSwitcher.action.timer="计时器" AdvSceneSwitcher.action.timer="计时器"
AdvSceneSwitcher.action.timer.type.pause="暂停" AdvSceneSwitcher.action.timer.type.pause="暂停"
AdvSceneSwitcher.action.timer.type.continue="继续" AdvSceneSwitcher.action.timer.type.continue="继续"
@ -1140,10 +1134,17 @@ AdvSceneSwitcher.action.twitch.type.user.getInfo="获取用户信息"
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息" AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制" AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="在未选择 Twitch 帐户的情况下无法选择类别!" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="在未选择 Twitch 帐户的情况下无法选择类别!"
AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}" AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
AdvSceneSwitcher.action.twitch.layout.chat="使用账户{{account}}{{actions}}{{channel}}" AdvSceneSwitcher.action.twitch.layout.chat="使用账户{{account}}{{actions}}{{channel}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="使用账户{{account}}{{actions}}{{userInfoQueryType}}{{userLogin}}{{userId}}" AdvSceneSwitcher.action.twitch.layout.channel.getInfo="使用账户{{account}}{{actions}}频道{{channel}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="使用账户{{account}}{{actions}} 频道{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}" AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="使用账户{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="使用账户{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="使用账户{{account}}{{actions}}超时{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}原因{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="使用账户{{account}}{{actions}} 频道{{channel}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="输入标题" AdvSceneSwitcher.action.twitch.title.title="输入标题"
AdvSceneSwitcher.action.twitch.marker.description="标记描述" AdvSceneSwitcher.action.twitch.marker.description="标记描述"
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟" AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"
@ -2116,8 +2117,6 @@ AdvSceneSwitcher.status.inactive="已停止"
AdvSceneSwitcher.running="插件正在运行" AdvSceneSwitcher.running="插件正在运行"
AdvSceneSwitcher.stopped="插件已停止" AdvSceneSwitcher.stopped="插件已停止"
AdvSceneSwitcher.firstBootMessage="<html><head/><body><p>这似乎是您第一次启动高级场景切换器.<br>请看一下 <a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\"><span style=\" text-decoration: underline; color:#268bd2;\">Wiki</span></a> 查看指南和示例列表.<br>如果有问题在在OBS论坛插件帖子内提问 <a href=\"https://obsproject.com/forum/threads/advanced-scene-switcher.48264\"><span style=\" text-decoration: underline; color:#268bd2;\">thread</span></a></p></body></html>"
AdvSceneSwitcher.deprecatedTabWarning="此选项卡的开发已停止!请考虑转换为使用宏来代替。\n可以在“常规”选项卡上禁用此提示." AdvSceneSwitcher.deprecatedTabWarning="此选项卡的开发已停止!请考虑转换为使用宏来代替。\n可以在“常规”选项卡上禁用此提示."
AdvSceneSwitcher.unit.milliseconds="毫秒" AdvSceneSwitcher.unit.milliseconds="毫秒"

View File

@ -68,7 +68,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>962</width> <width>962</width>
<height>1160</height> <height>1190</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_19"> <layout class="QVBoxLayout" name="verticalLayout_19">
@ -251,6 +251,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="suppressCrashRecoveryDialog">
<property name="text">
<string>AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -565,6 +572,13 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="openSetupWizard">
<property name="text">
<string>FirstRunWizard.openButton</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</widget> </widget>
@ -590,7 +604,7 @@
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="QGroupBox" name="groupBox_5"> <widget class="QGroupBox" name="macroListBox">
<property name="title"> <property name="title">
<string>AdvSceneSwitcher.macroTab.macros</string> <string>AdvSceneSwitcher.macroTab.macros</string>
</property> </property>
@ -786,25 +800,19 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="runMacroOnChange"> <widget class="QLabel" name="label_18">
<property name="toolTip">
<string>AdvSceneSwitcher.macroTab.onChange</string>
</property>
<property name="text"> <property name="text">
<string/> <string>AdvSceneSwitcher.macroTab.actionTriggerMode.label</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_18"> <widget class="QComboBox" name="actionTriggerMode">
<property name="toolTip"> <property name="toolTip">
<string>AdvSceneSwitcher.macroTab.onChange</string> <string>AdvSceneSwitcher.macroTab.actionTriggerMode.tooltip</string>
</property>
<property name="text">
<string>AdvSceneSwitcher.macroTab.onChange</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
@ -4118,7 +4126,7 @@
<tabstop>macroName</tabstop> <tabstop>macroName</tabstop>
<tabstop>runMacro</tabstop> <tabstop>runMacro</tabstop>
<tabstop>runMacroInParallel</tabstop> <tabstop>runMacroInParallel</tabstop>
<tabstop>runMacroOnChange</tabstop> <tabstop>actionTriggerMode</tabstop>
<tabstop>macroSettings</tabstop> <tabstop>macroSettings</tabstop>
<tabstop>macroEdit</tabstop> <tabstop>macroEdit</tabstop>
<tabstop>sceneGroups</tabstop> <tabstop>sceneGroups</tabstop>

View File

@ -322,8 +322,7 @@ void SwitcherData::SetPreconditions()
{ {
// Window title // Window title
lastTitle = currentTitle; lastTitle = currentTitle;
std::string title; auto title = GetCurrentWindowTitle();
GetCurrentWindowTitle(title);
for (auto &window : ignoreWindowsSwitches) { for (auto &window : ignoreWindowsSwitches) {
bool equals = (title == window); bool equals = (title == window);
bool matches = false; bool matches = false;
@ -343,7 +342,7 @@ void SwitcherData::SetPreconditions()
currentTitle = title; currentTitle = title;
// Process name // Process name
GetForegroundProcessName(currentForegroundProcess); currentForegroundProcess = GetForegroundProcessName();
// Macro // Macro
InvalidateMacroTempVarValues(); InvalidateMacroTempVarValues();

View File

@ -66,10 +66,12 @@ public slots:
void on_uiHintsDisable_stateChanged(int state); void on_uiHintsDisable_stateChanged(int state);
void on_disableComboBoxFilter_stateChanged(int state); void on_disableComboBoxFilter_stateChanged(int state);
void on_warnPluginLoadFailure_stateChanged(int state); void on_warnPluginLoadFailure_stateChanged(int state);
void on_suppressCrashRecoveryDialog_stateChanged(int state);
void on_hideLegacyTabs_stateChanged(int state); void on_hideLegacyTabs_stateChanged(int state);
void on_priorityUp_clicked(); void on_priorityUp_clicked();
void on_priorityDown_clicked(); void on_priorityDown_clicked();
void on_threadPriority_currentTextChanged(const QString &text); void on_threadPriority_currentTextChanged(const QString &text);
void on_openSetupWizard_clicked();
/* --- End of legacy tab section --- */ /* --- End of legacy tab section --- */
@ -107,7 +109,7 @@ public slots:
void on_macroDown_clicked() const; void on_macroDown_clicked() const;
void on_macroName_editingFinished(); void on_macroName_editingFinished();
void on_runMacroInParallel_stateChanged(int value) const; void on_runMacroInParallel_stateChanged(int value) const;
void on_runMacroOnChange_stateChanged(int value) const; void on_actionTriggerMode_currentIndexChanged(int index) const;
void MacroSelectionChanged(); void MacroSelectionChanged();
void ShowMacroContextMenu(const QPoint &); void ShowMacroContextMenu(const QPoint &);
void CopyMacro(); void CopyMacro();

View File

@ -1,19 +1,19 @@
#include "advanced-scene-switcher.hpp" #include "advanced-scene-switcher.hpp"
#include "crash-handler.hpp"
#include "file-selection.hpp" #include "file-selection.hpp"
#include "filter-combo-box.hpp" #include "filter-combo-box.hpp"
#include "first-run-wizard.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "macro.hpp" #include "macro.hpp"
#include "macro-search.hpp" #include "macro-search.hpp"
#include "macro-settings.hpp" #include "macro-settings.hpp"
#include "path-helpers.hpp" #include "path-helpers.hpp"
#include "selection-helpers.hpp"
#include "source-helpers.hpp" #include "source-helpers.hpp"
#include "splitter-helpers.hpp" #include "splitter-helpers.hpp"
#include "status-control.hpp" #include "status-control.hpp"
#include "switcher-data.hpp" #include "switcher-data.hpp"
#include "tab-helpers.hpp" #include "tab-helpers.hpp"
#include "ui-helpers.hpp" #include "ui-helpers.hpp"
#include "utility.hpp"
#include "variable.hpp" #include "variable.hpp"
#include "version.h" #include "version.h"
@ -195,6 +195,15 @@ void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
switcher->warnPluginLoadFailure = state; switcher->warnPluginLoadFailure = state;
} }
void AdvSceneSwitcher::on_suppressCrashRecoveryDialog_stateChanged(int state)
{
if (loading) {
return;
}
SetSuppressCrashDialog(state);
}
static bool isLegacyTab(const QString &name) static bool isLegacyTab(const QString &name)
{ {
return name == obs_module_text( return name == obs_module_text(
@ -361,14 +370,46 @@ void AdvSceneSwitcher::RestoreWindowGeo()
} }
} }
static void renameMacroIfNecessary(const std::shared_ptr<Macro> &macro)
{
if (!GetMacroByName(macro->Name().c_str())) {
return;
}
auto name = macro->Name();
int i = 2;
while (GetMacroByName((name + " " + std::to_string(i)).c_str())) {
i++;
}
macro->SetName(name + " " + std::to_string(i));
}
void AdvSceneSwitcher::CheckFirstTimeSetup() void AdvSceneSwitcher::CheckFirstTimeSetup()
{ {
if (switcher->firstBoot && !switcher->disableHints) { if (!IsFirstRun() || !GetTopLevelMacros().empty()) {
switcher->firstBoot = false; return;
DisplayMessage(
obs_module_text("AdvSceneSwitcher.firstBootMessage"));
switcher->Start();
} }
auto macro = FirstRunWizard::ShowWizard(this);
if (macro) {
renameMacroIfNecessary(macro);
QTimer::singleShot(0, this,
[this, macro]() { ui->macros->Add(macro); });
}
switcher->Start();
}
void AdvSceneSwitcher::on_openSetupWizard_clicked()
{
auto macro = FirstRunWizard::ShowWizard(this);
if (!macro) {
return;
}
renameMacroIfNecessary(macro);
ui->macros->Add(macro);
ui->tabWidget->setCurrentWidget(ui->macroTab);
} }
void AdvSceneSwitcher::on_tabWidget_currentChanged(int) void AdvSceneSwitcher::on_tabWidget_currentChanged(int)
@ -915,6 +956,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
FilterComboBox::SetFilterBehaviourEnabled( FilterComboBox::SetFilterBehaviourEnabled(
!switcher->disableFilterComboboxFilter); !switcher->disableFilterComboboxFilter);
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure); ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs); ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
populatePriorityFunctionList(ui->priorityList); populatePriorityFunctionList(ui->priorityList);

View File

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

View File

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

View File

@ -32,6 +32,9 @@
#endif #endif
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <climits>
#include <dirent.h>
#include <unistd.h>
#include "kwin-helpers.h" #include "kwin-helpers.h"
namespace advss { namespace advss {
@ -233,9 +236,9 @@ std::string getWindowName(Window window)
return windowTitle; 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()) { for (auto window : getTopLevelWindows()) {
auto name = getWindowName(window); auto name = getWindowName(window);
if (name.empty()) { if (name.empty()) {
@ -243,18 +246,7 @@ void GetWindowList(std::vector<std::string> &windows)
} }
windows.emplace_back(name); windows.emplace_back(name);
} }
} return windows;
void GetWindowList(QStringList &windows)
{
windows.clear();
for (auto window : getTopLevelWindows()) {
auto name = getWindowName(window);
if (name.empty()) {
continue;
}
windows << QString::fromStdString(name);
}
} }
int getActiveWindow(Window *&window) int getActiveWindow(Window *&window)
@ -278,29 +270,24 @@ int getActiveWindow(Window *&window)
&bytes, (uint8_t **)&window); &bytes, (uint8_t **)&window);
} }
void GetCurrentWindowTitle(std::string &title) std::string GetCurrentWindowTitle()
{ {
if (KWin) { if (KWin) {
title = FocusNotifier::getActiveWindowTitle(); return FocusNotifier::getActiveWindowTitle();
return;
} }
Window *data = 0; Window *data = 0;
if (getActiveWindow(data) != Success || !data) { if (getActiveWindow(data) != Success || !data) {
return; return {};
} }
if (!data[0]) { if (!data[0]) {
XFree(data); XFree(data);
return; return {};
} }
auto name = getWindowName(data[0]); auto name = getWindowName(data[0]);
XFree(data); XFree(data);
return name;
if (name.empty()) {
return;
}
title = name;
} }
bool windowStatesAreSet(const std::string &windowTitle, bool windowStatesAreSet(const std::string &windowTitle,
@ -409,16 +396,17 @@ static void getProcessListProcps2(QStringList &processes)
#endif #endif
} }
void GetProcessList(QStringList &processes) QStringList GetProcessList()
{ {
processes.clear(); QStringList processes;
if (libprocpsSupported) { if (libprocpsSupported) {
getProcessListProcps(processes); getProcessListProcps(processes);
return; return processes;
} }
if (libprocps2Supported) { if (libprocps2Supported) {
getProcessListProcps2(processes); getProcessListProcps2(processes);
} }
return processes;
} }
long getForegroundProcessPid() long getForegroundProcessPid()
@ -469,24 +457,83 @@ std::string getProcNameFromPid(long pid)
return name; return name;
} }
void GetForegroundProcessName(QString &proc) std::string GetForegroundProcessName()
{ {
std::string temp; auto pid = getForegroundProcessPid();
GetForegroundProcessName(temp); return getProcNameFromPid(pid);
proc = QString::fromStdString(temp);
} }
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(); 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) bool IsInFocus(const QString &executable)
{ {
std::string current; const auto current = GetForegroundProcessName();
GetForegroundProcessName(current);
// True if executable switch equals current window // True if executable switch equals current window
bool equals = (executable.toStdString() == current); bool equals = (executable.toStdString() == current);

View File

@ -4,6 +4,8 @@
#include "macro.hpp" #include "macro.hpp"
#include "macro-action-factory.hpp" #include "macro-action-factory.hpp"
#include <chrono>
namespace advss { namespace advss {
const std::string MacroActionMacro::id = "macro"; const std::string MacroActionMacro::id = "macro";
@ -118,6 +120,36 @@ bool MacroActionMacro::PerformAction()
case Action::TOGGLE_ACTION: case Action::TOGGLE_ACTION:
AdjustActionState(macro); AdjustActionState(macro);
break; break;
case Action::GET_INFO: {
SetTempVarValue(
"conditionCount",
std::to_string(macro->Conditions().size()));
SetTempVarValue(
"actionCount",
std::to_string(macro->Actions().size()));
SetTempVarValue(
"elseActionCount",
std::to_string(macro->ElseActions().size()));
SetTempVarValue("paused",
macro->Paused() ? "true" : "false");
SetTempVarValue("runCount",
std::to_string(macro->RunCount()));
const auto lastRun = macro->GetLastExecutionTime();
const bool hasRun =
lastRun.time_since_epoch().count() != 0;
const auto secondsSinceLastRun =
hasRun ? std::chrono::duration_cast<
std::chrono::seconds>(
std::chrono::
high_resolution_clock::
now() -
lastRun)
.count()
: -1;
SetTempVarValue("secondsSinceLastRun",
std::to_string(secondsSinceLastRun));
break;
}
default: default:
break; break;
} }
@ -175,6 +207,9 @@ void MacroActionMacro::LogAction() const
case Action::NESTED_MACRO: case Action::NESTED_MACRO:
ablog(LOG_INFO, "run nested macro"); ablog(LOG_INFO, "run nested macro");
break; break;
case Action::GET_INFO:
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
break;
default: default:
break; break;
} }
@ -202,8 +237,8 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
bool MacroActionMacro::Load(obs_data_t *obj) bool MacroActionMacro::Load(obs_data_t *obj)
{ {
MacroAction::Load(obj); MacroAction::Load(obj);
_action = static_cast<MacroActionMacro::Action>( SetAction(static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action")); obs_data_get_int(obj, "action")));
_macro.Load(obj); _macro.Load(obj);
_actionSelectionType = static_cast<SelectionType>( _actionSelectionType = static_cast<SelectionType>(
obs_data_get_int(obj, "actionSelectionType")); obs_data_get_int(obj, "actionSelectionType"));
@ -303,6 +338,56 @@ void MacroActionMacro::RunActions(Macro *actionMacro) const
} }
} }
void MacroActionMacro::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
void MacroActionMacro::SetupTempVars()
{
MacroAction::SetupTempVars();
if (_action != Action::GET_INFO) {
return;
}
AddTempvar(
"conditionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.conditionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.conditionCount.description"));
AddTempvar(
"actionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.actionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.actionCount.description"));
AddTempvar(
"elseActionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description"));
AddTempvar(
"paused",
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.paused"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.paused.description"));
AddTempvar(
"runCount",
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.runCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.runCount.description"));
AddTempvar(
"secondsSinceLastRun",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description"));
}
static void populateActionSelection(QComboBox *list) static void populateActionSelection(QComboBox *list)
{ {
static const std::vector<std::pair<MacroActionMacro::Action, std::string>> static const std::vector<std::pair<MacroActionMacro::Action, std::string>>
@ -327,6 +412,8 @@ static void populateActionSelection(QComboBox *list)
"AdvSceneSwitcher.action.macro.type.enableAction"}, "AdvSceneSwitcher.action.macro.type.enableAction"},
{MacroActionMacro::Action::TOGGLE_ACTION, {MacroActionMacro::Action::TOGGLE_ACTION,
"AdvSceneSwitcher.action.macro.type.toggleAction"}, "AdvSceneSwitcher.action.macro.type.toggleAction"},
{MacroActionMacro::Action::GET_INFO,
"AdvSceneSwitcher.action.macro.type.getInfo"},
}; };
for (const auto &[value, name] : actions) { for (const auto &[value, name] : actions) {
@ -496,7 +583,7 @@ void MacroActionMacroEdit::UpdateEntryData()
} }
_actions->setCurrentIndex( _actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action))); _actions->findData(static_cast<int>(_entryData->GetAction())));
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData( _actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
static_cast<int>(_entryData->_actionSelectionType))); static_cast<int>(_entryData->_actionSelectionType)));
_actionIndex->SetValue(_entryData->_actionIndex); _actionIndex->SetValue(_entryData->_actionIndex);
@ -545,8 +632,8 @@ void MacroActionMacroEdit::MacroChanged(const QString &text)
void MacroActionMacroEdit::ActionChanged(int idx) void MacroActionMacroEdit::ActionChanged(int idx)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionMacro::Action>( _entryData->SetAction(static_cast<MacroActionMacro::Action>(
_actions->itemData(idx).toInt()); _actions->itemData(idx).toInt()));
SetWidgetVisibility(); SetWidgetVisibility();
} }
@ -665,7 +752,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
}; };
const auto action = _entryData->_action; const auto action = _entryData->GetAction();
const char *layoutText = ""; const char *layoutText = "";
switch (action) { switch (action) {
case MacroActionMacro::Action::PAUSE: case MacroActionMacro::Action::PAUSE:
@ -674,6 +761,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
case MacroActionMacro::Action::RESET_COUNTER: case MacroActionMacro::Action::RESET_COUNTER:
case MacroActionMacro::Action::STOP: case MacroActionMacro::Action::STOP:
case MacroActionMacro::Action::NESTED_MACRO: case MacroActionMacro::Action::NESTED_MACRO:
case MacroActionMacro::Action::GET_INFO:
layoutText = "AdvSceneSwitcher.action.macro.layout.other"; layoutText = "AdvSceneSwitcher.action.macro.layout.other";
break; break;
case MacroActionMacro::Action::RUN_ACTIONS: case MacroActionMacro::Action::RUN_ACTIONS:

View File

@ -56,11 +56,14 @@ public:
TOGGLE_ACTION, TOGGLE_ACTION,
TOGGLE_PAUSE, TOGGLE_PAUSE,
NESTED_MACRO, NESTED_MACRO,
GET_INFO,
}; };
void SetAction(Action);
Action GetAction() const { return _action; }
enum class SelectionType { INDEX, LABEL, ID }; enum class SelectionType { INDEX, LABEL, ID };
Action _action = Action::NESTED_MACRO;
SelectionType _actionSelectionType = SelectionType::INDEX; SelectionType _actionSelectionType = SelectionType::INDEX;
bool _useElseSection = false; bool _useElseSection = false;
IntVariable _actionIndex = 1; IntVariable _actionIndex = 1;
@ -72,9 +75,12 @@ public:
int _customWidgetHeight = 0; int _customWidgetHeight = 0;
private: private:
void SetupTempVars();
void RunActions(Macro *actionMacro) const; void RunActions(Macro *actionMacro) const;
void AdjustActionState(Macro *) const; void AdjustActionState(Macro *) const;
Action _action = Action::NESTED_MACRO;
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;
}; };

View File

@ -17,6 +17,14 @@ namespace advss {
const std::string MacroActionVariable::id = "variable"; const std::string MacroActionVariable::id = "variable";
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionVariable::_registered = MacroActionFactory::Register( bool MacroActionVariable::_registered = MacroActionFactory::Register(
MacroActionVariable::id, MacroActionVariable::id,
{MacroActionVariable::Create, MacroActionVariableEdit::Create, {MacroActionVariable::Create, MacroActionVariableEdit::Create,
@ -1450,6 +1458,7 @@ void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var; _entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
SetWidgetVisibility(); SetWidgetVisibility();
} }

View File

@ -23,6 +23,7 @@ public:
bool PostLoad(); bool PostLoad();
std::string GetShortDesc() const; std::string GetShortDesc() const;
std::string GetId() const { return id; }; std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroAction> Create(Macro *m); static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const; std::shared_ptr<MacroAction> Copy() const;
void SetSegmentIndexValue(int); void SetSegmentIndexValue(int);

View File

@ -6,6 +6,14 @@ namespace advss {
const std::string MacroConditionTempVar::id = "temp_var"; const std::string MacroConditionTempVar::id = "temp_var";
std::vector<TempVariableRef> MacroConditionTempVar::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register( bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
MacroConditionTempVar::id, MacroConditionTempVar::id,
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create, {MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
@ -266,6 +274,7 @@ void MacroConditionTempVarEdit::VariableChanged(const TempVariableRef &var)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var; _entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
} }
void MacroConditionTempVarEdit::Variable2Changed(const QString &text) void MacroConditionTempVarEdit::Variable2Changed(const QString &text)

View File

@ -20,6 +20,7 @@ public:
bool Load(obs_data_t *obj); bool Load(obs_data_t *obj);
std::string GetShortDesc() const; std::string GetShortDesc() const;
std::string GetId() const { return id; }; std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroCondition> Create(Macro *m) static std::shared_ptr<MacroCondition> Create(Macro *m)
{ {
return std::make_shared<MacroConditionTempVar>(m); return std::make_shared<MacroConditionTempVar>(m);

View File

@ -7,6 +7,17 @@ MacroCondition::MacroCondition(Macro *m, bool supportsVariableValue)
{ {
} }
bool MacroCondition::EvaluateCondition()
{
bool newValue = CheckCondition();
_changed = _previousValue.has_value() && (*_previousValue != newValue);
const bool negate = _logic.IsNegationType(GetLogicType());
_risingEdge = _changed &&
((!negate && newValue) || (negate && !newValue));
_previousValue = newValue;
return newValue;
}
bool MacroCondition::Save(obs_data_t *obj) const bool MacroCondition::Save(obs_data_t *obj) const
{ {
MacroSegment::Save(obj); MacroSegment::Save(obj);

View File

@ -4,13 +4,19 @@
#include "duration-modifier.hpp" #include "duration-modifier.hpp"
#include "macro-ref.hpp" #include "macro-ref.hpp"
#include <optional>
namespace advss { namespace advss {
class EXPORT MacroCondition : public MacroSegment { class EXPORT MacroCondition : public MacroSegment {
public: public:
MacroCondition(Macro *m, bool supportsVariableValue = false); MacroCondition(Macro *m, bool supportsVariableValue = false);
virtual ~MacroCondition() = default; virtual ~MacroCondition() = default;
virtual bool CheckCondition() = 0;
bool EvaluateCondition();
bool HasChanged() const { return _changed; }
bool IsRisingEdge() const { return _risingEdge; }
virtual bool Save(obs_data_t *obj) const = 0; virtual bool Save(obs_data_t *obj) const = 0;
virtual bool Load(obs_data_t *obj) = 0; virtual bool Load(obs_data_t *obj) = 0;
@ -28,9 +34,15 @@ public:
static std::string_view GetDefaultID(); static std::string_view GetDefaultID();
protected:
virtual bool CheckCondition() = 0;
private: private:
Logic _logic = Logic(Logic::Type::ROOT_NONE); Logic _logic = Logic(Logic::Type::ROOT_NONE);
DurationModifier _durationModifier; DurationModifier _durationModifier;
std::optional<bool> _previousValue;
bool _changed = false;
bool _risingEdge = false;
}; };
class EXPORT MacroRefCondition : virtual public MacroCondition { class EXPORT MacroRefCondition : virtual public MacroCondition {

View File

@ -366,7 +366,7 @@ static void runSegmentHighlightChecksHelper(MacroSegmentList *list)
// highlight its segments if required // highlight its segments if required
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get()); auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
if (macroAction && if (macroAction &&
macroAction->_action == macroAction->GetAction() ==
MacroActionMacro::Action::NESTED_MACRO) { MacroActionMacro::Action::NESTED_MACRO) {
continue; continue;
} }
@ -1086,6 +1086,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
HighlightAction(idx); HighlightAction(idx);
ui->actionsList->SetHelpMsgVisible(false); ui->actionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->actionsList->ensureWidgetVisible(
ui->actionsList->WidgetAt(currentActionIdx));
});
} }
void MacroEdit::AddMacroAction(int idx) void MacroEdit::AddMacroAction(int idx)
@ -1389,6 +1394,11 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
HighlightElseAction(idx); HighlightElseAction(idx);
ui->elseActionsList->SetHelpMsgVisible(false); ui->elseActionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->elseActionsList->ensureWidgetVisible(
ui->elseActionsList->WidgetAt(currentElseActionIdx));
});
} }
void MacroEdit::AddMacroElseAction(int idx) void MacroEdit::AddMacroElseAction(int idx)
@ -1591,6 +1601,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
HighlightCondition(idx); HighlightCondition(idx);
ui->conditionsList->SetHelpMsgVisible(false); ui->conditionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->conditionsList->ensureWidgetVisible(
ui->conditionsList->WidgetAt(currentConditionIdx));
});
} }
void MacroEdit::on_conditionAdd_clicked() void MacroEdit::on_conditionAdd_clicked()

View File

@ -8,9 +8,6 @@ namespace advss {
class Macro; class Macro;
class MacroSegment; class MacroSegment;
/*******************************************************************************
* Advanced Scene Switcher window
*******************************************************************************/
class MacroEdit : public QWidget { class MacroEdit : public QWidget {
Q_OBJECT Q_OBJECT

View File

@ -13,6 +13,11 @@
namespace advss { namespace advss {
std::vector<TempVariableRef> MacroSegment::GetTempVarRefs() const
{
return {};
}
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue) MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
: _macro(m), : _macro(m),
_supportsVariableValue(supportsVariableValue) _supportsVariableValue(supportsVariableValue)
@ -152,6 +157,16 @@ void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
NotifyUIAboutTempVarChange(this); NotifyUIAboutTempVarChange(this);
} }
bool MacroSegment::IsTempVarInUse(const std::string &id) const
{
for (const auto &var : _tempVariables) {
if (var.ID() == id) {
return var.IsInUse();
}
}
return false;
}
void MacroSegment::SetTempVarValue(const std::string &id, void MacroSegment::SetTempVarValue(const std::string &id,
const std::string &value) const std::string &value)
{ {

View File

@ -34,6 +34,7 @@ public:
std::string GetCustomLabel() const { return _customLabel; } std::string GetCustomLabel() const { return _customLabel; }
virtual bool Save(obs_data_t *obj) const = 0; virtual bool Save(obs_data_t *obj) const = 0;
virtual bool Load(obs_data_t *obj) = 0; virtual bool Load(obs_data_t *obj) = 0;
virtual std::vector<TempVariableRef> GetTempVarRefs() const;
virtual bool PostLoad(); virtual bool PostLoad();
virtual std::string GetShortDesc() const; virtual std::string GetShortDesc() const;
virtual std::string GetId() const = 0; virtual std::string GetId() const = 0;
@ -54,6 +55,7 @@ protected:
void AddTempvar(const std::string &id, const std::string &name, void AddTempvar(const std::string &id, const std::string &name,
const std::string &description = ""); const std::string &description = "");
bool IsTempVarInUse(const std::string &id) const;
void SetTempVarValue(const std::string &id, const std::string &value); void SetTempVarValue(const std::string &id, const std::string &value);
template<typename T, typename = std::enable_if_t< template<typename T, typename = std::enable_if_t<
@ -64,6 +66,14 @@ protected:
: std::string("false")); : std::string("false"));
} }
template<typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
void SetTempVarValue(const std::string &id, F &&valueProvider)
{
if (IsTempVarInUse(id)) {
SetTempVarValue(id, valueProvider());
}
}
private: private:
void ClearAvailableTempvars(); void ClearAvailableTempvars();
std::optional<const TempVariable> std::optional<const TempVariable>

View File

@ -51,6 +51,7 @@ void MacroSelection::HideSelectedMacro()
return; return;
} }
#ifndef UNIT_TEST
const auto m = ssWindow->ui->macros->GetCurrentMacro(); const auto m = ssWindow->ui->macros->GetCurrentMacro();
if (!m) { if (!m) {
return; return;
@ -61,6 +62,7 @@ void MacroSelection::HideSelectedMacro()
} }
qobject_cast<QListView *>(view())->setRowHidden(idx, true); qobject_cast<QListView *>(view())->setRowHidden(idx, true);
#endif // !UNIT_TEST
} }
void MacroSelection::HideGroups() void MacroSelection::HideGroups()

View File

@ -474,14 +474,17 @@ void AdvSceneSwitcher::on_runMacroInParallel_stateChanged(int value) const
macro->SetRunInParallel(value); macro->SetRunInParallel(value);
} }
void AdvSceneSwitcher::on_runMacroOnChange_stateChanged(int value) const void AdvSceneSwitcher::on_actionTriggerMode_currentIndexChanged(int index) const
{ {
auto macro = GetSelectedMacro(); auto macro = GetSelectedMacro();
if (!macro) { if (!macro) {
return; return;
} }
auto lock = LockContext(); auto lock = LockContext();
macro->SetMatchOnChange(value); const auto mode = static_cast<Macro::ActionTriggerMode>(
ui->actionTriggerMode->itemData(index).toInt());
macro->SetActionTriggerMode(mode);
} }
void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable) const void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable) const
@ -489,7 +492,7 @@ void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable) const
ui->macroName->setDisabled(disable); ui->macroName->setDisabled(disable);
ui->runMacro->setDisabled(disable); ui->runMacro->setDisabled(disable);
ui->runMacroInParallel->setDisabled(disable); ui->runMacroInParallel->setDisabled(disable);
ui->runMacroOnChange->setDisabled(disable); ui->actionTriggerMode->setDisabled(disable);
ui->macroEdit->SetControlsDisabled(disable); ui->macroEdit->SetControlsDisabled(disable);
} }
@ -519,10 +522,12 @@ void AdvSceneSwitcher::MacroSelectionChanged()
{ {
const QSignalBlocker b1(ui->macroName); const QSignalBlocker b1(ui->macroName);
const QSignalBlocker b2(ui->runMacroInParallel); const QSignalBlocker b2(ui->runMacroInParallel);
const QSignalBlocker b3(ui->runMacroOnChange); const QSignalBlocker b3(ui->actionTriggerMode);
ui->macroName->setText(macro->Name().c_str()); ui->macroName->setText(macro->Name().c_str());
ui->runMacroInParallel->setChecked(macro->RunInParallel()); ui->runMacroInParallel->setChecked(macro->RunInParallel());
ui->runMacroOnChange->setChecked(macro->MatchOnChange()); ui->actionTriggerMode->setCurrentIndex(
ui->actionTriggerMode->findData(static_cast<int>(
macro->GetActionTriggerMode())));
} }
macro->ResetUIHelpers(); macro->ResetUIHelpers();
@ -552,9 +557,9 @@ void AdvSceneSwitcher::HighlightOnChange() const
return; return;
} }
if (macro->OnChangePreventedActionsSince( if (macro->ActionTriggerModePreventedActionsSince(
lastOnChangeHighlightCheckTime)) { lastOnChangeHighlightCheckTime)) {
HighlightWidget(ui->runMacroOnChange, Qt::yellow, HighlightWidget(ui->actionTriggerMode, Qt::yellow,
Qt::transparent, true); Qt::transparent, true);
} }
@ -665,23 +670,49 @@ void AdvSceneSwitcher::SetupMacroTab()
SLOT(HighlightOnChange())); SLOT(HighlightOnChange()));
onChangeHighlightTimer.start(); onChangeHighlightTimer.start();
// Reserve more space for macro edit area than for the macro list
ui->macroListMacroEditSplitter->setStretchFactor(0, 1);
ui->macroListMacroEditSplitter->setStretchFactor(1, 4);
if (switcher->saveWindowGeo) {
if (shouldRestoreSplitter(
switcher->macroListMacroEditSplitterPosition)) {
ui->macroListMacroEditSplitter->setSizes(
switcher->macroListMacroEditSplitterPosition);
}
}
SetupMacroSearchWidgets(ui->macroSearchLayout, ui->macroSearchText, SetupMacroSearchWidgets(ui->macroSearchLayout, ui->macroSearchText,
ui->macroSearchClear, ui->macroSearchType, ui->macroSearchClear, ui->macroSearchType,
ui->macroSearchRegex, ui->macroSearchRegex,
ui->macroSearchShowSettings, ui->macroSearchShowSettings,
[this]() { ui->macros->RefreshFilter(); }); [this]() { ui->macros->RefreshFilter(); });
static const std::vector<
std::pair<Macro::ActionTriggerMode, const char *>>
actionTriggerModes = {
{Macro::ActionTriggerMode::ALWAYS,
"AdvSceneSwitcher.macroTab.actionTriggerMode.always"},
{Macro::ActionTriggerMode::MACRO_RESULT_CHANGED,
"AdvSceneSwitcher.macroTab.actionTriggerMode.onOverallChange"},
{Macro::ActionTriggerMode::ANY_CONDITION_CHANGED,
"AdvSceneSwitcher.macroTab.actionTriggerMode.onAnyConditionChange"},
{Macro::ActionTriggerMode::ANY_CONDITION_TRIGGERED,
"AdvSceneSwitcher.macroTab.actionTriggerMode.onAnyConditionTriggered"},
};
for (const auto &[mode, name] : actionTriggerModes) {
ui->actionTriggerMode->addItem(obs_module_text(name),
static_cast<int>(mode));
}
ui->macroListBox->setSizePolicy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
ui->macroListBox->setMinimumWidth(0);
ui->macroEditGroup->setSizePolicy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
ui->macroEditGroup->setMinimumWidth(0);
if (shouldRestoreSplitter(
switcher->macroListMacroEditSplitterPosition)) {
ui->macroListMacroEditSplitter->setSizes(
switcher->macroListMacroEditSplitterPosition);
} else {
QTimer::singleShot(0, this, [this]() {
const auto totalWidth =
ui->macroListMacroEditSplitter->width();
ui->macroListMacroEditSplitter->setSizes(
{totalWidth / 5, totalWidth * 4 / 5});
});
}
} }
void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos) void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)

View File

@ -224,7 +224,8 @@ void MacroTreeItem::HighlightIfExecuted()
if (!wasHighlighted && if (!wasHighlighted &&
_lastHighlightCheckTime.time_since_epoch().count() != 0 && _lastHighlightCheckTime.time_since_epoch().count() != 0 &&
_macro->OnChangePreventedActionsSince(_lastHighlightCheckTime)) { _macro->ActionTriggerModePreventedActionsSince(
_lastHighlightCheckTime)) {
HighlightWidget(this, Qt::yellow, QColor(0, 0, 0, 0), true); HighlightWidget(this, Qt::yellow, QColor(0, 0, 0, 0), true);
} }

View File

@ -111,7 +111,7 @@ static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
const auto startTime = std::chrono::high_resolution_clock::now(); const auto startTime = std::chrono::high_resolution_clock::now();
bool conditionMatched = false; bool conditionMatched = false;
condition->WithLock([&condition, &conditionMatched]() { condition->WithLock([&condition, &conditionMatched]() {
conditionMatched = condition->CheckCondition(); conditionMatched = condition->EvaluateCondition();
}); });
const auto endTime = std::chrono::high_resolution_clock::now(); const auto endTime = std::chrono::high_resolution_clock::now();
const auto timeSpent = endTime - startTime; const auto timeSpent = endTime - startTime;
@ -241,12 +241,36 @@ bool Macro::CheckConditions(bool ignorePause)
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched); vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
_conditionSateChanged = _lastMatched != _matched; _actionModeMatch = false;
switch (_actionTriggerMode) {
case Macro::ActionTriggerMode::ALWAYS:
_actionModeMatch = true;
break;
case Macro::ActionTriggerMode::MACRO_RESULT_CHANGED:
_actionModeMatch = _lastMatched != _matched;
break;
case Macro::ActionTriggerMode::ANY_CONDITION_CHANGED:
for (const auto &condition : _conditions) {
if (condition->HasChanged()) {
_actionModeMatch = true;
}
}
break;
case Macro::ActionTriggerMode::ANY_CONDITION_TRIGGERED:
for (const auto &condition : _conditions) {
if (condition->IsRisingEdge()) {
_actionModeMatch = true;
}
}
break;
default:
break;
}
const bool hasActionsToExecute = _matched ? (_actions.size() > 0) const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
: (_elseActions.size() > 0); : (_elseActions.size() > 0);
if (!_conditionSateChanged && _performActionsOnChange && if (!_actionModeMatch && hasActionsToExecute) {
hasActionsToExecute) { _lastActionRunModePreventTime =
_lastOnChangeActionsPreventedTime =
std::chrono::high_resolution_clock::now(); std::chrono::high_resolution_clock::now();
} }
@ -306,9 +330,9 @@ bool Macro::WasExecutedSince(const TimePoint &time) const
return _lastExecutionTime > time; return _lastExecutionTime > time;
} }
bool Macro::OnChangePreventedActionsSince(const TimePoint &time) const bool Macro::ActionTriggerModePreventedActionsSince(const TimePoint &time) const
{ {
return _lastOnChangeActionsPreventedTime > time; return _lastActionRunModePreventTime > time;
} }
Macro::TimePoint Macro::GetLastExecutionTime() const Macro::TimePoint Macro::GetLastExecutionTime() const
@ -342,10 +366,9 @@ bool Macro::ShouldRunActions() const
const bool hasActionsToExecute = const bool hasActionsToExecute =
!_paused && (_matched || _elseActions.size() > 0) && !_paused && (_matched || _elseActions.size() > 0) &&
(!_performActionsOnChange || _conditionSateChanged); _actionModeMatch;
if (VerboseLoggingEnabled() && _performActionsOnChange && if (VerboseLoggingEnabled() && !_actionModeMatch) {
!_conditionSateChanged) {
if (_matched && _actions.size() > 0) { if (_matched && _actions.size() > 0) {
blog(LOG_INFO, "skip actions for Macro %s (on change)", blog(LOG_INFO, "skip actions for Macro %s (on change)",
_name.c_str()); _name.c_str());
@ -376,10 +399,24 @@ void Macro::ResetTimers()
_lastExecutionTime = {}; _lastExecutionTime = {};
} }
void Macro::SetActionTriggerMode(ActionTriggerMode mode)
{
_actionTriggerMode = mode;
}
Macro::ActionTriggerMode Macro::GetActionTriggerMode() const
{
return _actionTriggerMode;
}
bool Macro::RunActionsHelper( bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actionsToRun, const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
bool ignorePause) bool ignorePause)
{ {
if (_paused && !ignorePause) {
return true;
}
// Create copy of action list as elements might be removed, inserted, or // Create copy of action list as elements might be removed, inserted, or
// reordered while actions are currently being executed. // reordered while actions are currently being executed.
auto actions = actionsToRun; auto actions = actionsToRun;
@ -429,11 +466,6 @@ bool Macro::WasPausedSince(const TimePoint &time) const
return _lastUnpauseTime > time; return _lastUnpauseTime > time;
} }
void Macro::SetMatchOnChange(bool onChange)
{
_performActionsOnChange = onChange;
}
void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone) void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone)
{ {
_stopActionsIfNotDone = stopActionsIfNotDone; _stopActionsIfNotDone = stopActionsIfNotDone;
@ -754,7 +786,8 @@ bool Macro::Save(obs_data_t *obj, bool saveForCopy) const
obs_data_set_bool(obj, "pause", _paused); obs_data_set_bool(obj, "pause", _paused);
obs_data_set_bool(obj, "parallel", _runInParallel); obs_data_set_bool(obj, "parallel", _runInParallel);
obs_data_set_bool(obj, "checkConditionsInParallel", _checkInParallel); obs_data_set_bool(obj, "checkConditionsInParallel", _checkInParallel);
obs_data_set_bool(obj, "onChange", _performActionsOnChange); obs_data_set_int(obj, "actionTriggerMode",
static_cast<int>(_actionTriggerMode));
obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart); obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart);
obs_data_set_bool(obj, "stopActionsIfNotDone", _stopActionsIfNotDone); obs_data_set_bool(obj, "stopActionsIfNotDone", _stopActionsIfNotDone);
obs_data_set_bool(obj, "useShortCircuitEvaluation", obs_data_set_bool(obj, "useShortCircuitEvaluation",
@ -840,7 +873,15 @@ bool Macro::Load(obs_data_t *obj)
} }
_runInParallel = obs_data_get_bool(obj, "parallel"); _runInParallel = obs_data_get_bool(obj, "parallel");
_checkInParallel = obs_data_get_bool(obj, "checkConditionsInParallel"); _checkInParallel = obs_data_get_bool(obj, "checkConditionsInParallel");
_performActionsOnChange = obs_data_get_bool(obj, "onChange"); if (obs_data_has_user_value(obj, "onChange")) {
const bool onChange = obs_data_get_bool(obj, "onChange");
_actionTriggerMode =
onChange ? ActionTriggerMode::MACRO_RESULT_CHANGED
: ActionTriggerMode::ALWAYS;
} else {
_actionTriggerMode = static_cast<ActionTriggerMode>(
obs_data_get_int(obj, "actionTriggerMode"));
}
_skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart"); _skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart");
_stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone"); _stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone");
_useShortCircuitEvaluation = _useShortCircuitEvaluation =
@ -1100,9 +1141,11 @@ void Macro::ClearHotkeys() const
void setHotkeyDescriptionHelper(const char *formatModuleText, void setHotkeyDescriptionHelper(const char *formatModuleText,
const std::string name, const obs_hotkey_id id) const std::string name, const obs_hotkey_id id)
{ {
#ifndef UNIT_TEST
QString format{obs_module_text(formatModuleText)}; QString format{obs_module_text(formatModuleText)};
QString hotkeyDesc = format.arg(QString::fromStdString(name)); QString hotkeyDesc = format.arg(QString::fromStdString(name));
obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str()); obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str());
#endif // !UNIT_TEST
} }
void Macro::SetHotkeysDesc() const void Macro::SetHotkeysDesc() const

View File

@ -26,6 +26,16 @@ class Macro {
public: public:
enum class PauseStateSaveBehavior { PERSIST, PAUSE, UNPAUSE }; enum class PauseStateSaveBehavior { PERSIST, PAUSE, UNPAUSE };
enum class ActionTriggerMode {
// Trigger always
ALWAYS,
// Trigger when macro match result flips
MACRO_RESULT_CHANGED,
// Trigger when any individual condition changes
ANY_CONDITION_CHANGED,
// Trigger when any individual condition evaluates to true
ANY_CONDITION_TRIGGERED,
};
Macro(const std::string &name = ""); Macro(const std::string &name = "");
Macro(const std::string &name, const GlobalMacroSettings &settings); Macro(const std::string &name, const GlobalMacroSettings &settings);
@ -54,8 +64,8 @@ public:
bool GetStop() const { return _stop; } bool GetStop() const { return _stop; }
void ResetTimers(); void ResetTimers();
void SetMatchOnChange(bool onChange); void SetActionTriggerMode(ActionTriggerMode);
bool MatchOnChange() const { return _performActionsOnChange; } ActionTriggerMode GetActionTriggerMode() const;
void SetSkipExecOnStart(bool skip) { _skipExecOnStart = skip; } void SetSkipExecOnStart(bool skip) { _skipExecOnStart = skip; }
bool SkipExecOnStart() const { return _skipExecOnStart; } bool SkipExecOnStart() const { return _skipExecOnStart; }
@ -137,7 +147,7 @@ public:
const QList<int> &GetElseActionSplitterPosition() const; const QList<int> &GetElseActionSplitterPosition() const;
bool HasValidSplitterPositions() const; bool HasValidSplitterPositions() const;
bool WasExecutedSince(const TimePoint &) const; bool WasExecutedSince(const TimePoint &) const;
bool OnChangePreventedActionsSince(const TimePoint &) const; bool ActionTriggerModePreventedActionsSince(const TimePoint &) const;
TimePoint GetLastExecutionTime() const; TimePoint GetLastExecutionTime() const;
void ResetUIHelpers(); void ResetUIHelpers();
@ -169,7 +179,7 @@ private:
TimePoint _lastCheckTime{}; TimePoint _lastCheckTime{};
TimePoint _lastUnpauseTime{}; TimePoint _lastUnpauseTime{};
TimePoint _lastExecutionTime{}; TimePoint _lastExecutionTime{};
TimePoint _lastOnChangeActionsPreventedTime{}; TimePoint _lastActionRunModePreventTime{};
std::vector<std::thread> _helperThreads; std::vector<std::thread> _helperThreads;
std::deque<std::shared_ptr<MacroCondition>> _conditions; std::deque<std::shared_ptr<MacroCondition>> _conditions;
@ -184,14 +194,13 @@ private:
bool _useShortCircuitEvaluation = false; bool _useShortCircuitEvaluation = false;
bool _useCustomConditionCheckInterval = false; bool _useCustomConditionCheckInterval = false;
Duration _customConditionCheckInterval = 0.3; Duration _customConditionCheckInterval = 0.3;
bool _conditionSateChanged = false; bool _actionModeMatch = false;
bool _runInParallel = false; bool _runInParallel = false;
bool _checkInParallel = false; bool _checkInParallel = false;
bool _matched = false; bool _matched = false;
std::future<void> _conditionCheckFuture; std::future<void> _conditionCheckFuture;
bool _lastMatched = false; bool _lastMatched = false;
bool _performActionsOnChange = true;
bool _skipExecOnStart = false; bool _skipExecOnStart = false;
bool _stopActionsIfNotDone = false; bool _stopActionsIfNotDone = false;
bool _paused = false; bool _paused = false;
@ -201,6 +210,9 @@ private:
obs_hotkey_id _unpauseHotkey = OBS_INVALID_HOTKEY_ID; obs_hotkey_id _unpauseHotkey = OBS_INVALID_HOTKEY_ID;
obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID; obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID;
ActionTriggerMode _actionTriggerMode =
ActionTriggerMode::MACRO_RESULT_CHANGED;
PauseStateSaveBehavior _pauseSaveBehavior = PauseStateSaveBehavior _pauseSaveBehavior =
PauseStateSaveBehavior::PERSIST; PauseStateSaveBehavior::PERSIST;

View File

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

View File

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

View File

@ -152,7 +152,7 @@ bool SwitcherData::VersionChanged(obs_data_t *obj, std::string currentVersion)
if (!obs_data_has_user_value(obj, "version")) { if (!obs_data_has_user_value(obj, "version")) {
return false; return false;
} }
switcher->firstBoot = false;
std::string previousVersion = obs_data_get_string(obj, "version"); std::string previousVersion = obs_data_get_string(obj, "version");
return previousVersion != currentVersion; return previousVersion != currentVersion;
} }

View File

@ -94,7 +94,6 @@ public:
bool stop = false; bool stop = false;
std::condition_variable cv; std::condition_variable cv;
bool firstBoot = true;
bool transitionActive = false; bool transitionActive = false;
bool sceneCollectionStop = false; bool sceneCollectionStop = false;
bool obsIsShuttingDown = false; bool obsIsShuttingDown = false;

View File

@ -54,31 +54,18 @@ void AskForBackup(obs_data_t *settings)
// or crashing. // or crashing.
// Therefore, we ask the user whether they want to back up the settings // Therefore, we ask the user whether they want to back up the settings
// asynchronously. // asynchronously.
//
// On macOS, an additional QTimer::singleShot wrapper is required for
// this to work correctly.
auto json = obs_data_get_json(settings); auto json = obs_data_get_json(settings);
static QString jsonQString = json ? json : ""; static QString jsonQString = json ? json : "";
static const auto askForBackupWrapper = [](void *) { static const auto askForBackupWrapper = [](void *) {
#ifdef __APPLE__ showBackupDialogs(jsonQString);
QTimer::singleShot(0,
static_cast<QMainWindow *>(
obs_frontend_get_main_window()),
[]() {
#endif
showBackupDialogs(jsonQString);
#ifdef __APPLE__
});
#endif
}; };
std::thread t([]() { AddFinishedLoadingStep([]() {
obs_queue_task(OBS_TASK_UI, askForBackupWrapper, nullptr, obs_queue_task(OBS_TASK_UI, askForBackupWrapper, nullptr,
false); false);
}); });
t.detach();
} }
void BackupSettingsOfCurrentVersion() void BackupSettingsOfCurrentVersion()

View File

@ -2,17 +2,18 @@
#include "log-helper.hpp" #include "log-helper.hpp"
#include "obs-module-helper.hpp" #include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp" #include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include <obs-module.h> #include <obs-module.h>
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QLabel>
#include <QMainWindow> #include <QMainWindow>
#include <QTimer> #include <QVBoxLayout>
#include <thread>
namespace advss { namespace advss {
@ -25,10 +26,29 @@ static constexpr bool handleUncleanShutdown = true;
#endif #endif
static bool wasCleanShutdown = false; static bool wasCleanShutdown = false;
static bool suppressCrashDialog = false;
bool GetSuppressCrashDialog()
{
return suppressCrashDialog;
}
void SetSuppressCrashDialog(bool suppress)
{
suppressCrashDialog = suppress;
}
static void setup(); static void setup();
static bool setupDone = []() { static bool setupDone = []() {
AddPluginInitStep(setup); AddPluginInitStep(setup);
AddSaveStep([](obs_data_t *obj) {
obs_data_set_bool(obj, "suppressCrashDialog",
suppressCrashDialog);
});
AddLoadStep([](obs_data_t *obj) {
suppressCrashDialog =
obs_data_get_bool(obj, "suppressCrashDialog");
});
return true; return true;
}(); }();
@ -108,8 +128,39 @@ static bool wasUncleanShutdown()
static void askForStartupSkip() static void askForStartupSkip()
{ {
bool skipStart = DisplayMessage( auto mainWindow =
obs_module_text("AdvSceneSwitcher.crashDetected"), true, false); static_cast<QMainWindow *>(obs_frontend_get_main_window());
auto dialog = new QDialog(mainWindow);
dialog->setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
dialog->setWindowFlags(dialog->windowFlags() &
~Qt::WindowContextHelpButtonHint);
auto layout = new QVBoxLayout(dialog);
auto label = new QLabel(
obs_module_text("AdvSceneSwitcher.crashDetected"), dialog);
label->setWordWrap(true);
layout->addWidget(label);
auto checkbox = new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.crashDetected.suppressCheckbox"),
dialog);
layout->addWidget(checkbox);
auto buttonbox = new QDialogButtonBox(
QDialogButtonBox::Yes | QDialogButtonBox::No, dialog);
QObject::connect(buttonbox, &QDialogButtonBox::accepted, dialog,
&QDialog::accept);
QObject::connect(buttonbox, &QDialogButtonBox::rejected, dialog,
&QDialog::reject);
layout->addWidget(buttonbox);
dialog->setLayout(layout);
bool skipStart = dialog->exec() == QDialog::Accepted;
suppressCrashDialog = checkbox->isChecked();
dialog->deleteLater();
if (!skipStart) { if (!skipStart) {
StartPlugin(); StartPlugin();
} }
@ -121,31 +172,22 @@ bool ShouldSkipPluginStartOnUncleanShutdown()
return false; return false;
} }
if (suppressCrashDialog) {
return false;
}
// This function is called while the plugin settings are being loaded. // This function is called while the plugin settings are being loaded.
// Blocking at this stage can cause issues such as OBS failing to start // Blocking at this stage can cause issues such as OBS failing to start
// or crashing. // or crashing.
// Therefore, we ask the user whether they want to start the plugin // Therefore, we ask the user whether they want to start the plugin
// asynchronously. // asynchronously.
//
// On macOS, an additional QTimer::singleShot wrapper is required for
// this to work correctly.
static const auto showDialogWrapper = [](void *) { static const auto showDialogWrapper = [](void *) {
#ifdef __APPLE__ askForStartupSkip();
QTimer::singleShot(0,
static_cast<QMainWindow *>(
obs_frontend_get_main_window()),
[]() {
#endif
askForStartupSkip();
#ifdef __APPLE__
});
#endif
}; };
std::thread t([]() { AddFinishedLoadingStep([]() {
obs_queue_task(OBS_TASK_UI, showDialogWrapper, nullptr, false); obs_queue_task(OBS_TASK_UI, showDialogWrapper, nullptr, false);
}); });
t.detach();
return true; return true;
} }

View File

@ -4,4 +4,7 @@ namespace advss {
bool ShouldSkipPluginStartOnUncleanShutdown(); bool ShouldSkipPluginStartOnUncleanShutdown();
bool GetSuppressCrashDialog();
void SetSuppressCrashDialog(bool suppress);
} // namespace advss } // namespace advss

View File

@ -1,12 +1,5 @@
#pragma once #pragma once
#ifdef UNIT_TEST
#define EXPORT
#define ADVSS_EXPORT
#else
#ifdef _MSC_VER #ifdef _MSC_VER
#define EXPORT __declspec(dllexport) #define EXPORT __declspec(dllexport)
#else #else
@ -19,5 +12,3 @@
#else #else
#define ADVSS_EXPORT Q_DECL_IMPORT #define ADVSS_EXPORT Q_DECL_IMPORT
#endif #endif
#endif // UNIT_TEST

View File

@ -7,9 +7,11 @@
namespace advss { namespace advss {
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent) FileSelection::FileSelection(FileSelection::Type type, QWidget *parent,
const QString &browseTitle)
: QWidget(parent), : QWidget(parent),
_type(type), _type(type),
_browseTitle(browseTitle),
_filePath(new VariableLineEdit(this)), _filePath(new VariableLineEdit(this)),
_browseButton( _browseButton(
new QPushButton(obs_module_text("AdvSceneSwitcher.browse"))) new QPushButton(obs_module_text("AdvSceneSwitcher.browse")))
@ -55,11 +57,14 @@ void FileSelection::BrowseButtonClicked()
QString defaultPath = ValidPathOrDesktop(_filePath->text()); QString defaultPath = ValidPathOrDesktop(_filePath->text());
QString path; QString path;
if (_type == FileSelection::Type::WRITE) { if (_type == FileSelection::Type::WRITE) {
path = QFileDialog::getSaveFileName(this, "", defaultPath); path = QFileDialog::getSaveFileName(this, _browseTitle,
defaultPath);
} else if (_type == FileSelection::Type::READ) { } else if (_type == FileSelection::Type::READ) {
path = QFileDialog::getOpenFileName(this, "", defaultPath); path = QFileDialog::getOpenFileName(this, _browseTitle,
defaultPath);
} else { } else {
path = QFileDialog::getExistingDirectory(this, "", defaultPath); path = QFileDialog::getExistingDirectory(this, _browseTitle,
defaultPath);
} }
if (path.isEmpty()) { if (path.isEmpty()) {

View File

@ -19,7 +19,7 @@ public:
EXPORT EXPORT
FileSelection(FileSelection::Type type = FileSelection::Type::READ, FileSelection(FileSelection::Type type = FileSelection::Type::READ,
QWidget *parent = 0); QWidget *parent = 0, const QString &browseTitle = "");
EXPORT void SetPath(const StringVariable &); EXPORT void SetPath(const StringVariable &);
EXPORT void SetPath(const QString &); EXPORT void SetPath(const QString &);
EXPORT QString GetPath() const; EXPORT QString GetPath() const;
@ -34,6 +34,7 @@ signals:
private: private:
Type _type; Type _type;
QString _browseTitle;
VariableLineEdit *_filePath; VariableLineEdit *_filePath;
QPushButton *_browseButton; QPushButton *_browseButton;
}; };

View File

@ -0,0 +1,463 @@
#include "first-run-wizard.hpp"
#include "log-helper.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include "macro-settings.hpp"
#include "platform-funcs.hpp"
#include "selection-helpers.hpp"
#include <obs-frontend-api.h>
#include <obs-data.h>
#include <util/config-file.h>
#include <QFrame>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QRegularExpression>
#include <QVBoxLayout>
namespace advss {
static constexpr char kConditionIdWindow[] = "window";
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
// ---------------------------------------------------------------------------
// OBS global config helpers
// ---------------------------------------------------------------------------
static constexpr char kConfigSection[] = "AdvancedSceneSwitcher";
static constexpr char kFirstRunKey[] = "firstRun";
bool IsFirstRun()
{
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0)
config_t *cfg = obs_frontend_get_user_config();
#else
config_t *cfg = obs_frontend_get_global_config();
#endif
if (!config_has_user_value(cfg, kConfigSection, kFirstRunKey)) {
return true;
}
return config_get_bool(cfg, kConfigSection, kFirstRunKey);
}
static void WriteFirstRun(bool value)
{
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0)
config_t *cfg = obs_frontend_get_user_config();
#else
config_t *cfg = obs_frontend_get_global_config();
#endif
config_set_bool(cfg, kConfigSection, kFirstRunKey, value);
config_save_safe(cfg, "tmp", nullptr);
}
static QString DetectFocusedWindow()
{
return QString::fromStdString(GetCurrentWindowTitle());
}
// ===========================================================================
// WelcomePage
// ===========================================================================
WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent)
{
setTitle(obs_module_text("FirstRunWizard.welcome.title"));
setSubTitle(obs_module_text("FirstRunWizard.welcome.subtitle"));
auto body = new QLabel(obs_module_text("FirstRunWizard.welcome.body"),
this);
body->setWordWrap(true);
body->setTextFormat(Qt::RichText);
auto layout = new QVBoxLayout(this);
layout->addWidget(body);
layout->addStretch();
}
// ===========================================================================
// SceneSelectionPage
// ===========================================================================
SceneSelectionPage::SceneSelectionPage(QWidget *parent) : QWizardPage(parent)
{
setTitle(obs_module_text("FirstRunWizard.scene.title"));
setSubTitle(obs_module_text("FirstRunWizard.scene.subtitle"));
auto label =
new QLabel(obs_module_text("FirstRunWizard.scene.label"), this);
_sceneCombo = new QComboBox(this);
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
// registerField with * suffix means the field is mandatory for Next
registerField("targetScene*", _sceneCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_sceneCombo, 1);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addStretch();
}
void SceneSelectionPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames())
_sceneCombo->addItem(name);
}
bool SceneSelectionPage::isComplete() const
{
return _sceneCombo->count() > 0 &&
!_sceneCombo->currentText().isEmpty();
}
// ===========================================================================
// WindowConditionPage
// ===========================================================================
WindowConditionPage::WindowConditionPage(QWidget *parent)
: QWizardPage(parent),
_detectTimer(new QTimer(this))
{
setTitle(obs_module_text("FirstRunWizard.window.title"));
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
this);
_windowEdit = new QLineEdit(this);
_windowEdit->setPlaceholderText(
obs_module_text("FirstRunWizard.window.placeholder"));
_autoDetect = new QPushButton(
obs_module_text("FirstRunWizard.window.autoDetect"), this);
_autoDetect->setToolTip(
obs_module_text("FirstRunWizard.window.autoDetectTooltip"));
registerField("windowTitle*", _windowEdit);
connect(_windowEdit, &QLineEdit::textChanged, this,
&QWizardPage::completeChanged);
connect(_autoDetect, &QPushButton::clicked, this,
&WindowConditionPage::onAutoDetectClicked);
connect(_detectTimer, &QTimer::timeout, this,
&WindowConditionPage::onCountdownTick);
auto row = new QHBoxLayout;
row->addWidget(label);
row->addWidget(_windowEdit, 1);
auto hint =
new QLabel(obs_module_text("FirstRunWizard.window.hint"), this);
hint->setTextFormat(Qt::RichText);
hint->setWordWrap(true);
auto layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addWidget(_autoDetect, 0, Qt::AlignLeft);
layout->addWidget(hint);
layout->addStretch();
}
void WindowConditionPage::initializePage()
{
if (_windowEdit->text().isEmpty()) {
QString detected = DetectFocusedWindow();
if (!detected.isEmpty()) {
_windowEdit->setText(detected);
}
}
}
bool WindowConditionPage::isComplete() const
{
return !_windowEdit->text().trimmed().isEmpty();
}
void WindowConditionPage::onAutoDetectClicked()
{
_countdown = 3;
_autoDetect->setEnabled(false);
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
_detectTimer->start(1000);
}
void WindowConditionPage::onCountdownTick()
{
--_countdown;
if (_countdown > 0) {
_autoDetect->setText(
QString(obs_module_text(
"FirstRunWizard.window.autoDetectCountdown"))
.arg(_countdown));
return;
}
_detectTimer->stop();
QString title = DetectFocusedWindow();
if (!title.isEmpty()) {
_windowEdit->setText(title);
}
_autoDetect->setEnabled(true);
_autoDetect->setText(
obs_module_text("FirstRunWizard.window.autoDetect"));
}
// ===========================================================================
// ReviewPage
// ===========================================================================
ReviewPage::ReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.review.title"));
setSubTitle(obs_module_text("FirstRunWizard.review.subtitle"));
_summary = new QLabel(this);
_summary->setWordWrap(true);
_summary->setTextFormat(Qt::RichText);
_summary->setFrameShape(QFrame::StyledPanel);
_summary->setContentsMargins(12, 12, 12, 12);
auto layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void ReviewPage::initializePage()
{
const QString scene = field("targetScene").toString();
const QString window = field("windowTitle").toString();
_summary->setText(
QString(obs_module_text("FirstRunWizard.review.summary"))
.arg(scene.toHtmlEscaped(), window.toHtmlEscaped()));
}
static QString escapeForRegex(const QString &input)
{
return QRegularExpression::escape(input);
}
bool ReviewPage::validatePage()
{
const QString scene = field("targetScene").toString();
const QString window = escapeForRegex(field("windowTitle").toString());
const std::string name = ("Window -> " + scene).toStdString();
// Build condition data blob
// ---------------------------------------------------------------
// Condition blob — mirrors MacroConditionWindow::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "window",
// "checkTitle": true,
// "window": "<user input>",
// "windowRegexConfig": {
// "enable": true, // use regex-style partial matching
// "partial": true, // match anywhere in the title
// "options": 3 // case-insensitive (QRegularExpression flags)
// },
// "focus": true, // only trigger when window is focused
// "version": 1
// }
// ---------------------------------------------------------------
OBSDataAutoRelease condSegment = obs_data_create();
obs_data_set_bool(condSegment, "enabled", true);
obs_data_set_int(condSegment, "version", 1);
OBSDataAutoRelease condRegex = obs_data_create();
obs_data_set_bool(condRegex, "enable", true);
obs_data_set_bool(condRegex, "partial", true);
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
OBSDataAutoRelease condData = obs_data_create();
obs_data_set_obj(condData, "segmentSettings", condSegment);
obs_data_set_string(condData, "id", "window");
obs_data_set_bool(condData, "checkTitle", true);
obs_data_set_string(condData, "window", window.toUtf8().constData());
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
obs_data_set_bool(condData, "focus", true);
obs_data_set_int(condData, "version", 1);
// Build action data blob
// ---------------------------------------------------------------
// Action blob — mirrors MacroActionSwitchScene::Save() output:
//
// {
// "segmentSettings": { "enabled": true, "version": 1 },
// "id": "scene_switch",
// "action": 0, // 0 = switch scene
// "sceneSelection": {
// "type": 0, // 0 = scene by name
// "name": "<scene>",
// "canvasSelection": "Main"
// },
// "transitionType": 1, // 1 = use scene's default transition
// "blockUntilTransitionDone": false,
// "sceneType": 0
// }
// ---------------------------------------------------------------
OBSDataAutoRelease actionSegment = obs_data_create();
obs_data_set_bool(actionSegment, "enabled", true);
obs_data_set_int(actionSegment, "version", 1);
OBSDataAutoRelease sceneSelection = obs_data_create();
obs_data_set_int(sceneSelection, "type", 0);
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
OBSDataAutoRelease actionData = obs_data_create();
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
obs_data_set_string(actionData, "id", "scene_switch");
obs_data_set_int(actionData, "action", 0);
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
obs_data_set_int(actionData, "transitionType", 1);
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
obs_data_set_int(actionData, "sceneType", 0);
if (!FirstRunWizard::CreateMacro(_macro, name, kConditionIdWindow,
condData, kActionIdSceneSwitch,
actionData)) {
QMessageBox::warning(
this,
obs_module_text("FirstRunWizard.review.errorTitle"),
QString(obs_module_text(
"FirstRunWizard.review.errorBody"))
.arg(window, scene));
_macro.reset();
// Still advance so the user is not stuck.
}
return true;
}
// ===========================================================================
// DonePage
// ===========================================================================
DonePage::DonePage(QWidget *parent) : QWizardPage(parent)
{
setTitle(obs_module_text("FirstRunWizard.done.title"));
setSubTitle(obs_module_text("FirstRunWizard.done.subtitle"));
auto body =
new QLabel(obs_module_text("FirstRunWizard.done.body"), this);
body->setWordWrap(true);
body->setTextFormat(Qt::RichText);
body->setOpenExternalLinks(true);
auto layout = new QVBoxLayout(this);
layout->addWidget(body);
layout->addStretch();
}
// ===========================================================================
// FirstRunWizard
// ===========================================================================
FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
{
setWindowTitle(obs_module_text("FirstRunWizard.windowTitle"));
setWizardStyle(QWizard::ModernStyle);
setMinimumSize(540, 420);
setPage(PAGE_WELCOME, new WelcomePage(this));
setPage(PAGE_SCENE, new SceneSelectionPage(this));
setPage(PAGE_WINDOW, new WindowConditionPage(this));
setPage(PAGE_REVIEW, new ReviewPage(this, _macro));
setPage(PAGE_DONE, new DonePage(this));
setStartId(PAGE_WELCOME);
setOption(QWizard::NoBackButtonOnLastPage, true);
setOption(QWizard::NoCancelButtonOnLastPage, true);
// Mark done on both Accept (Finish) and Reject (Cancel / close)
connect(this, &QWizard::accepted, this,
&FirstRunWizard::markFirstRunComplete);
connect(this, &QWizard::rejected, this,
&FirstRunWizard::markFirstRunComplete);
}
void FirstRunWizard::markFirstRunComplete()
{
WriteFirstRun(false);
}
// static
std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent)
{
auto wizard = new FirstRunWizard(parent);
wizard->exec();
wizard->deleteLater();
return wizard->_macro;
}
// static
bool FirstRunWizard::CreateMacro(std::shared_ptr<Macro> &macro,
const std::string &macroName,
const std::string &conditionId,
obs_data_t *conditionData,
const std::string &actionId,
obs_data_t *actionData)
{
// 1. Create and register the Macro
macro = std::make_shared<Macro>(macroName, GetGlobalMacroSettings());
if (!macro) {
blog(LOG_WARNING, "FirstRunWizard: Macro allocation failed");
return false;
}
// 2. Instantiate condition via factory, then hydrate via Load()
auto condition =
MacroConditionFactory::Create(conditionId, macro.get());
if (!condition) {
blog(LOG_WARNING,
"FirstRunWizard: condition factory returned null "
"for id '%s' — is the base plugin loaded?",
conditionId.c_str());
return false;
}
if (!condition->Load(conditionData)) {
blog(LOG_WARNING,
"FirstRunWizard: condition Load() failed for id '%s'",
conditionId.c_str());
return false;
}
macro->Conditions().emplace_back(condition);
// 3. Instantiate action via factory, then hydrate via Load()
auto action = MacroActionFactory::Create(actionId, macro.get());
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: action factory returned null "
"for id '%s' — is the base plugin loaded?",
actionId.c_str());
return false;
}
if (!action->Load(actionData)) {
blog(LOG_WARNING,
"FirstRunWizard: action Load() failed for id '%s'",
actionId.c_str());
return false;
}
macro->Actions().emplace_back(action);
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", macroName.c_str());
return true;
}
} // namespace advss

View File

@ -0,0 +1,128 @@
#pragma once
#include <obs-data.h>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <QWizard>
#include <QWizardPage>
#include <string>
namespace advss {
class Macro;
bool IsFirstRun();
// ---------------------------------------------------------------------------
// Page IDs
// ---------------------------------------------------------------------------
enum WizardPageId {
PAGE_WELCOME = 0,
PAGE_SCENE,
PAGE_WINDOW,
PAGE_REVIEW,
PAGE_DONE,
};
// ---------------------------------------------------------------------------
// WelcomePage
// ---------------------------------------------------------------------------
class WelcomePage : public QWizardPage {
Q_OBJECT
public:
explicit WelcomePage(QWidget *parent = nullptr);
int nextId() const override { return PAGE_SCENE; }
};
// ---------------------------------------------------------------------------
// SceneSelectionPage
// Registers wizard field "targetScene" (QString).
// ---------------------------------------------------------------------------
class SceneSelectionPage : public QWizardPage {
Q_OBJECT
public:
explicit SceneSelectionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW; }
private:
QComboBox *_sceneCombo;
};
// ---------------------------------------------------------------------------
// WindowConditionPage
// Registers wizard field "windowTitle" (QString).
// Auto-detect button samples the focused window after a countdown.
// ---------------------------------------------------------------------------
class WindowConditionPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowConditionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_REVIEW; }
private slots:
void onAutoDetectClicked();
void onCountdownTick();
private:
QLineEdit *_windowEdit;
QPushButton *_autoDetect;
QTimer *_detectTimer;
int _countdown = 3;
};
// ---------------------------------------------------------------------------
// ReviewPage
// Displays a summary and calls FirstRunWizard::CreateMacro() on Finish.
// ---------------------------------------------------------------------------
class ReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit ReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<Macro> &_macro;
};
// ---------------------------------------------------------------------------
// DonePage
// ---------------------------------------------------------------------------
class DonePage : public QWizardPage {
Q_OBJECT
public:
explicit DonePage(QWidget *parent = nullptr);
int nextId() const override { return -1; }
};
// ---------------------------------------------------------------------------
// FirstRunWizard
// ---------------------------------------------------------------------------
class FirstRunWizard : public QWizard {
Q_OBJECT
public:
explicit FirstRunWizard(QWidget *parent = nullptr);
static std::shared_ptr<Macro> ShowWizard(QWidget *parent);
static bool
CreateMacro(std::shared_ptr<Macro> &macro, const std::string &macroName,
const std::string &conditionId, obs_data_t *conditionData,
const std::string &actionId, obs_data_t *actionData);
private:
void markFirstRunComplete();
std::shared_ptr<Macro> _macro;
};
} // namespace advss

View File

@ -32,6 +32,17 @@ ListControls::ListControls(QWidget *parent, bool reorder) : QToolBar(parent)
} }
} }
void ListControls::AddWidget(QWidget *widget)
{
addSeparator();
addWidget(widget);
}
void ListControls::AddSeparator()
{
addSeparator();
}
void ListControls::AddActionHelper(const char *theme, const char *className, void ListControls::AddActionHelper(const char *theme, const char *className,
const char *tooltip, const char *tooltip,
const std::function<void()> &signal) const std::function<void()> &signal)

View File

@ -10,6 +10,8 @@ class ADVSS_EXPORT ListControls final : public QToolBar {
public: public:
ListControls(QWidget *parent = nullptr, bool reorder = true); ListControls(QWidget *parent = nullptr, bool reorder = true);
void AddWidget(QWidget *widget);
void AddSeparator();
signals: signals:
void Add(); void Add();

View File

@ -1,13 +1,16 @@
#include "list-editor.hpp" #include "list-editor.hpp"
#include "ui-helpers.hpp" #include "ui-helpers.hpp"
#include <QEvent>
namespace advss { namespace advss {
ListEditor::ListEditor(QWidget *parent, bool reorder) ListEditor::ListEditor(QWidget *parent, bool reorder)
: QWidget(parent), : QWidget(parent),
_list(new QListWidget()), _list(new QListWidget()),
_controls(new ListControls(this, reorder)), _controls(new ListControls(this, reorder)),
_mainLayout(new QVBoxLayout()) _mainLayout(new QVBoxLayout()),
_placeholder(new QLabel(_list->viewport()))
{ {
QWidget::connect(_controls, SIGNAL(Add()), this, SLOT(Add())); QWidget::connect(_controls, SIGNAL(Add()), this, SLOT(Add()));
QWidget::connect(_controls, SIGNAL(Remove()), this, SLOT(Remove())); QWidget::connect(_controls, SIGNAL(Remove()), this, SLOT(Remove()));
@ -15,6 +18,19 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
QWidget::connect(_controls, SIGNAL(Down()), this, SLOT(Down())); QWidget::connect(_controls, SIGNAL(Down()), this, SLOT(Down()));
QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)), QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)),
this, SLOT(Clicked(QListWidgetItem *))); this, SLOT(Clicked(QListWidgetItem *)));
QWidget::connect(_list->model(),
SIGNAL(rowsInserted(QModelIndex, int, int)), this,
SLOT(UpdatePlaceholder()));
QWidget::connect(_list->model(),
SIGNAL(rowsRemoved(QModelIndex, int, int)), this,
SLOT(UpdatePlaceholder()));
QWidget::connect(_list->model(), SIGNAL(modelReset()), this,
SLOT(UpdatePlaceholder()));
_placeholder->setAlignment(Qt::AlignCenter);
_placeholder->setWordWrap(true);
_placeholder->hide();
_list->viewport()->installEventFilter(this);
_mainLayout->setContentsMargins(0, 0, 0, 0); _mainLayout->setContentsMargins(0, 0, 0, 0);
_mainLayout->addWidget(_list); _mainLayout->addWidget(_list);
@ -22,6 +38,40 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
setLayout(_mainLayout); setLayout(_mainLayout);
} }
void ListEditor::SetPlaceholderText(const QString &text)
{
_placeholder->setText(text);
UpdatePlaceholder();
}
void ListEditor::SetMinListHeight(int value)
{
_minHeight = value;
}
void ListEditor::SetMaxListHeight(int value)
{
_maxHeight = value;
}
void ListEditor::UpdatePlaceholder()
{
bool visible = !_placeholder->text().isEmpty() && _list->count() == 0;
_placeholder->setVisible(visible);
if (visible) {
_placeholder->setGeometry(_list->viewport()->rect());
}
}
bool ListEditor::eventFilter(QObject *obj, QEvent *event)
{
if (obj == _list->viewport() && event->type() == QEvent::Resize &&
_placeholder->isVisible()) {
_placeholder->setGeometry(_list->viewport()->rect());
}
return QWidget::eventFilter(obj, event);
}
void ListEditor::showEvent(QShowEvent *e) void ListEditor::showEvent(QShowEvent *e)
{ {
QWidget::showEvent(e); QWidget::showEvent(e);
@ -64,6 +114,24 @@ int ListEditor::GetIndexOfSignal() const
void ListEditor::UpdateListSize() void ListEditor::UpdateListSize()
{ {
SetHeightToContentHeight(_list); SetHeightToContentHeight(_list);
if (_list->count() == 0 && !_placeholder->text().isEmpty()) {
auto height = _list->fontMetrics().height() * 3;
_list->setMinimumHeight(height);
_list->setMaximumHeight(height);
}
if (_minHeight >= 0 && _list->minimumHeight() != _minHeight) {
_list->setMinimumHeight(_minHeight);
}
if (_maxHeight >= 0 && _list->maximumHeight() != _maxHeight) {
if (_list->minimumHeight() > _maxHeight) {
_list->setMinimumHeight(_maxHeight);
}
_list->setMaximumHeight(_maxHeight);
}
adjustSize(); adjustSize();
updateGeometry(); updateGeometry();
} }

View File

@ -2,6 +2,7 @@
#include "export-symbol-helper.hpp" #include "export-symbol-helper.hpp"
#include "list-controls.hpp" #include "list-controls.hpp"
#include <QLabel>
#include <QListWidget> #include <QListWidget>
#include <QLayout> #include <QLayout>
@ -13,9 +14,13 @@ class ADVSS_EXPORT ListEditor : public QWidget {
public: public:
ListEditor(QWidget *parent = nullptr, bool reorder = true); ListEditor(QWidget *parent = nullptr, bool reorder = true);
int count() const { return _list->count(); }; int count() const { return _list->count(); };
void SetPlaceholderText(const QString &text);
void SetMinListHeight(int);
void SetMaxListHeight(int);
protected: protected:
void showEvent(QShowEvent *); void showEvent(QShowEvent *);
bool eventFilter(QObject *, QEvent *);
private slots: private slots:
virtual void Add() = 0; virtual void Add() = 0;
@ -23,6 +28,7 @@ private slots:
virtual void Up(){}; virtual void Up(){};
virtual void Down(){}; virtual void Down(){};
virtual void Clicked(QListWidgetItem *) {} virtual void Clicked(QListWidgetItem *) {}
void UpdatePlaceholder();
protected: protected:
void UpdateListSize(); void UpdateListSize();
@ -31,6 +37,11 @@ protected:
QListWidget *_list; QListWidget *_list;
ListControls *_controls; ListControls *_controls;
QVBoxLayout *_mainLayout; QVBoxLayout *_mainLayout;
private:
QLabel *_placeholder;
int _minHeight = -1;
int _maxHeight = -1;
}; };
} // namespace advss } // namespace advss

View File

@ -1,7 +1,5 @@
#pragma once #pragma once
#ifndef UNIT_TEST
#include <util/base.h> #include <util/base.h>
#endif
namespace advss { namespace advss {
@ -9,6 +7,7 @@ namespace advss {
#define blog(level, msg, ...) #define blog(level, msg, ...)
#define vblog(level, msg, ...) #define vblog(level, msg, ...)
#define ablog(level, msg, ...) #define ablog(level, msg, ...)
#define mblog(level, msg, ...)
#else #else
// Print log with "[adv-ss] " prefix // Print log with "[adv-ss] " prefix

View File

@ -9,19 +9,15 @@ namespace advss {
std::variant<double, std::string> EvalMathExpression(const std::string &expr) std::variant<double, std::string> EvalMathExpression(const std::string &expr)
{ {
static bool setupDone = false;
static exprtk::symbol_table<double> symbolTable;
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<double> dis(0.0, 1.0);
static auto randomFunc = []() { static auto randomFunc = []() {
thread_local std::mt19937 gen(std::random_device{}());
thread_local std::uniform_real_distribution<double> dis(0.0,
1.0);
return dis(gen); return dis(gen);
}; };
if (!setupDone) { exprtk::symbol_table<double> symbolTable;
symbolTable.add_function("random", randomFunc); symbolTable.add_function("random", randomFunc);
setupDone = true;
}
exprtk::expression<double> expression; exprtk::expression<double> expression;
expression.register_symbol_table(symbolTable); expression.register_symbol_table(symbolTable);

View File

@ -3,12 +3,42 @@
#include "macro-signals.hpp" #include "macro-signals.hpp"
#include "switcher-data.hpp" #include "switcher-data.hpp"
#include "obs-frontend-api.h"
namespace advss { namespace advss {
static std::mutex initMutex; static std::mutex initMutex;
static std::mutex postLoadMutex; static std::mutex postLoadMutex;
static std::mutex finishLoadMutex;
static std::mutex mutex; static std::mutex mutex;
static bool setup();
static bool setupDonw = setup();
bool loadingFinished = false;
static std::vector<std::function<void()>> &getFinishLoadSteps();
static bool setup()
{
static auto handleEvent = [](enum obs_frontend_event event, void *) {
switch (event) {
case OBS_FRONTEND_EVENT_FINISHED_LOADING: {
std::lock_guard<std::mutex> lock(finishLoadMutex);
for (const auto &step : getFinishLoadSteps()) {
step();
}
getFinishLoadSteps().clear();
loadingFinished = true;
break;
}
default:
break;
};
};
obs_frontend_add_event_callback(handleEvent, nullptr);
return true;
}
static std::vector<std::function<void()>> &getPluginInitSteps() static std::vector<std::function<void()>> &getPluginInitSteps()
{ {
static std::vector<std::function<void()>> steps; static std::vector<std::function<void()>> steps;
@ -63,6 +93,12 @@ static std::vector<std::function<void()>> &getPostLoadSteps()
return steps; return steps;
} }
static std::vector<std::function<void()>> &getFinishLoadSteps()
{
static std::vector<std::function<void()>> steps;
return steps;
}
void SavePluginSettings(obs_data_t *obj) void SavePluginSettings(obs_data_t *obj)
{ {
GetSwitcher()->SaveSettings(obj); GetSwitcher()->SaveSettings(obj);
@ -178,6 +214,16 @@ void RunIntervalResetSteps()
} }
} }
void AddFinishedLoadingStep(std::function<void()> step)
{
std::lock_guard<std::mutex> lock(finishLoadMutex);
if (loadingFinished) {
return;
}
getFinishLoadSteps().emplace_back(step);
}
void AddStartStep(std::function<void()> step) void AddStartStep(std::function<void()> step)
{ {
std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> lock(mutex);

View File

@ -34,6 +34,9 @@ void RunStartSteps();
void RunStopSteps(); void RunStopSteps();
void RunIntervalResetSteps(); void RunIntervalResetSteps();
// Steps are executed after OBS_FRONTEND_EVENT_FINISHED_LOADING is fired
EXPORT void AddFinishedLoadingStep(std::function<void()>);
enum class NoMatchBehavior { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 }; enum class NoMatchBehavior { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 };
EXPORT void SetPluginNoMatchBehavior(NoMatchBehavior); EXPORT void SetPluginNoMatchBehavior(NoMatchBehavior);
EXPORT NoMatchBehavior GetPluginNoMatchBehavior(); EXPORT NoMatchBehavior GetPluginNoMatchBehavior();

View File

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

View File

@ -37,8 +37,8 @@ void CenterSplitterPosition(QSplitter *splitter)
void SetSplitterPositionByFraction(QSplitter *splitter, double fraction) void SetSplitterPositionByFraction(QSplitter *splitter, double fraction)
{ {
int value1 = (double)QWIDGETSIZE_MAX * fraction; int value1 = (int)((double)QWIDGETSIZE_MAX * fraction);
int value2 = (double)QWIDGETSIZE_MAX * (1.0 - fraction); int value2 = (int)((double)QWIDGETSIZE_MAX * (1.0 - fraction));
splitter->setSizes(QList<int>() << value1 << value2); splitter->setSizes(QList<int>() << value1 << value2);
} }

View File

@ -2,8 +2,22 @@
namespace advss { namespace advss {
#ifdef UNIT_TEST
std::mutex *GetSwitcherMutex()
{
static std::mutex m;
return &m;
}
std::unique_lock<std::mutex> *GetSwitcherLoopLock()
{
static std::mutex m;
static std::unique_lock<std::mutex> lock(m);
return &lock;
}
#else
std::mutex *GetSwitcherMutex(); std::mutex *GetSwitcherMutex();
std::unique_lock<std::mutex> *GetSwitcherLoopLock(); std::unique_lock<std::mutex> *GetSwitcherLoopLock();
#endif
std::mutex *GetMutex() std::mutex *GetMutex()
{ {
@ -52,4 +66,15 @@ void Lockable::WithLock(const std::function<void()> &func)
func(); func();
} }
SuspendLock::SuspendLock(Lockable &lockable)
: _mtx(static_cast<std::mutex &>(lockable._mtx))
{
_mtx.unlock();
}
SuspendLock::~SuspendLock()
{
_mtx.lock();
}
} // namespace advss } // namespace advss

View File

@ -2,7 +2,6 @@
#include "export-symbol-helper.hpp" #include "export-symbol-helper.hpp"
#include <functional> #include <functional>
#include <memory>
#include <mutex> #include <mutex>
namespace advss { namespace advss {
@ -52,6 +51,24 @@ public:
private: private:
PerInstanceMutex _mtx; 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 } // namespace advss

View File

@ -10,9 +10,12 @@
#include <QVariant> #include <QVariant>
#include <QAbstractItemView> #include <QAbstractItemView>
#include <atomic>
Q_DECLARE_METATYPE(advss::TempVariableRef); Q_DECLARE_METATYPE(advss::TempVariableRef);
static std::atomic<uint64_t> tempVarInUseGeneration{0};
namespace advss { namespace advss {
TempVariable::TempVariable(const std::string &id, const std::string &name, TempVariable::TempVariable(const std::string &id, const std::string &name,
@ -120,6 +123,60 @@ TempVariableRef TempVariable::GetRef() const
return ref; return ref;
} }
static bool refsContain(const std::vector<TempVariableRef> &refs,
const TempVariableRef &ref)
{
for (const auto &r : refs) {
if (r == ref) {
return true;
}
}
return false;
}
template<typename T>
static bool segmentsReferTo(const T &segments, const TempVariableRef &ref)
{
for (const auto &segment : segments) {
if (refsContain(segment->GetTempVarRefs(), ref)) {
return true;
}
}
return false;
}
bool TempVariable::IsInUse() const
{
const auto currentGen =
tempVarInUseGeneration.load(std::memory_order_relaxed);
if (_isInUseCacheGeneration == currentGen) {
return _isInUseCache;
}
bool inUse = false;
const auto ref = GetRef();
if (ref.HasValidID()) {
for (const auto &macro : GetAllMacros()) {
if (segmentsReferTo(macro->Conditions(), ref) ||
segmentsReferTo(macro->Actions(), ref) ||
segmentsReferTo(macro->ElseActions(), ref)) {
inUse = true;
break;
}
}
}
_isInUseCache = inUse;
_isInUseCacheGeneration = currentGen;
return inUse;
}
void IncrementTempVarInUseGeneration()
{
tempVarInUseGeneration.fetch_add(1, std::memory_order_relaxed);
}
TempVariableRef::SegmentType TempVariableRef::GetType() const TempVariableRef::SegmentType TempVariableRef::GetType() const
{ {
auto segment = _segment.lock(); auto segment = _segment.lock();
@ -665,13 +722,13 @@ TempVarSignalManager *TempVarSignalManager::Instance()
void NotifyUIAboutTempVarChange(MacroSegment *segment) void NotifyUIAboutTempVarChange(MacroSegment *segment)
{ {
obs_queue_task( IncrementTempVarInUseGeneration();
OBS_TASK_UI, QueueUITask(
[](void *segment) { [](void *segment) {
TempVarSignalManager::Instance()->SegmentTempVarsChanged( TempVarSignalManager::Instance()->SegmentTempVarsChanged(
(MacroSegment *)segment); (MacroSegment *)segment);
}, },
segment, false); segment);
} }
} // namespace advss } // namespace advss

View File

@ -44,6 +44,7 @@ public:
void SetValue(const std::string &val); void SetValue(const std::string &val);
void InvalidateValue(); void InvalidateValue();
TempVariableRef GetRef() const; TempVariableRef GetRef() const;
EXPORT bool IsInUse() const;
private: private:
std::string _id = ""; std::string _id = "";
@ -53,6 +54,8 @@ private:
mutable std::mutex _lastValuesMutex; mutable std::mutex _lastValuesMutex;
std::vector<std::string> _lastValues; std::vector<std::string> _lastValues;
bool _valueIsValid = false; bool _valueIsValid = false;
mutable bool _isInUseCache = false;
mutable uint64_t _isInUseCacheGeneration = UINT64_MAX;
std::weak_ptr<MacroSegment> _segment; std::weak_ptr<MacroSegment> _segment;
friend TempVariableSelection; friend TempVariableSelection;
@ -122,5 +125,6 @@ private:
}; };
void NotifyUIAboutTempVarChange(MacroSegment *); void NotifyUIAboutTempVarChange(MacroSegment *);
EXPORT void IncrementTempVarInUseGeneration();
} // namespace advss } // namespace advss

View File

@ -81,13 +81,13 @@ void GenericVariableSpinbox::DisableVariableSelection()
void GenericVariableSpinbox::setMinimum(double value) void GenericVariableSpinbox::setMinimum(double value)
{ {
_fixedValueInt->setMinimum(value); _fixedValueInt->setMinimum((int)value);
_fixedValueDouble->setMinimum(value); _fixedValueDouble->setMinimum(value);
} }
void GenericVariableSpinbox::setMaximum(double value) void GenericVariableSpinbox::setMaximum(double value)
{ {
_fixedValueInt->setMaximum(value); _fixedValueInt->setMaximum((int)value);
_fixedValueDouble->setMaximum(value); _fixedValueDouble->setMaximum(value);
} }

View File

@ -9,11 +9,12 @@ void StringVariable::Resolve() const
_resolvedValue = _value; _resolvedValue = _value;
return; return;
} }
if (_lastResolve == GetLastVariableChangeTime()) { const auto lastChange = GetLastVariableChangeTime();
if (_lastResolve == lastChange) {
return; return;
} }
_resolvedValue = SubstitueVariables(_value); _resolvedValue = SubstitueVariables(_value);
_lastResolve = GetLastVariableChangeTime(); _lastResolve = lastChange;
} }
StringVariable::operator std::string() const StringVariable::operator std::string() const
@ -81,7 +82,7 @@ std::string SubstitueVariables(std::string str)
const auto &variable = std::dynamic_pointer_cast<Variable>(v); const auto &variable = std::dynamic_pointer_cast<Variable>(v);
const std::string pattern = "${" + variable->Name() + "}"; const std::string pattern = "${" + variable->Name() + "}";
if (ReplaceAll(str, pattern, variable->Value(false))) { if (ReplaceAll(str, pattern, variable->Value(false))) {
variable->UpdateLastUsed(); variable->MarkAsUsed();
} }
} }
return str; return str;

View File

@ -12,16 +12,23 @@ static std::deque<std::shared_ptr<Item>> variables;
// Keep track of the last time a variable was changed to save some work when // Keep track of the last time a variable was changed to save some work when
// when resolving strings containing variables, etc. // when resolving strings containing variables, etc.
static std::mutex lastVariableChangeMutex;
static std::chrono::high_resolution_clock::time_point lastVariableChange{}; static std::chrono::high_resolution_clock::time_point lastVariableChange{};
static void setLastVariableChangeTime()
{
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
lastVariableChange = std::chrono::high_resolution_clock::now();
}
Variable::Variable() : Item() Variable::Variable() : Item()
{ {
lastVariableChange = std::chrono::high_resolution_clock::now(); setLastVariableChangeTime();
} }
Variable::~Variable() Variable::~Variable()
{ {
lastVariableChange = std::chrono::high_resolution_clock::now(); setLastVariableChangeTime();
} }
void Variable::Load(obs_data_t *obj) void Variable::Load(obs_data_t *obj)
@ -37,7 +44,7 @@ void Variable::Load(obs_data_t *obj)
SetValue(_defaultValue); SetValue(_defaultValue);
} }
lastVariableChange = std::chrono::high_resolution_clock::now(); setLastVariableChangeTime();
} }
void Variable::Save(obs_data_t *obj) const void Variable::Save(obs_data_t *obj) const
@ -45,8 +52,11 @@ void Variable::Save(obs_data_t *obj) const
Item::Save(obj); Item::Save(obj);
obs_data_set_int(obj, "saveAction", static_cast<int>(_saveAction)); obs_data_set_int(obj, "saveAction", static_cast<int>(_saveAction));
if (_saveAction == SaveAction::SAVE) { {
obs_data_set_string(obj, "value", _value.c_str()); std::lock_guard<std::mutex> lock(_mutex);
if (_saveAction == SaveAction::SAVE) {
obs_data_set_string(obj, "value", _value.c_str());
}
} }
obs_data_set_string(obj, "defaultValue", _defaultValue.c_str()); obs_data_set_string(obj, "defaultValue", _defaultValue.c_str());
@ -62,6 +72,18 @@ std::string Variable::Value(bool updateLastUsed) const
return _value; return _value;
} }
std::string Variable::GetPreviousValue() const
{
std::lock_guard<std::mutex> lock(_mutex);
return _previousValue;
}
int Variable::GetValueChangeCount() const
{
std::lock_guard<std::mutex> lock(_mutex);
return _valueChangeCount;
}
std::optional<double> Variable::DoubleValue() const std::optional<double> Variable::DoubleValue() const
{ {
return GetDouble(Value()); return GetDouble(Value());
@ -79,8 +101,11 @@ void Variable::SetValue(const std::string &value)
_value = value; _value = value;
UpdateLastUsed(); UpdateLastUsed();
UpdateLastChanged(); if (_previousValue != _value) {
lastVariableChange = std::chrono::high_resolution_clock::now(); _lastChanged = std::chrono::high_resolution_clock::now();
++_valueChangeCount;
}
setLastVariableChangeTime();
} }
void Variable::SetValue(double value) void Variable::SetValue(double value)
@ -90,6 +115,7 @@ void Variable::SetValue(double value)
std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
{ {
std::lock_guard<std::mutex> lock(_mutex);
if (_lastUsed.time_since_epoch().count() == 0) { if (_lastUsed.time_since_epoch().count() == 0) {
return {}; return {};
} }
@ -101,6 +127,7 @@ std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const
{ {
std::lock_guard<std::mutex> lock(_mutex);
if (_lastChanged.time_since_epoch().count() == 0) { if (_lastChanged.time_since_epoch().count() == 0) {
return {}; return {};
} }
@ -116,12 +143,10 @@ void Variable::UpdateLastUsed() const
_lastUsed = std::chrono::high_resolution_clock::now(); _lastUsed = std::chrono::high_resolution_clock::now();
} }
void Variable::UpdateLastChanged() void Variable::MarkAsUsed() const
{ {
if (_previousValue != _value) { std::lock_guard<std::mutex> lock(_mutex);
_lastChanged = std::chrono::high_resolution_clock::now(); UpdateLastUsed();
++_valueChangeCount;
}
} }
static void populateSaveActionSelection(QComboBox *list) static void populateSaveActionSelection(QComboBox *list)
@ -206,7 +231,7 @@ bool VariableSettingsDialog::AskForSettings(QWidget *parent, Variable &settings)
dialog._defaultValue->toPlainText().toStdString(); dialog._defaultValue->toPlainText().toStdString();
settings._saveAction = settings._saveAction =
static_cast<Variable::SaveAction>(dialog._save->currentIndex()); static_cast<Variable::SaveAction>(dialog._save->currentIndex());
lastVariableChange = std::chrono::high_resolution_clock::now(); setLastVariableChangeTime();
return true; return true;
} }
@ -461,6 +486,7 @@ void ImportVariables(obs_data_t *data)
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime() std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()
{ {
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
return lastVariableChange; return lastVariableChange;
} }

View File

@ -34,16 +34,15 @@ public:
EXPORT std::string Value(bool updateLastUsed = true) const; EXPORT std::string Value(bool updateLastUsed = true) const;
EXPORT std::optional<double> DoubleValue() const; EXPORT std::optional<double> DoubleValue() const;
EXPORT std::optional<int> IntValue() const; EXPORT std::optional<int> IntValue() const;
std::string GetPreviousValue() const { return _previousValue; }; std::string GetPreviousValue() const;
std::string GetDefaultValue() const { return _defaultValue; } std::string GetDefaultValue() const { return _defaultValue; }
EXPORT void SetValue(const std::string &value); EXPORT void SetValue(const std::string &value);
void SetValue(double value); void SetValue(double value);
SaveAction GetSaveAction() const { return _saveAction; } SaveAction GetSaveAction() const { return _saveAction; }
int GetValueChangeCount() const { return _valueChangeCount; } int GetValueChangeCount() const;
std::optional<uint64_t> GetSecondsSinceLastUse() const; std::optional<uint64_t> GetSecondsSinceLastUse() const;
std::optional<uint64_t> GetSecondsSinceLastChange() const; std::optional<uint64_t> GetSecondsSinceLastChange() const;
void UpdateLastUsed() const; void MarkAsUsed() const;
void UpdateLastChanged();
private: private:
SaveAction _saveAction = SaveAction::DONT_SAVE; SaveAction _saveAction = SaveAction::DONT_SAVE;
@ -55,6 +54,8 @@ private:
mutable std::chrono::high_resolution_clock::time_point _lastChanged; mutable std::chrono::high_resolution_clock::time_point _lastChanged;
mutable std::mutex _mutex; mutable std::mutex _mutex;
void UpdateLastUsed() const;
friend VariableSelection; friend VariableSelection;
friend VariableSettingsDialog; friend VariableSettingsDialog;
}; };

View File

@ -133,9 +133,9 @@ const std::vector<std::string> getOBSWindows()
return lastDoneHelper->windows; 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)); EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
// Also add OBS windows // Also add OBS windows
@ -147,20 +147,10 @@ void GetWindowList(std::vector<std::string> &windows)
// Add entry for OBS Studio itself - see GetCurrentWindowTitle() // Add entry for OBS Studio itself - see GetCurrentWindowTitle()
windows.emplace_back("OBS"); windows.emplace_back("OBS");
return windows;
} }
void GetWindowList(QStringList &windows) std::string GetCurrentWindowTitle()
{
windows.clear();
std::vector<std::string> w;
GetWindowList(w);
for (auto window : w) {
windows << QString::fromStdString(window);
}
}
void GetCurrentWindowTitle(std::string &title)
{ {
HWND window = GetForegroundWindow(); HWND window = GetForegroundWindow();
DWORD pid; DWORD pid;
@ -178,15 +168,15 @@ void GetCurrentWindowTitle(std::string &title)
// //
// So instead rely on Qt to get the title of the active window. // So instead rely on Qt to get the title of the active window.
if (GetCurrentProcessId() == pid) { if (GetCurrentProcessId() == pid) {
auto window = QApplication::activeWindow(); auto obsWindow = QApplication::activeWindow();
if (window) { if (obsWindow) {
title = window->windowTitle().toStdString(); return obsWindow->windowTitle().toStdString();
} else {
title = "OBS";
} }
return; return "OBS";
} }
std::string title;
GetWindowTitle(window, title); GetWindowTitle(window, title);
return title;
} }
static HWND getHWNDfromTitle(const std::string &title) static HWND getHWNDfromTitle(const std::string &title)
@ -350,22 +340,22 @@ bool IsFullscreen(const std::string &title)
return false; return false;
} }
void GetProcessList(QStringList &processes) QStringList GetProcessList()
{ {
QStringList processes;
HANDLE procSnapshot; HANDLE procSnapshot;
PROCESSENTRY32 procEntry; PROCESSENTRY32 procEntry;
procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (procSnapshot == INVALID_HANDLE_VALUE) { if (procSnapshot == INVALID_HANDLE_VALUE) {
return; return processes;
} }
procEntry.dwSize = sizeof(PROCESSENTRY32); procEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(procSnapshot, &procEntry)) { if (!Process32First(procSnapshot, &procEntry)) {
CloseHandle(procSnapshot); CloseHandle(procSnapshot);
return; return processes;
} }
do { do {
@ -383,9 +373,10 @@ void GetProcessList(QStringList &processes)
} while (Process32Next(procSnapshot, &procEntry)); } while (Process32Next(procSnapshot, &procEntry));
CloseHandle(procSnapshot); CloseHandle(procSnapshot);
return processes;
} }
static void GetForegroundProcessName(QString &proc) static QString getForegroundProcessNameStr()
{ {
// only checks if the current foreground window is from the same executable, // only checks if the current foreground window is from the same executable,
// may return true for any window from a program // may return true for any window from a program
@ -396,31 +387,90 @@ static void GetForegroundProcessName(QString &proc)
HANDLE process = OpenProcess( HANDLE process = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId); PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (process == NULL) { if (process == NULL) {
return; return {};
} }
WCHAR executablePath[600]; WCHAR executablePath[600];
GetModuleFileNameEx(process, 0, executablePath, 600); GetModuleFileNameEx(process, 0, executablePath, 600);
CloseHandle(process); CloseHandle(process);
proc = QString::fromWCharArray(executablePath) return QString::fromWCharArray(executablePath)
.split(QRegularExpression("(/|\\\\)")) .split(QRegularExpression("(/|\\\\)"))
.back(); .back();
} }
void GetForegroundProcessName(std::string &proc) std::string GetForegroundProcessName()
{ {
QString temp; return getForegroundProcessNameStr().toStdString();
GetForegroundProcessName(temp); }
proc = temp.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) bool IsInFocus(const QString &executable)
{ {
// only checks if the current foreground window is from the same executable, // only checks if the current foreground window is from the same executable,
// may return true for any window from a program // may return true for any window from a program
QString foregroundProc; const auto foregroundProc = getForegroundProcessNameStr();
GetForegroundProcessName(foregroundProc);
// True if executable switch equals current window // True if executable switch equals current window
bool equals = (executable == foregroundProc); bool equals = (executable == foregroundProc);

View File

@ -25,6 +25,8 @@ target_sources(
macro-action-obs-settings.hpp macro-action-obs-settings.hpp
macro-action-osc.cpp macro-action-osc.cpp
macro-action-osc.hpp macro-action-osc.hpp
macro-action-play-audio.cpp
macro-action-play-audio.hpp
macro-action-plugin-state.cpp macro-action-plugin-state.cpp
macro-action-plugin-state.hpp macro-action-plugin-state.hpp
macro-action-profile.cpp macro-action-profile.cpp
@ -57,6 +59,8 @@ target_sources(
macro-action-sequence.hpp macro-action-sequence.hpp
macro-action-source.cpp macro-action-source.cpp
macro-action-source.hpp macro-action-source.hpp
macro-action-source-interaction.cpp
macro-action-source-interaction.hpp
macro-action-streaming.cpp macro-action-streaming.cpp
macro-action-streaming.hpp macro-action-streaming.hpp
macro-action-studio-mode.cpp macro-action-studio-mode.cpp
@ -144,7 +148,17 @@ target_sources(
target_sources( target_sources(
${PROJECT_NAME} ${PROJECT_NAME}
PRIVATE utils/audio-helpers.cpp PRIVATE utils/source-interaction-recorder.cpp
utils/source-interaction-recorder.hpp
utils/source-interaction-step.cpp
utils/source-interaction-step.hpp
utils/source-interaction-step-edit.cpp
utils/source-interaction-step-edit.hpp
utils/source-interaction-step-list.cpp
utils/source-interaction-step-list.hpp
utils/source-preview-widget.cpp
utils/source-preview-widget.hpp
utils/audio-helpers.cpp
utils/audio-helpers.hpp utils/audio-helpers.hpp
utils/connection-manager.cpp utils/connection-manager.cpp
utils/connection-manager.hpp utils/connection-manager.hpp

View File

@ -144,7 +144,7 @@ void MacroActionFileEdit::UpdateEntryData()
} }
_actions->setCurrentIndex(static_cast<int>(_entryData->_action)); _actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_filePath->SetPath(QString::fromStdString(_entryData->_file)); _filePath->SetPath(_entryData->_file);
_text->setPlainText(_entryData->_text); _text->setPlainText(_entryData->_text);
adjustSize(); adjustSize();

View File

@ -9,6 +9,14 @@ namespace advss {
const std::string MacroActionFilter::id = "filter"; const std::string MacroActionFilter::id = "filter";
std::vector<TempVariableRef> MacroActionFilter::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionFilter::_registered = MacroActionFactory::Register( bool MacroActionFilter::_registered = MacroActionFactory::Register(
MacroActionFilter::id, MacroActionFilter::id,
{MacroActionFilter::Create, MacroActionFilterEdit::Create, {MacroActionFilter::Create, MacroActionFilterEdit::Create,
@ -428,6 +436,7 @@ void MacroActionFilterEdit::SelectionChanged(const TempVariableRef &var)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var; _entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
} }
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting) void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)

View File

@ -41,6 +41,8 @@ public:
SettingsInputMethod _settingsInputMethod = SettingsInputMethod _settingsInputMethod =
SettingsInputMethod::INDIVIDUAL_MANUAL; SettingsInputMethod::INDIVIDUAL_MANUAL;
std::vector<TempVariableRef> GetTempVarRefs() const;
SourceSelection _source; SourceSelection _source;
FilterSelection _filter; FilterSelection _filter;
Action _action = Action::ENABLE; Action _action = Action::ENABLE;

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

View File

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

View File

@ -0,0 +1,446 @@
#include "macro-action-play-audio.hpp"
#include "audio-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "sync-helpers.hpp"
#include <chrono>
#include <QFileInfo>
#include <QLabel>
namespace advss {
// Use a high output channel index that is unlikely to be claimed by OBS or
// other plugins. OBS supports channels 0-63; channel 0 is the main scene.
static constexpr uint32_t kPlaybackOutputChannel = 63;
const std::string MacroActionPlayAudio::id = "play_audio";
bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
MacroActionPlayAudio::id,
{MacroActionPlayAudio::Create, MacroActionPlayAudioEdit::Create,
"AdvSceneSwitcher.action.playAudio"});
static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
{
if (wantsOutput) {
obs_set_output_source(kPlaybackOutputChannel, nullptr);
} else {
obs_source_dec_active(source);
}
}
static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
int64_t maxMs = 0)
{
using namespace std::chrono_literals;
std::unique_lock<std::mutex> lock(*GetMutex());
SetMacroAbortWait(false);
// The media source needs time to open and decode before reaching
// PLAYING state. Poll until it starts (or the macro is stopped).
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (obs_source_media_get_state(source) ==
OBS_MEDIA_STATE_PLAYING) {
break;
}
GetMacroWaitCV().wait_for(lock, 10ms);
}
// Now wait for playback to end. Require two consecutive non-playing
// samples to avoid false positives on brief state transitions.
// If maxMs > 0, also stop once that many milliseconds have elapsed.
const auto playbackStart = std::chrono::steady_clock::now();
static const int kStopThreshold = 2;
int stoppedCount = 0;
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (maxMs > 0) {
const auto elapsed =
std::chrono::duration_cast<
std::chrono::milliseconds>(
std::chrono::steady_clock::now() -
playbackStart)
.count();
if (elapsed >= maxMs) {
obs_source_media_stop(source);
break;
}
}
if (obs_source_media_get_state(source) !=
OBS_MEDIA_STATE_PLAYING) {
if (++stoppedCount >= kStopThreshold) {
break;
}
} else {
stoppedCount = 0;
}
GetMacroWaitCV().wait_for(lock, 10ms);
}
}
bool MacroActionPlayAudio::PerformAction()
{
std::string path = _filePath;
if (path.empty()) {
return true;
}
if (!QFileInfo::exists(QString::fromStdString(path))) {
blog(LOG_WARNING, "audio file not found: \"%s\"", path.c_str());
return true;
}
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "local_file", path.c_str());
obs_data_set_bool(settings, "is_local_file", true);
obs_data_set_bool(settings, "looping", false);
// Disable automatic restart on activate so we control start explicitly.
obs_data_set_bool(settings, "restart_on_activate", false);
obs_data_set_bool(settings, "close_when_inactive", true);
OBSSourceAutoRelease source = obs_source_create_private(
"ffmpeg_source", "advss_play_audio", settings);
if (!source) {
blog(LOG_WARNING,
"Failed to create ffmpeg_source for audio playback of \"%s\"",
path.c_str());
return true;
}
const float vol =
DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
obs_source_set_volume(source, vol);
obs_source_set_monitoring_type(source, _monitorType);
// Fall back to monitor-only if all output tracks are deselected —
// there is no point routing through the output channel if the mixer
// mask would silence every track.
const bool wantsOutput =
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
(_audioMixers != 0);
if (!wantsOutput &&
_monitorType == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT) {
obs_source_set_monitoring_type(
source, OBS_MONITORING_TYPE_MONITOR_ONLY);
}
if (wantsOutput) {
obs_source_set_audio_mixers(source, _audioMixers);
// Route through a private scene so we can position the scene
// item far off-screen. This keeps the item "visible" (so audio
// is still mixed) while ensuring its video never intersects the
// output frame.
OBSSceneAutoRelease audioScene =
obs_scene_create_private("advss_audio_scene");
obs_sceneitem_t *item = obs_scene_add(audioScene, source);
if (item) {
struct vec2 pos = {-99999.0f, -99999.0f};
obs_sceneitem_set_pos(item, &pos);
}
obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene));
// audioScene released here; the output channel holds the
// remaining reference and keeps the scene alive.
} else {
obs_source_set_audio_mixers(source, 0);
obs_source_inc_active(source);
}
if (_useStartOffset && _startOffset.Milliseconds() > 0) {
obs_source_media_set_time(source, _startOffset.Milliseconds());
}
obs_source_media_play_pause(source, false);
const int64_t maxMs = _useDuration ? _playbackDuration.Milliseconds()
: 0;
if (_waitForCompletion) {
SetMacroAbortWait(false);
{
SuspendLock suspendLock(*this);
waitForPlaybackToEnd(GetMacro(), source, maxMs);
}
deactivatePlayback(source, wantsOutput);
return true;
}
// Keep the source alive in a background thread that cleans up
// once playback finishes. Grab an extra strong reference so the
// source survives beyond this stack frame.
auto rawSource = obs_source_get_ref(source);
auto macro = GetMacro();
std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
waitForPlaybackToEnd(macro, rawSource, maxMs);
deactivatePlayback(rawSource, wantsOutput);
obs_source_release(rawSource);
});
AddMacroHelperThread(macro, std::move(cleanupThread));
return true;
}
void MacroActionPlayAudio::LogAction() const
{
ablog(LOG_INFO,
"playing audio file \"%s\" at %.1f dB (monitoring type %d, wait %d)",
_filePath.UnresolvedValue().c_str(),
static_cast<double>(_volumeDB.GetFixedValue()), _monitorType,
(int)_waitForCompletion);
}
bool MacroActionPlayAudio::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_filePath.Save(obj, "filePath");
_volumeDB.Save(obj, "volumeDB");
obs_data_set_int(obj, "monitorType", _monitorType);
obs_data_set_int(obj, "audioMixers", _audioMixers);
obs_data_set_bool(obj, "useStartOffset", _useStartOffset);
_startOffset.Save(obj, "startOffset");
obs_data_set_bool(obj, "useDuration", _useDuration);
_playbackDuration.Save(obj, "playbackDuration");
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
return true;
}
bool MacroActionPlayAudio::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_filePath.Load(obj, "filePath");
_volumeDB.Load(obj, "volumeDB");
_monitorType = static_cast<obs_monitoring_type>(
obs_data_get_int(obj, "monitorType"));
_audioMixers =
static_cast<uint32_t>(obs_data_get_int(obj, "audioMixers"));
_useStartOffset = obs_data_get_bool(obj, "useStartOffset");
_startOffset.Load(obj, "startOffset");
_useDuration = obs_data_get_bool(obj, "useDuration");
_playbackDuration.Load(obj, "playbackDuration");
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
return true;
}
std::string MacroActionPlayAudio::GetShortDesc() const
{
return _filePath.UnresolvedValue();
}
std::shared_ptr<MacroAction> MacroActionPlayAudio::Create(Macro *m)
{
return std::make_shared<MacroActionPlayAudio>(m);
}
std::shared_ptr<MacroAction> MacroActionPlayAudio::Copy() const
{
return std::make_shared<MacroActionPlayAudio>(*this);
}
void MacroActionPlayAudio::ResolveVariablesToFixedValues()
{
_filePath.ResolveVariables();
_volumeDB.ResolveVariables();
_startOffset.ResolveVariables();
_playbackDuration.ResolveVariables();
}
MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
QWidget *parent, std::shared_ptr<MacroActionPlayAudio> entryData)
: QWidget(parent),
_filePath(new FileSelection(
FileSelection::Type::READ, this,
obs_module_text(
"AdvSceneSwitcher.action.playAudio.file.browse"))),
_volumeDB(new VariableDoubleSpinBox),
_monitorTypes(new QComboBox),
_tracksContainer(new QWidget),
_useStartOffset(new QCheckBox(this)),
_startOffset(new DurationSelection(this, true, 0.0)),
_useDuration(new QCheckBox(this)),
_playbackDuration(new DurationSelection(this, true, 0.0)),
_waitForCompletion(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.wait")))
{
_volumeDB->setMinimum(-100.0);
_volumeDB->setMaximum(0.0);
_volumeDB->setSuffix("dB");
_volumeDB->setSpecialValueText("-inf");
if (obs_audio_monitoring_available()) {
PopulateMonitorTypeSelection(_monitorTypes);
} else {
_monitorTypes->addItem(obs_module_text(
"AdvSceneSwitcher.action.playAudio.monitorUnavailable"));
_monitorTypes->setEnabled(false);
}
for (int i = 0; i < 6; ++i) {
_tracks[i] = new QCheckBox(QString::number(i + 1));
QWidget::connect(_tracks[i], SIGNAL(stateChanged(int)), this,
SLOT(TrackChanged()));
}
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
SLOT(FilePathChanged(const QString &)));
QWidget::connect(
_volumeDB,
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this, SLOT(VolumeDBChanged(const NumberVariable<double> &)));
QWidget::connect(_monitorTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(MonitorTypeChanged(int)));
QWidget::connect(_useStartOffset, SIGNAL(stateChanged(int)), this,
SLOT(UseStartOffsetChanged(int)));
QWidget::connect(_useDuration, SIGNAL(stateChanged(int)), this,
SLOT(UseDurationChanged(int)));
QWidget::connect(_startOffset,
SIGNAL(DurationChanged(const Duration &)), this,
SLOT(StartOffsetChanged(const Duration &)));
QWidget::connect(_playbackDuration,
SIGNAL(DurationChanged(const Duration &)), this,
SLOT(PlaybackDurationChanged(const Duration &)));
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
SLOT(WaitChanged(int)));
auto tracksLayout = new QHBoxLayout;
tracksLayout->setContentsMargins(0, 0, 0, 0);
tracksLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.playAudio.tracks")));
for (auto *cb : _tracks) {
tracksLayout->addWidget(cb);
}
tracksLayout->addStretch();
_tracksContainer->setLayout(tracksLayout);
auto volumeLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.volume"),
volumeLayout, {{"{{volumeDB}}", _volumeDB}});
auto monitorLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.monitor"),
monitorLayout, {{"{{monitorTypes}}", _monitorTypes}});
auto startOffsetLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.startOffset"),
startOffsetLayout,
{{"{{useStartOffset}}", _useStartOffset},
{"{{startOffset}}", _startOffset}});
auto playbackDurationLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.playbackDuration"),
playbackDurationLayout,
{{"{{useDuration}}", _useDuration},
{"{{playbackDuration}}", _playbackDuration}});
auto mainLayout = new QVBoxLayout;
mainLayout->addWidget(_filePath);
mainLayout->addLayout(volumeLayout);
mainLayout->addLayout(monitorLayout);
mainLayout->addWidget(_tracksContainer);
mainLayout->addLayout(startOffsetLayout);
mainLayout->addLayout(playbackDurationLayout);
mainLayout->addWidget(_waitForCompletion);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionPlayAudioEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_filePath->SetPath(_entryData->_filePath);
_volumeDB->SetValue(_entryData->_volumeDB);
_monitorTypes->setCurrentIndex(
static_cast<int>(_entryData->_monitorType));
for (int i = 0; i < 6; ++i) {
_tracks[i]->setChecked(_entryData->_audioMixers & (1u << i));
}
_tracksContainer->setVisible(_entryData->_monitorType !=
OBS_MONITORING_TYPE_MONITOR_ONLY);
_useStartOffset->setChecked(_entryData->_useStartOffset);
_startOffset->SetDuration(_entryData->_startOffset);
_startOffset->setEnabled(_entryData->_useStartOffset);
_useDuration->setChecked(_entryData->_useDuration);
_playbackDuration->SetDuration(_entryData->_playbackDuration);
_playbackDuration->setEnabled(_entryData->_useDuration);
_waitForCompletion->setChecked(_entryData->_waitForCompletion);
}
void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
_entryData->_filePath = path.toStdString();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionPlayAudioEdit::VolumeDBChanged(
const NumberVariable<double> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_volumeDB = value;
}
void MacroActionPlayAudioEdit::MonitorTypeChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_monitorType = static_cast<obs_monitoring_type>(value);
_tracksContainer->setVisible(value != OBS_MONITORING_TYPE_MONITOR_ONLY);
}
void MacroActionPlayAudioEdit::TrackChanged()
{
GUARD_LOADING_AND_LOCK();
uint32_t mixers = 0;
for (int i = 0; i < 6; ++i) {
if (_tracks[i]->isChecked()) {
mixers |= (1u << i);
}
}
_entryData->_audioMixers = mixers;
}
void MacroActionPlayAudioEdit::UseStartOffsetChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useStartOffset = value;
_startOffset->setEnabled(_entryData->_useStartOffset);
}
void MacroActionPlayAudioEdit::StartOffsetChanged(const Duration &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_startOffset = value;
}
void MacroActionPlayAudioEdit::UseDurationChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useDuration = value;
_playbackDuration->setEnabled(_entryData->_useDuration);
}
void MacroActionPlayAudioEdit::PlaybackDurationChanged(const Duration &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_playbackDuration = value;
}
void MacroActionPlayAudioEdit::WaitChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_waitForCompletion = value;
}
} // namespace advss

View File

@ -0,0 +1,91 @@
#pragma once
#include "macro-action-edit.hpp"
#include "duration-control.hpp"
#include "file-selection.hpp"
#include "variable-spinbox.hpp"
#include <obs.hpp>
#include <array>
#include <QCheckBox>
#include <QComboBox>
#include <QWidget>
namespace advss {
class MacroActionPlayAudio : public MacroAction {
public:
MacroActionPlayAudio(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; }
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
StringVariable _filePath =
obs_module_text("AdvSceneSwitcher.enterPath");
DoubleVariable _volumeDB = 0.0;
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_MONITOR_ONLY;
uint32_t _audioMixers = 0x3F; // tracks 1-6, all on by default
bool _useStartOffset = false;
Duration _startOffset;
bool _useDuration = false;
Duration _playbackDuration;
bool _waitForCompletion = false;
static bool _registered;
static const std::string id;
};
class MacroActionPlayAudioEdit : public QWidget {
Q_OBJECT
public:
MacroActionPlayAudioEdit(
QWidget *parent,
std::shared_ptr<MacroActionPlayAudio> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionPlayAudioEdit(
parent, std::dynamic_pointer_cast<MacroActionPlayAudio>(
action));
}
private slots:
void FilePathChanged(const QString &path);
void VolumeDBChanged(const NumberVariable<double> &value);
void MonitorTypeChanged(int value);
void TrackChanged();
void UseStartOffsetChanged(int);
void StartOffsetChanged(const Duration &);
void UseDurationChanged(int);
void PlaybackDurationChanged(const Duration &);
void WaitChanged(int value);
signals:
void HeaderInfoChanged(const QString &);
private:
FileSelection *_filePath;
VariableDoubleSpinBox *_volumeDB;
QComboBox *_monitorTypes;
QWidget *_tracksContainer;
std::array<QCheckBox *, 6> _tracks;
QCheckBox *_useStartOffset;
DurationSelection *_startOffset;
QCheckBox *_useDuration;
DurationSelection *_playbackDuration;
QCheckBox *_waitForCompletion;
std::shared_ptr<MacroActionPlayAudio> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@ -47,20 +47,28 @@ getNextMacros(std::vector<MacroRef> &macros, MacroRef &lastRandomMacroRef,
bool MacroActionRandom::PerformAction() bool MacroActionRandom::PerformAction()
{ {
if (_macros.size() == 0) { if (_macros.size() == 0) {
SetTempVarValue("macro", "");
return true; return true;
} }
auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat); auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat);
if (macros.size() == 0) { if (macros.size() == 0) {
SetTempVarValue("macro", "");
return true; return true;
} }
if (macros.size() == 1) { if (macros.size() == 1) {
lastRandomMacro = macros[0]; lastRandomMacro = macros[0];
SetTempVarValue("macro", GetMacroName(macros[0].get()));
return RunMacroActions(macros[0].get()); return RunMacroActions(macros[0].get());
} }
srand((unsigned int)time(0)); srand((unsigned int)time(0));
size_t idx = std::rand() % (macros.size()); size_t idx = std::rand() % (macros.size());
lastRandomMacro = macros[idx]; lastRandomMacro = macros[idx];
SetTempVarValue("macro", GetMacroName(macros[idx].get()));
return RunMacroActions(macros[idx].get()); return RunMacroActions(macros[idx].get());
} }
@ -85,6 +93,11 @@ bool MacroActionRandom::Load(obs_data_t *obj)
return true; return true;
} }
bool MacroActionRandom::PostLoad()
{
return MacroAction::PostLoad() && MultiMacroRefAction::PostLoad();
}
std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m) std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m)
{ {
return std::make_shared<MacroActionRandom>(m); return std::make_shared<MacroActionRandom>(m);
@ -95,6 +108,16 @@ std::shared_ptr<MacroAction> MacroActionRandom::Copy() const
return std::make_shared<MacroActionRandom>(*this); return std::make_shared<MacroActionRandom>(*this);
} }
void MacroActionRandom::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar(
"macro",
obs_module_text("AdvSceneSwitcher.tempVar.random.macro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.random.macro.description"));
}
MacroActionRandomEdit::MacroActionRandomEdit( MacroActionRandomEdit::MacroActionRandomEdit(
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData) QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
: QWidget(parent), : QWidget(parent),

View File

@ -15,6 +15,7 @@ public:
void LogAction() const; void LogAction() const;
bool Save(obs_data_t *obj) const; bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj); bool Load(obs_data_t *obj);
bool PostLoad();
std::string GetId() const { return id; }; std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m); static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const; std::shared_ptr<MacroAction> Copy() const;
@ -22,7 +23,10 @@ public:
bool _allowRepeat = false; bool _allowRepeat = false;
private: private:
void SetupTempVars();
MacroRef lastRandomMacro; MacroRef lastRandomMacro;
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;
}; };

View File

@ -22,12 +22,18 @@ const static std::map<MacroActionRecord::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.recording.type.pause"}, "AdvSceneSwitcher.action.recording.type.pause"},
{MacroActionRecord::Action::UNPAUSE, {MacroActionRecord::Action::UNPAUSE,
"AdvSceneSwitcher.action.recording.type.unpause"}, "AdvSceneSwitcher.action.recording.type.unpause"},
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(28, 0, 0)
{MacroActionRecord::Action::SPLIT, {MacroActionRecord::Action::SPLIT,
"AdvSceneSwitcher.action.recording.type.split"}, "AdvSceneSwitcher.action.recording.type.split"},
#endif
{MacroActionRecord::Action::FOLDER, {MacroActionRecord::Action::FOLDER,
"AdvSceneSwitcher.action.recording.type.changeOutputFolder"}, "AdvSceneSwitcher.action.recording.type.changeOutputFolder"},
{MacroActionRecord::Action::FILE_FORMAT, {MacroActionRecord::Action::FILE_FORMAT,
"AdvSceneSwitcher.action.recording.type.changeOutputFileFormat"}, "AdvSceneSwitcher.action.recording.type.changeOutputFileFormat"},
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
{MacroActionRecord::Action::ADD_CHAPTER,
"AdvSceneSwitcher.action.recording.type.addChapter"},
#endif
}; };
bool MacroActionRecord::PerformAction() bool MacroActionRecord::PerformAction()
@ -87,6 +93,13 @@ bool MacroActionRecord::PerformAction()
} }
break; break;
} }
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
case Action::ADD_CHAPTER:
if (!obs_frontend_recording_add_chapter(_chapterName.c_str())) {
blog(LOG_WARNING, "failed to add recoding chapter!");
}
break;
#endif
default: default:
break; break;
} }
@ -110,6 +123,7 @@ bool MacroActionRecord::Save(obs_data_t *obj) const
obs_data_set_int(obj, "action", static_cast<int>(_action)); obs_data_set_int(obj, "action", static_cast<int>(_action));
_folder.Save(obj, "folder"); _folder.Save(obj, "folder");
_fileFormat.Save(obj, "format"); _fileFormat.Save(obj, "format");
_chapterName.Save(obj, "chapterName");
return true; return true;
} }
@ -119,6 +133,7 @@ bool MacroActionRecord::Load(obs_data_t *obj)
_action = static_cast<Action>(obs_data_get_int(obj, "action")); _action = static_cast<Action>(obs_data_get_int(obj, "action"));
_folder.Load(obj, "folder"); _folder.Load(obj, "folder");
_fileFormat.Load(obj, "format"); _fileFormat.Load(obj, "format");
_chapterName.Load(obj, "chapterName");
return true; return true;
} }
@ -136,6 +151,7 @@ void MacroActionRecord::ResolveVariablesToFixedValues()
{ {
_folder.ResolveVariables(); _folder.ResolveVariables();
_fileFormat.ResolveVariables(); _fileFormat.ResolveVariables();
_chapterName.ResolveVariables();
} }
static inline void populateActionSelection(QComboBox *list) static inline void populateActionSelection(QComboBox *list)
@ -154,7 +170,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
_splitHint(new QLabel(obs_module_text( _splitHint(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.recording.split.hint"))), "AdvSceneSwitcher.action.recording.split.hint"))),
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)), _recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
_recordFileFormat(new VariableLineEdit(this)) _recordFileFormat(new VariableLineEdit(this)),
_chapterName(new VariableLineEdit(this))
{ {
populateActionSelection(_actions); populateActionSelection(_actions);
@ -164,6 +181,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
this, SLOT(FolderChanged(const QString &))); this, SLOT(FolderChanged(const QString &)));
QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this, QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this,
SLOT(FormatStringChanged())); SLOT(FormatStringChanged()));
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
SLOT(ChapterNameChanged()));
auto mainLayout = new QHBoxLayout; auto mainLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"), PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
@ -172,7 +191,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
{"{{pauseHint}}", _pauseHint}, {"{{pauseHint}}", _pauseHint},
{"{{splitHint}}", _splitHint}, {"{{splitHint}}", _splitHint},
{"{{recordFolder}}", _recordFolder}, {"{{recordFolder}}", _recordFolder},
{"{{recordFileFormat}}", _recordFileFormat}}); {"{{recordFileFormat}}", _recordFileFormat},
{"{{chapterName}}", _chapterName}});
setLayout(mainLayout); setLayout(mainLayout);
_entryData = entryData; _entryData = entryData;
@ -188,6 +208,7 @@ void MacroActionRecordEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_entryData->_action)); _actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_recordFolder->SetPath(_entryData->_folder); _recordFolder->SetPath(_entryData->_folder);
_recordFileFormat->setText(_entryData->_fileFormat); _recordFileFormat->setText(_entryData->_fileFormat);
_chapterName->setText(_entryData->_chapterName);
SetWidgetVisibility(); SetWidgetVisibility();
} }
@ -209,6 +230,12 @@ void MacroActionRecordEdit::FormatStringChanged()
_entryData->_fileFormat = _recordFileFormat->text().toStdString(); _entryData->_fileFormat = _recordFileFormat->text().toStdString();
} }
void MacroActionRecordEdit::ChapterNameChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_chapterName = _chapterName->text().toStdString();
}
void MacroActionRecordEdit::SetWidgetVisibility() void MacroActionRecordEdit::SetWidgetVisibility()
{ {
_pauseHint->setVisible(isPauseAction(_entryData->_action)); _pauseHint->setVisible(isPauseAction(_entryData->_action));
@ -218,6 +245,8 @@ void MacroActionRecordEdit::SetWidgetVisibility()
MacroActionRecord::Action::FOLDER); MacroActionRecord::Action::FOLDER);
_recordFileFormat->setVisible(_entryData->_action == _recordFileFormat->setVisible(_entryData->_action ==
MacroActionRecord::Action::FILE_FORMAT); MacroActionRecord::Action::FILE_FORMAT);
_chapterName->setVisible(_entryData->_action ==
MacroActionRecord::Action::ADD_CHAPTER);
} }
void MacroActionRecordEdit::ActionChanged(int value) void MacroActionRecordEdit::ActionChanged(int value)

View File

@ -29,11 +29,13 @@ public:
SPLIT, SPLIT,
FOLDER, FOLDER,
FILE_FORMAT, FILE_FORMAT,
ADD_CHAPTER,
}; };
Action _action = Action::STOP; Action _action = Action::STOP;
StringVariable _folder = QDir::homePath().toStdString() + "/Videos"; StringVariable _folder = QDir::homePath().toStdString() + "/Videos";
StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss"; StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss";
StringVariable _chapterName = "";
private: private:
static bool _registered; static bool _registered;
@ -60,6 +62,7 @@ private slots:
void ActionChanged(int value); void ActionChanged(int value);
void FolderChanged(const QString &); void FolderChanged(const QString &);
void FormatStringChanged(); void FormatStringChanged();
void ChapterNameChanged();
protected: protected:
QComboBox *_actions; QComboBox *_actions;
@ -67,6 +70,7 @@ protected:
QLabel *_splitHint; QLabel *_splitHint;
FileSelection *_recordFolder; FileSelection *_recordFolder;
VariableLineEdit *_recordFileFormat; VariableLineEdit *_recordFileFormat;
VariableLineEdit *_chapterName;
std::shared_ptr<MacroActionRecord> _entryData; std::shared_ptr<MacroActionRecord> _entryData;
private: private:

View File

@ -15,9 +15,20 @@ bool MacroActionRun::_registered = MacroActionFactory::Register(
bool MacroActionRun::PerformAction() bool MacroActionRun::PerformAction()
{ {
if (_wait) { if (_wait) {
_procConfig.StartProcessAndWait(_timeout.Milliseconds()); // Snapshot config before releasing the lock for the blocking wait
SetTempVarValues(); 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; return true;
} }

View File

@ -82,13 +82,22 @@ void MacroActionSequence::ResolveVariablesToFixedValues()
_resetIndex.ResolveVariables(); _resetIndex.ResolveVariables();
} }
void MacroActionSequence::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
bool MacroActionSequence::RunSequence() bool MacroActionSequence::RunSequence()
{ {
if (_macros.size() == 0) { if (_macros.size() == 0) {
SetTempVarValue("macro", "");
return true; return true;
} }
auto macro = GetNextMacro().GetMacro(); auto macro = GetNextMacro().GetMacro();
SetTempVarValue("macro", GetMacroName(macro.get()));
if (!macro.get()) { if (!macro.get()) {
return true; return true;
} }
@ -96,18 +105,21 @@ bool MacroActionSequence::RunSequence()
return RunMacroActions(macro.get()); return RunMacroActions(macro.get());
} }
bool MacroActionSequence::SetSequenceIndex() const bool MacroActionSequence::SetSequenceIndex()
{ {
auto macro = _macro.GetMacro(); auto macro = _macro.GetMacro();
if (!macro) { if (!macro) {
SetTempVarValue("nextMacro", "");
return true; return true;
} }
auto actions = GetMacroActions(macro.get()); auto actions = GetMacroActions(macro.get());
if (!actions) { if (!actions) {
SetTempVarValue("nextMacro", "");
return true; return true;
} }
std::string nextMacroName;
for (const auto &action : *actions) { for (const auto &action : *actions) {
if (action->GetId() != id) { if (action->GetId() != id) {
continue; continue;
@ -121,10 +133,34 @@ bool MacroActionSequence::SetSequenceIndex() const
// -2 is needed since the _lastIndex starts at -1 and the reset // -2 is needed since the _lastIndex starts at -1 and the reset
// index starts at 1 // index starts at 1
sequenceAction->_lastIdx = _resetIndex - 2; sequenceAction->_lastIdx = _resetIndex - 2;
nextMacroName = GetMacroName(
sequenceAction->GetNextMacro(false).GetMacro().get());
} }
SetTempVarValue("nextMacro", nextMacroName);
return true; return true;
} }
void MacroActionSequence::SetupTempVars()
{
MacroAction::SetupTempVars();
if (_action == Action::RUN_SEQUENCE) {
AddTempvar(
"macro",
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.macro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.macro.description"));
} else if (_action == Action::SET_INDEX) {
AddTempvar(
"nextMacro",
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.nextMacro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.nextMacro.description"));
}
}
bool MacroActionSequence::PerformAction() bool MacroActionSequence::PerformAction()
{ {
if (_action == Action::RUN_SEQUENCE) { if (_action == Action::RUN_SEQUENCE) {
@ -162,7 +198,7 @@ bool MacroActionSequence::Load(obs_data_t *obj)
LoadMacroList(obj, _macros); LoadMacroList(obj, _macros);
_restart = obs_data_get_bool(obj, "restart"); _restart = obs_data_get_bool(obj, "restart");
_macro.Load(obj); _macro.Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action")); SetAction(static_cast<Action>(obs_data_get_int(obj, "action")));
_resetIndex.Load(obj, "resetIndex"); _resetIndex.Load(obj, "resetIndex");
return true; return true;
} }
@ -256,7 +292,7 @@ void MacroActionSequenceEdit::UpdateEntryData()
_macroList->SetContent(_entryData->_macros); _macroList->SetContent(_entryData->_macros);
_restart->setChecked(_entryData->_restart); _restart->setChecked(_entryData->_restart);
_resetIndex->SetValue(_entryData->_resetIndex); _resetIndex->SetValue(_entryData->_resetIndex);
_actions->setCurrentIndex(static_cast<int>(_entryData->_action)); _actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
_macros->SetCurrentMacro(_entryData->_macro); _macros->SetCurrentMacro(_entryData->_macro);
SetWidgetVisibility(); SetWidgetVisibility();
adjustSize(); adjustSize();
@ -361,7 +397,7 @@ void MacroActionSequenceEdit::UpdateStatusLine()
void MacroActionSequenceEdit::ActionChanged(int value) void MacroActionSequenceEdit::ActionChanged(int value)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionSequence::Action>(value); _entryData->SetAction(static_cast<MacroActionSequence::Action>(value));
SetWidgetVisibility(); SetWidgetVisibility();
} }
@ -385,10 +421,10 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
ClearLayout(_layout); ClearLayout(_layout);
const auto action = _entryData->GetAction();
PlaceWidgets( PlaceWidgets(
obs_module_text( obs_module_text(
_entryData->_action == action == MacroActionSequence::Action::RUN_SEQUENCE
MacroActionSequence::Action::RUN_SEQUENCE
? "AdvSceneSwitcher.action.sequence.entry.run" ? "AdvSceneSwitcher.action.sequence.entry.run"
: "AdvSceneSwitcher.action.sequence.entry.setIndex"), : "AdvSceneSwitcher.action.sequence.entry.setIndex"),
_layout, _layout,
@ -396,15 +432,14 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
{"{{macros}}", _macros}, {"{{macros}}", _macros},
{"{{index}}", _resetIndex}}); {"{{index}}", _resetIndex}});
_macroList->setVisible(_entryData->_action == _macroList->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE); MacroActionSequence::Action::RUN_SEQUENCE);
_restart->setVisible(_entryData->_action == _restart->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE); MacroActionSequence::Action::RUN_SEQUENCE);
_statusLine->setVisible(_entryData->_action == _statusLine->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE); MacroActionSequence::Action::RUN_SEQUENCE);
_macros->setVisible(_entryData->_action == _macros->setVisible(action == MacroActionSequence::Action::SET_INDEX);
MacroActionSequence::Action::SET_INDEX); _resetIndex->setVisible(action ==
_resetIndex->setVisible(_entryData->_action ==
MacroActionSequence::Action::SET_INDEX); MacroActionSequence::Action::SET_INDEX);
adjustSize(); adjustSize();

View File

@ -34,7 +34,10 @@ public:
RUN_SEQUENCE, RUN_SEQUENCE,
SET_INDEX, SET_INDEX,
}; };
Action _action = Action::RUN_SEQUENCE;
Action GetAction() const { return _action; }
void SetAction(Action);
bool _restart = true; bool _restart = true;
IntVariable _resetIndex = 1; IntVariable _resetIndex = 1;
@ -43,7 +46,11 @@ public:
private: private:
bool RunSequence(); bool RunSequence();
bool SetSequenceIndex() const; bool SetSequenceIndex();
void SetupTempVars();
Action _action = Action::RUN_SEQUENCE;
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;

View File

@ -0,0 +1,292 @@
#include "macro-action-source-interaction.hpp"
#include "source-interaction-recorder.hpp"
#include "layout-helpers.hpp"
#include "selection-helpers.hpp"
#include "sync-helpers.hpp"
#include "log-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout>
namespace advss {
const std::string MacroActionSourceInteraction::id = "source_interaction";
bool MacroActionSourceInteraction::_registered = MacroActionFactory::Register(
MacroActionSourceInteraction::id,
{MacroActionSourceInteraction::Create,
MacroActionSourceInteractionEdit::Create,
"AdvSceneSwitcher.action.sourceInteraction"});
bool MacroActionSourceInteraction::PerformAction()
{
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source.GetSource());
if (!source) {
blog(LOG_WARNING, "source interaction: source not found");
return true;
}
uint32_t flags = obs_source_get_output_flags(source);
if (!(flags & OBS_SOURCE_INTERACTION)) {
blog(LOG_WARNING,
"source interaction: source \"%s\" does not support interaction",
obs_source_get_name(source));
return true;
}
for (const auto &step : _steps) {
PerformInteractionStep(source, step);
}
return true;
}
void MacroActionSourceInteraction::LogAction() const
{
ablog(LOG_INFO, "performed source interaction on \"%s\" (%d steps)",
_source.ToString(true).c_str(), (int)_steps.size());
}
bool MacroActionSourceInteraction::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_source.Save(obj);
OBSDataArrayAutoRelease arr = obs_data_array_create();
for (const auto &step : _steps) {
OBSDataAutoRelease stepObj = obs_data_create();
step.Save(stepObj);
obs_data_array_push_back(arr, stepObj);
}
obs_data_set_array(obj, "steps", arr);
return true;
}
bool MacroActionSourceInteraction::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_source.Load(obj);
_steps.clear();
OBSDataArrayAutoRelease arr = obs_data_get_array(obj, "steps");
size_t count = obs_data_array_count(arr);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease stepObj = obs_data_array_item(arr, i);
SourceInteractionStep step;
step.Load(stepObj);
_steps.push_back(step);
}
return true;
}
std::string MacroActionSourceInteraction::GetShortDesc() const
{
return _source.ToString();
}
void MacroActionSourceInteraction::ResolveVariablesToFixedValues()
{
for (auto &step : _steps) {
step.x.ResolveVariables();
step.y.ResolveVariables();
step.clickCount.ResolveVariables();
step.wheelDeltaX.ResolveVariables();
step.wheelDeltaY.ResolveVariables();
step.nativeVkey.ResolveVariables();
step.text.ResolveVariables();
step.waitMs.ResolveVariables();
}
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Create(Macro *m)
{
return std::make_shared<MacroActionSourceInteraction>(m);
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Copy() const
{
return std::make_shared<MacroActionSourceInteraction>(*this);
}
static QStringList getInteractableSourceNames()
{
QStringList names;
obs_enum_sources(
[](void *param, obs_source_t *source) -> bool {
uint32_t flags = obs_source_get_output_flags(source);
if (flags & OBS_SOURCE_INTERACTION) {
auto list = static_cast<QStringList *>(param);
const char *name = obs_source_get_name(source);
if (name) {
list->append(QString(name));
}
}
return true;
},
&names);
names.sort();
return names;
}
MacroActionSourceInteractionEdit::MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData)
: QWidget(parent),
_sources(new SourceSelectionWidget(this, getInteractableSourceNames,
true)),
_stepList(new SourceInteractionStepList(this)),
_recordButton(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record"))),
_noSelectionLabel(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.noSelection")))
{
_stepList->AddControlWidget(_recordButton);
auto sourceRow = new QHBoxLayout;
sourceRow->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.source")));
sourceRow->addWidget(_sources);
sourceRow->addStretch();
_noSelectionLabel->setAlignment(Qt::AlignCenter);
_noSelectionLabel->hide();
auto mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(sourceRow);
mainLayout->addWidget(_stepList);
mainLayout->addWidget(_noSelectionLabel);
setLayout(mainLayout);
connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this,
SLOT(SourceChanged(const SourceSelection &)));
connect(_stepList, &SourceInteractionStepList::StepsChanged, this,
&MacroActionSourceInteractionEdit::OnStepsChanged);
connect(_stepList, &SourceInteractionStepList::RowSelected, this,
&MacroActionSourceInteractionEdit::SetCurrentStepEditor);
connect(_recordButton, &QPushButton::clicked, this,
&MacroActionSourceInteractionEdit::OpenRecorder);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionSourceInteractionEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_sources->SetSource(_entryData->_source);
_stepList->SetSteps(_entryData->_steps);
_noSelectionLabel->setVisible(_stepList->count() > 0);
}
void MacroActionSourceInteractionEdit::SourceChanged(
const SourceSelection &source)
{
GUARD_LOADING_AND_LOCK();
_entryData->_source = source;
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionSourceInteractionEdit::OnStepsChanged(
const std::vector<SourceInteractionStep> &steps)
{
GUARD_LOADING_AND_LOCK();
_entryData->_steps = steps;
}
void MacroActionSourceInteractionEdit::StepChanged(
const SourceInteractionStep &step)
{
GUARD_LOADING_AND_LOCK();
int row = _stepList->CurrentRow();
if (row < 0 || row >= (int)_entryData->_steps.size()) {
return;
}
_entryData->_steps[row] = step;
_stepList->UpdateStep(row, step);
}
void MacroActionSourceInteractionEdit::OpenRecorder()
{
if (!_entryData) {
return;
}
OBSSourceAutoRelease source =
obs_weak_source_get_source(_entryData->_source.GetSource());
if (!source) {
QMessageBox::warning(
this,
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"),
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.invalidSource"));
return;
}
auto dlg = new SourceInteractionRecorder(
window(), _entryData->_source.GetSource());
connect(dlg, &SourceInteractionRecorder::StepsRecorded, this,
&MacroActionSourceInteractionEdit::AcceptRecorded);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->show();
}
void MacroActionSourceInteractionEdit::AcceptRecorded(
const std::vector<SourceInteractionStep> &steps)
{
if (!_entryData) {
return;
}
{
auto lock = _entryData->Lock();
_entryData->_steps.insert(_entryData->_steps.end(),
steps.begin(), steps.end());
}
_stepList->SetSteps(_entryData->_steps);
}
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
{
const QSignalBlocker b(this);
UpdateEntryData();
}
void MacroActionSourceInteractionEdit::SetCurrentStepEditor(int row)
{
if (_stepEditor) {
delete _stepEditor;
_stepEditor = nullptr;
}
if (!_entryData || row < 0 || row >= (int)_entryData->_steps.size()) {
_noSelectionLabel->setVisible(_stepList->count() > 0);
return;
}
_noSelectionLabel->setVisible(false);
_stepEditor =
new SourceInteractionStepEdit(this, _entryData->_steps[row]);
static_cast<QVBoxLayout *>(layout())->addWidget(_stepEditor);
connect(_stepEditor, &SourceInteractionStepEdit::StepChanged, this,
&MacroActionSourceInteractionEdit::StepChanged);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@ -0,0 +1,81 @@
#pragma once
#include "macro-action-edit.hpp"
#include "source-selection.hpp"
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-interaction-step-edit.hpp"
#include <QLabel>
#include <QPushButton>
namespace advss {
class MacroActionSourceInteraction : public MacroAction {
public:
MacroActionSourceInteraction(Macro *m) : MacroAction(m) {}
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
bool PerformAction();
void LogAction() const;
void ResolveVariablesToFixedValues() override;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; }
std::string GetShortDesc() const;
SourceSelection _source;
std::vector<SourceInteractionStep> _steps;
private:
static bool _registered;
static const std::string id;
};
class MacroActionSourceInteractionEdit : public QWidget {
Q_OBJECT
public:
MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData =
nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionSourceInteractionEdit(
parent,
std::dynamic_pointer_cast<MacroActionSourceInteraction>(
action));
}
private slots:
void SourceChanged(const SourceSelection &);
void OnStepsChanged(const std::vector<SourceInteractionStep> &);
void StepChanged(const SourceInteractionStep &);
void OpenRecorder();
void AcceptRecorded(const std::vector<SourceInteractionStep> &);
signals:
void HeaderInfoChanged(const QString &);
protected:
void showEvent(QShowEvent *event) override;
private:
void SetCurrentStepEditor(int row);
SourceSelectionWidget *_sources;
SourceInteractionStepList *_stepList;
QPushButton *_recordButton;
SourceInteractionStepEdit *_stepEditor = nullptr;
QLabel *_noSelectionLabel;
std::shared_ptr<MacroActionSourceInteraction> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@ -13,6 +13,14 @@ namespace advss {
const std::string MacroActionSource::id = "source"; const std::string MacroActionSource::id = "source";
std::vector<TempVariableRef> MacroActionSource::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionSource::_registered = MacroActionFactory::Register( bool MacroActionSource::_registered = MacroActionFactory::Register(
MacroActionSource::id, MacroActionSource::id,
{MacroActionSource::Create, MacroActionSourceEdit::Create, {MacroActionSource::Create, MacroActionSourceEdit::Create,
@ -45,6 +53,10 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.source.type.closeFilterDialog"}, "AdvSceneSwitcher.action.source.type.closeFilterDialog"},
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG, {MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
"AdvSceneSwitcher.action.source.type.closePropertiesDialog"}, "AdvSceneSwitcher.action.source.type.closePropertiesDialog"},
{MacroActionSource::Action::GET_SETTING,
"AdvSceneSwitcher.action.source.type.getSetting"},
{MacroActionSource::Action::GET_SETTINGS,
"AdvSceneSwitcher.action.source.type.getSettings"},
}; };
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = { const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
@ -273,6 +285,22 @@ bool MacroActionSource::PerformAction()
"OBSBasicProperties"); "OBSBasicProperties");
}); });
break; break;
case Action::GET_SETTING: {
const auto value =
GetSourceSettingValue(_source.GetSource(), _setting);
if (value) {
SetTempVarValue("setting", *value);
}
break;
}
case Action::GET_SETTINGS: {
const auto settings =
GetSourceSettings(_source.GetSource(), true);
if (settings) {
SetTempVarValue("settings", *settings);
}
break;
}
default: default:
break; break;
} }
@ -360,6 +388,31 @@ void MacroActionSource::ResolveVariablesToFixedValues()
_manualSettingValue.ResolveVariables(); _manualSettingValue.ResolveVariables();
} }
void MacroActionSource::SetupTempVars()
{
MacroAction::SetupTempVars();
switch (_action) {
case Action::GET_SETTING:
AddTempvar("setting",
obs_module_text(
"AdvSceneSwitcher.tempVar.source.setting"));
break;
case Action::GET_SETTINGS:
AddTempvar("settings",
obs_module_text(
"AdvSceneSwitcher.tempVar.source.settings"));
break;
default:
break;
}
}
void MacroActionSource::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
static inline void populateActionSelection(QComboBox *list) static inline void populateActionSelection(QComboBox *list)
{ {
for (auto &[actionType, name] : actionTypes) { for (auto &[actionType, name] : actionTypes) {
@ -502,7 +555,7 @@ void MacroActionSourceEdit::UpdateEntryData()
const auto weakSource = _entryData->_source.GetSource(); const auto weakSource = _entryData->_source.GetSource();
_settingsButtons->SetSelection(weakSource, _entryData->_button); _settingsButtons->SetSelection(weakSource, _entryData->_button);
_actions->setCurrentIndex(static_cast<int>(_entryData->_action)); _actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
_sources->SetSource(_entryData->_source); _sources->SetSource(_entryData->_source);
_sourceSettings->SetSelection(weakSource, _entryData->_setting); _sourceSettings->SetSelection(weakSource, _entryData->_setting);
_settingsString->setPlainText(_entryData->_settingsString); _settingsString->setPlainText(_entryData->_settingsString);
@ -537,7 +590,7 @@ void MacroActionSourceEdit::SourceChanged(const SourceSelection &source)
void MacroActionSourceEdit::ActionChanged(int value) void MacroActionSourceEdit::ActionChanged(int value)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionSource::Action>(value); _entryData->SetAction(static_cast<MacroActionSource::Action>(value));
SetWidgetVisibility(); SetWidgetVisibility();
} }
@ -607,6 +660,7 @@ void MacroActionSourceEdit::SelectionChanged(const TempVariableRef &var)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var; _entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
} }
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx) void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
@ -664,19 +718,29 @@ static QString GetIndividualListEntryName()
void MacroActionSourceEdit::SetWidgetVisibility() void MacroActionSourceEdit::SetWidgetVisibility()
{ {
const auto action = _entryData->GetAction();
const bool isSetSettings = action ==
MacroActionSource::Action::SETTINGS;
const bool isGetSetting = action ==
MacroActionSource::Action::GET_SETTING;
const bool isGetSettings = action ==
MacroActionSource::Action::GET_SETTINGS;
SetLayoutVisible(_settingsLayout, SetLayoutVisible(_settingsLayout,
_entryData->_action == isSetSettings || isGetSetting || isGetSettings);
MacroActionSource::Action::SETTINGS); _settingsInputMethods->setVisible(isSetSettings);
_sourceSettings->setVisible( _sourceSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS && (isSetSettings &&
_entryData->_settingsInputMethod != _entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::JSON_STRING); MacroActionSource::SettingsInputMethod::JSON_STRING) ||
isGetSetting);
_settingsString->setVisible( _settingsString->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS && isSetSettings &&
_entryData->_settingsInputMethod == _entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::JSON_STRING); MacroActionSource::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible( _getSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS && isSetSettings &&
_entryData->_settingsInputMethod != _entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod:: MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR); INDIVIDUAL_TEMPVAR);
@ -685,13 +749,12 @@ void MacroActionSourceEdit::SetWidgetVisibility()
GetIndividualListEntryName(), GetIndividualListEntryName(),
_entryData->_setting.IsList()); _entryData->_setting.IsList());
_tempVars->setVisible(_entryData->_action == _tempVars->setVisible(isSetSettings &&
MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod == _entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod:: MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR); INDIVIDUAL_TEMPVAR);
if (_entryData->_action == MacroActionSource::Action::SETTINGS && if (isSetSettings &&
(_entryData->_settingsInputMethod == (_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL || MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL ||
_entryData->_settingsInputMethod == _entryData->_settingsInputMethod ==
@ -704,35 +767,31 @@ void MacroActionSourceEdit::SetWidgetVisibility()
_manualSettingValue->hide(); _manualSettingValue->hide();
} }
const bool showWarning = const bool showWarning = action == MacroActionSource::Action::ENABLE ||
_entryData->_action == MacroActionSource::Action::ENABLE || action == MacroActionSource::Action::DISABLE;
_entryData->_action == MacroActionSource::Action::DISABLE;
_warning->setVisible(showWarning); _warning->setVisible(showWarning);
_settingsButtons->setVisible( _settingsButtons->setVisible(
_entryData->_action == action == MacroActionSource::Action::SETTINGS_BUTTON);
MacroActionSource::Action::SETTINGS_BUTTON);
_deinterlaceMode->setVisible( _deinterlaceMode->setVisible(
_entryData->_action == action == MacroActionSource::Action::DEINTERLACE_MODE);
MacroActionSource::Action::DEINTERLACE_MODE);
_deinterlaceOrder->setVisible( _deinterlaceOrder->setVisible(
_entryData->_action == action == MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
_refreshSettingSelection->setVisible( _refreshSettingSelection->setVisible(
(_entryData->_settingsInputMethod == ((isSetSettings &&
MacroActionSource::SettingsInputMethod:: (_entryData->_settingsInputMethod ==
INDIVIDUAL_MANUAL || MacroActionSource::SettingsInputMethod::
_entryData->_settingsInputMethod == INDIVIDUAL_MANUAL ||
MacroActionSource::SettingsInputMethod:: _entryData->_settingsInputMethod ==
INDIVIDUAL_LIST_ENTRY) && MacroActionSource::SettingsInputMethod::
INDIVIDUAL_LIST_ENTRY)) ||
isGetSetting) &&
_entryData->_source.GetType() == _entryData->_source.GetType() ==
SourceSelection::Type::VARIABLE); SourceSelection::Type::VARIABLE);
_acceptDialog->setVisible( _acceptDialog->setVisible(
_entryData->_action == action == MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
MacroActionSource::Action::CLOSE_FILTER_DIALOG || action == MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
_entryData->_action ==
MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
adjustSize(); adjustSize();
updateGeometry(); updateGeometry();

View File

@ -15,26 +15,22 @@ namespace advss {
class MacroActionSource : public MacroAction { class MacroActionSource : public MacroAction {
public: public:
MacroActionSource(Macro *m) : MacroAction(m) {} MacroActionSource(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m); static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const; std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
SourceSelection _source; bool PerformAction();
SourceSettingButton _button; void LogAction() const;
StringVariable _settingsString = "";
StringVariable _manualSettingValue = ""; bool Save(obs_data_t *obj) const;
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE; bool Load(obs_data_t *obj);
obs_deinterlace_field_order _deinterlaceOrder =
OBS_DEINTERLACE_FIELD_ORDER_TOP; std::string GetShortDesc() const;
TempVariableRef _tempVar; std::string GetId() const { return id; };
SourceSetting _setting;
bool _acceptDialog = false; void ResolveVariablesToFixedValues();
void SetupTempVars();
std::vector<TempVariableRef> GetTempVarRefs() const;
enum class Action { enum class Action {
ENABLE, ENABLE,
@ -50,8 +46,23 @@ public:
CLOSE_INTERACTION_DIALOG, CLOSE_INTERACTION_DIALOG,
CLOSE_FILTER_DIALOG, CLOSE_FILTER_DIALOG,
CLOSE_PROPERTIES_DIALOG, CLOSE_PROPERTIES_DIALOG,
GET_SETTING,
GET_SETTINGS,
}; };
Action _action = Action::SETTINGS;
void SetAction(Action);
Action GetAction() const { return _action; }
SourceSelection _source;
SourceSettingButton _button;
StringVariable _settingsString = "";
StringVariable _manualSettingValue = "";
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
obs_deinterlace_field_order _deinterlaceOrder =
OBS_DEINTERLACE_FIELD_ORDER_TOP;
TempVariableRef _tempVar;
SourceSetting _setting;
bool _acceptDialog = false;
enum class SettingsInputMethod { enum class SettingsInputMethod {
INDIVIDUAL_MANUAL, INDIVIDUAL_MANUAL,
@ -63,6 +74,8 @@ public:
SettingsInputMethod::INDIVIDUAL_MANUAL; SettingsInputMethod::INDIVIDUAL_MANUAL;
private: private:
Action _action = Action::SETTINGS;
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;
}; };

View File

@ -3,6 +3,7 @@
#include "transition-helpers.hpp" #include "transition-helpers.hpp"
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include <algorithm>
namespace advss { namespace advss {
@ -22,6 +23,10 @@ const static std::map<MacroActionTransition::Type, std::string> actionTypes = {
"AdvSceneSwitcher.action.transition.type.sourceShow"}, "AdvSceneSwitcher.action.transition.type.sourceShow"},
{MacroActionTransition::Type::SOURCE_HIDE, {MacroActionTransition::Type::SOURCE_HIDE,
"AdvSceneSwitcher.action.transition.type.sourceHide"}, "AdvSceneSwitcher.action.transition.type.sourceHide"},
{MacroActionTransition::Type::TBAR,
"AdvSceneSwitcher.action.transition.type.tbar"},
{MacroActionTransition::Type::RELEASE_TBAR,
"AdvSceneSwitcher.action.transition.type.releaseTbar"},
}; };
void MacroActionTransition::SetSceneTransition() void MacroActionTransition::SetSceneTransition()
@ -76,6 +81,13 @@ static void obs_sceneitem_set_transition_duration(obs_sceneitem_t *item,
} }
#endif #endif
void MacroActionTransition::SetTbarPosition()
{
const double percent = std::clamp(_tbarPosition.GetValue(), 0.0, 100.0);
const int tbarValue = (int)std::round(percent / 100.0 * 1023.0);
obs_frontend_set_tbar_position(tbarValue);
}
void MacroActionTransition::SetSourceTransition(bool show) void MacroActionTransition::SetSourceTransition(bool show)
{ {
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0) #if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0)
@ -110,6 +122,12 @@ bool MacroActionTransition::PerformAction()
case Type::SOURCE_HIDE: case Type::SOURCE_HIDE:
SetSourceTransition(false); SetSourceTransition(false);
break; break;
case Type::TBAR:
SetTbarPosition();
break;
case Type::RELEASE_TBAR:
obs_frontend_release_tbar();
break;
} }
return true; return true;
} }
@ -119,22 +137,29 @@ void MacroActionTransition::LogAction() const
std::string msgBegin; std::string msgBegin;
switch (_type) { switch (_type) {
case Type::SCENE: case Type::SCENE:
msgBegin += "set scene transition"; msgBegin = "set scene transition";
break; break;
case Type::SCENE_OVERRIDE: case Type::SCENE_OVERRIDE:
msgBegin += "set scene override transition of " + msgBegin = "set scene override transition of " +
_scene.ToString(true); _scene.ToString(true);
break; break;
case Type::SOURCE_SHOW: case Type::SOURCE_SHOW:
msgBegin += "set source show transition of " + msgBegin = "set source show transition of " +
_source.ToString(true) + " on scene " + _source.ToString(true) + " on scene " +
_scene.ToString(true); _scene.ToString(true);
break; break;
case Type::SOURCE_HIDE: case Type::SOURCE_HIDE:
msgBegin += "set source hide transition of " + msgBegin = "set source hide transition of " +
_source.ToString(true) + " on scene " + _source.ToString(true) + " on scene " +
_scene.ToString(true); _scene.ToString(true);
break; break;
case Type::TBAR:
ablog(LOG_INFO, "set T-Bar position to %.2f%%",
_tbarPosition.GetValue());
return;
case Type::RELEASE_TBAR:
ablog(LOG_INFO, "release T-Bar");
return;
} }
if (_setDuration) { if (_setDuration) {
ablog(LOG_INFO, "%s duration to %s", msgBegin.c_str(), ablog(LOG_INFO, "%s duration to %s", msgBegin.c_str(),
@ -156,6 +181,7 @@ bool MacroActionTransition::Save(obs_data_t *obj) const
_transition.Save(obj); _transition.Save(obj);
obs_data_set_bool(obj, "setDuration", _setDuration); obs_data_set_bool(obj, "setDuration", _setDuration);
obs_data_set_bool(obj, "setType", _setTransitionType); obs_data_set_bool(obj, "setType", _setTransitionType);
_tbarPosition.Save(obj, "tbarPosition");
return true; return true;
} }
@ -169,23 +195,25 @@ bool MacroActionTransition::Load(obs_data_t *obj)
_transition.Load(obj); _transition.Load(obj);
_setDuration = obs_data_get_bool(obj, "setDuration"); _setDuration = obs_data_get_bool(obj, "setDuration");
_setTransitionType = obs_data_get_bool(obj, "setType"); _setTransitionType = obs_data_get_bool(obj, "setType");
_tbarPosition.Load(obj, "tbarPosition");
return true; return true;
} }
std::string MacroActionTransition::GetShortDesc() const std::string MacroActionTransition::GetShortDesc() const
{ {
std::string msgBegin;
switch (_type) { switch (_type) {
case Type::SCENE: case Type::SCENE:
return _transition.ToString(); return _transition.ToString();
case Type::SCENE_OVERRIDE: case Type::SCENE_OVERRIDE:
return _scene.ToString() + " - " + _transition.ToString(); return _scene.ToString() + " - " + _transition.ToString();
case Type::SOURCE_SHOW: case Type::SOURCE_SHOW:
return _scene.ToString() + " - " + _source.ToString() + " - " +
_transition.ToString();
case Type::SOURCE_HIDE: case Type::SOURCE_HIDE:
return _scene.ToString() + " - " + _source.ToString() + " - " + return _scene.ToString() + " - " + _source.ToString() + " - " +
_transition.ToString(); _transition.ToString();
case Type::TBAR:
return std::to_string(_tbarPosition.GetValue()) + "%";
case Type::RELEASE_TBAR:
return "";
} }
return ""; return "";
} }
@ -205,6 +233,7 @@ void MacroActionTransition::ResolveVariablesToFixedValues()
_source.ResolveVariables(); _source.ResolveVariables();
_scene.ResolveVariables(); _scene.ResolveVariables();
_duration.ResolveVariables(); _duration.ResolveVariables();
_tbarPosition.ResolveVariables();
} }
static inline void populateActionSelection(QComboBox *list) static inline void populateActionSelection(QComboBox *list)
@ -236,9 +265,16 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
_setDuration(new QCheckBox), _setDuration(new QCheckBox),
_transitions(new TransitionSelectionWidget(this, false)), _transitions(new TransitionSelectionWidget(this, false)),
_duration(new DurationSelection(this, false)), _duration(new DurationSelection(this, false)),
_tbarPosition(new VariableDoubleSpinBox),
_transitionLayout(new QHBoxLayout), _transitionLayout(new QHBoxLayout),
_durationLayout(new QHBoxLayout) _durationLayout(new QHBoxLayout),
_tbarLayout(new QHBoxLayout)
{ {
_tbarPosition->setMinimum(0.0);
_tbarPosition->setMaximum(100.0);
_tbarPosition->setDecimals(2);
_tbarPosition->setSuffix("%");
populateActionSelection(_actions); populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this, QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
@ -260,6 +296,11 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
SLOT(SetTransitionChanged(int))); SLOT(SetTransitionChanged(int)));
QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this, QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this,
SLOT(SetDurationChanged(int))); SLOT(SetDurationChanged(int)));
QWidget::connect(
_tbarPosition,
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this,
SLOT(TbarPositionChanged(const NumberVariable<double> &)));
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = { const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{type}}", _actions}, {"{{type}}", _actions},
@ -269,22 +310,29 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
{"{{duration}}", _duration}, {"{{duration}}", _duration},
{"{{setTransition}}", _setTransition}, {"{{setTransition}}", _setTransition},
{"{{setDuration}}", _setDuration}, {"{{setDuration}}", _setDuration},
{"{{tbarPosition}}", _tbarPosition},
}; };
auto typeLayout = new QHBoxLayout; auto typeLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text( PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line1"), "AdvSceneSwitcher.action.transition.layout.type"),
typeLayout, widgetPlaceholders); typeLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.transition.layout.transition"),
_transitionLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.transition.layout.duration"),
_durationLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text( PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line2"), "AdvSceneSwitcher.action.transition.layout.tbar"),
_transitionLayout, widgetPlaceholders); _tbarLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line3"),
_durationLayout, widgetPlaceholders);
auto mainLayout = new QVBoxLayout; auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(typeLayout); mainLayout->addLayout(typeLayout);
mainLayout->addLayout(_transitionLayout); mainLayout->addLayout(_transitionLayout);
mainLayout->addLayout(_durationLayout); mainLayout->addLayout(_durationLayout);
mainLayout->addLayout(_tbarLayout);
setLayout(mainLayout); setLayout(mainLayout);
_entryData = entryData; _entryData = entryData;
@ -300,13 +348,14 @@ void MacroActionTransitionEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_entryData->_type)); _actions->setCurrentIndex(static_cast<int>(_entryData->_type));
_scenes->SetScene(_entryData->_scene); _scenes->SetScene(_entryData->_scene);
_sources->SetSceneItem((_entryData->_source)); _sources->SetSceneItem(_entryData->_source);
_setDuration->setChecked(_entryData->_setDuration); _setDuration->setChecked(_entryData->_setDuration);
_duration->SetDuration(_entryData->_duration); _duration->SetDuration(_entryData->_duration);
_setTransition->setChecked(_entryData->_setTransitionType); _setTransition->setChecked(_entryData->_setTransitionType);
_transitions->SetTransition(_entryData->_transition); _transitions->SetTransition(_entryData->_transition);
_transitions->setEnabled(_entryData->_setTransitionType); _transitions->setEnabled(_entryData->_setTransitionType);
_duration->setEnabled(_entryData->_setDuration); _duration->setEnabled(_entryData->_setDuration);
_tbarPosition->SetValue(_entryData->_tbarPosition);
SetWidgetVisibility(); SetWidgetVisibility();
} }
@ -353,11 +402,19 @@ void MacroActionTransitionEdit::DurationChanged(const Duration &dur)
void MacroActionTransitionEdit::SetWidgetVisibility() void MacroActionTransitionEdit::SetWidgetVisibility()
{ {
const bool isTbar = _entryData->_type ==
MacroActionTransition::Type::TBAR;
const bool isReleaseTbar = _entryData->_type ==
MacroActionTransition::Type::RELEASE_TBAR;
_sources->setVisible( _sources->setVisible(
_entryData->_type == MacroActionTransition::Type::SOURCE_HIDE || _entryData->_type == MacroActionTransition::Type::SOURCE_HIDE ||
_entryData->_type == MacroActionTransition::Type::SOURCE_SHOW); _entryData->_type == MacroActionTransition::Type::SOURCE_SHOW);
_scenes->setVisible(_entryData->_type != _scenes->setVisible(_entryData->_type !=
MacroActionTransition::Type::SCENE); MacroActionTransition::Type::SCENE &&
!isTbar && !isReleaseTbar);
SetLayoutVisible(_transitionLayout, !isTbar && !isReleaseTbar);
SetLayoutVisible(_durationLayout, !isTbar && !isReleaseTbar);
SetLayoutVisible(_tbarLayout, isTbar);
adjustSize(); adjustSize();
} }
@ -381,4 +438,13 @@ void MacroActionTransitionEdit::SetDurationChanged(int state)
_duration->setEnabled(state); _duration->setEnabled(state);
} }
void MacroActionTransitionEdit::TbarPositionChanged(
const NumberVariable<double> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tbarPosition = value;
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
} // namespace advss } // namespace advss

View File

@ -4,6 +4,7 @@
#include "transition-selection.hpp" #include "transition-selection.hpp"
#include "scene-selection.hpp" #include "scene-selection.hpp"
#include "scene-item-selection.hpp" #include "scene-item-selection.hpp"
#include "variable-spinbox.hpp"
#include <QCheckBox> #include <QCheckBox>
#include <QHBoxLayout> #include <QHBoxLayout>
@ -28,6 +29,8 @@ public:
SCENE_OVERRIDE, SCENE_OVERRIDE,
SOURCE_SHOW, SOURCE_SHOW,
SOURCE_HIDE, SOURCE_HIDE,
TBAR,
RELEASE_TBAR,
}; };
Type _type = Type::SCENE; Type _type = Type::SCENE;
@ -37,11 +40,13 @@ public:
bool _setTransitionType = true; bool _setTransitionType = true;
TransitionSelection _transition; TransitionSelection _transition;
Duration _duration; Duration _duration;
DoubleVariable _tbarPosition = 0.0;
private: private:
void SetSceneTransition(); void SetSceneTransition();
void SetTransitionOverride(); void SetTransitionOverride();
void SetSourceTransition(bool); void SetSourceTransition(bool);
void SetTbarPosition();
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;
@ -72,6 +77,7 @@ private slots:
void SetDurationChanged(int state); void SetDurationChanged(int state);
void TransitionChanged(const TransitionSelection &); void TransitionChanged(const TransitionSelection &);
void DurationChanged(const Duration &seconds); void DurationChanged(const Duration &seconds);
void TbarPositionChanged(const NumberVariable<double> &);
signals: signals:
void HeaderInfoChanged(const QString &); void HeaderInfoChanged(const QString &);
@ -83,8 +89,10 @@ protected:
QCheckBox *_setDuration; QCheckBox *_setDuration;
TransitionSelectionWidget *_transitions; TransitionSelectionWidget *_transitions;
DurationSelection *_duration; DurationSelection *_duration;
VariableDoubleSpinBox *_tbarPosition;
QHBoxLayout *_transitionLayout; QHBoxLayout *_transitionLayout;
QHBoxLayout *_durationLayout; QHBoxLayout *_durationLayout;
QHBoxLayout *_tbarLayout;
std::shared_ptr<MacroActionTransition> _entryData; std::shared_ptr<MacroActionTransition> _entryData;
private: private:

View File

@ -57,8 +57,11 @@ bool MacroActionWait::PerformAction()
std::chrono::milliseconds((int)(sleepDuration * 1000)); std::chrono::milliseconds((int)(sleepDuration * 1000));
SetMacroAbortWait(false); 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(); return !MacroWaitShouldAbort();
} }

View File

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

View File

@ -710,6 +710,12 @@ void MacroConditionDateEdit::UpdateEntryData()
SetWidgetStatus(); SetWidgetStatus();
} }
void MacroConditionDateEdit::showEvent(QShowEvent *event)
{
const QSignalBlocker b(this);
UpdateEntryData();
}
void MacroConditionDateEdit::SetupSimpleView() void MacroConditionDateEdit::SetupSimpleView()
{ {
SetLayoutVisible(_simpleLayout, true); SetLayoutVisible(_simpleLayout, true);

View File

@ -107,6 +107,16 @@ signals:
void HeaderInfoChanged(const QString &); void HeaderInfoChanged(const QString &);
protected: protected:
void showEvent(QShowEvent *event) override;
private:
void SetupSimpleView();
void SetupAdvancedView();
void SetupPatternView();
void SetWidgetStatus();
void ShowFirstDateSelection(bool visible);
void ShowSecondDateSelection(bool visible);
QComboBox *_weekCondition; QComboBox *_weekCondition;
DayOfWeekSelector *_days; DayOfWeekSelector *_days;
QCheckBox *_ignoreWeekTime; QCheckBox *_ignoreWeekTime;
@ -134,16 +144,9 @@ protected:
QHBoxLayout *_repeatUpdateLayout; QHBoxLayout *_repeatUpdateLayout;
QHBoxLayout *_patternLayout; QHBoxLayout *_patternLayout;
std::shared_ptr<MacroConditionDate> _entryData;
private:
void SetupSimpleView();
void SetupAdvancedView();
void SetupPatternView();
void SetWidgetStatus();
void ShowFirstDateSelection(bool visible);
void ShowSecondDateSelection(bool visible);
QTimer _timer; QTimer _timer;
std::shared_ptr<MacroConditionDate> _entryData;
bool _loading = true; bool _loading = true;
}; };

View File

@ -1,12 +1,10 @@
#include "macro-condition-file.hpp" #include "macro-condition-file.hpp"
#include "curl-helper.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp" #include "plugin-state-helpers.hpp"
#include "utility.hpp" #include "utility.hpp"
#include <QFileDialog>
#include <QTextStream> #include <QTextStream>
#include <regex> #include <QFileInfo>
namespace advss { namespace advss {
@ -19,29 +17,6 @@ bool MacroConditionFile::_registered = MacroConditionFactory::Register(
static std::hash<std::string> strHash; static std::hash<std::string> strHash;
static size_t WriteCallback(void *contents, size_t size, size_t nmemb,
void *userp)
{
((std::string *)userp)->append((char *)contents, size * nmemb);
return size * nmemb;
}
static std::string getRemoteData(std::string &url)
{
std::string readBuffer;
CurlHelper::SetOpt(CURLOPT_URL, url.c_str());
CurlHelper::SetOpt(CURLOPT_WRITEFUNCTION, WriteCallback);
CurlHelper::SetOpt(CURLOPT_WRITEDATA, &readBuffer);
// Set timeout to at least one second
int timeout = GetIntervalValue() / 1000;
if (timeout == 0) {
timeout = 1;
}
CurlHelper::SetOpt(CURLOPT_TIMEOUT, 1);
CurlHelper::Perform();
return readBuffer;
}
void MacroConditionFile::SetCondition(Condition condition) void MacroConditionFile::SetCondition(Condition condition)
{ {
_condition = condition; _condition = condition;
@ -50,14 +25,6 @@ void MacroConditionFile::SetCondition(Condition condition)
bool MacroConditionFile::MatchFileContent(QString &filedata) bool MacroConditionFile::MatchFileContent(QString &filedata)
{ {
if (_onlyMatchIfChanged) {
size_t newHash = strHash(filedata.toUtf8().constData());
if (newHash == _lastHash) {
return false;
}
_lastHash = newHash;
}
if (_regex.Enabled()) { if (_regex.Enabled()) {
return _regex.Matches(filedata, QString::fromStdString(_text)); return _regex.Matches(filedata, QString::fromStdString(_text));
} }
@ -66,31 +33,13 @@ bool MacroConditionFile::MatchFileContent(QString &filedata)
return CompareIgnoringLineEnding(text, filedata); return CompareIgnoringLineEnding(text, filedata);
} }
bool MacroConditionFile::CheckRemoteFileContent() bool MacroConditionFile::CheckFileContent()
{
std::string path = _file;
std::string data = getRemoteData(path);
SetVariableValue(data);
SetTempVarValue("content", data);
QString qdata = QString::fromStdString(data);
return MatchFileContent(qdata);
}
bool MacroConditionFile::CheckLocalFileContent()
{ {
QFile file(QString::fromStdString(_file)); QFile file(QString::fromStdString(_file));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false; return false;
} }
if (_useTime) {
QDateTime newLastMod = QFileInfo(file).lastModified();
if (_lastMod == newLastMod) {
return false;
}
_lastMod = newLastMod;
}
QString filedata = QTextStream(&file).readAll(); QString filedata = QTextStream(&file).readAll();
SetVariableValue(filedata.toStdString()); SetVariableValue(filedata.toStdString());
SetTempVarValue("content", filedata.toStdString()); SetTempVarValue("content", filedata.toStdString());
@ -103,42 +52,30 @@ bool MacroConditionFile::CheckLocalFileContent()
bool MacroConditionFile::CheckChangeContent() bool MacroConditionFile::CheckChangeContent()
{ {
QString filedata; QString filedata;
switch (_fileType) {
case FileType::LOCAL: { std::string path = _file;
std::string path = _file; QFile file(QString::fromStdString(path));
QFile file(QString::fromStdString(path)); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false;
return false;
}
filedata = QTextStream(&file).readAll();
file.close();
} break;
case FileType::REMOTE: {
std::string path = _file;
std::string data = getRemoteData(path);
QString filedata = QString::fromStdString(data);
} break;
default:
break;
} }
filedata = QTextStream(&file).readAll();
file.close();
SetTempVarValue("content", filedata.toStdString()); SetTempVarValue("content", filedata.toStdString());
size_t newHash = strHash(filedata.toUtf8().constData()); size_t newHash = strHash(filedata.toUtf8().constData());
const bool contentChanged = newHash != _lastHash; const bool contentChanged = !_firstContentCheck &&
(newHash != _lastHash);
_lastHash = newHash; _lastHash = newHash;
_firstContentCheck = false;
return contentChanged; return contentChanged;
} }
bool MacroConditionFile::CheckChangeDate() bool MacroConditionFile::CheckChangeDate()
{ {
if (_fileType == FileType::REMOTE) {
return false;
}
QFile file(QString::fromStdString(_file)); QFile file(QString::fromStdString(_file));
QDateTime newLastMod = QFileInfo(file).lastModified(); QDateTime newLastMod = QFileInfo(file).lastModified();
SetVariableValue(newLastMod.toString().toStdString()); SetVariableValue(newLastMod.toString().toStdString());
const bool dateChanged = _lastMod != newLastMod; const bool dateChanged = _lastMod.isValid() && (_lastMod != newLastMod);
_lastMod = newLastMod; _lastMod = newLastMod;
SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString()); SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString());
return dateChanged; return dateChanged;
@ -157,10 +94,6 @@ void MacroConditionFile::SetupTempVars()
"AdvSceneSwitcher.tempVar.file.content")); "AdvSceneSwitcher.tempVar.file.content"));
} }
if (_fileType == FileType::REMOTE) {
return;
}
AddTempvar( AddTempvar(
"basename", "basename",
obs_module_text("AdvSceneSwitcher.tempVar.file.basename"), obs_module_text("AdvSceneSwitcher.tempVar.file.basename"),
@ -199,11 +132,7 @@ bool MacroConditionFile::CheckCondition()
bool ret = false; bool ret = false;
switch (_condition) { switch (_condition) {
case Condition::MATCH: case Condition::MATCH:
if (_fileType == FileType::REMOTE) { ret = CheckFileContent();
ret = CheckRemoteFileContent();
break;
}
ret = CheckLocalFileContent();
break; break;
case Condition::CONTENT_CHANGE: case Condition::CONTENT_CHANGE:
ret = CheckChangeContent(); ret = CheckChangeContent();
@ -264,10 +193,7 @@ bool MacroConditionFile::Save(obs_data_t *obj) const
_regex.Save(obj); _regex.Save(obj);
_file.Save(obj, "file"); _file.Save(obj, "file");
_text.Save(obj, "text"); _text.Save(obj, "text");
obs_data_set_int(obj, "fileType", static_cast<int>(_fileType));
obs_data_set_int(obj, "condition", static_cast<int>(_condition)); obs_data_set_int(obj, "condition", static_cast<int>(_condition));
obs_data_set_bool(obj, "useTime", _useTime);
obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged);
return true; return true;
} }
@ -275,18 +201,10 @@ bool MacroConditionFile::Load(obs_data_t *obj)
{ {
MacroCondition::Load(obj); MacroCondition::Load(obj);
_regex.Load(obj); _regex.Load(obj);
// TODO: remove in future version
if (obs_data_has_user_value(obj, "useRegex")) {
_regex.CreateBackwardsCompatibleRegex(
obs_data_get_bool(obj, "useRegex"));
}
_file.Load(obj, "file"); _file.Load(obj, "file");
_text.Load(obj, "text"); _text.Load(obj, "text");
_fileType = static_cast<FileType>(obs_data_get_int(obj, "fileType"));
SetCondition( SetCondition(
static_cast<Condition>(obs_data_get_int(obj, "condition"))); static_cast<Condition>(obs_data_get_int(obj, "condition")));
_useTime = obs_data_get_bool(obj, "useTime");
_onlyMatchIfChanged = obs_data_get_bool(obj, "onlyMatchIfChanged");
return true; return true;
} }
@ -295,13 +213,6 @@ std::string MacroConditionFile::GetShortDesc() const
return _file.UnresolvedValue(); return _file.UnresolvedValue();
} }
static void populateFileTypes(QComboBox *list)
{
list->addItem(obs_module_text("AdvSceneSwitcher.condition.file.local"));
list->addItem(
obs_module_text("AdvSceneSwitcher.condition.file.remote"));
}
static void populateConditions(QComboBox *list) static void populateConditions(QComboBox *list)
{ {
list->addItem( list->addItem(
@ -321,21 +232,13 @@ static void populateConditions(QComboBox *list)
MacroConditionFileEdit::MacroConditionFileEdit( MacroConditionFileEdit::MacroConditionFileEdit(
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData) QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
: QWidget(parent), : QWidget(parent),
_fileTypes(new QComboBox()),
_conditions(new QComboBox()), _conditions(new QComboBox()),
_filePath(new FileSelection()), _filePath(new FileSelection()),
_matchText(new VariableTextEdit(this)), _matchText(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(parent)), _regex(new RegexConfigWidget(parent))
_checkModificationDate(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.fileTab.checkfileContentTime"))),
_checkFileContent(new QCheckBox(
obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")))
{ {
populateFileTypes(_fileTypes);
populateConditions(_conditions); populateConditions(_conditions);
QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(FileTypeChanged(int)));
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this, QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int))); SLOT(ConditionChanged(int)));
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this, QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
@ -345,42 +248,20 @@ MacroConditionFileEdit::MacroConditionFileEdit(
QWidget::connect(_regex, QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this, SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &))); SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_checkModificationDate, SIGNAL(stateChanged(int)),
this, SLOT(CheckModificationDateChanged(int)));
QWidget::connect(_checkFileContent, SIGNAL(stateChanged(int)), this,
SLOT(OnlyMatchIfChangedChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = { auto widgetLayout = new QHBoxLayout;
{"{{fileType}}", _fileTypes}, widgetLayout->setContentsMargins(0, 0, 0, 0);
{"{{conditions}}", _conditions}, PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.file.layout"),
{"{{filePath}}", _filePath}, widgetLayout,
{"{{matchText}}", _matchText}, {{"{{conditions}}", _conditions},
{"{{useRegex}}", _regex}, {"{{filePath}}", _filePath},
{"{{checkModificationDate}}", _checkModificationDate}, {"{{regex}}", _regex}});
{"{{checkFileContent}}", _checkFileContent},
};
QVBoxLayout *mainLayout = new QVBoxLayout; auto layout = new QVBoxLayout;
QHBoxLayout *line1Layout = new QHBoxLayout; layout->addLayout(widgetLayout);
QHBoxLayout *line2Layout = new QHBoxLayout; layout->addWidget(_matchText);
QHBoxLayout *line3Layout = new QHBoxLayout;
line1Layout->setContentsMargins(0, 0, 0, 0);
line2Layout->setContentsMargins(0, 0, 0, 0);
line3Layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line1"),
line1Layout, widgetPlaceholders);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line2"),
line2Layout, widgetPlaceholders, false);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line3"),
line3Layout, widgetPlaceholders);
mainLayout->addLayout(line1Layout);
mainLayout->addLayout(line2Layout);
mainLayout->addLayout(line3Layout);
setLayout(mainLayout); setLayout(layout);
_entryData = entryData; _entryData = entryData;
UpdateEntryData(); UpdateEntryData();
@ -393,47 +274,15 @@ void MacroConditionFileEdit::UpdateEntryData()
return; return;
} }
_fileTypes->setCurrentIndex(static_cast<int>(_entryData->_fileType));
_conditions->setCurrentIndex( _conditions->setCurrentIndex(
static_cast<int>(_entryData->GetCondition())); static_cast<int>(_entryData->GetCondition()));
_filePath->SetPath(_entryData->_file); _filePath->SetPath(_entryData->_file);
_matchText->setPlainText(_entryData->_text); _matchText->setPlainText(_entryData->_text);
_regex->SetRegexConfig(_entryData->_regex); _regex->SetRegexConfig(_entryData->_regex);
_checkModificationDate->setChecked(_entryData->_useTime);
_checkFileContent->setChecked(_entryData->_onlyMatchIfChanged);
// TODO: Remove in future version
if (!_entryData->_useTime) {
_checkModificationDate->hide();
}
if (!_entryData->_onlyMatchIfChanged) {
_checkFileContent->hide();
}
SetWidgetVisibility(); SetWidgetVisibility();
} }
void MacroConditionFileEdit::FileTypeChanged(int index)
{
if (_loading || !_entryData) {
return;
}
MacroConditionFile::FileType type =
static_cast<MacroConditionFile::FileType>(index);
if (type == MacroConditionFile::FileType::LOCAL) {
_filePath->Button()->setDisabled(false);
_checkModificationDate->setDisabled(false);
} else {
_filePath->Button()->setDisabled(true);
_checkModificationDate->setDisabled(true);
}
auto lock = LockContext();
_entryData->_fileType = type;
}
void MacroConditionFileEdit::ConditionChanged(int index) void MacroConditionFileEdit::ConditionChanged(int index)
{ {
GUARD_LOADING_AND_LOCK(); GUARD_LOADING_AND_LOCK();
@ -467,18 +316,6 @@ void MacroConditionFileEdit::RegexChanged(const RegexConfig &conf)
updateGeometry(); updateGeometry();
} }
void MacroConditionFileEdit::CheckModificationDateChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useTime = state;
}
void MacroConditionFileEdit::OnlyMatchIfChangedChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_onlyMatchIfChanged = state;
}
void MacroConditionFileEdit::SetWidgetVisibility() void MacroConditionFileEdit::SetWidgetVisibility()
{ {
if (!_entryData) { if (!_entryData) {
@ -489,20 +326,6 @@ void MacroConditionFileEdit::SetWidgetVisibility()
MacroConditionFile::Condition::MATCH); MacroConditionFile::Condition::MATCH);
_regex->setVisible(_entryData->GetCondition() == _regex->setVisible(_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH); MacroConditionFile::Condition::MATCH);
_checkModificationDate->setVisible(
_entryData->_useTime &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
_checkFileContent->setVisible(
_entryData->_onlyMatchIfChanged &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
// TODO: Remove remote file support in future version in favor of HTTP
// action.
// Hide the option for now, if it is not used already.
_fileTypes->setVisible(_entryData->_fileType ==
MacroConditionFile::FileType::REMOTE);
adjustSize(); adjustSize();
updateGeometry(); updateGeometry();

View File

@ -7,9 +7,6 @@
#include <QWidget> #include <QWidget>
#include <QComboBox> #include <QComboBox>
#include <QDateTime> #include <QDateTime>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
namespace advss { namespace advss {
@ -26,11 +23,6 @@ public:
return std::make_shared<MacroConditionFile>(m); return std::make_shared<MacroConditionFile>(m);
} }
enum class FileType {
LOCAL,
REMOTE,
};
enum class Condition { enum class Condition {
MATCH, MATCH,
CONTENT_CHANGE, CONTENT_CHANGE,
@ -46,15 +38,9 @@ public:
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText"); StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
RegexConfig _regex; RegexConfig _regex;
// TODO: Remove in future version
bool _useTime = false;
bool _onlyMatchIfChanged = false;
FileType _fileType = FileType::LOCAL;
private: private:
bool MatchFileContent(QString &filedata); bool MatchFileContent(QString &filedata);
bool CheckRemoteFileContent(); bool CheckFileContent();
bool CheckLocalFileContent();
bool CheckChangeContent(); bool CheckChangeContent();
bool CheckChangeDate(); bool CheckChangeDate();
void SetupTempVars(); void SetupTempVars();
@ -62,6 +48,7 @@ private:
Condition _condition = Condition::MATCH; Condition _condition = Condition::MATCH;
QDateTime _lastMod; QDateTime _lastMod;
size_t _lastHash = 0; size_t _lastHash = 0;
bool _firstContentCheck = true;
std::string _lastFile; std::string _lastFile;
std::string _basename; std::string _basename;
std::string _basenameComplete; std::string _basenameComplete;
@ -92,29 +79,22 @@ public:
} }
private slots: private slots:
void FileTypeChanged(int index);
void ConditionChanged(int index); void ConditionChanged(int index);
void PathChanged(const QString &text); void PathChanged(const QString &text);
void MatchTextChanged(); void MatchTextChanged();
void RegexChanged(const RegexConfig &); void RegexChanged(const RegexConfig &);
void CheckModificationDateChanged(int state);
void OnlyMatchIfChangedChanged(int state);
signals: signals:
void HeaderInfoChanged(const QString &); void HeaderInfoChanged(const QString &);
protected: private:
QComboBox *_fileTypes; void SetWidgetVisibility();
QComboBox *_conditions; QComboBox *_conditions;
FileSelection *_filePath; FileSelection *_filePath;
VariableTextEdit *_matchText; VariableTextEdit *_matchText;
RegexConfigWidget *_regex; RegexConfigWidget *_regex;
QCheckBox *_checkModificationDate;
QCheckBox *_checkFileContent;
std::shared_ptr<MacroConditionFile> _entryData; std::shared_ptr<MacroConditionFile> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true; bool _loading = true;
}; };

View File

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

View File

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

View File

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

View File

@ -0,0 +1,237 @@
#include "source-interaction-recorder.hpp"
#include "obs-module-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
namespace advss {
SourceInteractionRecorder::SourceInteractionRecorder(QWidget *parent,
obs_weak_source_t *source)
: QDialog(parent),
_source(source),
_preview(new SourcePreviewWidget(this, source)),
_stepList(new SourceInteractionStepList(this)),
_startStopButton(new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"),
this))
{
setWindowTitle(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"));
_preview->setMouseTracking(true);
_preview->setFocusPolicy(Qt::StrongFocus);
_preview->installEventFilter(this);
_stepList->HideControls();
_stepList->SetMinListHeight(50);
auto buttonRow = new QHBoxLayout;
buttonRow->addWidget(_startStopButton);
buttonRow->addStretch();
auto acceptButton = new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.accept"),
this);
buttonRow->addWidget(acceptButton);
auto layout = new QVBoxLayout(this);
layout->addWidget(_preview, 1);
layout->addWidget(_stepList);
layout->addLayout(buttonRow);
setLayout(layout);
connect(_startStopButton, &QPushButton::clicked, this,
&SourceInteractionRecorder::StartStop);
connect(acceptButton, &QPushButton::clicked, this, [this]() {
if (_recording) {
StartStop();
}
emit StepsRecorded(_steps);
accept();
});
}
SourceInteractionRecorder::~SourceInteractionRecorder() {}
void SourceInteractionRecorder::StartStop()
{
_recording = !_recording;
if (_recording) {
_steps.clear();
_stepList->Clear();
_firstEvent = true;
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.stop"));
} else {
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"));
}
}
void SourceInteractionRecorder::AppendStep(const SourceInteractionStep &step)
{
if (!_firstEvent) {
auto now = std::chrono::steady_clock::now();
int ms = static_cast<int>(
std::chrono::duration_cast<std::chrono::milliseconds>(
now - _lastEventTime)
.count());
if (ms > 10) {
SourceInteractionStep wait;
wait.type = SourceInteractionStep::Type::WAIT;
wait.waitMs.SetValue(ms);
_steps.push_back(wait);
_stepList->Insert(
QString::fromStdString(wait.ToString()));
}
}
_firstEvent = false;
_lastEventTime = std::chrono::steady_clock::now();
_steps.push_back(step);
_stepList->Insert(QString::fromStdString(step.ToString()));
_stepList->ScrollToBottom();
}
bool SourceInteractionRecorder::eventFilter(QObject *obj, QEvent *event)
{
if (obj != _preview || !_recording) {
return QDialog::eventFilter(obj, event);
}
switch (event->type()) {
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
return HandleMouseClick(static_cast<QMouseEvent *>(event));
case QEvent::MouseMove:
return HandleMouseMove(static_cast<QMouseEvent *>(event));
case QEvent::Wheel:
return HandleMouseWheel(static_cast<QWheelEvent *>(event));
case QEvent::KeyPress:
case QEvent::KeyRelease:
return HandleKeyEvent(static_cast<QKeyEvent *>(event));
case QEvent::FocusIn:
case QEvent::FocusOut: {
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_send_focus(source,
event->type() == QEvent::FocusIn);
}
return true;
}
default:
break;
}
return QDialog::eventFilter(obj, event);
}
bool SourceInteractionRecorder::HandleMouseClick(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_CLICK;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
step.mouseUp = (event->type() == QEvent::MouseButtonRelease);
step.clickCount.SetValue(
(event->type() == QEvent::MouseButtonDblClick) ? 2 : 1);
switch (event->button()) {
case Qt::LeftButton:
step.button = MOUSE_LEFT;
break;
case Qt::MiddleButton:
step.button = MOUSE_MIDDLE;
break;
case Qt::RightButton:
step.button = MOUSE_RIGHT;
break;
default:
return false;
}
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseMove(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_MOVE;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
// Always forward moves to the source so hover/cursor effects work,
// but only record them when a button is held (drag), to reduce noise.
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
if (event->buttons() != Qt::NoButton) {
AppendStep(step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseWheel(QWheelEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_WHEEL;
int srcX = 0, srcY = 0;
const QPointF pos = event->position();
_preview->GetSourceRelativeXY((int)pos.x(), (int)pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
const QPoint angle = event->angleDelta();
step.wheelDeltaX.SetValue(angle.x());
step.wheelDeltaY.SetValue(angle.y());
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleKeyEvent(QKeyEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::KEY_PRESS;
step.keyUp = (event->type() == QEvent::KeyRelease);
step.nativeVkey.SetValue(static_cast<int>(event->nativeVirtualKey()));
step.text = event->text().toStdString();
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
} // namespace advss

View File

@ -0,0 +1,54 @@
#pragma once
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-preview-widget.hpp"
#include <chrono>
#include <vector>
#include <QDialog>
#include <QPushButton>
class QKeyEvent;
class QMouseEvent;
class QWheelEvent;
namespace advss {
class SourceInteractionRecorder : public QDialog {
Q_OBJECT
public:
SourceInteractionRecorder(QWidget *parent, obs_weak_source_t *source);
~SourceInteractionRecorder();
const std::vector<SourceInteractionStep> &GetSteps() const
{
return _steps;
}
signals:
void StepsRecorded(const std::vector<SourceInteractionStep> &);
private slots:
void StartStop();
private:
bool eventFilter(QObject *obj, QEvent *event) override;
bool HandleMouseClick(QMouseEvent *);
bool HandleMouseMove(QMouseEvent *);
bool HandleMouseWheel(QWheelEvent *);
bool HandleKeyEvent(QKeyEvent *);
void AppendStep(const SourceInteractionStep &);
obs_weak_source_t *_source;
SourcePreviewWidget *_preview;
SourceInteractionStepList *_stepList;
QPushButton *_startStopButton;
bool _recording = false;
std::vector<SourceInteractionStep> _steps;
std::chrono::steady_clock::time_point _lastEventTime;
bool _firstEvent = true;
};
} // namespace advss

View File

@ -0,0 +1,315 @@
#include "source-interaction-step-edit.hpp"
#include "obs-module-helper.hpp"
#include "variable-line-edit.hpp"
#include "variable-spinbox.hpp"
#include <map>
#include <string>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QVBoxLayout>
namespace advss {
static const std::map<SourceInteractionStep::Type, std::string> stepTypeNames = {
{SourceInteractionStep::Type::MOUSE_MOVE,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove"},
{SourceInteractionStep::Type::MOUSE_CLICK,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick"},
{SourceInteractionStep::Type::MOUSE_WHEEL,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel"},
{SourceInteractionStep::Type::KEY_PRESS,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress"},
{SourceInteractionStep::Type::TYPE_TEXT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText"},
{SourceInteractionStep::Type::WAIT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wait"},
};
static const std::map<obs_mouse_button_type, std::string> mouseButtonNames = {
{MOUSE_LEFT, "AdvSceneSwitcher.action.sourceInteraction.button.left"},
{MOUSE_MIDDLE,
"AdvSceneSwitcher.action.sourceInteraction.button.middle"},
{MOUSE_RIGHT, "AdvSceneSwitcher.action.sourceInteraction.button.right"},
};
static void populateTypeCombo(QComboBox *combo)
{
for (const auto &[type, name] : stepTypeNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(type));
}
}
static void populateButtonCombo(QComboBox *combo)
{
for (const auto &[btn, name] : mouseButtonNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(btn));
}
}
static QHBoxLayout *labelledRow(const char *labelKey, QWidget *w)
{
auto row = new QHBoxLayout;
row->addWidget(new QLabel(obs_module_text(labelKey)));
row->addWidget(w);
row->addStretch();
return row;
}
SourceInteractionStepEdit::SourceInteractionStepEdit(
QWidget *parent, const SourceInteractionStep &step)
: QWidget(parent),
_typeCombo(new QComboBox(this)),
_fields(new QStackedWidget(this)),
_step(step)
{
populateTypeCombo(_typeCombo);
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
auto typeRow = labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.type",
_typeCombo);
mainLayout->addLayout(typeRow);
mainLayout->addWidget(_fields);
setLayout(mainLayout);
connect(_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SourceInteractionStepEdit::TypeChanged);
RebuildFields();
int idx = _typeCombo->findData(static_cast<int>(_step.type));
if (idx >= 0) {
_typeCombo->setCurrentIndex(idx);
}
}
void SourceInteractionStepEdit::TypeChanged(int)
{
_step.type = static_cast<SourceInteractionStep::Type>(
_typeCombo->currentData().toInt());
RebuildFields();
emit StepChanged(_step);
}
void SourceInteractionStepEdit::UpdateStep()
{
emit StepChanged(_step);
}
void SourceInteractionStepEdit::RebuildFields()
{
while (_fields->count() > 0) {
auto w = _fields->widget(0);
_fields->removeWidget(w);
delete w;
}
auto makeVarSpin = [](int min, int max,
const NumberVariable<int> &val) {
auto sb = new VariableSpinBox;
sb->setMinimum(min);
sb->setMaximum(max);
sb->SetValue(val);
return sb;
};
switch (_step.type) {
case SourceInteractionStep::Type::MOUSE_MOVE:
case SourceInteractionStep::Type::MOUSE_CLICK:
case SourceInteractionStep::Type::MOUSE_WHEEL: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto xSpin = makeVarSpin(-32768, 32767, _step.x);
auto ySpin = makeVarSpin(-32768, 32767, _step.y);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.x",
xSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.y",
ySpin));
connect(xSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, xSpin](const NumberVariable<int> &) {
_step.x = xSpin->Value();
UpdateStep();
});
connect(ySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, ySpin](const NumberVariable<int> &) {
_step.y = ySpin->Value();
UpdateStep();
});
if (_step.type == SourceInteractionStep::Type::MOUSE_CLICK) {
auto btnCombo = new QComboBox;
populateButtonCombo(btnCombo);
btnCombo->setCurrentIndex(btnCombo->findData(
static_cast<int>(_step.button)));
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp"));
upCheck->setChecked(_step.mouseUp);
auto countSpin = makeVarSpin(1, 3, _step.clickCount);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.button",
btnCombo));
layout->addWidget(upCheck);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount",
countSpin));
connect(btnCombo,
QOverload<int>::of(
&QComboBox::currentIndexChanged),
this, [this, btnCombo]() {
_step.button = static_cast<
obs_mouse_button_type>(
btnCombo->currentData().toInt());
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.mouseUp = upCheck->isChecked();
UpdateStep();
});
connect(countSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, countSpin](const NumberVariable<int> &) {
_step.clickCount = countSpin->Value();
UpdateStep();
});
} else if (_step.type ==
SourceInteractionStep::Type::MOUSE_WHEEL) {
auto dxSpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaX);
auto dySpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaY);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx",
dxSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy",
dySpin));
connect(dxSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dxSpin](const NumberVariable<int> &) {
_step.wheelDeltaX = dxSpin->Value();
UpdateStep();
});
connect(dySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dySpin](const NumberVariable<int> &) {
_step.wheelDeltaY = dySpin->Value();
UpdateStep();
});
}
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::KEY_PRESS: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto vkeySpin = makeVarSpin(0, 0xFFFF, _step.nativeVkey);
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp"));
upCheck->setChecked(_step.keyUp);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey",
vkeySpin));
layout->addWidget(upCheck);
connect(vkeySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, vkeySpin](const NumberVariable<int> &) {
_step.nativeVkey = vkeySpin->Value();
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.keyUp = upCheck->isChecked();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::TYPE_TEXT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto textEdit = new VariableLineEdit(page);
textEdit->setText(_step.text);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.text",
textEdit));
connect(textEdit, &QLineEdit::editingFinished, this,
[this, textEdit]() {
_step.text = textEdit->text().toStdString();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::WAIT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto msSpin = makeVarSpin(0, 60000, _step.waitMs);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs",
msSpin));
connect(msSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, msSpin](const NumberVariable<int> &) {
_step.waitMs = msSpin->Value();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
}
}
} // namespace advss

Some files were not shown because too many files have changed in this diff Show More