Compare commits

..

66 Commits

Author SHA1 Message Date
WarmUpTill
16bdcc348b Fix crash when changing scene collection on MacOS 2026-08-23 18:51:29 +02:00
WarmUpTill
e76df589e2 Fix cmake configure errors during download retry
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-08-14 22:45:29 +02:00
WarmUpTill
1390565fc6 Disable additional warnings for whisper.cpp 2026-08-14 22:45:29 +02:00
WarmUpTill
d76a37c785 Add AdvancedSceneSwitcherVersion vendor request
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
Returns version and commit information
2026-08-12 20:55:34 +02:00
WarmUpTill
ce8a35adea CI: Include Ubuntu 26 artifacts in release draft 2026-08-12 20:55:34 +02:00
Rygtx
9030ba1adf Update locale (zh-CN.ini)
Some checks failed
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-08-10 21:27:25 +02:00
WarmUpTill
5b8d829bba Add "Speech Recognition" 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
Allows checking for speech patterns on a given OBS audio source
2026-08-05 12:00:34 +02:00
WarmUpTill
ad03b92d2d CI: Install vulkan SDK
Required to build whisper.cpp with GPU acceleration support
2026-08-05 12:00:34 +02:00
WarmUpTill
88fc412900 deps: Add whisper.cpp 2026-08-05 12:00:34 +02:00
WarmUpTill
c2ec6848aa Fix sources of type "Group" being listed twice in filter selections
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-08-04 21:05:55 +02:00
WarmUpTill
01da2877b0 CI: Add Ubuntu 26 build 2026-08-03 20:52:39 +02:00
WarmUpTill
107d77f1b8 Split macro-condition-video.cpp UI into per-widget files
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
This makes it easier to conditionally exclude video-edit-object.cpp
from the build when OpenCV >= 5 drops CascadeClassifier support.
2026-08-01 22:34:01 +02:00
WarmUpTill
1030868a75 CI: Build "xobjdetect" when building OpenCV 5.x 2026-08-01 22:34:01 +02:00
WarmUpTill
797dc2ceb1 deps: Add OpenCV contrib repo
Needed for Haar Cascade Classifiers support since OpenCV 5.0.0
2026-08-01 22:34:01 +02:00
WarmUpTill
b971bfe800 deps: Switch to OpenCV 5.0.0 2026-08-01 22:34:01 +02:00
WarmUpTill
f88407b731 Export Section symbols 2026-08-01 22:34:01 +02:00
WarmUpTill
725e3cacb8 Add license files
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-07-30 20:47:39 +02:00
WarmUpTill
c9eed5d997 Add un-/pause option to macro tab context menu
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-07-22 11:27:15 +02:00
Arimil
bf682136c3 OpenCV 5 support
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-07-19 19:56:07 +02:00
WarmUpTill
7be2a4f544 Add support to snapshot vars before queue insertion
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-07-17 23:24:02 +02:00
WarmUpTill
616664e6f3 Add "Loop" action 2026-07-17 23:24:02 +02:00
WarmUpTill
0caf8628b7 Add HideElseSection() in preparation for loop action 2026-07-17 23:24:02 +02:00
WarmUpTill
463e7caa2c Reword locale keys 2026-07-17 23:24:02 +02:00
WarmUpTill
0e2315807f Statically link SSL to avoid dll conflicts on Windows 2026-07-17 21:43:58 +02:00
WarmUpTill
46bf6c666f Cleanup includes
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-07-11 19:50:58 +02:00
WarmUpTill
902be7759d Speed up build times by using pre-compiled headers 2026-07-11 19:50:58 +02:00
WarmUpTill
b4a1593dc9 Fix crash on shutdown caused by "Profile" condition
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-07-07 20:19:51 +02:00
WarmUpTill
577cfc6ba9 Add message_text temp var for subscription message event
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
The existing "message" temp var for the subscription message event
contained a raw JSON object (including emote metadata) rather than
plain text, making it difficult to use.
2026-07-06 17:19:32 +02:00
WarmUpTill
f7a29dfa69 Add option to start and stop polls 2026-07-06 17:19:32 +02:00
WarmUpTill
e3475cf8ee Add option to wait for scene item transition
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-07-05 20:41:12 +02:00
WarmUpTill
b344f75a30 Add variable setter / getter to inline script generated code 2026-07-05 20:26:36 +02:00
WarmUpTill
338a478ead Cleanup Twitch timestamp verification
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-07-04 12:09:31 +02:00
WarmUpTill
2658ced18e Add "Audio" option to the first run wizard
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-07-03 18:57:32 +02:00
WarmUpTill
cd6ff4d52b Add "Sequence" option to the first run wizard 2026-07-03 18:57:32 +02:00
WarmUpTill
5438a6a364 Split first run wizard into multiple files 2026-07-03 18:57:32 +02:00
WarmUpTill
ea1dea8621 Add option to wait for variable changes to "Wait" action type
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-07-03 15:59:23 +02:00
WarmUpTill
3a556113aa Expose API to notify about variable value change 2026-07-03 15:59:23 +02:00
WarmUpTill
eded0367db Add option to get commercial info 2026-07-03 15:59:23 +02:00
WarmUpTill
6c90f517e4 Fix freeze when stopping waiting macro via "Run macro" button 2026-07-03 15:59:23 +02:00
WarmUpTill
c07c00325f Switch to VariableColorButton for OCR and color match
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-06-30 20:30:50 +02:00
WarmUpTill
0a2e2cd771 Add VariableColorButton
Used for color selection which also support variables
2026-06-30 20:30:50 +02:00
WarmUpTill
8f3b5f9dfc CI: Rename "legacy" Windows artifacts to "portable"
This is consistent with the naming used by other plugin developers
2026-06-30 19:54:52 +02:00
WarmUpTill
78499bcf5f Improve layout spacing for "Recording" action
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-06-29 17:48:42 +02:00
WarmUpTill
78fa776ce5 Move recording tips to HelpIcons 2026-06-29 17:48:42 +02:00
WarmUpTill
93c6415547 Add restart hint to recording folder/format actions 2026-06-29 17:48:42 +02:00
WarmUpTill
12f26c29e6 Clarify COPY_VAR layout to show source and destination variables
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-06-26 22:29:06 +02:00
WarmUpTill
e4f6d94247 Add option to automatically extract value from JSON query result 2026-06-26 22:29:06 +02:00
WarmUpTill
a04c51397d Fix variables of nested macros' actions not being resolved
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-06-17 21:12:48 +02:00
WarmUpTill
2073ef6ce9 Fix nested macro actions being executed twice when added to a queue 2026-06-17 21:12:48 +02:00
WarmUpTill
7dfd9ddcb5 Print plugin enabled/disabled summary during CMake configure
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-06-16 17:40:22 +02:00
WarmUpTill
6215c39024 Don't fail cmake configure step if openvr deps are not met 2026-06-16 17:40:22 +02:00
WarmUpTill
1c856911e8 Set default value for CMAKE_GENERATOR_PLATFORM on Windows 2026-06-16 17:40:22 +02:00
WarmUpTill
ed17c2ac6d Disconnect Twitch chat and event connection on plugin stop
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-06-13 22:05:32 +02:00
WarmUpTill
1ce332a91d Track all kWin wayland windows 2026-06-13 22:01:52 +02:00
WarmUpTill
2aa27fc051 Fix window conditions not triggering for native Wayland windows
When KWin compat is active, GetWindows() only returned XWayland windows
since it relied on X11's _NET_CLIENT_LIST. Native Wayland windows were
invisible to conditions even though GetCurrentWindowTitle() reported them
correctly via the KWin DBus notifier.
2026-06-13 22:01:52 +02:00
WarmUpTill
31b239d5dc Focus segment type selection when new segment is added
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-06-11 18:25:29 +02:00
WarmUpTill
7c3779e287 Sort macro export dialog resources alphabetically and add scrollbars 2026-06-11 18:25:29 +02:00
WarmUpTill
3f13c33b10 Fix crash on transition start in studio mode 2026-06-11 18:25:29 +02:00
WarmUpTill
8727a65513 Support both recommended and legacy layouts in Windows installer
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
Default to %ProgramData%\obs-studio\plugins\ on fresh installs.
Restore the previously recorded install path on upgrades, so users
who installed to a custom location are not silently moved.
2026-05-30 16:01:11 +02:00
WarmUpTill
baf3d0e63c Fix race condition in Twitch event sub reconnection logic 2026-05-30 16:01:11 +02:00
WarmUpTill
ea2d1e7e9d Fix crash when importing macros is slow
Probably not a real issue.
Noticed during debug of another problem.
2026-05-30 16:01:11 +02:00
WarmUpTill
f9f26ad50b Fix crash on shutdown
StopAllMacros was called before th->wait(), allowing Macro::Stop()
to run concurrently with the other threads still inside
CheckConditions. This caused races on _conditionCheckFuture (called
from both threads) and _helperThreads.

This could happen if conditions / actions were marked to run in parallel
to the main loop.
2026-05-30 16:01:11 +02:00
WarmUpTill
9911c41a35 Split advanced-scene-switcher-*-windows-x64.zip
One zip archive for the ProgramData path.
Another zip archive for the legacy Program Files path.

Requested in:
https://github.com/WarmUpTill/SceneSwitcher/discussions/1628
2026-05-30 16:01:11 +02:00
WarmUpTill
d590bc11f8 Fix freeze when choosing invalid OCR model 2026-05-30 16:01:11 +02:00
WarmUpTill
31c4c1f5d7 Convert "Program Files/" paths to "ProgramData/" paths on Windows
Affects OCR and object detection functionality of the video condition
2026-05-30 16:01:11 +02:00
WarmUpTill
42ff48d637 Add option to lock settings dialog with simple password
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
This is meant as a simple tampering protection.
The settings are not encrypted and the password is stored in plaintext.
2026-05-28 21:50:00 +02:00
156 changed files with 9949 additions and 3440 deletions

View File

@@ -8,6 +8,7 @@ package 'libcurl4-openssl-dev'
package 'libxtst-dev'
package 'libxss-dev'
package 'libopencv-dev'
package 'libopencv-contrib-dev'
package 'libtesseract-dev'
package 'libproc2-dev'
package 'libusb-1.0-0-dev'

View File

@@ -284,30 +284,56 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
case ${host_os} {
macos)
local opencv_dir="${project_root}/deps/opencv"
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
local opencv_contrib_dir="${project_root}/deps/opencv_contrib"
local opencv_install_x86="${opencv_dir}/install_x86_64"
local opencv_install_arm="${opencv_dir}/install_arm64"
local -a opencv_cmake_args=(
local -a opencv_cmake_args_common=(
-DCMAKE_BUILD_TYPE=Release
-DBUILD_LIST=core,imgproc,objdetect
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DBUILD_LIST=core,imgproc,objdetect,xobjdetect,dnn
-DOPENCV_EXTRA_MODULES_PATH="${opencv_contrib_dir}/modules"
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
-DWITH_KLEIDICV=OFF
)
if [ "${target}" != "macos-x86_64" ]; then
opencv_cmake_args+=(-DWITH_IPP=OFF)
fi
pushd ${opencv_dir}
log_info "Configure OpenCV ..."
cmake -S . -B ${opencv_build_dir} ${opencv_cmake_args}
log_info "Building OpenCV ..."
cmake --build ${opencv_build_dir} --config Release
log_info "Configure OpenCV (x86_64) ..."
cmake -S . -B build_x86_64 ${opencv_cmake_args_common} \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_PROJECT_INCLUDE="${SCRIPT_HOME}/opencv-force-processor-x86_64.cmake" \
-DCMAKE_INSTALL_PREFIX="${opencv_install_x86}" \
-DWITH_IPP=OFF
log_info "Installing OpenCV ..."
cmake --install ${opencv_build_dir} --prefix "${advss_dep_path}" --config Release || true
log_info "Building OpenCV (x86_64) ..."
cmake --build build_x86_64 --config Release
log_info "Installing OpenCV (x86_64) ..."
cmake --install build_x86_64 --prefix "${opencv_install_x86}" --config Release || true
log_info "Configure OpenCV (arm64) ..."
cmake -S . -B build_arm64 ${opencv_cmake_args_common} \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_INSTALL_PREFIX="${opencv_install_arm}"
log_info "Building OpenCV (arm64) ..."
cmake --build build_arm64 --config Release
log_info "Installing OpenCV (arm64) ..."
cmake --install build_arm64 --prefix "${opencv_install_arm}" --config Release || true
log_info "Merging OpenCV into universal binaries ..."
cp -R "${opencv_install_arm}/." "${advss_dep_path}"
for arm_lib in ${opencv_install_arm}/lib/**/*.(dylib|a)(.); do
local rel="${arm_lib#${opencv_install_arm}/}"
local x86_lib="${opencv_install_x86}/${rel}"
if [[ -f "${x86_lib}" ]]; then
lipo -create "${x86_lib}" "${arm_lib}" -output "${advss_dep_path}/${rel}"
fi
done
rm -rf "${opencv_install_x86}" "${opencv_install_arm}"
popd
local leptonica_dir="${project_root}/deps/leptonica"
@@ -319,7 +345,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DOPENJPEG_SUPPORT=OFF
-DLIBWEBP_SUPPORT=OFF
-DENABLE_WEBP=OFF
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE

View File

@@ -259,7 +259,7 @@ ${_usage_host:-}"
cmake --build build_${target##*-} --config ${config} -t package ${cmake_args}
# Mark certain deps as optional
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)'
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)|(libobs)'
if [ ! -e ${project_root}/release/${output_name}.deb ]; then
mv ${project_root}/release/*.deb ${project_root}/release/${output_name}.deb

View File

@@ -106,6 +106,7 @@ function Build {
$ADVSSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/${OutDirName})"
$OpenCVPath = "${ProjectRoot}/deps/opencv"
$OpenCVContribPath = "${ProjectRoot}/deps/opencv_contrib"
$OpenCVBuildPath = "${OpenCVPath}/build"
Push-Location -Stack BuildOpenCVTemp
@@ -115,7 +116,8 @@ function Build {
"-DCMAKE_BUILD_TYPE=Release"
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DBUILD_LIST=core,imgproc,objdetect"
"-DBUILD_LIST=core,imgproc,objdetect,xobjdetect"
"-DOPENCV_EXTRA_MODULES_PATH:PATH=${OpenCVContribPath}/modules"
)
Log-Information "Configuring OpenCV..."

View File

@@ -68,44 +68,53 @@ 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"
$ReleasePath = "${ProjectRoot}/release/${Configuration}"
$NewBinPath = "${ReleasePath}/${ProductName}/bin/64bit"
$NewDataPath = "${ReleasePath}/${ProductName}/data"
$CIWindowsDir = "${ProjectRoot}/build-aux/CI/windows"
# --- Recommended zip (new layout, extract to %ProgramData%\obs-studio\) ---
Log-Group "Archiving ${ProductName} (recommended)..."
$RecStaging = "${ProjectRoot}/release/zip-staging-rec"
Remove-Item -Path $RecStaging -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $RecStaging | Out-Null
Copy-Item -Path "${CIWindowsDir}/README.txt" -Destination "${RecStaging}/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
$RecBinPath = "${RecStaging}/${ProductName}/bin/64bit"
New-Item -ItemType Directory -Force -Path $RecBinPath | Out-Null
Copy-Item -Path "${NewBinPath}/*" -Destination $RecBinPath -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
$RecDataPath = "${RecStaging}/${ProductName}/data"
New-Item -ItemType Directory -Force -Path $RecDataPath | Out-Null
Copy-Item -Path "${NewDataPath}/*" -Destination $RecDataPath -Recurse -Force
}
Compress-Archive -Force -Path (Get-ChildItem -Path $RecStaging) `
-CompressionLevel Optimal `
-DestinationPath "${ProjectRoot}/release/${OutputName}.zip"
Remove-Item -Path $RecStaging -Recurse -Force
Log-Group
Log-Group "Archiving ${ProductName}..."
$CompressArgs = @{
Path = (Get-ChildItem -Path $StagingPath)
CompressionLevel = 'Optimal'
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
Verbose = ($null -ne $Env:CI)
# --- Legacy zip (old layout, extract to OBS install directory) ---
Log-Group "Archiving ${ProductName} (portable)..."
$PortableStaging = "${ProjectRoot}/release/zip-staging-portable"
Remove-Item -Path $PortableStaging -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $PortableStaging | Out-Null
Copy-Item -Path "${CIWindowsDir}/README-portable.txt" -Destination "${PortableStaging}/README.txt"
if ( Test-Path -Path $NewBinPath ) {
$PortableBinPath = "${PortableStaging}/obs-plugins/64bit"
New-Item -ItemType Directory -Force -Path $PortableBinPath | Out-Null
Copy-Item -Path "${NewBinPath}/*" -Destination $PortableBinPath -Recurse -Force
}
Compress-Archive -Force @CompressArgs
Remove-Item -Path $StagingPath -Recurse -Force
if ( Test-Path -Path $NewDataPath ) {
$PortableDataPath = "${PortableStaging}/data/obs-plugins/${ProductName}"
New-Item -ItemType Directory -Force -Path $PortableDataPath | Out-Null
Copy-Item -Path "${NewDataPath}/*" -Destination $PortableDataPath -Recurse -Force
}
Compress-Archive -Force -Path (Get-ChildItem -Path $PortableStaging) `
-CompressionLevel Optimal `
-DestinationPath "${ProjectRoot}/release/${OutputName}-portable.zip"
Remove-Item -Path $PortableStaging -Recurse -Force
Log-Group
if ( ( $BuildInstaller ) ) {
@@ -120,7 +129,24 @@ function Package {
Push-Location -Stack BuildTemp
Ensure-Location -Path "${ProjectRoot}/release"
Remove-Item -Path Package -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item -Path ${Configuration} -Destination Package -Recurse
# Recommended layout (for %ProgramData%\obs-studio\plugins\)
$PkgRec = "Package/recommended"
New-Item -ItemType Directory -Force -Path $PkgRec | Out-Null
Copy-Item -Path "${Configuration}/*" -Destination $PkgRec -Recurse -Force
# Legacy layout (for OBS installation directory)
if ( Test-Path "${Configuration}/${ProductName}/bin/64bit" ) {
$PkgLegBin = "Package/portable/obs-plugins/64bit"
New-Item -ItemType Directory -Force -Path $PkgLegBin | Out-Null
Copy-Item -Path "${Configuration}/${ProductName}/bin/64bit/*" -Destination $PkgLegBin -Recurse -Force
}
if ( Test-Path "${Configuration}/${ProductName}/data" ) {
$PkgLegData = "Package/portable/data/obs-plugins/${ProductName}"
New-Item -ItemType Directory -Force -Path $PkgLegData | Out-Null
Copy-Item -Path "${Configuration}/${ProductName}/data/*" -Destination $PkgLegData -Recurse -Force
}
Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer"
Remove-Item -Path Package -Recurse
Pop-Location -Stack BuildTemp

View File

@@ -0,0 +1,7 @@
# Injected via CMAKE_PROJECT_INCLUDE after opencv's project() call. Forces
# CMAKE_SYSTEM_PROCESSOR into the cache so that third-party subdirectories
# (mlas) that read the cache directly see x86_64 instead of the arm64 host
# processor on Apple Silicon CI runners.
set(CMAKE_SYSTEM_PROCESSOR
x86_64
CACHE INTERNAL "" FORCE)

View File

@@ -35,14 +35,21 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
sudo apt-get install ${apt_args} gcc-${${target##*-}//_/-}-linux-gnu g++-${${target##*-}//_/-}-linux-gnu
}
sudo add-apt-repository --yes ppa:obsproject/obs-studio
sudo apt update
local dist_version
read -r dist_version <<< "$(source /etc/os-release; print "${VERSION_ID}")"
sudo apt-get install ${apt_args} \
build-essential \
libgles2-mesa-dev \
libsimde-dev \
obs-studio
# The OBS PPA may not yet support newer Ubuntu versions.
# On those, use the native obs-studio packages and install libobs-dev separately,
# as it is not bundled in obs-studio itself on newer Ubuntu releases.
local -a obs_packages=(build-essential libgles2-mesa-dev libsimde-dev obs-studio)
if is-at-least 26.04 ${dist_version}; then
obs_packages+=(libobs-dev)
else
sudo add-apt-repository --yes ppa:obsproject/obs-studio
sudo apt update
fi
sudo apt-get install ${apt_args} ${obs_packages}
local -a _qt_packages=()

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-5
DEP_DIR: .deps/advss-build-dependencies-6
jobs:
check-event:
name: Check GitHub Event Data 🔎
@@ -213,6 +213,17 @@ jobs:
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@main
- name: Install Vulkan SDK 🌋
shell: bash
run: |
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc \
| sudo tee /etc/apt/trusted.gpg.d/lunarg.asc
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-noble.list \
https://packages.lunarg.com/vulkan/lunarg-vulkan-noble.list
sudo apt-get update -qq
sudo apt-get install -y vulkan-sdk
echo "VULKAN_SDK=/usr" >> "$GITHUB_ENV"
- name: Build Plugin 🧱
uses: ./.github/actions/build-plugin
with:
@@ -238,6 +249,14 @@ jobs:
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: Rename artifacts for Ubuntu 24 🏷️
run: |
: Rename artifacts for Ubuntu 24 🏷️
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
[ -e "${f}" ] || continue
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu24.04-linux-gnu}"
done
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v7
with:
@@ -252,6 +271,85 @@ jobs:
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
if-no-files-found: ignore
ubuntu-26-build:
name: Build for Ubuntu 26 🐧
runs-on: ubuntu-26.04
needs: check-event
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Set Up Environment 🔧
id: setup
run: |
: Set Up Environment 🔧
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
git_tag="$(git describe --tags)"
read -r product_name product_version <<< \
"$(jq -r '. | {name, version} | join(" ")' buildspec.json)"
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: ccache-cache
with:
path: ${{ github.workspace }}/.ccache
key: ${{ runner.os }}-ccache-x86_64-${{ needs.check-event.outputs.config }}
restore-keys: |
${{ runner.os }}-ccache-x86_64-
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@main
- name: Build Plugin 🧱
uses: ./.github/actions/build-plugin
with:
target: x86_64
config: ${{ needs.check-event.outputs.config }}
- name: Run tests
uses: ./.github/actions/run-tests
with:
target: x86_64
config: ${{ needs.check-event.outputs.config }}
- name: Package Plugin 📀
uses: ./.github/actions/package-plugin
with:
package: ${{ fromJSON(needs.check-event.outputs.package) }}
target: x86_64
config: ${{ needs.check-event.outputs.config }}
- name: Rename artifacts for Ubuntu 26 🏷️
run: |
: Rename artifacts for Ubuntu 26 🏷️
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
[ -e "${f}" ] || continue
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu26.04-linux-gnu}"
done
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.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@v7
if: ${{ fromJSON(needs.check-event.outputs.package) }}
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.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 🪟
runs-on: windows-2022
@@ -287,6 +385,12 @@ jobs:
target: x64
config: ${{ needs.check-event.outputs.config }}
- name: Install Vulkan SDK 🌋
uses: jakoch/install-vulkan-sdk-action@v1
with:
install_runtime: false
cache: true
- name: Build Plugin 🧱
uses: ./.github/actions/build-plugin
with:

View File

@@ -78,6 +78,7 @@ jobs:
'windows-x64;zip|exe'
'macos-universal;tar.xz|pkg'
'ubuntu-24.04-x86_64;tar.xz|deb|ddeb'
'ubuntu-26.04-x86_64;tar.xz|deb|ddeb'
'sources;tar.xz'
)

9
.gitmodules vendored
View File

@@ -28,12 +28,15 @@
[submodule "deps/libusb"]
path = deps/libusb
url = https://github.com/libusb/libusb.git
[submodule "deps/date"]
path = deps/date
url = https://github.com/HowardHinnant/date.git
[submodule "deps/jsoncons"]
path = deps/jsoncons
url = https://github.com/danielaparker/jsoncons.git
[submodule "deps/paho.mqtt.cpp"]
path = deps/paho.mqtt.cpp
url = https://github.com/eclipse-paho/paho.mqtt.cpp.git
[submodule "deps/opencv_contrib"]
path = deps/opencv_contrib
url = https://github.com/opencv/opencv_contrib.git
[submodule "deps/whisper.cpp"]
path = deps/whisper.cpp
url = https://github.com/ggml-org/whisper.cpp.git

View File

@@ -16,6 +16,14 @@ if(BUILD_OUT_OF_TREE)
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake"
NO_POLICY_SCOPE)
include(compilerconfig)
# OPENSSL_USE_STATIC_LIBS here ensures FindOpenSSL picks up the static libs
# preventing DLL name collisions with other plugins (e.g.NVIDIA AR SDK).
if(OS_WINDOWS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
include(cmake/windows/wingetssl.cmake)
endif()
include(defaults)
include(helpers)
endif()
@@ -34,11 +42,6 @@ include(cmake/common/get_git_revision_description.cmake)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
git_describe(GIT_TAG)
# Helper for OpenSSL
if(OS_WINDOWS)
include(cmake/windows/wingetssl.cmake)
endif()
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
set(GIT_TAG ${PROJECT_VERSION})
endif()
@@ -104,6 +107,8 @@ target_sources(
lib/macro/macro-action-edit.hpp
lib/macro/macro-action-factory.cpp
lib/macro/macro-action-factory.hpp
lib/macro/macro-action-loop.cpp
lib/macro/macro-action-loop.hpp
lib/macro/macro-action-macro.cpp
lib/macro/macro-action-macro.hpp
lib/macro/macro-action-queue.cpp
@@ -209,6 +214,13 @@ target_sources(
lib/utils/filter-combo-box.hpp
lib/utils/first-run-wizard.cpp
lib/utils/first-run-wizard.hpp
lib/utils/first-run-wizard-audio.cpp
lib/utils/first-run-wizard-audio.hpp
lib/utils/first-run-wizard-helpers.hpp
lib/utils/first-run-wizard-sequence.cpp
lib/utils/first-run-wizard-sequence.hpp
lib/utils/first-run-wizard-window.cpp
lib/utils/first-run-wizard-window.hpp
lib/utils/help-icon.hpp
lib/utils/help-icon.cpp
lib/utils/item-selection-helpers.cpp
@@ -291,6 +303,10 @@ target_sources(
lib/utils/volume-control.hpp
lib/utils/websocket-api.cpp
lib/utils/websocket-api.hpp
lib/variables/variable-color-button.cpp
lib/variables/variable-color-button.hpp
lib/variables/variable-color.cpp
lib/variables/variable-color.hpp
lib/variables/variable-line-edit.cpp
lib/variables/variable-line-edit.hpp
lib/variables/variable-number.hpp
@@ -363,6 +379,9 @@ set_target_properties(${LIB_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
target_compile_features(${LIB_NAME} PUBLIC cxx_std_17)
target_precompile_headers(${LIB_NAME} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/pch.hpp")
target_include_directories(
${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib"
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
@@ -540,3 +559,59 @@ else()
set_target_properties_obs(${PROJECT_NAME} PROPERTIES PREFIX "")
endif()
endif()
# --- Install third-party dependency licenses ---
#
# Each entry is "display-name:path/to/license/file" relative to deps/. Files are
# silently skipped when a submodule is not checked out.
set(_dep_license_files
"asio:asio/asio/LICENSE_1_0.txt"
"cpp-httplib:cpp-httplib/LICENSE"
"json:json/LICENSE.MIT"
"jsoncons:jsoncons/LICENSE"
"leptonica:leptonica/leptonica-license.txt"
"libremidi:libremidi/LICENSE.md"
"libusb:libusb/COPYING"
"opencv:opencv/LICENSE"
"openvr:openvr/LICENSE"
"paho-mqtt:paho.mqtt.cpp/LICENSE"
"tesseract:tesseract/LICENSE"
"websocketpp:websocketpp/COPYING"
"whisper:whisper.cpp/LICENSE")
if(DEB_INSTALL)
if(NOT DATA_OUT_DIR)
set(DATA_OUT_DIR "/share/obs/obs-plugins/${PROJECT_NAME}")
endif()
set(_license_dest "${CMAKE_INSTALL_PREFIX}/${DATA_OUT_DIR}/licenses")
elseif(OS_MACOS)
# macOS: license files become bundle Resources/licenses/* entries
set(_license_dest "")
else()
# Windows and non-DEB Linux: alongside the plugin's data directory
set(_license_dest "${CMAKE_PROJECT_NAME}/data/licenses")
endif()
foreach(_entry IN LISTS _dep_license_files)
string(REGEX MATCH "^([^:]+):(.+)$" _m "${_entry}")
set(_dep_name "${CMAKE_MATCH_1}")
set(_dep_file "${CMAKE_CURRENT_SOURCE_DIR}/deps/${CMAKE_MATCH_2}")
if(NOT EXISTS "${_dep_file}")
continue()
endif()
if(OS_MACOS)
# Copy to binary dir so we can give the file a clean name
set(_staged "${CMAKE_CURRENT_BINARY_DIR}/licenses/LICENSE-${_dep_name}")
configure_file("${_dep_file}" "${_staged}" COPYONLY)
target_sources(${PROJECT_NAME} PRIVATE "${_staged}")
set_source_files_properties("${_staged}" PROPERTIES MACOSX_PACKAGE_LOCATION
"Resources/licenses")
else()
install(
FILES "${_dep_file}"
DESTINATION "${_license_dest}"
RENAME "LICENSE-${_dep_name}")
endif()
endforeach()

View File

@@ -0,0 +1,23 @@
Advanced Scene Switcher - Windows Installation (Portable)
==========================================================
For full installation instructions visit:
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
This archive uses the portable plugin layout for older OBS versions,
portable OBS installs, and Steam.
Extract the CONTENTS of this archive 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 this archive into that directory.
For OBS 28 and newer the recommended archive is preferred:
advanced-scene-switcher-<version>-windows-x64.zip

View File

@@ -1,22 +1,15 @@
Advanced Scene Switcher - Windows Installation
==============================================
===============================================
For full installation instructions visit:
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
Quick guide
-----------
This archive uses the recommended plugin layout for OBS 28 and newer.
This archive contains two installation methods.
Choose ONE depending on your OBS version.
Extract the CONTENTS of this archive into:
recommended\ (preferred)
---------------------------------------------
Extract the CONTENTS of this folder into:
C:\ProgramData\obs-studio\
C:\ProgramData\obs-studio\plugins\
Tip: press Win+R and type %ProgramData%\obs-studio\plugins to open that folder.
@@ -24,14 +17,5 @@ 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.
If the plugin does not appear in OBS, try the portable archive instead:
advanced-scene-switcher-<version>-windows-x64-portable.zip

View File

@@ -157,7 +157,9 @@ else()
set_target_properties(${target} PROPERTIES INSTALL_RPATH
"$ORIGIN:$ORIGIN/..")
endif()
message(STATUS "ADVSS: ENABLED PLUGIN ${target}")
get_filename_component(_plugin_dir_name "${CMAKE_CURRENT_SOURCE_DIR}" NAME)
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_ENABLED
${_plugin_dir_name})
endfunction()
function(install_advss_plugin_dependency_target target dep)
@@ -331,6 +333,8 @@ function(setup_advss_plugin target)
PRIVATE "${ADVSS_SOURCE_DIR}/lib" "${ADVSS_SOURCE_DIR}/lib/legacy"
"${ADVSS_SOURCE_DIR}/lib/macro" "${ADVSS_SOURCE_DIR}/lib/utils"
"${ADVSS_SOURCE_DIR}/lib/variables" "${ADVSS_SOURCE_DIR}/forms")
target_precompile_headers(${target} PRIVATE "${ADVSS_SOURCE_DIR}/lib/pch.hpp")
endfunction()
function(install_advss_plugin_dependency)

View File

@@ -191,14 +191,21 @@ function(_check_dependencies)
foreach(i RANGE 1 ${MAX_DOWNLOAD_RETRIES})
message(STATUS "Attempt ${i}/${MAX_DOWNLOAD_RETRIES} for ${url}")
file(
DOWNLOAD "${url}" "${dependencies_dir}/${file}"
STATUS download_status
EXPECTED_HASH SHA256=${hash})
file(DOWNLOAD "${url}" "${dependencies_dir}/${file}"
STATUS download_status)
list(GET download_status 0 error_code)
list(GET download_status 1 error_message)
if(error_code EQUAL 0)
file(SHA256 "${dependencies_dir}/${file}" actual_hash)
if(NOT actual_hash STREQUAL hash)
set(error_code 1)
set(error_message
"hash mismatch (expected ${hash}, got ${actual_hash})")
endif()
endif()
if(error_code EQUAL 0)
message(STATUS "Downloading ${url} - success on attempt ${i}")
set(download_success TRUE)

View File

@@ -22,6 +22,7 @@ set(CPACK_GENERATOR
"DEB"
CACHE STRING "CPack generator to use")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "obs-studio")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
set(CPACK_SET_DESTDIR ON)

View File

@@ -6,7 +6,11 @@ include(buildspec_common)
# _check_dependencies_windows: Set up Windows slice for _check_dependencies
function(_check_dependencies_windows)
set(arch ${CMAKE_GENERATOR_PLATFORM})
if(CMAKE_GENERATOR_PLATFORM)
set(arch ${CMAKE_GENERATOR_PLATFORM})
else()
set(arch x64)
endif()
set(platform windows-${arch})
set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps")

View File

@@ -23,14 +23,17 @@ SolidCompression=yes
DirExistsWarning=no
UsePreviousAppDir=no
SetupIconFile=installer.ico
UninstallDisplayIcon={app}\advanced-scene-switcher\data\res\images\logo.ico
UninstallDisplayIcon={code:GetUninstallIconPath}
DisableDirPage=no
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; Recommended layout - used when installing to %ProgramData%\obs-studio\plugins\
Source: "..\release\Package\recommended\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsRecommendedLayout
; Legacy layout - used when installing to the OBS installation directory
Source: "..\release\Package\portable\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsLegacyLayout
Source: "..\LICENSE"; Flags: dontcopy
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
@@ -53,23 +56,54 @@ begin
);
end;
function GetDirName(Value: string): string;
// Returns true when the selected install directory is under %ProgramData%,
// meaning the recommended plugin layout should be used.
function IsRecommendedLayout(): Boolean;
begin
Result := ExpandConstant('{commonappdata}\obs-studio\plugins');
Result := Pos(LowerCase(ExpandConstant('{commonappdata}')),
LowerCase(WizardDirValue())) = 1;
end;
// Remove files placed by the legacy installer (obs-plugins/64bit and
function IsLegacyLayout(): Boolean;
begin
Result := not IsRecommendedLayout();
end;
function GetUninstallIconPath(Param: string): string;
begin
if IsRecommendedLayout() then
Result := ExpandConstant('{app}\advanced-scene-switcher\data\res\images\logo.ico')
else
Result := ExpandConstant('{app}\data\obs-plugins\advanced-scene-switcher\res\images\logo.ico');
end;
function GetDirName(Value: string): string;
var
PrevPath: string;
UninstallKey: string;
begin
Result := ExpandConstant('{commonappdata}\obs-studio\plugins');
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
if RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', PrevPath) and (PrevPath <> '') then
Result := PrevPath;
end;
// Remove files placed by the portable 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.
// Only runs when upgrading from a portable location to the recommended one.
procedure RemoveLegacyFiles();
var
OldInstallPath: string;
UninstallKey: string;
begin
if not IsRecommendedLayout() then
Exit;
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
if not RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', OldInstallPath) then
Exit;
// Skip if the previous install was already in the recommended location
if Pos(LowerCase(ExpandConstant('{commonappdata}')), LowerCase(OldInstallPath)) = 1 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');
@@ -83,4 +117,3 @@ begin
if CurStep = ssInstall then
RemoveLegacyFiles();
end;

View File

@@ -40,19 +40,27 @@ if(WIN32 AND (NOT OpenSSL_FOUND))
message(STATUS "Looking for OpenSSL built with CRT variant: ${_crt_kind}")
# Try to find the root and corresponding lib path
if(OPENSSL_USE_STATIC_LIBS)
set(_crypto_lib_name "libcrypto_static.lib")
set(_ssl_lib_name "libssl_static.lib")
else()
set(_crypto_lib_name "libcrypto.lib")
set(_ssl_lib_name "libssl.lib")
endif()
foreach(_root ${_openssl_roots})
if(EXISTS "${_root}/include/openssl/ssl.h")
foreach(_suffix ${_openssl_lib_suffixes})
if(_suffix MATCHES "${_crt_kind}$"
AND EXISTS "${_root}/${_suffix}/libcrypto.lib")
AND EXISTS "${_root}/${_suffix}/${_crypto_lib_name}")
set(OPENSSL_ROOT_DIR
"${_root}"
CACHE PATH "Path to OpenSSL root")
set(OPENSSL_CRYPTO_LIBRARY
"${_root}/${_suffix}/libcrypto.lib"
"${_root}/${_suffix}/${_crypto_lib_name}"
CACHE FILEPATH "OpenSSL crypto lib")
set(OPENSSL_SSL_LIBRARY
"${_root}/${_suffix}/libssl.lib"
"${_root}/${_suffix}/${_ssl_lib_name}"
CACHE FILEPATH "OpenSSL ssl lib")
set(OPENSSL_INCLUDE_DIR
"${_root}/include"

View File

@@ -382,8 +382,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Warten, bis der Übergan
AdvSceneSwitcher.action.wait="Warten"
AdvSceneSwitcher.action.wait.type.fixed="fixe"
AdvSceneSwitcher.action.wait.type.random="zufällig"
AdvSceneSwitcher.action.wait.entry.fixed="Warte für {{waitType}} Dauer von {{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Warte für {{waitType}} Dauer von {{duration}} bis {{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Warte für {{waitType}} Dauer von {{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Warte für {{waitType}} Dauer von {{duration}} bis {{duration2}}"
AdvSceneSwitcher.action.audio="Audio"
AdvSceneSwitcher.action.audio.type.mute="Stummgeschaltet"
AdvSceneSwitcher.action.audio.type.unmute="Nicht stummgeschaltet"

View File

@@ -47,6 +47,13 @@ AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can
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)"
@@ -199,6 +206,8 @@ AdvSceneSwitcher.macroTab.expandAllGroups="Expand all Groups"
AdvSceneSwitcher.macroTab.collapseAllGroups="Collapse all Groups"
AdvSceneSwitcher.macroTab.rename="Rename"
AdvSceneSwitcher.macroTab.remove="Remove"
AdvSceneSwitcher.macroTab.pause="Pause"
AdvSceneSwitcher.macroTab.unpause="Unpause"
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"
@@ -437,6 +446,7 @@ AdvSceneSwitcher.condition.video.ocrConfigReload="Reload configuration file"
AdvSceneSwitcher.condition.video.ocrConfigHint="Tesseract config files consist of lines with parameter-value pairs (space separated).\nFor example:\n\ntessedit_char_blacklist\t\t\t\t\"abc\"\nlanguage_model_penalty_non_dict_word\t0"
AdvSceneSwitcher.condition.video.modelLoadFail="Model data could not be loaded!"
AdvSceneSwitcher.condition.video.selectColor="Select Color"
AdvSceneSwitcher.condition.video.colorVariableTooltip="Expected format: #RRGGBB or #AARRGGBB\nInvalid values will default to black."
AdvSceneSwitcher.condition.video.ocrMode.singleColumn="Single column of text of variable sizes"
AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText="Single uniform block of vertically aligned text"
AdvSceneSwitcher.condition.video.ocrMode.singleBlock="Single uniform block of text"
@@ -460,12 +470,12 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="Minimum neighbors:{{minNeig
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Reduce CPU load by performing check only every{{throttleCount}}milliseconds"
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Perform check only in area"
AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Check for text color:{{textColor}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Check for text color:{{color}}"
AdvSceneSwitcher.condition.video.layout.ocrTextType="Check for text type:{{textType}}"
AdvSceneSwitcher.condition.video.layout.ocrBaseDir="Tesseract base directory:{{tesseractBaseDir}}"
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Check for language:{{languageCode}}"
AdvSceneSwitcher.condition.video.layout.ocrConfig="Config file:{{configFile}}{{openConfigFile}}{{reloadConfig}}{{configFileHint}}"
AdvSceneSwitcher.condition.video.layout.color="Check for color:{{color}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.color="Check for color:{{color}}"
AdvSceneSwitcher.condition.video.minSize="Minimum size:"
AdvSceneSwitcher.condition.video.maxSize="Maximum size:"
AdvSceneSwitcher.condition.video.selectArea="Select area"
@@ -830,6 +840,38 @@ AdvSceneSwitcher.condition.clipboard.condition.isImage="Clipboard contains an im
AdvSceneSwitcher.condition.clipboard.condition.isURL="Clipboard contains an URL"
AdvSceneSwitcher.condition.clipboard.condition.matches="Clipboard content matches"
AdvSceneSwitcher.condition.clipboard.condition.entry="{{conditions}}{{regex}}{{urlInfo}}"
AdvSceneSwitcher.condition.speech="Speech Recognition (beta)"
AdvSceneSwitcher.condition.speech.condition.any="Any speech is detected for"
AdvSceneSwitcher.condition.speech.condition.contains="contains phrase"
AdvSceneSwitcher.condition.speech.condition.matches="matches"
AdvSceneSwitcher.condition.speech.layout.any="{{conditions}}{{source}}"
AdvSceneSwitcher.condition.speech.layout.contains="Transcript of{{source}}{{conditions}}:"
AdvSceneSwitcher.condition.speech.layout.matches="Transcript of{{source}}{{conditions}}:"
AdvSceneSwitcher.condition.speech.layout.phrase="{{phrase}}{{regex}}"
AdvSceneSwitcher.condition.speech.layout.model="Whisper model:{{modelPath}}{{help}}"
AdvSceneSwitcher.condition.speech.model.help="GGML model files can be downloaded from https://huggingface.co/ggerganov/whisper.cpp (e.g. ggml-base.bin).\nLarger models are more accurate but slower."
AdvSceneSwitcher.condition.speech.layout.buffer="Audio buffer:{{bufferDuration}}{{help}}"
AdvSceneSwitcher.condition.speech.browse="Browse..."
AdvSceneSwitcher.condition.speech.browse.title="Select Whisper model file"
AdvSceneSwitcher.condition.speech.browse.filter="GGML model files (*.bin);;All files (*)"
AdvSceneSwitcher.condition.speech.buffer.help="Longer buffer durations improve accuracy but increase latency."
AdvSceneSwitcher.condition.speech.advanced="Advanced"
AdvSceneSwitcher.condition.speech.layout.advanced.threads="Threads:{{threads}}"
AdvSceneSwitcher.condition.speech.layout.advanced.language="Language:{{language}}{{help}}"
AdvSceneSwitcher.condition.speech.advanced.language.help="Whisper language code (e.g. 'en', 'de', 'fr') or 'auto' to detect automatically."
AdvSceneSwitcher.condition.speech.layout.advanced.translate="{{translate}}{{help}}"
AdvSceneSwitcher.condition.speech.advanced.translate="Translate to English"
AdvSceneSwitcher.condition.speech.advanced.translate.help="Translate non-English speech to English before transcribing.\nUseful when the phrase or regex is written in English but the source speaks another language."
AdvSceneSwitcher.condition.speech.layout.advanced.vad="VAD energy threshold:{{vad}}{{help}}"
AdvSceneSwitcher.condition.speech.advanced.vad.help="Minimum RMS energy a buffer must have before running inference. Buffers below this level are treated as silence and skipped. Lower values are more sensitive; raise it if inference triggers on background noise."
AdvSceneSwitcher.condition.speech.layout.advanced.suppress="{{suppress}}{{help}}"
AdvSceneSwitcher.condition.speech.advanced.suppress="Suppress non-speech tokens"
AdvSceneSwitcher.condition.speech.advanced.suppress.help="Remove filler tokens such as [MUSIC] or (applause) that Whisper tends to insert when it detects non-speech sounds."
AdvSceneSwitcher.condition.speech.layout.advanced.noContext="{{noContext}}{{help}}"
AdvSceneSwitcher.condition.speech.advanced.noContext="No context"
AdvSceneSwitcher.condition.speech.advanced.noContext.help="Do not feed the previous transcription back as a prompt for the next buffer.\nPrevents repetition across buffer boundaries at the cost of slightly reduced coherence."
AdvSceneSwitcher.condition.speech.advanced.listenWhenMuted="Listen when source is muted"
AdvSceneSwitcher.condition.speech.advanced.useGpu="Use GPU"
AdvSceneSwitcher.condition.folder="Folder watch"
AdvSceneSwitcher.condition.folder.tooltip="This condition type will allow you to monitor the contents of a folder.\nNote that the monitoring will *not* recursively scan for changes in sub directories within directories of the selected folder!\nNote that if there are several changes during a short period of time, some of the changes might not emit this signal.\nHowever, the last change in the sequence of changes always will."
AdvSceneSwitcher.condition.folder.condition.any="Any change happened"
@@ -891,8 +933,18 @@ AdvSceneSwitcher.action.scene.canvasNotSupported="Not supported by the currently
AdvSceneSwitcher.action.wait="Wait"
AdvSceneSwitcher.action.wait.type.fixed="fixed"
AdvSceneSwitcher.action.wait.type.random="random"
AdvSceneSwitcher.action.wait.entry.fixed="Wait for{{waitType}}duration of{{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Wait for{{waitType}}duration from{{duration}}to{{duration2}}"
AdvSceneSwitcher.action.wait.type.variableCondition="variable"
AdvSceneSwitcher.action.wait.layout.fixed="Wait for{{waitType}}duration of{{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Wait for{{waitType}}duration from{{duration}}to{{duration2}}"
AdvSceneSwitcher.action.wait.condition.equals="equals"
AdvSceneSwitcher.action.wait.condition.doesNotEqual="does not equal"
AdvSceneSwitcher.action.wait.condition.isEmpty="is empty"
AdvSceneSwitcher.action.wait.condition.lessThan="is less than"
AdvSceneSwitcher.action.wait.condition.greaterThan="is greater than"
AdvSceneSwitcher.action.wait.layout.variableCondition="Wait until{{waitType}}{{variable}}{{condition}}{{strValue}}{{numValue}}{{regex}}"
AdvSceneSwitcher.action.wait.layout.timeout="Timeout:{{useTimeout}}{{conditionTimeout}}"
AdvSceneSwitcher.tempVar.wait.timedOut="Timed out"
AdvSceneSwitcher.tempVar.wait.timedOut.description="True if the wait action timed out before the condition was met, false if the condition was met in time"
AdvSceneSwitcher.action.audio="Audio"
AdvSceneSwitcher.action.audio.type.mute="Mute"
AdvSceneSwitcher.action.audio.type.unmute="Unmute"
@@ -932,7 +984,8 @@ AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename f
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}}{{chapterName}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.recording.outputNotActive.hint="This change will only take effect when the Recording, Replay Buffer, ... output is not active.\nThe output needs to be restarted for the change to apply."
AdvSceneSwitcher.action.recording.layout="{{actions}}{{recordFolder}}{{recordFileFormat}}{{outputNotActiveHelp}}{{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!"
@@ -963,6 +1016,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Any"
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.sceneVisibility.blockUntilTransitionDone="Wait until visibility transition is complete"
AdvSceneSwitcher.action.filter="Filter"
AdvSceneSwitcher.action.filter.type.enable="Enable"
AdvSceneSwitcher.action.filter.type.disable="Disable"
@@ -974,7 +1028,7 @@ AdvSceneSwitcher.action.filter.refresh.tooltip="Repopulate the filter settings s
AdvSceneSwitcher.action.filter.entry="On{{sources}}{{actions}}{{filters}}{{refresh}}{{settingsButtons}}"
AdvSceneSwitcher.action.filter.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.filter.getSettings="Get current settings"
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to fixed value"
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to value"
AdvSceneSwitcher.action.filter.inputMethod.individualListEntryManual="Set to list entry"
AdvSceneSwitcher.action.filter.inputMethod.individualTempvar="Set to macro property"
AdvSceneSwitcher.action.filter.inputMethod.json="Set setting JSON string"
@@ -1010,7 +1064,7 @@ AdvSceneSwitcher.action.source.deinterlaceMode.yadif="Yadif"
AdvSceneSwitcher.action.source.deinterlaceMode.yadif2x="Yadif 2x"
AdvSceneSwitcher.action.source.deinterlaceOrder.topFieldFirst="Top Field First"
AdvSceneSwitcher.action.source.deinterlaceOrder.bottomFieldFirst="Bottom Field First"
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to fixed value"
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to value"
AdvSceneSwitcher.action.source.inputMethod.individualListEntryManual="Set to list entry"
AdvSceneSwitcher.action.source.inputMethod.individualTempvar="Set to macro property"
AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
@@ -1102,6 +1156,12 @@ AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
AdvSceneSwitcher.action.macro.actionSelectionType.id="of action type"
AdvSceneSwitcher.action.loop="Loop"
AdvSceneSwitcher.action.loop.maxIterations="Max iterations"
AdvSceneSwitcher.action.loop.conditionHelp="Define the loop condition. The actions below will be repeated while these conditions are met.\n\nClick the plus button below to add a new condition."
AdvSceneSwitcher.action.loop.actionHelp="Define the actions to perform each iteration.\n\nClick the plus button below to add a new action."
AdvSceneSwitcher.tempVar.loop.count="Iteration count"
AdvSceneSwitcher.tempVar.loop.count.description="The total number of iterations completed."
AdvSceneSwitcher.action.macro.type.nestedMacro.conditionHelp="This section allows you to define macro conditions.\n\nClick the plus button below to add a new condition."
AdvSceneSwitcher.action.macro.type.nestedMacro.actionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are met.\n\nClick the plus button below to add a new action."
AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are *not* met.\n\nClick the plus button below to add a new action."
@@ -1281,6 +1341,8 @@ AdvSceneSwitcher.action.variable.type.stringLength="Set to length of string"
AdvSceneSwitcher.action.variable.type.extractJsonField="Extract JSON field with name"
AdvSceneSwitcher.action.variable.type.queryJson="Query JSON"
AdvSceneSwitcher.action.variable.type.queryJson.info="You can use \"JSONPath\" (RFC 9535) syntax here.\nSo, for example:\n\n • $['books'][0]['category']\n • $.books[0].category\n\nIf the query or the JSON should be invalid, the variable value will not be changed.\nIf the input is valid, the result of the query will always be a JSON array."
AdvSceneSwitcher.action.variable.type.queryJson.extractSingle="Extract value if single result"
AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info="When enabled and the query returns exactly one result, the value is extracted from the result array and stored directly.\nIf the query matches zero or more than one element, the full JSON array is stored as usual."
AdvSceneSwitcher.action.variable.type.accessJsonArray="Access JSON array at index"
AdvSceneSwitcher.action.variable.type.setToTempvar="Set to macro property"
AdvSceneSwitcher.action.variable.type.setToTempvar.help="This action type will allow you to extract values out of segments of the current macro and assign those values to the selected variable.\nFor example, you can get the current scene name from a scene condition and assign this name to a variable."
@@ -1313,6 +1375,7 @@ AdvSceneSwitcher.action.variable.actionNoVariableSupport="Getting variable value
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Getting variable values from %1 conditions is not supported!"
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.copy="{{actions}}{{variables2}}to{{variables}}"
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}}{{subStringRegex}}"
@@ -1352,6 +1415,8 @@ AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream langua
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.poll.start="Start poll"
AdvSceneSwitcher.action.twitch.type.poll.end="End poll"
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"
@@ -1383,6 +1448,7 @@ 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.type.channel.ads.getInfo="Get channel ad schedule"
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}}{{nonModDelayDuration}}{{channel}}{{pointsReward}}"
@@ -1396,6 +1462,10 @@ AdvSceneSwitcher.action.twitch.layout.user.ban.row1="Using account{{account}}{{a
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.layout.poll.start.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.poll.start.row2="on{{channel}}for{{duration}}"
AdvSceneSwitcher.action.twitch.layout.poll.end.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.poll.end.row2="on{{channel}}{{pollEndStatus}}"
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"
@@ -1405,6 +1475,9 @@ AdvSceneSwitcher.action.twitch.announcement.blue="Blue"
AdvSceneSwitcher.action.twitch.announcement.green="Green"
AdvSceneSwitcher.action.twitch.announcement.orange="Orange"
AdvSceneSwitcher.action.twitch.announcement.purple="Purple"
AdvSceneSwitcher.action.twitch.poll.choices="Enter one choice per line (2-10 choices, max 25 characters each)"
AdvSceneSwitcher.action.twitch.poll.end.terminated="Terminate (show results)"
AdvSceneSwitcher.action.twitch.poll.end.archived="Archive (hide results)"
AdvSceneSwitcher.action.twitch.user.getInfo.queryType.id="User Id"
AdvSceneSwitcher.action.twitch.user.getInfo.queryType.login="User login"
AdvSceneSwitcher.action.twitch.tags.add="Add Channel Tag"
@@ -1589,6 +1662,8 @@ AdvSceneSwitcher.actionQueues.invalid="Invalid action queue selection"
AdvSceneSwitcher.actionQueues.name="Name:"
AdvSceneSwitcher.actionQueues.runOnStartup="Run action queue when starting the plugin"
AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd="Resolve variables when action is inserted into the queue"
AdvSceneSwitcher.actionQueues.cloneVariableContext="Clone variable context when action is inserted into the queue"
AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip="Captures the current values of all variables when the action is added to the queue.\nDuring execution, actions read from and write to this snapshot instead of the global variables.\nUnlike \"Resolve variables\", changes made by one action in the queue are visible to subsequent actions."
AdvSceneSwitcher.actionQueues.running="Queue is running"
AdvSceneSwitcher.actionQueues.stopped="Queue is stopped"
AdvSceneSwitcher.actionQueues.start="Start action queue"
@@ -1876,8 +1951,10 @@ AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message="Subscriber Twitch
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message.description="The user login of the user who sent a resubscription chat message."
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message="Subscriber Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message.description="The user display name of the user who a resubscription chat message."
AdvSceneSwitcher.tempVar.twitch.message.subscribe="Subscription message"
AdvSceneSwitcher.tempVar.twitch.message.subscribe.description="An object that contains the resubscription message and emote information needed to recreate the message."
AdvSceneSwitcher.tempVar.twitch.message.subscribe="Subscription message (JSON)"
AdvSceneSwitcher.tempVar.twitch.message.subscribe.description="A JSON object containing the resubscription message text and emote information. Use 'Subscription message text' to get the plain text."
AdvSceneSwitcher.tempVar.twitch.message_text.subscribe="Subscription message text"
AdvSceneSwitcher.tempVar.twitch.message_text.subscribe.description="The plain text of the resubscription message, with emotes replaced by their names."
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe="Number of months subscribed"
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe.description="The total number of months the user has been subscribed to the channel."
AdvSceneSwitcher.tempVar.twitch.streak_months.subscribe="Subscription streak"
@@ -2266,6 +2343,19 @@ AdvSceneSwitcher.tempVar.twitch.requester_user_login.commercial.description="The
AdvSceneSwitcher.tempVar.twitch.requester_user_name.commercial="Requester user name"
AdvSceneSwitcher.tempVar.twitch.requester_user_name.commercial.description="The display name of the user that requested the ad."
AdvSceneSwitcher.tempVar.twitch.snooze_count.ads="Snooze count"
AdvSceneSwitcher.tempVar.twitch.snooze_count.ads.description="The number of snoozes available for the broadcaster."
AdvSceneSwitcher.tempVar.twitch.snooze_refresh_at.ads="Snooze refresh time"
AdvSceneSwitcher.tempVar.twitch.snooze_refresh_at.ads.description="The UTC timestamp of when the broadcaster will have an additional snooze available, in RFC3339 format."
AdvSceneSwitcher.tempVar.twitch.next_ad_at.ads="Next ad time"
AdvSceneSwitcher.tempVar.twitch.next_ad_at.ads.description="The UTC timestamp of when the channel's next scheduled ad break will begin, in RFC3339 format."
AdvSceneSwitcher.tempVar.twitch.duration.ads="Ad duration"
AdvSceneSwitcher.tempVar.twitch.duration.ads.description="The length in seconds of the scheduled ad break."
AdvSceneSwitcher.tempVar.twitch.last_ad_at.ads="Last ad time"
AdvSceneSwitcher.tempVar.twitch.last_ad_at.ads.description="The UTC timestamp of the channel's last ad break, in RFC3339 format."
AdvSceneSwitcher.tempVar.twitch.preroll_free_time.ads="Pre-roll free time"
AdvSceneSwitcher.tempVar.twitch.preroll_free_time.ads.description="The amount of time in seconds the broadcaster must wait before running another commercial."
AdvSceneSwitcher.tempVar.audio.output_volume="Output volume"
AdvSceneSwitcher.tempVar.audio.output_volume.description="The volume the audio source is outputting."
AdvSceneSwitcher.tempVar.audio.configured_volume="Configured volume"
@@ -2402,6 +2492,9 @@ AdvSceneSwitcher.tempVar.streaming.serviceName.description="The name of the stre
AdvSceneSwitcher.tempVar.clipboard.text="Clipboard text"
AdvSceneSwitcher.tempVar.clipboard.text.description="The text contained in the clipboard.\nWill be empty if the clipboard does not contain text."
AdvSceneSwitcher.tempVar.speech.speech="Transcribed speech"
AdvSceneSwitcher.tempVar.speech.speech.description="The text transcribed from the last audio buffer. Only populated when the condition matched."
AdvSceneSwitcher.tempVar.file.content="File content"
AdvSceneSwitcher.tempVar.file.date="File modification date"
AdvSceneSwitcher.tempVar.file.basename="File basename"
@@ -2668,7 +2761,28 @@ 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.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 will guide you through creating your first automation. On the next page, choose the type of macro you want to set up.</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.template.title="Choose an Automation Type"
FirstRunWizard.template.subtitle="Select the kind of macro you would like to create."
FirstRunWizard.template.window="Switch to a scene when a window comes into focus"
FirstRunWizard.template.sequence="Run a timed sequence of scene switches"
FirstRunWizard.template.audio="Show or hide a source based on audio activity"
FirstRunWizard.audio.source.title="Choose an Audio Source"
FirstRunWizard.audio.source.subtitle="Select the audio source to monitor and configure when it should be considered active."
FirstRunWizard.audio.source.sourceRow="Audio source:{{source}}"
FirstRunWizard.audio.source.thresholdRow="Show when volume is above{{threshold}}for{{duration}}"
FirstRunWizard.audio.target.title="Choose a Target Source"
FirstRunWizard.audio.target.subtitle="Select the source to show when audio is active and hide when it is not."
FirstRunWizard.audio.target.sourceRow="Source to show/hide:{{source}}"
FirstRunWizard.audio.review.title="Review Your Macro"
FirstRunWizard.audio.review.subtitle="Click Back to make changes, or Finish to create the macro."
FirstRunWizard.audio.review.summary="<b>When</b> <i>%1</i> output volume is above <b>%2 dB</b> for <b>%3</b>:<br><br>&nbsp;&nbsp;<b>Show</b> <i>%4</i> on the current scene<br><br><b>Otherwise:</b><br><br>&nbsp;&nbsp;<b>Hide</b> <i>%4</i> on the current scene"
FirstRunWizard.audio.review.errorTitle="Macro Creation Failed"
FirstRunWizard.audio.review.errorBody="The audio macro could not be created. Please check the OBS log for details."
FirstRunWizard.scene.title="Choose a Target Scene"
FirstRunWizard.scene.subtitle="Which OBS scene should become active when your chosen window comes into focus?"
@@ -2689,6 +2803,26 @@ FirstRunWizard.review.summary="<b>Macro name:</b> Window -> %1<br><br><b>Conditi
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.seqTrigger.title="Choose a Trigger Scene"
FirstRunWizard.seqTrigger.subtitle="The sequence will start when you have been on this scene for the specified duration."
FirstRunWizard.seqTrigger.scene="Trigger scene:{{scene}}"
FirstRunWizard.seqTrigger.delay="Start sequence after{{duration}}"
FirstRunWizard.seqScenes.title="Build Your Sequence"
FirstRunWizard.seqScenes.subtitle="Add at least two scenes. The plugin will switch through them in order, waiting the configured time between each switch."
FirstRunWizard.seqScenes.triggerInfo="When on \"%1\" for %2, switch to:"
FirstRunWizard.seqScenes.addScene="Add scene"
FirstRunWizard.seqScenes.removeTooltip="Remove this scene from the sequence"
FirstRunWizard.seqScenes.switchTo="Switch to{{scene}}"
FirstRunWizard.seqScenes.thenWait="then wait{{duration}}"
FirstRunWizard.seqReview.title="Review Your Sequence"
FirstRunWizard.seqReview.subtitle="Click Back to make changes, or Finish to create the macro."
FirstRunWizard.seqReview.trigger="When on <b>%1</b> for at least <b>%2</b>:"
FirstRunWizard.seqReview.step="After %1, switch to %2"
FirstRunWizard.seqReview.errorTitle="Macro Creation Failed"
FirstRunWizard.seqReview.errorBody="The sequence macro could not be created. Please check the OBS log for details."
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>"

View File

@@ -312,8 +312,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Espere hasta que se comp
AdvSceneSwitcher.action.wait="Esperar"
AdvSceneSwitcher.action.wait.type.fixed="fijo"
AdvSceneSwitcher.action.wait.type.random="aleatorio"
AdvSceneSwitcher.action.wait.entry.fixed="Espere {{waitType}} de {{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Espere {{waitType}} de {{duration}} a {{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Espere {{waitType}} de {{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Espere {{waitType}} de {{duration}} a {{duration2}}"
AdvSceneSwitcher.action.audio="Audio"
AdvSceneSwitcher.action.audio.type.mute="Silencio"
AdvSceneSwitcher.action.audio.type.unmute="Activar silencio"

View File

@@ -257,10 +257,10 @@ AdvSceneSwitcher.condition.video.layout.modelPath="Données du modèle (classifi
AdvSceneSwitcher.condition.video.layout.minNeighbor="Nombre minimum de voisins :{{minNeighbors}}"
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Réduire la charge CPU en effectuant la vérification uniquement toutes les{{throttleCount}}millisecondes"
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Effectuer la vérification uniquement dans la zone"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Vérifier la couleur du texte :{{textColor}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Vérifier la couleur du texte :{{color}}"
AdvSceneSwitcher.condition.video.layout.ocrTextType="Vérifier le type de texte :{{textType}}"
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Vérifier la langue :{{languageCode}}"
AdvSceneSwitcher.condition.video.layout.color="Vérifier la couleur :{{color}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.color="Vérifier la couleur :{{color}}"
AdvSceneSwitcher.condition.video.minSize="Taille minimale :"
AdvSceneSwitcher.condition.video.maxSize="Taille maximale :"
AdvSceneSwitcher.condition.video.selectArea="Sélectionner la zone"
@@ -460,8 +460,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Attendre que la transiti
AdvSceneSwitcher.action.wait="Attendre"
AdvSceneSwitcher.action.wait.type.fixed="fixe"
AdvSceneSwitcher.action.wait.type.random="aléatoire"
AdvSceneSwitcher.action.wait.entry.fixed="Attendre pour{{waitType}}une durée de{{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Attendre pour{{waitType}}une durée de{{duration}}à{{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Attendre pour{{waitType}}une durée de{{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Attendre pour{{waitType}}une durée de{{duration}}à{{duration2}}"
AdvSceneSwitcher.action.audio="Audio"
AdvSceneSwitcher.action.audio.type.mute="Muet"
AdvSceneSwitcher.action.audio.type.unmute="Activer le son"

View File

@@ -414,12 +414,12 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="最小近傍数:{{minNeighb
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}:{{throttleCount}}ミリ秒ごとにのみパフォーマンスチェックを実行することでCPU負荷を軽減します"
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="エリア内のみチェックを実施"
; AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="テキストカラーの確認:{{textColor}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="テキストカラーの確認:{{color}}"
AdvSceneSwitcher.condition.video.layout.ocrTextType="テキストタイプの確認:{{textType}}"
AdvSceneSwitcher.condition.video.layout.ocrBaseDir="Tesseractベースディレクトリ:{{tesseractBaseDir}}"
AdvSceneSwitcher.condition.video.layout.ocrLanguage="言語の確認:{{languageCode}}"
AdvSceneSwitcher.condition.video.layout.ocrConfig="設定ファイル:{{configFile}}{{openConfigFile}}{{reloadConfig}}{{configFileHint}}"
AdvSceneSwitcher.condition.video.layout.color="カラーを確認してください:{{color}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.color="カラーを確認してください:{{color}}"
AdvSceneSwitcher.condition.video.minSize="最小サイズ:"
AdvSceneSwitcher.condition.video.maxSize="最大サイズ:"
AdvSceneSwitcher.condition.video.selectArea="範囲指定"
@@ -800,8 +800,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="目的のシーンへの
; AdvSceneSwitcher.action.wait="Wait"
AdvSceneSwitcher.action.wait.type.fixed="指定"
AdvSceneSwitcher.action.wait.type.random="ランダム"
AdvSceneSwitcher.action.wait.entry.fixed="{{waitType}}期間{{duration}}待機します"
AdvSceneSwitcher.action.wait.entry.random="{{duration}}から{{duration2}}までの{{waitType}}期間待ちます"
AdvSceneSwitcher.action.wait.layout.fixed="{{waitType}}期間{{duration}}待機します"
AdvSceneSwitcher.action.wait.layout.random="{{duration}}から{{duration2}}までの{{waitType}}期間待ちます"
AdvSceneSwitcher.action.audio="音声"
AdvSceneSwitcher.action.audio.type.mute="ミュート"
AdvSceneSwitcher.action.audio.type.unmute="ミュート解除"
@@ -830,7 +830,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="注意: 最大リプレイ時間の変更は、次回再生バッファを開始したときから適用されます!"

View File

@@ -371,10 +371,10 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="Vizinhos mínimos:{{minNeig
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Reduzir carga de CPU realizando a verificação apenas a cada {{throttleCount}} milissegundos"
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Realizar verificação apenas na área"
AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Verificar cor do texto:{{textColor}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Verificar cor do texto:{{color}}"
AdvSceneSwitcher.condition.video.layout.ocrTextType="Verificar tipo de texto:{{textType}}"
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Verificar idioma:{{languageCode}}"
AdvSceneSwitcher.condition.video.layout.color="Verificar cor:{{color}}{{selectColor}}"
AdvSceneSwitcher.condition.video.layout.color="Verificar cor:{{color}}"
AdvSceneSwitcher.condition.video.minSize="Tamanho mínimo:"
AdvSceneSwitcher.condition.video.maxSize="Tamanho máximo:"
AdvSceneSwitcher.condition.video.selectArea="Selecionar área"
@@ -718,8 +718,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Aguardar até que a tran
AdvSceneSwitcher.action.wait="Aguardar"
AdvSceneSwitcher.action.wait.type.fixed="fixo"
AdvSceneSwitcher.action.wait.type.random="aleatório"
AdvSceneSwitcher.action.wait.entry.fixed="Aguardando por{{waitType}}duração de{{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Aguardando por{{waitType}}duração de{{duration}}para{{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Aguardando por{{waitType}}duração de{{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Aguardando por{{waitType}}duração de{{duration}}para{{duration2}}"
AdvSceneSwitcher.action.audio="Áudio"
AdvSceneSwitcher.action.audio.type.mute="Silenciar"
AdvSceneSwitcher.action.audio.type.unmute="Ativar som"

View File

@@ -114,8 +114,8 @@ AdvSceneSwitcher.action.scene="Переключить сцену"
AdvSceneSwitcher.action.wait="Подождать"
AdvSceneSwitcher.action.wait.type.fixed="фиксированный"
AdvSceneSwitcher.action.wait.type.random="случайный"
AdvSceneSwitcher.action.wait.entry.fixed="Ожидать {{waitType}} в течении {{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Ожидание {{waitType}} длительностью от {{duration}} до {{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Ожидать {{waitType}} в течении {{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Ожидание {{waitType}} длительностью от {{duration}} до {{duration2}}"
AdvSceneSwitcher.action.audio="Аудио"
AdvSceneSwitcher.action.audio.type.mute="Замьютить"
AdvSceneSwitcher.action.audio.type.unmute="Размьютить"

View File

@@ -249,8 +249,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Hedef sahneye geçiş ta
AdvSceneSwitcher.action.wait="Bekle"
AdvSceneSwitcher.action.wait.type.fixed="sabit"
AdvSceneSwitcher.action.wait.type.random="rastgele"
AdvSceneSwitcher.action.wait.entry.fixed="Şunu bekle {{waitType}} süre {{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Şunu bekle {{waitType}} bu aralıktan {{duration}} buna {{duration2}}"
AdvSceneSwitcher.action.wait.layout.fixed="Şunu bekle {{waitType}} süre {{duration}}"
AdvSceneSwitcher.action.wait.layout.random="Şunu bekle {{waitType}} bu aralıktan {{duration}} buna {{duration2}}"
AdvSceneSwitcher.action.audio="Ses"
AdvSceneSwitcher.action.audio.type.mute="Sessiz"
AdvSceneSwitcher.action.audio.type.unmute="Ses açmak"

File diff suppressed because it is too large Load Diff

Binary file not shown.

1
deps/date vendored

Submodule deps/date deleted from 5bdb7e6f31

2
deps/opencv vendored

1
deps/opencv_contrib vendored Submodule

Submodule deps/opencv_contrib added at 755e50675d

1
deps/whisper.cpp vendored Submodule

Submodule deps/whisper.cpp added at 306c88f4d1

View File

@@ -258,6 +258,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="settingsLock">
<property name="text">
<string>AdvSceneSwitcher.generalTab.generalBehavior.settingsLock</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -19,8 +19,10 @@
#include <obs-frontend-api.h>
#include <QAction>
#include <QDirIterator>
#include <QInputDialog>
#include <QLibrary>
#include <QMainWindow>
#include <QMessageBox>
#include <QTextStream>
#include <regex>
@@ -459,12 +461,13 @@ void SwitcherData::Stop()
SetMacroAbortWait(true);
GetMacroWaitCV().notify_all();
GetMacroTransitionCV().notify_all();
StopAllMacros();
SignalStopAllMacros();
StopAndClearAllActionQueues();
CloseAllInputDialogs();
th->wait();
delete th;
th = nullptr;
WaitForAllMacros();
RunStopSteps();
}
@@ -718,19 +721,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)

View File

@@ -1,7 +1,4 @@
#pragma once
#include "macro-segment-list.hpp"
#include "condition-logic.hpp"
#include "log-helper.hpp"
#include <ui_advanced-scene-switcher.h>
@@ -68,6 +65,7 @@ public slots:
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);
@@ -116,6 +114,8 @@ public slots:
void RenameSelectedMacro();
void ExportMacros() const;
void ImportMacros();
void PauseSelectedMacros();
void UnpauseSelectedMacros();
void HighlightOnChange() const;
void on_macroSettings_clicked();

View File

@@ -4,9 +4,10 @@
#include "filter-combo-box.hpp"
#include "first-run-wizard.hpp"
#include "layout-helpers.hpp"
#include "macro.hpp"
#include "macro-helpers.hpp"
#include "macro-search.hpp"
#include "macro-settings.hpp"
#include "macro.hpp"
#include "path-helpers.hpp"
#include "source-helpers.hpp"
#include "splitter-helpers.hpp"
@@ -18,7 +19,10 @@
#include "version.h"
#include <obs-frontend-api.h>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
namespace advss {
@@ -243,6 +247,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 =
@@ -392,7 +454,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
}
bool wasSkipped = false;
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
auto macro = wiz::FirstRunWizard::ShowWizard(this, &wasSkipped);
if (macro) {
renameMacroIfNecessary(macro);
QTimer::singleShot(0, this,
@@ -406,7 +468,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
void AdvSceneSwitcher::on_openSetupWizard_clicked()
{
auto macro = FirstRunWizard::ShowWizard(this);
auto macro = wiz::FirstRunWizard::ShowWizard(this);
if (!macro) {
return;
}
@@ -567,6 +629,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);
@@ -638,6 +703,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);
@@ -960,6 +1027,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
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

@@ -294,6 +294,30 @@ std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
result.emplace_back(std::move(info));
}
// When KWin compat is active, native Wayland windows are not tracked by
// X11 and therefore do not appear in the list above. Merge in any window
// reported by KWin that is not already present (matched by title) so that
// title and focus conditions work for native Wayland windows.
if (KWin) {
for (const auto &[pid, title] :
FocusNotifier::getWindowList()) {
bool found = false;
for (const auto &info : result) {
if (info.title == title) {
found = true;
break;
}
}
if (!found) {
WindowInfo waylandWindow;
waylandWindow.title = title;
waylandWindow.focused =
(title == foregroundTitle);
result.emplace_back(std::move(waylandWindow));
}
}
}
return result;
}

View File

@@ -12,35 +12,82 @@
namespace advss {
std::mutex FocusNotifier::_mutex;
int FocusNotifier::activePID = -1;
std::string FocusNotifier::activeTitle = {};
std::map<std::string, std::pair<int, std::string>> FocusNotifier::_windows;
int FocusNotifier::getActiveWindowPID()
{
std::lock_guard<std::mutex> lock(_mutex);
return activePID;
}
std::string FocusNotifier::getActiveWindowTitle()
{
std::lock_guard<std::mutex> lock(_mutex);
return activeTitle;
}
std::vector<std::pair<int, std::string>> FocusNotifier::getWindowList()
{
std::lock_guard<std::mutex> lock(_mutex);
std::vector<std::pair<int, std::string>> result;
result.reserve(_windows.size());
for (const auto &entry : _windows) {
result.emplace_back(entry.second);
}
return result;
}
void FocusNotifier::focusChanged(const int pid)
{
std::lock_guard<std::mutex> lock(_mutex);
activePID = pid;
}
void FocusNotifier::focusTitle(const QString &title)
{
std::lock_guard<std::mutex> lock(_mutex);
activeTitle = title.toStdString();
}
void FocusNotifier::focusUpdate(const int pid, const QString &title)
{
std::lock_guard<std::mutex> lock(_mutex);
activePID = pid;
activeTitle = title.toStdString();
}
void FocusNotifier::windowAdded(const QString &id, const int pid,
const QString &title)
{
std::lock_guard<std::mutex> lock(_mutex);
_windows[id.toStdString()] = {pid, title.toStdString()};
}
void FocusNotifier::windowRemoved(const QString &id)
{
std::lock_guard<std::mutex> lock(_mutex);
_windows.erase(id.toStdString());
}
void FocusNotifier::windowTitleChanged(const QString &id, const QString &title)
{
std::lock_guard<std::mutex> lock(_mutex);
auto it = _windows.find(id.toStdString());
if (it != _windows.end()) {
it->second.second = title.toStdString();
}
}
bool isKWinAvailable()
{
const QDBusConnectionInterface *interface =
QDBusConnection::sessionBus().interface();
if (!interface)
if (!interface) {
return false;
}
const QStringList services =
interface->registeredServiceNames().value();
@@ -53,25 +100,32 @@ bool startKWinScript(QString &scriptObjectPath)
"/tmp/AdvancedSceneSwitcher/KWinFocusNotifier.js";
const QString script =
R"(workspace.windowActivated.connect(function(client) {
if (!client) return;
if (!client.pid) return;
if (!client.caption) return;
R"(var adss = "com.github.AdvancedSceneSwitcher";
var adssPath = "/com/github/AdvancedSceneSwitcher";
callDBus(
"com.github.AdvancedSceneSwitcher",
"/com/github/AdvancedSceneSwitcher",
"com.github.AdvancedSceneSwitcher",
"focusChanged",
client.pid
);
callDBus(
"com.github.AdvancedSceneSwitcher",
"/com/github/AdvancedSceneSwitcher",
"com.github.AdvancedSceneSwitcher",
"focusTitle",
client.caption
);
function trackWindow(window) {
var id = window.internalId.toString();
callDBus(adss, adssPath, adss, "windowAdded", id, window.pid, window.caption);
window.captionChanged.connect(function() {
callDBus(adss, adssPath, adss, "windowTitleChanged", id, window.caption);
});
}
var windows = workspace.windowList();
for (var i = 0; i < windows.length; i++) {
trackWindow(windows[i]);
}
workspace.windowAdded.connect(trackWindow);
workspace.windowRemoved.connect(function(window) {
callDBus(adss, adssPath, adss, "windowRemoved", window.internalId.toString());
});
workspace.windowActivated.connect(function(client) {
callDBus(adss, adssPath, adss, "focusUpdate",
client ? client.pid : 0,
client ? client.caption : "");
}))";
if (const QDir dir; !dir.mkpath(QFileInfo(scriptPath).absolutePath())) {

View File

@@ -2,7 +2,11 @@
#include <QObject>
#include <QString>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
namespace advss {
@@ -10,18 +14,27 @@ class FocusNotifier final : public QObject {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "com.github.AdvancedSceneSwitcher")
static std::mutex _mutex;
static int activePID;
static std::string activeTitle;
// Maps KWin internal window ID to {pid, title}
static std::map<std::string, std::pair<int, std::string>> _windows;
public:
using QObject::QObject;
static int getActiveWindowPID();
static std::string getActiveWindowTitle();
static std::vector<std::pair<int, std::string>> getWindowList();
public slots:
void focusChanged(const int pid);
void focusTitle(const QString &title);
void focusUpdate(const int pid, const QString &title);
void windowAdded(const QString &id, const int pid,
const QString &title);
void windowRemoved(const QString &id);
void windowTitleChanged(const QString &id, const QString &title);
};
bool isKWinAvailable();

View File

@@ -164,4 +164,9 @@ std::shared_ptr<MacroSegment> MacroActionEdit::Data() const
return *_entryData;
}
void MacroActionEdit::FocusTypeSelection()
{
_actionSelection->setFocus();
}
} // namespace advss

View File

@@ -17,6 +17,7 @@ public:
std::shared_ptr<MacroAction> * = nullptr);
void SetupWidgets(bool basicSetup = false);
void SetEntryData(std::shared_ptr<MacroAction> *);
void FocusTypeSelection() override;
private slots:
void ActionSelectionChanged(const QString &text);

View File

@@ -0,0 +1,196 @@
#include "macro-action-loop.hpp"
#include "layout-helpers.hpp"
#include "macro-action-factory.hpp"
#include "macro.hpp"
namespace advss {
const std::string MacroActionLoop::id = "loop";
bool MacroActionLoop::_registered = MacroActionFactory::Register(
MacroActionLoop::id,
{MacroActionLoop::Create, MacroActionLoopEdit::Create,
"AdvSceneSwitcher.action.loop"});
bool MacroActionLoop::PerformAction()
{
int iterations = 0;
const int maxIterations = _maxIterations;
Macro *parentMacro = GetMacro();
if (maxIterations <= 0) {
blog(LOG_WARNING,
"loop action has invalid max iterations value of %d",
maxIterations);
return true;
}
while (_loopMacro->CheckConditions()) {
if (iterations >= maxIterations) {
blog(LOG_WARNING,
"loop action reached iteration limit of %d",
maxIterations);
break;
}
ablog(LOG_INFO, "loop iteration %d", iterations);
if (!_loopMacro->PerformActions(true, false, true)) {
break;
}
++iterations;
if (parentMacro && parentMacro->GetStop()) {
break;
}
}
ablog(LOG_INFO, "loop completed after %d iteration(s)", iterations);
SetTempVarValue("count", std::to_string(iterations));
return true;
}
void MacroActionLoop::LogAction() const
{
ablog(LOG_INFO, "running loop (max %d iterations)",
_maxIterations.GetValue());
}
bool MacroActionLoop::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_maxIterations.Save(obj, "maxIterations");
OBSDataAutoRelease loopMacroData = obs_data_create();
_loopMacro->Save(loopMacroData);
obs_data_set_obj(obj, "loopMacro", loopMacroData);
obs_data_set_int(obj, "customWidgetHeight", _customWidgetHeight);
return true;
}
bool MacroActionLoop::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_maxIterations.Load(obj, "maxIterations");
if (obs_data_has_user_value(obj, "loopMacro")) {
OBSDataAutoRelease loopMacroData =
obs_data_get_obj(obj, "loopMacro");
_loopMacro = std::make_shared<Macro>();
_loopMacro->Load(loopMacroData);
}
_customWidgetHeight = obs_data_get_int(obj, "customWidgetHeight");
return true;
}
bool MacroActionLoop::PostLoad()
{
MacroAction::PostLoad();
_loopMacro->PostLoad();
_loopMacro->SetActionTriggerMode(Macro::ActionTriggerMode::ALWAYS);
return true;
}
std::shared_ptr<MacroAction> MacroActionLoop::Create(Macro *m)
{
return std::make_shared<MacroActionLoop>(m);
}
std::shared_ptr<MacroAction> MacroActionLoop::Copy() const
{
auto copy = std::make_shared<MacroActionLoop>(*this);
OBSDataAutoRelease data = obs_data_create();
_loopMacro->Save(data);
copy->_loopMacro = std::make_shared<Macro>();
copy->_loopMacro->Load(data);
copy->_loopMacro->PostLoad();
return copy;
}
void MacroActionLoop::ResolveVariablesToFixedValues()
{
_maxIterations.ResolveVariables();
for (auto &action : _loopMacro->Actions()) {
action->ResolveVariablesToFixedValues();
}
}
void MacroActionLoop::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar("count",
obs_module_text("AdvSceneSwitcher.tempVar.loop.count"),
obs_module_text(
"AdvSceneSwitcher.tempVar.loop.count.description"));
}
MacroActionLoopEdit::MacroActionLoopEdit(
QWidget *parent, std::shared_ptr<MacroActionLoop> entryData)
: ResizableWidget(parent),
_macroEdit(new MacroEdit(
this, QStringList()
<< "AdvSceneSwitcher.action.loop.conditionHelp"
<< "AdvSceneSwitcher.action.loop.actionHelp"
<< "")),
_maxIterations(new VariableSpinBox())
{
_maxIterations->setMinimum(1);
_maxIterations->setMaximum(10000000);
QWidget::connect(
_maxIterations,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(MaxIterationsChanged(const NumberVariable<int> &)));
auto controlsLayout = new QHBoxLayout();
controlsLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.loop.maxIterations")));
controlsLayout->addWidget(_maxIterations);
controlsLayout->addStretch();
auto layout = new QVBoxLayout();
layout->addLayout(controlsLayout);
layout->addWidget(_macroEdit);
setLayout(layout);
_macroEdit->HideElseSection();
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
MacroActionLoopEdit::~MacroActionLoopEdit()
{
if (!_entryData) {
return;
}
_entryData->_customWidgetHeight = GetCustomHeight();
_macroEdit->SetMacro({});
}
void MacroActionLoopEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_maxIterations->SetValue(_entryData->_maxIterations);
_macroEdit->SetMacro(_entryData->_loopMacro);
if (_macroEdit->IsEmpty()) {
_macroEdit->ShowAllMacroSections();
_entryData->_customWidgetHeight = 600;
}
SetResizingEnabled(true);
SetCustomHeight(_entryData->_customWidgetHeight);
adjustSize();
updateGeometry();
}
void MacroActionLoopEdit::MaxIterationsChanged(const NumberVariable<int> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_maxIterations = value;
}
} // namespace advss

View File

@@ -0,0 +1,64 @@
#pragma once
#include "macro-action-edit.hpp"
#include "macro-edit.hpp"
#include "resizable-widget.hpp"
#include "variable-spinbox.hpp"
#include <QLabel>
#include <QVBoxLayout>
namespace advss {
class MacroActionLoop : public MacroAction {
public:
MacroActionLoop(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
bool PostLoad();
std::string GetId() const { return id; }
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
std::shared_ptr<Macro> _loopMacro = std::make_shared<Macro>();
IntVariable _maxIterations = 100;
int _customWidgetHeight = 0;
private:
void SetupTempVars();
static bool _registered;
static const std::string id;
};
class MacroActionLoopEdit : public ResizableWidget {
Q_OBJECT
public:
MacroActionLoopEdit(
QWidget *parent,
std::shared_ptr<MacroActionLoop> entryData = nullptr);
~MacroActionLoopEdit();
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionLoopEdit(
parent,
std::dynamic_pointer_cast<MacroActionLoop>(action));
}
private slots:
void MaxIterationsChanged(const NumberVariable<int> &value);
private:
MacroEdit *_macroEdit;
VariableSpinBox *_maxIterations;
std::shared_ptr<MacroActionLoop> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -1,8 +1,11 @@
#include "macro-action-macro.hpp"
#include "condition-logic.hpp"
#include "help-icon.hpp"
#include "layout-helpers.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp"
#include <chrono>
@@ -263,6 +266,7 @@ bool MacroActionMacro::Load(obs_data_t *obj)
if (obs_data_has_user_value(obj, "nestedMacro")) {
OBSDataAutoRelease nestedMacroData =
obs_data_get_obj(obj, "nestedMacro");
_nestedMacro = std::make_shared<Macro>();
_nestedMacro->Load(nestedMacroData);
}
@@ -309,6 +313,15 @@ void MacroActionMacro::ResolveVariablesToFixedValues()
{
_actionIndex.ResolveVariables();
_label.ResolveVariables();
if (_action == Action::NESTED_MACRO) {
for (auto &action : _nestedMacro->Actions()) {
action->ResolveVariablesToFixedValues();
}
for (auto &action : _nestedMacro->ElseActions()) {
action->ResolveVariablesToFixedValues();
}
}
}
static void runActionsHelper(Macro *macro, bool runElseActions, bool setInputs,

View File

@@ -447,7 +447,19 @@ bool MacroActionVariable::PerformAction()
if (!value.has_value()) {
return true;
}
var->SetValue(*value);
if (!_jsonQueryExtractSingle) {
var->SetValue(*value);
return true;
}
auto extracted = ExtractSingleJsonArrayElement(*value);
if (extracted.has_value()) {
var->SetValue(*extracted);
} else {
var->SetValue(*value);
}
return true;
}
case Action::ARRAY_JSON: {
@@ -551,6 +563,8 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
obs_data_set_bool(obj, "allowRepeatValues", _allowRepeatValues);
_jsonQuery.Save(obj, "jsonQuery");
_jsonIndex.Save(obj, "jsonIndex");
obs_data_set_bool(obj, "jsonQueryExtractSingle",
_jsonQueryExtractSingle);
obs_data_set_int(obj, "version", 1);
@@ -611,6 +625,8 @@ bool MacroActionVariable::Load(obs_data_t *obj)
_allowRepeatValues = obs_data_get_bool(obj, "allowRepeatValues");
_jsonQuery.Load(obj, "jsonQuery");
_jsonIndex.Load(obj, "jsonIndex");
_jsonQueryExtractSingle =
obs_data_get_bool(obj, "jsonQueryExtractSingle");
return true;
}
@@ -904,6 +920,15 @@ MacroActionVariableEdit::MacroActionVariableEdit(
"AdvSceneSwitcher.action.variable.type.queryJson.info"),
this)),
_jsonIndex(new VariableSpinBox(this)),
_jsonExtractSingle(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.extractSingle"),
this)),
_jsonExtractSingleHelp(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info"),
this)),
_jsonQueryLayout(new QVBoxLayout()),
_entryLayout(new QHBoxLayout())
{
_numValue->setMinimum(-9999999999);
@@ -935,6 +960,11 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_randomNumberEnd->setMaximum(9999999999);
_randomValues->SetMaxStringSize(99999999);
_jsonIndex->setMaximum(999);
auto jsonExtractSingleLayout = new QHBoxLayout();
jsonExtractSingleLayout->addWidget(_jsonExtractSingle);
jsonExtractSingleLayout->addWidget(_jsonExtractSingleHelp);
jsonExtractSingleLayout->addStretch();
_jsonQueryLayout->addLayout(jsonExtractSingleLayout);
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(VariableChanged(const QString &)));
@@ -1033,6 +1063,8 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_jsonIndex,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(JsonIndexChanged(const NumberVariable<int> &)));
QWidget::connect(_jsonExtractSingle, SIGNAL(stateChanged(int)), this,
SLOT(JsonQueryExtractSingleChanged(int)));
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{variables}}", _variables},
@@ -1109,6 +1141,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
auto layout = new QVBoxLayout;
layout->addLayout(_entryLayout);
layout->addLayout(_jsonQueryLayout);
layout->addLayout(_substringLayout);
layout->addWidget(_segmentValueStatus);
layout->addWidget(_segmentValue);
@@ -1180,6 +1213,7 @@ void MacroActionVariableEdit::UpdateEntryData()
_randomValues->SetStringList(_entryData->_randomValues);
_jsonQuery->setText(_entryData->_jsonQuery);
_jsonIndex->SetValue(_entryData->_jsonIndex);
_jsonExtractSingle->setChecked(_entryData->_jsonQueryExtractSingle);
SetWidgetVisibility();
}
@@ -1545,6 +1579,12 @@ void MacroActionVariableEdit::JsonIndexChanged(const NumberVariable<int> &value)
_entryData->_jsonIndex = value;
}
void MacroActionVariableEdit::JsonQueryExtractSingleChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_jsonQueryExtractSingle = value;
}
void MacroActionVariableEdit::SetWidgetVisibility()
{
if (!_entryData) {
@@ -1593,6 +1633,10 @@ void MacroActionVariableEdit::SetWidgetVisibility()
MacroActionVariable::Action::TRUNCATE) {
layoutString = obs_module_text(
"AdvSceneSwitcher.action.variable.layout.truncate");
} else if (_entryData->_action ==
MacroActionVariable::Action::COPY_VAR) {
layoutString = obs_module_text(
"AdvSceneSwitcher.action.variable.layout.copy");
} else {
layoutString = obs_module_text(
"AdvSceneSwitcher.action.variable.layout.other");
@@ -1740,6 +1784,9 @@ void MacroActionVariableEdit::SetWidgetVisibility()
MacroActionVariable::Action::QUERY_JSON);
_jsonIndex->setVisible(_entryData->_action ==
MacroActionVariable::Action::ARRAY_JSON);
SetLayoutVisible(_jsonQueryLayout,
_entryData->_action ==
MacroActionVariable::Action::QUERY_JSON);
adjustSize();
updateGeometry();

View File

@@ -116,6 +116,7 @@ public:
StringVariable _jsonQuery = "$.some.nested.value";
IntVariable _jsonIndex = 0;
bool _jsonQueryExtractSingle = true;
private:
void DecrementCurrentSegmentVariableRef();
@@ -187,6 +188,7 @@ private slots:
void AllowRepeatValuesChanged(int);
void JsonQueryChanged();
void JsonIndexChanged(const NumberVariable<int> &);
void JsonQueryExtractSingleChanged(int);
signals:
void HeaderInfoChanged(const QString &);
@@ -239,8 +241,11 @@ private:
QCheckBox *_allowRepeatValues;
QVBoxLayout *_randomValueLayout;
VariableLineEdit *_jsonQuery;
QLabel *_jsonQueryHelp;
HelpIcon *_jsonQueryHelp;
VariableSpinBox *_jsonIndex;
QCheckBox *_jsonExtractSingle;
HelpIcon *_jsonExtractSingleHelp;
QVBoxLayout *_jsonQueryLayout;
QHBoxLayout *_entryLayout;
std::shared_ptr<MacroActionVariable> _entryData;

View File

@@ -1,7 +1,9 @@
#include "macro-condition-edit.hpp"
#include "advanced-scene-switcher.hpp"
#include "macro-signals.hpp"
#include "condition-logic.hpp"
#include "macro-settings.hpp"
#include "macro-signals.hpp"
#include "macro.hpp"
#include "path-helpers.hpp"
#include "plugin-state-helpers.hpp"
@@ -305,4 +307,9 @@ std::shared_ptr<MacroSegment> MacroConditionEdit::Data() const
return *_entryData;
}
void MacroConditionEdit::FocusTypeSelection()
{
_conditionSelection->setFocus();
}
} // namespace advss

View File

@@ -43,6 +43,7 @@ public:
void SetRootNode(bool);
void SetupWidgets(bool basicSetup = false);
void SetEntryData(std::shared_ptr<MacroCondition> *);
void FocusTypeSelection() override;
private slots:
void LogicSelectionChanged(int idx);

View File

@@ -1,6 +1,9 @@
#include "macro-condition-macro.hpp"
#include "layout-helpers.hpp"
#include "macro-action-edit.hpp"
#include "macro-helpers.hpp"
#include "macro-ref.hpp"
#include "macro-signals.hpp"
#include "macro.hpp"

View File

@@ -1,5 +1,7 @@
#include "macro-condition.hpp"
#include "condition-logic.hpp"
namespace advss {
MacroCondition::MacroCondition(Macro *m, bool supportsVariableValue)

View File

@@ -1,8 +1,11 @@
#include "macro-dock-settings.hpp"
#include "macro-dock.hpp"
#include "macro-dock-window.hpp"
#include "macro-dock.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp"
#include "plugin-state-helpers.hpp"
#include "variable-string.hpp"
#include <obs-frontend-api.h>
#include <util/platform.h>

View File

@@ -1,17 +1,20 @@
#include "macro-edit.hpp"
#include "condition-logic.hpp"
#include "cursor-shape-changer.hpp"
#include "macro.hpp"
#include "macro-action-edit.hpp"
#include "macro-action-macro.hpp"
#include "macro-condition-edit.hpp"
#include "macro-helpers.hpp"
#include "macro-segment-copy-paste.hpp"
#include "macro-segment-list.hpp"
#include "macro-settings.hpp"
#include "macro-signals.hpp"
#include "macro.hpp"
#include "math-helpers.hpp"
#include "name-dialog.hpp"
#include "path-helpers.hpp"
#include "obs-module-helper.hpp"
#include "path-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "splitter-helpers.hpp"
#include "tab-helpers.hpp"
@@ -650,6 +653,18 @@ void MacroEdit::HighlightCondition(int idx, QColor color) const
ui->conditionsList->Highlight(idx, color);
}
void MacroEdit::ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
int *currentIdx)
{
QTimer::singleShot(0, this, [this, list, newIdx, currentIdx]() {
list->ensureWidgetVisible(list->WidgetAt(*currentIdx));
auto *widget = list->WidgetAt(newIdx);
if (widget) {
widget->FocusTypeSelection();
}
});
}
static void resetSegmentHighlights(MacroSegmentList *list)
{
MacroSegmentEdit *widget = nullptr;
@@ -1087,10 +1102,7 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
ui->actionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->actionsList->ensureWidgetVisible(
ui->actionsList->WidgetAt(currentActionIdx));
});
ScrollAndFocusNewSegment(ui->actionsList, idx, &currentActionIdx);
}
void MacroEdit::AddMacroAction(int idx)
@@ -1398,10 +1410,8 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
ui->elseActionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->elseActionsList->ensureWidgetVisible(
ui->elseActionsList->WidgetAt(currentElseActionIdx));
});
ScrollAndFocusNewSegment(ui->elseActionsList, idx,
&currentElseActionIdx);
}
void MacroEdit::AddMacroElseAction(int idx)
@@ -1611,10 +1621,7 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
ui->conditionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->conditionsList->ensureWidgetVisible(
ui->conditionsList->WidgetAt(currentConditionIdx));
});
ScrollAndFocusNewSegment(ui->conditionsList, idx, &currentConditionIdx);
}
void MacroEdit::on_conditionAdd_clicked()
@@ -1846,6 +1853,13 @@ bool MacroEdit::IsEmpty() const
ui->elseActionsList->IsEmpty();
}
void MacroEdit::HideElseSection() const
{
ui->toggleElseActions->setVisible(false);
ui->macroElseActions->setMaximumHeight(0);
ui->macroElseActionSplitter->handle(1)->setCursor(Qt::ArrowCursor);
}
void MacroEdit::ShowAllMacroSections()
{
if (!ElseSectionIsVisible()) {

View File

@@ -34,6 +34,7 @@ public:
void PasteMacroSegment();
bool IsEmpty() const;
void ShowAllMacroSections();
void HideElseSection() const;
private slots:
void on_conditionAdd_clicked();
@@ -116,6 +117,8 @@ private:
void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0);
void PopulateMacroElseActions(Macro &m, uint32_t afterIdx = 0);
void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0);
void ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
int *currentIdx);
void SetupMacroSegmentSelection(MacroSection type, int idx);
void
SetupContextMenu(const QPoint &pos,

View File

@@ -1,11 +1,14 @@
#include "macro-export-import-dialog.hpp"
#include "macro-export-extensions.hpp"
#include "obs-module-helper.hpp"
#include "section.hpp"
#include <obs.hpp>
#include <QDialogButtonBox>
#include <QLabel>
#include <QLayout>
#include <QScrollArea>
#include <QSplitter>
#include <algorithm>
#include <numeric>
@@ -44,10 +47,14 @@ static bool isValidData(const QString &json)
// Extension section
// ---------------------------------------------------------------------------
Section *MacroExportImportDialog::BuildExtensionSection()
QWidget *MacroExportImportDialog::BuildExtensionWidget()
{
auto outerSection = new Section(300, this);
outerSection->AddHeaderWidget(new QLabel(obs_module_text(
auto wrapper = new QWidget(this);
auto wrapperLayout = new QVBoxLayout(wrapper);
wrapperLayout->setContentsMargins(0, 0, 0, 0);
wrapperLayout->setSpacing(4);
wrapperLayout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.macroTab.export.additionalContent")));
auto outerContent = new QWidget();
@@ -57,7 +64,6 @@ Section *MacroExportImportDialog::BuildExtensionSection()
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) {
@@ -81,6 +87,7 @@ Section *MacroExportImportDialog::BuildExtensionSection()
&MacroExportImportDialog::UpdateExportString);
outerLayout->addWidget(ui.mainCheck);
_extensionUIs.append(std::move(ui));
_extensionOrder.push_back(extIdx);
continue;
}
@@ -88,11 +95,18 @@ Section *MacroExportImportDialog::BuildExtensionSection()
// 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();
auto items = ext.getExportItems();
if (items.isEmpty()) {
continue;
}
std::sort(items.begin(), items.end(),
[](const QPair<QString, QString> &a,
const QPair<QString, QString> &b) {
return QString::localeAwareCompare(
a.second, b.second) < 0;
});
auto innerSection = new Section(200, outerContent);
ui.mainCheck =
@@ -132,13 +146,22 @@ Section *MacroExportImportDialog::BuildExtensionSection()
UpdateExportString();
});
innerSection->SetContent(subWidget, false);
innerSection->SetContent(subWidget, true);
outerLayout->addWidget(innerSection);
_extensionUIs.append(std::move(ui));
_extensionOrder.push_back(extIdx);
}
outerSection->SetContent(outerContent, true);
return outerSection;
outerLayout->addStretch();
auto scrollArea = new QScrollArea(wrapper);
scrollArea->setWidget(outerContent);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setFrameShape(QFrame::NoFrame);
wrapperLayout->addWidget(scrollArea);
return wrapper;
}
// ---------------------------------------------------------------------------
@@ -180,12 +203,25 @@ MacroExportImportDialog::MacroExportImportDialog(Type type,
auto layout = new QVBoxLayout(this);
if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) {
layout->addWidget(BuildExtensionSection());
auto splitter = new QSplitter(Qt::Vertical, this);
splitter->setChildrenCollapsible(false);
splitter->addWidget(BuildExtensionWidget());
auto textWidget = new QWidget(splitter);
auto textLayout = new QVBoxLayout(textWidget);
textLayout->setContentsMargins(0, 0, 0, 0);
textLayout->addWidget(label);
textLayout->addWidget(_importExportString);
textLayout->addWidget(_usePlainText);
splitter->addWidget(textWidget);
layout->addWidget(splitter);
} else {
layout->addWidget(label);
layout->addWidget(_importExportString);
layout->addWidget(_usePlainText);
}
layout->addWidget(label);
layout->addWidget(_importExportString);
layout->addWidget(_usePlainText);
layout->addWidget(buttons);
setLayout(layout);
@@ -209,19 +245,26 @@ QString MacroExportImportDialog::BuildExportJson() const
}
const auto &extensions = GetMacroExportExtensions();
for (int i = 0;
i < (int)extensions.size() && i < (int)_extensionUIs.size(); ++i) {
if (!_extensionUIs[i].mainCheck->isChecked()) {
continue;
}
for (int i = 0; i < (int)_extensionUIs.size(); ++i) {
const auto &ui = _extensionUIs[i];
QStringList selectedIds;
for (const auto &[id, cb] : _extensionUIs[i].itemChecks) {
if (cb->isChecked()) {
selectedIds << id;
if (ui.itemChecks.isEmpty()) {
if (!ui.mainCheck->isChecked()) {
continue;
}
} else {
for (const auto &[id, cb] : ui.itemChecks) {
if (cb->isChecked()) {
selectedIds << id;
}
}
if (selectedIds.isEmpty()) {
continue;
}
}
extensions[i].save(data, selectedIds);
extensions[_extensionOrder[i]].save(data, selectedIds);
}
const char *json = obs_data_get_json(data);

View File

@@ -1,13 +1,12 @@
#pragma once
#include "section.hpp"
#include <QCheckBox>
#include <QDialog>
#include <QList>
#include <QPlainTextEdit>
#include <obs-data.h>
#include <vector>
namespace advss {
@@ -34,7 +33,7 @@ private:
QList<QPair<QString, QCheckBox *>> itemChecks;
};
Section *BuildExtensionSection();
QWidget *BuildExtensionWidget();
QString BuildExportJson() const;
void RefreshExportText();
@@ -42,6 +41,7 @@ private:
QPlainTextEdit *_importExportString;
QCheckBox *_usePlainText;
QList<ExtensionUI> _extensionUIs;
std::vector<int> _extensionOrder;
};
} // namespace advss

View File

@@ -76,7 +76,8 @@ EXPORT bool RunMacroElseActions(Macro *, bool forceParallel = false,
bool ignorePause = false);
EXPORT bool RunMacros();
void StopAllMacros();
void SignalStopAllMacros();
void WaitForAllMacros();
EXPORT void LoadMacros(obs_data_t *obj);
EXPORT void SaveMacros(obs_data_t *obj);

View File

@@ -1,6 +1,7 @@
#include "macro-ref.hpp"
#include "macro.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp"
#include "plugin-state-helpers.hpp"
namespace advss {

View File

@@ -108,7 +108,7 @@ void MacroRunButton::Pressed()
const bool abortMacro = DisplayMessage(
err.arg(QString::fromStdString(macro->Name())), true);
if (abortMacro) {
macro->Stop();
macro->SignalStop();
}
}

View File

@@ -1,5 +1,7 @@
#include "macro-segment-copy-paste.hpp"
#include "advanced-scene-switcher.hpp"
#include "condition-logic.hpp"
#include "macro.hpp"
#include <QShortcut>

View File

@@ -1,4 +1,5 @@
#include "macro-segment.hpp"
#include "macro.hpp"
#include "mouse-wheel-guard.hpp"
#include "path-helpers.hpp"
@@ -8,10 +9,13 @@
#include <QApplication>
#include <QEvent>
#include <QFrame>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollBar>
#include <QVBoxLayout>
namespace advss {

View File

@@ -7,15 +7,17 @@
#include "sync-helpers.hpp"
#include "temp-variable.hpp"
#include <QWidget>
#include <QFrame>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTimer>
#include <obs-data.h>
#include <QHBoxLayout>
#include <QPushButton>
#include <QWidget>
#include <memory>
class QFrame;
class QLabel;
class QVBoxLayout;
namespace advss {
@@ -134,6 +136,7 @@ public:
void SetSelected(bool);
virtual std::shared_ptr<MacroSegment> Data() const = 0;
virtual void SetupWidgets(bool basicSetup = false) = 0;
virtual void FocusTypeSelection() {}
void SetupVarMappings(MacroSegment *segment);

View File

@@ -1,8 +1,11 @@
#include "macro-selection.hpp"
#include "advanced-scene-switcher.hpp"
#include "layout-helpers.hpp"
#include "macro.hpp"
#include "macro-helpers.hpp"
#include "macro-ref.hpp"
#include "macro-signals.hpp"
#include "macro.hpp"
#include "ui-helpers.hpp"
#include <QDialogButtonBox>

View File

@@ -2,8 +2,9 @@
#include "macro-action-edit.hpp"
#include "macro-condition-edit.hpp"
#include "macro-export-import-dialog.hpp"
#include "macro-settings.hpp"
#include "macro-helpers.hpp"
#include "macro-search.hpp"
#include "macro-settings.hpp"
#include "macro-signals.hpp"
#include "macro-tree.hpp"
#include "macro.hpp"
@@ -17,6 +18,7 @@
#include "version.h"
#include <obs-frontend-api.h>
#include <QColor>
#include <QMenu>
@@ -395,7 +397,7 @@ void AdvSceneSwitcher::ImportMacros()
std::vector<std::shared_ptr<Macro>> importedMacros;
auto &tempMacros = GetTemporaryMacros();
auto lock = LockContext();
std::unique_lock<std::mutex> lock(*GetMutex());
for (size_t i = 0; i < count; i++) {
tempMacros.clear();
@@ -405,10 +407,14 @@ void AdvSceneSwitcher::ImportMacros()
macro->Load(array_obj);
RunAndClearPostLoadSteps();
if (macroNameExists(macro->Name()) &&
!ResolveMacroImportNameConflict(macro)) {
groupSize--;
continue;
if (macroNameExists(macro->Name())) {
lock.unlock();
bool resolved = ResolveMacroImportNameConflict(macro);
lock.lock();
if (!resolved) {
groupSize--;
continue;
}
}
importedMacros.emplace_back(macro);
@@ -739,6 +745,17 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
remove->setDisabled(ui->macros->SelectionEmpty());
menu.addSeparator();
auto pause = menu.addAction(
obs_module_text("AdvSceneSwitcher.macroTab.pause"), this,
&AdvSceneSwitcher::PauseSelectedMacros);
pause->setDisabled(ui->macros->SelectionEmpty());
auto unpause = menu.addAction(
obs_module_text("AdvSceneSwitcher.macroTab.unpause"), this,
&AdvSceneSwitcher::UnpauseSelectedMacros);
unpause->setDisabled(ui->macros->SelectionEmpty());
menu.addSeparator();
auto group = menu.addAction(
obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros,
&MacroTree::GroupSelectedItems);
@@ -799,6 +816,48 @@ void AdvSceneSwitcher::CopyMacro()
MacroSignalManager::Instance()->Add(QString::fromStdString(name));
}
void AdvSceneSwitcher::PauseSelectedMacros()
{
auto selectedMacros = GetSelectedMacros();
if (selectedMacros.empty()) {
return;
}
auto lock = LockContext();
for (const auto &macro : selectedMacros) {
if (macro->IsGroup()) {
for (const auto &subitem :
GetGroupMacroEntries(macro.get())) {
subitem->SetPaused(true);
}
} else {
macro->SetPaused(true);
}
}
ui->macros->UpdateRunningStates();
}
void AdvSceneSwitcher::UnpauseSelectedMacros()
{
auto selectedMacros = GetSelectedMacros();
if (selectedMacros.empty()) {
return;
}
auto lock = LockContext();
for (const auto &macro : selectedMacros) {
if (macro->IsGroup()) {
for (const auto &subitem :
GetGroupMacroEntries(macro.get())) {
subitem->SetPaused(false);
}
} else {
macro->SetPaused(false);
}
}
ui->macros->UpdateRunningStates();
}
bool MacroTabIsInFocus()
{
return AdvSceneSwitcher::window &&

View File

@@ -1,21 +1,25 @@
#include "macro-tree.hpp"
#include "macro.hpp"
#include "macro-helpers.hpp"
#include "macro-search.hpp"
#include "macro-signals.hpp"
#include "macro.hpp"
#include "path-helpers.hpp"
#include "sync-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs.h>
#include <string>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QSpacerItem>
#include <QStylePainter>
#include <QToolTip>
#include <QVBoxLayout>
#include <string>
Q_DECLARE_METATYPE(std::shared_ptr<advss::Macro>);
@@ -977,6 +981,17 @@ void MacroTree::UpdateWidgets(bool force)
}
}
void MacroTree::UpdateRunningStates()
{
MacroTreeModel *mtm = GetModel();
for (int i = 0; i < (int)mtm->_macros.size(); i++) {
auto widget = GetItemWidget(i);
if (widget) {
widget->UpdateRunning();
}
}
}
static inline void MoveItem(std::deque<std::shared_ptr<Macro>> &items,
std::shared_ptr<Macro> &item, int to)
{

View File

@@ -145,6 +145,7 @@ public:
void ExpandGroup(std::shared_ptr<Macro> item) const;
void CollapseGroup(std::shared_ptr<Macro> item) const;
void RefreshFilter();
void UpdateRunningStates();
public slots:
void GroupSelectedItems();

View File

@@ -1,4 +1,6 @@
#include "macro.hpp"
#include "condition-logic.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include "macro-helpers.hpp"
@@ -7,12 +9,14 @@
#include "splitter-helpers.hpp"
#include "sync-helpers.hpp"
#include <obs-frontend-api.h>
#include <QAction>
#include <QMainWindow>
#include <chrono>
#include <limits>
#undef max
#include <obs-frontend-api.h>
#include <QAction>
#include <QMainWindow>
#include <unordered_map>
namespace advss {
@@ -552,6 +556,12 @@ Macro::PauseStateSaveBehavior Macro::GetPauseStateSaveBehavior() const
return _pauseSaveBehavior;
}
void Macro::SignalStop()
{
_stop = true;
GetMacroWaitCV().notify_all();
}
void Macro::Stop()
{
_stop = true;
@@ -1316,7 +1326,14 @@ bool RunMacros()
return true;
}
void StopAllMacros()
void SignalStopAllMacros()
{
for (const auto &m : GetAllMacros()) {
m->SignalStop();
}
}
void WaitForAllMacros()
{
for (const auto &m : GetAllMacros()) {
m->Stop();

View File

@@ -1,21 +1,20 @@
#pragma once
#include "macro-action.hpp"
#include "macro-condition.hpp"
#include "macro-dock-settings.hpp"
#include "macro-helpers.hpp"
#include "macro-input.hpp"
#include "macro-ref.hpp"
#include "variable-string.hpp"
#include "temp-variable.hpp"
#include <future>
#include <string>
#include <deque>
#include <memory>
#include <map>
#include <thread>
#include <obs.hpp>
#include <obs-module-helper.hpp>
#include <QList>
#include <deque>
#include <future>
#include <memory>
#include <string>
#include <thread>
namespace advss {
@@ -61,6 +60,7 @@ public:
PauseStateSaveBehavior GetPauseStateSaveBehavior() const;
void Stop();
void SignalStop();
bool GetStop() const { return _stop; }
void ResetTimers();
@@ -224,12 +224,7 @@ private:
MacroDockSettings _dockSettings;
};
void LoadMacros(obs_data_t *obj);
void SaveMacros(obs_data_t *obj);
bool CheckMacros();
bool RunMacros();
void StopAllMacros();
void InvalidateMacroTempVarValues();
void WaitForAllMacros();
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval();
} // namespace advss

40
lib/pch.hpp Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
// OBS
#include <obs-data.h>
#include <obs-frontend-api.h>
#include <obs.hpp>
// Qt
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDoubleSpinBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QList>
#include <QPushButton>
#include <QSpinBox>
#include <QString>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
// Standard library
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>

View File

@@ -3,6 +3,9 @@
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <QGridLayout>
#include <QHBoxLayout>
namespace advss {
static std::deque<std::shared_ptr<Item>> queues;
@@ -42,6 +45,7 @@ void ActionQueue::Save(obs_data_t *obj) const
obs_data_set_string(obj, "name", _name.c_str());
obs_data_set_bool(obj, "runOnStartup", _runOnStartup);
obs_data_set_bool(obj, "resolveVariablesOnAdd", _resolveVariablesOnAdd);
obs_data_set_bool(obj, "cloneVariableContext", _cloneVariableContext);
}
void ActionQueue::Load(obs_data_t *obj)
@@ -51,6 +55,7 @@ void ActionQueue::Load(obs_data_t *obj)
_runOnStartup = obs_data_get_bool(obj, "runOnStartup");
_resolveVariablesOnAdd =
obs_data_get_bool(obj, "resolveVariablesOnAdd");
_cloneVariableContext = obs_data_get_bool(obj, "cloneVariableContext");
if (_runOnStartup) {
Start();
@@ -111,9 +116,17 @@ void ActionQueue::Add(const std::shared_ptr<MacroAction> &action)
copy->PostLoad();
RunAndClearPostLoadSteps();
copy->ResolveVariablesToFixedValues();
_actions.emplace_back(copy);
_actions.push_back({copy, {}});
} else if (_cloneVariableContext) {
auto copy = action->Copy();
OBSDataAutoRelease data = obs_data_create();
action->Save(data);
copy->Load(data);
copy->PostLoad();
RunAndClearPostLoadSteps();
_actions.push_back({copy, CreateVariableContext()});
} else {
_actions.emplace_back(action);
_actions.push_back({action, {}});
}
_cv.notify_all();
}
@@ -140,7 +153,7 @@ size_t ActionQueue::Size()
void ActionQueue::RunActions()
{
std::shared_ptr<MacroAction> action;
QueueEntry entry;
while (true) {
{ // Grab next action to run
std::unique_lock<std::mutex> lock(_mutex);
@@ -153,20 +166,25 @@ void ActionQueue::RunActions()
if (_stop) {
return;
}
action = _actions.front();
entry = _actions.front();
_actions.pop_front();
}
if (!action) {
if (!entry.action) {
continue;
}
if (ActionLoggingEnabled()) {
blog(LOG_INFO, "Performing action '%s' in queue '%s'",
action->GetId().c_str(), _name.c_str());
action->LogAction();
entry.action->GetId().c_str(), _name.c_str());
entry.action->LogAction();
}
action->PerformAction();
if (entry.context) {
SetActiveVariableContext(&*entry.context);
}
entry.action->PerformAction();
SetActiveVariableContext(nullptr);
}
}
@@ -184,6 +202,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
obs_module_text("AdvSceneSwitcher.actionQueues.clear"))),
_runOnStartup(new QCheckBox()),
_resolveVariablesOnAdd(new QCheckBox()),
_cloneVariableContext(new QCheckBox()),
_queue(settings)
{
QWidget::connect(_startStopToggle, SIGNAL(clicked()), this,
@@ -192,6 +211,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
_runOnStartup->setChecked(settings._runOnStartup);
_resolveVariablesOnAdd->setChecked(settings._resolveVariablesOnAdd);
_cloneVariableContext->setChecked(settings._cloneVariableContext);
UpdateLabels();
auto layout = new QGridLayout();
@@ -221,6 +241,14 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
_resolveVariablesOnAdd->setToolTip(obs_module_text(
"AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd"));
++row;
layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.actionQueues.cloneVariableContext")),
row, 0);
layout->addWidget(_cloneVariableContext, row, 1);
_cloneVariableContext->setToolTip(obs_module_text(
"AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip"));
++row;
layout->addWidget(_queueRunStatus, row, 0);
layout->addWidget(_startStopToggle, row, 1);
++row;
@@ -250,6 +278,8 @@ bool ActionQueueSettingsDialog::AskForSettings(QWidget *parent,
settings._runOnStartup = dialog._runOnStartup->isChecked();
settings._resolveVariablesOnAdd =
dialog._resolveVariablesOnAdd->isChecked();
settings._cloneVariableContext =
dialog._cloneVariableContext->isChecked();
return true;
}

View File

@@ -1,11 +1,13 @@
#pragma once
#include "item-selection-helpers.hpp"
#include "macro-action.hpp"
#include "variable.hpp"
#include <chrono>
#include <condition_variable>
#include <deque>
#include <obs-data.h>
#include <optional>
#include <QCheckBox>
#include <thread>
@@ -17,6 +19,11 @@ class ActionQueueSettingsDialog;
class ActionQueue : public Item {
using TimePoint = std::chrono::high_resolution_clock::time_point;
struct QueueEntry {
std::shared_ptr<MacroAction> action;
std::optional<VariableContext> context;
};
public:
ActionQueue();
~ActionQueue();
@@ -43,11 +50,12 @@ private:
bool _runOnStartup = true;
bool _resolveVariablesOnAdd = true;
bool _cloneVariableContext = false;
std::atomic_bool _stop = {true};
std::mutex _mutex;
std::condition_variable _cv;
std::thread _thread;
std::deque<std::shared_ptr<MacroAction>> _actions;
std::deque<QueueEntry> _actions;
TimePoint _lastEmpty;
friend ActionQueueSelection;
@@ -73,6 +81,7 @@ private:
QPushButton *_clear;
QCheckBox *_runOnStartup;
QCheckBox *_resolveVariablesOnAdd;
QCheckBox *_cloneVariableContext;
ActionQueue &_queue;
};

View File

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

@@ -0,0 +1,425 @@
#include "first-run-wizard-audio.hpp"
#include "first-run-wizard-helpers.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "macro-settings.hpp"
#include "selection-helpers.hpp"
#include <obs-data.h>
#include <obs.hpp>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout>
#include <QWidget>
namespace advss {
namespace wiz {
// Builds the obs_data blob for an audio volume condition,
// matching MacroConditionAudio::Save() output.
//
// Checks output volume (checkType 0) ABOVE (outputCondition 0) the given
// threshold in dB, held for at least `duration`.
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "audio",
// "logic": 0,
// "durationModifier": {
// "time_constraint": 1, // AT_LEAST
// "seconds": <Duration::Save output>
// },
// "audioSource": { "type": 0, "name": "<source>" },
// "monitor": 0,
// "volume": { "value": 0.0, "type": 0 },
// "syncOffset": { "value": 0, "type": 0 },
// "balance": { "value": 0.5, "type": 0 },
// "checkType": 0, // OUTPUT_VOLUME
// "outputCondition": 0, // ABOVE
// "volumeCondition": 0,
// "useDb": true,
// "volumeDB": { "value": <thresholdDb>, "type": 0 },
// "version": 3
// }
static OBSDataAutoRelease buildAudioConditionData(const QString &sourceName,
double thresholdDb,
const Duration &duration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease durMod = obs_data_create();
obs_data_set_int(durMod, "time_constraint", 1);
duration.Save(durMod, "seconds");
OBSDataAutoRelease audioSrc = obs_data_create();
obs_data_set_int(audioSrc, "type", 0);
obs_data_set_string(audioSrc, "name", sourceName.toUtf8().constData());
OBSDataAutoRelease volume = obs_data_create();
obs_data_set_double(volume, "value", 0.0);
obs_data_set_int(volume, "type", 0);
OBSDataAutoRelease syncOffset = obs_data_create();
obs_data_set_int(syncOffset, "value", 0);
obs_data_set_int(syncOffset, "type", 0);
OBSDataAutoRelease balance = obs_data_create();
obs_data_set_double(balance, "value", 0.5);
obs_data_set_int(balance, "type", 0);
OBSDataAutoRelease volumeDB = obs_data_create();
obs_data_set_double(volumeDB, "value", thresholdDb);
obs_data_set_int(volumeDB, "type", 0);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "audio");
obs_data_set_int(data, "logic", 0);
obs_data_set_obj(data, "durationModifier", durMod);
obs_data_set_obj(data, "audioSource", audioSrc);
obs_data_set_int(data, "monitor", 0);
obs_data_set_obj(data, "volume", volume);
obs_data_set_obj(data, "syncOffset", syncOffset);
obs_data_set_obj(data, "balance", balance);
obs_data_set_int(data, "checkType", 0);
obs_data_set_int(data, "outputCondition", 0);
obs_data_set_int(data, "volumeCondition", 0);
obs_data_set_bool(data, "useDb", true);
obs_data_set_obj(data, "volumeDB", volumeDB);
obs_data_set_int(data, "version", 3);
return data;
}
// Builds the obs_data blob for a scene-visibility action on the current scene,
// matching MacroActionSceneVisibility::Save() output.
//
// sceneSelection type 3 = current scene.
// action 0 = SHOW, action 1 = HIDE.
static OBSDataAutoRelease buildVisibilityActionData(const QString &sourceName,
int action)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 3); // current scene
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease itemSel = obs_data_create();
obs_data_set_int(itemSel, "type", 0);
obs_data_set_int(itemSel, "idxType", 0);
obs_data_set_int(itemSel, "idx", 0);
obs_data_set_string(itemSel, "item", sourceName.toUtf8().constData());
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene_visibility");
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_obj(data, "sceneItemSelection", itemSel);
obs_data_set_bool(data, "updateTransition", false);
obs_data_set_int(data, "transitionType", 0);
obs_data_set_string(data, "transition", "");
obs_data_set_bool(data, "updateDuration", false);
Duration().Save(data, "duration");
obs_data_set_int(data, "action", action);
return data;
}
// ===========================================================================
// AudioSourcePage
// ===========================================================================
AudioSourcePage::AudioSourcePage(QWidget *parent)
: QWizardPage(parent),
_sourceCombo(new QComboBox(this)),
_thresholdSpinbox(new QDoubleSpinBox(this)),
_durationSelection(new DurationSelection(this, true, 0.0)),
_volmeterLayout(new QVBoxLayout)
{
setTitle(obs_module_text("FirstRunWizard.audio.source.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.source.subtitle"));
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
_thresholdSpinbox->setRange(-100.0, 0.0);
_thresholdSpinbox->setValue(-24.0);
_thresholdSpinbox->setDecimals(1);
_thresholdSpinbox->setSingleStep(1.0);
_thresholdSpinbox->setSuffix(" dB");
_durationSelection->SetDuration(Duration(1.0));
registerField("audioSourceName*", _sourceCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&AudioSourcePage::UpdateVolmeter);
connect(_thresholdSpinbox,
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
&AudioSourcePage::SyncSliderFromSpinbox);
auto *sourceRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.audio.source.sourceRow"),
sourceRow, {{"{{source}}", _sourceCombo}}, false);
auto *thresholdRow = new QHBoxLayout;
PlaceWidgets(
obs_module_text("FirstRunWizard.audio.source.thresholdRow"),
thresholdRow,
{{"{{threshold}}", _thresholdSpinbox},
{"{{duration}}", _durationSelection}},
false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(sourceRow);
layout->addLayout(thresholdRow);
layout->addLayout(_volmeterLayout);
layout->addStretch();
}
void AudioSourcePage::initializePage()
{
_sourceCombo->clear();
PopulateAudioSelection(_sourceCombo, false);
}
void AudioSourcePage::UpdateVolmeter()
{
delete _volControl;
_volControl = nullptr;
OBSSourceAutoRelease source = obs_get_source_by_name(
_sourceCombo->currentText().toUtf8().constData());
if (!source) {
return;
}
_volControl = new VolControl(source.Get());
_volmeterLayout->addWidget(_volControl);
connect(_volControl->GetSlider(), &DoubleSlider::DoubleValChanged, this,
&AudioSourcePage::SyncSpinboxFromSlider);
SyncSliderFromSpinbox();
}
void AudioSourcePage::SyncSpinboxFromSlider()
{
if (!_volControl) {
return;
}
const QSignalBlocker blocker(_thresholdSpinbox);
_thresholdSpinbox->setValue(_volControl->GetSlider()->DoubleValue() -
100.0);
}
void AudioSourcePage::SyncSliderFromSpinbox()
{
if (!_volControl) {
return;
}
const QSignalBlocker blocker(_volControl->GetSlider());
_volControl->GetSlider()->SetDoubleVal(_thresholdSpinbox->value() +
100.0);
}
bool AudioSourcePage::isComplete() const
{
return _sourceCombo->count() > 0 &&
!_sourceCombo->currentText().isEmpty();
}
Duration AudioSourcePage::GetDuration() const
{
return _durationSelection->GetDuration();
}
// ===========================================================================
// AudioTargetPage
// ===========================================================================
AudioTargetPage::AudioTargetPage(QWidget *parent)
: QWizardPage(parent),
_sourceCombo(new QComboBox(this))
{
setTitle(obs_module_text("FirstRunWizard.audio.target.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.target.subtitle"));
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
registerField("audioTargetSource*", _sourceCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto *row = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.audio.target.sourceRow"),
row, {{"{{source}}", _sourceCombo}}, false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(row);
layout->addStretch();
}
void AudioTargetPage::initializePage()
{
QStringList names;
auto enumScenes = [](void *param, obs_source_t *src) -> bool {
auto *list = reinterpret_cast<QStringList *>(param);
obs_scene_t *scene = obs_scene_from_source(src);
obs_scene_enum_items(
scene,
[](obs_scene_t *, obs_sceneitem_t *item,
void *p) -> bool {
auto *l = reinterpret_cast<QStringList *>(p);
OBSSource s = obs_sceneitem_get_source(item);
if (s) {
*l << obs_source_get_name(s);
}
return true;
},
list);
return true;
};
obs_enum_scenes(enumScenes, &names);
names.sort(Qt::CaseInsensitive);
names.removeDuplicates();
const QString prev = _sourceCombo->currentText();
_sourceCombo->clear();
_sourceCombo->addItems(names);
const int idx = _sourceCombo->findText(prev);
if (idx >= 0) {
_sourceCombo->setCurrentIndex(idx);
}
}
bool AudioTargetPage::isComplete() const
{
return _sourceCombo->count() > 0 &&
!_sourceCombo->currentText().isEmpty();
}
// ===========================================================================
// AudioReviewPage
// ===========================================================================
AudioReviewPage::AudioReviewPage(QWidget *parent, std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_summary(new QLabel(this)),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.audio.review.title"));
setSubTitle(obs_module_text("FirstRunWizard.audio.review.subtitle"));
detail::setupSummaryLabel(_summary);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void AudioReviewPage::initializePage()
{
const QString audioSource = field("audioSourceName").toString();
const QString targetSource = field("audioTargetSource").toString();
auto *sourcePage = qobject_cast<AudioSourcePage *>(
wizard()->page(PAGE_AUDIO_SOURCE));
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
: -24.0;
const Duration duration = sourcePage ? sourcePage->GetDuration()
: Duration(1.0);
_summary->setText(
QString(obs_module_text("FirstRunWizard.audio.review.summary"))
.arg(audioSource.toHtmlEscaped())
.arg(thresholdDb, 0, 'f', 1)
.arg(QString::fromStdString(duration.ToString()))
.arg(targetSource.toHtmlEscaped()));
}
bool AudioReviewPage::validatePage()
{
const QString audioSource = field("audioSourceName").toString();
const QString targetSource = field("audioTargetSource").toString();
const std::string name =
("Audio: " + audioSource + " -> " + targetSource).toStdString();
auto *sourcePage = qobject_cast<AudioSourcePage *>(
wizard()->page(PAGE_AUDIO_SOURCE));
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
: -24.0;
const Duration duration = sourcePage ? sourcePage->GetDuration()
: Duration(1.0);
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: audio macro allocation failed");
return true;
}
_macro->SetActionConditionSplitterPosition(
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
_macro->SetElseActionSplitterPosition(
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
OBSDataAutoRelease condData =
buildAudioConditionData(audioSource, thresholdDb, duration);
if (!detail::addCondition(_macro.get(), "audio", condData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
OBSDataAutoRelease showData =
buildVisibilityActionData(targetSource, 0);
if (!detail::addAction(_macro.get(), "scene_visibility", showData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
OBSDataAutoRelease hideData =
buildVisibilityActionData(targetSource, 1);
if (!detail::addElseAction(_macro.get(), "scene_visibility",
hideData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.audio.review.errorTitle"),
obs_module_text(
"FirstRunWizard.audio.review.errorBody"));
return true;
}
blog(LOG_INFO, "FirstRunWizard: created audio macro '%s'",
name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@@ -0,0 +1,83 @@
#pragma once
#include "duration-control.hpp"
#include "duration.hpp"
#include "first-run-wizard.hpp"
#include "volume-control.hpp"
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QLabel>
#include <QVBoxLayout>
#include <memory>
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// AudioSourcePage
// Registers wizard field "audioSourceName" (QString).
// Volume threshold and duration are read back via getters.
// ---------------------------------------------------------------------------
class AudioSourcePage : public QWizardPage {
Q_OBJECT
public:
explicit AudioSourcePage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_AUDIO_TARGET; }
double GetThresholdDb() const { return _thresholdSpinbox->value(); }
Duration GetDuration() const;
private slots:
void UpdateVolmeter();
void SyncSpinboxFromSlider();
void SyncSliderFromSpinbox();
private:
QComboBox *_sourceCombo;
QDoubleSpinBox *_thresholdSpinbox;
DurationSelection *_durationSelection;
QVBoxLayout *_volmeterLayout;
VolControl *_volControl = nullptr;
};
// ---------------------------------------------------------------------------
// AudioTargetPage
// Registers wizard field "audioTargetSource" (QString).
// ---------------------------------------------------------------------------
class AudioTargetPage : public QWizardPage {
Q_OBJECT
public:
explicit AudioTargetPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_AUDIO_REVIEW; }
private:
QComboBox *_sourceCombo;
};
// ---------------------------------------------------------------------------
// AudioReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class AudioReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit AudioReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@@ -0,0 +1,85 @@
#pragma once
#include "log-helper.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include <obs-data.h>
#include <QFrame>
#include <QLabel>
#include <string>
namespace advss::wiz::detail {
static bool addCondition(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto cond = MacroConditionFactory::Create(id, macro);
if (!cond) {
blog(LOG_WARNING,
"FirstRunWizard: condition factory returned null for '%s'",
id.c_str());
return false;
}
if (!cond->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: condition Load() failed for '%s'",
id.c_str());
return false;
}
macro->Conditions().emplace_back(cond);
return true;
}
static bool addAction(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto action = MacroActionFactory::Create(id, macro);
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: action factory returned null for '%s'",
id.c_str());
return false;
}
if (!action->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: action Load() failed for '%s'",
id.c_str());
return false;
}
macro->Actions().emplace_back(action);
return true;
}
static bool addElseAction(advss::Macro *macro, const std::string &id,
obs_data_t *data)
{
auto action = MacroActionFactory::Create(id, macro);
if (!action) {
blog(LOG_WARNING,
"FirstRunWizard: else-action factory returned null for '%s'",
id.c_str());
return false;
}
if (!action->Load(data)) {
blog(LOG_WARNING,
"FirstRunWizard: else-action Load() failed for '%s'",
id.c_str());
return false;
}
macro->ElseActions().emplace_back(action);
return true;
}
static void setupSummaryLabel(QLabel *label)
{
label->setWordWrap(true);
label->setTextFormat(Qt::RichText);
label->setFrameShape(QFrame::StyledPanel);
label->setContentsMargins(12, 12, 12, 12);
}
} // namespace advss::wiz::detail

View File

@@ -0,0 +1,499 @@
#include "first-run-wizard-sequence.hpp"
#include "first-run-wizard-helpers.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "macro-settings.hpp"
#include "selection-helpers.hpp"
#include <obs-data.h>
#include <QFrame>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QScrollArea>
#include <QVBoxLayout>
namespace advss {
namespace wiz {
// Builds the obs_data blob for a "current scene == sceneName for at least
// triggerDuration" condition, matching MacroConditionScene::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "scene",
// "logic": 0,
// "durationModifier": {
// "time_constraint": 1, // AT_LEAST
// "seconds": <Duration::Save output>
// },
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
// "type": 10, // CURRENT_SCENE
// "version": 1
// }
static OBSDataAutoRelease
buildSceneConditionData(const QString &scene, const Duration &triggerDuration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease durMod = obs_data_create();
obs_data_set_int(durMod, "time_constraint", 1);
triggerDuration.Save(durMod, "seconds");
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 0);
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene");
obs_data_set_int(data, "logic", 0);
obs_data_set_obj(data, "durationModifier", durMod);
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_int(data, "type", 10);
obs_data_set_int(data, "version", 1);
return data;
}
// Builds the obs_data blob for a scene-switch action,
// matching MacroActionSwitchScene::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "scene_switch",
// "action": 0,
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
// "transitionType": 1, // scene's default transition
// "blockUntilTransitionDone": true,
// "sceneType": 0
// }
static OBSDataAutoRelease buildSceneSwitchData(const QString &scene)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease sceneSel = obs_data_create();
obs_data_set_int(sceneSel, "type", 0);
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
obs_data_set_string(sceneSel, "canvasSelection", "Main");
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "scene_switch");
obs_data_set_int(data, "action", 0);
obs_data_set_obj(data, "sceneSelection", sceneSel);
obs_data_set_int(data, "transitionType", 1);
obs_data_set_bool(data, "blockUntilTransitionDone", true);
obs_data_set_int(data, "sceneType", 0);
return data;
}
// Builds the obs_data blob for a wait action, matching MacroActionWait::Save().
//
// {
// "segmentSettings": { "enabled": true, "version": 2 },
// "id": "wait",
// "duration": <Duration::Save output>,
// "waitType": 0,
// "version": 1
// }
static OBSDataAutoRelease buildWaitData(const Duration &duration)
{
OBSDataAutoRelease seg = obs_data_create();
obs_data_set_bool(seg, "enabled", true);
obs_data_set_int(seg, "version", 2);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_obj(data, "segmentSettings", seg);
obs_data_set_string(data, "id", "wait");
duration.Save(data, "duration");
obs_data_set_int(data, "waitType", 0);
obs_data_set_int(data, "version", 1);
return data;
}
// ===========================================================================
// SequenceTriggerPage
// ===========================================================================
SequenceTriggerPage::SequenceTriggerPage(QWidget *parent)
: QWizardPage(parent),
_sceneCombo(new QComboBox(this)),
_delaySelection(new DurationSelection(this, true, 0.0))
{
setTitle(obs_module_text("FirstRunWizard.seqTrigger.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqTrigger.subtitle"));
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
_delaySelection->SetDuration(Duration(5.0));
registerField("seqTriggerScene*", _sceneCombo, "currentText",
SIGNAL(currentTextChanged(QString)));
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
&QWizardPage::completeChanged);
auto *sceneRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.scene"),
sceneRow, {{"{{scene}}", _sceneCombo}}, false);
auto *delayRow = new QHBoxLayout;
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.delay"),
delayRow, {{"{{duration}}", _delaySelection}}, false);
auto *layout = new QVBoxLayout(this);
layout->addLayout(sceneRow);
layout->addLayout(delayRow);
layout->addStretch();
}
void SequenceTriggerPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames()) {
_sceneCombo->addItem(name);
}
}
bool SequenceTriggerPage::isComplete() const
{
return _sceneCombo->count() > 0 &&
!_sceneCombo->currentText().isEmpty();
}
Duration SequenceTriggerPage::GetTriggerDuration() const
{
return _delaySelection->GetDuration();
}
// ===========================================================================
// SequenceScenesPage
// ===========================================================================
SequenceScenesPage::SequenceScenesPage(QWidget *parent)
: QWizardPage(parent),
_triggerInfoLabel(new QLabel(this)),
_stepsContainer(new QWidget(this)),
_stepsLayout(new QVBoxLayout(_stepsContainer))
{
setTitle(obs_module_text("FirstRunWizard.seqScenes.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqScenes.subtitle"));
_triggerInfoLabel->setWordWrap(true);
_triggerInfoLabel->setFrameShape(QFrame::StyledPanel);
_triggerInfoLabel->setContentsMargins(8, 4, 8, 4);
_stepsLayout->setContentsMargins(0, 0, 0, 0);
_stepsLayout->setSpacing(4);
auto *scrollArea = new QScrollArea(this);
scrollArea->setWidget(_stepsContainer);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto *addBtn = new QPushButton(
obs_module_text("FirstRunWizard.seqScenes.addScene"), this);
connect(addBtn, &QPushButton::clicked, this,
&SequenceScenesPage::onAddStepClicked);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_triggerInfoLabel);
layout->addWidget(scrollArea, 1);
layout->addWidget(addBtn, 0, Qt::AlignLeft);
}
void SequenceScenesPage::initializePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
_triggerInfoLabel->setText(
QString(obs_module_text("FirstRunWizard.seqScenes.triggerInfo"))
.arg(triggerScene)
.arg(QString::fromStdString(
triggerDuration.ToString())));
if (_initialized) {
return;
}
_initialized = true;
// Pre-select the scenes after the trigger scene so the user doesn't
// have to start by deselecting it manually.
const QStringList scenes = GetSceneNames();
const int count = static_cast<int>(scenes.size());
const int triggerIdx = scenes.indexOf(triggerScene);
const int firstIdx = count > 0 ? (triggerIdx + 1) % count : 0;
const int secondIdx = count > 0 ? (triggerIdx + 2) % count : 0;
AddStep(firstIdx);
AddStep(secondIdx);
}
bool SequenceScenesPage::isComplete() const
{
return _rows.size() >= 2;
}
QVector<QPair<QString, Duration>> SequenceScenesPage::GetSteps() const
{
QVector<QPair<QString, Duration>> steps;
steps.reserve(_rows.size());
for (int i = 0; i < _rows.size(); ++i) {
const Duration delay = (i < _rows.size() - 1)
? _rows[i].delay->GetDuration()
: Duration();
steps.append({_rows[i].scene->currentText(), delay});
}
return steps;
}
void SequenceScenesPage::onAddStepClicked()
{
const QStringList scenes = GetSceneNames();
const int count = static_cast<int>(scenes.size());
int nextIdx = 0;
if (!_rows.isEmpty() && count > 0) {
const int lastIdx = _rows.last().scene->currentIndex();
nextIdx = (lastIdx + 1) % count;
}
AddStep(nextIdx);
emit completeChanged();
}
void SequenceScenesPage::AddStep(int defaultSceneIndex)
{
auto *row = new QWidget(_stepsContainer);
auto *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
auto *sceneCombo = new QComboBox(row);
sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
const QStringList scenes = GetSceneNames();
for (const QString &name : scenes) {
sceneCombo->addItem(name);
}
if (defaultSceneIndex >= 0 && defaultSceneIndex < scenes.size()) {
sceneCombo->setCurrentIndex(defaultSceneIndex);
}
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.switchTo"),
rowLayout, {{"{{scene}}", sceneCombo}}, false);
auto *delayWidget = new QWidget(row);
auto *delayLayout = new QHBoxLayout(delayWidget);
delayLayout->setContentsMargins(0, 0, 0, 0);
auto *durationSel = new DurationSelection(delayWidget, true, 0.1);
durationSel->SetDuration(Duration(5.0));
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.thenWait"),
delayLayout, {{"{{duration}}", durationSel}}, false);
rowLayout->addWidget(delayWidget);
auto *removeBtn = new QPushButton(row);
removeBtn->setProperty("themeID",
QVariant(QString::fromUtf8("removeIconSmall")));
removeBtn->setProperty("class",
QVariant(QString::fromUtf8("icon-trash")));
removeBtn->setToolTip(
obs_module_text("FirstRunWizard.seqScenes.removeTooltip"));
removeBtn->setMaximumSize(22, 22);
rowLayout->addWidget(removeBtn);
_rows.append({row, sceneCombo, delayWidget, durationSel, removeBtn});
_stepsLayout->addWidget(row);
UpdateDelayVisibility();
UpdateRemoveButtons();
connect(removeBtn, &QPushButton::clicked, this,
[this, row]() { RemoveStep(row); });
}
void SequenceScenesPage::RemoveStep(QWidget *rowWidget)
{
int idx = -1;
for (int i = 0; i < _rows.size(); ++i) {
if (_rows[i].row == rowWidget) {
idx = i;
break;
}
}
if (idx < 0) {
return;
}
_stepsLayout->removeWidget(_rows[idx].row);
_rows[idx].row->deleteLater();
_rows.removeAt(idx);
UpdateDelayVisibility();
UpdateRemoveButtons();
emit completeChanged();
}
void SequenceScenesPage::UpdateDelayVisibility()
{
for (int i = 0; i < _rows.size(); ++i) {
_rows[i].delayWidget->setVisible(i < _rows.size() - 1);
}
}
void SequenceScenesPage::UpdateRemoveButtons()
{
const bool canRemove = _rows.size() > 1;
for (auto &step : _rows) {
step.remove->setEnabled(canRemove);
}
}
// ===========================================================================
// SequenceReviewPage
// ===========================================================================
SequenceReviewPage::SequenceReviewPage(QWidget *parent,
std::shared_ptr<Macro> &macro)
: QWizardPage(parent),
_summary(new QLabel(this)),
_macro(macro)
{
setTitle(obs_module_text("FirstRunWizard.seqReview.title"));
setSubTitle(obs_module_text("FirstRunWizard.seqReview.subtitle"));
detail::setupSummaryLabel(_summary);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void SequenceReviewPage::initializePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
auto *seqPage = qobject_cast<SequenceScenesPage *>(
wizard()->page(PAGE_SEQ_SCENES));
const auto steps = seqPage ? seqPage->GetSteps()
: QVector<QPair<QString, Duration>>{};
QString html =
QString("<p>%1</p>")
.arg(QString(obs_module_text(
"FirstRunWizard.seqReview.trigger"))
.arg(triggerScene.toHtmlEscaped())
.arg(QString::fromStdString(
triggerDuration.ToString())));
html += "<ol>";
for (int i = 0; i < steps.size(); ++i) {
const QString scene = steps[i].first.toHtmlEscaped();
if (i == 0) {
html += "<li>" + scene + "</li>";
} else {
const QString waitStr = QString::fromStdString(
steps[i - 1].second.ToString());
html += "<li>" +
QString(obs_module_text(
"FirstRunWizard.seqReview.step"))
.arg(waitStr)
.arg(scene) +
"</li>";
}
}
html += "</ol>";
_summary->setText(html);
}
bool SequenceReviewPage::validatePage()
{
const QString triggerScene = field("seqTriggerScene").toString();
const std::string name = ("Sequence: " + triggerScene).toStdString();
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
wizard()->page(PAGE_SEQ_TRIGGER));
const Duration triggerDuration =
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
auto *seqPage = qobject_cast<SequenceScenesPage *>(
wizard()->page(PAGE_SEQ_SCENES));
if (!seqPage) {
return true;
}
const auto steps = seqPage->GetSteps();
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: sequence macro allocation failed");
return true;
}
_macro->SetRunInParallel(true);
// --- Condition ---
OBSDataAutoRelease condData =
buildSceneConditionData(triggerScene, triggerDuration);
if (!detail::addCondition(_macro.get(), "scene", condData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text("FirstRunWizard.seqReview.errorTitle"),
obs_module_text("FirstRunWizard.seqReview.errorBody"));
return true;
}
// --- Actions: scene_switch interleaved with wait ---
for (int i = 0; i < steps.size(); ++i) {
OBSDataAutoRelease switchData =
buildSceneSwitchData(steps[i].first);
if (!detail::addAction(_macro.get(), "scene_switch",
switchData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.seqReview.errorTitle"),
obs_module_text(
"FirstRunWizard.seqReview.errorBody"));
return true;
}
const bool isLastStep = (i == steps.size() - 1);
if (!isLastStep) {
OBSDataAutoRelease waitData =
buildWaitData(steps[i].second);
if (!detail::addAction(_macro.get(), "wait",
waitData)) {
_macro.reset();
QMessageBox::warning(
this,
obs_module_text(
"FirstRunWizard.seqReview.errorTitle"),
obs_module_text(
"FirstRunWizard.seqReview.errorBody"));
return true;
}
}
}
blog(LOG_INFO, "FirstRunWizard: created sequence macro '%s'",
name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@@ -0,0 +1,101 @@
#pragma once
#include "duration-control.hpp"
#include "duration.hpp"
#include "first-run-wizard.hpp"
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QVector>
#include <QWidget>
#include <memory>
class QVBoxLayout;
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// SequenceTriggerPage
// Registers wizard field "seqTriggerScene" (QString).
// Trigger duration is read back via GetTriggerDuration().
// ---------------------------------------------------------------------------
class SequenceTriggerPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceTriggerPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_SEQ_SCENES; }
Duration GetTriggerDuration() const;
private:
QComboBox *_sceneCombo;
DurationSelection *_delaySelection;
};
// ---------------------------------------------------------------------------
// SequenceScenesPage
// Lets the user build an ordered list of scenes with delays between them.
// The delay after the last scene is ignored when building the macro.
// ---------------------------------------------------------------------------
struct SequenceStep {
QWidget *row;
QComboBox *scene;
QWidget *delayWidget; // hidden for the last row
DurationSelection *delay;
QPushButton *remove;
};
class SequenceScenesPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceScenesPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_SEQ_REVIEW; }
// Returns (sceneName, delayAfter) pairs. The last entry's delayAfter
// is always a default-constructed Duration and must be ignored by the caller.
QVector<QPair<QString, Duration>> GetSteps() const;
private slots:
void onAddStepClicked();
private:
void AddStep(int defaultSceneIndex = 0);
void RemoveStep(QWidget *rowWidget);
void UpdateDelayVisibility();
void UpdateRemoveButtons();
QLabel *_triggerInfoLabel;
QWidget *_stepsContainer;
QVBoxLayout *_stepsLayout;
QVector<SequenceStep> _rows;
bool _initialized = false;
};
// ---------------------------------------------------------------------------
// SequenceReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class SequenceReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit SequenceReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@@ -0,0 +1,308 @@
#include "first-run-wizard-window.hpp"
#include "first-run-wizard-helpers.hpp"
#include "macro-settings.hpp"
#include "obs-module-helper.hpp"
#include "platform-funcs.hpp"
#include "selection-helpers.hpp"
#include <obs.hpp>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QRegularExpression>
#include <QVBoxLayout>
namespace advss {
namespace wiz {
static constexpr char kConditionIdWindow[] = "window";
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
static QString detectFocusedWindow()
{
return QString::fromStdString(GetCurrentWindowTitle());
}
static QString escapeForRegex(const QString &input)
{
return QRegularExpression::escape(input);
}
// ===========================================================================
// WindowSceneSelectionPage
// ===========================================================================
WindowSceneSelectionPage::WindowSceneSelectionPage(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 WindowSceneSelectionPage::initializePage()
{
_sceneCombo->clear();
for (const QString &name : GetSceneNames()) {
_sceneCombo->addItem(name);
}
}
bool WindowSceneSelectionPage::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"));
}
// ===========================================================================
// WindowReviewPage
// ===========================================================================
WindowReviewPage::WindowReviewPage(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);
detail::setupSummaryLabel(_summary);
auto layout = new QVBoxLayout(this);
layout->addWidget(_summary);
layout->addStretch();
}
void WindowReviewPage::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()));
}
bool WindowReviewPage::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);
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
if (!_macro) {
blog(LOG_WARNING,
"FirstRunWizard: window macro allocation failed");
return true;
}
if (!detail::addCondition(_macro.get(), kConditionIdWindow, condData) ||
!detail::addAction(_macro.get(), 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;
}
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", name.c_str());
return true;
}
} // namespace wiz
} // namespace advss

View File

@@ -0,0 +1,76 @@
#pragma once
#include "first-run-wizard.hpp"
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <memory>
namespace advss {
namespace wiz {
// ---------------------------------------------------------------------------
// WindowSceneSelectionPage
// Registers wizard field "targetScene" (QString).
// ---------------------------------------------------------------------------
class WindowSceneSelectionPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowSceneSelectionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW_CONDITION; }
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_WINDOW_REVIEW; }
private slots:
void onAutoDetectClicked();
void onCountdownTick();
private:
QLineEdit *_windowEdit;
QPushButton *_autoDetect;
QTimer *_detectTimer;
int _countdown = 3;
};
// ---------------------------------------------------------------------------
// WindowReviewPage
// Displays a summary and builds the macro from wizard fields on Finish.
// ---------------------------------------------------------------------------
class WindowReviewPage : public QWizardPage {
Q_OBJECT
public:
explicit WindowReviewPage(QWidget *parent,
std::shared_ptr<advss::Macro> &macro);
void initializePage() override;
bool validatePage() override;
int nextId() const override { return PAGE_DONE; }
private:
QLabel *_summary;
std::shared_ptr<advss::Macro> &_macro;
};
} // namespace wiz
} // namespace advss

View File

@@ -1,28 +1,17 @@
#include "first-run-wizard.hpp"
#include "first-run-wizard-audio.hpp"
#include "first-run-wizard-sequence.hpp"
#include "first-run-wizard-window.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
// ---------------------------------------------------------------------------
@@ -42,7 +31,7 @@ bool IsFirstRun()
return config_get_bool(cfg, kConfigSection, kFirstRunKey);
}
static void WriteFirstRun(bool value)
static void writeFirstRun(bool value)
{
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0)
config_t *cfg = obs_frontend_get_user_config();
@@ -53,10 +42,10 @@ static void WriteFirstRun(bool value)
config_save_safe(cfg, "tmp", nullptr);
}
static QString DetectFocusedWindow()
{
return QString::fromStdString(GetCurrentWindowTitle());
}
} // namespace advss
namespace advss {
namespace wiz {
// ===========================================================================
// WelcomePage
@@ -78,271 +67,39 @@ WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent)
}
// ===========================================================================
// SceneSelectionPage
// TemplatePage
// ===========================================================================
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)
TemplatePage::TemplatePage(QWidget *parent)
: QWizardPage(parent),
_detectTimer(new QTimer(this))
_windowRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.window"), this)),
_sequenceRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.sequence"), this)),
_audioRadio(new QRadioButton(
obs_module_text("FirstRunWizard.template.audio"), this))
{
setTitle(obs_module_text("FirstRunWizard.window.title"));
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
setTitle(obs_module_text("FirstRunWizard.template.title"));
setSubTitle(obs_module_text("FirstRunWizard.template.subtitle"));
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
this);
_windowEdit = new QLineEdit(this);
_windowEdit->setPlaceholderText(
obs_module_text("FirstRunWizard.window.placeholder"));
_windowRadio->setChecked(true);
_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);
auto *layout = new QVBoxLayout(this);
layout->addWidget(_windowRadio);
layout->addWidget(_sequenceRadio);
layout->addWidget(_audioRadio);
layout->addStretch();
}
void WindowConditionPage::initializePage()
int TemplatePage::nextId() const
{
if (_windowEdit->text().isEmpty()) {
QString detected = DetectFocusedWindow();
if (!detected.isEmpty()) {
_windowEdit->setText(detected);
}
if (_sequenceRadio->isChecked()) {
return PAGE_SEQ_TRIGGER;
}
}
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;
if (_audioRadio->isChecked()) {
return PAGE_AUDIO_SOURCE;
}
_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;
return PAGE_WINDOW_SCENE;
}
// ===========================================================================
@@ -373,12 +130,19 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
{
setWindowTitle(obs_module_text("FirstRunWizard.windowTitle"));
setWizardStyle(QWizard::ModernStyle);
setMinimumSize(540, 420);
setMinimumSize(600, 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_TEMPLATE, new TemplatePage(this));
setPage(PAGE_WINDOW_SCENE, new WindowSceneSelectionPage(this));
setPage(PAGE_WINDOW_CONDITION, new WindowConditionPage(this));
setPage(PAGE_WINDOW_REVIEW, new WindowReviewPage(this, _macro));
setPage(PAGE_SEQ_TRIGGER, new SequenceTriggerPage(this));
setPage(PAGE_SEQ_SCENES, new SequenceScenesPage(this));
setPage(PAGE_SEQ_REVIEW, new SequenceReviewPage(this, _macro));
setPage(PAGE_AUDIO_SOURCE, new AudioSourcePage(this));
setPage(PAGE_AUDIO_TARGET, new AudioTargetPage(this));
setPage(PAGE_AUDIO_REVIEW, new AudioReviewPage(this, _macro));
setPage(PAGE_DONE, new DonePage(this));
setStartId(PAGE_WELCOME);
@@ -394,7 +158,7 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
void FirstRunWizard::markFirstRunComplete()
{
WriteFirstRun(false);
writeFirstRun(false);
}
// static
@@ -410,58 +174,5 @@ std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent,
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 wiz
} // namespace advss

View File

@@ -1,31 +1,34 @@
#pragma once
#include <obs-data.h>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <QRadioButton>
#include <QWizard>
#include <QWizardPage>
#include <string>
#include <memory>
namespace advss {
bool IsFirstRun();
class Macro;
bool IsFirstRun();
namespace wiz {
// ---------------------------------------------------------------------------
// Page IDs
// ---------------------------------------------------------------------------
enum WizardPageId {
PAGE_WELCOME = 0,
PAGE_SCENE,
PAGE_WINDOW,
PAGE_REVIEW,
PAGE_TEMPLATE,
PAGE_WINDOW_SCENE,
PAGE_WINDOW_CONDITION,
PAGE_WINDOW_REVIEW,
PAGE_SEQ_TRIGGER,
PAGE_SEQ_SCENES,
PAGE_SEQ_REVIEW,
PAGE_AUDIO_SOURCE,
PAGE_AUDIO_TARGET,
PAGE_AUDIO_REVIEW,
PAGE_DONE,
};
@@ -36,64 +39,23 @@ class WelcomePage : public QWizardPage {
Q_OBJECT
public:
explicit WelcomePage(QWidget *parent = nullptr);
int nextId() const override { return PAGE_SCENE; }
int nextId() const override { return PAGE_TEMPLATE; }
};
// ---------------------------------------------------------------------------
// SceneSelectionPage
// Registers wizard field "targetScene" (QString).
// TemplatePage
// Lets the user choose which kind of automation to create.
// ---------------------------------------------------------------------------
class SceneSelectionPage : public QWizardPage {
class TemplatePage : public QWizardPage {
Q_OBJECT
public:
explicit SceneSelectionPage(QWidget *parent = nullptr);
void initializePage() override;
bool isComplete() const override;
int nextId() const override { return PAGE_WINDOW; }
explicit TemplatePage(QWidget *parent = nullptr);
int nextId() const override;
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;
QRadioButton *_windowRadio;
QRadioButton *_sequenceRadio;
QRadioButton *_audioRadio;
};
// ---------------------------------------------------------------------------
@@ -113,17 +75,14 @@ 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);
static std::shared_ptr<advss::Macro>
ShowWizard(QWidget *parent, bool *wasSkipped = nullptr);
private:
void markFirstRunComplete();
std::shared_ptr<Macro> _macro;
std::shared_ptr<advss::Macro> _macro;
};
} // namespace wiz
} // namespace advss

View File

@@ -99,4 +99,23 @@ std::optional<std::string> AccessJsonArrayIndex(const std::string &jsonStr,
return {};
}
std::optional<std::string>
ExtractSingleJsonArrayElement(const std::string &jsonStr)
{
try {
nlohmann::json json = nlohmann::json::parse(jsonStr);
if (!json.is_array() || json.size() != 1) {
return {};
}
auto result = json.at(0);
if (result.is_string()) {
return result.get<std::string>();
}
return result.dump();
} catch (const nlohmann::json::exception &) {
return {};
}
return {};
}
} // namespace advss

View File

@@ -17,5 +17,7 @@ EXPORT std::optional<std::string> QueryJson(const std::string &json,
const std::string &query);
EXPORT std::optional<std::string> AccessJsonArrayIndex(const std::string &json,
const int index);
EXPORT std::optional<std::string>
ExtractSingleJsonArrayElement(const std::string &json);
} // namespace advss

View File

@@ -1,6 +1,8 @@
#include "priority-helper.hpp"
#include "switcher-data.hpp"
#include "advanced-scene-switcher.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp"
#include "scene-group.hpp"
#include "switch-audio.hpp"
#include "switch-executable.hpp"
@@ -10,15 +12,16 @@
#include "switch-pause.hpp"
#include "switch-random.hpp"
#include "switch-screen-region.hpp"
#include "switch-sequence.hpp"
#include "switch-time.hpp"
#include "switch-transitions.hpp"
#include "switch-window.hpp"
#include "switch-sequence.hpp"
#include "switch-video.hpp"
#include "macro.hpp"
#include "switch-window.hpp"
#include "switcher-data.hpp"
#include <QThread>
#include <algorithm>
#include <QThread>
namespace advss {

View File

@@ -1,5 +1,7 @@
#pragma once
#include <export-symbol-helper.hpp>
#include <QFrame>
#include <QGridLayout>
#include <QParallelAnimationGroup>
@@ -8,7 +10,7 @@
namespace advss {
class Section : public QWidget {
class ADVSS_EXPORT Section : public QWidget {
Q_OBJECT
public:

View File

@@ -10,6 +10,7 @@
#include <obs-frontend-api.h>
#include <QStandardItemModel>
#include <unordered_set>
namespace advss {
@@ -42,34 +43,46 @@ static void hasFilterEnum(obs_source_t *, obs_source_t *filter, void *ptr)
QStringList GetSourcesWithFilterNames()
{
struct EnumParam {
QStringList list;
// Some items appear in both obs_enum_sources and
// obs_enum_scenes, so track pointers to avoid adding them
// twice.
std::unordered_set<obs_source_t *> seen;
};
static auto enumSourcesWithFilters = [](void *param,
obs_source_t *source) {
if (!source) {
return true;
}
QStringList *list = reinterpret_cast<QStringList *>(param);
auto *ep = reinterpret_cast<EnumParam *>(param);
const auto [_, inserted] = ep->seen.insert(source);
if (!inserted) {
return true;
}
bool hasFilter = false;
obs_source_enum_filters(source, hasFilterEnum, &hasFilter);
if (hasFilter) {
*list << obs_source_get_name(source);
ep->list << obs_source_get_name(source);
}
return true;
};
QStringList list;
obs_enum_sources(enumSourcesWithFilters, &list);
EnumParam ep;
obs_enum_sources(enumSourcesWithFilters, &ep);
#if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 1, 0)
obs_enum_scenes(enumSourcesWithFilters, &list);
obs_enum_scenes(enumSourcesWithFilters, &ep);
#else
static const auto enumCanvases = [](void *listPtr,
static const auto enumCanvases = [](void *param,
obs_canvas_t *canvas) -> bool {
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, listPtr);
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, param);
return true;
};
obs_enum_canvases(enumCanvases, &list);
obs_enum_canvases(enumCanvases, &ep);
#endif
return list;
return ep.list;
}
QStringList GetMediaSourceNames()

View File

@@ -1,9 +1,11 @@
#include "temp-variable.hpp"
#include "obs-module-helper.hpp"
#include "macro.hpp"
#include "macro-action-macro.hpp"
#include "macro-edit.hpp"
#include "macro-helpers.hpp"
#include "macro-segment.hpp"
#include "macro.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "sync-helpers.hpp"
#include "ui-helpers.hpp"

View File

@@ -1,12 +1,18 @@
#include "ui-helpers.hpp"
#include "advanced-scene-switcher.hpp"
#include "non-modal-dialog.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include <obs-frontend-api.h>
#include <QAbstractButton>
#include <QComboBox>
#include <QCursor>
#include <QGraphicsColorizeEffect>
#include <QListView>
#include <QListWidget>
#include <QMainWindow>
#include <QPropertyAnimation>
#include <QScrollBar>

View File

@@ -1,15 +1,18 @@
#pragma once
#include "export-symbol-helper.hpp"
#include <QAbstractButton>
#include <QColor>
#include <QComboBox>
#include <QIcon>
#include <QListWidget>
#include <QString>
#include <QWidget>
#include <string>
class QAbstractButton;
class QComboBox;
class QListWidget;
class QWidget;
namespace advss {
// Returns QObject* to QPropertyAnimation object

View File

@@ -1,6 +1,9 @@
#include "utility.hpp"
#include <QCursor>
#include <QListWidget>
#include <QTextStream>
#include <sstream>
namespace advss {

View File

@@ -1,14 +1,15 @@
#pragma once
#include "export-symbol-helper.hpp"
#include <optional>
#include <QListWidget>
#include <QMetaObject>
#include <QPushButton>
#include <QString>
#include <QWidget>
#include <string>
class QListWidget;
class QObject;
class QWidget;
namespace advss {
EXPORT std::pair<int, int> GetCursorPos();

View File

@@ -1,6 +1,7 @@
#include "websocket-api.hpp"
#include "obs-websocket-api.h"
#include "plugin-state-helpers.hpp"
#include "version.h"
// Must be after "obs-websocket-api.h" to avoid logging function conflict
#include "log-helper.hpp"
@@ -15,6 +16,7 @@ static constexpr char VendorName[] = "AdvancedSceneSwitcher";
static constexpr char VendorRequestStart[] = "AdvancedSceneSwitcherStart";
static constexpr char VendorRequestStop[] = "AdvancedSceneSwitcherStop";
static constexpr char VendorRequestStatus[] = "IsAdvancedSceneSwitcherRunning";
static constexpr char VendorRequestVersion[] = "AdvancedSceneSwitcherVersion";
static obs_websocket_vendor vendor;
static void registerWebsocketVendor();
@@ -82,6 +84,12 @@ static void registerWebsocketVendor()
obs_data_set_bool(response, "isRunning",
PluginIsRunning());
});
registerWebsocketVendorRequest(
VendorRequestVersion,
[](obs_data_t *, obs_data_t *response, void *) {
obs_data_set_string(response, "version", g_GIT_TAG);
obs_data_set_string(response, "commit", g_GIT_SHA1);
});
}
const char *GetWebsocketVendorName()

View File

@@ -0,0 +1,106 @@
#include "variable-color-button.hpp"
#include "obs-module-helper.hpp"
#include "ui-helpers.hpp"
#include <QColorDialog>
#include <QHBoxLayout>
namespace advss {
VariableColorButton::VariableColorButton(QWidget *parent,
const QString &selectText)
: QWidget(parent),
_colorSwatch(new QLabel()),
_selectColor(new QPushButton(selectText)),
_variable(new VariableSelection(this)),
_toggleType(new QPushButton())
{
_toggleType->setCheckable(true);
_toggleType->setMaximumWidth(11);
SetButtonIcon(_toggleType, GetThemeTypeName() == "Light"
? ":/res/images/dots-vert.svg"
: "theme:Dark/dots-vert.svg");
QWidget::connect(_selectColor, SIGNAL(clicked()), this,
SLOT(SelectColorClicked()));
QWidget::connect(_toggleType, SIGNAL(toggled(bool)), this,
SLOT(ToggleTypeClicked(bool)));
QWidget::connect(_variable, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(VariableChanged(const QString &)));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_colorSwatch);
layout->addWidget(_selectColor);
layout->addWidget(_variable);
layout->addWidget(_toggleType);
setLayout(layout);
SetVisibility();
}
void VariableColorButton::SetValue(const ColorVariable &color)
{
_color = color;
const QSignalBlocker b1(_toggleType);
const QSignalBlocker b2(_variable);
_toggleType->setChecked(!color.IsFixedType());
SetupColorLabel(color.GetFixedValue());
_variable->SetVariable(color.GetVariable());
SetVisibility();
}
void VariableColorButton::SelectColorClicked()
{
const QColor color = QColorDialog::getColor(
_color.GetFixedValue(), this, _selectColor->text(),
QColorDialog::ColorDialogOption());
if (!color.isValid()) {
return;
}
SetupColorLabel(color);
_color._value = color;
emit ColorVariableChanged(_color);
}
void VariableColorButton::VariableChanged(const QString &name)
{
_color._variable = GetWeakVariableByQString(name);
emit ColorVariableChanged(_color);
}
void VariableColorButton::ToggleTypeClicked(bool useVariable)
{
_color._type = useVariable ? ColorVariable::Type::VARIABLE
: ColorVariable::Type::FIXED_VALUE;
SetVisibility();
emit ColorVariableChanged(_color);
}
void VariableColorButton::SetupColorLabel(const QColor &color)
{
_colorSwatch->setText(color.name());
_colorSwatch->setStyleSheet(
QString("background-color: %1;").arg(color.name()));
}
void VariableColorButton::SetVisibility()
{
if (_color.IsFixedType()) {
SetupColorLabel(_color.GetFixedValue());
_colorSwatch->show();
_selectColor->show();
_variable->hide();
_toggleType->setVisible(!GetVariables().empty());
} else {
_colorSwatch->hide();
_selectColor->hide();
_variable->show();
_variable->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.colorVariableTooltip"));
_toggleType->show();
}
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,37 @@
#pragma once
#include "export-symbol-helper.hpp"
#include "variable-color.hpp"
#include <QLabel>
#include <QPushButton>
#include <QWidget>
namespace advss {
class ADVSS_EXPORT VariableColorButton : public QWidget {
Q_OBJECT
public:
VariableColorButton(QWidget *parent, const QString &selectText);
void SetValue(const ColorVariable &);
ColorVariable Value() const { return _color; }
public slots:
void SelectColorClicked();
void VariableChanged(const QString &);
void ToggleTypeClicked(bool useVariable);
signals:
void ColorVariableChanged(const ColorVariable &);
private:
void SetupColorLabel(const QColor &);
void SetVisibility();
ColorVariable _color;
QLabel *_colorSwatch;
QPushButton *_selectColor;
VariableSelection *_variable;
QPushButton *_toggleType;
};
} // namespace advss

View File

@@ -0,0 +1,53 @@
#include "variable-color.hpp"
#include <obs.hpp>
namespace advss {
ColorVariable::ColorVariable(const QColor &value) : _value(value) {}
void ColorVariable::Save(obs_data_t *obj, const char *name) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_int(data, "version", 1);
obs_data_set_int(data, "type", static_cast<int>(_type));
auto var = _variable.lock();
if (var) {
obs_data_set_string(data, "variable", var->Name().c_str());
}
obs_data_set_int(data, "red", _value.red());
obs_data_set_int(data, "green", _value.green());
obs_data_set_int(data, "blue", _value.blue());
obs_data_set_obj(obj, name, data);
}
void ColorVariable::Load(obs_data_t *obj, const char *name)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, name);
_value.setRed(obs_data_get_int(data, "red"));
_value.setGreen(obs_data_get_int(data, "green"));
_value.setBlue(obs_data_get_int(data, "blue"));
if (!obs_data_has_user_value(data, "version")) {
// Old format: no variable support, just R/G/B
_type = Type::FIXED_VALUE;
return;
}
auto variableName = obs_data_get_string(data, "variable");
_variable = GetWeakVariableByName(variableName);
_type = static_cast<Type>(obs_data_get_int(data, "type"));
}
QColor ColorVariable::GetValue() const
{
if (_type == Type::FIXED_VALUE) {
return _value;
}
auto var = _variable.lock();
if (!var) {
return Qt::black;
}
const QColor color(QString::fromStdString(var->Value()));
return color.isValid() ? color : Qt::black;
}
} // namespace advss

View File

@@ -0,0 +1,35 @@
#pragma once
#include "variable.hpp"
#include <obs-data.h>
#include <QColor>
namespace advss {
class VariableColorButton;
class ADVSS_EXPORT ColorVariable {
public:
enum class Type { FIXED_VALUE, VARIABLE };
ColorVariable() = default;
ColorVariable(const QColor &value);
void Save(obs_data_t *obj, const char *name) const;
void Load(obs_data_t *obj, const char *name);
QColor GetValue() const;
QColor GetFixedValue() const { return _value; }
bool IsFixedType() const { return _type == Type::FIXED_VALUE; }
Type GetType() const { return _type; }
std::weak_ptr<Variable> GetVariable() const { return _variable; }
private:
Type _type = Type::FIXED_VALUE;
QColor _value = Qt::black;
std::weak_ptr<Variable> _variable;
friend class VariableColorButton;
};
} // namespace advss

View File

@@ -18,6 +18,30 @@ static std::deque<std::shared_ptr<Item>> variables;
static std::mutex lastVariableChangeMutex;
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
// When set, Variable::Value() and Variable::SetValue() operate on this context
// instead of the global variable state. Used by action queues to isolate
// variable reads and writes to a snapshot taken at the time the action was
// added to the queue, so that actions can modify variables without affecting
// the global state or other queue entries.
thread_local static VariableContext *activeVarContext = nullptr;
VariableContext CreateVariableContext()
{
VariableContext context;
for (const auto &v : variables) {
const auto &var = std::dynamic_pointer_cast<Variable>(v);
if (var) {
context[var->Name()] = var->Value(false);
}
}
return context;
}
void SetActiveVariableContext(VariableContext *context)
{
activeVarContext = context;
}
static bool setup()
{
AddEarlySaveStep(SaveVariables);
@@ -76,6 +100,14 @@ void Variable::Save(obs_data_t *obj) const
std::string Variable::Value(bool updateLastUsed) const
{
if (activeVarContext) {
auto it = activeVarContext->find(Name());
if (it == activeVarContext->end()) {
return "";
}
return it->second;
}
std::lock_guard<std::mutex> lock(_mutex);
if (updateLastUsed) {
UpdateLastUsed();
@@ -108,16 +140,31 @@ std::optional<int> Variable::IntValue() const
void Variable::SetValue(const std::string &value)
{
std::lock_guard<std::mutex> lock(_mutex);
_previousValue = _value;
_value = value;
UpdateLastUsed();
if (_previousValue != _value) {
_lastChanged = std::chrono::high_resolution_clock::now();
++_valueChangeCount;
if (activeVarContext) {
auto it = activeVarContext->find(Name());
if (it == activeVarContext->end()) {
return;
}
it->second = value;
setLastVariableChangeTime();
return;
}
setLastVariableChangeTime();
{
std::lock_guard<std::mutex> lock(_mutex);
_previousValue = _value;
_value = value;
UpdateLastUsed();
if (_previousValue != _value) {
_lastChanged =
std::chrono::high_resolution_clock::now();
++_valueChangeCount;
}
setLastVariableChangeTime();
}
_cv.notify_all();
}
void Variable::SetValue(double value)

View File

@@ -3,10 +3,12 @@
#include "item-selection-helpers.hpp"
#include "resizing-text-edit.hpp"
#include <condition_variable>
#include <mutex>
#include <obs-data.h>
#include <optional>
#include <string>
#include <unordered_map>
#include <QStringList>
namespace advss {
@@ -43,6 +45,11 @@ public:
std::optional<uint64_t> GetSecondsSinceLastUse() const;
std::optional<uint64_t> GetSecondsSinceLastChange() const;
void MarkAsUsed() const;
EXPORT std::condition_variable &GetCV() { return _cv; }
EXPORT std::unique_lock<std::mutex> GetCVLock()
{
return std::unique_lock<std::mutex>(_cvMutex);
}
private:
SaveAction _saveAction = SaveAction::DONT_SAVE;
@@ -53,6 +60,8 @@ private:
mutable std::chrono::high_resolution_clock::time_point _lastUsed;
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
mutable std::mutex _mutex;
std::mutex _cvMutex;
std::condition_variable _cv;
void UpdateLastUsed() const;
@@ -108,6 +117,10 @@ signals:
void Remove(const QString &);
};
using VariableContext = std::unordered_map<std::string, std::string>;
VariableContext CreateVariableContext();
void SetActiveVariableContext(VariableContext *context);
std::deque<std::shared_ptr<Item>> &GetVariables();
EXPORT Variable *GetVariableByName(const std::string &name);
EXPORT Variable *GetVariableByQString(const QString &name);

View File

@@ -1,7 +1,11 @@
macro(add_plugin plugin)
option(ENABLE_${plugin}_PLUGIN "Enable the ${plugin} plugin" ON)
if(ENABLE_${plugin}_PLUGIN)
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_PENDING ${plugin})
add_subdirectory(${plugin})
else()
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_DISABLED_BY_OPTION
${plugin})
endif()
endmacro()
@@ -37,3 +41,33 @@ add_plugin(stream-deck)
add_plugin(twitch)
add_plugin(usb)
add_plugin(video)
add_plugin(speech)
# ---------------------------------------------------------------------------- #
# Print plugin summary
get_property(_advss_enabled GLOBAL PROPERTY ADVSS_PLUGINS_ENABLED)
get_property(_advss_pending GLOBAL PROPERTY ADVSS_PLUGINS_PENDING)
get_property(_advss_disabled_opt GLOBAL
PROPERTY ADVSS_PLUGINS_DISABLED_BY_OPTION)
set(_advss_disabled_deps)
foreach(_p ${_advss_pending})
if(NOT _p IN_LIST _advss_enabled)
list(APPEND _advss_disabled_deps ${_p})
endif()
endforeach()
message(STATUS "")
message(STATUS "ADVSS Plugin Summary")
message(STATUS "====================")
foreach(_p ${_advss_enabled})
message(STATUS " ENABLED: ${_p}")
endforeach()
foreach(_p ${_advss_disabled_deps})
message(STATUS " DISABLED: ${_p} (missing dependencies)")
endforeach()
foreach(_p ${_advss_disabled_opt})
message(STATUS " DISABLED: ${_p} (ENABLE_${_p}_PLUGIN=OFF)")
endforeach()
message(STATUS "")

View File

@@ -165,12 +165,20 @@ MacroActionRecordEdit::MacroActionRecordEdit(
QWidget *parent, std::shared_ptr<MacroActionRecord> entryData)
: QWidget(parent),
_actions(new QComboBox()),
_pauseHint(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.recording.pause.hint"))),
_splitHint(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.recording.split.hint"))),
_pauseHint(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.recording.pause.hint"),
this)),
_splitHint(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.recording.split.hint"),
this)),
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
_recordFileFormat(new VariableLineEdit(this)),
_outputNotActiveHelp(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.recording.outputNotActive.hint"),
this)),
_chapterName(new VariableLineEdit(this))
{
populateActionSelection(_actions);
@@ -184,16 +192,18 @@ MacroActionRecordEdit::MacroActionRecordEdit(
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
SLOT(ChapterNameChanged()));
auto mainLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
mainLayout,
{{"{{actions}}", _actions},
{"{{pauseHint}}", _pauseHint},
{"{{splitHint}}", _splitHint},
{"{{recordFolder}}", _recordFolder},
{"{{recordFileFormat}}", _recordFileFormat},
{"{{chapterName}}", _chapterName}});
setLayout(mainLayout);
_mainLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.recording.layout"),
_mainLayout,
{{"{{actions}}", _actions},
{"{{pauseHint}}", _pauseHint},
{"{{splitHint}}", _splitHint},
{"{{recordFolder}}", _recordFolder},
{"{{recordFileFormat}}", _recordFileFormat},
{"{{outputNotActiveHelp}}", _outputNotActiveHelp},
{"{{chapterName}}", _chapterName}});
setLayout(_mainLayout);
_entryData = entryData;
UpdateEntryData();
@@ -245,8 +255,20 @@ void MacroActionRecordEdit::SetWidgetVisibility()
MacroActionRecord::Action::FOLDER);
_recordFileFormat->setVisible(_entryData->_action ==
MacroActionRecord::Action::FILE_FORMAT);
_outputNotActiveHelp->setVisible(
_entryData->_action == MacroActionRecord::Action::FOLDER ||
_entryData->_action == MacroActionRecord::Action::FILE_FORMAT);
_chapterName->setVisible(_entryData->_action ==
MacroActionRecord::Action::ADD_CHAPTER);
const bool hasExtraWidget =
_entryData->_action == MacroActionRecord::Action::FOLDER ||
_entryData->_action == MacroActionRecord::Action::FILE_FORMAT ||
_entryData->_action == MacroActionRecord::Action::ADD_CHAPTER;
if (hasExtraWidget) {
RemoveStretchIfPresent(_mainLayout);
} else {
AddStretchIfNecessary(_mainLayout);
}
}
void MacroActionRecordEdit::ActionChanged(int value)

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