Compare commits

...

149 Commits
1.32.5 ... lock

Author SHA1 Message Date
WarmUpTill
8174004eda Add option to lock settings dialog with simple password
This is meant as a simple tampering protection.
The settings are not encrypted and the password is stored in plaintext.
2026-05-26 22:57:06 +02:00
WarmUpTill
d61099f9c0 Fix freeze when showing setup wizzard
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-05-25 12:04:57 +02:00
WarmUpTill
c2b73a02ca Prevent plugin from being loaded twice 2026-05-25 11:49:37 +02:00
WarmUpTill
49a681bcfe Show "Macro Schedule" tab when date / timer condition is created 2026-05-25 11:49:37 +02:00
WarmUpTill
4cedb29123 Fix issues with Windows installer
Don't reuse the old installation dir since it might not be compatible
with the new layout.
Ensure previous legacy install is cleaned up to avoid plugin being
installed twice.
2026-05-25 11:49:37 +02:00
WarmUpTill
d80b3aaf69 CI: Ensure ccache is used on macOS 2026-05-25 11:49:37 +02:00
WarmUpTill
b1cc15386f Fix warnings 2026-05-25 11:49:37 +02:00
WarmUpTill
748744d1b7 Fix packaging issue on Ubuntu 2026-05-25 11:49:37 +02:00
WarmUpTill
912ff98e27 Ignore warnings generated by libremidi 2026-05-25 11:49:37 +02:00
WarmUpTill
a9614c5306 CI: Fix warnings 2026-05-25 11:49:34 +02:00
WarmUpTill
cb2f97f11d Adapt macOS fullscreen window detection to be more reliable 2026-05-25 00:22:59 +02:00
WarmUpTill
017ea233ec Fix macOS fullscreen window detection across "Spaces" 2026-05-25 00:22:59 +02:00
WarmUpTill
e8206173ff Tests: Add test-macro-condition-window.cpp 2026-05-25 00:22:59 +02:00
WarmUpTill
680f72e4f9 Rework temp var mapping handling on copy of macro segments 2026-05-25 00:22:59 +02:00
WarmUpTill
930e8b38d4 Refactor window platform helper functions 2026-05-25 00:22:59 +02:00
WarmUpTill
ee1d0136d9 Add temp vars for window position and size 2026-05-25 00:22:59 +02:00
WarmUpTill
20c276f4ea CI: Fix occasional formatting check failures 2026-05-25 00:22:59 +02:00
WarmUpTill
bea4f66eb1 Fix crash when accessing files in profile dir 2026-05-25 00:22:59 +02:00
WarmUpTill
c5185765ee CI: Pin openssl version on Windows
Must stick with OpenSSL 3 for now to stay compatbile with the asio
version supported by websocketpp
2026-05-25 00:22:59 +02:00
WarmUpTill
90130c119e Fix crash due to "keep source active" setting 2026-05-25 00:22:59 +02:00
WarmUpTill
a7ca06d25c Prevent crash when OCR traineddata file is missing 2026-05-25 00:22:59 +02:00
WarmUpTill
5a5d590c51 CI: Trigger rebuild of dependencies 2026-05-25 00:22:58 +02:00
WarmUpTill
336b398bc3 deps: Update libusb submodule to v1.0.30-rc2
Required for v145 toolset support on Windows
2026-05-25 00:22:58 +02:00
WarmUpTill
56c8f335db deps: Update leptonica submodule to 1.87.0
Required to support cmake 4+
2026-05-25 00:22:58 +02:00
WarmUpTill
b6a4d48948 Fix temp var mapping not working for new macro segments 2026-05-25 00:22:58 +02:00
WarmUpTill
774ccb7fbd Fix temp var mapping not working after copy-paste of segment 2026-05-25 00:22:58 +02:00
WarmUpTill
eda9dde23d Add temp vars indicating OCR match location 2026-05-25 00:22:58 +02:00
WarmUpTill
bbf91efd0d Add "Macro Schedule" tab 2026-05-25 00:22:54 +02:00
WarmUpTill
b022d59391 Add calendar widget 2026-05-15 19:15:19 +02:00
WarmUpTill
cf46a010a7 Add support for units "days" 2026-05-15 19:15:19 +02:00
WarmUpTill
595bab0d83 Add option to export Websocket, Twitch, HTTP connections 2026-05-15 19:15:19 +02:00
WarmUpTill
6fc819d72b Add API to expand export / import dialog 2026-05-15 19:15:19 +02:00
WarmUpTill
0553a9b2ee Call MacroRef::PostLoad() automatically 2026-05-15 19:15:19 +02:00
WarmUpTill
3caa3a4735 Add helpers to check, run, stop, and pause macros 2026-05-15 19:15:19 +02:00
WarmUpTill
2469057ad6 Fix double free
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-05-11 17:30:18 +02:00
WarmUpTill
b230ec5bc5 locale: Clarify "reevaluate condition state" option for run macro action 2026-05-11 17:30:18 +02:00
WarmUpTill
1669b4cb72 Fix "Run Macro" help being visible when option is not selected 2026-05-11 17:30:18 +02:00
WarmUpTill
2841fcaadb Fix variable dock not listing variables on startup 2026-05-11 17:30:11 +02:00
WarmUpTill
91810d9c40 Enable resolving plugin symbols in OBS crash reports 2026-05-11 17:24:35 +02:00
WarmUpTill
ccb5c8ba2c Don't highlight paused macro in macro list 2026-05-11 17:24:35 +02:00
WarmUpTill
265daebdf3 Don't log "on change" when macro is paused 2026-05-11 17:24:35 +02:00
WarmUpTill
a149a11c57 Don't highlight action trigger mode when macro is paused 2026-05-11 17:24:35 +02:00
WarmUpTill
d7cc7a4c7b Ensure plugin is stopped before running cleanup steps 2026-05-11 17:24:35 +02:00
WarmUpTill
41866bcb24 Replace "Ignore Entry" condition logic with SwitchButton toggle 2026-05-11 17:24:35 +02:00
WarmUpTill
c73a5866d7 Add option to keep source of video condition active
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-05-10 12:47:55 +02:00
WarmUpTill
18aaf1dd31 Add option to keep selected game capture sources active 2026-05-10 12:47:55 +02:00
WarmUpTill
d523a2e674 Add helper to keep sources active
To be used for e.g. the video condition / game capture condition, which
can only get the relevant information while a given source is "active".

A source can become active if it is not part of the currently selected
scene.
2026-05-10 12:47:55 +02:00
WarmUpTill
0f7eed6257 Fix "transitioning from" check not working in studio mode
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-05-09 13:05:50 +02:00
WarmUpTill
1f5a346190 Fix transition signals not being connected on scene selection change
This would be the case if a new transition condition was created, which
immediately is changed to "transitioning from ..." or "transitioning to
..." type.
2026-05-09 13:05:50 +02:00
WarmUpTill
7fd2979e86 Support legacy and new Windows install paths in zip archive
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
See https://obsproject.com/kb/plugins-guide for more details
2026-05-08 22:15:44 +02:00
WarmUpTill
2077e1971d Adapt to new plugin folder structure / location for Windows 2026-05-08 22:15:44 +02:00
WarmUpTill
873e073eb6 Show all feature tabs when setup wizard is skipped
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-05-05 19:36:41 +02:00
WarmUpTill
4f3d9e3a00 Use static self-registration for action queue and variable setup
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Removes explicit setup calls from the plugin core. Each module now
registers its own save/load/cleanup steps via a static initializer,
decoupling them from InitSceneSwitcher and SwitcherData.
2026-04-30 22:39:06 +02:00
WarmUpTill
c02896ea17 Clear websocket and mqtt connections on shutdown
This was previously implicitly done by a settings reload on shutdown,
but this has since been removed, and we need to clear the connections
explicitly.
2026-04-30 22:39:06 +02:00
WarmUpTill
4bfd40219b Deregister all inline scripts on SCRIPTING_SHUTDOWN 2026-04-30 22:39:06 +02:00
WarmUpTill
6a3de18069 Clear macros on shutdown
This was done implicitly in the shutdown handler by clearing the
plugin's settings.
Due to unwanted side effects this behavior has been disabled for the
shutdown and thus macros have to be cleared explicitly on shutdown.
2026-04-30 22:39:06 +02:00
WarmUpTill
8c51b44fbb Fix potential event sub freeze on disconnect 2026-04-30 22:39:06 +02:00
WarmUpTill
10daa38921 Cleanup 2026-04-30 22:39:06 +02:00
WarmUpTill
6e0f7ab38f Default to assuming OBS is shutting down
This reverts commit eea91b79b38b7c33561dc6c639d01dea8a802cdd.
2026-04-30 22:39:06 +02:00
WarmUpTill
86061b3cf1 Ignore OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP on shutdown
This prevents the plugin being restarted, connections being reset, and
potentially delaying the shutdown of OBS.
2026-04-30 22:39:06 +02:00
WarmUpTill
c3669cae3e Tests: Make building tests optional, enable for CI 2026-04-30 20:11:52 +02:00
WarmUpTill
a116360f87 Tests: Handle QFile::open() return value 2026-04-30 20:11:52 +02:00
WarmUpTill
713f695aa4 Show variable mapping for "Source" and "Twitch" action
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-27 19:20:51 +02:00
WarmUpTill
dec5c2d763 Add option to directly map temp var values to variables 2026-04-27 19:20:51 +02:00
WarmUpTill
f090dea136 Add "Run macro" option
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-21 17:48:05 +02:00
WarmUpTill
d00986df03 Add button to get current values to "Streaming" action 2026-04-21 17:45:35 +02:00
WarmUpTill
03e5397b7e CI: remove openssl workaround
Some checks are pending
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-04-20 20:47:46 +02:00
WarmUpTill
94d49db196 Add tests for process condition
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-15 14:02:59 +02:00
WarmUpTill
1483f9d9dc Cleanup / style changes 2026-04-15 14:02:59 +02:00
WarmUpTill
99629e8c66 Add process path check 2026-04-15 14:02:59 +02:00
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
WarmUpTill
f35ef558ee Fix crash when restoring scene item transition in parallel
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-19 20:28:10 +01:00
WarmUpTill
d7ff9088f8 Enable selecting multiple days in simple view of date condition 2026-02-19 20:11:12 +01:00
WarmUpTill
7048925d6e Add DayOfWeekSelector 2026-02-19 20:11:12 +01:00
WarmUpTill
fcb3ea50d3 Add QTimer::singleShot() wrappers to display startup dialogs on macOS 2026-02-19 20:11:12 +01:00
WarmUpTill
87c45e2b32 Focus name edit for new items 2026-02-19 20:11:12 +01:00
WarmUpTill
eb5046a9d6 Add websocket callbacks to run macros and set variables
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-17 22:07:54 +01:00
WarmUpTill
58c05f3f6e Fix race condition in plugin post load steps 2026-02-17 22:07:54 +01:00
WarmUpTill
0b284da3de Fix stream deck condition not matching key "down" state properly 2026-02-17 22:07:54 +01:00
WarmUpTill
03fe7016e4 Fix crash in game capture condition when receiving null data
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-07 22:38:26 +01:00
WarmUpTill
2ea89912a3 Fix transition duration selection not being editable
Some checks failed
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
Check locale / ubuntu64 (push) Has been cancelled
2026-02-07 21:32:58 +01:00
WarmUpTill
78d2efa083 Skip invalid json object settings value 2026-02-07 21:32:58 +01:00
WarmUpTill
149fce1c2b Show tooltip updates while the tooltip is visible 2026-02-07 21:32:58 +01:00
WarmUpTill
2dd9120265 Add tooltip for last execution time of macro in macro list 2026-02-07 21:32:58 +01:00
WarmUpTill
3a9f315b67 Highlight macros blocked by the "on change" setting in the macro list 2026-02-07 21:32:58 +01:00
WarmUpTill
6ba01dca39 Fix Twitch event sub reconnect handling
If the connection was aborted by the local machine, the active
subsciptions were not cleared causing the new connection not attempt to
register any new subsciptions.

An event sub connection without any active subsciptions will be dropped
by Twitch after a certain amount of time.

Additionally the reconnection logic was triggering to frequently causing
unecessary load.
2026-02-07 21:32:58 +01:00
WarmUpTill
ad1f1effeb Add option to pick random value 2026-02-07 21:32:58 +01:00
WarmUpTill
0a53a8649f Add initializer_list support and set default add dialog window title 2026-02-07 21:32:57 +01:00
WarmUpTill
7ea721f9f1 Add option to set transition when changing scene item visibility 2026-01-27 19:28:45 +01:00
WarmUpTill
718a899a98 Move functions 2026-01-27 19:28:45 +01:00
WarmUpTill
4c493451f4 Keep URL parameters if parameters input is not used 2026-01-18 19:44:19 +01:00
WarmUpTill
a8d483f5a7 Enable un-/pausing all macros in a group 2026-01-13 21:45:04 +01:00
WarmUpTill
dc786a5313 Cleanup 'find and replace' layout in variable action 2026-01-13 21:44:17 +01:00
WarmUpTill
628a4d896c Add option to stop plugin in case unclean shutdown is detected 2026-01-05 10:58:55 +01:00
WarmUpTill
74a9681c8e Fix deadlock when opening settings window 2026-01-05 10:58:55 +01:00
WarmUpTill
faf52c38bb Add temp var for window text 2026-01-05 10:58:55 +01:00
WarmUpTill
bad1a548fb Fix resource table shortcuts conflicting with OBS shortcuts
This could happen when adding the variable tab as a dock.
The problem is visible in the OBS log:
QAction::event: Ambiguous shortcut overload: Del
2025-12-27 20:38:53 +01:00
WarmUpTill
f706416df5 Fix warnings
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
2025-12-21 14:33:08 +01:00
WarmUpTill
c344c88acd Add support for scenes of secondary canvases in source selection 2025-12-21 14:33:08 +01:00
WarmUpTill
2cd9e61717 Adapt paths to new OBS directory structure 2025-12-21 14:33:08 +01:00
258 changed files with 17120 additions and 2829 deletions

View File

@@ -19,14 +19,9 @@ inputs:
runs:
using: 'composite'
steps:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.x.x'
- name: Restore cached dependencies
id: restore-cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ env.DEP_DIR }}
key: ${{ env.DEP_DIR }}-${{ runner.os }}-${{ inputs.target }}

View File

@@ -29,6 +29,7 @@ runs:
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
echo "/home/linuxbrew/.linuxbrew/opt/clang-format@16/bin" >> $GITHUB_PATH
brew update
brew install --quiet zsh
echo ::endgroup::
@@ -52,6 +53,7 @@ runs:
if (( ${changes[(I)(*.c|*.h|*.cpp|*.hpp|*.m|*.mm)]} )) {
echo ::group::Install clang-format-16
brew update
brew install --quiet obsproject/tools/clang-format@16
echo ::endgroup::

View File

@@ -1,4 +1,4 @@
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl'
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl', version: '3.6.2'
package 'Microsoft.VisualStudio.Locator', path: 'vswhere', bin: 'vswhere'

View File

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

View File

@@ -68,14 +68,44 @@ function Package {
Remove-Item @RemoveArgs
# Build a staging directory with two subfolders:
# recommended/ - new layout, extract to %ProgramData%\obs-studio\
# legacy/ - old layout, extract to the OBS install directory
$ReleasePath = "${ProjectRoot}/release/${Configuration}"
$StagingPath = "${ProjectRoot}/release/zip-staging"
$NewBinPath = "${ReleasePath}/${ProductName}/bin/64bit"
$NewDataPath = "${ReleasePath}/${ProductName}/data"
Remove-Item -Path $StagingPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $StagingPath | Out-Null
Copy-Item -Path "${ProjectRoot}/build-aux/CI/windows/README.txt" -Destination "${StagingPath}/README.txt"
if ( Test-Path -Path $NewBinPath ) {
$RecBinPath = "${StagingPath}/recommended/${ProductName}/bin/64bit"
$LegacyBinPath = "${StagingPath}/legacy/obs-plugins/64bit"
New-Item -ItemType Directory -Force -Path $RecBinPath | Out-Null
New-Item -ItemType Directory -Force -Path $LegacyBinPath | Out-Null
Copy-Item -Path "${NewBinPath}/*" -Destination $RecBinPath -Recurse -Force
Copy-Item -Path "${NewBinPath}/*" -Destination $LegacyBinPath -Recurse -Force
}
if ( Test-Path -Path $NewDataPath ) {
$RecDataPath = "${StagingPath}/recommended/${ProductName}/data"
$LegacyDataPath = "${StagingPath}/legacy/data/obs-plugins/${ProductName}"
New-Item -ItemType Directory -Force -Path $RecDataPath | Out-Null
New-Item -ItemType Directory -Force -Path $LegacyDataPath | Out-Null
Copy-Item -Path "${NewDataPath}/*" -Destination $RecDataPath -Recurse -Force
Copy-Item -Path "${NewDataPath}/*" -Destination $LegacyDataPath -Recurse -Force
}
Log-Group "Archiving ${ProductName}..."
$CompressArgs = @{
Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*")
Path = (Get-ChildItem -Path $StagingPath)
CompressionLevel = 'Optimal'
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
Verbose = ($Env:CI -ne $null)
Verbose = ($null -ne $Env:CI)
}
Compress-Archive -Force @CompressArgs
Remove-Item -Path $StagingPath -Recurse -Force
Log-Group
if ( ( $BuildInstaller ) ) {
@@ -89,6 +119,7 @@ function Package {
Log-Information 'Creating InnoSetup installer...'
Push-Location -Stack BuildTemp
Ensure-Location -Path "${ProjectRoot}/release"
Remove-Item -Path Package -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item -Path ${Configuration} -Destination Package -Recurse
Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer"
Remove-Item -Path Package -Recurse

View File

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

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: "recursive"
- name: check_libobs_revision
@@ -50,7 +50,7 @@ jobs:
mv ../*.deb .
- name: Publish
if: success()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: "obs-scene-switcher.deb"
path: ${{ github.workspace }}/*.deb

View File

@@ -6,7 +6,7 @@ on:
description: "Project name detected by parsing build spec file"
value: ${{ jobs.check-event.outputs.pluginName }}
env:
DEP_DIR: .deps/advss-build-dependencies-3
DEP_DIR: .deps/advss-build-dependencies-5
jobs:
check-event:
name: Check GitHub Event Data 🔎
@@ -22,7 +22,7 @@ jobs:
commitHash: ${{ steps.setup.outputs.commitHash }}
pluginName: ${{ steps.setup.outputs.pluginName }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check Event Data ☑️
@@ -81,7 +81,7 @@ jobs:
run:
shell: zsh --no-rcs --errexit --pipefail {0}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
@@ -111,7 +111,7 @@ jobs:
sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer
print '::endgroup::'
- uses: actions/cache@v4
- uses: actions/cache@v5
id: ccache-cache
with:
path: ${{ github.workspace }}/.ccache
@@ -163,13 +163,13 @@ jobs:
codesignPass: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-macos-universal.*
- name: Upload Debug Symbol Artifacts 🪲
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: ${{ needs.check-event.outputs.config == 'Release' }}
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}-dSYMs
@@ -183,7 +183,7 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
@@ -202,7 +202,7 @@ jobs:
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
id: ccache-cache
with:
path: ${{ github.workspace }}/.ccache
@@ -210,13 +210,8 @@ jobs:
restore-keys: |
${{ runner.os }}-ccache-x86_64-
- name: Set up CMake 🏗️
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.x.x'
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@master
uses: Homebrew/actions/setup-homebrew@main
- name: Build Plugin 🧱
uses: ./.github/actions/build-plugin
@@ -238,23 +233,24 @@ jobs:
config: ${{ needs.check-event.outputs.config }}
- name: Upload Source Tarball 🗜️
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*.*
- name: Upload debug symbol artifacts 🪲
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
if-no-files-found: ignore
windows-build:
name: Build for Windows 🪟
@@ -264,7 +260,7 @@ jobs:
run:
shell: pwsh
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
@@ -311,7 +307,7 @@ jobs:
package: ${{ fromJSON(needs.check-event.outputs.package) }}
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-windows-x64-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-windows-x64*.*

View File

@@ -5,7 +5,7 @@ jobs:
clang-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: clang-format check 🐉
@@ -17,7 +17,7 @@ jobs:
cmake-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: cmake-format check 🎛️

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check locale files
run: |

View File

@@ -59,7 +59,7 @@ jobs:
esac
- name: Download Build Artifacts 📥
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
if: fromJSON(steps.check.outputs.validTag)
id: download
@@ -106,7 +106,7 @@ jobs:
- name: Create Release 🛫
if: fromJSON(steps.check.outputs.validTag)
id: create_release
uses: softprops/action-gh-release@d4e8205d7e959a9107da6396278b2f1f07af0f9b
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
with:
draft: true
prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }}

View File

@@ -6,18 +6,18 @@ You have the option to ...
Both methods require [CMake](https://cmake.org/download/).
The plugin can be compiled for OBS 27 and above, although using the latest version of OBS is recommended to support all features.
The plugin can be compiled for OBS 31 and above, although using the latest version of OBS is recommended to support all features.
## Compiling in tree (recommended for development)
This section assumes that you have a working [OBS Studio development environment](https://obsproject.com/wiki/Building-OBS-Studio).
Add the "SceneSwitcher" source directory to your obs-studio source directory under obs-studio/UI/frontend-plugins/:
Add the "SceneSwitcher" source directory to your obs-studio source directory under obs-studio/plugins:
```
cd obs-studio/UI/frontend-plugins/
cd obs-studio/plugins
git clone --recursive https://github.com/WarmUpTill/SceneSwitcher.git
```
Then modify the obs-studio/UI/frontend-plugins/CMakeLists.txt Example and add an entry for the scene switcher:
Then modify the obs-studio/plugins/CMakeLists.txt and add an entry for the scene switcher:
```
add_subdirectory(SceneSwitcher)
```

View File

@@ -134,6 +134,9 @@ target_sources(
lib/macro/macro-dock-window.hpp
lib/macro/macro-edit.cpp
lib/macro/macro-edit.hpp
lib/macro/macro-export-builtin-extensions.cpp
lib/macro/macro-export-extensions.cpp
lib/macro/macro-export-extensions.hpp
lib/macro/macro-export-import-dialog.cpp
lib/macro/macro-export-import-dialog.hpp
lib/macro/macro-helpers.cpp
@@ -166,6 +169,7 @@ target_sources(
lib/macro/macro-tab.cpp
lib/macro/macro-tree.cpp
lib/macro/macro-tree.hpp
lib/macro/macro-websocket-trigger.cpp
lib/macro/macro.cpp
lib/macro/macro.hpp)
@@ -184,6 +188,8 @@ target_sources(
lib/utils/canvas-helpers.hpp
lib/utils/condition-logic.cpp
lib/utils/condition-logic.hpp
lib/utils/crash-handler.cpp
lib/utils/crash-handler.hpp
lib/utils/curl-helper.cpp
lib/utils/curl-helper.hpp
lib/utils/cursor-shape-changer.cpp
@@ -201,6 +207,8 @@ target_sources(
lib/utils/file-selection.hpp
lib/utils/filter-combo-box.cpp
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.cpp
lib/utils/item-selection-helpers.cpp
@@ -451,14 +459,29 @@ else()
endif()
if(PROCPS2_INCLUDE_DIR)
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)
set(PROC_INCLUDE_DIR "${PROCPS2_INCLUDE_DIR}")
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_AVAILABLE)
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()
if(NOT DEFINED PROCESS_CONDITION_SUPPORTED)
message(
@@ -484,7 +507,11 @@ endif()
# --- End of section ---
add_subdirectory(plugins)
add_subdirectory(tests)
option(ADVSS_ENABLE_TESTS "Build advanced-scene-switcher unit tests" OFF)
if(ADVSS_ENABLE_TESTS)
add_subdirectory(tests)
endif()
# --- Install ---

View File

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

View File

@@ -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
```
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).
## Contributing

View File

@@ -0,0 +1,37 @@
Advanced Scene Switcher - Windows Installation
==============================================
For full installation instructions visit:
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
Quick guide
-----------
This archive contains two installation methods.
Choose ONE depending on your OBS version.
recommended\ (preferred)
---------------------------------------------
Extract the CONTENTS of this folder into:
C:\ProgramData\obs-studio\
Tip: press Win+R and type %ProgramData%\obs-studio\plugins to open that folder.
After extracting, the path should look like:
C:\ProgramData\obs-studio\plugins\advanced-scene-switcher\bin\64bit\advanced-scene-switcher.dll
legacy\ (older OBS versions, portable OBS installs, or if the recommended method does not work)
---------------------------------------------------------------------------
Extract the CONTENTS of this folder into your OBS installation directory,
typically:
C:\Program Files\obs-studio\
Merge the data\ and obs-plugins\ folders with the existing ones when prompted.
Steam users: right-click OBS in your library -> Manage -> Browse local files,
then extract the contents of legacy\ into that directory.

View File

@@ -2,7 +2,7 @@
if(BUILD_OUT_OF_TREE)
if(OS_WINDOWS)
set(OBS_PLUGIN_DESTINATION "obs-plugins/64bit")
set(OBS_PLUGIN_DESTINATION "${CMAKE_PROJECT_NAME}/bin/64bit")
else()
set(OBS_PLUGIN_DESTINATION "${CMAKE_INSTALL_LIBDIR}/obs-plugins")
endif()

View File

@@ -64,7 +64,7 @@ function(_git_find_closest_git_dir _start_dir _git_dir_var)
while(NOT EXISTS "${git_dir}")
# .git dir not found, search parent directories
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)
# We have reached the root directory, we are not in git
set(${_git_dir_var}

View File

@@ -18,7 +18,9 @@ set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}"
)
set(CPACK_GENERATOR "DEB")
set(CPACK_GENERATOR
"DEB"
CACHE STRING "CPack generator to use")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
set(CPACK_SET_DESTDIR ON)
@@ -46,6 +48,19 @@ set(CPACK_SOURCE_PACKAGE_FILE_NAME
set(CPACK_ARCHIVE_THREADS 0)
include(CPack)
# CPack.cmake has a side effect that leaves binary packaging variables in the
# state intended for source packaging when it finishes. Specifically it sets
# CPACK_GENERATOR to CPACK_SOURCE_GENERATOR ("TXZ"), sets
# CPACK_INSTALLED_DIRECTORIES to the source root (which would cause CPackDeb to
# scan the entire source tree, including ccache object files that lack a Build
# ID), and clears CPACK_INSTALL_CMAKE_PROJECTS (preventing the cmake install
# mechanism from being used). In some cmake versions these overrides happen
# before CPackConfig.cmake is written. Reset all three so that binary DEB
# packaging works correctly regardless of cmake version.
set(CPACK_GENERATOR "DEB")
set(CPACK_INSTALLED_DIRECTORIES "")
set(CPACK_INSTALL_CMAKE_PROJECTS
"${CMAKE_BINARY_DIR};${CMAKE_PROJECT_NAME};ALL;/")
find_package(libobs QUIET)

View File

@@ -6,3 +6,9 @@ include_guard(GLOBAL)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
include(buildspec)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX
"$ENV{ALLUSERSPROFILE}/obs-studio/plugins"
CACHE STRING "Default plugin installation directory" FORCE)
endif()

View File

@@ -30,13 +30,13 @@ function(set_target_properties_plugin target)
install(
TARGETS ${target}
RUNTIME DESTINATION bin/64bit
LIBRARY DESTINATION obs-plugins/64bit)
RUNTIME DESTINATION "${target}/bin/64bit"
LIBRARY DESTINATION "${target}/bin/64bit")
install(
FILES "$<TARGET_PDB_FILE:${target}>"
CONFIGURATIONS RelWithDebInfo Debug Release
DESTINATION obs-plugins/64bit
DESTINATION "${target}/bin/64bit"
OPTIONAL)
if(OBS_BUILD_DIR)
@@ -44,11 +44,11 @@ function(set_target_properties_plugin target)
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory
"${OBS_BUILD_DIR}/obs-plugins/64bit"
"${OBS_BUILD_DIR}/plugins/${target}/bin/64bit"
COMMAND
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>"
"$<$<CONFIG:Debug,RelWithDebInfo,Release>:$<TARGET_PDB_FILE:${target}>>"
"${OBS_BUILD_DIR}/obs-plugins/64bit"
"${OBS_BUILD_DIR}/plugins/${target}/bin/64bit"
COMMENT "Copy ${target} to obs-studio directory ${OBS_BUILD_DIR}"
VERBATIM)
endif()
@@ -105,7 +105,7 @@ function(target_install_resources target)
install(
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
DESTINATION data/obs-plugins/${target}
DESTINATION "${target}/data"
USE_SOURCE_PERMISSIONS)
if(OBS_BUILD_DIR)
@@ -113,11 +113,11 @@ function(target_install_resources target)
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
"${OBS_BUILD_DIR}/plugins/${target}/data"
COMMAND
"${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data"
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
"${OBS_BUILD_DIR}/plugins/${target}/data"
COMMENT "Copy ${target} resources to data directory"
VERBATIM)
endif()
@@ -133,7 +133,7 @@ function(target_add_resource target resource)
install(
FILES "${resource}"
DESTINATION data/obs-plugins/${target}
DESTINATION "${target}/data"
COMPONENT Runtime)
if(OBS_BUILD_DIR)
@@ -141,9 +141,9 @@ function(target_add_resource target resource)
TARGET ${target}
POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
"${OBS_BUILD_DIR}/plugins/${target}/data"
COMMAND "${CMAKE_COMMAND}" -E copy "${resource}"
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
"${OBS_BUILD_DIR}/plugins/${target}/data"
COMMENT "Copy ${target} resource ${resource} to library directory"
VERBATIM)
endif()

View File

@@ -2,12 +2,13 @@
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
#define MyAppPublisher "@PLUGIN_AUTHOR@"
#define MyAppURL "@PLUGIN_WEBSITE@"
#define MyAppId "@UUID_APP@"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{@UUID_APP@}
AppId={{{#MyAppId}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
@@ -20,8 +21,9 @@ OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer
Compression=lzma
SolidCompression=yes
DirExistsWarning=no
UsePreviousAppDir=no
SetupIconFile=installer.ico
UninstallDisplayIcon={app}\data\obs-plugins\advanced-scene-switcher\res\images\logo.ico
UninstallDisplayIcon={app}\advanced-scene-switcher\data\res\images\logo.ico
DisableDirPage=no
[Languages]
@@ -51,17 +53,34 @@ begin
);
end;
// credit where it's due :
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
function GetDirName(Value: string): string;
var
InstallPath: string;
begin
// initialize default path, which will be returned when the following registry
// key queries fail due to missing keys or for some different reason
Result := '{autopf}\obs-studio';
// query the first registry value; if this succeeds, return the obtained value
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath
Result := ExpandConstant('{commonappdata}\obs-studio\plugins');
end;
// Remove files placed by the legacy installer (obs-plugins/64bit and
// data/obs-plugins layout) so the plugin is not loaded twice by OBS.
// We read InstallLocation from our own previous uninstall entry, which is
// the exact directory the old installer used regardless of OBS install path.
procedure RemoveLegacyFiles();
var
OldInstallPath: string;
UninstallKey: string;
begin
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
if not RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', OldInstallPath) then
Exit;
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.dll');
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.pdb');
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.dll');
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.pdb');
DelTree(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-plugins', True, True, True);
DelTree(OldInstallPath + '\data\obs-plugins\advanced-scene-switcher', True, True, True);
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
RemoveLegacyFiles();
end;

View File

@@ -76,7 +76,6 @@ AdvSceneSwitcher.macroTab.name="Name:"
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.runInParallel="Parallel zu anderen Makros ausführen"
AdvSceneSwitcher.macroTab.onChange="Nur bei Änderung ausführen"
AdvSceneSwitcher.macroTab.defaultname="Makro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Gruppe %1"
AdvSceneSwitcher.macroTab.removeGroupPopup.text="Sicher, dass \"%1\" und alle zugehörigen Elemente gelöscht werden?"
@@ -100,7 +99,6 @@ AdvSceneSwitcher.macroList.deleted="gelöscht"
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
; Macro Logic
AdvSceneSwitcher.logic.none="Eintrag ignorieren"
AdvSceneSwitcher.logic.and="Und"
AdvSceneSwitcher.logic.or="Oder"
AdvSceneSwitcher.logic.andNot="Und nicht"
@@ -146,11 +144,7 @@ AdvSceneSwitcher.condition.file="Datei"
AdvSceneSwitcher.condition.file.type.match="entspricht"
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
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.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medien"
AdvSceneSwitcher.condition.media.source="Quelle"
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
@@ -216,8 +210,8 @@ AdvSceneSwitcher.condition.record.state.start="Aufnahme läuft"
AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert"
AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt"
AdvSceneSwitcher.condition.process="Prozess"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.entry.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.layout.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="Leerlauf"
AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin-Status"
@@ -291,14 +285,14 @@ AdvSceneSwitcher.condition.replay.state.started="Replay Buffer gestartet"
AdvSceneSwitcher.condition.replay.state.saved="Replay Buffer gespeichert"
AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="Datum"
AdvSceneSwitcher.condition.date.anyDay="Beliebiger Tag"
AdvSceneSwitcher.condition.date.monday="Montag"
AdvSceneSwitcher.condition.date.tuesday="Dienstag"
AdvSceneSwitcher.condition.date.wednesday="Mittwoch"
AdvSceneSwitcher.condition.date.thursday="Donnerstag"
AdvSceneSwitcher.condition.date.friday="Freitag"
AdvSceneSwitcher.condition.date.saturday="Samstag"
AdvSceneSwitcher.condition.date.sunday="Sonntag"
AdvSceneSwitcher.day.any="Beliebiger Tag"
AdvSceneSwitcher.day.monday="Montag"
AdvSceneSwitcher.day.tuesday="Dienstag"
AdvSceneSwitcher.day.wednesday="Mittwoch"
AdvSceneSwitcher.day.thursday="Donnerstag"
AdvSceneSwitcher.day.friday="Freitag"
AdvSceneSwitcher.day.saturday="Samstag"
AdvSceneSwitcher.day.sunday="Sonntag"
AdvSceneSwitcher.condition.date.state.at="Am"
AdvSceneSwitcher.condition.date.state.after="Nach"
AdvSceneSwitcher.condition.date.state.before="Vor"
@@ -309,12 +303,13 @@ AdvSceneSwitcher.condition.date.ignoreDate="Wenn diese Option nicht aktiviert is
AdvSceneSwitcher.condition.date.ignoreTime="Wenn diese Option nicht aktiviert ist, wird die Zeitkomponente ignoriert"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Erweiterte Einstellungen anzeigen"
AdvSceneSwitcher.condition.date.showSimpleSettings="Einfache Einstellungen anzeigen"
AdvSceneSwitcher.condition.date.entry.simple="Am {{dayOfWeek}} {{weekCondition}} {{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}} Wiederholen alle {{duration}} bei Datumsübereinstimmung"
AdvSceneSwitcher.condition.date.entry.pattern="Aktuelles Datum \"{{currentDate}}\" entspricht dem Muster {{pattern}}"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Nächster Treffer bei: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} Bei Wiederholung ausgewähltes Datum auf Wiederholungsdatum aktualisieren"
AdvSceneSwitcher.condition.date.layout.simple.day="Am{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}} Wiederholen alle {{duration}} bei Datumsübereinstimmung"
AdvSceneSwitcher.condition.date.layout.pattern="Aktuelles Datum \"{{currentDate}}\" entspricht dem Muster {{pattern}}"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="Nächster Treffer bei: %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}} Bei Wiederholung ausgewähltes Datum auf Wiederholungsdatum aktualisieren"
AdvSceneSwitcher.condition.sceneTransform="Szenenelement transformieren"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Transformation erhalten"
AdvSceneSwitcher.condition.sceneTransform.condition.match="entspricht Transformation"
@@ -412,7 +407,6 @@ AdvSceneSwitcher.action.recording.type.unpause="Aufnahme nicht mehr pausieren"
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.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.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"
@@ -421,7 +415,6 @@ AdvSceneSwitcher.action.replay.type.save="Replay Buffer speichern"
AdvSceneSwitcher.action.streaming="Stream"
AdvSceneSwitcher.action.streaming.type.stop="Stream stoppen"
AdvSceneSwitcher.action.streaming.type.start="Stream starten"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ausführen"
AdvSceneSwitcher.action.sceneVisibility="Sichtbarkeit von Szenenelementen"
AdvSceneSwitcher.action.sceneVisibility.type.show="Anzeigen"
@@ -429,7 +422,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="Verstecken"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="Umschalten"
AdvSceneSwitcher.action.sceneVisibility.type.source="Quelle"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Beliebig"
AdvSceneSwitcher.action.sceneVisibility.entry="Auf{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="Auf{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="Filter"
AdvSceneSwitcher.action.filter.type.enable="Aktivieren"
AdvSceneSwitcher.action.filter.type.disable="Deaktivieren"
@@ -457,7 +450,7 @@ AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Pausieren"
AdvSceneSwitcher.action.macro.type.unpause="Nicht mehr pausieren"
AdvSceneSwitcher.action.macro.type.resetCounter="Zähler zurücksetzen"
AdvSceneSwitcher.action.macro.type.run="Aktionen ausführen"
AdvSceneSwitcher.action.macro.type.runActions="Aktionen ausführen"
AdvSceneSwitcher.action.macro.type.stop="Aktionen stoppen"
AdvSceneSwitcher.action.pluginState="Plugin-Status"
AdvSceneSwitcher.action.pluginState.type.stop="Erweiterten Automatischen Szenenwechsler stoppen"
@@ -505,9 +498,9 @@ AdvSceneSwitcher.action.transition.type.scene="Szenenübergang"
AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben"
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
AdvSceneSwitcher.action.transition.layout.type="Anpassen {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"
@@ -834,8 +827,6 @@ AdvSceneSwitcher.status.inactive="Inaktiv"
AdvSceneSwitcher.running="Plugin läuft"
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.unit.milliseconds="Millisekunden"

View File

@@ -40,12 +40,20 @@ AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Show system t
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints"
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.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.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.alwaysShowMacroSearch="Always show macro search"
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs="Always show all feature tabs"
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs.tooltip="Show tabs that are normally hidden until first used in a macro (e.g. Action Queues, Variables, Twitch, Websockets)."
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock="Lock settings dialog with password"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title="Settings Locked"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel="Enter password:"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword="Wrong password!"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.newPasswordLabel="New password:"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.confirmPasswordLabel="Confirm password:"
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordMismatch="Passwords do not match!"
AdvSceneSwitcher.generalTab.matchBehavior="Match behavior"
AdvSceneSwitcher.generalTab.priority="Priority"
AdvSceneSwitcher.generalTab.priority.description="Switching methods priority (Highest priority is at the top)"
@@ -160,6 +168,8 @@ AdvSceneSwitcher.macroTab.title="Macro"
AdvSceneSwitcher.macroTab.macros="Macros"
AdvSceneSwitcher.macroTab.priorityWarning="Note: It is recommended to configure macros to be the highest priority functionality.\nThis setting can be changed on the General tab."
AdvSceneSwitcher.macroTab.help="Macros allow you to execute a string of actions depending on multiple conditions.\n\nClick on the highlighted plus symbol to add a new Macro."
AdvSceneSwitcher.macroTab.macroLastExecutedTooltip="Was last executed %1 seconds ago."
AdvSceneSwitcher.macroTab.macroNotYetExecutedTooltip="Was not yet executed."
AdvSceneSwitcher.macroTab.editConditionHelp="This section allows you to define macro conditions.\n\nSelect an existing or add a new macro on the left.\nThen click the plus button below to add a new condition."
AdvSceneSwitcher.macroTab.editActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are met.\n\nSelect an existing or add a new macro on the left.\nThen click the plus button below to add a new action."
AdvSceneSwitcher.macroTab.editElseActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are *not* met.\n\nSelect an existing or add a new macro on the left.\nThen click the plus button below to add a new action."
@@ -175,7 +185,12 @@ AdvSceneSwitcher.macroTab.run.tooltip="Run all macro actions regardless of condi
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.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.defaultGroupName="Group %1"
AdvSceneSwitcher.macroTab.macroNameExists="The name \"%1\" is already used by a macro."
@@ -194,6 +209,13 @@ AdvSceneSwitcher.macroTab.remove="Remove"
AdvSceneSwitcher.macroTab.export="Export"
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text"
AdvSceneSwitcher.macroTab.export.additionalContent="Export Additional Content"
AdvSceneSwitcher.macroTab.export.variables="Variables"
AdvSceneSwitcher.macroTab.export.actionQueues="Action Queues"
AdvSceneSwitcher.macroTab.export.httpServers="HTTP Servers"
AdvSceneSwitcher.macroTab.export.macroScheduleEntries="Macro Schedule"
AdvSceneSwitcher.macroTab.export.twitchConnections="Twitch Connections"
AdvSceneSwitcher.macroTab.export.websocketConnections="Websocket Connections"
AdvSceneSwitcher.macroTab.import="Import"
AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:"
AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!"
@@ -298,7 +320,6 @@ AdvSceneSwitcher.macroList.deleted="deleted"
AdvSceneSwitcher.macroList.duplicate="\"%1\" is alreay selected!"
# Macro Logic
AdvSceneSwitcher.logic.none="Ignore entry"
AdvSceneSwitcher.logic.and="And"
AdvSceneSwitcher.logic.or="Or"
AdvSceneSwitcher.logic.andNot="And not"
@@ -361,11 +382,7 @@ AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
AdvSceneSwitcher.condition.file.type.exists="exists"
AdvSceneSwitcher.condition.file.type.isFile="is a file"
AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
AdvSceneSwitcher.condition.file.remote="Remote file"
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.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.checkType.state="State matches"
AdvSceneSwitcher.condition.media.checkType.time="Time restriction matches"
@@ -480,8 +497,9 @@ AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
AdvSceneSwitcher.condition.record.state.duration="Recording duration is longer than"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.layout.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout.path="{{checkPath}}Match binary path:{{path}}{{pathRegex}}"
AdvSceneSwitcher.condition.idle="Idle"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin state"
@@ -587,14 +605,6 @@ AdvSceneSwitcher.condition.replay.state.started="Replay buffer started"
AdvSceneSwitcher.condition.replay.state.saved="Replay buffer saved"
AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="Date"
AdvSceneSwitcher.condition.date.anyDay="Any day"
AdvSceneSwitcher.condition.date.monday="Monday"
AdvSceneSwitcher.condition.date.tuesday="Tuesday"
AdvSceneSwitcher.condition.date.wednesday="Wednesday"
AdvSceneSwitcher.condition.date.thursday="Thursday"
AdvSceneSwitcher.condition.date.friday="Friday"
AdvSceneSwitcher.condition.date.saturday="Saturday"
AdvSceneSwitcher.condition.date.sunday="Sunday"
AdvSceneSwitcher.condition.date.state.at="At"
AdvSceneSwitcher.condition.date.state.after="After"
AdvSceneSwitcher.condition.date.state.before="Before"
@@ -605,12 +615,13 @@ AdvSceneSwitcher.condition.date.ignoreDate="If unchecked the date component will
AdvSceneSwitcher.condition.date.ignoreTime="If unchecked the time component will be ignored"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Show advanced settings"
AdvSceneSwitcher.condition.date.showSimpleSettings="Show simple settings"
AdvSceneSwitcher.condition.date.entry.simple="On{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}}Repeat every{{duration}}on date match"
AdvSceneSwitcher.condition.date.entry.pattern="Current date \"{{currentDate}}\" matches pattern{{pattern}}"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Next match at: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}On repeat update selected date to repeat date"
AdvSceneSwitcher.condition.date.layout.simple.day="On{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}}Repeat every{{duration}}on date match"
AdvSceneSwitcher.condition.date.layout.pattern="Current date \"{{currentDate}}\" matches pattern{{pattern}}"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="Next match at: %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}}On repeat update selected date to repeat date"
AdvSceneSwitcher.condition.sceneTransform="Scene item transform"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Get transform"
AdvSceneSwitcher.condition.sceneTransform.getCurrentValue="Get current value"
@@ -676,6 +687,34 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
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.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="equals"
@@ -830,6 +869,7 @@ AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="<html><head/><body><p>The Stream Deck plugin can be found <a href=\"https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin/releases\"><span style=\" text-decoration: underline; color:#268bd2;\">here on GitHub</span></a>.</p></body></html>"
AdvSceneSwitcher.condition.gameCapture="Game capture"
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
AdvSceneSwitcher.condition.screenshot="Screenshot"
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
AdvSceneSwitcher.condition.mqtt="MQTT"
@@ -879,6 +919,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.abort="Abort already active fade."
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.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording"
@@ -887,9 +936,10 @@ AdvSceneSwitcher.action.recording.type.unpause="Unpause recording"
AdvSceneSwitcher.action.recording.type.split="Split recording file"
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
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.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.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!"
@@ -906,7 +956,8 @@ AdvSceneSwitcher.action.streaming.type.server="Set server URL"
AdvSceneSwitcher.action.streaming.type.streamKey="Set stream key"
AdvSceneSwitcher.action.streaming.type.username="Set username"
AdvSceneSwitcher.action.streaming.type.password="Set password"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.streaming.getCurrentValue="Get current value"
AdvSceneSwitcher.action.streaming.layout="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}{{getCurrentValue}}"
AdvSceneSwitcher.action.run="Run"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}Wait for process exit or at most {{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="Note that macro properties won't work if you leave this unticked, as the process spawns detached from the rest of logic and there's no control over it."
@@ -916,7 +967,9 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="Hide"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="Toggle"
AdvSceneSwitcher.action.sceneVisibility.type.source="Source"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Any"
AdvSceneSwitcher.action.sceneVisibility.entry="On{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="On{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout.transition="{{updateTransition}}Set transition to{{transitions}}"
AdvSceneSwitcher.action.sceneVisibility.layout.duration="{{updateDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.filter="Filter"
AdvSceneSwitcher.action.filter.type.enable="Enable"
AdvSceneSwitcher.action.filter.type.disable="Disable"
@@ -947,6 +1000,8 @@ AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog
AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog"
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter 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.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\""
@@ -969,6 +1024,49 @@ AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
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.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.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause"
@@ -988,12 +1086,12 @@ AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Unpause"
AdvSceneSwitcher.action.macro.type.togglePause="Toggle pause"
AdvSceneSwitcher.action.macro.type.resetCounter="Reset counter"
AdvSceneSwitcher.action.macro.type.run="Run macro"
AdvSceneSwitcher.action.macro.type.runActions="Run macro actions"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="Do not consider condition state"
AdvSceneSwitcher.action.macro.type.run.conditions.true="Only if conditions evaluate to true"
AdvSceneSwitcher.action.macro.type.run.conditions.false="Only if conditions evaluate to false"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Reevaluate the condition state before executing this action"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nCheck this option if you want those side effects to be taken into account when evaluating the condition state."
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Check conditions now, even if macro is paused"
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nAdditionally, paused macros are assumed to not match their conditions.\nCheck this option to force a fresh condition evaluation before running this action, regardless of whether the macro is paused."
AdvSceneSwitcher.action.macro.type.run.actionType.regular="actions"
AdvSceneSwitcher.action.macro.type.run.actionType.else="else-actions"
AdvSceneSwitcher.action.macro.type.run.skipWhenPaused="Skip execution when macro is paused"
@@ -1003,6 +1101,10 @@ AdvSceneSwitcher.action.macro.type.stop="Stop actions"
AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
AdvSceneSwitcher.action.macro.type.getInfo="Get macro info"
AdvSceneSwitcher.action.macro.type.runMacro="Run macro"
AdvSceneSwitcher.action.macro.type.runMacro.noConditionsWarning="Warning: The selected macro has no conditions!"
AdvSceneSwitcher.action.macro.type.runMacro.help="Unlike \"Run macro actions\", this option will immediately re-evaluate the conditions of the selected macro and then run either its actions or else-actions depending on the result."
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
@@ -1013,6 +1115,7 @@ AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allo
AdvSceneSwitcher.action.macro.layout.run="{{actions}}{{actionSections}}of{{macros}}"
AdvSceneSwitcher.action.macro.layout.run.condition="{{conditionBehaviors}}of{{conditionMacros}}"
AdvSceneSwitcher.action.macro.layout.actionState="{{actions}}{{actionSelectionType}}{{actionIndex}}{{label}}{{regex}}{{actionTypes}}in{{actionSections}}section of{{macros}}"
AdvSceneSwitcher.action.macro.layout.runMacro="{{actions}}{{runMacroHelp}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.other="{{actions}}{{macros}}"
AdvSceneSwitcher.action.pluginState="Plugin state"
AdvSceneSwitcher.action.pluginState.type.stop="Stop the Advanced Scene Switcher plugin"
@@ -1085,13 +1188,16 @@ AdvSceneSwitcher.action.studioMode.type.enable="Enable studio mode"
AdvSceneSwitcher.action.studioMode.type.disable="Disable studio mode"
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
AdvSceneSwitcher.action.transition="Transition"
AdvSceneSwitcher.action.transition.type.scene="scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition"
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.transition.type.scene="Modify scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="Modify scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="Modify source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="Modify source hide transition"
AdvSceneSwitcher.action.transition.type.tbar="Modify T-Bar position"
AdvSceneSwitcher.action.transition.type.releaseTbar="Release T-Bar"
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.type.pause="Pause"
AdvSceneSwitcher.action.timer.type.continue="Continue"
@@ -1192,6 +1298,8 @@ AdvSceneSwitcher.action.variable.type.swapValues="Swap variable values"
AdvSceneSwitcher.action.variable.type.trim="Trim whitespace"
AdvSceneSwitcher.action.variable.type.changeCase="Change case"
AdvSceneSwitcher.action.variable.type.randomNumber="Generate random number"
AdvSceneSwitcher.action.variable.type.randomListValue="Select random value"
AdvSceneSwitcher.action.variable.type.allowRepeat="Allow repeat values"
AdvSceneSwitcher.action.variable.case.type.lowerCase="lowercase"
AdvSceneSwitcher.action.variable.case.type.upperCase="UPPERCASE"
AdvSceneSwitcher.action.variable.case.type.capitalized="Capitalized"
@@ -1214,8 +1322,8 @@ AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
AdvSceneSwitcher.action.variable.layout.other="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}{{tempVarsHelp}}{{sceneItemIndex}}{{direction}}{{stringLength}}{{paddingCharSelection}}{{caseType}}{{jsonQuery}}{{jsonQueryHelp}}{{jsonIndex}}"
AdvSceneSwitcher.action.variable.layout.pad="{{actions}}of{{variables}}to length{{stringLength}}by adding{{paddingCharSelection}}to the{{direction}}"
AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}of{{variables}}to length{{stringLength}}by removing characters from the{{direction}}"
AdvSceneSwitcher.action.variable.layout.substringIndex="Substring start:{{subStringStart}}Substring size:{{subStringSize}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
AdvSceneSwitcher.action.variable.layout.substringIndex="Substring start:{{subStringStart}}Substring size:{{subStringSize}}{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.findAndReplace="{{findStr}}{{findRegex}}{{replaceStr}}"
AdvSceneSwitcher.action.variable.layout.userInput.customPrompt="{{useCustomPrompt}}Use custom prompt{{inputPrompt}}"
AdvSceneSwitcher.action.variable.layout.userInput.placeholder="{{useInputPlaceholder}}Fill with placeholder{{inputPlaceholder}}"
@@ -1249,23 +1357,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.language.set="Set stream language"
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.snooze="Snooze next ad"
AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
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.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.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.channel.getInfo="Get channel information"
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.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.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.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="Using account{{account}}{{actions}}"
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.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
@@ -1286,6 +1423,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.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.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.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."
@@ -1390,6 +1529,9 @@ AdvSceneSwitcher.hotkey.macro.segment.remove="Remove selected macro segment"
AdvSceneSwitcher.askBackup="Detected a new version of the Advanced Scene Switcher.\nShould a backup of the old settings be created?"
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.suppressCheckbox="Do not show this dialog again"
AdvSceneSwitcher.close="Close"
AdvSceneSwitcher.browse="Browse"
@@ -1658,6 +1800,7 @@ AdvSceneSwitcher.script.language.lua="LUA"
AdvSceneSwitcher.script.language.select="--select language--"
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
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.warning.notFound="Warning: The given file path was not found!"
AdvSceneSwitcher.script.file.warning.openFail="Warning: Could not open the script file!"
@@ -1666,6 +1809,10 @@ AdvSceneSwitcher.script.file.layout="Script file:{{path}}{{open}}"
AdvSceneSwitcher.tempVar.select="--select value--"
AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:"
AdvSceneSwitcher.tempVar.outputMappings="Save outputs to variables:"
AdvSceneSwitcher.tempVar.outputMappings.add="+ Add output mapping"
AdvSceneSwitcher.tempVar.outputMappings.toggle="Assign outputs to variables"
AdvSceneSwitcher.tempVar.outputMappings.remove="Remove output mapping"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id="Twitch broadcaster user ID"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id.description="The numerical Twitch user ID of the broadcaster."
@@ -2147,8 +2294,18 @@ AdvSceneSwitcher.tempVar.scene.position="Position in scenes list"
AdvSceneSwitcher.tempVar.window.window="Window title"
AdvSceneSwitcher.tempVar.window.window.description="The window title of the matching window."
AdvSceneSwitcher.tempVar.window.windowX="Window X position"
AdvSceneSwitcher.tempVar.window.windowX.description="The X position (left edge) of the matching window in screen coordinates."
AdvSceneSwitcher.tempVar.window.windowY="Window Y position"
AdvSceneSwitcher.tempVar.window.windowY.description="The Y position (top edge) of the matching window in screen coordinates."
AdvSceneSwitcher.tempVar.window.windowWidth="Window width"
AdvSceneSwitcher.tempVar.window.windowWidth.description="The width of the matching window in pixels."
AdvSceneSwitcher.tempVar.window.windowHeight="Window height"
AdvSceneSwitcher.tempVar.window.windowHeight.description="The height of the matching window in pixels."
AdvSceneSwitcher.tempVar.window.windowClass="Window class"
AdvSceneSwitcher.tempVar.window.windowClass.description="The window class of the matching window."
AdvSceneSwitcher.tempVar.window.windowText="Window text"
AdvSceneSwitcher.tempVar.window.windowText.description="Text contained within the window (won't work on all window types)."
AdvSceneSwitcher.tempVar.timer.seconds="Seconds"
AdvSceneSwitcher.tempVar.timer.minutes="Minutes"
@@ -2173,8 +2330,21 @@ AdvSceneSwitcher.tempVar.macro.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed."
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.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.path="Process path"
AdvSceneSwitcher.tempVar.run.process.id="Process ID"
AdvSceneSwitcher.tempVar.run.process.id.description="PID of the process assigned by the system."
@@ -2193,8 +2363,18 @@ AdvSceneSwitcher.tempVar.recording.durationSeconds.description="Recording durati
AdvSceneSwitcher.tempVar.recording.lastSavePath="Last save path"
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.description="The number of times the given pattern has been found in a given video input frame."
AdvSceneSwitcher.tempVar.video.matchX="Match X coordinate"
AdvSceneSwitcher.tempVar.video.matchX.description="The X coordinate of the top-left corner of the best pattern match.\nIf the 'check area' option is enabled, the coordinate is relative to the video source, not the area.\nSet to -1 if no match was found."
AdvSceneSwitcher.tempVar.video.matchY="Match Y coordinate"
AdvSceneSwitcher.tempVar.video.matchY.description="The Y coordinate of the top-left corner of the best pattern match.\nIf the 'check area' option is enabled, the coordinate is relative to the video source, not the area.\nSet to -1 if no match was found."
AdvSceneSwitcher.tempVar.video.matchWidth="Match width"
AdvSceneSwitcher.tempVar.video.matchWidth.description="The width of the matched pattern in pixels."
AdvSceneSwitcher.tempVar.video.matchHeight="Match height"
AdvSceneSwitcher.tempVar.video.matchHeight.description="The height of the matched pattern in pixels."
AdvSceneSwitcher.tempVar.video.objectCount="Object count"
AdvSceneSwitcher.tempVar.video.objectCount.description="The number of objects the given model has identified in a given video input frame."
AdvSceneSwitcher.tempVar.video.brightness="Average brightness"
@@ -2203,6 +2383,8 @@ AdvSceneSwitcher.tempVar.video.text="OCR text"
AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame."
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.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.description="The received websocket message, which matched the given pattern"
@@ -2332,9 +2514,14 @@ AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.body="Message body"
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.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"
@@ -2344,6 +2531,14 @@ AdvSceneSwitcher.tempVar.cursor.y="Cursor position (Y)"
AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path"
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.selectCanvas="--select canvas--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
@@ -2373,6 +2568,9 @@ AdvSceneSwitcher.selectDisplay="--select display--"
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
AdvSceneSwitcher.selectWindowTip="Use \"OBS\" to specify OBS window\nUse \"Task Switching\"to specify ALT + TAB"
AdvSceneSwitcher.keepSourceActive="Keep source active"
AdvSceneSwitcher.keepSourceActive.help="When a source is not visible in the active scene it becomes inactive, which can cause it to stop producing output (e.g. video frames or game capture hooks).\nEnabling this option forces the source to remain active at all times, ensuring it continues to produce output even when not displayed.\n\nNote: Keeping a source permanently active may increase CPU and GPU usage, as the source continues to run even when not shown."
AdvSceneSwitcher.settings.suffix.type.invalid=" (Invalid)"
AdvSceneSwitcher.settings.suffix.type.bool=" (Bool)"
AdvSceneSwitcher.settings.suffix.type.int=" (Int)"
@@ -2433,8 +2631,6 @@ AdvSceneSwitcher.status.inactive="Inactive"
AdvSceneSwitcher.running="Plugin running"
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.unit.milliseconds="milliseconds"
@@ -2464,6 +2660,47 @@ AdvSceneSwitcher.clearBufferOnMatch="Clear message buffer when matching message
AdvSceneSwitcher.script.settings="Settings"
AdvSceneSwitcher.script.timeout="Script timeout:{{timeout}}"
AdvSceneSwitcher.day.monday="Monday"
AdvSceneSwitcher.day.tuesday="Tuesday"
AdvSceneSwitcher.day.wednesday="Wednesday"
AdvSceneSwitcher.day.thursday="Thursday"
AdvSceneSwitcher.day.friday="Friday"
AdvSceneSwitcher.day.saturday="Saturday"
AdvSceneSwitcher.day.sunday="Sunday"
AdvSceneSwitcher.day.none="No 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
# OBS commonly shared locale
@@ -2501,6 +2738,64 @@ Basic.Settings.Video.FPSCommon="Common FPS Values"
Basic.Settings.Video.FPSInteger="Integer FPS Value"
Basic.Settings.Video.FPSFraction="Fractional FPS Value"
# Calendar widget
AdvSceneSwitcher.calendar.today="Today"
AdvSceneSwitcher.calendar.month="Month"
AdvSceneSwitcher.calendar.week="Week"
AdvSceneSwitcher.calendar.day="Day"
AdvSceneSwitcher.calendar.moreEvents="+%1 more"
# Macro Schedule tab
AdvSceneSwitcher.macroScheduleTab.title="Macro Schedule"
AdvSceneSwitcher.macroScheduleTab.help="No scheduled entries.\nClick '+' to schedule a macro."
AdvSceneSwitcher.macroScheduleTab.add="Add"
AdvSceneSwitcher.macroScheduleTab.add.tooltip="Add schedule entry"
AdvSceneSwitcher.macroScheduleTab.edit="Edit"
AdvSceneSwitcher.macroScheduleTab.remove="Remove"
AdvSceneSwitcher.macroScheduleTab.remove.tooltip="Remove schedule entry"
AdvSceneSwitcher.macroScheduleTab.enable="Enable"
AdvSceneSwitcher.macroScheduleTab.disable="Disable"
AdvSceneSwitcher.macroScheduleTab.showAll="Show All"
AdvSceneSwitcher.macroScheduleTab.column.name="Name"
AdvSceneSwitcher.macroScheduleTab.column.macro="Macro"
AdvSceneSwitcher.macroScheduleTab.column.schedule="Schedule"
AdvSceneSwitcher.macroScheduleTab.column.nextTrigger="Next Trigger"
AdvSceneSwitcher.macroScheduleTab.column.status="Status"
AdvSceneSwitcher.macroScheduleTab.remove.confirm="Are you sure you want to remove \"%1\"?"
AdvSceneSwitcher.macroScheduleTab.remove.confirmMultiple="Are you sure you want to remove %1 entries?"
AdvSceneSwitcher.macroScheduleTab.status.active="Active"
AdvSceneSwitcher.macroScheduleTab.status.disabled="Disabled"
AdvSceneSwitcher.macroScheduleTab.status.expired="Expired"
# Macro Schedule entry dialog
AdvSceneSwitcher.macroScheduleEntry.dialog.title.add="Add Schedule Entry"
AdvSceneSwitcher.macroScheduleEntry.dialog.title.edit="Edit Schedule Entry"
AdvSceneSwitcher.macroScheduleEntry.name="Name (optional):"
AdvSceneSwitcher.macroScheduleEntry.name.placeholder="e.g. Daily stream start"
AdvSceneSwitcher.macroScheduleEntry.macro="Macro:"
AdvSceneSwitcher.macroScheduleEntry.startDateTime="Start:"
AdvSceneSwitcher.macroScheduleEntry.endDate="End date:"
AdvSceneSwitcher.macroScheduleEntry.endDate.help="Controls how far the entry spans visually in the calendar and optionally triggers an action when the date is reached."
AdvSceneSwitcher.macroScheduleEntry.endDate.action.none="No action"
AdvSceneSwitcher.macroScheduleEntry.endDate.action.disableEntry="Disable schedule entry"
AdvSceneSwitcher.macroScheduleEntry.endDate.action.pauseMacro="Pause macro"
AdvSceneSwitcher.macroScheduleEntry.endDate.action.stopMacro="Stop macro"
AdvSceneSwitcher.macroScheduleEntry.repeat="Repeat"
AdvSceneSwitcher.macroScheduleEntry.repeat.once="Once"
AdvSceneSwitcher.macroScheduleEntry.repeat.every="Every %1"
AdvSceneSwitcher.macroScheduleEntry.repeat.interval="Repeat interval:"
AdvSceneSwitcher.macroScheduleEntry.noMacro="(no macro)"
AdvSceneSwitcher.macroScheduleEntry.repeatEnd="End repeat:"
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.never="Never"
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes="After"
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes.suffix="occurrence(s)"
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.untilDate="Until"
AdvSceneSwitcher.macroScheduleEntry.color="Color:"
AdvSceneSwitcher.macroScheduleEntry.color.dialog="Pick entry color"
AdvSceneSwitcher.macroScheduleEntry.checkConditions="Check macro conditions before running"
AdvSceneSwitcher.macroScheduleEntry.runElseActionsOnConditionFailure="Run else actions if conditions are not met"
AdvSceneSwitcher.macroScheduleEntry.enabled="Enabled"
# Legacy tabs below - please don't waste your time adding translations for these :)
# Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"

View File

@@ -72,7 +72,6 @@ AdvSceneSwitcher.macroTab.add="Agregar nueva macro"
AdvSceneSwitcher.macroTab.name="Nombre:"
AdvSceneSwitcher.macroTab.run="Ejecutar macro"
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.copy="Crear copia"
AdvSceneSwitcher.macroTab.expandAll="Expandir todo"
@@ -84,7 +83,6 @@ AdvSceneSwitcher.macroTab.highlightTrueConditions="Resaltar condiciones de la ma
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
; Lógica de macros
AdvSceneSwitcher.logic.none="Omitir entrada"
AdvSceneSwitcher.logic.and="Y"
AdvSceneSwitcher.logic.or="O"
AdvSceneSwitcher.logic.andNot="Y no"
@@ -118,9 +116,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.window="Ventana"
AdvSceneSwitcher.condition.file="Archivo"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="Contenido de{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medios"
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
@@ -176,7 +172,7 @@ AdvSceneSwitcher.condition.record.state.start="Grabación en ejecución"
AdvSceneSwitcher.condition.record.state.pause="Grabación en pausa"
AdvSceneSwitcher.condition.record.state.stop="Grabación detenida"
AdvSceneSwitcher.condition.process="Proceso"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}se está ejecutando{{focused}}y está enfocado"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}se está ejecutando{{focused}}y está enfocado"
AdvSceneSwitcher.condition.idle="Inactivo"
AdvSceneSwitcher.condition.idle.entry="No hay entradas de teclado o ratón durante {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado del complemento"
@@ -241,14 +237,14 @@ AdvSceneSwitcher.condition.replay.state.started="Búfer de reproducción iniciad
AdvSceneSwitcher.condition.replay.state.saved="Búfer de reproducción guardado"
AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="Fecha"
AdvSceneSwitcher.condition.date.anyDay="Cualquier día"
AdvSceneSwitcher.condition.date.monday="Lunes"
AdvSceneSwitcher.condition.date.tuesday="Martes"
AdvSceneSwitcher.condition.date.wednesday="Miércoles"
AdvSceneSwitcher.condition.date.thursday="Jueves"
AdvSceneSwitcher.condition.date.friday="Viernes"
AdvSceneSwitcher.condition.date.saturday="Sábado"
AdvSceneSwitcher.condition.date.sunday="Domingo"
AdvSceneSwitcher.day.any="Cualquier día"
AdvSceneSwitcher.day.monday="Lunes"
AdvSceneSwitcher.day.tuesday="Martes"
AdvSceneSwitcher.day.wednesday="Miércoles"
AdvSceneSwitcher.day.thursday="Jueves"
AdvSceneSwitcher.day.friday="Viernes"
AdvSceneSwitcher.day.saturday="Sábado"
AdvSceneSwitcher.day.sunday="Domingo"
AdvSceneSwitcher.condition.date.state.at="A las"
AdvSceneSwitcher.condition.date.state.after="Después"
AdvSceneSwitcher.condition.date.state.before="Antes"
@@ -258,11 +254,12 @@ AdvSceneSwitcher.condition.date.ignoreDate="Si no se marca, se ignorará el comp
AdvSceneSwitcher.condition.date.ignoreTime="Si no se marca, se ignorará el componente de tiempo"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Mostrar configuración avanzada"
AdvSceneSwitcher.condition.date.showSimpleSettings="Mostrar configuración simple"
AdvSceneSwitcher.condition.date.entry.simple="El {{dayOfWeek}} {{weekCondition}} {{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}} Repetir cada {{duration}} en la coincidencia de fechas"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Próxima coincidencia en: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} Al repetir actualizar la fecha seleccionada para repetir la fecha"
AdvSceneSwitcher.condition.date.layout.simple.day="El{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}} Repetir cada {{duration}} en la coincidencia de fechas"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="Próxima coincidencia en: %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}} Al repetir actualizar la fecha seleccionada para repetir la fecha"
AdvSceneSwitcher.condition.sceneTransform="Transformar elemento de escena"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Obtener transformación"
AdvSceneSwitcher.condition.sceneTransform.condition.match="coincide con la transformación"
@@ -334,7 +331,6 @@ AdvSceneSwitcher.action.recording.type.start="Iniciar grabación"
AdvSceneSwitcher.action.recording.type.pause="Pausar 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.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
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.type.stop="Detener el búfer de reproducción"
@@ -343,14 +339,13 @@ AdvSceneSwitcher.action.replay.type.save="Guardar búfer de reproducción"
AdvSceneSwitcher.action.streaming="Transmisión"
AdvSceneSwitcher.action.streaming.type.stop="Detener transmisión"
AdvSceneSwitcher.action.streaming.type.start="Iniciar transmisión"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ejecutar"
AdvSceneSwitcher.action.sceneVisibility="Visibilidad del elemento de escena"
AdvSceneSwitcher.action.sceneVisibility.type.show="Mostrar"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Ocultar"
AdvSceneSwitcher.action.sceneVisibility.type.source="Fuente"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Cualquiera"
AdvSceneSwitcher.action.sceneVisibility.entry="En{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="En{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="Filtro"
AdvSceneSwitcher.action.filter.type.enable="Habilitar"
AdvSceneSwitcher.action.filter.type.disable="Deshabilitar"
@@ -375,7 +370,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pausa"
AdvSceneSwitcher.action.macro.type.unpause="Reanudar"
AdvSceneSwitcher.action.macro.type.resetCounter="Reiniciar contador"
AdvSceneSwitcher.action.macro.type.run="Ejecutar"
AdvSceneSwitcher.action.macro.type.runActions="Ejecutar"
AdvSceneSwitcher.action.macro.type.stop="Detener"
AdvSceneSwitcher.action.pluginState="Estado del complemento"
AdvSceneSwitcher.action.pluginState.type.stop="Detener el complemento Advanced Scene Switcher"
@@ -423,9 +418,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.sourceShow="transición del programa de origen"
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
AdvSceneSwitcher.action.transition.entry.line1="Modificar {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{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.type="Modificar {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausa"
AdvSceneSwitcher.action.timer.type.continue="Continuar"
@@ -680,8 +675,6 @@ AdvSceneSwitcher.status.inactive="Inactivo"
AdvSceneSwitcher.running="Iniciar complemento"
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.unit.milliseconds="milisegundos"

View File

@@ -78,7 +78,6 @@ AdvSceneSwitcher.macroTab.add="Ajouter une nouvelle macro"
AdvSceneSwitcher.macroTab.name="Nom :"
AdvSceneSwitcher.macroTab.run="Exécuter la macro"
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.defaultGroupName="Groupe %1"
AdvSceneSwitcher.macroTab.removeSingleMacroPopup.text="Êtes-vous sûr de vouloir supprimer \"%1\" ?"
@@ -132,7 +131,6 @@ AdvSceneSwitcher.macroList.deleted="supprimé"
AdvSceneSwitcher.macroList.duplicate="\"%1\" est déjà sélectionné !"
; Macro Logic
AdvSceneSwitcher.logic.none="Ignorer l'entrée"
AdvSceneSwitcher.logic.and="Et"
AdvSceneSwitcher.logic.or="Ou"
AdvSceneSwitcher.logic.andNot="Et pas"
@@ -188,8 +186,6 @@ AdvSceneSwitcher.condition.file="Fichier"
AdvSceneSwitcher.condition.file.type.match="correspond à"
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
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.source="Source"
AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène"
@@ -282,8 +278,8 @@ AdvSceneSwitcher.condition.record.state.start="Enregistrement en cours"
AdvSceneSwitcher.condition.record.state.pause="Enregistrement en pause"
AdvSceneSwitcher.condition.record.state.stop="Arrêt de l'enregistrement"
AdvSceneSwitcher.condition.process="Processus"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.entry.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.layout.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inactif"
AdvSceneSwitcher.condition.idle.entry="Aucune entrée de clavier ou de souris pendant{{duration}}"
AdvSceneSwitcher.condition.pluginState="État du plugin"
@@ -356,14 +352,14 @@ AdvSceneSwitcher.condition.replay.state.stopped="Tampon de répétition arrêté
AdvSceneSwitcher.condition.replay.state.started="Tampon de répétition démarré"
AdvSceneSwitcher.condition.replay.state.saved="Tampon de répétition enregistré"
AdvSceneSwitcher.condition.date="Date"
AdvSceneSwitcher.condition.date.anyDay="N'importe quel jour"
AdvSceneSwitcher.condition.date.monday="Lundi"
AdvSceneSwitcher.condition.date.tuesday="Mardi"
AdvSceneSwitcher.condition.date.wednesday="Mercredi"
AdvSceneSwitcher.condition.date.thursday="Jeudi"
AdvSceneSwitcher.condition.date.friday="Vendredi"
AdvSceneSwitcher.condition.date.saturday="Samedi"
AdvSceneSwitcher.condition.date.sunday="Dimanche"
AdvSceneSwitcher.day.any="N'importe quel jour"
AdvSceneSwitcher.day.monday="Lundi"
AdvSceneSwitcher.day.tuesday="Mardi"
AdvSceneSwitcher.day.wednesday="Mercredi"
AdvSceneSwitcher.day.thursday="Jeudi"
AdvSceneSwitcher.day.friday="Vendredi"
AdvSceneSwitcher.day.saturday="Samedi"
AdvSceneSwitcher.day.sunday="Dimanche"
AdvSceneSwitcher.condition.date.state.at="À"
AdvSceneSwitcher.condition.date.state.after="Après"
AdvSceneSwitcher.condition.date.state.before="Avant"
@@ -374,11 +370,12 @@ AdvSceneSwitcher.condition.date.ignoreDate="Si non cochée, la composante date s
AdvSceneSwitcher.condition.date.ignoreTime="Si non cochée, la composante heure sera ignorée"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Afficher les paramètres avancés"
AdvSceneSwitcher.condition.date.showSimpleSettings="Afficher les paramètres simples"
AdvSceneSwitcher.condition.date.entry.simple="Le{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}}Répéter toutes les{{duration}}lorsque la date correspond"
AdvSceneSwitcher.condition.date.entry.pattern="La date actuelle \"{{currentDate}}\" correspond au motif{{pattern}}"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Prochaine correspondance à : %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}À la répétition, mettre à jour la date sélectionnée pour la date de répétition"
AdvSceneSwitcher.condition.date.layout.simple.day="Le{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}}Répéter toutes les{{duration}}lorsque la date correspond"
AdvSceneSwitcher.condition.date.layout.pattern="La date actuelle \"{{currentDate}}\" correspond au motif{{pattern}}"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="Prochaine correspondance à : %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}}À la répétition, mettre à jour la date sélectionnée pour la date de répétition"
AdvSceneSwitcher.condition.sceneTransform="Transformation de l'élément de la scène"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Obtenir la transformation"
AdvSceneSwitcher.condition.sceneTransform.condition.match="correspond à la transformation"
@@ -510,7 +507,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="Masquer"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="Basculer"
AdvSceneSwitcher.action.sceneVisibility.type.source="Source"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="N'importe"
AdvSceneSwitcher.action.sceneVisibility.entry="Sur{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="Sur{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="Filtre"
AdvSceneSwitcher.action.filter.type.enable="Activer"
AdvSceneSwitcher.action.filter.type.disable="Désactiver"
@@ -553,7 +550,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Reprendre"
AdvSceneSwitcher.action.macro.type.resetCounter="Réinitialiser le compteur"
AdvSceneSwitcher.action.macro.type.run="Exécuter les actions"
AdvSceneSwitcher.action.macro.type.runActions="Exécuter les actions"
AdvSceneSwitcher.action.macro.type.stop="Arrêter les actions"
AdvSceneSwitcher.action.macro.type.disableAction="Désactiver l'action"
AdvSceneSwitcher.action.macro.type.enableAction="Activer l'action"
@@ -622,9 +619,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.sourceShow="Transition d'affichage 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.entry.line2="{{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.type="Modifier{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Définir le type de transition sur{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
AdvSceneSwitcher.action.timer="Minuterie"
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
AdvSceneSwitcher.action.timer.type.continue="Continuer"
@@ -701,8 +698,8 @@ AdvSceneSwitcher.action.variable.invalidSelection="Sélection invalide !"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="La récupération de valeurs de variables à partir d'actions %1 n'est pas prise en charge !"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="La récupération de valeurs de variables à partir de conditions %1 n'est pas prise en charge !"
AdvSceneSwitcher.action.variable.currentSegmentValue="Valeur actuelle :"
AdvSceneSwitcher.action.variable.layout.substringIndex="Début de la sous-chaîne :{{subStringStart}}Taille de la sous-chaîne :{{subStringSize}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Attribuer la valeur du match{{regexMatchIdx}}en utilisant une expression régulière :"
AdvSceneSwitcher.action.variable.layout.substringIndex="Début de la sous-chaîne :{{subStringStart}}Taille de la sous-chaîne :{{subStringSize}}{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Attribuer la valeur du match{{regexMatchIdx}}en utilisant une expression régulière:{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.userInput.customPrompt="{{useCustomPrompt}}Utiliser un message personnalisé{{inputPrompt}}"
AdvSceneSwitcher.action.variable.layout.userInput.placeholder="{{useInputPlaceholder}}Remplir avec un indicateur de position{{inputPlaceholder}}"
AdvSceneSwitcher.action.projector="Projecteur"
@@ -1122,8 +1119,6 @@ AdvSceneSwitcher.status.inactive="Inactif"
AdvSceneSwitcher.running="Plugin en cours d'exécution"
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.unit.milliseconds="millisecondes"

View File

@@ -164,7 +164,6 @@ AdvSceneSwitcher.macroTab.run.tooltip="条件に関係なくすべてのマク
AdvSceneSwitcher.macroTab.runElse="マクロ実行else"
AdvSceneSwitcher.macroTab.runFail="\"%1\" の実行に失敗しました!\nいずれかのアクションが失敗したか、マクロがすでに実行されています。\n停止しますか"
AdvSceneSwitcher.macroTab.runInParallel="他のマクロと並行してマクロを実行する"
AdvSceneSwitcher.macroTab.onChange="条件変更時のみアクションを実行"
AdvSceneSwitcher.macroTab.defaultname="マクロ %1"
AdvSceneSwitcher.macroTab.defaultGroupName="グループ %1"
AdvSceneSwitcher.macroTab.macroNameExists="名前 \"%1\" は既にマクロで使用されています。"
@@ -276,7 +275,6 @@ AdvSceneSwitcher.macroList.deleted="削除"
AdvSceneSwitcher.macroList.duplicate="\"%1\" はすでに選択されています!"
# Macro Logic
AdvSceneSwitcher.logic.none="入力無視"
; AdvSceneSwitcher.logic.and="And"
; AdvSceneSwitcher.logic.or="Or"
; AdvSceneSwitcher.logic.andNot="And not"
@@ -332,14 +330,6 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}ウィンドウにテ
AdvSceneSwitcher.condition.window.entry.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。"
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
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.checkType.state="状態が一致"
AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
@@ -454,8 +444,8 @@ AdvSceneSwitcher.condition.stream.service.tooltip="現在のサービス名: %1"
AdvSceneSwitcher.condition.record.state.duration="録音時間が長くなっています。"
; AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="プロセス"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}が実行中{{focused}}に集中しています"
AdvSceneSwitcher.condition.process.entry.focus="現在のフォアグラウンドプロセス:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}が実行中{{focused}}に集中しています"
AdvSceneSwitcher.condition.process.layout.focus="現在のフォアグラウンドプロセス:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="アイドル"
AdvSceneSwitcher.condition.idle.entry="{{duration}}の間、キーボードまたはマウスの入力がありません"
AdvSceneSwitcher.condition.pluginState="プラグインの状態"
@@ -555,14 +545,14 @@ AdvSceneSwitcher.condition.replay.state.started="リプレイバッファを開
AdvSceneSwitcher.condition.replay.state.saved="リプレイバッファを保存"
; AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="日付条件"
AdvSceneSwitcher.condition.date.anyDay="いつでも"
AdvSceneSwitcher.condition.date.monday="月曜日"
AdvSceneSwitcher.condition.date.tuesday="火曜日"
AdvSceneSwitcher.condition.date.wednesday="水曜日"
AdvSceneSwitcher.condition.date.thursday="木曜日"
AdvSceneSwitcher.condition.date.friday="金曜日"
AdvSceneSwitcher.condition.date.saturday="土曜日"
AdvSceneSwitcher.condition.date.sunday="日曜日"
AdvSceneSwitcher.day.any="いつでも"
AdvSceneSwitcher.day.monday="月曜日"
AdvSceneSwitcher.day.tuesday="火曜日"
AdvSceneSwitcher.day.wednesday="水曜日"
AdvSceneSwitcher.day.thursday="木曜日"
AdvSceneSwitcher.day.friday="金曜日"
AdvSceneSwitcher.day.saturday="土曜日"
AdvSceneSwitcher.day.sunday="日曜日"
; AdvSceneSwitcher.condition.date.state.at="At"
AdvSceneSwitcher.condition.date.state.after="アフター"
AdvSceneSwitcher.condition.date.state.before="前"
@@ -573,12 +563,9 @@ AdvSceneSwitcher.condition.date.ignoreDate="チェックを外すと日付部分
AdvSceneSwitcher.condition.date.ignoreTime="チェックを外すと時間要素は無視されます"
AdvSceneSwitcher.condition.date.showAdvancedSettings="詳細設定を表示"
AdvSceneSwitcher.condition.date.showSimpleSettings="簡単設定を表示"
; AdvSceneSwitcher.condition.date.entry.simple="On{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
; AdvSceneSwitcher.condition.date.entry.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}}日付の一致ごとに{{duration}}繰り返します"
AdvSceneSwitcher.condition.date.entry.pattern="現在の日付「{{currentDate}}」はパターン{{pattern}}と一致します"
; AdvSceneSwitcher.condition.date.entry.nextMatchDate="Next match at: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}繰り返しの場合、選択した日付を繰り返しの日付に更新します"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}}日付の一致ごとに{{duration}}繰り返します"
AdvSceneSwitcher.condition.date.layout.pattern="現在の日付「{{currentDate}}」はパターン{{pattern}}と一致します"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}}繰り返しの場合、選択した日付を繰り返しの日付に更新します"
; AdvSceneSwitcher.condition.sceneTransform="Scene item transform"
; AdvSceneSwitcher.condition.sceneTransform.getTransform="Get transform"
AdvSceneSwitcher.condition.sceneTransform.getCurrentValue="現在の値を取得"
@@ -870,7 +857,6 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="非表示"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="切り替え"
; AdvSceneSwitcher.action.sceneVisibility.type.source="Source"
; AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Any"
; AdvSceneSwitcher.action.sceneVisibility.entry="On{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="フィルタ"
AdvSceneSwitcher.action.filter.type.enable="有効にする"
AdvSceneSwitcher.action.filter.type.disable="無効化"
@@ -936,7 +922,7 @@ AdvSceneSwitcher.action.macro.type.pause="一時停止"
AdvSceneSwitcher.action.macro.type.unpause="一時停止解除"
AdvSceneSwitcher.action.macro.type.togglePause="一時停止を切り替え"
AdvSceneSwitcher.action.macro.type.resetCounter="カウンターリセット"
AdvSceneSwitcher.action.macro.type.run="マクロを実行"
AdvSceneSwitcher.action.macro.type.runActions="マクロを実行"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="条件の状態を考慮しない"
AdvSceneSwitcher.action.macro.type.run.conditions.true="条件が true と評価された場合のみ"
AdvSceneSwitcher.action.macro.type.run.conditions.false="条件が false と評価された場合のみ"
@@ -1025,9 +1011,9 @@ AdvSceneSwitcher.action.transition="トランジション"
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
; AdvSceneSwitcher.action.transition.entry.line1="{{type}}{{scenes}}{{sources}} の変更"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}移行時間を {{duration}} 秒に設定します"
; AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}} の変更"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}移行時間を {{duration}} 秒に設定します"
AdvSceneSwitcher.action.timer="タイマー"
AdvSceneSwitcher.action.timer.type.pause="一時停止"
AdvSceneSwitcher.action.timer.type.continue="続行"
@@ -1149,8 +1135,8 @@ AdvSceneSwitcher.action.variable.currentSegmentValue="現在値:"
; AdvSceneSwitcher.action.variable.layout.other="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}{{tempVarsHelp}}{{sceneItemIndex}}{{direction}}{{stringLength}}{{paddingCharSelection}}"
AdvSceneSwitcher.action.variable.layout.pad="{{actions}}の{{variables}}の長さを{{stringLength}}にするには、{{paddingCharSelection}}を{{direction}}に追加します。"
AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}の{{variables}}の長さを{{stringLength}}にするには、{{direction}}から文字を削除します。"
AdvSceneSwitcher.action.variable.layout.substringIndex="部分文字列の開始:{{subStringStart}}部分文字列のサイズ:{{subStringSize}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="正規表現を使用して{{regexMatchIdx}}一致の値を割り当てます:"
AdvSceneSwitcher.action.variable.layout.substringIndex="部分文字列の開始:{{subStringStart}}部分文字列のサイズ:{{subStringSize}}{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="正規表現を使用して{{regexMatchIdx}}一致の値を割り当てます:{{subStringRegex}}"
; AdvSceneSwitcher.action.variable.layout.findAndReplace="{{findStr}}{{findRegex}}{{replaceStr}}"
AdvSceneSwitcher.action.variable.layout.userInput.customPrompt="{{useCustomPrompt}}カスタム プロンプトを使用する{{inputPrompt}}"
AdvSceneSwitcher.action.variable.layout.userInput.placeholder="{{useInputPlaceholder}}プレースホルダーを入力{{inputPlaceholder}}"
@@ -1195,8 +1181,15 @@ AdvSceneSwitcher.action.twitch.reward.toggleControl="リワード名/変数選
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません"
; 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.user.getInfo="{{userInfoQueryType}}{{userLogin}}{{userId}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="チャンネル{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="チャンネル{{channel}}でアカウント{{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.marker.description="マーカーの説明"
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"
@@ -2256,8 +2249,6 @@ AdvSceneSwitcher.status.inactive="停止中"
AdvSceneSwitcher.running="プラグイン実行中"
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.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.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.onChange="Executar ações apenas quando houver mudança na condição"
AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.defaultGroupName="Grupo %1"
AdvSceneSwitcher.macroTab.macroNameExists="O nome \"%1\" já está em uso por uma macro."
@@ -240,7 +239,6 @@ AdvSceneSwitcher.macroList.deleted="excluída"
AdvSceneSwitcher.macroList.duplicate="\"%1\" já está selecionada!"
; Macro Logic
AdvSceneSwitcher.logic.none="Ignorar entrada"
AdvSceneSwitcher.logic.and="E"
AdvSceneSwitcher.logic.or="Ou"
AdvSceneSwitcher.logic.andNot="E não"
@@ -296,11 +294,6 @@ AdvSceneSwitcher.condition.file="Arquivo"
AdvSceneSwitcher.condition.file.type.match="corresponde"
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo 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.checkType.state="Estado corresponde"
AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo corresponde"
@@ -401,8 +394,8 @@ AdvSceneSwitcher.condition.record.state.stop="Gravação parada"
AdvSceneSwitcher.condition.record.state.duration="Duração da gravação é maior que"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Processo"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}está em execução{{focused}}e está em foco"
AdvSceneSwitcher.condition.process.entry.focus="Processo atual em primeiro plano:{{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}está em execução{{focused}}e está em foco"
AdvSceneSwitcher.condition.process.layout.focus="Processo atual em primeiro plano:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inativo"
AdvSceneSwitcher.condition.idle.entry="Sem entradas de teclado ou mouse por {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado do plugin"
@@ -502,14 +495,14 @@ AdvSceneSwitcher.condition.replay.state.started="Buffer de replay iniciado"
AdvSceneSwitcher.condition.replay.state.saved="Buffer de replay salvo"
AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="Data"
AdvSceneSwitcher.condition.date.anyDay="Qualquer dia"
AdvSceneSwitcher.condition.date.monday="Segunda-feira"
AdvSceneSwitcher.condition.date.tuesday="Terça-feira"
AdvSceneSwitcher.condition.date.wednesday="Quarta-feira"
AdvSceneSwitcher.condition.date.thursday="Quinta-feira"
AdvSceneSwitcher.condition.date.friday="Sexta-feira"
AdvSceneSwitcher.condition.date.saturday="Sábado"
AdvSceneSwitcher.condition.date.sunday="Domingo"
AdvSceneSwitcher.day.any="Qualquer dia"
AdvSceneSwitcher.day.monday="Segunda-feira"
AdvSceneSwitcher.day.tuesday="Terça-feira"
AdvSceneSwitcher.day.wednesday="Quarta-feira"
AdvSceneSwitcher.day.thursday="Quinta-feira"
AdvSceneSwitcher.day.friday="Sexta-feira"
AdvSceneSwitcher.day.saturday="Sábado"
AdvSceneSwitcher.day.sunday="Domingo"
AdvSceneSwitcher.condition.date.state.at="Às"
AdvSceneSwitcher.condition.date.state.after="Após"
AdvSceneSwitcher.condition.date.state.before="Antes"
@@ -520,12 +513,13 @@ AdvSceneSwitcher.condition.date.ignoreDate="Se desmarcado, o componente de data
AdvSceneSwitcher.condition.date.ignoreTime="Se desmarcado, o componente de tempo será ignorado"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Mostrar configurações avançadas"
AdvSceneSwitcher.condition.date.showSimpleSettings="Mostrar configurações simples"
AdvSceneSwitcher.condition.date.entry.simple="Em{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}}Repetir a cada{{duration}}em correspondência de data"
AdvSceneSwitcher.condition.date.entry.pattern="Data atual \"{{currentDate}}\" corresponde ao padrão{{pattern}}"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Próxima correspondência em: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}Ao repetir, atualize a data selecionada para a data de repetição"
AdvSceneSwitcher.condition.date.layout.simple.day="Em{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}}Repetir a cada{{duration}}em correspondência de data"
AdvSceneSwitcher.condition.date.layout.pattern="Data atual \"{{currentDate}}\" corresponde ao padrão{{pattern}}"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="Próxima correspondência em: %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}}Ao repetir, atualize a data selecionada para a data de repetição"
AdvSceneSwitcher.condition.sceneTransform="Transformação do item de cena"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Obter transformação"
AdvSceneSwitcher.condition.sceneTransform.getCurrentValue="Obter valor atual"
@@ -754,7 +748,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.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.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
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.durationWarn="Aviso: Alterar o tempo máximo de replay só se aplicará na próxima vez que o buffer de replay for iniciado!"
@@ -771,7 +764,6 @@ AdvSceneSwitcher.action.streaming.type.server="Definir URL do servidor"
AdvSceneSwitcher.action.streaming.type.streamKey="Definir chave de transmissão"
AdvSceneSwitcher.action.streaming.type.username="Definir nome de usuário"
AdvSceneSwitcher.action.streaming.type.password="Definir senha"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Executar"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}Aguardar a saída do processo ou no máximo {{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="Observe que as propriedades da macro não funcionarão se você deixar isso desmarcado, pois o processo é iniciado de forma independente do restante da lógica e não há controle sobre ele."
@@ -781,7 +773,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="Ocultar"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="Alternar"
AdvSceneSwitcher.action.sceneVisibility.type.source="Fonte"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Qualquer"
AdvSceneSwitcher.action.sceneVisibility.entry="Em{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="Em{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="Filtro"
AdvSceneSwitcher.action.filter.type.enable="Habilitar"
AdvSceneSwitcher.action.filter.type.disable="Desabilitar"
@@ -846,7 +838,7 @@ AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pausar"
AdvSceneSwitcher.action.macro.type.unpause="Retomar"
AdvSceneSwitcher.action.macro.type.resetCounter="Reiniciar contador"
AdvSceneSwitcher.action.macro.type.run="Executar macro"
AdvSceneSwitcher.action.macro.type.runActions="Executar macro"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="Não considerar o estado da condição"
AdvSceneSwitcher.action.macro.type.run.conditions.true="Apenas se as condições forem verdadeiras"
AdvSceneSwitcher.action.macro.type.run.conditions.false="Apenas se as condições forem falsas"
@@ -933,9 +925,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.sourceShow="transição de exibiçã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.entry.line2="{{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.type="Modificar{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Definir tipo de transição para{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Definir duração da transição para{{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausar"
AdvSceneSwitcher.action.timer.type.continue="Continuar"
@@ -1030,8 +1022,8 @@ AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Obter valores de va
AdvSceneSwitcher.action.variable.currentSegmentValue="Valor atual:"
AdvSceneSwitcher.action.variable.layout.pad="{{actions}}de{{variables}}para comprimento{{stringLength}}adicionando{{paddingCharSelection}}ao{{direction}}"
AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}de{{variables}}para comprimento{{stringLength}}removendo caracteres da{{direction}}"
AdvSceneSwitcher.action.variable.layout.substringIndex="Início da substring:{{subStringStart}}Tamanho da substring:{{subStringSize}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Atribuir valor do{{regexMatchIdx}}correspondência usando expressão regular:"
AdvSceneSwitcher.action.variable.layout.substringIndex="Início da substring:{{subStringStart}}Tamanho da substring:{{subStringSize}}{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="Atribuir valor do{{regexMatchIdx}}correspondência usando expressão regular:{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.findAndReplace="{{findStr}}{{findRegex}}{{replaceStr}}"
AdvSceneSwitcher.action.variable.layout.userInput.customPrompt="{{useCustomPrompt}}Usar prompt personalizado{{inputPrompt}}"
AdvSceneSwitcher.action.variable.layout.userInput.placeholder="{{useInputPlaceholder}}Preencher com placeholder{{inputPlaceholder}}"
@@ -1065,7 +1057,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.sendMessage="Enviar mensagem de chat"
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.title.title="Digite o título"
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"
@@ -1871,8 +1863,6 @@ AdvSceneSwitcher.status.inactive="Inativo"
AdvSceneSwitcher.running="Plugin em execução"
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.unit.milliseconds="milissegundos"

View File

@@ -70,7 +70,6 @@ AdvSceneSwitcher.macroTab.name="Имя:"
AdvSceneSwitcher.macroTab.defaultname="Макрос %1"
AdvSceneSwitcher.macroTab.copy="Создать копию"
; Macro Logic
AdvSceneSwitcher.logic.none="Игнорировать вход"
AdvSceneSwitcher.logic.and="И"
AdvSceneSwitcher.logic.or="Или"
AdvSceneSwitcher.logic.andNot="И не"
@@ -85,8 +84,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
AdvSceneSwitcher.condition.window="Окно"
AdvSceneSwitcher.condition.file="Файл"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Медиа"
AdvSceneSwitcher.condition.video="Видео"
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
@@ -105,7 +102,7 @@ AdvSceneSwitcher.condition.record.state.start="Запись запущена"
AdvSceneSwitcher.condition.record.state.pause="Запись приостановлена"
AdvSceneSwitcher.condition.record.state.stop="Запись остановлена"
AdvSceneSwitcher.condition.process="Процесс"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.idle="Простой"
AdvSceneSwitcher.condition.idle.entry="Не было ни клавиатуры, ни мыши в течении{{duration}}"
AdvSceneSwitcher.condition.pluginState="Состояние плагина"
@@ -130,7 +127,6 @@ AdvSceneSwitcher.action.recording.type.start="Начать запись"
AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера"
@@ -138,7 +134,6 @@ AdvSceneSwitcher.action.replay.type.save="Сохранить буфер восп
AdvSceneSwitcher.action.streaming="Потоковое вещание"
AdvSceneSwitcher.action.streaming.type.stop="Остановить потоковое вещание"
AdvSceneSwitcher.action.streaming.type.start="Начать потоковое вещание"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Запустить"

View File

@@ -72,14 +72,12 @@ AdvSceneSwitcher.macroTab.add="Yeni Makro ekle"
AdvSceneSwitcher.macroTab.name="İsim:"
AdvSceneSwitcher.macroTab.run="Makro Çalıştırma"
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.copy="Kopya oluştur"
AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet"
AdvSceneSwitcher.macroTab.collapseAll="Hepsini Küçült"
; Macro Logic
AdvSceneSwitcher.logic.none="Girişi yoksay"
AdvSceneSwitcher.logic.and="Ve"
AdvSceneSwitcher.logic.or="Ya da"
AdvSceneSwitcher.logic.andNot="ve değil"
@@ -108,9 +106,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.window="Pencere"
AdvSceneSwitcher.condition.file="Dosya"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="İçerik{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medya"
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
@@ -153,7 +149,7 @@ AdvSceneSwitcher.condition.record.state.start="Kayıt Çalışıyor"
AdvSceneSwitcher.condition.record.state.pause="Kayıt durakladı"
AdvSceneSwitcher.condition.record.state.stop="Kayıt durdu"
AdvSceneSwitcher.condition.process="İşlem"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}çalışıyor{{focused}}ve odaklandı"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}çalışıyor{{focused}}ve odaklandı"
AdvSceneSwitcher.condition.idle="Boşta"
AdvSceneSwitcher.condition.idle.entry="...için klavye veya fare girişi yok {{duration}}"
AdvSceneSwitcher.condition.pluginState="Eklenti durumu"
@@ -266,7 +262,6 @@ AdvSceneSwitcher.action.recording.type.start="Kayıt Başlat"
AdvSceneSwitcher.action.recording.type.pause="Kayıt Duraklat"
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.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Tekrar arabelleği"
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat"
@@ -274,14 +269,13 @@ AdvSceneSwitcher.action.replay.type.save="Tekrar arabelleğini kaydet"
AdvSceneSwitcher.action.streaming="Yayın"
AdvSceneSwitcher.action.streaming.type.stop="Yayın durdur"
AdvSceneSwitcher.action.streaming.type.start="Yayın başlat"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Çalıştır"
AdvSceneSwitcher.action.sceneVisibility="Sahne öğesi görünürlüğü"
AdvSceneSwitcher.action.sceneVisibility.type.show="Göster"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Gizle"
AdvSceneSwitcher.action.sceneVisibility.type.source="Kayıt"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Herhangi"
AdvSceneSwitcher.action.sceneVisibility.entry="Açık{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="Açık{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="Filtrele"
AdvSceneSwitcher.action.filter.type.enable="Etkin"
AdvSceneSwitcher.action.filter.type.disable="Etkisiz"
@@ -305,7 +299,7 @@ AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Duraklat"
AdvSceneSwitcher.action.macro.type.unpause="Duraklatma"
AdvSceneSwitcher.action.macro.type.resetCounter="Sayacı sıfırla"
AdvSceneSwitcher.action.macro.type.run="Çalıştır"
AdvSceneSwitcher.action.macro.type.runActions="Çalıştır"
AdvSceneSwitcher.action.pluginState="Eklenti durumu"
AdvSceneSwitcher.action.pluginState.type.stop="Advanced Scene Switcher eklentisini durdurun"
AdvSceneSwitcher.action.pluginState.type.noMatch="Eşleşmeme davranışını değiştirin:"
@@ -342,8 +336,8 @@ AdvSceneSwitcher.action.file.type.write="Yaz"
AdvSceneSwitcher.action.file.type.append="Ekle"
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
AdvSceneSwitcher.action.transition="Geçiş"
AdvSceneSwitcher.action.transition.entry.line2="{{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.transition="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
AdvSceneSwitcher.action.timer="Zamanlayıcı"
AdvSceneSwitcher.action.timer.type.pause="Duraklat"
AdvSceneSwitcher.action.timer.type.continue="Devam et"
@@ -587,8 +581,6 @@ AdvSceneSwitcher.status.inactive="İnaktif"
AdvSceneSwitcher.running="Eklenti çalışıyor"
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.seconds="saniye"
AdvSceneSwitcher.unit.minutes="dakika"

View File

@@ -151,7 +151,6 @@ AdvSceneSwitcher.macroTab.run.tooltip="无论条件如何,都运行所有宏
AdvSceneSwitcher.macroTab.runElse="运行宏(不满足条件)"
AdvSceneSwitcher.macroTab.runFail="运行 \"%1\" 失败!\n要么其中一个操作失败要么宏已在运行中.\n你想停止它吗?"
AdvSceneSwitcher.macroTab.runInParallel="与其他宏并行运行宏"
AdvSceneSwitcher.macroTab.onChange="仅在条件结果发生变化时执行操作(条件结果不变时只执行一次操作)"
AdvSceneSwitcher.macroTab.defaultname="宏 %1"
AdvSceneSwitcher.macroTab.defaultGroupName="分组 %1"
AdvSceneSwitcher.macroTab.macroNameExists="名称 \"%1\" 已被宏使用."
@@ -260,7 +259,6 @@ AdvSceneSwitcher.macroList.deleted="删除"
AdvSceneSwitcher.macroList.duplicate="\"%1\" 已选择!"
; Macro Logic
AdvSceneSwitcher.logic.none="忽略条件"
AdvSceneSwitcher.logic.and="且"
AdvSceneSwitcher.logic.or="或"
AdvSceneSwitcher.logic.andNot="且不"
@@ -319,11 +317,7 @@ AdvSceneSwitcher.condition.file="文件"
AdvSceneSwitcher.condition.file.type.match="内容匹配"
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.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="媒体"
AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
@@ -429,8 +423,8 @@ AdvSceneSwitcher.condition.record.state.stop="录制停止"
AdvSceneSwitcher.condition.record.state.duration="录制时间长于"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="进程"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点"
AdvSceneSwitcher.condition.process.entry.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.process.layout="{{processes}}{{regex}}为正在运行中{{focused}}且为焦点"
AdvSceneSwitcher.condition.process.layout.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="闲置检测"
AdvSceneSwitcher.condition.idle.entry="{{duration}}内没有键盘或鼠标输入"
AdvSceneSwitcher.condition.pluginState="插件状态"
@@ -530,14 +524,14 @@ AdvSceneSwitcher.condition.replay.state.started="回放缓存已启动"
AdvSceneSwitcher.condition.replay.state.saved="已保存回放缓存"
AdvSceneSwitcher.condition.replay.entry="{{state}}"
AdvSceneSwitcher.condition.date="日期"
AdvSceneSwitcher.condition.date.anyDay="每一天"
AdvSceneSwitcher.condition.date.monday="周一"
AdvSceneSwitcher.condition.date.tuesday="周二"
AdvSceneSwitcher.condition.date.wednesday="周三"
AdvSceneSwitcher.condition.date.thursday="周四"
AdvSceneSwitcher.condition.date.friday="周五"
AdvSceneSwitcher.condition.date.saturday="周六"
AdvSceneSwitcher.condition.date.sunday="周日"
AdvSceneSwitcher.day.any="每一天"
AdvSceneSwitcher.day.monday="周一"
AdvSceneSwitcher.day.tuesday="周二"
AdvSceneSwitcher.day.wednesday="周三"
AdvSceneSwitcher.day.thursday="周四"
AdvSceneSwitcher.day.friday="周五"
AdvSceneSwitcher.day.saturday="周六"
AdvSceneSwitcher.day.sunday="周日"
AdvSceneSwitcher.condition.date.state.at="现在"
AdvSceneSwitcher.condition.date.state.after="之后"
AdvSceneSwitcher.condition.date.state.before="之前"
@@ -548,12 +542,13 @@ AdvSceneSwitcher.condition.date.ignoreDate="如果未选中,日期组件将被
AdvSceneSwitcher.condition.date.ignoreTime="如果未选中,时间组件将被忽略"
AdvSceneSwitcher.condition.date.showAdvancedSettings="显示高级设置"
AdvSceneSwitcher.condition.date.showSimpleSettings="显示简单设置"
AdvSceneSwitcher.condition.date.entry.simple="{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}} 每 {{duration}} 重复一次时间/日期匹配"
AdvSceneSwitcher.condition.date.entry.pattern="当前日期 \"{{currentDate}}\" 与 {{pattern}} 模式匹配 , 例子:2025 03 18 24 00 00 = .... .. .. .. .. .. ,难崩,我测试了一段时间才明白"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="下一次匹配 在: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} 在重复时将选定日期更新为重复日期"
AdvSceneSwitcher.condition.date.layout.simple.day="{{dayOfWeek}}"
AdvSceneSwitcher.condition.date.layout.simple.time="{{ignoreWeekTime}}{{weekCondition}}{{weekTime}}"
AdvSceneSwitcher.condition.date.layout.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.layout.repeat="{{repeat}} 每 {{duration}} 重复一次时间/日期匹配"
AdvSceneSwitcher.condition.date.layout.pattern="当前日期 \"{{currentDate}}\" 与 {{pattern}} 模式匹配 , 例子:2025 03 18 24 00 00 = .... .. .. .. .. .. ,难崩,我测试了一段时间才明白"
AdvSceneSwitcher.condition.date.layout.nextMatchDate="下一次匹配 在: %1"
AdvSceneSwitcher.condition.date.layout.updateOnRepeat="{{updateOnRepeat}} 在重复时将选定日期更新为重复日期"
AdvSceneSwitcher.condition.sceneTransform="场景项目变换"
AdvSceneSwitcher.condition.sceneTransform.getTransform="获取变换"
AdvSceneSwitcher.condition.sceneTransform.getCurrentValue="获取当前值"
@@ -813,7 +808,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="更改输出文件夹
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
AdvSceneSwitcher.action.recording.split.hint="注意请先确保在OBS设置中启用自动分割文件!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="回放缓存"
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
@@ -830,7 +824,6 @@ AdvSceneSwitcher.action.streaming.type.server="设置服务器"
AdvSceneSwitcher.action.streaming.type.streamKey="设置推流码"
AdvSceneSwitcher.action.streaming.type.username="设置用户名"
AdvSceneSwitcher.action.streaming.type.password="设置密码"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="运行"
AdvSceneSwitcher.action.run.wait.entry="{{wait}}进程退出或最多等待{{timeout}}{{waitHelp}}"
AdvSceneSwitcher.action.run.wait.help.tooltip="请注意,如果不勾选此选项,宏属性将不起作用,因为进程的启动会脱离逻辑的其他部分,因此无法对其进行控制."
@@ -840,7 +833,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.hide="隐藏"
AdvSceneSwitcher.action.sceneVisibility.type.toggle="切换可见性"
AdvSceneSwitcher.action.sceneVisibility.type.source="源"
AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="任何"
AdvSceneSwitcher.action.sceneVisibility.entry="{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.sceneVisibility.layout="{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.filter="滤镜"
AdvSceneSwitcher.action.filter.type.enable="开启"
AdvSceneSwitcher.action.filter.type.disable="关闭"
@@ -905,7 +898,7 @@ AdvSceneSwitcher.action.macro="宏"
AdvSceneSwitcher.action.macro.type.pause="停用"
AdvSceneSwitcher.action.macro.type.unpause="启用"
AdvSceneSwitcher.action.macro.type.resetCounter="重置计数器"
AdvSceneSwitcher.action.macro.type.run="运行宏"
AdvSceneSwitcher.action.macro.type.runActions="运行宏"
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="忽略条件结果"
AdvSceneSwitcher.action.macro.type.run.conditions.true="仅当条件结果为真时"
AdvSceneSwitcher.action.macro.type.run.conditions.false="仅当条件结果为假时"
@@ -994,9 +987,9 @@ AdvSceneSwitcher.action.transition.type.scene="场景转场动画"
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
AdvSceneSwitcher.action.transition.entry.line1="更改 {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}将转场动画类型设置为 {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
AdvSceneSwitcher.action.transition.layout.type="更改 {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}将转场动画类型设置为 {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
AdvSceneSwitcher.action.timer="计时器"
AdvSceneSwitcher.action.timer.type.pause="暂停"
AdvSceneSwitcher.action.timer.type.continue="继续"
@@ -1100,8 +1093,8 @@ AdvSceneSwitcher.action.variable.conditionNoVariableSupport="不支持从 %1 条
AdvSceneSwitcher.action.variable.currentSegmentValue="当前值:"
AdvSceneSwitcher.action.variable.layout.pad="{{actions}}于{{variables}}的{{direction}}起,内容为{{paddingCharSelection}}将其变量值长度增加至{{stringLength}}."
AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}于{{variables}}的{{direction}}起,将变量长度缩减至{{stringLength}}"
AdvSceneSwitcher.action.variable.layout.substringIndex="截取字符串开始位置:{{subStringStart}} 截取字符串长度:{{subStringSize}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="使用正则表达式为 {{regexMatchIdx}} 匹配的值:"
AdvSceneSwitcher.action.variable.layout.substringIndex="截取字符串开始位置:{{subStringStart}} 截取字符串长度:{{subStringSize}}{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.substringRegex="使用正则表达式为 {{regexMatchIdx}} 匹配的值:{{subStringRegex}}"
AdvSceneSwitcher.action.variable.layout.findAndReplace="{{findStr}}{{findRegex}}{{replaceStr}}"
AdvSceneSwitcher.action.variable.layout.userInput.customPrompt="{{useCustomPrompt}}使用自定义提示{{inputPrompt}}"
AdvSceneSwitcher.action.variable.layout.userInput.placeholder="{{useInputPlaceholder}}用占位符填充{{inputPlaceholder}}"
@@ -1139,10 +1132,17 @@ AdvSceneSwitcher.action.twitch.type.user.getInfo="获取用户信息"
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
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.user.getInfo="使用账户{{account}}{{actions}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="使用账户{{account}}{{actions}} 频道{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="使用账户{{account}}{{actions}}频道{{channel}}"
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.marker.description="标记描述"
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"
@@ -2115,8 +2115,6 @@ AdvSceneSwitcher.status.inactive="已停止"
AdvSceneSwitcher.running="插件正在运行"
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.unit.milliseconds="毫秒"

View File

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

After

Width:  |  Height:  |  Size: 973 B

View File

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

After

Width:  |  Height:  |  Size: 973 B

2
deps/leptonica vendored

2
deps/libusb vendored

View File

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

View File

@@ -1,5 +1,6 @@
#include "advanced-scene-switcher.hpp"
#include "backup.hpp"
#include "crash-handler.hpp"
#include "log-helper.hpp"
#include "macro-helpers.hpp"
#include "obs-module-helper.hpp"
@@ -18,8 +19,10 @@
#include <obs-frontend-api.h>
#include <QAction>
#include <QDirIterator>
#include <QInputDialog>
#include <QLibrary>
#include <QMainWindow>
#include <QMessageBox>
#include <QTextStream>
#include <regex>
@@ -40,9 +43,12 @@ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
{
switcher->settingsWindowOpened = true;
ui->setupUi(this);
std::lock_guard<std::mutex> lock(switcher->m);
switcher->Prune();
LoadUI();
{
std::lock_guard<std::mutex> lock(switcher->m);
switcher->Prune();
LoadUI();
}
CheckFirstTimeSetup();
}
AdvSceneSwitcher::~AdvSceneSwitcher()
@@ -138,7 +144,6 @@ void AdvSceneSwitcher::LoadUI()
SetTabOrder(ui->tabWidget);
SetCurrentTab(ui->tabWidget);
RestoreWindowGeo();
CheckFirstTimeSetup();
loading = false;
}
@@ -191,25 +196,21 @@ static void SaveSceneSwitcher(obs_data_t *save_data, bool saving, void *)
switcher->m.lock();
if (switcher->VersionChanged(data, g_GIT_SHA1)) {
auto json = obs_data_get_json(data);
static QString jsonQString = json ? json : "";
std::thread t([]() {
obs_queue_task(
OBS_TASK_UI,
[](void *) {
AskForBackup(jsonQString);
},
nullptr, false);
});
t.detach();
AskForBackup(data);
}
switcher->LoadSettings(data);
switcher->m.unlock();
if (!switcher->stop) {
switcher->Start();
if (switcher->stop) {
return;
}
if (ShouldSkipPluginStartOnUncleanShutdown()) {
return;
}
switcher->Start();
}
}
@@ -325,8 +326,7 @@ void SwitcherData::SetPreconditions()
{
// Window title
lastTitle = currentTitle;
std::string title;
GetCurrentWindowTitle(title);
auto title = GetCurrentWindowTitle();
for (auto &window : ignoreWindowsSwitches) {
bool equals = (title == window);
bool matches = false;
@@ -346,7 +346,7 @@ void SwitcherData::SetPreconditions()
currentTitle = title;
// Process name
GetForegroundProcessName(currentForegroundProcess);
currentForegroundProcess = GetForegroundProcessName();
// Macro
InvalidateMacroTempVarValues();
@@ -510,6 +510,8 @@ bool SwitcherData::AnySceneTransitionStarted()
******************************************************************************/
extern "C" EXPORT void FreeSceneSwitcher()
{
switcher->Stop();
PlatformCleanup();
RunPluginCleanupSteps();
@@ -631,13 +633,20 @@ static void handleSceneCollectionCleanup()
return;
}
// OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP is also called on
// shutdown.
// Here we also don't want to clear the settings.
if (switcher->obsIsShuttingDown) {
return;
}
SaveSceneSwitcher(nullptr, false, nullptr);
}
// Note to future self:
// be careful using switcher->m here as there is potential for deadlocks when using
// frontend functions such as obs_frontend_set_current_scene()
static void OBSEvent(enum obs_frontend_event event, void *switcher)
static void OBSEvent(enum obs_frontend_event event, void *)
{
if (!switcher) {
return;
@@ -711,19 +720,54 @@ static void LoadPlugins()
}
}
static bool settingsPasswordIsCorrect(QWidget *parent)
{
bool ok;
QString input = QInputDialog::getText(
parent,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel"),
QLineEdit::Password, QString(), &ok);
if (!ok) {
return false;
}
if (input.toStdString() == switcher->settingsLockPassword) {
return true;
}
QMessageBox::warning(
parent,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword"));
return false;
}
void OpenSettingsWindow()
{
if (switcher->settingsWindowOpened) {
AdvSceneSwitcher::window->show();
AdvSceneSwitcher::window->raise();
AdvSceneSwitcher::window->activateWindow();
} else {
AdvSceneSwitcher::window =
new AdvSceneSwitcher(static_cast<QMainWindow *>(
obs_frontend_get_main_window()));
AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
AdvSceneSwitcher::window->show();
return;
}
auto parent =
static_cast<QMainWindow *>(obs_frontend_get_main_window());
if (switcher->settingsLockEnabled &&
!switcher->settingsLockPassword.empty() &&
!settingsPasswordIsCorrect(parent)) {
return;
}
AdvSceneSwitcher::window = new AdvSceneSwitcher(parent);
AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
AdvSceneSwitcher::window->show();
}
void AdvSceneSwitcher::HighlightMacroSettingsButton(bool enable)
@@ -771,8 +815,6 @@ void HighlightMacroSettingsButton(bool enable)
enable);
}
void SetupActionQueues();
extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
translateFunc translate)
{
@@ -784,12 +826,11 @@ extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
PlatformInit();
LoadPlugins();
SetupDock();
SetupActionQueues();
RunPluginInitSteps();
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
obs_frontend_add_event_callback(OBSEvent, switcher);
obs_frontend_add_event_callback(OBSEvent, nullptr);
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
obs_module_text("AdvSceneSwitcher.pluginName"));

View File

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

View File

@@ -1,24 +1,26 @@
#include "advanced-scene-switcher.hpp"
#include "crash-handler.hpp"
#include "file-selection.hpp"
#include "filter-combo-box.hpp"
#include "first-run-wizard.hpp"
#include "layout-helpers.hpp"
#include "macro.hpp"
#include "macro-search.hpp"
#include "macro-settings.hpp"
#include "path-helpers.hpp"
#include "selection-helpers.hpp"
#include "source-helpers.hpp"
#include "splitter-helpers.hpp"
#include "status-control.hpp"
#include "switcher-data.hpp"
#include "tab-helpers.hpp"
#include "ui-helpers.hpp"
#include "utility.hpp"
#include "variable.hpp"
#include "version.h"
#include <obs-frontend-api.h>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
namespace advss {
@@ -195,6 +197,15 @@ void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
switcher->warnPluginLoadFailure = state;
}
void AdvSceneSwitcher::on_suppressCrashRecoveryDialog_stateChanged(int state)
{
if (loading) {
return;
}
SetSuppressCrashDialog(state);
}
static bool isLegacyTab(const QString &name)
{
return name == obs_module_text(
@@ -234,6 +245,64 @@ void AdvSceneSwitcher::on_hideLegacyTabs_stateChanged(int state)
ui->priorityBox->setVisible(!switcher->hideLegacyTabs);
}
void AdvSceneSwitcher::on_settingsLock_stateChanged(int state)
{
if (loading) {
return;
}
if (!state) {
switcher->settingsLockEnabled = false;
switcher->settingsLockPassword = "";
return;
}
bool ok;
QString newPassword = QInputDialog::getText(
this,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.newPasswordLabel"),
QLineEdit::Password, QString(), &ok);
const auto disablePassword = [this]() {
QSignalBlocker blocker(ui->settingsLock);
ui->settingsLock->setChecked(false);
ui->settingsLock->setCheckState(Qt::Unchecked);
switcher->settingsLockEnabled = false;
switcher->settingsLockPassword = "";
};
if (!ok || newPassword.isEmpty()) {
disablePassword();
return;
}
QString confirm = QInputDialog::getText(
this,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.confirmPasswordLabel"),
QLineEdit::Password, QString(), &ok);
if (!ok || newPassword != confirm) {
if (ok) {
QMessageBox::warning(
this,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordMismatch"));
}
disablePassword();
return;
}
switcher->settingsLockEnabled = true;
switcher->settingsLockPassword = newPassword.toStdString();
}
void AdvSceneSwitcher::SetDeprecationWarnings()
{
QString toolTip =
@@ -361,14 +430,50 @@ 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()
{
if (switcher->firstBoot && !switcher->disableHints) {
switcher->firstBoot = false;
DisplayMessage(
obs_module_text("AdvSceneSwitcher.firstBootMessage"));
switcher->Start();
if (!IsFirstRun() || !GetTopLevelMacros().empty()) {
return;
}
bool wasSkipped = false;
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
if (macro) {
renameMacroIfNecessary(macro);
QTimer::singleShot(0, this,
[this, macro]() { ui->macros->Add(macro); });
}
if (wasSkipped) {
ui->alwaysShowFeatureTabs->setChecked(true);
}
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)
@@ -427,7 +532,6 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
// Needs to be loaded before any entries which might rely on scene group
// selections to be available.
loadSceneGroups(obj);
LoadVariables(obj);
RunLoadSteps(obj);
@@ -466,7 +570,6 @@ void SwitcherData::SaveSettings(obs_data_t *obj)
saveSceneGroups(obj);
SaveMacros(obj);
SaveGlobalMacroSettings(obj);
SaveVariables(obj);
saveWindowTitleSwitches(obj);
saveScreenRegionSwitches(obj);
savePauseSwitches(obj);
@@ -524,6 +627,9 @@ void SwitcherData::SaveGeneralSettings(obs_data_t *obj)
disableFilterComboboxFilter);
obs_data_set_bool(obj, "warnPluginLoadFailure", warnPluginLoadFailure);
obs_data_set_bool(obj, "hideLegacyTabs", hideLegacyTabs);
obs_data_set_bool(obj, "settingsLockEnabled", settingsLockEnabled);
obs_data_set_string(obj, "settingsLockPassword",
settingsLockPassword.c_str());
SaveFunctionPriorities(obj, functionNamesByPriority);
@@ -595,6 +701,8 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
warnPluginLoadFailure = obs_data_get_bool(obj, "warnPluginLoadFailure");
obs_data_set_default_bool(obj, "hideLegacyTabs", true);
hideLegacyTabs = obs_data_get_bool(obj, "hideLegacyTabs");
settingsLockEnabled = obs_data_get_bool(obj, "settingsLockEnabled");
settingsLockPassword = obs_data_get_string(obj, "settingsLockPassword");
SetDefaultFunctionPriorities(obj);
LoadFunctionPriorities(obj, functionNamesByPriority);
@@ -915,7 +1023,9 @@ void AdvSceneSwitcher::SetupGeneralTab()
FilterComboBox::SetFilterBehaviourEnabled(
!switcher->disableFilterComboboxFilter);
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
ui->settingsLock->setChecked(switcher->settingsLockEnabled);
populatePriorityFunctionList(ui->priorityList);
populateThreadPriorityList(ui->threadPriority);

View File

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

View File

@@ -178,47 +178,19 @@ void AdvSceneSwitcher::on_ignoreWindows_currentRowChanged(int idx)
}
}
void checkWindowTitleSwitchDirect(WindowSwitch &s,
std::string &currentWindowTitle, bool &match,
OBSWeakSource &scene,
OBSWeakSource &transition)
static bool windowInfoMatchesSwitch(const WindowInfo &info,
const WindowSwitch &s)
{
bool focus = (!s.focus || s.window == currentWindowTitle);
bool fullscreen = (!s.fullscreen || IsFullscreen(s.window));
bool max = (!s.maximized || IsMaximized(s.window));
if (focus && fullscreen && max) {
match = true;
scene = s.getScene();
transition = s.transition;
if (s.focus && !info.focused) {
return false;
}
}
void checkWindowTitleSwitchRegex(WindowSwitch &s,
std::string &currentWindowTitle,
std::vector<std::string> windowList,
bool &match, OBSWeakSource &scene,
OBSWeakSource &transition)
{
for (auto &window : windowList) {
try {
std::regex expr(s.window);
if (!std::regex_match(window, expr)) {
continue;
}
} catch (const std::regex_error &) {
}
bool focus = (!s.focus || window == currentWindowTitle);
bool fullscreen = (!s.fullscreen || IsFullscreen(window));
bool max = (!s.maximized || IsMaximized(window));
if (focus && fullscreen && max) {
match = true;
scene = s.getScene();
transition = s.transition;
}
if (s.fullscreen && !info.fullscreen) {
return false;
}
if (s.maximized && !info.maximized) {
return false;
}
return true;
}
bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
@@ -228,24 +200,37 @@ bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
return false;
}
std::string currentWindowTitle = switcher->currentTitle;
bool match = false;
std::vector<std::string> windowList;
GetWindowList(windowList);
WindowQueryOptions options;
options.focus = true;
options.fullscreen = true;
options.maximized = true;
const auto windows = GetWindows(options);
for (WindowSwitch &s : windowSwitches) {
if (!s.initialized()) {
continue;
}
if (std::find(windowList.begin(), windowList.end(), s.window) !=
windowList.end()) {
checkWindowTitleSwitchDirect(s, currentWindowTitle,
match, scene, transition);
} else {
checkWindowTitleSwitchRegex(s, currentWindowTitle,
windowList, match, scene,
transition);
for (const auto &info : windows) {
bool titleMatch = false;
try {
std::regex expr(s.window);
titleMatch = info.title == s.window ||
std::regex_match(info.title, expr);
} catch (const std::regex_error &) {
titleMatch = info.title == s.window;
}
if (!titleMatch || !windowInfoMatchesSwitch(info, s)) {
continue;
}
match = true;
scene = s.getScene();
transition = s.transition;
break;
}
if (match) {

View File

@@ -32,6 +32,9 @@
#endif
#include <fstream>
#include <sstream>
#include <climits>
#include <dirent.h>
#include <unistd.h>
#include "kwin-helpers.h"
namespace advss {
@@ -233,28 +236,65 @@ std::string getWindowName(Window window)
return windowTitle;
}
void GetWindowList(std::vector<std::string> &windows)
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
{
windows.resize(0);
for (auto window : getTopLevelWindows()) {
auto name = getWindowName(window);
if (name.empty()) {
continue;
}
windows.emplace_back(name);
}
}
const std::string foregroundTitle = GetCurrentWindowTitle();
const auto topLevel = getTopLevelWindows();
auto display = disp();
void GetWindowList(QStringList &windows)
{
windows.clear();
for (auto window : getTopLevelWindows()) {
std::vector<WindowInfo> result;
result.reserve(topLevel.size());
for (auto window : topLevel) {
auto name = getWindowName(window);
if (name.empty()) {
continue;
}
windows << QString::fromStdString(name);
WindowInfo info;
info.title = name;
if (options.focus) {
info.focused = (name == foregroundTitle);
}
if (options.fullscreen || options.maximized) {
QStringList states = getStates(window);
if (options.fullscreen) {
info.fullscreen = states.contains(
"_NET_WM_STATE_FULLSCREEN");
}
if (options.maximized) {
info.maximized =
states.contains(
"_NET_WM_STATE_MAXIMIZED_VERT") &&
states.contains(
"_NET_WM_STATE_MAXIMIZED_HORZ");
}
}
if (options.geometry && display) {
XWindowAttributes attrs;
if (XGetWindowAttributes(display, window, &attrs)) {
int x = 0;
int y = 0;
Window child;
XTranslateCoordinates(
display, window,
DefaultRootWindow(display), 0, 0, &x,
&y, &child);
info.x = x;
info.y = y;
info.width = attrs.width;
info.height = attrs.height;
}
}
// windowClass and text not implemented on Linux
result.emplace_back(std::move(info));
}
return result;
}
int getActiveWindow(Window *&window)
@@ -278,91 +318,24 @@ int getActiveWindow(Window *&window)
&bytes, (uint8_t **)&window);
}
void GetCurrentWindowTitle(std::string &title)
std::string GetCurrentWindowTitle()
{
if (KWin) {
title = FocusNotifier::getActiveWindowTitle();
return;
return FocusNotifier::getActiveWindowTitle();
}
Window *data = 0;
if (getActiveWindow(data) != Success || !data) {
return;
return {};
}
if (!data[0]) {
XFree(data);
return;
return {};
}
auto name = getWindowName(data[0]);
XFree(data);
if (name.empty()) {
return;
}
title = name;
}
bool windowStatesAreSet(const std::string &windowTitle,
std::vector<QString> &expectedStates)
{
if (!ewmhIsSupported()) {
return false;
}
std::vector<Window> windows = getTopLevelWindows();
for (auto &window : windows) {
auto name = getWindowName(window);
if (name.empty()) {
continue;
}
bool equals = windowTitle == name;
bool matches = QString::fromStdString(name).contains(
QRegularExpression(
QString::fromStdString(windowTitle)));
if (!(equals || matches)) {
continue;
}
QStringList states = getStates(window);
if (states.isEmpty()) {
if (expectedStates.empty()) {
return true;
}
return false;
}
for (const auto &state : expectedStates) {
if (!states.contains(state)) {
return false;
}
}
return true;
}
return false;
}
bool IsMaximized(const std::string &title)
{
std::vector<QString> states;
states.emplace_back("_NET_WM_STATE_MAXIMIZED_VERT");
states.emplace_back("_NET_WM_STATE_MAXIMIZED_HORZ");
return windowStatesAreSet(title, states);
}
bool IsFullscreen(const std::string &title)
{
std::vector<QString> states;
states.emplace_back("_NET_WM_STATE_FULLSCREEN");
return windowStatesAreSet(title, states);
}
std::optional<std::string> GetTextInWindow(const std::string &)
{
// Not implemented
return {};
return name;
}
static void getProcessListProcps(QStringList &processes)
@@ -409,16 +382,17 @@ static void getProcessListProcps2(QStringList &processes)
#endif
}
void GetProcessList(QStringList &processes)
QStringList GetProcessList()
{
processes.clear();
QStringList processes;
if (libprocpsSupported) {
getProcessListProcps(processes);
return;
return processes;
}
if (libprocps2Supported) {
getProcessListProcps2(processes);
}
return processes;
}
long getForegroundProcessPid()
@@ -469,24 +443,83 @@ std::string getProcNameFromPid(long pid)
return name;
}
void GetForegroundProcessName(QString &proc)
std::string GetForegroundProcessName()
{
std::string temp;
GetForegroundProcessName(temp);
proc = QString::fromStdString(temp);
auto pid = getForegroundProcessPid();
return getProcNameFromPid(pid);
}
void GetForegroundProcessName(std::string &proc)
static std::string getProcessPathFromPid(long pid)
{
std::string linkPath = "/proc/" + std::to_string(pid) + "/exe";
char buf[PATH_MAX];
ssize_t len = readlink(linkPath.c_str(), buf, sizeof(buf) - 1);
if (len <= 0) {
return {};
}
buf[len] = '\0';
return buf;
}
std::string GetForegroundProcessPath()
{
proc.resize(0);
auto pid = getForegroundProcessPid();
proc = getProcNameFromPid(pid);
if (pid <= 0) {
return {};
}
return getProcessPathFromPid(pid);
}
QStringList GetProcessPathsFromName(const QString &name)
{
QStringList paths;
const std::string nameStr = name.toStdString();
DIR *procDir = opendir("/proc");
if (!procDir) {
return paths;
}
struct dirent *entry;
while ((entry = readdir(procDir)) != nullptr) {
bool isPid = (entry->d_name[0] != '\0');
for (const char *c = entry->d_name; *c; c++) {
if (!isdigit(*c)) {
isPid = false;
break;
}
}
if (!isPid) {
continue;
}
std::string pid = entry->d_name;
std::string commPath = "/proc/" + pid + "/comm";
std::ifstream commFile(commPath);
if (!commFile) {
continue;
}
std::string comm;
std::getline(commFile, comm);
if (comm != nameStr) {
continue;
}
std::string path = getProcessPathFromPid(std::stol(pid));
if (path.empty()) {
continue;
}
QString qPath = QString::fromStdString(path);
if (!paths.contains(qPath)) {
paths.append(qPath);
}
}
closedir(procDir);
return paths;
}
bool IsInFocus(const QString &executable)
{
std::string current;
GetForegroundProcessName(current);
const auto current = GetForegroundProcessName();
// True if executable switch equals current window
bool equals = (executable.toStdString() == current);

View File

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

View File

@@ -4,6 +4,8 @@
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include <chrono>
namespace advss {
const std::string MacroActionMacro::id = "macro";
@@ -118,6 +120,45 @@ bool MacroActionMacro::PerformAction()
case Action::TOGGLE_ACTION:
AdjustActionState(macro);
break;
case Action::RUN_MACRO: {
if (_runOptions.skipWhenPaused && macro->Paused()) {
break;
}
const bool conditionsMatched =
macro->CheckConditions(true);
macro->PerformActions(conditionsMatched, false, true);
break;
}
case Action::GET_INFO: {
SetTempVarValue(
"conditionCount",
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:
break;
}
@@ -175,6 +216,12 @@ void MacroActionMacro::LogAction() const
case Action::NESTED_MACRO:
ablog(LOG_INFO, "run nested macro");
break;
case Action::GET_INFO:
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
break;
case Action::RUN_MACRO:
ablog(LOG_INFO, "run macro \"%s\"", macro->Name().c_str());
break;
default:
break;
}
@@ -202,8 +249,8 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
bool MacroActionMacro::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action"));
SetAction(static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action")));
_macro.Load(obj);
_actionSelectionType = static_cast<SelectionType>(
obs_data_get_int(obj, "actionSelectionType"));
@@ -303,6 +350,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 const std::vector<std::pair<MacroActionMacro::Action, std::string>>
@@ -318,7 +415,9 @@ static void populateActionSelection(QComboBox *list)
{MacroActionMacro::Action::NESTED_MACRO,
"AdvSceneSwitcher.action.macro.type.nestedMacro"},
{MacroActionMacro::Action::RUN_ACTIONS,
"AdvSceneSwitcher.action.macro.type.run"},
"AdvSceneSwitcher.action.macro.type.runActions"},
{MacroActionMacro::Action::RUN_MACRO,
"AdvSceneSwitcher.action.macro.type.runMacro"},
{MacroActionMacro::Action::STOP,
"AdvSceneSwitcher.action.macro.type.stop"},
{MacroActionMacro::Action::DISABLE_ACTION,
@@ -327,6 +426,8 @@ static void populateActionSelection(QComboBox *list)
"AdvSceneSwitcher.action.macro.type.enableAction"},
{MacroActionMacro::Action::TOGGLE_ACTION,
"AdvSceneSwitcher.action.macro.type.toggleAction"},
{MacroActionMacro::Action::GET_INFO,
"AdvSceneSwitcher.action.macro.type.getInfo"},
};
for (const auto &[value, name] : actions) {
@@ -396,6 +497,10 @@ MacroActionMacroEdit::MacroActionMacroEdit(
_actionSections(new QComboBox(this)),
_skipWhenPaused(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.skipWhenPaused"))),
_noConditionsWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.macro.type.runMacro.noConditionsWarning"))),
_runMacroHelp(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.action.macro.type.runMacro.help"))),
_setInputs(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.setInputs"))),
_inputs(new MacroInputEdit()),
@@ -469,6 +574,7 @@ MacroActionMacroEdit::MacroActionMacroEdit(
layout->addLayout(_setInputsLayout);
layout->addWidget(_inputs);
layout->addWidget(_skipWhenPaused);
layout->addWidget(_noConditionsWarning);
layout->addWidget(_nestedMacro);
setLayout(layout);
_entryData = entryData;
@@ -496,7 +602,7 @@ void MacroActionMacroEdit::UpdateEntryData()
}
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_actions->findData(static_cast<int>(_entryData->GetAction())));
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
static_cast<int>(_entryData->_actionSelectionType)));
_actionIndex->SetValue(_entryData->_actionIndex);
@@ -545,8 +651,8 @@ void MacroActionMacroEdit::MacroChanged(const QString &text)
void MacroActionMacroEdit::ActionChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionMacro::Action>(
_actions->itemData(idx).toInt());
_entryData->SetAction(static_cast<MacroActionMacro::Action>(
_actions->itemData(idx).toInt()));
SetWidgetVisibility();
}
@@ -638,6 +744,7 @@ void MacroActionMacroEdit::InputsChanged(const StringList &inputs)
void MacroActionMacroEdit::SetWidgetVisibility()
{
_entryLayout->removeWidget(_actions);
_entryLayout->removeWidget(_runMacroHelp);
_entryLayout->removeWidget(_actionIndex);
_entryLayout->removeWidget(_macros);
_entryLayout->removeWidget(_actionSections);
@@ -653,6 +760,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
const std::unordered_map<std::string, QWidget *> placeholders = {
{"{{actions}}", _actions},
{"{{runMacroHelp}}", _runMacroHelp},
{"{{actionIndex}}", _actionIndex},
{"{{macros}}", _macros},
{"{{actionSections}}", _actionSections},
@@ -665,7 +773,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
};
const auto action = _entryData->_action;
const auto action = _entryData->GetAction();
const char *layoutText = "";
switch (action) {
case MacroActionMacro::Action::PAUSE:
@@ -674,8 +782,12 @@ void MacroActionMacroEdit::SetWidgetVisibility()
case MacroActionMacro::Action::RESET_COUNTER:
case MacroActionMacro::Action::STOP:
case MacroActionMacro::Action::NESTED_MACRO:
case MacroActionMacro::Action::GET_INFO:
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
break;
case MacroActionMacro::Action::RUN_MACRO:
layoutText = "AdvSceneSwitcher.action.macro.layout.runMacro";
break;
case MacroActionMacro::Action::RUN_ACTIONS:
layoutText = "AdvSceneSwitcher.action.macro.layout.run";
break;
@@ -702,7 +814,8 @@ void MacroActionMacroEdit::SetWidgetVisibility()
}
if (action == MacroActionMacro::Action::RUN_ACTIONS ||
action == MacroActionMacro::Action::STOP) {
action == MacroActionMacro::Action::STOP ||
action == MacroActionMacro::Action::RUN_MACRO) {
_macros->HideSelectedMacro();
} else {
_macros->ShowAllMacros();
@@ -747,8 +860,19 @@ void MacroActionMacroEdit::SetWidgetVisibility()
_actionSections->setVisible(
action == MacroActionMacro::Action::RUN_ACTIONS ||
isModifyingActionState);
_skipWhenPaused->setVisible(action ==
MacroActionMacro::Action::RUN_ACTIONS);
_skipWhenPaused->setVisible(
action == MacroActionMacro::Action::RUN_ACTIONS ||
action == MacroActionMacro::Action::RUN_MACRO);
if (action == MacroActionMacro::Action::RUN_MACRO) {
auto macro = _entryData->_macro.GetMacro();
_noConditionsWarning->setVisible(!macro ||
macro->Conditions().empty());
} else {
_noConditionsWarning->setVisible(false);
}
_runMacroHelp->setVisible(action ==
MacroActionMacro::Action::RUN_MACRO);
_nestedMacro->setVisible(action ==
MacroActionMacro::Action::NESTED_MACRO);

View File

@@ -11,6 +11,7 @@
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
namespace advss {
@@ -56,11 +57,15 @@ public:
TOGGLE_ACTION,
TOGGLE_PAUSE,
NESTED_MACRO,
GET_INFO,
RUN_MACRO,
};
void SetAction(Action);
Action GetAction() const { return _action; }
enum class SelectionType { INDEX, LABEL, ID };
Action _action = Action::NESTED_MACRO;
SelectionType _actionSelectionType = SelectionType::INDEX;
bool _useElseSection = false;
IntVariable _actionIndex = 1;
@@ -72,9 +77,12 @@ public:
int _customWidgetHeight = 0;
private:
void SetupTempVars();
void RunActions(Macro *actionMacro) const;
void AdjustActionState(Macro *) const;
Action _action = Action::NESTED_MACRO;
static bool _registered;
static const std::string id;
};
@@ -125,6 +133,8 @@ private:
QCheckBox *_reevaluateConditionState;
QComboBox *_actionSections;
QCheckBox *_skipWhenPaused;
QLabel *_noConditionsWarning;
HelpIcon *_runMacroHelp;
QCheckBox *_setInputs;
MacroInputEdit *_inputs;
QHBoxLayout *_entryLayout;

View File

@@ -17,6 +17,15 @@ namespace advss {
const std::string MacroActionVariable::id = "variable";
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
{
auto refs = MacroSegment::GetTempVarRefs();
if (_tempVar.HasValidID()) {
refs.push_back(_tempVar);
}
return refs;
}
bool MacroActionVariable::_registered = MacroActionFactory::Register(
MacroActionVariable::id,
{MacroActionVariable::Create, MacroActionVariableEdit::Create,
@@ -219,6 +228,43 @@ void MacroActionVariable::GenerateRandomNumber(Variable *var)
}
}
void MacroActionVariable::PickRandomValue(Variable *var)
{
static std::random_device rd;
static std::mt19937 gen(rd());
static const uint8_t maxIter = 255;
if (_randomValues.isEmpty()) {
return;
}
if (_randomValues.size() == 1) {
const auto &value = _randomValues.at(0);
var->SetValue(value);
_lastRandomValue = value;
}
std::uniform_int_distribution<int> dis(0, _randomValues.size() - 1);
StringVariable value = _randomValues.at(dis(gen));
uint8_t iter = 0;
for (; !_allowRepeatValues && iter < maxIter &&
_lastRandomValue.has_value() &&
std::string(value) == _lastRandomValue.value();
iter++) {
value = _randomValues.at(dis(gen));
}
if (iter == maxIter) {
blog(LOG_INFO,
"giving up picking non-repeat random value after %d tries",
maxIter);
}
var->SetValue(value);
_lastRandomValue = value;
}
struct AskForInputParams {
AskForInputParams(const QString &prompt_, const QString &placeholder_)
: prompt(prompt_),
@@ -458,6 +504,9 @@ bool MacroActionVariable::PerformAction()
case Action::RANDOM_NUMBER:
GenerateRandomNumber(var.get());
return true;
case Action::RANDOM_LIST_VALUE:
PickRandomValue(var.get());
return true;
}
return true;
@@ -498,6 +547,8 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
_randomNumberStart.Save(obj, "randomNumberStart");
_randomNumberEnd.Save(obj, "randomNumberEnd");
obs_data_set_bool(obj, "generateInteger", _generateInteger);
_randomValues.Save(obj, "randomValues", "value");
obs_data_set_bool(obj, "allowRepeatValues", _allowRepeatValues);
_jsonQuery.Save(obj, "jsonQuery");
_jsonIndex.Save(obj, "jsonIndex");
@@ -556,6 +607,8 @@ bool MacroActionVariable::Load(obs_data_t *obj)
_randomNumberStart.Load(obj, "randomNumberStart");
_randomNumberEnd.Load(obj, "randomNumberEnd");
_generateInteger = obs_data_get_bool(obj, "generateInteger");
_randomValues.Load(obj, "randomValues", "value");
_allowRepeatValues = obs_data_get_bool(obj, "allowRepeatValues");
_jsonQuery.Load(obj, "jsonQuery");
_jsonIndex.Load(obj, "jsonIndex");
@@ -724,6 +777,8 @@ static inline void populateActionSelection(QComboBox *list)
"AdvSceneSwitcher.action.variable.type.roundToInt"},
{MacroActionVariable::Action::RANDOM_NUMBER,
"AdvSceneSwitcher.action.variable.type.randomNumber"},
{MacroActionVariable::Action::RANDOM_LIST_VALUE,
"AdvSceneSwitcher.action.variable.type.randomListValue"},
{true, ""}, // Separator
{MacroActionVariable::Action::USER_INPUT,
@@ -801,11 +856,10 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_segmentValueStatus(new QLabel()),
_segmentValue(new ResizingPlainTextEdit(this, 10, 1, 1)),
_substringLayout(new QVBoxLayout()),
_subStringIndexEntryLayout(new QHBoxLayout()),
_subStringRegexEntryLayout(new QHBoxLayout()),
_subStringControlsLayout(new QHBoxLayout()),
_subStringStart(new VariableSpinBox(this)),
_subStringSize(new VariableSpinBox(this)),
_substringRegex(new RegexConfigWidget(parent)),
_subStringRegex(new RegexConfigWidget(parent)),
_regexPattern(new ResizingPlainTextEdit(this, 10, 1, 1)),
_regexMatchIdx(new VariableSpinBox(this)),
_findReplaceLayout(new QHBoxLayout()),
@@ -837,7 +891,13 @@ MacroActionVariableEdit::MacroActionVariableEdit(
obs_module_text(
"AdvSceneSwitcher.action.variable.generateInteger"),
this)),
_randomLayout(new QVBoxLayout()),
_randomNumberLayout(new QVBoxLayout()),
_randomValues(new StringListEdit(this)),
_allowRepeatValues(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.variable.type.allowRepeat"),
this)),
_randomValueLayout(new QVBoxLayout()),
_jsonQuery(new VariableLineEdit(this)),
_jsonQueryHelp(new HelpIcon(
obs_module_text(
@@ -873,6 +933,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_randomNumberStart->setMaximum(9999999999);
_randomNumberEnd->setMinimum(-9999999999);
_randomNumberEnd->setMaximum(9999999999);
_randomValues->SetMaxStringSize(99999999);
_jsonIndex->setMaximum(999);
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),
@@ -901,7 +962,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_subStringSize,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(SubStringSizeChanged(const NumberVariable<int> &)));
QWidget::connect(_substringRegex,
QWidget::connect(_subStringRegex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(SubStringRegexChanged(const RegexConfig &)));
QWidget::connect(_regexPattern, SIGNAL(textChanged()), this,
@@ -961,6 +1022,11 @@ MacroActionVariableEdit::MacroActionVariableEdit(
SLOT(RandomNumberEndChanged(const NumberVariable<double> &)));
QWidget::connect(_generateInteger, SIGNAL(stateChanged(int)), this,
SLOT(GenerateIntegerChanged(int)));
QWidget::connect(_randomValues,
SIGNAL(StringListChanged(const StringList &)), this,
SLOT(RandomValueListChanged(const StringList &)));
QWidget::connect(_allowRepeatValues, SIGNAL(stateChanged(int)), this,
SLOT(AllowRepeatValuesChanged(int)));
QWidget::connect(_jsonQuery, SIGNAL(editingFinished()), this,
SLOT(JsonQueryChanged()));
QWidget::connect(
@@ -975,9 +1041,6 @@ MacroActionVariableEdit::MacroActionVariableEdit(
{"{{strValue}}", _strValue},
{"{{numValue}}", _numValue},
{"{{segmentIndex}}", _segmentIdx},
{"{{subStringStart}}", _subStringStart},
{"{{subStringSize}}", _subStringSize},
{"{{regexMatchIdx}}", _regexMatchIdx},
{"{{findRegex}}", _findRegex},
{"{{findStr}}", _findStr},
{"{{replaceStr}}", _replaceStr},
@@ -1009,12 +1072,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.variable.layout.substringIndex"),
_subStringIndexEntryLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.variable.layout.substringRegex"),
_subStringRegexEntryLayout, widgetPlaceholders);
_subStringControlsLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
@@ -1035,15 +1093,17 @@ MacroActionVariableEdit::MacroActionVariableEdit(
obs_module_text(
"AdvSceneSwitcher.action.variable.layout.randomNumber"),
randomLayout, widgetPlaceholders);
_randomLayout->addLayout(randomLayout);
_randomLayout->addWidget(_generateInteger);
_randomNumberLayout->addLayout(randomLayout);
_randomNumberLayout->addWidget(_generateInteger);
_randomValueLayout->addWidget(_randomValues);
_randomValueLayout->addWidget(_allowRepeatValues);
auto regexConfigLayout = new QHBoxLayout;
regexConfigLayout->addWidget(_substringRegex);
regexConfigLayout->addWidget(_subStringRegex);
regexConfigLayout->addStretch();
_substringLayout->addLayout(_subStringIndexEntryLayout);
_substringLayout->addLayout(_subStringRegexEntryLayout);
_substringLayout->addLayout(_subStringControlsLayout);
_substringLayout->addWidget(_regexPattern);
_substringLayout->addLayout(regexConfigLayout);
@@ -1056,7 +1116,8 @@ MacroActionVariableEdit::MacroActionVariableEdit(
layout->addWidget(_mathExpressionResult);
layout->addLayout(_promptLayout);
layout->addLayout(_placeholderLayout);
layout->addLayout(_randomLayout);
layout->addLayout(_randomNumberLayout);
layout->addLayout(_randomValueLayout);
setLayout(layout);
_entryData = entryData;
@@ -1090,7 +1151,7 @@ void MacroActionVariableEdit::UpdateEntryData()
: MacroSegmentSelection::Type::ACTION);
_subStringStart->SetValue(_entryData->_subStringStart);
_subStringSize->SetValue(_entryData->_subStringSize);
_substringRegex->SetRegexConfig(_entryData->_subStringRegex);
_subStringRegex->SetRegexConfig(_entryData->_subStringRegex);
_findRegex->SetRegexConfig(_entryData->_findRegex);
_regexPattern->setPlainText(
QString::fromStdString(_entryData->_regexPattern));
@@ -1115,6 +1176,8 @@ void MacroActionVariableEdit::UpdateEntryData()
_randomNumberStart->SetValue(_entryData->_randomNumberStart);
_randomNumberEnd->SetValue(_entryData->_randomNumberEnd);
_generateInteger->setChecked(_entryData->_generateInteger);
_allowRepeatValues->setChecked(_entryData->_allowRepeatValues);
_randomValues->SetStringList(_entryData->_randomValues);
_jsonQuery->setText(_entryData->_jsonQuery);
_jsonIndex->SetValue(_entryData->_jsonIndex);
@@ -1396,6 +1459,7 @@ void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
SetWidgetVisibility();
}
@@ -1457,6 +1521,18 @@ void MacroActionVariableEdit::GenerateIntegerChanged(int value)
_entryData->_generateInteger = value;
}
void MacroActionVariableEdit::RandomValueListChanged(const StringList &values)
{
GUARD_LOADING_AND_LOCK();
_entryData->_randomValues = values;
}
void MacroActionVariableEdit::AllowRepeatValuesChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_allowRepeatValues = value;
}
void MacroActionVariableEdit::JsonQueryChanged()
{
GUARD_LOADING_AND_LOCK();
@@ -1495,8 +1571,20 @@ void MacroActionVariableEdit::SetWidgetVisibility()
{"{{jsonQuery}}", _jsonQuery},
{"{{jsonQueryHelp}}", _jsonQueryHelp},
{"{{jsonIndex}}", _jsonIndex},
{"{{subStringRegex}}", _subStringRegex},
{"{{subStringStart}}", _subStringStart},
{"{{subStringSize}}", _subStringSize},
{"{{regexMatchIdx}}", _regexMatchIdx},
};
for (const auto &[_, widget] : widgetPlaceholders) {
_entryLayout->removeWidget(widget);
_subStringControlsLayout->removeWidget(widget);
}
ClearLayout(_entryLayout);
ClearLayout(_subStringControlsLayout);
const char *layoutString = "";
if (_entryData->_action == MacroActionVariable::Action::PAD) {
layoutString = obs_module_text(
@@ -1510,11 +1598,6 @@ void MacroActionVariableEdit::SetWidgetVisibility()
"AdvSceneSwitcher.action.variable.layout.other");
}
for (const auto &[_, widget] : widgetPlaceholders) {
_entryLayout->removeWidget(widget);
}
ClearLayout(_entryLayout);
PlaceWidgets(layoutString, _entryLayout, widgetPlaceholders);
if (_entryData->_action == MacroActionVariable::Action::SET_VALUE ||
@@ -1561,15 +1644,25 @@ void MacroActionVariableEdit::SetWidgetVisibility()
MacroActionVariable::Action::SET_ACTION_VALUE ||
_entryData->_action ==
MacroActionVariable::Action::SET_CONDITION_VALUE);
bool showRegex = _entryData->_subStringRegex.Enabled();
layoutString =
showRegex
? "AdvSceneSwitcher.action.variable.layout.substringRegex"
: "AdvSceneSwitcher.action.variable.layout.substringIndex";
PlaceWidgets(obs_module_text(layoutString), _subStringControlsLayout,
widgetPlaceholders);
_subStringStart->setVisible(!showRegex);
_subStringSize->setVisible(!showRegex);
_regexMatchIdx->setVisible(showRegex);
SetLayoutVisible(_substringLayout,
_entryData->_action ==
MacroActionVariable::Action::SUBSTRING);
if (_entryData->_action == MacroActionVariable::Action::SUBSTRING) {
bool showRegex = _entryData->_subStringRegex.Enabled();
SetLayoutVisible(_subStringIndexEntryLayout, !showRegex);
SetLayoutVisible(_subStringRegexEntryLayout, showRegex);
_regexPattern->setVisible(showRegex);
}
_regexPattern->setVisible(
showRegex &&
_entryData->_action == MacroActionVariable::Action::SUBSTRING);
SetLayoutVisible(_findReplaceLayout,
_entryData->_action ==
MacroActionVariable::Action::FIND_AND_REPLACE);
@@ -1634,9 +1727,13 @@ void MacroActionVariableEdit::SetWidgetVisibility()
MacroActionVariable::Action::PAD);
_caseType->setVisible(_entryData->_action ==
MacroActionVariable::Action::CHANGE_CASE);
SetLayoutVisible(_randomLayout,
SetLayoutVisible(_randomNumberLayout,
_entryData->_action ==
MacroActionVariable::Action::RANDOM_NUMBER);
SetLayoutVisible(
_randomValueLayout,
_entryData->_action ==
MacroActionVariable::Action::RANDOM_LIST_VALUE);
_jsonQuery->setVisible(_entryData->_action ==
MacroActionVariable::Action::QUERY_JSON);
_jsonQueryHelp->setVisible(_entryData->_action ==

View File

@@ -6,6 +6,7 @@
#include "resizing-text-edit.hpp"
#include "scene-selection.hpp"
#include "single-char-selection.hpp"
#include "string-list.hpp"
#include "variable-line-edit.hpp"
#include "variable-text-edit.hpp"
#include "variable-spinbox.hpp"
@@ -22,6 +23,7 @@ public:
bool PostLoad();
std::string GetShortDesc() const;
std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void SetSegmentIndexValue(int);
@@ -56,6 +58,7 @@ public:
QUERY_JSON,
ARRAY_JSON,
COPY_VAR,
RANDOM_LIST_VALUE,
};
Action _action = Action::SET_VALUE;
@@ -107,6 +110,10 @@ public:
DoubleVariable _randomNumberEnd = 100;
bool _generateInteger = true;
StringList _randomValues = {"value1", "value2", "value3"};
bool _allowRepeatValues = true;
std::optional<std::string> _lastRandomValue;
StringVariable _jsonQuery = "$.some.nested.value";
IntVariable _jsonIndex = 0;
@@ -119,6 +126,7 @@ private:
void HandleCaseChange(Variable *);
void SetToSceneItemName(Variable *);
void GenerateRandomNumber(Variable *);
void PickRandomValue(Variable *);
std::weak_ptr<MacroSegment> _macroSegment;
int _segmentIdxLoadValue = -1;
@@ -175,6 +183,8 @@ private slots:
void RandomNumberStartChanged(const NumberVariable<double> &);
void RandomNumberEndChanged(const NumberVariable<double> &);
void GenerateIntegerChanged(int);
void RandomValueListChanged(const StringList &);
void AllowRepeatValuesChanged(int);
void JsonQueryChanged();
void JsonIndexChanged(const NumberVariable<int> &);
@@ -194,11 +204,10 @@ private:
QLabel *_segmentValueStatus;
ResizingPlainTextEdit *_segmentValue;
QVBoxLayout *_substringLayout;
QHBoxLayout *_subStringIndexEntryLayout;
QHBoxLayout *_subStringRegexEntryLayout;
QHBoxLayout *_subStringControlsLayout;
VariableSpinBox *_subStringStart;
VariableSpinBox *_subStringSize;
RegexConfigWidget *_substringRegex;
RegexConfigWidget *_subStringRegex;
ResizingPlainTextEdit *_regexPattern;
VariableSpinBox *_regexMatchIdx;
QHBoxLayout *_findReplaceLayout;
@@ -225,7 +234,10 @@ private:
VariableDoubleSpinBox *_randomNumberStart;
VariableDoubleSpinBox *_randomNumberEnd;
QCheckBox *_generateInteger;
QVBoxLayout *_randomLayout;
QVBoxLayout *_randomNumberLayout;
StringListEdit *_randomValues;
QCheckBox *_allowRepeatValues;
QVBoxLayout *_randomValueLayout;
VariableLineEdit *_jsonQuery;
QLabel *_jsonQueryHelp;
VariableSpinBox *_jsonIndex;

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ public:
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroCondition> Create(Macro *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
{
MacroSegment::Save(obj);
@@ -20,6 +31,10 @@ bool MacroCondition::Load(obs_data_t *obj)
{
MacroSegment::Load(obj);
_logic.Load(obj, "logic");
if (_logic.GetType() == Logic::Type::NONE) {
SetEnabled(false);
_logic.SetType(Logic::Type::AND);
}
_durationModifier.Load(obj);
return true;
}
@@ -39,7 +54,8 @@ void MacroCondition::ValidateLogicSelection(bool isRootCondition,
return;
}
_logic.SetType(Logic::Type::NONE);
_logic.SetType(Logic::Type::AND);
SetEnabled(false);
blog(LOG_WARNING,
"setting invalid logic selection to 'ignore' for macro %s",
context);

View File

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

View File

@@ -366,7 +366,7 @@ static void runSegmentHighlightChecksHelper(MacroSegmentList *list)
// highlight its segments if required
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
if (macroAction &&
macroAction->_action ==
macroAction->GetAction() ==
MacroActionMacro::Action::NESTED_MACRO) {
continue;
}
@@ -1086,6 +1086,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
HighlightAction(idx);
ui->actionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->actionsList->ensureWidgetVisible(
ui->actionsList->WidgetAt(currentActionIdx));
});
}
void MacroEdit::AddMacroAction(int idx)
@@ -1113,6 +1118,9 @@ void MacroEdit::AddMacroAction(int idx)
macro->Actions().at(idx - 1)->Save(data);
}
AddMacroAction(macro.get(), idx, id, data);
if (idx < (int)macro->Actions().size()) {
macro->Actions().at(idx)->FixupVarMappingRefs();
}
}
void MacroEdit::on_actionAdd_clicked()
@@ -1389,6 +1397,11 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
HighlightElseAction(idx);
ui->elseActionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->elseActionsList->ensureWidgetVisible(
ui->elseActionsList->WidgetAt(currentElseActionIdx));
});
}
void MacroEdit::AddMacroElseAction(int idx)
@@ -1416,6 +1429,9 @@ void MacroEdit::AddMacroElseAction(int idx)
macro->ElseActions().at(idx - 1)->Save(data);
}
AddMacroElseAction(macro.get(), idx, id, data);
if (idx < (int)macro->ElseActions().size()) {
macro->ElseActions().at(idx)->FixupVarMappingRefs();
}
}
void MacroEdit::RemoveMacroElseAction(int idx)
@@ -1560,6 +1576,9 @@ void MacroEdit::AddMacroCondition(int idx)
macro->Conditions().at(idx - 1)->Save(data);
}
AddMacroCondition(macro.get(), idx, id, data.Get(), logic);
if (idx < (int)macro->Conditions().size()) {
macro->Conditions().at(idx)->FixupVarMappingRefs();
}
}
void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
@@ -1591,6 +1610,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
HighlightCondition(idx);
ui->conditionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->conditionsList->ensureWidgetVisible(
ui->conditionsList->WidgetAt(currentConditionIdx));
});
}
void MacroEdit::on_conditionAdd_clicked()

View File

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

View File

@@ -0,0 +1,89 @@
#include "macro-export-extensions.hpp"
#include "action-queue.hpp"
#include "variable.hpp"
#include <obs.hpp>
namespace advss {
static bool setup();
static bool setupDone = setup();
static bool setup()
{
// --- Variables ---
AddMacroExportExtension(
{"AdvSceneSwitcher.macroTab.export.variables", "variables",
[](obs_data_t *data, const QStringList &selectedIds) {
if (selectedIds.isEmpty()) {
SaveVariables(data);
return;
}
OBSDataArrayAutoRelease array =
obs_data_array_create();
for (const auto &v : GetVariables()) {
const QString name =
QString::fromStdString(v->Name());
if (!selectedIds.contains(name)) {
continue;
}
OBSDataAutoRelease item = obs_data_create();
v->Save(item);
obs_data_array_push_back(array, item);
}
obs_data_set_array(data, "variables", array);
},
[](obs_data_t *data, const QStringList &) {
ImportVariables(data);
},
[]() -> QList<QPair<QString, QString>> {
QList<QPair<QString, QString>> items;
for (const auto &v : GetVariables()) {
const QString name =
QString::fromStdString(v->Name());
items.append({name, name});
}
return items;
}});
// --- Action Queues ---
AddMacroExportExtension(
{"AdvSceneSwitcher.macroTab.export.actionQueues",
"actionQueues",
[](obs_data_t *data, const QStringList &selectedIds) {
if (selectedIds.isEmpty()) {
SaveActionQueues(data);
return;
}
OBSDataArrayAutoRelease array =
obs_data_array_create();
for (const auto &q : GetActionQueues()) {
const QString name =
QString::fromStdString(q->Name());
if (!selectedIds.contains(name)) {
continue;
}
OBSDataAutoRelease item = obs_data_create();
q->Save(item);
obs_data_array_push_back(array, item);
}
obs_data_set_array(data, "actionQueues", array);
},
[](obs_data_t *data, const QStringList &) {
ImportQueues(data);
},
[]() -> QList<QPair<QString, QString>> {
QList<QPair<QString, QString>> items;
for (const auto &q : GetActionQueues()) {
const QString name =
QString::fromStdString(q->Name());
items.append({name, name});
}
return items;
}});
return true;
}
} // namespace advss

View File

@@ -0,0 +1,17 @@
#include "macro-export-extensions.hpp"
namespace advss {
static std::vector<MacroExportExtension> extensions;
void AddMacroExportExtension(const MacroExportExtension &ext)
{
extensions.emplace_back(ext);
}
const std::vector<MacroExportExtension> &GetMacroExportExtensions()
{
return extensions;
}
} // namespace advss

View File

@@ -0,0 +1,41 @@
#pragma once
#include "export-symbol-helper.hpp"
#include <functional>
#include <obs-data.h>
#include <QList>
#include <QPair>
#include <QString>
#include <vector>
namespace advss {
struct MacroExportExtension {
const char *displayNameKey;
const char *jsonKey;
// Export callback.
// Write content into 'data'. 'selectedIds' contains the item IDs the
// user explicitly checked; if empty (no per-item widget provided) save
// everything.
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
save;
// Import callback.
// Read content from 'data'. 'selectedIds' contains the item IDs the
// user explicitly checked; if empty import every item found in 'data'.
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
load;
// Optional: enumerate items available for export as (id, displayName)
// pairs. When nullptr the extension appears as a single all-or-nothing
// checkbox in the export dialog.
std::function<QList<QPair<QString, QString>>()> getExportItems =
nullptr;
};
EXPORT void AddMacroExportExtension(const MacroExportExtension &ext);
const std::vector<MacroExportExtension> &GetMacroExportExtensions();
} // namespace advss

View File

@@ -1,22 +1,160 @@
#include "macro-export-import-dialog.hpp"
#include "macro-export-extensions.hpp"
#include "obs-module-helper.hpp"
#include <obs.hpp>
#include <QLayout>
#include <QLabel>
#include <QDialogButtonBox>
#include <QLabel>
#include <QLayout>
#include <algorithm>
#include <numeric>
namespace advss {
static bool usePlainText = false;
MacroExportImportDialog::MacroExportImportDialog(Type type)
// ---------------------------------------------------------------------------
// Compress / decompress helpers
// ---------------------------------------------------------------------------
static QString compressMacroString(const QString &input)
{
QByteArray inputData = input.toUtf8();
auto compressedData = qCompress(inputData);
QByteArray encodedData = compressedData.toBase64();
return QString::fromUtf8(encodedData);
}
static QString decompressMacroString(const QString &input)
{
QByteArray encodedData = input.toUtf8();
QByteArray compressedData = QByteArray::fromBase64(encodedData);
auto outputData = qUncompress(compressedData);
return QString::fromUtf8(outputData);
}
static bool isValidData(const QString &json)
{
OBSDataAutoRelease data =
obs_data_create_from_json(json.toStdString().c_str());
return !!data;
}
// ---------------------------------------------------------------------------
// Extension section
// ---------------------------------------------------------------------------
Section *MacroExportImportDialog::BuildExtensionSection()
{
auto outerSection = new Section(300, this);
outerSection->AddHeaderWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.macroTab.export.additionalContent")));
auto outerContent = new QWidget();
auto outerLayout = new QVBoxLayout(outerContent);
outerLayout->setContentsMargins(4, 4, 4, 4);
outerLayout->setSpacing(4);
const auto &extensions = GetMacroExportExtensions();
// Build a sorted index list (alphabetical by translated display name).
std::vector<int> order(extensions.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](int a, int b) {
return QString::localeAwareCompare(
obs_module_text(extensions[a].displayNameKey),
obs_module_text(extensions[b].displayNameKey)) <
0;
});
for (int extIdx : order) {
const auto &ext = extensions[extIdx];
ExtensionUI ui;
// Simple all-or-nothing checkbox (no sub-items).
if (!ext.getExportItems) {
ui.mainCheck = new QCheckBox(
obs_module_text(ext.displayNameKey),
outerContent);
ui.mainCheck->setChecked(false);
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
&MacroExportImportDialog::UpdateExportString);
outerLayout->addWidget(ui.mainCheck);
_extensionUIs.append(std::move(ui));
continue;
}
// Extensions with per-item selection get their own inner
// Section: the main checkbox sits in the header (next to the
// toggle arrow) and the per-item checkboxes live in the
// collapsible content area.
const auto items = ext.getExportItems();
if (items.isEmpty()) {
continue;
}
auto innerSection = new Section(200, outerContent);
ui.mainCheck =
new QCheckBox(obs_module_text(ext.displayNameKey));
ui.mainCheck->setChecked(false);
innerSection->AddHeaderWidget(ui.mainCheck);
auto subWidget = new QWidget();
auto subLayout = new QVBoxLayout(subWidget);
subLayout->setContentsMargins(4, 2, 4, 2);
subLayout->setSpacing(2);
for (const auto &[id, displayName] : items) {
auto cb = new QCheckBox(displayName, subWidget);
cb->setChecked(false);
subLayout->addWidget(cb);
ui.itemChecks.append({id, cb});
connect(cb, &QCheckBox::stateChanged, this,
&MacroExportImportDialog::UpdateExportString);
}
// Main checkbox toggles all sub-items.
// Capture uiIdx (position in _extensionUIs after append).
const int uiIdx = _extensionUIs.size();
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
[this, uiIdx](int state) {
if (state == Qt::PartiallyChecked)
return;
const bool checked = (state == Qt::Checked);
if (uiIdx < _extensionUIs.size()) {
for (auto &[id, cb] :
_extensionUIs[uiIdx].itemChecks) {
QSignalBlocker b(cb);
cb->setChecked(checked);
}
}
UpdateExportString();
});
innerSection->SetContent(subWidget, false);
outerLayout->addWidget(innerSection);
_extensionUIs.append(std::move(ui));
}
outerSection->SetContent(outerContent, true);
return outerSection;
}
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
MacroExportImportDialog::MacroExportImportDialog(Type type,
const QString &baseJson)
: QDialog(nullptr),
_baseJson(baseJson),
_importExportString(new QPlainTextEdit(this)),
_usePlainText(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.macroTab.export.usePlainText")))
{
_importExportString->setReadOnly(type == Type::EXPORT_MACRO);
auto label = new QLabel(obs_module_text(
type == Type::EXPORT_MACRO
? "AdvSceneSwitcher.macroTab.export.info"
@@ -39,7 +177,12 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
connect(_usePlainText, &QCheckBox::stateChanged, this,
&MacroExportImportDialog::UsePlainTextChanged);
auto layout = new QVBoxLayout;
auto layout = new QVBoxLayout(this);
if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) {
layout->addWidget(BuildExtensionSection());
}
layout->addWidget(label);
layout->addWidget(_importExportString);
layout->addWidget(_usePlainText);
@@ -47,32 +190,57 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
setLayout(layout);
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
if (type == Type::EXPORT_MACRO) {
RefreshExportText();
}
}
QString compressMacroString(const QString &input)
// ---------------------------------------------------------------------------
// Export string helpers
// ---------------------------------------------------------------------------
QString MacroExportImportDialog::BuildExportJson() const
{
QByteArray inputData = input.toUtf8();
auto compressedData = qCompress(inputData);
QByteArray encodedData = compressedData.toBase64();
return QString::fromUtf8(encodedData);
OBSDataAutoRelease data =
obs_data_create_from_json(_baseJson.toStdString().c_str());
if (!data) {
return _baseJson;
}
const auto &extensions = GetMacroExportExtensions();
for (int i = 0;
i < (int)extensions.size() && i < (int)_extensionUIs.size(); ++i) {
if (!_extensionUIs[i].mainCheck->isChecked()) {
continue;
}
QStringList selectedIds;
for (const auto &[id, cb] : _extensionUIs[i].itemChecks) {
if (cb->isChecked()) {
selectedIds << id;
}
}
extensions[i].save(data, selectedIds);
}
const char *json = obs_data_get_json(data);
return json ? QString::fromUtf8(json) : QString{};
}
QString decompressMacroString(const QString &input)
void MacroExportImportDialog::RefreshExportText()
{
QByteArray encodedData = input.toUtf8();
QByteArray compressedData = QByteArray::fromBase64(encodedData);
auto outputData = qUncompress(compressedData);
return QString::fromUtf8(outputData);
const QString json = BuildExportJson();
if (usePlainText) {
_importExportString->setPlainText(json);
} else {
_importExportString->setPlainText(compressMacroString(json));
}
}
void MacroExportImportDialog::ExportMacros(const QString &json)
void MacroExportImportDialog::UpdateExportString()
{
MacroExportImportDialog dialog(
MacroExportImportDialog::Type::EXPORT_MACRO);
dialog._importExportString->setPlainText(compressMacroString(json));
dialog.adjustSize();
dialog.updateGeometry();
dialog.exec();
RefreshExportText();
}
void MacroExportImportDialog::UsePlainTextChanged(int value)
@@ -90,26 +258,45 @@ void MacroExportImportDialog::UsePlainTextChanged(int value)
usePlainText = value;
}
static bool isValidData(const QString &json)
// ---------------------------------------------------------------------------
// Static entry points
// ---------------------------------------------------------------------------
void MacroExportImportDialog::ExportMacros(obs_data_t *macroData)
{
OBSDataAutoRelease data =
obs_data_create_from_json(json.toStdString().c_str());
return !!data;
const char *rawJson = obs_data_get_json(macroData);
const QString baseJson = rawJson ? QString::fromUtf8(rawJson)
: QString{};
MacroExportImportDialog dialog(Type::EXPORT_MACRO, baseJson);
dialog.adjustSize();
dialog.updateGeometry();
dialog.exec();
}
bool MacroExportImportDialog::ImportMacros(QString &json)
{
MacroExportImportDialog dialog(
MacroExportImportDialog::Type::IMPORT_MACRO);
if (dialog.exec() == QDialog::Accepted) {
json = decompressMacroString(
dialog._importExportString->toPlainText());
if (!isValidData(json)) { // Fallback to support raw json format
json = dialog._importExportString->toPlainText();
}
return true;
MacroExportImportDialog dialog(Type::IMPORT_MACRO);
if (dialog.exec() != QDialog::Accepted) {
return false;
}
return false;
json = decompressMacroString(dialog._importExportString->toPlainText());
if (!isValidData(json)) {
// Fallback: support raw (uncompressed) JSON pasted directly.
json = dialog._importExportString->toPlainText();
}
// Invoke all extension load callbacks.
OBSDataAutoRelease data =
obs_data_create_from_json(json.toStdString().c_str());
if (data) {
for (const auto &ext : GetMacroExportExtensions()) {
ext.load(data, {});
}
}
return true;
}
} // namespace advss

View File

@@ -1,25 +1,47 @@
#pragma once
#include "section.hpp"
#include <QCheckBox>
#include <QDialog>
#include <QList>
#include <QPlainTextEdit>
#include <obs-data.h>
namespace advss {
class MacroExportImportDialog : public QDialog {
Q_OBJECT
public:
enum class Type { EXPORT_MACRO, IMPORT_MACRO };
MacroExportImportDialog(Type type);
static void ExportMacros(const QString &json);
static void ExportMacros(obs_data_t *macroData);
static bool ImportMacros(QString &json);
private slots:
void UsePlainTextChanged(int);
void UpdateExportString();
private:
explicit MacroExportImportDialog(Type type,
const QString &baseJson = {});
// Per-extension widgets
struct ExtensionUI {
QCheckBox *mainCheck = nullptr;
// (itemId, checkbox) pairs - empty when no per-item selection.
QList<QPair<QString, QCheckBox *>> itemChecks;
};
Section *BuildExtensionSection();
QString BuildExportJson() const;
void RefreshExportText();
QString _baseJson;
QPlainTextEdit *_importExportString;
QCheckBox *_usePlainText;
QList<ExtensionUI> _extensionUIs;
};
} // namespace advss

View File

@@ -214,6 +214,20 @@ bool MacroIsPaused(const Macro *macro)
return macro ? macro->Paused() : true;
}
void SetMacroPaused(Macro *macro, bool paused)
{
if (macro) {
macro->SetPaused(paused);
}
}
void StopMacro(Macro *macro)
{
if (macro) {
macro->Stop();
}
}
bool MacroWasPausedSince(
const Macro *macro,
const std::chrono::high_resolution_clock::time_point &time)
@@ -237,9 +251,20 @@ void AddMacroHelperThread(Macro *macro, std::thread &&newThread)
macro->AddHelperThread(std::move(newThread));
}
bool RunMacroActions(Macro *macro)
bool RunMacroActions(Macro *macro, bool forceParallel, bool ignorePause)
{
return macro && macro->PerformActions(true);
return macro && macro->PerformActions(true, forceParallel, ignorePause);
}
bool RunMacroElseActions(Macro *macro, bool forceParallel, bool ignorePause)
{
return macro &&
macro->PerformActions(false, forceParallel, ignorePause);
}
bool CheckMacroConditions(Macro *macro, bool ignorePause)
{
return macro && macro->CheckConditions(ignorePause);
}
void ResetMacroConditionTimers(Macro *macro)

View File

@@ -8,7 +8,6 @@
#include <condition_variable>
#include <deque>
#include <optional>
#include <string_view>
#include <thread>
struct obs_data;
@@ -59,6 +58,8 @@ LastMacroConditionCheckTime(const Macro *);
EXPORT bool MacroIsStopped(const Macro *);
EXPORT bool MacroIsPaused(const Macro *);
EXPORT void SetMacroPaused(Macro *, bool paused);
EXPORT void StopMacro(Macro *);
EXPORT bool
MacroWasPausedSince(const Macro *,
const std::chrono::high_resolution_clock::time_point &);
@@ -67,9 +68,14 @@ EXPORT bool MacroWasCheckedSinceLastStart(const Macro *);
EXPORT void AddMacroHelperThread(Macro *, std::thread &&);
EXPORT bool CheckMacros();
EXPORT bool CheckMacroConditions(Macro *, bool ignorePause = false);
EXPORT bool RunMacroActions(Macro *);
EXPORT bool RunMacroActions(Macro *, bool forceParallel = false,
bool ignorePause = false);
EXPORT bool RunMacroElseActions(Macro *, bool forceParallel = false,
bool ignorePause = false);
EXPORT bool RunMacros();
void StopAllMacros();
EXPORT void LoadMacros(obs_data_t *obj);

View File

@@ -1,6 +1,8 @@
#include "macro-ref.hpp"
#include "macro.hpp"
#include "plugin-state-helpers.hpp"
namespace advss {
MacroRef::MacroRef(std::string name)
@@ -19,6 +21,7 @@ void MacroRef::Load(obs_data_t *obj)
auto name = obs_data_get_string(obj, "macro");
_postLoadName = name;
_macro = GetWeakMacroByName(name);
AddPostLoadStep([this]() { MacroRef::PostLoad(); });
}
void MacroRef::PostLoad()

View File

@@ -72,15 +72,24 @@ void MacroEdit::PasteMacroSegment()
}
AddMacroCondition(macro.get(), macro->Conditions().size(),
copyInfo.segment->GetId(), data.Get(), logic);
if (!macro->Conditions().empty()) {
macro->Conditions().back()->FixupVarMappingRefs();
}
break;
}
case MacroSegmentCopyInfo::Type::ACTION:
AddMacroAction(macro.get(), macro->Actions().size(),
copyInfo.segment->GetId(), data.Get());
if (!macro->Actions().empty()) {
macro->Actions().back()->FixupVarMappingRefs();
}
break;
case MacroSegmentCopyInfo::Type::ELSE:
AddMacroElseAction(macro.get(), macro->ElseActions().size(),
copyInfo.segment->GetId(), data.Get());
if (!macro->ElseActions().empty()) {
macro->ElseActions().back()->FixupVarMappingRefs();
}
break;
default:
break;

View File

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

View File

@@ -9,15 +9,23 @@
#include <QWidget>
#include <QFrame>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTimer>
#include <obs-data.h>
#include <memory>
class QLabel;
namespace advss {
class Macro;
class Variable;
struct VarMapping {
TempVariableRef tempVar;
std::weak_ptr<Variable> variable;
};
class EXPORT MacroSegment : public Lockable {
public:
@@ -28,12 +36,15 @@ public:
int GetIndex() const { return _idx; }
void SetCollapsed(bool collapsed) { _collapsed = collapsed; }
bool GetCollapsed() const { return _collapsed; }
void SetVarMappingExpanded(bool expanded);
bool GetVarMappingExpanded() const { return _varMappingExpanded; }
void SetUseCustomLabel(bool enable) { _useCustomLabel = enable; }
bool GetUseCustomLabel() const { return _useCustomLabel; }
void SetCustomLabel(const std::string &label) { _customLabel = label; }
std::string GetCustomLabel() const { return _customLabel; }
virtual bool Save(obs_data_t *obj) const = 0;
virtual bool Load(obs_data_t *obj) = 0;
virtual std::vector<TempVariableRef> GetTempVarRefs() const;
virtual bool PostLoad();
virtual std::string GetShortDesc() const;
virtual std::string GetId() const = 0;
@@ -42,6 +53,11 @@ public:
void SetEnabled(bool);
bool Enabled() const;
virtual std::string GetVariableValue() const;
void ApplyVarMappings();
const std::vector<VarMapping> &GetVarMappings() const;
void SetVarMappings(const std::vector<VarMapping> &mappings);
std::vector<TempVariable> GetOwnTempVars() const;
void FixupVarMappingRefs();
protected:
friend bool SupportsVariableValue(MacroSegment *);
@@ -54,6 +70,7 @@ protected:
void AddTempvar(const std::string &id, const std::string &name,
const std::string &description = "");
bool IsTempVarInUse(const std::string &id) const;
void SetTempVarValue(const std::string &id, const std::string &value);
template<typename T, typename = std::enable_if_t<
@@ -64,6 +81,14 @@ protected:
: 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:
void ClearAvailableTempvars();
std::optional<const TempVariable>
@@ -77,6 +102,7 @@ private:
// UI helper
bool _highlight = false;
bool _collapsed = false;
bool _varMappingExpanded = false;
bool _enabled = true;
// Custom header labels
@@ -89,6 +115,7 @@ private:
int _variableRefs = 0;
std::string _variableValue;
std::vector<TempVariable> _tempVariables;
std::vector<VarMapping> _varMappings;
friend class Macro;
};
@@ -108,8 +135,11 @@ public:
virtual std::shared_ptr<MacroSegment> Data() const = 0;
virtual void SetupWidgets(bool basicSetup = false) = 0;
void SetupVarMappings(MacroSegment *segment);
public slots:
void HeaderInfoChanged(const QString &);
void ShowVariableMappings(bool show);
protected slots:
void Collapsed(bool) const;
@@ -124,6 +154,8 @@ protected:
QLabel *_headerInfo;
QWidget *_frame;
QVBoxLayout *_contentLayout;
TempVarOutputMappingsWidget *_outputMappings;
QPushButton *_varMappingToggle;
bool _allWidgetsAreSetup = false;
private:

View File

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

View File

@@ -18,6 +18,7 @@ signals:
void Remove(const QString &);
void SegmentOrderChanged();
void HighlightChanged(bool value);
void ConditionTypeCreated(const QString &typeId);
private:
MacroSignalManager(QObject *parent = nullptr);

View File

@@ -1,5 +1,4 @@
#include "advanced-scene-switcher.hpp"
#include "action-queue.hpp"
#include "macro-action-edit.hpp"
#include "macro-condition-edit.hpp"
#include "macro-export-import-dialog.hpp"
@@ -25,6 +24,8 @@ namespace advss {
static QObject *addPulse = nullptr;
static QTimer onChangeHighlightTimer;
static std::chrono::high_resolution_clock::time_point
lastOnChangeHighlightCheckTime{};
static void disableAddButtonHighlight()
{
@@ -314,13 +315,9 @@ void AdvSceneSwitcher::ExportMacros() const
obs_data_array_push_back(macroArray, obj);
}
obs_data_set_array(data, "macros", macroArray);
SaveVariables(data);
SaveActionQueues(data);
obs_data_set_string(data, "version", g_GIT_TAG);
auto json = obs_data_get_json(data);
QString exportString(json);
MacroExportImportDialog::ExportMacros(exportString);
MacroExportImportDialog::ExportMacros(data);
}
bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
@@ -382,8 +379,6 @@ void AdvSceneSwitcher::ImportMacros()
ImportMacros();
return;
}
ImportVariables(data);
ImportQueues(data);
auto version = obs_data_get_string(data, "version");
if (strcmp(version, g_GIT_TAG) != 0) {
@@ -472,14 +467,17 @@ void AdvSceneSwitcher::on_runMacroInParallel_stateChanged(int value) const
macro->SetRunInParallel(value);
}
void AdvSceneSwitcher::on_runMacroOnChange_stateChanged(int value) const
void AdvSceneSwitcher::on_actionTriggerMode_currentIndexChanged(int index) const
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
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
@@ -487,7 +485,7 @@ void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable) const
ui->macroName->setDisabled(disable);
ui->runMacro->setDisabled(disable);
ui->runMacroInParallel->setDisabled(disable);
ui->runMacroOnChange->setDisabled(disable);
ui->actionTriggerMode->setDisabled(disable);
ui->macroEdit->SetControlsDisabled(disable);
}
@@ -517,10 +515,12 @@ void AdvSceneSwitcher::MacroSelectionChanged()
{
const QSignalBlocker b1(ui->macroName);
const QSignalBlocker b2(ui->runMacroInParallel);
const QSignalBlocker b3(ui->runMacroOnChange);
const QSignalBlocker b3(ui->actionTriggerMode);
ui->macroName->setText(macro->Name().c_str());
ui->runMacroInParallel->setChecked(macro->RunInParallel());
ui->runMacroOnChange->setChecked(macro->MatchOnChange());
ui->actionTriggerMode->setCurrentIndex(
ui->actionTriggerMode->findData(static_cast<int>(
macro->GetActionTriggerMode())));
}
macro->ResetUIHelpers();
@@ -550,10 +550,18 @@ void AdvSceneSwitcher::HighlightOnChange() const
return;
}
if (macro->OnChangePreventedActionsRecently()) {
HighlightWidget(ui->runMacroOnChange, Qt::yellow,
if (macro->Paused()) {
return;
}
if (macro->ActionTriggerModePreventedActionsSince(
lastOnChangeHighlightCheckTime)) {
HighlightWidget(ui->actionTriggerMode, Qt::yellow,
Qt::transparent, true);
}
lastOnChangeHighlightCheckTime =
std::chrono::high_resolution_clock::now();
}
void AdvSceneSwitcher::on_macroSettings_clicked()
@@ -652,28 +660,56 @@ void AdvSceneSwitcher::SetupMacroTab()
ui->macroPriorityWarning->setVisible(
switcher->functionNamesByPriority[0] != macro_func);
lastOnChangeHighlightCheckTime =
std::chrono::high_resolution_clock::now();
onChangeHighlightTimer.setInterval(1500);
connect(&onChangeHighlightTimer, SIGNAL(timeout()), this,
SLOT(HighlightOnChange()));
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,
ui->macroSearchClear, ui->macroSearchType,
ui->macroSearchRegex,
ui->macroSearchShowSettings,
[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)

View File

@@ -5,18 +5,17 @@
#include "path-helpers.hpp"
#include "sync-helpers.hpp"
#include "ui-helpers.hpp"
#include "utility.hpp"
#include <obs.h>
#include <string>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QStylePainter>
#include <QToolTip>
Q_DECLARE_METATYPE(std::shared_ptr<advss::Macro>);
@@ -62,12 +61,11 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr<Macro> macroItem,
_boxLayout = new QHBoxLayout();
_boxLayout->setContentsMargins(0, 0, 0, 0);
_boxLayout->addWidget(_running);
if (isGroup) {
_boxLayout->addWidget(_iconLabel);
_boxLayout->addSpacing(2);
_running->hide();
}
_boxLayout->addWidget(_running);
_boxLayout->addWidget(_label);
#ifdef __APPLE__
/* Hack: Fixes a bug where scrollbars would be above the lock icon */
@@ -77,29 +75,138 @@ MacroTreeItem::MacroTreeItem(MacroTree *tree, std::shared_ptr<Macro> macroItem,
Update(true);
setLayout(_boxLayout);
auto setRunning = [this](bool val) {
_macro->SetPaused(!val);
};
connect(_running, &QAbstractButton::clicked, setRunning);
connect(_running, SIGNAL(clicked(bool)), this,
SLOT(RunningClicked(bool)));
connect(MacroSignalManager::Instance(), SIGNAL(HighlightChanged(bool)),
this, SLOT(EnableHighlight(bool)));
connect(MacroSignalManager::Instance(),
SIGNAL(Rename(const QString &, const QString &)), this,
SLOT(MacroRenamed(const QString &, const QString &)));
connect(&_timer, SIGNAL(timeout()), this, SLOT(HighlightIfExecuted()));
connect(&_timer, SIGNAL(timeout()), this, SLOT(UpdatePaused()));
connect(&_timer, SIGNAL(timeout()), this, SLOT(UpdateRunning()));
UpdateRunning();
_timer.start(1500);
}
bool MacroTreeItem::event(QEvent *event)
{
if (event->type() != QEvent::ToolTip) {
return QFrame::event(event);
}
if (_macro->IsGroup()) {
return true;
}
QString text;
if (!_macro->WasExecutedSince({})) {
text = obs_module_text(
"AdvSceneSwitcher.macroTab.macroNotYetExecutedTooltip");
} else {
const QString formatStr = obs_module_text(
"AdvSceneSwitcher.macroTab.macroLastExecutedTooltip");
const auto secondsSinceLastRun =
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::high_resolution_clock::now() -
_macro->GetLastExecutionTime());
text = formatStr.arg(secondsSinceLastRun.count());
}
auto helpEvent = static_cast<QHelpEvent *>(event);
QToolTip::showText(helpEvent->globalPos(), text, this);
return true;
}
void MacroTreeItem::EnableHighlight(bool enable)
{
_highlight = enable;
}
void MacroTreeItem::UpdatePaused()
void MacroTreeItem::RunningClicked(bool running)
{
const auto updateWidget = [this](Macro *macro) {
if (!macro) {
return;
}
const auto &macros = GetTopLevelMacros();
bool found = false;
int idx = 0;
for (const auto &m : macros) {
if (m.get() == macro) {
found = true;
break;
}
idx++;
}
if (!found) {
return;
}
const auto widget = _tree->GetItemWidget(idx);
if (widget) {
widget->UpdateRunning();
}
};
if (!_macro->IsGroup()) {
_macro->SetPaused(!running);
if (!_macro->IsSubitem()) {
return;
}
const auto group = _macro->Parent();
updateWidget(group.get());
return;
}
const auto macros = GetGroupMacroEntries(_macro.get());
for (const auto &macro : macros) {
macro->SetPaused(!running);
}
// Update backend values before updating UI to prevent flickering in
// running state of the group
for (const auto &macro : macros) {
updateWidget(macro.get());
}
UpdateRunning();
}
void MacroTreeItem::UpdateRunning()
{
const QSignalBlocker blocker(_running);
_running->setChecked(!_macro->Paused());
if (!_macro->IsGroup()) {
_running->setChecked(!_macro->Paused());
return;
}
const auto macros = GetGroupMacroEntries(_macro.get());
bool allRunning = true;
bool allPaused = true;
for (const auto &macro : macros) {
if (macro->Paused()) {
allRunning = false;
} else {
allPaused = false;
}
}
if (allRunning) {
_running->setCheckState(Qt::Checked);
return;
}
if (allPaused) {
_running->setCheckState(Qt::Unchecked);
return;
}
_running->setCheckState(Qt::PartiallyChecked);
}
void MacroTreeItem::HighlightIfExecuted()
@@ -108,10 +215,20 @@ void MacroTreeItem::HighlightIfExecuted()
return;
}
bool wasHighlighted = false;
if (_lastHighlightCheckTime.time_since_epoch().count() != 0 &&
_macro->WasExecutedSince(_lastHighlightCheckTime)) {
HighlightWidget(this, Qt::green, QColor(0, 0, 0, 0), true);
wasHighlighted = true;
}
if (!wasHighlighted &&
_lastHighlightCheckTime.time_since_epoch().count() != 0 &&
_macro->ActionTriggerModePreventedActionsSince(
_lastHighlightCheckTime)) {
HighlightWidget(this, Qt::yellow, QColor(0, 0, 0, 0), true);
}
_lastHighlightCheckTime = std::chrono::high_resolution_clock::now();
}
@@ -188,7 +305,7 @@ void MacroTreeItem::Update(bool force)
_expand->blockSignals(true);
_expand->setChecked(_macro->IsCollapsed());
_expand->blockSignals(false);
connect(_expand, &QPushButton::toggled, this,
connect(_expand, &QCheckBox::toggled, this,
&MacroTreeItem::ExpandClicked);
} else {
_spacer = new QSpacerItem(3, 1);
@@ -214,7 +331,6 @@ void MacroTreeModel::Reset(std::deque<std::shared_ptr<Macro>> &newItems)
_macros = newItems;
endResetModel();
UpdateGroupState(false);
_mt->ResetWidgets();
}
@@ -459,9 +575,6 @@ void MacroTreeModel::Remove(std::shared_ptr<Macro> item)
_mt->selectionModel()->clear();
if (isGroup) {
UpdateGroupState(true);
}
assert(IsInValidState());
}
@@ -550,7 +663,6 @@ MacroTreeModel::MacroTreeModel(MacroTree *st_,
_mt(st_),
_macros(macros)
{
UpdateGroupState(false);
}
int MacroTreeModel::rowCount(const QModelIndex &parent) const
@@ -704,7 +816,6 @@ void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices)
offset++;
}
_hasGroups = true;
_mt->selectionModel()->clear();
Reset(_macros);
@@ -762,24 +873,6 @@ void MacroTreeModel::CollapseGroup(std::shared_ptr<Macro> item)
assert(IsInValidState());
}
void MacroTreeModel::UpdateGroupState(bool update)
{
bool nowHasGroups = false;
for (auto &item : _macros) {
if (item->IsGroup()) {
nowHasGroups = true;
break;
}
}
if (nowHasGroups != _hasGroups) {
_hasGroups = nowHasGroups;
if (update) {
_mt->UpdateWidgets(true);
}
}
}
void MacroTree::Reset(std::deque<std::shared_ptr<Macro>> &macros,
bool highlight)
{
@@ -842,7 +935,6 @@ MacroTree::MacroTree(QWidget *parent_) : QListView(parent_)
void MacroTree::ResetWidgets()
{
MacroTreeModel *mtm = GetModel();
mtm->UpdateGroupState(false);
int modelIdx = 0;
for (int i = 0; i < (int)mtm->_macros.size(); i++) {
QModelIndex index = mtm->createIndex(modelIdx, 0, nullptr);

View File

@@ -34,10 +34,14 @@ public:
explicit MacroTreeItem(MacroTree *tree, std::shared_ptr<Macro> macro,
bool highlight);
protected:
bool event(QEvent *) override;
private slots:
void ExpandClicked(bool checked);
void EnableHighlight(bool enable);
void UpdatePaused();
void RunningClicked(bool);
void UpdateRunning();
void HighlightIfExecuted();
void MacroRenamed(const QString &, const QString &);
@@ -106,7 +110,6 @@ private:
void UngroupSelectedGroups(QModelIndexList &indices);
void ExpandGroup(std::shared_ptr<Macro> item);
void CollapseGroup(std::shared_ptr<Macro> item);
void UpdateGroupState(bool update);
int GetItemMacroIndex(const std::shared_ptr<Macro> &item) const;
int GetItemModelIndex(const std::shared_ptr<Macro> &item) const;
bool IsLastItem(std::shared_ptr<Macro> item) const;
@@ -114,7 +117,6 @@ private:
MacroTree *_mt;
std::deque<std::shared_ptr<Macro>> &_macros;
bool _hasGroups = false;
friend class MacroTree;
friend class MacroTreeItem;

View File

@@ -0,0 +1,111 @@
#include "websocket-api.hpp"
#include "log-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "variable.hpp"
#include "macro-helpers.hpp"
#include <obs.hpp>
namespace advss {
static bool setup();
static bool setupDone = setup();
static void receiveRunMacroMessage(obs_data_t *data, obs_data_t *);
static void receiveSetVariablesMessage(obs_data_t *data, obs_data_t *);
static const char *runMacroMessage = "AdvancedSceneSwitcherRunMacro";
static const char *setVariablesMessage = "AdvancedSceneSwitcherSetVariables";
static bool setup()
{
AddPluginInitStep([]() {
RegisterWebsocketRequest(runMacroMessage,
receiveRunMacroMessage);
RegisterWebsocketRequest(setVariablesMessage,
receiveSetVariablesMessage);
});
return true;
}
static void setVariables(obs_data_t *data)
{
OBSDataArrayAutoRelease variables =
obs_data_get_array(data, "variables");
size_t count = obs_data_array_count(variables);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease item = obs_data_array_item(variables, i);
if (!obs_data_has_user_value(item, "name")) {
blog(LOG_INFO,
"ignoring invalid variable entry in \"%s\" websocket message (missing \"name\")",
runMacroMessage);
continue;
}
if (!obs_data_has_user_value(item, "value")) {
blog(LOG_INFO,
"ignoring invalid variable entry in \"%s\" websocket message (missing \"value\")",
runMacroMessage);
continue;
}
auto varName = obs_data_get_string(item, "name");
auto value = obs_data_get_string(item, "value");
auto weakVar = GetWeakVariableByName(varName);
auto var = weakVar.lock();
if (!var) {
blog(LOG_INFO,
"ignoring invalid variable name \"%s\" in \"%s\" websocket message",
varName, runMacroMessage);
continue;
}
var->SetValue(value);
}
}
static void receiveRunMacroMessage(obs_data_t *data, obs_data_t *)
{
if (!obs_data_has_user_value(data, "name")) {
blog(LOG_INFO,
"ignoring invalid \"%s\" websocket message (missing \"name\")",
runMacroMessage);
return;
}
auto name = obs_data_get_string(data, "name");
const auto weakMacro =
GetWeakMacroByName(obs_data_get_string(data, "name"));
const auto macro = weakMacro.lock();
if (!macro) {
blog(LOG_INFO,
"ignoring invalid \"%s\" websocket message (macro \"%s\") not found",
runMacroMessage, name);
return;
}
if (obs_data_has_user_value(data, "variables")) {
setVariables(data);
}
const bool runElse = obs_data_get_bool(data, "runElseActions");
if (runElse) {
RunMacroElseActions(macro.get());
} else {
RunMacroActions(macro.get());
}
}
static void receiveSetVariablesMessage(obs_data_t *data, obs_data_t *)
{
if (!obs_data_has_user_value(data, "variables")) {
blog(LOG_INFO,
"ignoring invalid \"%s\" websocket message (missing \"variables\")",
setVariablesMessage);
return;
}
setVariables(data);
}
} // namespace advss

View File

@@ -17,6 +17,17 @@
namespace advss {
static bool setup()
{
AddPluginCleanupStep([]() {
GetTopLevelMacros().clear();
GetTemporaryMacros().clear();
});
return true;
}
static bool setupDone = setup();
Macro::Macro(const std::string &name) : _dockSettings(this)
{
SetName(name);
@@ -111,18 +122,18 @@ static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
const auto startTime = std::chrono::high_resolution_clock::now();
bool conditionMatched = false;
condition->WithLock([&condition, &conditionMatched]() {
conditionMatched = condition->CheckCondition();
conditionMatched = condition->EvaluateCondition();
});
condition->ApplyVarMappings();
const auto endTime = std::chrono::high_resolution_clock::now();
const auto timeSpent = endTime - startTime;
if (timeSpent >= perfLogThreshold) {
const long int ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
timeSpent)
.count();
blog(LOG_WARNING,
"spent %ld ms in %s condition check of macro '%s'!", ms,
"spent %lld ms in %s condition check of macro '%s'!",
(long long)std::chrono::duration_cast<
std::chrono::milliseconds>(timeSpent)
.count(),
condition->GetId().c_str(),
condition->GetMacro()->Name().c_str());
}
@@ -145,6 +156,15 @@ bool Macro::CheckConditionHelper(
return conditionMatched;
};
if (!condition->Enabled()) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
condition->GetId().c_str(), _name.c_str());
if (!_useShortCircuitEvaluation) {
(void)evaluateCondition();
}
return _matched;
}
const auto logicType = condition->GetLogicType();
if (logicType == Logic::Type::NONE) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
@@ -241,9 +261,37 @@ bool Macro::CheckConditions(bool ignorePause)
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
_conditionSateChanged = _lastMatched != _matched;
if (!_conditionSateChanged && _performActionsOnChange) {
_onPreventedActionExecution = true;
_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)
: (_elseActions.size() > 0);
if (!_actionModeMatch && hasActionsToExecute && !_paused) {
_lastActionRunModePreventTime =
std::chrono::high_resolution_clock::now();
}
_lastMatched = _matched;
@@ -302,6 +350,16 @@ bool Macro::WasExecutedSince(const TimePoint &time) const
return _lastExecutionTime > time;
}
bool Macro::ActionTriggerModePreventedActionsSince(const TimePoint &time) const
{
return _lastActionRunModePreventTime > time;
}
Macro::TimePoint Macro::GetLastExecutionTime() const
{
return _lastExecutionTime;
}
bool Macro::ConditionsShouldBeChecked() const
{
if (!_useCustomConditionCheckInterval) {
@@ -328,10 +386,9 @@ bool Macro::ShouldRunActions() const
const bool hasActionsToExecute =
!_paused && (_matched || _elseActions.size() > 0) &&
(!_performActionsOnChange || _conditionSateChanged);
_actionModeMatch;
if (VerboseLoggingEnabled() && _performActionsOnChange &&
!_conditionSateChanged) {
if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
if (_matched && _actions.size() > 0) {
blog(LOG_INFO, "skip actions for Macro %s (on change)",
_name.c_str());
@@ -362,10 +419,24 @@ void Macro::ResetTimers()
_lastExecutionTime = {};
}
void Macro::SetActionTriggerMode(ActionTriggerMode mode)
{
_actionTriggerMode = mode;
}
Macro::ActionTriggerMode Macro::GetActionTriggerMode() const
{
return _actionTriggerMode;
}
bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
bool ignorePause)
{
if (_paused && !ignorePause) {
return true;
}
// Create copy of action list as elements might be removed, inserted, or
// reordered while actions are currently being executed.
auto actions = actionsToRun;
@@ -381,6 +452,7 @@ bool Macro::RunActionsHelper(
action->WithLock([&action, &actionResult]() {
actionResult = action->PerformAction();
});
action->ApplyVarMappings();
actionsExecutedSuccessfully =
actionsExecutedSuccessfully && actionResult;
} else {
@@ -415,11 +487,6 @@ bool Macro::WasPausedSince(const TimePoint &time) const
return _lastUnpauseTime > time;
}
void Macro::SetMatchOnChange(bool onChange)
{
_performActionsOnChange = onChange;
}
void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone)
{
_stopActionsIfNotDone = stopActionsIfNotDone;
@@ -740,7 +807,8 @@ bool Macro::Save(obs_data_t *obj, bool saveForCopy) const
obs_data_set_bool(obj, "pause", _paused);
obs_data_set_bool(obj, "parallel", _runInParallel);
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, "stopActionsIfNotDone", _stopActionsIfNotDone);
obs_data_set_bool(obj, "useShortCircuitEvaluation",
@@ -826,7 +894,15 @@ bool Macro::Load(obs_data_t *obj)
}
_runInParallel = obs_data_get_bool(obj, "parallel");
_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");
_stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone");
_useShortCircuitEvaluation =
@@ -986,18 +1062,8 @@ bool Macro::HasValidSplitterPositions() const
!_elseActionSplitterPosition.empty();
}
bool Macro::OnChangePreventedActionsRecently()
{
if (_onPreventedActionExecution) {
_onPreventedActionExecution = false;
return _matched ? _actions.size() > 0 : _elseActions.size() > 0;
}
return false;
}
void Macro::ResetUIHelpers()
{
_onPreventedActionExecution = false;
for (auto c : _conditions) {
c->GetHighlightAndReset();
}
@@ -1096,9 +1162,11 @@ void Macro::ClearHotkeys() const
void setHotkeyDescriptionHelper(const char *formatModuleText,
const std::string name, const obs_hotkey_id id)
{
#ifndef UNIT_TEST
QString format{obs_module_text(formatModuleText)};
QString hotkeyDesc = format.arg(QString::fromStdString(name));
obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str());
#endif // !UNIT_TEST
}
void Macro::SetHotkeysDesc() const

View File

@@ -26,6 +26,16 @@ class Macro {
public:
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, const GlobalMacroSettings &settings);
@@ -54,8 +64,8 @@ public:
bool GetStop() const { return _stop; }
void ResetTimers();
void SetMatchOnChange(bool onChange);
bool MatchOnChange() const { return _performActionsOnChange; }
void SetActionTriggerMode(ActionTriggerMode);
ActionTriggerMode GetActionTriggerMode() const;
void SetSkipExecOnStart(bool skip) { _skipExecOnStart = skip; }
bool SkipExecOnStart() const { return _skipExecOnStart; }
@@ -137,7 +147,8 @@ public:
const QList<int> &GetElseActionSplitterPosition() const;
bool HasValidSplitterPositions() const;
bool WasExecutedSince(const TimePoint &) const;
bool OnChangePreventedActionsRecently();
bool ActionTriggerModePreventedActionsSince(const TimePoint &) const;
TimePoint GetLastExecutionTime() const;
void ResetUIHelpers();
// Hotkeys
@@ -168,6 +179,7 @@ private:
TimePoint _lastCheckTime{};
TimePoint _lastUnpauseTime{};
TimePoint _lastExecutionTime{};
TimePoint _lastActionRunModePreventTime{};
std::vector<std::thread> _helperThreads;
std::deque<std::shared_ptr<MacroCondition>> _conditions;
@@ -182,14 +194,13 @@ private:
bool _useShortCircuitEvaluation = false;
bool _useCustomConditionCheckInterval = false;
Duration _customConditionCheckInterval = 0.3;
bool _conditionSateChanged = false;
bool _actionModeMatch = false;
bool _runInParallel = false;
bool _checkInParallel = false;
bool _matched = false;
std::future<void> _conditionCheckFuture;
bool _lastMatched = false;
bool _performActionsOnChange = true;
bool _skipExecOnStart = false;
bool _stopActionsIfNotDone = false;
bool _paused = false;
@@ -199,14 +210,14 @@ private:
obs_hotkey_id _unpauseHotkey = OBS_INVALID_HOTKEY_ID;
obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID;
ActionTriggerMode _actionTriggerMode =
ActionTriggerMode::MACRO_RESULT_CHANGED;
PauseStateSaveBehavior _pauseSaveBehavior =
PauseStateSaveBehavior::PERSIST;
MacroInputVariables _inputVariables;
// UI helpers
bool _onPreventedActionExecution = false;
QList<int> _actionConditionSplitterPosition;
QList<int> _elseActionSplitterPosition;

View File

@@ -6,6 +6,7 @@
#import <Carbon/Carbon.h>
#include <Carbon/Carbon.h>
#include <util/platform.h>
#include <set>
#include <vector>
#include <QStringList>
#include <QRegularExpression>
@@ -14,56 +15,18 @@
namespace advss {
void GetWindowList(std::vector<std::string> &windows)
static std::string nsStringToStdString(NSString *str)
{
windows.resize(0);
@autoreleasepool {
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
for (NSDictionary *app in apps) {
std::string name([[app objectForKey:@"kCGWindowName"]
UTF8String],
[[app objectForKey:@"kCGWindowName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
std::string owner(
[[app objectForKey:@"kCGWindowOwnerName"]
UTF8String],
[[app objectForKey:@"kCGWindowOwnerName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
if (!name.empty() &&
find(windows.begin(), windows.end(), name) ==
windows.end()) {
windows.emplace_back(name);
}
if (!owner.empty() &&
find(windows.begin(), windows.end(), owner) ==
windows.end()) {
windows.emplace_back(owner);
}
}
apps = nil;
CFRelease(cfApps);
if (!str) {
return "";
}
const char *utf8 = [str UTF8String];
return utf8 ? utf8 : "";
}
void GetWindowList(QStringList &windows)
std::string GetCurrentWindowTitle()
{
windows.clear();
std::vector<std::string> temp;
GetWindowList(temp);
for (auto &w : temp) {
windows << QString::fromStdString(w);
}
}
void GetCurrentWindowTitle(std::string &title)
{
title.resize(0);
std::string title;
@autoreleasepool {
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
@@ -107,102 +70,52 @@ void GetCurrentWindowTitle(std::string &title)
apps = nil;
CFRelease(cfApps);
}
return title;
}
bool isWindowOriginOnScreen(NSDictionary *app, NSScreen *screen,
bool fullscreen = false)
{
NSArray *screens = [NSScreen screens];
NSRect mainScreenFrame = [screens[0] frame];
NSRect screenFrame;
if (fullscreen) {
screenFrame = [screen frame];
} else {
screenFrame = [screen visibleFrame];
}
NSRect windowBounds;
CGRectMakeWithDictionaryRepresentation(
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
&windowBounds);
return (windowBounds.origin.x == screenFrame.origin.x &&
(mainScreenFrame.size.height - screenFrame.size.height -
windowBounds.origin.y ==
screenFrame.origin.y));
}
bool isWindowMaximizedOnScreen(NSDictionary *app, NSScreen *screen)
{
double maximizedTolerance = 0.99;
NSRect screenFrame = [screen frame];
NSRect windowBounds;
CGRectMakeWithDictionaryRepresentation(
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
&windowBounds);
int sumX = windowBounds.origin.x + windowBounds.size.width;
int sumY = windowBounds.origin.y + windowBounds.size.height;
// CGWindowList: flipped coords, origin at top-left of main display.
// NSScreen: Cocoa coords, origin at bottom-left of main display.
NSRect mainFrame = [[NSScreen screens][0] frame];
NSRect visFrame = [screen visibleFrame];
// Return false if window spans over multiple screens
if (sumX > screenFrame.size.width) {
// Convert visible area to CGWindow coordinates.
// screenTop is just below the menubar; screenBottom is just above the
// dock.
CGFloat screenLeft = visFrame.origin.x;
CGFloat screenTop =
mainFrame.size.height - visFrame.origin.y - visFrame.size.height;
CGFloat screenRight = screenLeft + visFrame.size.width;
CGFloat screenBottom = screenTop + visFrame.size.height;
const double tolerance = 4.0;
// Width must span the full visible width.
if (fabs(windowBounds.origin.x - screenLeft) > tolerance) {
return false;
}
if (sumY > screenFrame.size.height) {
if (fabs((windowBounds.origin.x + windowBounds.size.width) -
screenRight) > tolerance) {
return false;
}
return ((double)sumX / (double)screenFrame.size.width) >
maximizedTolerance &&
((double)sumY / (double)screenFrame.size.height) >
maximizedTolerance;
}
bool nameMachesPattern(std::string windowName, std::string pattern)
{
return QString::fromStdString(windowName)
.contains(QRegularExpression(QString::fromStdString(pattern)));
}
bool IsMaximized(const std::string &title)
{
@autoreleasepool {
NSArray *screens = [NSScreen screens];
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
for (NSDictionary *app in apps) {
std::string name([[app objectForKey:@"kCGWindowName"]
UTF8String],
[[app objectForKey:@"kCGWindowName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
std::string owner(
[[app objectForKey:@"kCGWindowOwnerName"]
UTF8String],
[[app objectForKey:@"kCGWindowOwnerName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
bool equals = (title == name || title == owner);
bool matches = nameMachesPattern(name, title) ||
nameMachesPattern(owner, title);
if (equals || matches) {
for (NSScreen *screen in screens) {
if (isWindowOriginOnScreen(app,
screen) &&
isWindowMaximizedOnScreen(app,
screen)) {
apps = nil;
CFRelease(cfApps);
return true;
}
}
}
}
apps = nil;
CFRelease(cfApps);
// Bottom edge must align with the visible bottom (above dock).
if (fabs((windowBounds.origin.y + windowBounds.size.height) -
screenBottom) > tolerance) {
return false;
}
return false;
// Top edge must be at or above the menubar line (some apps extend
// their window frame behind the menubar).
if (windowBounds.origin.y > screenTop + tolerance) {
return false;
}
return true;
}
bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
@@ -216,52 +129,176 @@ bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
return NSEqualSizes(windowBounds.size, screenFrame.size);
}
bool IsFullscreen(const std::string &title)
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
{
std::vector<WindowInfo> result;
const std::string foregroundTitle = GetCurrentWindowTitle();
@autoreleasepool {
NSArray *screens = [NSScreen screens];
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
for (NSDictionary *app in apps) {
std::string name([[app objectForKey:@"kCGWindowName"]
UTF8String],
[[app objectForKey:@"kCGWindowName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
std::string owner(
[[app objectForKey:@"kCGWindowOwnerName"]
UTF8String],
[[app objectForKey:@"kCGWindowOwnerName"]
lengthOfBytesUsingEncoding:
NSUTF8StringEncoding]);
bool equals = (title == name || title == owner);
bool matches = nameMachesPattern(name, title) ||
nameMachesPattern(owner, title);
if (equals || matches) {
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
kCGWindowListExcludeDesktopElements, kCGNullWindowID);
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
// Pre-compute which PIDs own at least one fullscreen-sized window.
// A fullscreen app reports a small chrome/title bar window first and
// its full-size content window second; checking only the first
// window's bounds gives a false negative.
std::set<int> fullscreenPIDs;
if (options.fullscreen) {
for (NSDictionary *app in apps) {
int layer = [[app objectForKey:@"kCGWindowLayer"]
intValue];
if (layer != 0) {
continue;
}
for (NSScreen *screen in screens) {
if (isWindowOriginOnScreen(app, screen,
true) &&
isWindowFullscreenOnScreen(
app, screen)) {
apps = nil;
CFRelease(cfApps);
return true;
if (isWindowFullscreenOnScreen(app,
screen)) {
int pid = [[app objectForKey:
@"kCGWindowOwnerPID"]
intValue];
fullscreenPIDs.insert(pid);
break;
}
}
}
}
std::set<int> maximizedPIDs;
if (options.maximized) {
for (NSDictionary *app in apps) {
int layer = [[app objectForKey:@"kCGWindowLayer"]
intValue];
if (layer != 0) {
continue;
}
for (NSScreen *screen in screens) {
if (isWindowMaximizedOnScreen(app,
screen)) {
int pid = [[app objectForKey:
@"kCGWindowOwnerPID"]
intValue];
maximizedPIDs.insert(pid);
break;
}
}
}
}
// Pre-compute the largest-area window bounds per PID.
// CGWindowList often lists small auxiliary windows (toolbars,
// chrome) before the main content window; using the largest area
// avoids reporting those wrong bounds for geometry.
std::map<int, NSRect> largestBoundsPerPID;
if (options.geometry || options.maximized) {
for (NSDictionary *app in apps) {
int layer = [[app objectForKey:@"kCGWindowLayer"]
intValue];
if (layer != 0) {
continue;
}
NSRect b = NSZeroRect;
CGRectMakeWithDictionaryRepresentation(
(CFDictionaryRef)[app
objectForKey:@"kCGWindowBounds"],
&b);
int pid = [[app objectForKey:@"kCGWindowOwnerPID"]
intValue];
auto it = largestBoundsPerPID.find(pid);
if (it == largestBoundsPerPID.end() ||
b.size.width * b.size.height >
it->second.size.width *
it->second.size.height) {
largestBoundsPerPID[pid] = b;
}
}
}
// Track titles already added to avoid duplicates (name + owner)
std::vector<std::string> seen;
for (NSDictionary *app in apps) {
int layer =
[[app objectForKey:@"kCGWindowLayer"] intValue];
if (layer != 0) {
continue;
}
std::string name = nsStringToStdString(
[app objectForKey:@"kCGWindowName"]);
std::string owner = nsStringToStdString(
[app objectForKey:@"kCGWindowOwnerName"]);
// Collect geometry once (used for multiple fields)
NSRect bounds = NSZeroRect;
if (options.geometry || options.fullscreen ||
options.maximized) {
CGRectMakeWithDictionaryRepresentation(
(CFDictionaryRef)[app
objectForKey:@"kCGWindowBounds"],
&bounds);
}
// Process both the window name and the owner name as
// separate entries, matching the old GetWindowList() behaviour
for (int pass = 0; pass < 2; ++pass) {
const std::string &title = (pass == 0) ? name
: owner;
if (title.empty()) {
continue;
}
if (std::find(seen.begin(), seen.end(),
title) != seen.end()) {
continue;
}
seen.emplace_back(title);
WindowInfo info;
info.title = title;
if (options.focus) {
info.focused =
(title == foregroundTitle);
}
if (options.geometry || options.fullscreen ||
options.maximized) {
int pid = [[app objectForKey:
@"kCGWindowOwnerPID"]
intValue];
auto it = largestBoundsPerPID.find(pid);
const NSRect &geoBounds =
(it != largestBoundsPerPID.end())
? it->second
: bounds;
info.x = (int)geoBounds.origin.x;
info.y = (int)geoBounds.origin.y;
info.width = (int)geoBounds.size.width;
info.height = (int)geoBounds.size.height;
if (options.fullscreen) {
info.fullscreen =
fullscreenPIDs.count(pid) >
0;
}
if (options.maximized) {
info.maximized =
maximizedPIDs.count(pid) >
0;
}
}
// windowClass and text not implemented on macOS
result.emplace_back(std::move(info));
}
}
apps = nil;
CFRelease(cfApps);
}
return false;
}
std::optional<std::string> GetTextInWindow(const std::string &)
{
// Not implemented
return {};
return result;
}
int SecondsSinceLastInput()
@@ -273,9 +310,9 @@ int SecondsSinceLastInput()
return (int)time;
}
void GetProcessList(QStringList &list)
QStringList GetProcessList()
{
list.clear();
QStringList list;
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
NSArray *array = [ws runningApplications];
@@ -291,11 +328,11 @@ void GetProcessList(QStringList &list)
}
}
}
return list;
}
void GetForegroundProcessName(std::string &proc)
std::string GetForegroundProcessName()
{
proc.resize(0);
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
NSArray *array = [ws runningApplications];
@@ -308,23 +345,72 @@ void GetForegroundProcessName(std::string &proc)
break;
}
const char *str = name.UTF8String;
proc = std::string(str);
if (str) {
return str;
}
break;
}
}
return {};
}
void GetForegroundProcessName(QString &proc)
std::string GetForegroundProcessPath()
{
std::string temp;
GetForegroundProcessName(temp);
proc = QString::fromStdString(temp);
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
for (NSRunningApplication *app in [ws runningApplications]) {
if (!app.isActive) {
continue;
}
NSURL *url = app.executableURL;
if (!url) {
break;
}
const char *str = url.path.UTF8String;
if (str) {
return str;
}
break;
}
}
return {};
}
QStringList GetProcessPathsFromName(const QString &name)
{
QStringList paths;
@autoreleasepool {
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
for (NSRunningApplication *app in [ws runningApplications]) {
NSString *appName = app.localizedName;
if (!appName) {
continue;
}
const char *nameStr = appName.UTF8String;
if (!nameStr || name != QString::fromUtf8(nameStr)) {
continue;
}
NSURL *url = app.executableURL;
if (!url) {
continue;
}
const char *pathStr = url.path.UTF8String;
if (!pathStr) {
continue;
}
QString path = QString::fromUtf8(pathStr);
if (!paths.contains(path)) {
paths.append(path);
}
}
}
return paths;
}
bool IsInFocus(const QString &executable)
{
std::string current;
GetForegroundProcessName(current);
const auto current = GetForegroundProcessName();
// True if executable switch equals current window
bool equals = (executable.toStdString() == current);

View File

@@ -11,15 +11,36 @@ namespace advss {
enum class HotkeyType;
EXPORT void GetWindowList(std::vector<std::string> &windows);
EXPORT void GetWindowList(QStringList &windows);
EXPORT void GetCurrentWindowTitle(std::string &title);
EXPORT bool IsFullscreen(const std::string &title);
EXPORT bool IsMaximized(const std::string &title);
EXPORT std::optional<std::string> GetTextInWindow(const std::string &window);
struct WindowQueryOptions {
bool geometry = false;
bool focus = false;
bool fullscreen = false;
bool maximized = false;
bool windowClass = false; // Windows only, no-op on other platforms
bool text = false; // Windows only, expensive, no-op on other platforms
};
struct WindowInfo {
std::string title;
bool focused = false;
bool fullscreen = false;
bool maximized = false;
int x = 0;
int y = 0;
int width = 0;
int height = 0;
std::string windowClass;
std::optional<std::string> text;
};
EXPORT std::vector<WindowInfo>
GetWindows(const WindowQueryOptions &options = {});
EXPORT std::string GetCurrentWindowTitle();
EXPORT int SecondsSinceLastInput();
EXPORT void GetProcessList(QStringList &processes);
EXPORT void GetForegroundProcessName(std::string &name);
EXPORT QStringList GetProcessList();
EXPORT std::string GetForegroundProcessName();
EXPORT std::string GetForegroundProcessPath();
EXPORT QStringList GetProcessPathsFromName(const QString &name);
EXPORT bool IsInFocus(const QString &executable);
void PlatformInit();
void PlatformCleanup();

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,7 +94,6 @@ public:
bool stop = false;
std::condition_variable cv;
bool firstBoot = true;
bool transitionActive = false;
bool sceneCollectionStop = false;
bool obsIsShuttingDown = false;
@@ -164,6 +163,8 @@ public:
bool disableHints = false;
bool disableFilterComboboxFilter = false;
bool hideLegacyTabs = true;
bool settingsLockEnabled = false;
std::string settingsLockPassword;
bool saveWindowGeo = false;
QPoint windowPos = {};
QSize windowSize = {};

View File

@@ -1,5 +1,7 @@
#include "auto-update-tooltip-label.hpp"
#include <QToolTip>
namespace advss {
AutoUpdateHelpIcon::AutoUpdateHelpIcon(
@@ -16,6 +18,7 @@ AutoUpdateHelpIcon::AutoUpdateHelpIcon(
void AutoUpdateHelpIcon::enterEvent(QEnterEvent *event)
{
UpdateTooltip();
_timer->start(_updateIntervalMs);
QLabel::enterEvent(event);
}
@@ -28,7 +31,12 @@ void AutoUpdateHelpIcon::leaveEvent(QEvent *event)
void AutoUpdateHelpIcon::UpdateTooltip()
{
setToolTip(_callback());
if (!underMouse()) {
return;
}
const QString text = _callback();
QToolTip::showText(QCursor::pos(), text, this);
}
} // namespace advss

View File

@@ -8,13 +8,18 @@
#include <obs-frontend-api.h>
#include <obs-module.h>
#include <QFileDialog>
#include <QTextStream>
#include <util/config-file.h>
#include <QFileDialog>
#include <QMainWindow>
#include <QTextStream>
#include <QTimer>
#include <thread>
namespace advss {
void AskForBackup(const QString &json)
static void showBackupDialogs(const QString &json)
{
const bool backupWasConfirmed = DisplayMessage(
obs_module_text("AdvSceneSwitcher.askBackup"), true, false);
@@ -42,6 +47,27 @@ void AskForBackup(const QString &json)
out << json;
}
void AskForBackup(obs_data_t *settings)
{
// This function is called while the plugin settings are being loaded.
// Blocking at this stage can cause issues such as OBS failing to start
// or crashing.
// Therefore, we ask the user whether they want to back up the settings
// asynchronously.
auto json = obs_data_get_json(settings);
static QString jsonQString = json ? json : "";
static const auto askForBackupWrapper = [](void *) {
showBackupDialogs(jsonQString);
};
AddFinishedLoadingStep([]() {
obs_queue_task(OBS_TASK_UI, askForBackupWrapper, nullptr,
false);
});
}
void BackupSettingsOfCurrentVersion()
{
auto sceneCollectionName = obs_frontend_get_current_scene_collection();

View File

@@ -1,9 +1,9 @@
#pragma once
#include <QString>
#include <obs-data.h>
namespace advss {
void AskForBackup(const QString &json);
void AskForBackup(obs_data_t *settings);
void BackupSettingsOfCurrentVersion();
} // namespace advss

View File

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

196
lib/utils/crash-handler.cpp Normal file
View File

@@ -0,0 +1,196 @@
#include "crash-handler.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include <obs-frontend-api.h>
#include <obs-module.h>
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QLabel>
#include <QMainWindow>
#include <QVBoxLayout>
namespace advss {
static constexpr std::string_view sentinel = ".running";
#ifndef NDEBUG
static constexpr bool handleUncleanShutdown = false;
#else
static constexpr bool handleUncleanShutdown = true;
#endif
static bool wasCleanShutdown = false;
static bool suppressCrashDialog = false;
static char *sentinelFile = nullptr;
bool GetSuppressCrashDialog()
{
return suppressCrashDialog;
}
void SetSuppressCrashDialog(bool suppress)
{
suppressCrashDialog = suppress;
}
static void setup();
static bool setupDone = []() {
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;
}();
static void handleShutdown(enum obs_frontend_event event, void *)
{
if (event != OBS_FRONTEND_EVENT_EXIT) {
return;
}
if (!sentinelFile) {
return;
}
QFile file(sentinelFile);
if (!file.exists()) {
bfree(sentinelFile);
return;
}
if (!file.remove()) {
blog(LOG_WARNING, "failed to remove sentinel file");
}
bfree(sentinelFile);
}
static void setup()
{
// Freed in handleShutdown()
sentinelFile = obs_module_config_path(sentinel.data());
if (!sentinelFile) {
return;
}
QString dirPath = QFileInfo(sentinelFile).absolutePath();
QDir dir(dirPath);
if (!dir.exists()) {
if (!dir.mkpath(dirPath)) {
blog(LOG_WARNING,
"failed to create directory for sentinel file");
bfree(sentinelFile);
return;
}
}
QFile file(sentinelFile);
wasCleanShutdown = file.exists();
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
blog(LOG_WARNING, "failed to create sentinel file");
bfree(sentinelFile);
return;
}
file.write("running");
file.close();
obs_frontend_add_event_callback(handleShutdown, nullptr);
return;
}
static bool wasUncleanShutdown()
{
static bool alreadyHandled = false;
if (!handleUncleanShutdown || !wasCleanShutdown || alreadyHandled) {
alreadyHandled = true;
return false;
}
alreadyHandled = true;
blog(LOG_WARNING, "unclean shutdown detected");
return true;
}
static void askForStartupSkip()
{
auto mainWindow =
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) {
StartPlugin();
}
}
bool ShouldSkipPluginStartOnUncleanShutdown()
{
if (!wasUncleanShutdown()) {
return false;
}
if (suppressCrashDialog) {
return false;
}
// This function is called while the plugin settings are being loaded.
// Blocking at this stage can cause issues such as OBS failing to start
// or crashing.
// Therefore, we ask the user whether they want to start the plugin
// asynchronously.
static const auto showDialogWrapper = [](void *) {
askForStartupSkip();
};
AddFinishedLoadingStep([]() {
obs_queue_task(OBS_TASK_UI, showDialogWrapper, nullptr, false);
});
return true;
}
} // namespace advss

View File

@@ -0,0 +1,10 @@
#pragma once
namespace advss {
bool ShouldSkipPluginStartOnUncleanShutdown();
bool GetSuppressCrashDialog();
void SetSuppressCrashDialog(bool suppress);
} // namespace advss

View File

@@ -11,6 +11,7 @@ static void populateUnits(QComboBox *list)
list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"));
list->addItem(obs_module_text("AdvSceneSwitcher.unit.minutes"));
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
list->addItem(obs_module_text("AdvSceneSwitcher.unit.days"));
}
DurationSelection::DurationSelection(QWidget *parent, bool showUnitSelection,
@@ -66,6 +67,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
return 60;
case Duration::Unit::HOURS:
return 3600;
case Duration::Unit::DAYS:
return 86400;
default:
break;
}

View File

@@ -121,6 +121,8 @@ static std::string durationUnitToString(Duration::Unit u)
return obs_module_text("AdvSceneSwitcher.unit.minutes");
case Duration::Unit::HOURS:
return obs_module_text("AdvSceneSwitcher.unit.hours");
case Duration::Unit::DAYS:
return obs_module_text("AdvSceneSwitcher.unit.days");
default:
break;
}
@@ -151,6 +153,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
return 60;
case Duration::Unit::HOURS:
return 3600;
case Duration::Unit::DAYS:
return 86400;
default:
break;
}

View File

@@ -27,6 +27,7 @@ public:
SECONDS,
MINUTES,
HOURS,
DAYS,
};
EXPORT Unit GetUnit() const { return _unit; }

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ public:
EXPORT
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 QString &);
EXPORT QString GetPath() const;
@@ -34,6 +34,7 @@ signals:
private:
Type _type;
QString _browseTitle;
VariableLineEdit *_filePath;
QPushButton *_browseButton;
};

View File

@@ -0,0 +1,467 @@
#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,
bool *wasSkipped)
{
auto wizard = new FirstRunWizard(parent);
wizard->exec();
if (wasSkipped) {
*wasSkipped = (wizard->result() == QDialog::Rejected);
}
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,129 @@
#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,
bool *wasSkipped = nullptr);
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

@@ -4,9 +4,11 @@
#include "ui-helpers.hpp"
#include <algorithm>
#include <QAction>
#include <QMenu>
#include <QLayout>
#include <QMenu>
#include <QTimer>
Q_DECLARE_METATYPE(advss::Item *);
@@ -330,6 +332,13 @@ void ItemSettingsDialog::NameChanged(const QString &text)
SetNameWarning("");
}
void ItemSettingsDialog::showEvent(QShowEvent *)
{
if (_showNameEmptyWarning && _name->text().isEmpty()) {
_name->setFocus(Qt::OtherFocusReason);
}
}
void ItemSettingsDialog::SetNameWarning(const QString warn)
{
if (warn.isEmpty()) {

View File

@@ -52,6 +52,7 @@ private slots:
void NameChanged(const QString &);
protected:
virtual void showEvent(QShowEvent *) override;
void SetNameWarning(const QString);
QLineEdit *_name;

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,
const char *tooltip,
const std::function<void()> &signal)

View File

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

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