Compare commits

..

48 Commits

Author SHA1 Message Date
WarmUpTill
efa34d080e CI: Use dependency build cache 2023-09-17 17:50:25 +02:00
WarmUpTill
19a6277841 Enable use of throttling for "has changed" checks. 2023-09-17 17:08:15 +02:00
Przemek Pawlas
107e1413ed Add Twitch clip and marker actions
* Refactor stream title variable name
* Limit commerical duration to 180s
* Set maximum stream title size to 140 chars
2023-09-16 22:23:34 +02:00
Przemek Pawlas
94416dd881 Add .vscode to .gitignore 2023-09-16 22:23:34 +02:00
WarmUpTill
8efc39958f Add action to assign scene item count to variable
To be able to iterate through the scene items of a scene
2023-09-16 19:49:54 +02:00
WarmUpTill
569f794e33 Move GetSceneItemCount() to utility 2023-09-16 19:49:54 +02:00
WarmUpTill
fc1460e592 Fix crash on shutdown related to macro docks
It could happen that the macro was already deleted while the macro dock
was still attempting to update its displayed information.
2023-09-13 19:49:55 +02:00
WarmUpTill
d6e4ee0203 Add option to run "else" actions using the "Run macros" button
If there are else actions to run holding control and clicking the "Run
macros" button will allow you to test them.
2023-09-13 19:49:55 +02:00
WarmUpTill
0c8135078d Rework PerformActions() to enable choosing which action type to run 2023-09-13 19:49:55 +02:00
WarmUpTill
24f4b864c4 Fix macro splitter position not saving when switching to empty selection 2023-09-13 19:49:55 +02:00
WarmUpTill
41e47f32b6 Display warning if selected token does not support selected action 2023-09-11 18:27:47 +02:00
WarmUpTill
a73ccb15a1 Enable MacOS support for Twitch action 2023-09-11 18:27:47 +02:00
WarmUpTill
36a78f49a5 Add openssl to MacOS build scripts to enable Twitch support 2023-09-11 18:27:47 +02:00
WarmUpTill
cbd459b0a5 Improve error logging 2023-09-11 18:27:47 +02:00
WarmUpTill
03bb1fd089 Stop token server if token was received / timeout was reached 2023-09-11 18:27:47 +02:00
WarmUpTill
4e2c254717 Set commercial duration suffix to "s" to inidicate unit is in seconds 2023-09-11 18:27:47 +02:00
WarmUpTill
fffb6c5f29 Adaptions for "else" support
* Fix "on change" option always being highlighted
* Fix "run macro" button performing actions based on condition state
* Fix "run" option of macro action performing action based on condition
  stated of selected macro
2023-09-11 18:25:46 +02:00
WarmUpTill
81f669e226 CI: Separate dependency build from plugin build
* Goal is to enable caching of dependency builds to speed up CI build
  times significantly
* Should ease transition to new plugin build template version
2023-09-10 12:26:41 +02:00
Denis KARUPOVIC
6484ae54c0 Create fr-FR.ini (#863)
* Create fr-FR.ini

add french language

* Update fr-FR.ini (fix white spaces)

* Update fr-FR.ini (remove some useless trads + fix others)

* Update fr-FR.ini (correct some trads)

* Update fr-FR.ini (fix some trads)

* Update fr-FR.ini (fix some trads)

* Update fr-FR.ini (fix errors)

---------

Co-authored-by: WarmUpTill <WarmUpTill@web.de>
2023-09-10 01:41:01 +02:00
WarmUpTill
1c52caf5e9 Move legacy tab entries to end of file 2023-09-09 11:17:29 +02:00
WarmUpTill
ce1d4cce57 Add support for "else" action block
* Changed the "on change" match behaviour
  Previously macros with true conditions but "on change" enabled would
  be considered to be evaluating to false.
  Now they are considered to be evaluating to true and the "on change"
  option only has an effect on if the actions are performed or not.

  This could have effects on user which use the "Macro" condition type
  to check what other macros' conditions evaluate to.

* The splitter position separating the conditions, actions, and
  else-actions are not saved and restored based on which macro is being
  selected.
2023-09-09 11:16:50 +02:00
WarmUpTill
8246197ae6 Add "else" action block 2023-09-09 11:16:50 +02:00
WarmUpTill
8f90bf8846 Move splitter save/load helpers to utility 2023-09-09 11:16:50 +02:00
WarmUpTill
b955e8a0ea Move action and condition macro segement list to Ui_AdvSceneSwitcher 2023-09-09 11:16:50 +02:00
WarmUpTill
81da32f7a2 Only show slide show condition type if OBS supports necessary features 2023-09-09 09:56:29 +02:00
WarmUpTill
5324fa9350 Reset monitor selection in projector action when monitor setup changed 2023-09-06 21:30:55 +02:00
WarmUpTill
c375daa258 Add option to start commercial 2023-09-03 14:29:14 +02:00
WarmUpTill
4f3bd699c8 Fix tooltip not being translated 2023-09-03 14:29:14 +02:00
WarmUpTill
f5024621c0 Add option to assign environment variable value to variable 2023-09-01 20:37:02 +02:00
WarmUpTill
8308506d71 Rely on Qt when using GetCurrentWindowTitle() and OBS is in focus 2023-09-01 19:22:02 +02:00
WarmUpTill
6605c19202 Add OBS windows to GetWindowList() 2023-09-01 19:22:02 +02:00
WarmUpTill
fdf1a94f97 Add option to set placeholder text when asking for input 2023-08-29 20:46:33 +02:00
WarmUpTill
6b6e37da92 Hide preview scene options from studio mode action and condition
The options will only be hidden if the it is not being used.
Thus the functionality of existing macros will not be affected.
2023-08-28 21:19:38 +02:00
WarmUpTill
0f0fc8ae4e Add option to switch preview scene to scene switch action
Same functionality of the "Studio mode" action will be hidden and
eventually be removed.
2023-08-28 21:19:38 +02:00
WarmUpTill
4d7e2992e5 Add preview scene support to scene condition
Existing functionality of the "Studio mode" condition will be hidden and
eventually be removed.
2023-08-28 21:19:38 +02:00
WarmUpTill
a5050d4810 Add Twitch action
Allows you to set the stream title and stream category
2023-08-26 17:32:36 +02:00
WarmUpTill
9df6963f08 Add cpp-httplib 2023-08-26 17:32:36 +02:00
WarmUpTill
b833dd4576 Export FilterComboBox widget symbols
To be used for Twitch action
2023-08-26 17:32:36 +02:00
WarmUpTill
39378ae20f Enusre that placeholder text is visible for FilterComboBox 2023-08-26 17:32:36 +02:00
WarmUpTill
5a2cb943f7 Add option to open interaction dialog of source 2023-08-25 18:19:23 +02:00
WarmUpTill
b88209f63d Add option to check for settings change to source and filter condition 2023-08-19 19:21:19 +02:00
WarmUpTill
9c848938f8 Add support for registering additional save and load steps
Also renamed the reset steps to make it more clear what their use is.
2023-08-15 18:24:03 +02:00
WarmUpTill
07b86dda41 Cleanup ItemSettingsDialog and ItemSelection
* Added option to configure name conflict locale
 * Fixed some locale options not being displayed properly due to missing
   obs_module_text() calls and name conflicts not being handled
   properly.
 * No longer force a fixed width
 * Added option to hide the rename context menu entry
 * Use locale for invalid connection and variable selection string
2023-08-15 18:24:03 +02:00
WarmUpTill
49e8bf7639 Export ItemSelection and ItemSettingsDialog widget symbols
This is done to be able to support use of these widgets in plugins.
2023-08-15 18:24:03 +02:00
WarmUpTill
a2d0a7f544 Fix OCR value not being assigned to variables with enabled regex 2023-08-15 18:24:03 +02:00
WarmUpTill
1a8c894ddf Cleanup
* Fix warnings
 * Remove unused includes
 * Fix build issue if libremidi CMakeLists.txt does not exist
2023-08-15 18:24:03 +02:00
WarmUpTill
3f51f63298 Cleanup locale
* Remove unnecessary widget spacing
* Remove unused values
2023-08-15 18:24:03 +02:00
WarmUpTill
a0b4df574b Add option to disable filtering by typing in FilterComboBox 2023-08-14 15:47:38 +02:00
94 changed files with 6527 additions and 1329 deletions

View File

@@ -0,0 +1,79 @@
name: 'Setup plugin build dependencies'
description: 'Builds the plugin build dependencies'
inputs:
target:
description: 'Build target for dependencies'
required: true
config:
description: 'Build configuration'
required: false
default: 'Release'
visualStudio:
description: 'Visual Studio version (Windows only)'
required: false
default: 'Visual Studio 16 2019'
workingDirectory:
description: 'Working directory for packaging'
required: false
default: ${{ github.workspace }}
runs:
using: 'composite'
steps:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.24.x'
- name: Restore cached dependencies
id: restore-cache
uses: actions/cache@v3
with:
path: ${{ env.DEP_DIR }}
key: ${{ env.DEP_DIR }}-${{ runner.os }}-${{ inputs.target }}
- name: Run macOS Build
if: ${{ runner.os == 'macOS' && steps.restore-cache.outputs.cache-hit != 'true' }}
shell: zsh {0}
run: |
build_args=(
-c ${{ inputs.config }}
-t macos-${{ inputs.target }}
)
if (( ${+CI} && ${+RUNNER_DEBUG} )) build_args+=(--debug)
${{ inputs.workingDirectory }}/.github/scripts/build-deps-macos.zsh -o ${{ env.DEP_DIR }} ${build_args}
- name: Run Linux Build
if: ${{ runner.os == 'Linux' && steps.restore-cache.outputs.cache-hit != 'true' }}
shell: bash
run: |
build_args=(
-c ${{ inputs.config }}
-t linux-${{ inputs.target }}
)
if [[ -n "${CI}" && -n "${RUNNER_DEBUG}" ]]; then
build_args+=(--debug)
fi
${{ inputs.workingDirectory }}/.github/scripts/build-deps-linux.sh -o ${{ env.DEP_DIR }} "${build_args[@]}"
- name: Run Windows Build
if: ${{ runner.os == 'Windows' && steps.restore-cache.outputs.cache-hit != 'true' }}
shell: pwsh
run: |
$BuildArgs = @{
Target = '${{ inputs.target }}'
Configuration = '${{ inputs.config }}'
CMakeGenerator = '${{ inputs.visualStudio }}'
}
if ( ( Test-Path env:CI ) -and ( Test-Path env:RUNNER_DEBUG ) ) {
$BuildArgs += @{
Debug = $true
}
}
${{ inputs.workingDirectory }}/.github/scripts/Build-Deps-Windows.ps1 -OutDirName ${{ env.DEP_DIR }} @BuildArgs

View File

@@ -49,7 +49,7 @@ runs:
if [[ '${{ inputs.codesign }}' == 'true' ]] build_args+=(-s)
if (( ${+CI} && ${+RUNNER_DEBUG} )) build_args+=(--debug)
${{ inputs.workingDirectory }}/.github/scripts/build-macos.zsh ${build_args}
${{ inputs.workingDirectory }}/.github/scripts/build-macos.zsh -d ${{ env.DEP_DIR }} ${build_args}
- name: Run Linux Build
if: ${{ runner.os == 'Linux' }}
@@ -68,7 +68,7 @@ runs:
build_args+=(-p)
fi
${{ inputs.workingDirectory }}/.github/scripts/build-linux.sh "${build_args[@]}"
${{ inputs.workingDirectory }}/.github/scripts/build-linux.sh -d ${{ env.DEP_DIR }} "${build_args[@]}"
- name: Run Windows Build
if: ${{ runner.os == 'Windows' }}
@@ -86,4 +86,4 @@ runs:
}
}
${{ inputs.workingDirectory }}/.github/scripts/Build-Windows.ps1 @BuildArgs
${{ inputs.workingDirectory }}/.github/scripts/Build-Windows.ps1 -ADVSSDepName ${{ env.DEP_DIR }} @BuildArgs

View File

@@ -1,3 +1,4 @@
package '7zip.7zip', path: '7-zip', bin: '7z'
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl'

324
.github/scripts/.build-deps.zsh vendored Executable file
View File

@@ -0,0 +1,324 @@
#!/usr/bin/env zsh
builtin emulate -L zsh
setopt EXTENDED_GLOB
setopt PUSHD_SILENT
setopt ERR_EXIT
setopt ERR_RETURN
setopt NO_UNSET
setopt PIPE_FAIL
setopt NO_AUTO_PUSHD
setopt NO_PUSHD_IGNORE_DUPS
setopt FUNCTION_ARGZERO
## Enable for script debugging
# setopt WARN_CREATE_GLOBAL
# setopt WARN_NESTED_VAR
# setopt XTRACE
autoload -Uz is-at-least && if ! is-at-least 5.2; then
print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade Zsh to fix this issue."
exit 1
fi
_trap_error() {
print -u2 -PR '%F{1} ✖︎ script execution error%f'
print -PR -e "
Callstack:
${(j:\n :)funcfiletrace}
"
exit 2
}
build() {
if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h}
local host_os=${${(s:-:)ZSH_ARGZERO:t:r}[3]}
local target="${host_os}-${CPUTYPE}"
local project_root=${SCRIPT_HOME:A:h:h}
local buildspec_file="${project_root}/buildspec.json"
trap '_trap_error' ZERR
fpath=("${SCRIPT_HOME}/utils.zsh" ${fpath})
autoload -Uz log_info log_error log_output set_loglevel check_${host_os} setup_${host_os} setup_obs setup_ccache
if [[ ! -r ${buildspec_file} ]] {
log_error \
'No buildspec.json found. Please create a build specification for your project.' \
'A buildspec.json.template file is provided in the repository to get you started.'
return 2
}
typeset -g -a skips=()
local -i _verbosity=1
local -r _version='1.0.0'
local -r -a _valid_targets=(
macos-x86_64
macos-arm64
macos-universal
linux-x86_64
)
local -r -a _valid_configs=(Debug RelWithDebInfo Release MinSizeRel)
if [[ ${host_os} == 'macos' ]] {
local -r -a _valid_generators=(Xcode Ninja 'Unix Makefiles')
local generator="${${CI:+Ninja}:-Xcode}"
} else {
local -r -a _valid_generators=(Ninja 'Unix Makefiles')
local generator='Ninja'
}
local -r _usage="
Usage: %B${functrace[1]%:*}%b <option> [<options>]
%BOptions%b:
%F{yellow} Build configuration options%f
-----------------------------------------------------------------------------
%B-t | --target%b Specify target - default: %B%F{green}${host_os}-${CPUTYPE}%f%b
%B-c | --config%b Build configuration - default: %B%F{green}RelWithDebInfo%f%b
%B-o | --out%b Output directory - default: %B%F{green}RelWithDebInfo%f%b
%B--generator%b Specify build system to generate - default: %B%F{green}Ninja%f%b
Available generators:
- Ninja
- Unix Makefiles
- Xcode (macOS only)
%F{yellow} Output options%f
-----------------------------------------------------------------------------
%B-q | --quiet%b Quiet (error output only)
%B-v | --verbose%b Verbose (more detailed output)
%B--skip-[all|build|deps|unpack]%b Skip all|building OBS|checking for dependencies|unpacking dependencies
%B--debug%b Debug (very detailed and added output)
%F{yellow} General options%f
-----------------------------------------------------------------------------
%B-h | --help%b Print this usage help
%B-V | --version%b Print script version information"
local -a args
while (( # )) {
case ${1} {
-t|--target|-c|--config|--generator)
if (( # == 1 )) || [[ ${2:0:1} == '-' ]] {
log_error "Missing value for option %B${1}%b"
log_output ${_usage}
exit 2
}
;;
}
case ${1} {
--)
shift
args+=($@)
break
;;
-t|--target)
if (( ! ${_valid_targets[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
target=${2}
shift 2
;;
-c|--config)
if (( ! ${_valid_configs[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
BUILD_CONFIG=${2}
shift 2
;;
-o|--out)
OUT_DIR="${2}"
shift 2
;;
-q|--quiet) (( _verbosity -= 1 )) || true; shift ;;
-v|--verbose) (( _verbosity += 1 )); shift ;;
-h|--help) log_output ${_usage}; exit 0 ;;
-V|--version) print -Pr "${_version}"; exit 0 ;;
--debug) _verbosity=3; shift ;;
--generator)
if (( ! ${_valid_generators[(Ie)${2}]} )) {
log_error "Invalid value %B${2}%b for option %B${1}%b"
log_output ${_usage}
exit 2
}
generator=${2}
shift 2
;;
--skip-*)
local _skip="${${(s:-:)1}[-1]}"
local _check=(all deps unpack build)
(( ${_check[(Ie)${_skip}]} )) || log_warning "Invalid skip mode %B${_skip}%b supplied"
typeset -g -a skips=(${skips} ${_skip})
shift
;;
*) log_error "Unknown option: %B${1}%b"; log_output ${_usage}; exit 2 ;;
}
}
set -- ${(@)args}
set_loglevel ${_verbosity}
check_${host_os}
setup_ccache
typeset -g QT_VERSION
typeset -g DEPLOYMENT_TARGET
typeset -g OBS_DEPS_VERSION
setup_${host_os}
local product_name
local product_version
local git_tag="$(git describe --tags)"
read -r product_name product_version <<< \
"$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})"
if [[ "${git_tag}" =~ '^([0-9]+\.){0,2}(\*|[0-9]+)$' ]] {
log_info "Using git tag as version identifier '${git_tag}'"
product_version="${git_tag}"
} else {
log_info "Using buildspec.json version identifier '${product_version}'"
}
if [[ -z "${OUT_DIR}" ]] {
OUT_DIR="advss-build-dependencies"
}
mkdir -p "${project_root}/../${OUT_DIR}"
local advss_dep_path="$(realpath ${project_root}/../${OUT_DIR})"
local _plugin_deps="${project_root:h}/obs-build-dependencies/plugin-deps-${OBS_DEPS_VERSION}-qt${QT_VERSION}-${target##*-}"
case ${host_os} {
macos)
local opencv_dir="${project_root}/deps/opencv"
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
local -a opencv_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DBUILD_LIST=core,imgproc,objdetect
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
)
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} -G ${generator} ${opencv_cmake_args}
log_info "Building OpenCV ..."
cmake --build ${opencv_build_dir} --config Release
log_info "Installing OpenCV..."
cmake --install ${opencv_build_dir} --prefix "${advss_dep_path}" --config Release || true
popd
local leptonica_dir="${project_root}/deps/leptonica"
local leptonica_build_dir="${leptonica_dir}/build_${target##*-}"
local -a leptonica_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DOPENJPEG_SUPPORT=OFF
-DLIBWEBP_SUPPORT=OFF
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
)
pushd ${leptonica_dir}
log_info "Configure Leptonica ..."
cmake -S . -B ${leptonica_build_dir} -G ${generator} ${leptonica_cmake_args}
log_info "Building Leptonica ..."
cmake --build ${leptonica_build_dir} --config Release
log_info "Installing Leptonica..."
# Workaround for "unknown file attribute: H" errors when running install
cmake --install ${leptonica_build_dir} --prefix "${advss_dep_path}" --config Release || :
popd
local tesseract_dir="${project_root}/deps/tesseract"
local tesseract_build_dir="${tesseract_dir}/build_${target##*-}"
local -a tesseract_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DBUILD_TRAINING_TOOLS=OFF
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
)
if [ "${target}" != "macos-x86_64" ]; then
tesseract_cmake_args+=(
-DCMAKE_SYSTEM_PROCESSOR=aarch64
-DHAVE_AVX=FALSE
-DHAVE_AVX2=FALSE
-DHAVE_AVX512F=FALSE
-DHAVE_FMA=FALSE
-DHAVE_SSE4_1=FALSE
-DHAVE_NEON=TRUE
)
sed -i'.original' 's/HAVE_NEON FALSE/HAVE_NEON TRUE/g' "${tesseract_dir}/CMakeLists.txt"
fi
pushd ${tesseract_dir}
log_info "Configure Tesseract ..."
cmake -S . -B ${tesseract_build_dir} -G ${generator} ${tesseract_cmake_args}
log_info "Building Tesseract ..."
cmake --build ${tesseract_build_dir} --config Release
log_info "Installing Tesseract..."
cmake --install ${tesseract_build_dir} --prefix "${advss_dep_path}" --config Release
popd
pushd ${advss_dep_path}
log_info "Prepare openssl ..."
rm -rf openssl
git clone git://git.openssl.org/openssl.git --branch openssl-3.1.2 --depth 1
mv openssl openssl_x86
cp -r openssl_x86 openssl_arm
log_info "Building openssl x86 ..."
export MACOSX_DEPLOYMENT_TARGET=10.9
cd openssl_x86
./Configure darwin64-x86_64-cc shared
make
log_info "Building openssl arm ..."
export MACOSX_DEPLOYMENT_TARGET=10.15
cd ../openssl_arm
./Configure enable-rc5 zlib darwin64-arm64-cc no-asm
make
log_info "Combine arm and x86 openssl binaries ..."
cd ..
mkdir openssl-combined
lipo -create openssl_x86/libcrypto.a openssl_arm/libcrypto.a -output openssl-combined/libcrypto.a
lipo -create openssl_x86/libssl.a openssl_arm/libssl.a -output openssl-combined/libssl.a
log_info "Clean up openssl dir..."
mv openssl_x86 openssl
rm -rf openssl_arm
;;
linux)
# Nothing to do for now
;;
}
}
build ${@}

View File

@@ -36,6 +36,7 @@ build() {
local target="${host_os}-${CPUTYPE}"
local project_root=${SCRIPT_HOME:A:h:h}
local buildspec_file="${project_root}/buildspec.json"
local dep_dir=""
trap '_trap_error' ZERR
@@ -77,6 +78,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
%B-c | --config%b Build configuration - default: %B%F{green}RelWithDebInfo%f%b
%B-s | --codesign%b Enable codesigning (macOS only)
%B-p | --portable%b Enable portable mode (Linux only)
%B-d | --dep%b Dependency directory name - default: %B%F{green}advss-build-dependencies%f%b
%B--generator%b Specify build system to generate - default: %B%F{green}Ninja%f%b
Available generators:
- Ninja
@@ -130,6 +132,10 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
BUILD_CONFIG=${2}
shift 2
;;
-d|--dep)
dep_dir="${2}"
shift 2
;;
-s|--codesign) CODESIGN=1; shift ;;
-p|--portable) typeset -g PORTABLE=1; shift ;;
-q|--quiet) (( _verbosity -= 1 )) || true; shift ;;
@@ -168,6 +174,15 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
typeset -g OBS_DEPS_VERSION
setup_${host_os}
local advss_deps_path
if [[ -z "${dep_dir}" ]] {
log_info "Building advss deps ..."
dep_dir="advss-build-dependencies"
${SCRIPT_HOME}/build-deps-${host_os}.zsh -c "${BUILD_CONFIG:-RelWithDebInfo}" -t "${target}" --generator "${generator}" -o "${dep_dir}" --skip-deps --skip-unpack
}
advss_deps_path=$(realpath ${project_root}/../${dep_dir})
log_info "Using advss deps at $advss_deps_path ..."
local product_name
local product_version
local git_tag="$(git describe --tags)"
@@ -187,96 +202,6 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
sed -i '' \
"s/project(\(.*\) VERSION \(.*\))/project(${product_name} VERSION ${product_version})/" \
"${project_root}/CMakeLists.txt"
local opencv_dir="${project_root}/deps/opencv"
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
local -a opencv_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DBUILD_LIST=core,imgproc,objdetect
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
)
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} -G ${generator} ${opencv_cmake_args}
log_info "Building OpenCV ..."
cmake --build ${opencv_build_dir} --config Release
log_info "Installing OpenCV..."
cmake --install ${opencv_build_dir} --config Release || true
popd
local leptonica_dir="${project_root}/deps/leptonica"
local leptonica_build_dir="${leptonica_dir}/build_${target##*-}"
local -a leptonica_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DOPENJPEG_SUPPORT=OFF
-DLIBWEBP_SUPPORT=OFF
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE
)
pushd ${leptonica_dir}
log_info "Configure Leptonica ..."
cmake -S . -B ${leptonica_build_dir} -G ${generator} ${leptonica_cmake_args}
log_info "Building Leptonica ..."
cmake --build ${leptonica_build_dir} --config Release
log_info "Installing Leptonica..."
# Workaround for "unknown file attribute: H" errors when running install
cmake --install ${leptonica_build_dir} --config Release || :
popd
local tesseract_dir="${project_root}/deps/tesseract"
local tesseract_build_dir="${tesseract_dir}/build_${target##*-}"
local -a tesseract_cmake_args=(
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DBUILD_TRAINING_TOOLS=OFF
)
if [ "${target}" != "macos-x86_64" ]; then
tesseract_cmake_args+=(
-DCMAKE_SYSTEM_PROCESSOR=aarch64
-DHAVE_AVX=FALSE
-DHAVE_AVX2=FALSE
-DHAVE_AVX512F=FALSE
-DHAVE_FMA=FALSE
-DHAVE_SSE4_1=FALSE
-DHAVE_NEON=TRUE
)
sed -i'.original' 's/HAVE_NEON FALSE/HAVE_NEON TRUE/g' "${tesseract_dir}/CMakeLists.txt"
fi
pushd ${tesseract_dir}
log_info "Configure Tesseract ..."
cmake -S . -B ${tesseract_build_dir} -G ${generator} ${tesseract_cmake_args}
log_info "Building Tesseract ..."
#cmake --build ${tesseract_build_dir} --config Release --target libtesseract
cmake --build ${tesseract_build_dir} --config Release
log_info "Installing Tesseract..."
#cmake --install ${tesseract_build_dir} --config Release --component libtesseract
cmake --install ${tesseract_build_dir} --config Release
popd
;;
linux)
sed -i'' \
@@ -295,7 +220,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
local -a cmake_args=(
-DCMAKE_BUILD_TYPE=${BUILD_CONFIG:-RelWithDebInfo}
-DQT_VERSION=${QT_VERSION}
-DCMAKE_PREFIX_PATH="${_plugin_deps}"
-DCMAKE_PREFIX_PATH="${_plugin_deps};${advss_deps_path}"
)
if (( _loglevel == 0 )) cmake_args+=(-Wno_deprecated -Wno-dev --log-level=ERROR)
@@ -312,12 +237,17 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
num_procs=$(( $(sysctl -n hw.ncpu) + 1 ))
local openssl_lib_dir="${advss_deps_path}/openssl-combined/"
local openssl_include_dir="${advss_deps_path}/openssl/include"
cmake_args+=(
-DCMAKE_FRAMEWORK_PATH="${_plugin_deps}/Frameworks"
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DOBS_CODESIGN_LINKER=ON
-DOBS_BUNDLE_CODESIGN_IDENTITY="${CODESIGN_IDENT:--}"
-DOPENSSL_INCLUDE_DIR="${openssl_include_dir}"
-DOPENSSL_LIBRARIES="${openssl_lib_dir}/libcrypto.a;${openssl_lib_dir}/libssl.a"
)
;;

172
.github/scripts/Build-Deps-Windows.ps1 vendored Normal file
View File

@@ -0,0 +1,172 @@
[CmdletBinding()]
param(
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
[ValidateSet('x86', 'x64')]
[string] $Target,
[ValidateSet('Visual Studio 17 2022', 'Visual Studio 16 2019')]
[string] $CMakeGenerator,
[string] $OutDirName,
[switch] $SkipDeps,
[switch] $SkipUnpack
)
$ErrorActionPreference = 'Stop'
if ( $DebugPreference -eq 'Continue' ) {
$VerbosePreference = 'Continue'
$InformationPreference = 'Continue'
}
if ( $PSVersionTable.PSVersion -lt '7.0.0' ) {
Write-Warning 'The obs-deps PowerShell build script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6'
exit 2
}
function Build {
trap {
Pop-Location -Stack BuildTemp -ErrorAction 'SilentlyContinue'
Write-Error $_
exit 2
}
$ScriptHome = $PSScriptRoot
$ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.."
$BuildSpecFile = "${ProjectRoot}/buildspec.json"
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
foreach ($Utility in $UtilityFunctions) {
Write-Debug "Loading $($Utility.FullName)"
. $Utility.FullName
}
$BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json
$ProductName = $BuildSpec.name
$ProductVersion = $BuildSpec.version
$script:VisualStudioVersion = ''
$script:PlatformSDK = '10.0.18363.657'
Setup-Host
if ( $CmakeGenerator -eq '' ) {
$CmakeGenerator = $script:VisualStudioVersion
}
$DepsPath = "plugin-deps-${script:DepsVersion}-*-${script:Target}"
$OBSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
if ( $OutDirName -eq '' ) {
$OutDirName = "advss-build-dependencies"
}
New-Item -ItemType Directory -Force -Path ${ProjectRoot}/../${OutDirName}
$ADVSSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/../${OutDirName})"
$OpenCVPath = "${ProjectRoot}/deps/opencv"
$OpenCVBuildPath = "${OpenCVPath}/build"
Push-Location -Stack BuildOpenCVTemp
Ensure-Location $ProjectRoot
$OpenCVCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=Release"
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DBUILD_LIST=core,imgproc,objdetect"
)
Log-Information "Configuring OpenCV..."
Invoke-External cmake -S ${OpenCVPath} -B ${OpenCVBuildPath} @OpenCVCmakeArgs
$OpenCVCmakeArgs = @(
'--config', "Release"
)
if ( $VerbosePreference -eq 'Continue' ) {
$OpenCVCmakeArgs += ('--verbose')
}
Log-Information "Building OpenCV..."
Invoke-External cmake --build "${OpenCVBuildPath}" @OpenCVCmakeArgs
Log-Information "Install OpenCV}..."
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${ADVSSDepPath}" @OpenCVCmakeArgs
$LeptonicaPath = "${ProjectRoot}/deps/leptonica"
$LeptonicaBuildPath = "${LeptonicaPath}/build"
Push-Location -Stack BuildLeptonicaTemp
Ensure-Location $ProjectRoot
$LeptonicaCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DSW_BUILD=OFF"
"-DLIBWEBP_SUPPORT=OFF"
)
Log-Information "Configuring leptonica..."
Invoke-External cmake -S ${LeptonicaPath} -B ${LeptonicaBuildPath} @LeptonicaCmakeArgs
$LeptonicaCmakeArgs = @(
'--config', "${Configuration}"
)
if ( $VerbosePreference -eq 'Continue' ) {
$LeptonicaCmakeArgs += ('--verbose')
}
Log-Information "Building leptonica..."
Invoke-External cmake --build "${LeptonicaBuildPath}" @LeptonicaCmakeArgs
Log-Information "Install leptonica..."
Invoke-External cmake --install "${LeptonicaBuildPath}" --prefix "${ADVSSDepPath}" @LeptonicaCmakeArgs
Push-Location -Stack BuildTesseractTemp
Ensure-Location $ProjectRoot
$TesseractPath = "${ProjectRoot}/deps/tesseract"
$TesseractBuildPath = "${TesseractPath}/build"
# Explicitly disable PkgConfig and tiff as it will lead build errors
$TesseractCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DSW_BUILD=OFF"
"-DDISABLE_CURL=ON"
"-DBUILD_TRAINING_TOOLS=OFF"
"-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE"
"-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=TRUE"
)
Log-Information "Configuring tesseract..."
Invoke-External cmake -S ${TesseractPath} -B ${TesseractBuildPath} @TesseractCmakeArgs
$TesseractCmakeArgs = @(
'--config', "${Configuration}"
)
if ( $VerbosePreference -eq 'Continue' ) {
$TesseractCmakeArgs += ('--verbose')
}
Log-Information "Building tesseract..."
Invoke-External cmake --build "${TesseractBuildPath}" @TesseractCmakeArgs
Log-Information "Install tesseract..."
Invoke-External cmake --install "${TesseractBuildPath}" --prefix "${ADVSSDepPath}" @TesseractCmakeArgs
}
Build

View File

@@ -6,6 +6,7 @@ param(
[string] $Target,
[ValidateSet('Visual Studio 17 2022', 'Visual Studio 16 2019')]
[string] $CMakeGenerator,
[string] $ADVSSDepName,
[switch] $SkipAll,
[switch] $SkipBuild,
[switch] $SkipDeps,
@@ -60,112 +61,13 @@ function Build {
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
$DepInstallPath = "$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
$OpenCVPath = "${ProjectRoot}/deps/opencv"
$OpenCVBuildPath = "${OpenCVPath}/build"
Push-Location -Stack BuildOpenCVTemp
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
Ensure-Location $ProjectRoot
$OpenCVCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=Release"
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
"-DBUILD_LIST=core,imgproc,objdetect"
)
Log-Information "Configuring OpenCV..."
Invoke-External cmake -S ${OpenCVPath} -B ${OpenCVBuildPath} @OpenCVCmakeArgs
$OpenCVCmakeArgs = @(
'--config', "Release"
)
if ( $VerbosePreference -eq 'Continue' ) {
$OpenCVCmakeArgs += ('--verbose')
}
Log-Information "Building OpenCV..."
Invoke-External cmake --build "${OpenCVBuildPath}" @OpenCVCmakeArgs
if ( $ADVSSDepName -eq '' ) {
Log-Information "Building advss deps ..."
$ADVSSDepName = "advss-build-dependencies"
invoke-expression -Command "$PSScriptRoot/Build-Deps-Windows.ps1 -Configuration $Configuration -Target $Target -CMakeGenerator `"$CMakeGenerator`" -OutDirName $ADVSSDepName -SkipDeps -SkipUnpack"
}
Log-Information "Install OpenCV}..."
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${DepInstallPath}" @OpenCVCmakeArgs
$LeptonicaPath = "${ProjectRoot}/deps/leptonica"
$LeptonicaBuildPath = "${LeptonicaPath}/build"
Push-Location -Stack BuildLeptonicaTemp
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
Ensure-Location $ProjectRoot
$LeptonicaCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
"-DSW_BUILD=OFF"
"-DLIBWEBP_SUPPORT=OFF"
)
Log-Information "Configuring leptonica..."
Invoke-External cmake -S ${LeptonicaPath} -B ${LeptonicaBuildPath} @LeptonicaCmakeArgs
$LeptonicaCmakeArgs = @(
'--config', "${Configuration}"
)
if ( $VerbosePreference -eq 'Continue' ) {
$LeptonicaCmakeArgs += ('--verbose')
}
Log-Information "Building leptonica..."
Invoke-External cmake --build "${LeptonicaBuildPath}" @LeptonicaCmakeArgs
}
Log-Information "Install leptonica..."
Invoke-External cmake --install "${LeptonicaBuildPath}" --prefix "${DepInstallPath}" @LeptonicaCmakeArgs
Push-Location -Stack BuildTesseractTemp
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
Ensure-Location $ProjectRoot
$TesseractPath = "${ProjectRoot}/deps/tesseract"
$TesseractBuildPath = "${TesseractPath}/build"
# Explicitly disable PkgConfig and tiff as it will lead build errors
$TesseractCmakeArgs = @(
'-G', $CmakeGenerator
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
"-DSW_BUILD=OFF"
"-DDISABLE_CURL=ON"
"-DBUILD_TRAINING_TOOLS=OFF"
"-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE"
"-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=TRUE"
)
Log-Information "Configuring tesseract..."
Invoke-External cmake -S ${TesseractPath} -B ${TesseractBuildPath} @TesseractCmakeArgs
$TesseractCmakeArgs = @(
'--config', "${Configuration}"
)
if ( $VerbosePreference -eq 'Continue' ) {
$TesseractCmakeArgs += ('--verbose')
}
Log-Information "Building tesseract..."
Invoke-External cmake --build "${TesseractBuildPath}" @TesseractCmakeArgs
}
Log-Information "Install tesseract..."
Invoke-External cmake --install "${TesseractBuildPath}" --prefix "${DepInstallPath}" @TesseractCmakeArgs
$ADVSSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/../${script:ADVSSDepName})"
Log-Information "Using advss deps at $ADVSSDepPath ..."
(Get-Content -Path ${ProjectRoot}/CMakeLists.txt -Raw) `
-replace "project\((.*) VERSION (.*)\)", "project(${ProductName} VERSION ${ProductVersion})" `
@@ -182,7 +84,7 @@ function Build {
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
"-DCMAKE_BUILD_TYPE=${Configuration}"
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath};${ADVSSDepPath}"
"-DQT_VERSION=${script:QtVersion}"
)

13
.github/scripts/build-deps-linux.sh vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
if ! type zsh > /dev/null 2>&1; then
echo ' => Installing script dependency Zsh.'
sudo apt-get -y update
sudo apt-get -y install zsh
fi
SCRIPT=$(readlink -f "${0}")
SCRIPT_DIR=$(dirname "${SCRIPT}")
zsh ${SCRIPT_DIR}/build-deps-linux.zsh "${@}"

1
.github/scripts/build-deps-linux.zsh vendored Symbolic link
View File

@@ -0,0 +1 @@
.build-deps.zsh

1
.github/scripts/build-deps-macos.zsh vendored Symbolic link
View File

@@ -0,0 +1 @@
.build-deps.zsh

View File

@@ -17,6 +17,7 @@ on:
env:
PLUGIN_NAME: SceneSwitcher
LIB_NAME: advanced-scene-switcher
DEP_DIR: advss-build-dependencies-1
jobs:
clang_check:
@@ -115,9 +116,6 @@ jobs:
with:
path: |
${{ github.workspace }}/.ccache
${{ github.workspace }}/plugin/deps/opencv/build_*
${{ github.workspace }}/plugin/deps/leptonica/build_*
${{ github.workspace }}/plugin/deps/tesseract/build*
key: macos-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
restore-keys: |
macos-${{ matrix.arch }}-ccache-plugin-
@@ -146,6 +144,13 @@ jobs:
print "CODESIGN_IDENT=${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}" >> $GITHUB_ENV
print "CODESIGN_IDENT_INSTALLER=${{ secrets.MACOS_SIGNING_INSTALLER_IDENTITY }}" >> $GITHUB_ENV
- name: Build Dependencies
uses: ./plugin/.github/actions/build-dependencies
with:
workingDirectory: ${{ github.workspace }}/plugin
target: ${{ matrix.arch }}
config: RelWithDebInfo
- name: Build Plugin
uses: ./plugin/.github/actions/build-plugin
with:
@@ -248,6 +253,13 @@ jobs:
echo 'found=false' >> $GITHUB_OUTPUT
fi
- name: Build Dependencies
uses: ./plugin/.github/actions/build-dependencies
with:
workingDirectory: ${{ github.workspace }}/plugin
target: ${{ matrix.arch }}
config: RelWithDebInfo
- name: Build Plugin
uses: ./plugin/.github/actions/build-plugin
with:
@@ -329,9 +341,6 @@ jobs:
with:
path: |
${{ github.workspace }}/.ccache
${{ github.workspace }}/plugin/deps/opencv/build_*
${{ github.workspace }}/plugin/deps/leptonica/build_*
${{ github.workspace }}/plugin/deps/tesseract/build*
key: windows-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
restore-keys: |
windows-${{ matrix.arch }}-ccache-plugin-
@@ -357,6 +366,14 @@ jobs:
"found=$(([string]${LabelFound}).ToLower())" >> $env:GITHUB_OUTPUT
- name: Build Dependencies
uses: ./plugin/.github/actions/build-dependencies
with:
workingDirectory: ${{ github.workspace }}/plugin
target: ${{ matrix.arch }}
config: RelWithDebInfo
visualStudio: 'Visual Studio 17 2022'
- name: Build Plugin
uses: ./plugin/.github/actions/build-plugin
with:

1
.gitignore vendored
View File

@@ -91,6 +91,7 @@ Thumbs.db
.settings
.idea
.metadata
.vscode
*.iml
*.ipr
*.sublime*

3
.gitmodules vendored
View File

@@ -19,3 +19,6 @@
[submodule "deps/libremidi"]
path = deps/libremidi
url = https://github.com/jcelerier/libremidi.git
[submodule "deps/cpp-httplib"]
path = deps/cpp-httplib
url = https://github.com/yhirose/cpp-httplib.git

View File

@@ -254,6 +254,7 @@ target_sources(
src/utils/curl-helper.hpp
src/utils/duration-control.cpp
src/utils/duration-control.hpp
src/utils/export-symbol-helper.hpp
src/utils/item-selection-helpers.cpp
src/utils/item-selection-helpers.hpp
src/utils/log-helper.hpp
@@ -267,6 +268,8 @@ target_sources(
src/utils/macro-export-import-dialog.hpp
src/utils/macro-list.cpp
src/utils/macro-list.hpp
src/utils/macro-run-button.cpp
src/utils/macro-run-button.hpp
src/utils/macro-segment-selection.cpp
src/utils/macro-segment-selection.hpp
src/utils/math-helpers.cpp
@@ -375,6 +378,8 @@ target_include_directories(
"${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib"
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
target_compile_definitions(${LIB_NAME} PRIVATE ADVSS_EXPORT_SYMBOLS=1)
# --- End of section ---
# --- Windows-specific build settings and tasks ---

View File

@@ -146,18 +146,15 @@ AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Während des
AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}"
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
AdvSceneSwitcher.condition.window="Fenster"
AdvSceneSwitcher.condition.window.entry.line1="{{windows}} existiert und ..."
AdvSceneSwitcher.condition.window.entry.line2="... ist {{fullscreen}} Vollbild {{maximized}} Maximiert {{focused}} Fokusiert {{windowFocusChanged}} Vordergrundfenster geändert"
AdvSceneSwitcher.condition.window.entry.line3="Aktuelles Vordergrundfenster: {{focusWindow}}"
AdvSceneSwitcher.condition.file="Datei"
AdvSceneSwitcher.condition.file.type.match="entspricht"
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
AdvSceneSwitcher.condition.file.local="Lokale Datei"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medien"
AdvSceneSwitcher.condition.media.source="Quelle"
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
@@ -395,9 +392,9 @@ AdvSceneSwitcher.condition.variable.type.greaterThanVariable="ist größer als d
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="Szene wechseln"
AdvSceneSwitcher.action.scene.entry="Wechsle zu Szene{{scenes}}mittels{{transitions}}mit einer Dauer von{{duration}}Sekunden"
AdvSceneSwitcher.action.scene.entry.noDuration="Wechsle zu Szene{{scenes}}mittels{{transitions}}"
AdvSceneSwitcher.action.scene="Szene wechseln"
AdvSceneSwitcher.action.scene.entry="Wechsle{{sceneTypes}}Szene zu{{scenes}}mittels{{transitions}}mit einer Dauer von{{duration}}Sekunden"
AdvSceneSwitcher.action.scene.entry.noDuration="Wechsle{{sceneTypes}}Szene zu{{scenes}}mittels{{transitions}}"
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Warten, bis der Übergang zur Zielszene abgeschlossen ist"
AdvSceneSwitcher.action.wait="Warten"
AdvSceneSwitcher.action.wait.type.fixed="fixe"
@@ -440,11 +437,6 @@ AdvSceneSwitcher.action.streaming.type.stop="Stream stoppen"
AdvSceneSwitcher.action.streaming.type.start="Stream starten"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ausführen"
AdvSceneSwitcher.action.run.arguments="Argumente:"
AdvSceneSwitcher.action.run.addArgument="Argument hinzufügen"
AdvSceneSwitcher.action.run.addArgumentDescription="Neues Argument hinzufügen:"
AdvSceneSwitcher.action.run.entry="Ausführen {{filePath}}"
AdvSceneSwitcher.action.run.entry.workingDirectory="Arbeitsverzeichnis:{{workingDirectory}}"
AdvSceneSwitcher.action.sceneVisibility="Sichtbarkeit von Szenenelementen"
AdvSceneSwitcher.action.sceneVisibility.type.show="Anzeigen"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Verstecken"
@@ -506,7 +498,6 @@ AdvSceneSwitcher.action.hotkey.leftMeta="Linke Meta-Taste"
AdvSceneSwitcher.action.hotkey.rightMeta="Rechte Meta-Taste"
AdvSceneSwitcher.action.hotkey.onlyOBS="Tastendruck nur an OBS senden"
AdvSceneSwitcher.action.hotkey.disabled="Globale Tastendrücke können nicht simuliert werden - die Funktionalität beschränkt sich auf das Senden von Tastendrücken an OBS!"
AdvSceneSwitcher.action.hotkey.entry="Drücke {{keys}} für {{duration}}"
AdvSceneSwitcher.action.sceneOrder="Reihenfolge der Szenenelemente"
AdvSceneSwitcher.action.sceneOrder.type.moveUp="Nach oben verschieben"
AdvSceneSwitcher.action.sceneOrder.type.moveDown="Nach unten verschieben"
@@ -567,8 +558,6 @@ AdvSceneSwitcher.action.sequence.continueFrom="Weiter mit ausgewähltem Element"
AdvSceneSwitcher.action.websocket="Websocket"
AdvSceneSwitcher.action.websocket.type.request="Anfrage"
AdvSceneSwitcher.action.websocket.type.event="Ereignis"
AdvSceneSwitcher.action.websocket.entry.request="Szenenwechsler {{type}} via {{connection}} senden"
AdvSceneSwitcher.action.websocket.entry.event="Szenenwechsler {{type}} an verbundene Clients senden"
AdvSceneSwitcher.action.http="HTTP"
AdvSceneSwitcher.action.http.type.get="GET"
AdvSceneSwitcher.action.http.type.post="POST"
@@ -586,7 +575,7 @@ AdvSceneSwitcher.action.variable.invalidSelection="Ungültige Auswahl!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Aktionen wird nicht unterstützt!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Bedingungen wird nicht unterstützt!"
AdvSceneSwitcher.action.variable.currentSegmentValue="Aktueller Wert:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
; Transition Tab

View File

@@ -30,6 +30,7 @@ AdvSceneSwitcher.generalTab.generalBehavior.verboseLogging="Enable verbose loggi
AdvSceneSwitcher.generalTab.generalBehavior.saveWindowGeo="Save window position and size"
AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Show system tray notifications"
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints"
AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailure="Display warning if plugins cannot be loaded"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage="<html><body>Loading of the following plugin libraries was unsuccessful, which could result in some Advanced Scene Switcher functions not being available:%1Check the OBS logs for details.<br>This message can be disabled on the General tab.</body></html>"
AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can be represented via macros"
@@ -70,6 +71,7 @@ AdvSceneSwitcher.macroTab.priorityWarning="Note: It is recommended to configure
AdvSceneSwitcher.macroTab.help="Macros allow you to execute a string of actions depending on multiple conditions.\n\nClick on the highlighted plus symbol to add a new Macro."
AdvSceneSwitcher.macroTab.editConditionHelp="This section allows you to define Macro conditions.\n\nSelect an existing or add a new Macro on the left.\nThen click the plus button below to add a new condition."
AdvSceneSwitcher.macroTab.editActionHelp="This section allows you to define Macro actions.\n\nSelect an existing or add a new Macro on the left.\nThen click the plus button below to add a new action."
AdvSceneSwitcher.macroTab.editElseActionHelp="This section allows you to define Macro actions, which are executed if the conditions are *not* met.\n\nSelect an existing or add a new Macro on the left.\nThen click the plus button below to add a new action."
AdvSceneSwitcher.macroTab.edit="Edit macro"
AdvSceneSwitcher.macroTab.edit.logic="Logic type:"
AdvSceneSwitcher.macroTab.edit.condition="Condition type:"
@@ -77,6 +79,7 @@ AdvSceneSwitcher.macroTab.edit.action="Action type:"
AdvSceneSwitcher.macroTab.add="Add new macro"
AdvSceneSwitcher.macroTab.name="Name:"
AdvSceneSwitcher.macroTab.run="Run macro"
AdvSceneSwitcher.macroTab.runElse="Run macro (else)"
AdvSceneSwitcher.macroTab.runFail="Running \"%1\" failed!\nEither one of the actions failed or the macro is running already."
AdvSceneSwitcher.macroTab.runInParallel="Run macro in parallel to other macros"
AdvSceneSwitcher.macroTab.onChange="Perform actions only on condition change"
@@ -154,7 +157,7 @@ AdvSceneSwitcher.condition.audio.type.volume="Configured volume level"
AdvSceneSwitcher.condition.audio.type.syncOffset="Sync offset"
AdvSceneSwitcher.condition.audio.type.monitor="Audio monitoring"
AdvSceneSwitcher.condition.audio.type.balance="Audio balance"
AdvSceneSwitcher.condition.audio.entry="{{checkType}} of {{audioSources}} is {{condition}}{{volume}}{{syncOffset}}{{monitorTypes}}"
AdvSceneSwitcher.condition.audio.entry="{{checkType}}of{{audioSources}}is{{condition}}{{volume}}{{syncOffset}}{{monitorTypes}}"
AdvSceneSwitcher.condition.cursor="Cursor"
AdvSceneSwitcher.condition.cursor.type.region="is in region"
AdvSceneSwitcher.condition.cursor.type.moving="is moving"
@@ -164,15 +167,17 @@ AdvSceneSwitcher.condition.cursor.button.middle="Middle mouse button"
AdvSceneSwitcher.condition.cursor.button.right="Right mouse button"
AdvSceneSwitcher.condition.cursor.showFrame="Show frame"
AdvSceneSwitcher.condition.cursor.hideFrame="Hide frame"
AdvSceneSwitcher.condition.cursor.entry.line1="Cursor {{conditions}}{{buttons}}{{minX}}{{minY}}{{maxX}}{{maxY}}{{toggleFrameButton}}"
AdvSceneSwitcher.condition.cursor.entry.line2="Cursor is currently at {{xPos}} x {{yPos}}"
AdvSceneSwitcher.condition.cursor.entry.line1="Cursor{{conditions}}{{buttons}}{{minX}}{{minY}}{{maxX}}{{maxY}}{{toggleFrameButton}}"
AdvSceneSwitcher.condition.cursor.entry.line2="Cursor is currently at{{xPos}}x{{yPos}}"
AdvSceneSwitcher.condition.scene="Scene"
AdvSceneSwitcher.condition.scene.type.current="Current scene is"
AdvSceneSwitcher.condition.scene.type.previous="Previous scene is"
AdvSceneSwitcher.condition.scene.type.preview="Preview scene is"
AdvSceneSwitcher.condition.scene.type.changed="Scene changed"
AdvSceneSwitcher.condition.scene.type.notChanged="Scene has not changed"
AdvSceneSwitcher.condition.scene.type.currentPattern="Current scene matches"
AdvSceneSwitcher.condition.scene.type.previousPattern="Previous scene matches"
AdvSceneSwitcher.condition.scene.type.previewPattern="Preview scene matches"
AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="During transition check for transition target scene"
AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="During transition check for transition source scene"
AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}"
@@ -192,9 +197,9 @@ AdvSceneSwitcher.condition.file.type.contentChange="content changed"
AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
AdvSceneSwitcher.condition.file.remote="Remote file"
AdvSceneSwitcher.condition.file.local="Local file"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.source="Source"
AdvSceneSwitcher.condition.media.anyOnScene="Any media source on"
@@ -223,7 +228,7 @@ AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip="This will al
AdvSceneSwitcher.condition.video.patternThreshold="Threshold: "
AdvSceneSwitcher.condition.video.patternThresholdDescription="A higher threshold value means that the pattern needs to match the video source more closely."
AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask="Use alpha channel as mask for pattern."
AdvSceneSwitcher.condition.video.patternMatchMode="Use pattern matching mode {{patternMatchingModes}}"
AdvSceneSwitcher.condition.video.patternMatchMode="Use pattern matching mode{{patternMatchingModes}}"
AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation="Cross correlation"
AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient="Correlation coefficient"
AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference="Squared difference"
@@ -264,9 +269,9 @@ AdvSceneSwitcher.condition.video.type.main="OBS's main output"
AdvSceneSwitcher.condition.video.type.source="Source"
AdvSceneSwitcher.condition.video.type.scene="Scene"
AdvSceneSwitcher.condition.video.entry="{{videoInputTypes}}{{sources}}{{scenes}}{{condition}}{{imagePath}}"
AdvSceneSwitcher.condition.video.entry.modelPath="Model data (haar cascade classifier): {{modelDataPath}}"
AdvSceneSwitcher.condition.video.entry.minNeighbor="Minimum neighbors: {{minNeighbors}}"
AdvSceneSwitcher.condition.video.entry.throttle="{{throttleEnable}}Reduce CPU load by performing check only every {{throttleCount}} milliseconds"
AdvSceneSwitcher.condition.video.entry.modelPath="Model data (haar cascade classifier):{{modelDataPath}}"
AdvSceneSwitcher.condition.video.entry.minNeighbor="Minimum neighbors:{{minNeighbors}}"
AdvSceneSwitcher.condition.video.entry.throttle="{{throttleEnable}}Reduce CPU load by performing check only every{{throttleCount}}milliseconds"
AdvSceneSwitcher.condition.video.entry.checkAreaEnable="Perform check only in area"
AdvSceneSwitcher.condition.video.entry.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
AdvSceneSwitcher.condition.video.entry.orcColorPick="Check for text color:{{textColor}}{{selectColor}}"
@@ -292,10 +297,10 @@ AdvSceneSwitcher.condition.record.state.pause="Recording paused"
AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}} is running {{focused}} and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process: {{focusProcess}}"
AdvSceneSwitcher.condition.process.entry="{{processes}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Idle"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for {{duration}}"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}"
AdvSceneSwitcher.condition.pluginState="Plugin state"
AdvSceneSwitcher.condition.pluginState.state.start="Plugin started"
AdvSceneSwitcher.condition.pluginState.state.restart="Plugin restarted"
@@ -310,10 +315,10 @@ AdvSceneSwitcher.condition.timer.type.fixed="Fixed"
AdvSceneSwitcher.condition.timer.type.random="Random"
AdvSceneSwitcher.condition.timer.pause="Pause"
AdvSceneSwitcher.condition.timer.continue="Continue"
AdvSceneSwitcher.condition.timer.entry.line1.fixed="{{type}} duration of {{duration}} has passed"
AdvSceneSwitcher.condition.timer.entry.line1.random="{{type}} duration from {{duration}} to {{duration2}} has passed"
AdvSceneSwitcher.condition.timer.entry.line2="Time remaining: {{remaining}} seconds"
AdvSceneSwitcher.condition.timer.entry.line3="{{pauseContinue}} {{reset}} {{saveRemaining}} Save time remaining {{autoReset}} Automatically reset timer after duration was reached"
AdvSceneSwitcher.condition.timer.entry.line1.fixed="{{type}}duration of{{duration}}has passed"
AdvSceneSwitcher.condition.timer.entry.line1.random="{{type}}duration from{{duration}}to{{duration2}}has passed"
AdvSceneSwitcher.condition.timer.entry.line2="Time remaining:{{remaining}}seconds"
AdvSceneSwitcher.condition.timer.entry.line3="{{pauseContinue}}{{reset}}{{saveRemaining}}Save time remaining{{autoReset}}Automatically reset timer after duration was reached"
AdvSceneSwitcher.condition.timer.reset="Reset"
AdvSceneSwitcher.condition.macro="Macro"
AdvSceneSwitcher.condition.macro.type.count="Macro run count"
@@ -330,16 +335,17 @@ AdvSceneSwitcher.condition.macro.state.type.above="More than"
AdvSceneSwitcher.condition.macro.state.type.equal="Exactly"
AdvSceneSwitcher.condition.macro.count.reset="Reset"
AdvSceneSwitcher.condition.macro.pausedWarning="Selected macro is currently paused!"
AdvSceneSwitcher.condition.macro.state.entry="Conditions of {{macros}} are true"
AdvSceneSwitcher.condition.macro.multistate.entry="Conditions of {{multiStateConditions}}{{multiStateCount}} of the following macros are true:"
AdvSceneSwitcher.condition.macro.count.entry.line1="{{macros}} was executed {{conditions}} {{count}} times"
AdvSceneSwitcher.condition.macro.count.entry.line2="Current count: {{currentCount}} {{resetCount}}"
AdvSceneSwitcher.condition.macro.state.entry="Conditions of{{macros}}are true"
AdvSceneSwitcher.condition.macro.multistate.entry="Conditions of{{multiStateConditions}}{{multiStateCount}}of the following macros are true:"
AdvSceneSwitcher.condition.macro.count.entry.line1="{{macros}}was executed{{conditions}}{{count}}times"
AdvSceneSwitcher.condition.macro.count.entry.line2="Current count:{{currentCount}}{{resetCount}}"
AdvSceneSwitcher.condition.macro.actionState.disabled.entry="Action{{actionIndex}}of{{macros}}is disabled"
AdvSceneSwitcher.condition.macro.actionState.enabled.entry="Action{{actionIndex}}of{{macros}}is enabled"
AdvSceneSwitcher.condition.source="Source"
AdvSceneSwitcher.condition.source.type.active="Is active"
AdvSceneSwitcher.condition.source.type.showing="Is showing"
AdvSceneSwitcher.condition.source.type.settings="Settings match"
AdvSceneSwitcher.condition.source.type.settingsChanged="Settings changed"
AdvSceneSwitcher.condition.source.sceneVisibilityHint="Scene specific visibility can be checked using the \"Scene item visibility\" condition"
AdvSceneSwitcher.condition.source.getSettings="Get current settings"
AdvSceneSwitcher.condition.source.entry.line1="{{sources}}{{conditions}}"
@@ -353,8 +359,9 @@ AdvSceneSwitcher.condition.filter="Filter"
AdvSceneSwitcher.condition.filter.type.active="Is enabled"
AdvSceneSwitcher.condition.filter.type.showing="Is disabled"
AdvSceneSwitcher.condition.filter.type.settings="Settings match"
AdvSceneSwitcher.condition.filter.type.settingsChanged="Settings changed"
AdvSceneSwitcher.condition.filter.getSettings="Get current settings"
AdvSceneSwitcher.condition.filter.entry.line1="On {{sources}} {{filters}} {{conditions}}"
AdvSceneSwitcher.condition.filter.entry.line1="On{{sources}}{{filters}}{{conditions}}"
AdvSceneSwitcher.condition.filter.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.filter.entry.line3="{{regex}}{{getSettings}}"
AdvSceneSwitcher.condition.sceneOrder="Scene item order"
@@ -367,7 +374,7 @@ AdvSceneSwitcher.condition.hotkey="Hotkey"
AdvSceneSwitcher.condition.hotkey.name="Macro trigger hotkey"
AdvSceneSwitcher.condition.hotkey.tip="Note: You can configure the keybindings for this hotkey in the OBS settings window"
AdvSceneSwitcher.condition.hotkey.entry.line1="Hotkey is pressed"
AdvSceneSwitcher.condition.hotkey.entry.line2="Name: {{name}}"
AdvSceneSwitcher.condition.hotkey.entry.line2="Name:{{name}}"
AdvSceneSwitcher.condition.replay="Replay buffer"
AdvSceneSwitcher.condition.replay.state.stopped="Replay buffer stopped"
AdvSceneSwitcher.condition.replay.state.started="Replay buffer started"
@@ -392,17 +399,17 @@ AdvSceneSwitcher.condition.date.ignoreDate="If unchecked the date component will
AdvSceneSwitcher.condition.date.ignoreTime="If unchecked the time component will be ignored"
AdvSceneSwitcher.condition.date.showAdvancedSettings="Show advanced settings"
AdvSceneSwitcher.condition.date.showSimpleSettings="Show simple settings"
AdvSceneSwitcher.condition.date.entry.simple="On {{dayOfWeek}} {{weekCondition}} {{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}} {{ignoreDate}}{{date}} {{ignoreTime}}{{time}} {{separator}} {{date2}} {{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}} Repeat every {{duration}} on date match"
AdvSceneSwitcher.condition.date.entry.pattern="Current date \"{{currentDate}}\" matches pattern {{pattern}}"
AdvSceneSwitcher.condition.date.entry.simple="On{{dayOfWeek}}{{weekCondition}}{{ignoreWeekTime}}{{weekTime}}"
AdvSceneSwitcher.condition.date.entry.advanced="{{condition}}{{ignoreDate}}{{date}}{{ignoreTime}}{{time}}{{separator}}{{date2}}{{time2}}"
AdvSceneSwitcher.condition.date.entry.repeat="{{repeat}}Repeat every{{duration}}on date match"
AdvSceneSwitcher.condition.date.entry.pattern="Current date \"{{currentDate}}\" matches pattern{{pattern}}"
AdvSceneSwitcher.condition.date.entry.nextMatchDate="Next match at: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} On repeat update selected date to repeat date"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}On repeat update selected date to repeat date"
AdvSceneSwitcher.condition.sceneTransform="Scene item transform"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Get transform"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="On{{scenes}}{{sources}}matches transform"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}}{{getSettings}}"
AdvSceneSwitcher.condition.transition="Transition"
AdvSceneSwitcher.condition.transition.type.current="Current transition type is"
AdvSceneSwitcher.condition.transition.type.duration="Current transition duration is"
@@ -426,7 +433,7 @@ AdvSceneSwitcher.condition.openvr="OpenVR"
AdvSceneSwitcher.condition.openvr.errorStatus="OpenVR error: "
AdvSceneSwitcher.condition.openvr.entry.line1="HMD is in ..."
AdvSceneSwitcher.condition.openvr.entry.line2="{{controls}}"
AdvSceneSwitcher.condition.openvr.entry.line3="HMD is currently at {{xPos}} x {{yPos}} x {{zPos}}"
AdvSceneSwitcher.condition.openvr.entry.line3="HMD is currently at{{xPos}}x{{yPos}}x{{zPos}}"
AdvSceneSwitcher.condition.stats="OBS stats"
AdvSceneSwitcher.condition.stats.type.fps="FPS"
AdvSceneSwitcher.condition.stats.type.CPUUsage="CPU Usage"
@@ -445,15 +452,15 @@ AdvSceneSwitcher.condition.stats.condition.above="above"
AdvSceneSwitcher.condition.stats.condition.equals="equal to"
AdvSceneSwitcher.condition.stats.condition.below="below"
AdvSceneSwitcher.condition.stats.dockHint="You can open the \"Stats\" dock to view the current status"
AdvSceneSwitcher.condition.stats.entry="{{stats}} is {{condition}} {{value}}"
AdvSceneSwitcher.condition.stats.entry="{{stats}}is{{condition}}{{value}}"
AdvSceneSwitcher.condition.profile="Profile"
AdvSceneSwitcher.condition.profile.entry="Current active profile is {{profiles}}"
AdvSceneSwitcher.condition.profile.entry="Current active profile is{{profiles}}"
AdvSceneSwitcher.condition.websocket="Websocket"
AdvSceneSwitcher.condition.websocket.type.request="Scene Switcher Request"
AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}} was received:"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}} was received from {{connection}}:"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
AdvSceneSwitcher.condition.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="equals"
AdvSceneSwitcher.condition.variable.type.empty="is empty"
@@ -466,11 +473,11 @@ AdvSceneSwitcher.condition.variable.type.lessThanVariable="is less than variable
AdvSceneSwitcher.condition.variable.type.greaterThanVariable="is greater than variable"
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
AdvSceneSwitcher.condition.run="Run"
AdvSceneSwitcher.condition.run.entry="Process exits before timeout of{{timeout}} seconds"
AdvSceneSwitcher.condition.run.entry="Process exits before timeout of{{timeout}}seconds"
AdvSceneSwitcher.condition.run.entry.exit="{{checkExitCode}}Check for exit code{{exitCode}}"
AdvSceneSwitcher.condition.midi="MIDI"
AdvSceneSwitcher.condition.midi.entry="Mesasge was received from {{device}} which matches:"
AdvSceneSwitcher.condition.midi.entry.listen="Set MIDI message selection to messages incoming on selected device: {{listenButton}}"
AdvSceneSwitcher.condition.midi.entry="Mesasge was received from{{device}}which matches:"
AdvSceneSwitcher.condition.midi.entry.listen="Set MIDI message selection to messages incoming on selected device:{{listenButton}}"
AdvSceneSwitcher.condition.display="Display"
AdvSceneSwitcher.condition.display.type.displayName="Name of connected displays matches"
AdvSceneSwitcher.condition.display.type.displayCount="Number of connected displays is"
@@ -483,15 +490,18 @@ AdvSceneSwitcher.condition.slideshow.updateIntervalTooltip="Information about th
AdvSceneSwitcher.condition.slideshow.entry="{{sources}}{{conditions}}{{index}}{{path}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="Switch scene"
AdvSceneSwitcher.action.scene.entry="Switch to scene{{scenes}}using{{transitions}}with a duration of{{duration}}seconds"
AdvSceneSwitcher.action.scene.entry.noDuration="Switch to scene{{scenes}}using{{transitions}}"
AdvSceneSwitcher.action.scene="Switch scene"
AdvSceneSwitcher.action.scene.type.program="Program"
AdvSceneSwitcher.action.scene.type.preview="Preview"
AdvSceneSwitcher.action.scene.entry="Switch{{sceneTypes}}scene to{{scenes}}using{{transitions}}with a duration of{{duration}}seconds"
AdvSceneSwitcher.action.scene.entry.noDuration="Switch{{sceneTypes}}scene to{{scenes}}using{{transitions}}"
AdvSceneSwitcher.action.scene.entry.preview="Switch{{sceneTypes}}scene to{{scenes}}"
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Wait until transition to target scene is complete"
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.entry.fixed="Wait for{{waitType}}duration of{{duration}}"
AdvSceneSwitcher.action.wait.entry.random="Wait for{{waitType}}duration from{{duration}}to{{duration2}}"
AdvSceneSwitcher.action.audio="Audio"
AdvSceneSwitcher.action.audio.type.mute="Mute"
AdvSceneSwitcher.action.audio.type.unmute="Unmute"
@@ -503,8 +513,8 @@ AdvSceneSwitcher.action.audio.type.balance="Set balance"
AdvSceneSwitcher.action.audio.balance.description="Move the slider in the direction of the audio channel you want to focus on."
AdvSceneSwitcher.action.audio.fade.type.duration="over a duration of"
AdvSceneSwitcher.action.audio.fade.type.rate="at a rate of"
AdvSceneSwitcher.action.audio.fade.duration="{{fade}}Fade {{fadeTypes}} {{duration}} seconds."
AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Fade {{fadeTypes}} {{rate}}per second."
AdvSceneSwitcher.action.audio.fade.duration="{{fade}}Fade{{fadeTypes}}{{duration}}seconds."
AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Fade{{fadeTypes}}{{rate}}per second."
AdvSceneSwitcher.action.audio.fade.wait="Wait for fade to complete."
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{syncOffset}}{{monitorTypes}}"
@@ -547,7 +557,7 @@ AdvSceneSwitcher.action.filter.type.enable="Enable"
AdvSceneSwitcher.action.filter.type.disable="Disable"
AdvSceneSwitcher.action.filter.type.toggle="Toggle"
AdvSceneSwitcher.action.filter.type.settings="Set settings"
AdvSceneSwitcher.action.filter.entry="On {{sources}} {{actions}} {{filters}}"
AdvSceneSwitcher.action.filter.entry="On{{sources}}{{actions}}{{filters}}"
AdvSceneSwitcher.action.filter.getSettings="Get current settings"
AdvSceneSwitcher.action.source="Source"
AdvSceneSwitcher.action.source.type.enable="Enable"
@@ -558,6 +568,7 @@ AdvSceneSwitcher.action.source.type.pressSettingsButton="Press settings button"
AdvSceneSwitcher.action.source.type.refreshSettings.tooltip="Can be used to refresh browser, media, etc. sources"
AdvSceneSwitcher.action.source.type.deinterlaceMode="Set deinterlace mode"
AdvSceneSwitcher.action.source.type.deinterlaceOrder="Set deinterlace field order"
AdvSceneSwitcher.action.source.type.openInteractionDialog="Open interaction dialog"
AdvSceneSwitcher.action.source.noSettingsButtons="No buttons found!"
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}"
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
@@ -647,7 +658,7 @@ AdvSceneSwitcher.action.sceneTransform.entry="On{{scenes}}{{action}}{{rotation}}
AdvSceneSwitcher.action.file="File"
AdvSceneSwitcher.action.file.type.write="Write"
AdvSceneSwitcher.action.file.type.append="Append"
AdvSceneSwitcher.action.file.entry="{{actions}} to {{filePath}}:"
AdvSceneSwitcher.action.file.entry="{{actions}}to{{filePath}}:"
AdvSceneSwitcher.action.studioMode="Studio mode"
AdvSceneSwitcher.action.studioMode.type.swap="Swap preview and program scene"
AdvSceneSwitcher.action.studioMode.type.setScene="Set preview scene to"
@@ -659,15 +670,15 @@ AdvSceneSwitcher.action.transition.type.scene="scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition"
AdvSceneSwitcher.action.transition.entry.line1="Modify {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to {{duration}}seconds"
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pause"
AdvSceneSwitcher.action.timer.type.continue="Continue"
AdvSceneSwitcher.action.timer.type.reset="Reset"
AdvSceneSwitcher.action.timer.type.setTimeRemaining="Set time remaining of"
AdvSceneSwitcher.action.timer.entry="{{timerAction}} timers on {{macros}} {{duration}}"
AdvSceneSwitcher.action.timer.entry="{{timerAction}}timers on{{macros}}{{duration}}"
AdvSceneSwitcher.action.random="Random"
AdvSceneSwitcher.action.random.allowRepeat="Allow consecutive execution of the same macro"
AdvSceneSwitcher.action.random.entry="Randomly run any of the following macros (paused macros are ignored)"
@@ -685,9 +696,9 @@ AdvSceneSwitcher.action.screenshot.mainOutput="OBS's main output"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Sources or scenes, which are not always rendered, may result in some parts of screenshots to remain blank."
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}and save to{{saveType}}location"
AdvSceneSwitcher.action.profile="Profile"
AdvSceneSwitcher.action.profile.entry="Switch active profile to {{profiles}}"
AdvSceneSwitcher.action.profile.entry="Switch active profile to{{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Scene collection"
AdvSceneSwitcher.action.sceneCollection.entry="Switch active scene collection to {{sceneCollections}}"
AdvSceneSwitcher.action.sceneCollection.entry="Switch active scene collection to{{sceneCollections}}"
AdvSceneSwitcher.action.sceneCollection.warning="Note: Any actions following after this will not be executed as the changing scene collection will also reload the scene switcher settings.\nThe scene collection action will be ignored while the settings window is opened."
AdvSceneSwitcher.action.sequence="Sequence"
AdvSceneSwitcher.action.sequence.entry="Each time this action is performed run the next macro in the list (paused macros are ignored)"
@@ -712,8 +723,8 @@ AdvSceneSwitcher.action.http.headers="Headers:"
AdvSceneSwitcher.action.http.addHeader="Add header"
AdvSceneSwitcher.action.http.type.get="GET"
AdvSceneSwitcher.action.http.type.post="POST"
AdvSceneSwitcher.action.http.entry.line1="Send {{method}} to {{url}}"
AdvSceneSwitcher.action.http.entry.line2="Timeout: {{timeout}} seconds"
AdvSceneSwitcher.action.http.entry.line1="Send{{method}}to{{url}}"
AdvSceneSwitcher.action.http.entry.line2="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.variable="Variable"
AdvSceneSwitcher.action.variable.type.set="Set to fixed value"
AdvSceneSwitcher.action.variable.type.append="Append"
@@ -727,6 +738,8 @@ AdvSceneSwitcher.action.variable.type.subString="Set to substring of current val
AdvSceneSwitcher.action.variable.type.findAndReplace="Find and replace in current value"
AdvSceneSwitcher.action.variable.type.mathExpression="Mathematical expression"
AdvSceneSwitcher.action.variable.type.askForValue="Get user input"
AdvSceneSwitcher.action.variable.type.environmentVariable="Set to environment variable value"
AdvSceneSwitcher.action.variable.type.sceneItemCount="Set to scene item count of scene"
AdvSceneSwitcher.action.variable.askForValuePromptDefault="Assign value to variable \"%1\":"
AdvSceneSwitcher.action.variable.askForValuePrompt="Assign value to variable:"
AdvSceneSwitcher.action.variable.mathExpression.example="( 1 + 2 * 3 ) / 4"
@@ -738,11 +751,12 @@ AdvSceneSwitcher.action.variable.invalidSelection="Invalid selection!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Getting variable values from %1 actions is not supported!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Getting variable values from %1 conditions is not supported!"
AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}} Substring size:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}}Substring size:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
AdvSceneSwitcher.action.variable.entry.findAndReplace="{{findStr}}{{replaceStr}}"
AdvSceneSwitcher.action.variable.entry.userInput="{{useCustomPrompt}}Use custom prompt{{inputPrompt}}"
AdvSceneSwitcher.action.variable.entry.userInput.customPrompt="{{useCustomPrompt}}Use custom prompt{{inputPrompt}}"
AdvSceneSwitcher.action.variable.entry.userInput.placeholder="{{useInputPlaceholder}}Fill with placeholder{{inputPlaceholder}}"
AdvSceneSwitcher.action.projector="Projector"
AdvSceneSwitcher.action.projector.type.source="Source"
AdvSceneSwitcher.action.projector.type.scene="Scene"
@@ -755,247 +769,26 @@ AdvSceneSwitcher.action.projector.fullscreen="Fullscreen"
AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="Send message to {{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on {{listenDevices}}: {{listenButton}}"
AdvSceneSwitcher.action.midi.entry="Send message to{{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on{{listenDevices}}:{{listenButton}}"
AdvSceneSwitcher.action.osc="Open Sound Control"
AdvSceneSwitcher.action.sceneLock="Scene item lock"
AdvSceneSwitcher.action.sceneLock.type.lock="lock"
AdvSceneSwitcher.action.sceneLock.type.unlock="unlock"
AdvSceneSwitcher.action.sceneLock.type.toggle="toggle lock of"
AdvSceneSwitcher.action.sceneLock.entry="On{{scenes}}{{actions}}{{sources}}"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
AdvSceneSwitcher.transitionTab.transitionForAToB="Use transition for automated scene switch from scene A to scene B"
AdvSceneSwitcher.transitionTab.transitionsHelp="<html><head/><body><p>These settings <span style=\"font-style:italic;\">only</span> affect transitions caused by the scene switcher - Check out <a href=\"https://obsproject.com/forum/resources/transition-table.1174/\"><span style=\" text-decoration: underline; color:#268bd2;\">Transition Table</span></a> if you want to configure this for manual scene changes.<br/>Settings defined here take priority over transition settings configured elsewhere in the scene switcher.<br/><br/>Click the plus symbol below to add a new entry.</p></body></html>"
AdvSceneSwitcher.transitionTab.defaultTransition="Change transition if scene is active"
AdvSceneSwitcher.transitionTab.entry="Switch from {{scenes}} to {{scenes2}} using {{transitions}} with a duration of {{duration}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="When scene {{scenes}} is active change default scene transition to {{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionsHelp="Click on the plus symbol to add an entry."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Switch transition {{defTransitionDelay}} after scene change."
AdvSceneSwitcher.transitionTab.defaultTransition.delay.help="The delay is used to avoid cancelled scene switches, which can happen if the transition type is changed while a transition is still ongoing."
; Pause Scenes Tab
AdvSceneSwitcher.pauseTab.title="Pause"
AdvSceneSwitcher.pauseTab.pauseOnScene="Pause the Scene Switcher on scene"
AdvSceneSwitcher.pauseTab.pauseInFocus1="Pause the Scene Switcher when "
AdvSceneSwitcher.pauseTab.pauseInFocus2="is in focus"
AdvSceneSwitcher.pauseTab.pauseTypeScene="scene is active"
AdvSceneSwitcher.pauseTab.pauseTypeWindow="window is in focus"
AdvSceneSwitcher.pauseTab.pauseTargetAll="all"
AdvSceneSwitcher.pauseTab.pauseEntry="Pause {{pauseTargets}} checks when {{pauseTypes}} {{scenes}} {{windows}}"
AdvSceneSwitcher.pauseTab.help="On this tab you can configure to pause individual switching methods if a scene is active or window is in focus.\n\nClick on the highlighted plus symbol to continue."
; Window Title Tab
AdvSceneSwitcher.windowTitleTab.title="Title"
AdvSceneSwitcher.windowTitleTab.regexrDescription="<html><head/><body><p>Enter either direct window titles or valid regex. You can check syntax and matches for regular expressions using <a href=\"https://regexr.com\"><span style=\" text-decoration: underline; color:#268bd2;\">RegExr</span></a></p></body></html>"
AdvSceneSwitcher.windowTitleTab.stayInFocus1="Ignore this window name"
AdvSceneSwitcher.windowTitleTab.stayInFocus2=" "
AdvSceneSwitcher.windowTitleTab.fullscreen="if fullscreen"
AdvSceneSwitcher.windowTitleTab.maximized="if maximized"
AdvSceneSwitcher.windowTitleTab.focused="if focused"
AdvSceneSwitcher.windowTitleTab.entry="{{windows}} {{scenes}} {{transitions}} {{fullscreen}} {{maximized}} {{focused}}"
AdvSceneSwitcher.windowTitleTab.windowsHelp="Switch scenes based on the window title of running applications.\nThe following additional conditions can be selected:\nThe window is Fullscreen\nThe window is maximized\nThe window is focused\n\nClick on the highlighted plus symbol to continue."
AdvSceneSwitcher.windowTitleTab.ignoreWindowsHelp="If a window title is ignored the scene switcher will act as if the previously selected window is still in focus.\nThis will allow you to avoid scene switches, if you frequently switch to a different window, which shall not trigger a scene change.\n\nChoose a window or enter a window title above and click on the plus symbol below to add it to the list."
; Executable Tab
AdvSceneSwitcher.executableTab.title="Executable"
AdvSceneSwitcher.executableTab.implemented="Implemented by dasOven"
AdvSceneSwitcher.executableTab.requiresFocus="only if focused"
AdvSceneSwitcher.executableTab.entry="When {{processes}} is running switch to {{scenes}} using {{transitions}} {{requiresFocus}}"
AdvSceneSwitcher.executableTab.help="This tab will allow you to automatically switch scenes if a process is running.\nThis can be useful in situations where the window name could change or is not known.\n\nClick on the highlighted plus symbol to continue."
; Screen Region Tab
AdvSceneSwitcher.screenRegionTab.title="Region"
AdvSceneSwitcher.screenRegionTab.currentPosition="Cursor is currently at:"
AdvSceneSwitcher.screenRegionTab.showGuideFrames="Show guide frames"
AdvSceneSwitcher.screenRegionTab.hideGuideFrames="Hide guide frames"
AdvSceneSwitcher.screenRegionTab.excludeScenes.None="No selection"
AdvSceneSwitcher.screenRegionTab.entry="If cursor is in {{minX}} {{minY}} x {{maxX}} {{maxY}} switch to {{scenes}} using {{transitions}} unless in {{excludeScenes}}"
AdvSceneSwitcher.screenRegionTab.help="This tab will allow you to automatically switch scenes based on the current position of your mouse cursor.\n\nClick on the highlighted plus symbol to continue."
; Media Tab
AdvSceneSwitcher.mediaTab.title="Media"
AdvSceneSwitcher.mediaTab.implemented="Implemented by Exeldro"
AdvSceneSwitcher.mediaTab.states.none="None"
AdvSceneSwitcher.mediaTab.states.playing="Playing"
AdvSceneSwitcher.mediaTab.states.opening="Opening"
AdvSceneSwitcher.mediaTab.states.buffering="Buffering"
AdvSceneSwitcher.mediaTab.states.paused="Paused"
AdvSceneSwitcher.mediaTab.states.stopped="Stopped"
AdvSceneSwitcher.mediaTab.states.ended="Ended"
AdvSceneSwitcher.mediaTab.states.error="Error"
AdvSceneSwitcher.mediaTab.states.playlistEnd="Ended(Playlist)"
AdvSceneSwitcher.mediaTab.states.any="Any"
AdvSceneSwitcher.mediaTab.timeRestriction.none="None"
AdvSceneSwitcher.mediaTab.timeRestriction.shorter="Time shorter"
AdvSceneSwitcher.mediaTab.timeRestriction.longer="Time longer"
AdvSceneSwitcher.mediaTab.timeRestriction.remainShorter="Time remaining shorter"
AdvSceneSwitcher.mediaTab.timeRestriction.remainLonger="Time remaining longer"
AdvSceneSwitcher.mediaTab.entry="When {{mediaSources}} state is {{states}} and {{timeRestrictions}} {{time}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.mediaTab.help="This tab will allow you to switch scenes based on the states of media sources.\nFor example, you can automatically switch back to the previous scene once the selected media sourced ended its playback.\n\nClick on the highlighted plus symbol to continue."
; File Tab
AdvSceneSwitcher.fileTab.title="File"
AdvSceneSwitcher.fileTab.readWriteSceneFile="Read / write scene from / to file"
AdvSceneSwitcher.fileTab.currentSceneOutputFile="Write the name of the current scene to this file:"
AdvSceneSwitcher.fileTab.switchSceneBaseOnFile="Enable switching of scenes based on file input"
AdvSceneSwitcher.fileTab.switchSceneNameInputFile="Read scene name to be switched to from this file:"
AdvSceneSwitcher.fileTab.switchSceneBaseOnFileContent="Switch scene based on file contents"
AdvSceneSwitcher.fileTab.remoteFileWarning="Please note that if you choose the remote option the scene switcher will try to access the remote location every x ms as specified on the General tab!"
AdvSceneSwitcher.fileTab.remoteFileWarning1="Note that the scene switcher will try to access the remote location every "
AdvSceneSwitcher.fileTab.remoteFileWarning2="ms"
AdvSceneSwitcher.fileTab.libcurlWarning="Failed to load libcurl! Accessing remote files will not be possible!"
AdvSceneSwitcher.fileTab.selectWrite="Select a file to write to ..."
AdvSceneSwitcher.fileTab.selectRead="Select a file to read from ..."
AdvSceneSwitcher.fileTab.textFileType="Text files (*.txt)"
AdvSceneSwitcher.fileTab.anyFileType="Any files (*.*)"
AdvSceneSwitcher.fileTab.remote="remote file"
AdvSceneSwitcher.fileTab.local="local file"
AdvSceneSwitcher.fileTab.useRegExp="use regular expressions (pattern matching)"
AdvSceneSwitcher.fileTab.checkfileContentTime="if modification date changed"
AdvSceneSwitcher.fileTab.checkfileContent="if content changed"
AdvSceneSwitcher.fileTab.entry="Switch to {{scenes}} using {{transitions}} if content of {{fileType}} {{filePath}} {{browseButton}} matches:"
AdvSceneSwitcher.fileTab.entry2="{{matchText}}"
AdvSceneSwitcher.fileTab.entry3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.fileTab.help="This tab will allow you to automatically switch scenes based on the content of remote or local files.\n\nClick on the highlighted plus symbol to continue."
; Random Tab
AdvSceneSwitcher.randomTab.title="Random"
AdvSceneSwitcher.randomTab.randomDisabledWarning="Functionality disabled - To activate select \"If no switch condition is met switch to any scene in Random tab\" on General tab"
AdvSceneSwitcher.randomTab.entry="If no switch condition is met switch to {{scenes}} using {{transitions}} for {{delay}}"
AdvSceneSwitcher.randomTab.help="The scene switcher will randomly choose an entry on this tab to switch to for the configured time.\nNote that the same entry will not be chosen twice in a row.\n\nClick on the highlighted plus symbol to continue."
; Time Tab
AdvSceneSwitcher.timeTab.title="Time"
AdvSceneSwitcher.timeTab.anyDay="On any day"
AdvSceneSwitcher.timeTab.mondays="Mondays"
AdvSceneSwitcher.timeTab.tuesdays="Tuesdays"
AdvSceneSwitcher.timeTab.wednesdays="Wednesdays"
AdvSceneSwitcher.timeTab.thursdays="Thursdays"
AdvSceneSwitcher.timeTab.fridays="Fridays"
AdvSceneSwitcher.timeTab.saturdays="Saturdays"
AdvSceneSwitcher.timeTab.sundays="Sundays"
AdvSceneSwitcher.timeTab.afterstart="After streaming/recording start"
AdvSceneSwitcher.timeTab.afterstart.tip="The time relative to the start of streaming / recording will be used"
AdvSceneSwitcher.timeTab.entry="{{triggers}} at {{time}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.timeTab.help="This tab will allow you to automatically switch to a different scene based on the current local time.\n\nNote that the scene switcher will only switch scenes at the exact time you specified.\nMake sure you have configured the priority settings on the General tab to your liking so the selected time point will not be missed due to other switching methods having a higher priority.\n\nClick on the highlighted plus symbol to continue."
; Idle Tab
AdvSceneSwitcher.idleTab.title="Idle"
AdvSceneSwitcher.idleTab.enable="Enable Idle Detection"
AdvSceneSwitcher.idleTab.idleswitch="After {{duration}} of no keyboard or mouse inputs switch to scene {{scenes}} using the {{transitions}}"
AdvSceneSwitcher.idleTab.dontSwitchIfFocus1="Do not switch if"
AdvSceneSwitcher.idleTab.dontSwitchIfFocus2="is in focus"
; Scene Sequence Tab
AdvSceneSwitcher.sceneSequenceTab.title="Sequence"
AdvSceneSwitcher.sceneSequenceTab.description="A sequence of automatic scene switches can be cancelled by either pausing/stopping the scene switcher or manually switching to a different scene"
AdvSceneSwitcher.sceneSequenceTab.save="Save scene sequences to file"
AdvSceneSwitcher.sceneSequenceTab.load="Load scene sequences from file"
AdvSceneSwitcher.sceneSequenceTab.saveTitle="Save Scene Sequence to file ..."
AdvSceneSwitcher.sceneSequenceTab.loadTitle="Select a file to read Scene Sequence from ..."
AdvSceneSwitcher.sceneSequenceTab.loadFail="Advanced Scene Switcher failed to import settings!"
AdvSceneSwitcher.sceneSequenceTab.loadSuccess="Advanced Scene Switcher settings imported successfully!"
AdvSceneSwitcher.sceneSequenceTab.fileType="Text files (*.txt)"
AdvSceneSwitcher.sceneSequenceTab.interruptible="interruptible"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Other switching methods are allowed to interrupt this scene sequence"
AdvSceneSwitcher.sceneSequenceTab.entry="When {{startScenes}} is active switch to {{scenes}} after {{delay}} using {{transitions}} {{interruptible}}"
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Extend Sequence"
AdvSceneSwitcher.sceneSequenceTab.extendEntry="After {{delay}} switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.sceneSequenceTab.help="This tab will allow you to automatically switch to a different scene if a scene was active for a configured period of time.\nFor example, you could automatically cycle back and forth between two scenes automatically.\n\nClick on the highlighted plus symbol to continue."
; Audio Tab
AdvSceneSwitcher.audioTab.title="Audio"
AdvSceneSwitcher.audioTab.condition.above="above"
AdvSceneSwitcher.audioTab.condition.below="below"
AdvSceneSwitcher.audioTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.audioTab.entry="When the volume of {{audioSources}} is {{condition}} {{volumeWidget}} for {{duration}} seconds switch to {{scenes}} using {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.audioTab.multiMatchfallbackCondition="If multiple entries match ..."
AdvSceneSwitcher.audioTab.multiMatchfallback="... for {{duration}} seconds switch to {{scenes}} using {{transitions}}"
AdvSceneSwitcher.audioTab.help="This tab will allow you to switch scenes based on the volume of sources.\nFor example, you could automatically switch to a different scene if the volume of your microphone reaches a certain threshold.\n\nClick on the highlighted plus symbol to continue."
; Video Tab
AdvSceneSwitcher.videoTab.title="Video"
AdvSceneSwitcher.videoTab.getScreenshot="Get screenshot for selected entry"
AdvSceneSwitcher.videoTab.getScreenshotHelp="Get Screenshot of the currently selected entry's video source and automatically set it as the target image"
AdvSceneSwitcher.videoTab.condition.match="exactly matches"
AdvSceneSwitcher.videoTab.condition.match.tooltip="An exact match requires the target and the source image to be of the same resolution.\nAdditionally every single pixel needs to match, which is why use of image formats which use compression (e.g. .JPG) is not recommended!"
AdvSceneSwitcher.videoTab.condition.differ="does not match"
AdvSceneSwitcher.videoTab.condition.hasNotChanged="has not changed"
AdvSceneSwitcher.videoTab.condition.hasChanged="has changed"
AdvSceneSwitcher.videoTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.videoTab.entry="When {{videoSources}} {{condition}} {{filePath}} {{browseButton}} for {{duration}} switch to {{scenes}} using {{transitions}} {{ignoreInactiveSource}}"
AdvSceneSwitcher.videoTab.help="<html><head/><body><p>This tab will allow you to switch scenes based on the current video output of selected sources.<br/>Make sure to check out <a href=\"https://obsproject.com/forum/resources/pixel-match-switcher.1202/\"><span style=\" text-decoration: underline; color:#268bd2;\">Pixel Match Switcher</span></a> for an even better implementation of this functionality.<br/><br/> Click on the highlighted plus symbol to continue.</p></body></html>"
; Network Tab
AdvSceneSwitcher.networkTab.title="Network"
AdvSceneSwitcher.networkTab.description="This tab will allow you to remotely control the active scene of another OBS instance.\nPlease note that the scene names have to match exactly on all OBS instances."
AdvSceneSwitcher.networkTab.warning="Running the server outside of a local network will allow third parties to read the active scene."
AdvSceneSwitcher.networkTab.server="Start server (Sends scene switch messages to all connected clients)"
AdvSceneSwitcher.networkTab.server.port="Port"
AdvSceneSwitcher.networkTab.server.lockToIPv4="Lock server to only using IPv4"
AdvSceneSwitcher.networkTab.server.sendSceneChange="Send messages for scene changes"
AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches="Only send messages for automated scene switches"
AdvSceneSwitcher.networkTab.server.sendPreview="Send messages for preview scene change when running in Studio mode"
AdvSceneSwitcher.networkTab.startFailed.message="The WebSockets server failed to start, maybe because:\n - TCP port %1 may currently be in use elsewhere on this system, possibly by another application. Try setting a different TCP port in the WebSocket server settings, or stop any application that could be using this port.\n - Error message: %2"
AdvSceneSwitcher.networkTab.server.status.currentStatus="Current status"
AdvSceneSwitcher.networkTab.server.status.notRunning="Not running"
AdvSceneSwitcher.networkTab.server.status.starting="Starting"
AdvSceneSwitcher.networkTab.server.status.running="Running"
AdvSceneSwitcher.networkTab.server.restart="Restart server"
AdvSceneSwitcher.networkTab.client="Start client (Receives scene switches messages)"
AdvSceneSwitcher.networkTab.client.address="Hostname or IP address"
AdvSceneSwitcher.networkTab.client.port="Port"
AdvSceneSwitcher.networkTab.client.status.currentStatus="Current status"
AdvSceneSwitcher.networkTab.client.status.disconnected="Disconnected"
AdvSceneSwitcher.networkTab.client.status.connecting="Connecting"
AdvSceneSwitcher.networkTab.client.status.connected="Connected"
AdvSceneSwitcher.networkTab.client.reconnect="Force reconnect"
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Scene Group"
AdvSceneSwitcher.sceneGroupTab.list="Scene Groups"
AdvSceneSwitcher.sceneGroupTab.edit="Edit Scene Groups"
AdvSceneSwitcher.sceneGroupTab.edit.name="Name:"
AdvSceneSwitcher.sceneGroupTab.edit.type="Type: {{type}}"
AdvSceneSwitcher.sceneGroupTab.type.count="Count"
AdvSceneSwitcher.sceneGroupTab.type.time="Time"
AdvSceneSwitcher.sceneGroupTab.type.random="Random"
AdvSceneSwitcher.sceneGroupTab.edit.count="Advance to next scene in list after {{count}} matches"
AdvSceneSwitcher.sceneGroupTab.edit.time="Advance to next scene in list after {{time}} has passed"
AdvSceneSwitcher.sceneGroupTab.edit.random="Choose next scene in list at random"
AdvSceneSwitcher.sceneGroupTab.edit.repeat="Start from beginning if end of scene list is reached"
AdvSceneSwitcher.sceneGroupTab.edit.addScene="Add scene"
AdvSceneSwitcher.sceneGroupTab.add="Add Scene Group"
AdvSceneSwitcher.sceneGroupTab.defaultname="Scene Group %1"
AdvSceneSwitcher.sceneGroupTab.exists="Scene Group or Scene name exists already"
AdvSceneSwitcher.sceneGroupTab.help="Scene Groups can be selected as a target just like a regular scene.\n\nAs the name suggests a scene group is a collection of multiple scenes.\nThe scene group will advance through the list of its assigned scenes depending on the configured settings, which can be found on the right side.\n\nYou can configure the scene group to advance to the next scene in the list:\nAfter a number of times the scene group is selected as a target.\nAfter a certain amount of time has passed.\nOr randomly.\n\nFor example, a scene group containing the scenes ...\nScene 1\nScene 2\nScene 3 \n... will activate \"Scene 1\" the first time it is selected as a target.\nThe second time it will activate \"Scene 2\".\nThe remaining times \"Scene 3\" will be activated.\n\nClick the highlighted plus symbol below to add a new scene group."
AdvSceneSwitcher.sceneGroupTab.scenes.help="Select the scene group you want to modify on the left.\n\nSelect a scene to add to this scene group by selecting the scene above and clicking the plus symbol below.\n\nA scene can be added multiple times to the same scene group."
; Scene Trigger Tab
AdvSceneSwitcher.sceneTriggerTab.title="Scene Triggers"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.none="--select trigger--"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneActive="is active"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneInactive="is not active"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneLeave="switched away from"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.none="--select action--"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startRecording="start recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.pauseRecording="pause recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unpauseRecording="unpause recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopRecording="stop recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopStreaming="stop streaming"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startStreaming="start streaming"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startReplayBuffer="start replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopReplayBuffer="stop replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource="mute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unmuteSource="unmute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startSwitcher="start the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopSwitcher="stop the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startVirtualCamera="start virtual camera"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopVirtualCamera="stop virtual camera"
AdvSceneSwitcher.sceneTriggerTab.entry="When {{scenes}} {{triggers}} {{actions}} {{audioSources}} after {{duration}}"
AdvSceneSwitcher.sceneTriggerTab.help="This tab allows you to trigger actions on scene changes, like stopping recording or streaming."
AdvSceneSwitcher.action.twitch="Twitch"
AdvSceneSwitcher.action.twitch.type.title="Set stream title"
AdvSceneSwitcher.action.twitch.type.category="Set stream category"
AdvSceneSwitcher.action.twitch.type.marker="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip="Create stream clip"
AdvSceneSwitcher.action.twitch.type.commercial="Start commercial with duration"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{streamTitle}}{{category}}{{manualCategorySearch}}{{markerDescription}}{{clipHasDelay}}{{duration}}"
AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
; Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@@ -1009,7 +802,7 @@ AdvSceneSwitcher.hotkey.downMacroSegmentHotkey="Move macro segment selection dow
AdvSceneSwitcher.hotkey.removeMacroSegmentHotkey="Remove selected macro segment"
AdvSceneSwitcher.askBackup="Detected a new version of the Advanced Scene Switcher.\nShould a backup of the old settings be created?"
AdvSceneSwitcher.askForMacro="Select macro {{macroSelection}}"
AdvSceneSwitcher.askForMacro="Select macro{{macroSelection}}"
AdvSceneSwitcher.close="Close"
AdvSceneSwitcher.browse="Browse"
@@ -1029,6 +822,7 @@ AdvSceneSwitcher.macroSegmentSelection.invalid="Invalid selection!"
AdvSceneSwitcher.variable.select="--select variable--"
AdvSceneSwitcher.variable.add="Add new variable"
AdvSceneSwitcher.variable.configure="Configure variable settings"
AdvSceneSwitcher.variable.invalid="Invalid varialbe selection"
AdvSceneSwitcher.variable.name="Name:"
AdvSceneSwitcher.variable.value="Current value:"
AdvSceneSwitcher.variable.save="Save / load behavior"
@@ -1039,6 +833,7 @@ AdvSceneSwitcher.variable.save.default="Set to value"
AdvSceneSwitcher.connection.select="--select connection--"
AdvSceneSwitcher.connection.add="Add new connection"
AdvSceneSwitcher.connection.configure="Configure connection settings"
AdvSceneSwitcher.connection.invalid="Invalid connection selection"
AdvSceneSwitcher.connection.name="Name:"
AdvSceneSwitcher.connection.useCustomURI="Use custom URI"
AdvSceneSwitcher.connection.customURI="Address:"
@@ -1125,6 +920,41 @@ AdvSceneSwitcher.osc.message.type.false="False"
AdvSceneSwitcher.osc.message.type.infinity="Infinitum"
AdvSceneSwitcher.osc.message.type.null="Nil"
AdvSceneSwitcher.twitchToken.name="Account name:"
AdvSceneSwitcher.twitchToken.nameNotAvailable="Account already in use"
AdvSceneSwitcher.twitchToken.select="--select Twitch connection--"
AdvSceneSwitcher.twitchToken.add="Add new connection"
AdvSceneSwitcher.twitchToken.configure="Configure Twitch connection settings"
AdvSceneSwitcher.twitchToken.value="Token:"
AdvSceneSwitcher.twitchToken.invalid="Invalid twitch token"
AdvSceneSwitcher.twitchToken.request="Request token"
AdvSceneSwitcher.twitchToken.request.waiting="Waiting for token approval ..."
AdvSceneSwitcher.twitchToken.request.fail="Failed to get token!"
AdvSceneSwitcher.twitchToken.request.fail.browser="Authentication failed! (%1)\nYou can close this window now."
AdvSceneSwitcher.twitchToken.request.fail.stateMismatch="State mismatch"
AdvSceneSwitcher.twitchToken.request.success="Successfully received token!"
AdvSceneSwitcher.twitchToken.request.success.browser="Authentication successful! You can close this window now."
AdvSceneSwitcher.twitchToken.request.notSet="No token set - Please request new token!"
AdvSceneSwitcher.twitchToken.permissions="Token permissions:"
AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for the Twitch Extensions owned by the authenticated account."
AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account."
AdvSceneSwitcher.twitchToken.bits.read="View Bits information for a channel."
AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Manage a channels broadcast configuration, including updating channel configuration and managing stream markers and stream tags."
AdvSceneSwitcher.twitchToken.channel.startCommercial="Run commercials on a channel."
AdvSceneSwitcher.twitchToken.channel.createClip="Create clips from channel's broadcasts."
AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..."
AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories."
AdvSceneSwitcher.twitchCategories.fetchSkip="Skip fetching more stream categories"
AdvSceneSwitcher.twitchCategories.fetchStop="Stop"
AdvSceneSwitcher.twitchCategories.search="Search for stream category ..."
AdvSceneSwitcher.twitchCategories.name="Category name:"
AdvSceneSwitcher.twitchCategories.manualSearch="Search for additional category and it add to the selection list"
AdvSceneSwitcher.twitchCategories.noViewersCategoriesMissing="Categories without any viewers will have to be searched for manually"
AdvSceneSwitcher.twitchCategories.searchFailed="No new categories were found for \"%1\"."
AdvSceneSwitcher.twitchCategories.searchSuccess="%1 new categories were found for \"%2\" and were added to the list!"
AdvSceneSwitcher.twitchCategories.select="--select category--"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
@@ -1149,6 +979,7 @@ AdvSceneSwitcher.enterPath="--enter path--"
AdvSceneSwitcher.enterText="--enter text--"
AdvSceneSwitcher.enterURL="--enter URL--"
AdvSceneSwitcher.selectHotkey="--select hotkey--"
AdvSceneSwitcher.selectDisplay="--select display--"
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
AdvSceneSwitcher.selectWindowTip="Use \"OBS\" to specify OBS window\nUse \"Task Switching\"to specify ALT + TAB"
@@ -1192,3 +1023,237 @@ AdvSceneSwitcher.duration.condition.within="Within the last"
AdvSceneSwitcher.audio.monitor.none="Monitor Off"
AdvSceneSwitcher.audio.monitor.monitorOnly="Monitor Only (mute output)"
AdvSceneSwitcher.audio.monitor.both="Monitor and Output"
; Legacy tabs below - please don't waste your time adding translations for these :)
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
AdvSceneSwitcher.transitionTab.transitionForAToB="Use transition for automated scene switch from scene A to scene B"
AdvSceneSwitcher.transitionTab.transitionsHelp="<html><head/><body><p>These settings <span style=\"font-style:italic;\">only</span> affect transitions caused by the scene switcher - Check out <a href=\"https://obsproject.com/forum/resources/transition-table.1174/\"><span style=\" text-decoration: underline; color:#268bd2;\">Transition Table</span></a> if you want to configure this for manual scene changes.<br/>Settings defined here take priority over transition settings configured elsewhere in the scene switcher.<br/><br/>Click the plus symbol below to add a new entry.</p></body></html>"
AdvSceneSwitcher.transitionTab.defaultTransition="Change transition if scene is active"
AdvSceneSwitcher.transitionTab.entry="Switch from{{scenes}}to{{scenes2}}using{{transitions}}with a duration of{{duration}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="When scene{{scenes}}is active change default scene transition to{{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionsHelp="Click on the plus symbol to add an entry."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Switch transition{{defTransitionDelay}}after scene change."
AdvSceneSwitcher.transitionTab.defaultTransition.delay.help="The delay is used to avoid cancelled scene switches, which can happen if the transition type is changed while a transition is still ongoing."
; Pause Scenes Tab
AdvSceneSwitcher.pauseTab.title="Pause"
AdvSceneSwitcher.pauseTab.pauseOnScene="Pause the Scene Switcher on scene"
AdvSceneSwitcher.pauseTab.pauseInFocus1="Pause the Scene Switcher when "
AdvSceneSwitcher.pauseTab.pauseInFocus2="is in focus"
AdvSceneSwitcher.pauseTab.pauseTypeScene="scene is active"
AdvSceneSwitcher.pauseTab.pauseTypeWindow="window is in focus"
AdvSceneSwitcher.pauseTab.pauseTargetAll="all"
AdvSceneSwitcher.pauseTab.pauseEntry="Pause{{pauseTargets}}checks when{{pauseTypes}}{{scenes}}{{windows}}"
AdvSceneSwitcher.pauseTab.help="On this tab you can configure to pause individual switching methods if a scene is active or window is in focus.\n\nClick on the highlighted plus symbol to continue."
; Window Title Tab
AdvSceneSwitcher.windowTitleTab.title="Title"
AdvSceneSwitcher.windowTitleTab.regexrDescription="<html><head/><body><p>Enter either direct window titles or valid regex. You can check syntax and matches for regular expressions using <a href=\"https://regexr.com\"><span style=\" text-decoration: underline; color:#268bd2;\">RegExr</span></a></p></body></html>"
AdvSceneSwitcher.windowTitleTab.stayInFocus1="Ignore this window name"
AdvSceneSwitcher.windowTitleTab.stayInFocus2=" "
AdvSceneSwitcher.windowTitleTab.fullscreen="if fullscreen"
AdvSceneSwitcher.windowTitleTab.maximized="if maximized"
AdvSceneSwitcher.windowTitleTab.focused="if focused"
AdvSceneSwitcher.windowTitleTab.entry="{{windows}}{{scenes}}{{transitions}}{{fullscreen}}{{maximized}}{{focused}}"
AdvSceneSwitcher.windowTitleTab.windowsHelp="Switch scenes based on the window title of running applications.\nThe following additional conditions can be selected:\nThe window is Fullscreen\nThe window is maximized\nThe window is focused\n\nClick on the highlighted plus symbol to continue."
AdvSceneSwitcher.windowTitleTab.ignoreWindowsHelp="If a window title is ignored the scene switcher will act as if the previously selected window is still in focus.\nThis will allow you to avoid scene switches, if you frequently switch to a different window, which shall not trigger a scene change.\n\nChoose a window or enter a window title above and click on the plus symbol below to add it to the list."
; Executable Tab
AdvSceneSwitcher.executableTab.title="Executable"
AdvSceneSwitcher.executableTab.implemented="Implemented by dasOven"
AdvSceneSwitcher.executableTab.requiresFocus="only if focused"
AdvSceneSwitcher.executableTab.entry="When{{processes}}is running switch to{{scenes}}using{{transitions}}{{requiresFocus}}"
AdvSceneSwitcher.executableTab.help="This tab will allow you to automatically switch scenes if a process is running.\nThis can be useful in situations where the window name could change or is not known.\n\nClick on the highlighted plus symbol to continue."
; Screen Region Tab
AdvSceneSwitcher.screenRegionTab.title="Region"
AdvSceneSwitcher.screenRegionTab.currentPosition="Cursor is currently at:"
AdvSceneSwitcher.screenRegionTab.showGuideFrames="Show guide frames"
AdvSceneSwitcher.screenRegionTab.hideGuideFrames="Hide guide frames"
AdvSceneSwitcher.screenRegionTab.excludeScenes.None="No selection"
AdvSceneSwitcher.screenRegionTab.entry="If cursor is in{{minX}}{{minY}}x{{maxX}}{{maxY}}switch to{{scenes}}using{{transitions}}unless in{{excludeScenes}}"
AdvSceneSwitcher.screenRegionTab.help="This tab will allow you to automatically switch scenes based on the current position of your mouse cursor.\n\nClick on the highlighted plus symbol to continue."
; Media Tab
AdvSceneSwitcher.mediaTab.title="Media"
AdvSceneSwitcher.mediaTab.implemented="Implemented by Exeldro"
AdvSceneSwitcher.mediaTab.states.none="None"
AdvSceneSwitcher.mediaTab.states.playing="Playing"
AdvSceneSwitcher.mediaTab.states.opening="Opening"
AdvSceneSwitcher.mediaTab.states.buffering="Buffering"
AdvSceneSwitcher.mediaTab.states.paused="Paused"
AdvSceneSwitcher.mediaTab.states.stopped="Stopped"
AdvSceneSwitcher.mediaTab.states.ended="Ended"
AdvSceneSwitcher.mediaTab.states.error="Error"
AdvSceneSwitcher.mediaTab.states.playlistEnd="Ended(Playlist)"
AdvSceneSwitcher.mediaTab.states.any="Any"
AdvSceneSwitcher.mediaTab.timeRestriction.none="None"
AdvSceneSwitcher.mediaTab.timeRestriction.shorter="Time shorter"
AdvSceneSwitcher.mediaTab.timeRestriction.longer="Time longer"
AdvSceneSwitcher.mediaTab.timeRestriction.remainShorter="Time remaining shorter"
AdvSceneSwitcher.mediaTab.timeRestriction.remainLonger="Time remaining longer"
AdvSceneSwitcher.mediaTab.entry="When{{mediaSources}}state is{{states}}and{{timeRestrictions}}{{time}}switch to{{scenes}}using{{transitions}}"
AdvSceneSwitcher.mediaTab.help="This tab will allow you to switch scenes based on the states of media sources.\nFor example, you can automatically switch back to the previous scene once the selected media sourced ended its playback.\n\nClick on the highlighted plus symbol to continue."
; File Tab
AdvSceneSwitcher.fileTab.title="File"
AdvSceneSwitcher.fileTab.readWriteSceneFile="Read / write scene from / to file"
AdvSceneSwitcher.fileTab.currentSceneOutputFile="Write the name of the current scene to this file:"
AdvSceneSwitcher.fileTab.switchSceneBaseOnFile="Enable switching of scenes based on file input"
AdvSceneSwitcher.fileTab.switchSceneNameInputFile="Read scene name to be switched to from this file:"
AdvSceneSwitcher.fileTab.switchSceneBaseOnFileContent="Switch scene based on file contents"
AdvSceneSwitcher.fileTab.remoteFileWarning="Please note that if you choose the remote option the scene switcher will try to access the remote location every x ms as specified on the General tab!"
AdvSceneSwitcher.fileTab.remoteFileWarning1="Note that the scene switcher will try to access the remote location every "
AdvSceneSwitcher.fileTab.remoteFileWarning2="ms"
AdvSceneSwitcher.fileTab.libcurlWarning="Failed to load libcurl! Accessing remote files will not be possible!"
AdvSceneSwitcher.fileTab.selectWrite="Select a file to write to ..."
AdvSceneSwitcher.fileTab.selectRead="Select a file to read from ..."
AdvSceneSwitcher.fileTab.textFileType="Text files (*.txt)"
AdvSceneSwitcher.fileTab.anyFileType="Any files (*.*)"
AdvSceneSwitcher.fileTab.remote="remote file"
AdvSceneSwitcher.fileTab.local="local file"
AdvSceneSwitcher.fileTab.useRegExp="use regular expressions (pattern matching)"
AdvSceneSwitcher.fileTab.checkfileContentTime="if modification date changed"
AdvSceneSwitcher.fileTab.checkfileContent="if content changed"
AdvSceneSwitcher.fileTab.entry="Switch to{{scenes}}using{{transitions}}if content of{{fileType}}{{filePath}}{{browseButton}}matches:"
AdvSceneSwitcher.fileTab.entry2="{{matchText}}"
AdvSceneSwitcher.fileTab.entry3="{{useRegex}}{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.fileTab.help="This tab will allow you to automatically switch scenes based on the content of remote or local files.\n\nClick on the highlighted plus symbol to continue."
; Random Tab
AdvSceneSwitcher.randomTab.title="Random"
AdvSceneSwitcher.randomTab.randomDisabledWarning="Functionality disabled - To activate select \"If no switch condition is met switch to any scene in Random tab\" on General tab"
AdvSceneSwitcher.randomTab.entry="If no switch condition is met switch to{{scenes}}using{{transitions}}for{{delay}}"
AdvSceneSwitcher.randomTab.help="The scene switcher will randomly choose an entry on this tab to switch to for the configured time.\nNote that the same entry will not be chosen twice in a row.\n\nClick on the highlighted plus symbol to continue."
; Time Tab
AdvSceneSwitcher.timeTab.title="Time"
AdvSceneSwitcher.timeTab.anyDay="On any day"
AdvSceneSwitcher.timeTab.mondays="Mondays"
AdvSceneSwitcher.timeTab.tuesdays="Tuesdays"
AdvSceneSwitcher.timeTab.wednesdays="Wednesdays"
AdvSceneSwitcher.timeTab.thursdays="Thursdays"
AdvSceneSwitcher.timeTab.fridays="Fridays"
AdvSceneSwitcher.timeTab.saturdays="Saturdays"
AdvSceneSwitcher.timeTab.sundays="Sundays"
AdvSceneSwitcher.timeTab.afterstart="After streaming/recording start"
AdvSceneSwitcher.timeTab.afterstart.tip="The time relative to the start of streaming / recording will be used"
AdvSceneSwitcher.timeTab.entry="{{triggers}}at{{time}}switch to{{scenes}}using{{transitions}}"
AdvSceneSwitcher.timeTab.help="This tab will allow you to automatically switch to a different scene based on the current local time.\n\nNote that the scene switcher will only switch scenes at the exact time you specified.\nMake sure you have configured the priority settings on the General tab to your liking so the selected time point will not be missed due to other switching methods having a higher priority.\n\nClick on the highlighted plus symbol to continue."
; Idle Tab
AdvSceneSwitcher.idleTab.title="Idle"
AdvSceneSwitcher.idleTab.enable="Enable Idle Detection"
AdvSceneSwitcher.idleTab.idleswitch="After{{duration}}of no keyboard or mouse inputs switch to scene{{scenes}}using the{{transitions}}"
AdvSceneSwitcher.idleTab.dontSwitchIfFocus1="Do not switch if"
AdvSceneSwitcher.idleTab.dontSwitchIfFocus2="is in focus"
; Scene Sequence Tab
AdvSceneSwitcher.sceneSequenceTab.title="Sequence"
AdvSceneSwitcher.sceneSequenceTab.description="A sequence of automatic scene switches can be cancelled by either pausing/stopping the scene switcher or manually switching to a different scene"
AdvSceneSwitcher.sceneSequenceTab.save="Save scene sequences to file"
AdvSceneSwitcher.sceneSequenceTab.load="Load scene sequences from file"
AdvSceneSwitcher.sceneSequenceTab.saveTitle="Save Scene Sequence to file ..."
AdvSceneSwitcher.sceneSequenceTab.loadTitle="Select a file to read Scene Sequence from ..."
AdvSceneSwitcher.sceneSequenceTab.loadFail="Advanced Scene Switcher failed to import settings!"
AdvSceneSwitcher.sceneSequenceTab.loadSuccess="Advanced Scene Switcher settings imported successfully!"
AdvSceneSwitcher.sceneSequenceTab.fileType="Text files (*.txt)"
AdvSceneSwitcher.sceneSequenceTab.interruptible="interruptible"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Other switching methods are allowed to interrupt this scene sequence"
AdvSceneSwitcher.sceneSequenceTab.entry="When{{startScenes}}is active switch to{{scenes}}after{{delay}}using{{transitions}}{{interruptible}}"
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Extend Sequence"
AdvSceneSwitcher.sceneSequenceTab.extendEntry="After{{delay}}switch to{{scenes}}using{{transitions}}"
AdvSceneSwitcher.sceneSequenceTab.help="This tab will allow you to automatically switch to a different scene if a scene was active for a configured period of time.\nFor example, you could automatically cycle back and forth between two scenes automatically.\n\nClick on the highlighted plus symbol to continue."
; Audio Tab
AdvSceneSwitcher.audioTab.title="Audio"
AdvSceneSwitcher.audioTab.condition.above="above"
AdvSceneSwitcher.audioTab.condition.below="below"
AdvSceneSwitcher.audioTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.audioTab.entry="When the volume of{{audioSources}}is{{condition}}{{volumeWidget}}for{{duration}}seconds switch to{{scenes}}using{{transitions}}{{ignoreInactiveSource}}"
AdvSceneSwitcher.audioTab.multiMatchfallbackCondition="If multiple entries match ..."
AdvSceneSwitcher.audioTab.multiMatchfallback="... for{{duration}}seconds switch to{{scenes}}using{{transitions}}"
AdvSceneSwitcher.audioTab.help="This tab will allow you to switch scenes based on the volume of sources.\nFor example, you could automatically switch to a different scene if the volume of your microphone reaches a certain threshold.\n\nClick on the highlighted plus symbol to continue."
; Video Tab
AdvSceneSwitcher.videoTab.title="Video"
AdvSceneSwitcher.videoTab.getScreenshot="Get screenshot for selected entry"
AdvSceneSwitcher.videoTab.getScreenshotHelp="Get Screenshot of the currently selected entry's video source and automatically set it as the target image"
AdvSceneSwitcher.videoTab.condition.match="exactly matches"
AdvSceneSwitcher.videoTab.condition.match.tooltip="An exact match requires the target and the source image to be of the same resolution.\nAdditionally every single pixel needs to match, which is why use of image formats which use compression (e.g. .JPG) is not recommended!"
AdvSceneSwitcher.videoTab.condition.differ="does not match"
AdvSceneSwitcher.videoTab.condition.hasNotChanged="has not changed"
AdvSceneSwitcher.videoTab.condition.hasChanged="has changed"
AdvSceneSwitcher.videoTab.ignoreInactiveSource="unless source is inactive"
AdvSceneSwitcher.videoTab.entry="When{{videoSources}}{{condition}}{{filePath}}{{browseButton}}for{{duration}}switch to{{scenes}}using{{transitions}}{{ignoreInactiveSource}}"
AdvSceneSwitcher.videoTab.help="<html><head/><body><p>This tab will allow you to switch scenes based on the current video output of selected sources.<br/>Make sure to check out <a href=\"https://obsproject.com/forum/resources/pixel-match-switcher.1202/\"><span style=\" text-decoration: underline; color:#268bd2;\">Pixel Match Switcher</span></a> for an even better implementation of this functionality.<br/><br/> Click on the highlighted plus symbol to continue.</p></body></html>"
; Network Tab
AdvSceneSwitcher.networkTab.title="Network"
AdvSceneSwitcher.networkTab.description="This tab will allow you to remotely control the active scene of another OBS instance.\nPlease note that the scene names have to match exactly on all OBS instances."
AdvSceneSwitcher.networkTab.warning="Running the server outside of a local network will allow third parties to read the active scene."
AdvSceneSwitcher.networkTab.server="Start server (Sends scene switch messages to all connected clients)"
AdvSceneSwitcher.networkTab.server.port="Port"
AdvSceneSwitcher.networkTab.server.lockToIPv4="Lock server to only using IPv4"
AdvSceneSwitcher.networkTab.server.sendSceneChange="Send messages for scene changes"
AdvSceneSwitcher.networkTab.server.restrictSendToAutomatedSwitches="Only send messages for automated scene switches"
AdvSceneSwitcher.networkTab.server.sendPreview="Send messages for preview scene change when running in Studio mode"
AdvSceneSwitcher.networkTab.startFailed.message="The WebSockets server failed to start, maybe because:\n - TCP port %1 may currently be in use elsewhere on this system, possibly by another application. Try setting a different TCP port in the WebSocket server settings, or stop any application that could be using this port.\n - Error message: %2"
AdvSceneSwitcher.networkTab.server.status.currentStatus="Current status"
AdvSceneSwitcher.networkTab.server.status.notRunning="Not running"
AdvSceneSwitcher.networkTab.server.status.starting="Starting"
AdvSceneSwitcher.networkTab.server.status.running="Running"
AdvSceneSwitcher.networkTab.server.restart="Restart server"
AdvSceneSwitcher.networkTab.client="Start client (Receives scene switches messages)"
AdvSceneSwitcher.networkTab.client.address="Hostname or IP address"
AdvSceneSwitcher.networkTab.client.port="Port"
AdvSceneSwitcher.networkTab.client.status.currentStatus="Current status"
AdvSceneSwitcher.networkTab.client.status.disconnected="Disconnected"
AdvSceneSwitcher.networkTab.client.status.connecting="Connecting"
AdvSceneSwitcher.networkTab.client.status.connected="Connected"
AdvSceneSwitcher.networkTab.client.reconnect="Force reconnect"
; Scene Group Tab
AdvSceneSwitcher.sceneGroupTab.title="Scene Group"
AdvSceneSwitcher.sceneGroupTab.list="Scene Groups"
AdvSceneSwitcher.sceneGroupTab.edit="Edit Scene Groups"
AdvSceneSwitcher.sceneGroupTab.edit.name="Name:"
AdvSceneSwitcher.sceneGroupTab.edit.type="Type:{{type}}"
AdvSceneSwitcher.sceneGroupTab.type.count="Count"
AdvSceneSwitcher.sceneGroupTab.type.time="Time"
AdvSceneSwitcher.sceneGroupTab.type.random="Random"
AdvSceneSwitcher.sceneGroupTab.edit.count="Advance to next scene in list after{{count}}matches"
AdvSceneSwitcher.sceneGroupTab.edit.time="Advance to next scene in list after{{time}}has passed"
AdvSceneSwitcher.sceneGroupTab.edit.random="Choose next scene in list at random"
AdvSceneSwitcher.sceneGroupTab.edit.repeat="Start from beginning if end of scene list is reached"
AdvSceneSwitcher.sceneGroupTab.edit.addScene="Add scene"
AdvSceneSwitcher.sceneGroupTab.add="Add Scene Group"
AdvSceneSwitcher.sceneGroupTab.defaultname="Scene Group %1"
AdvSceneSwitcher.sceneGroupTab.exists="Scene Group or Scene name exists already"
AdvSceneSwitcher.sceneGroupTab.help="Scene Groups can be selected as a target just like a regular scene.\n\nAs the name suggests a scene group is a collection of multiple scenes.\nThe scene group will advance through the list of its assigned scenes depending on the configured settings, which can be found on the right side.\n\nYou can configure the scene group to advance to the next scene in the list:\nAfter a number of times the scene group is selected as a target.\nAfter a certain amount of time has passed.\nOr randomly.\n\nFor example, a scene group containing the scenes ...\nScene 1\nScene 2\nScene 3 \n... will activate \"Scene 1\" the first time it is selected as a target.\nThe second time it will activate \"Scene 2\".\nThe remaining times \"Scene 3\" will be activated.\n\nClick the highlighted plus symbol below to add a new scene group."
AdvSceneSwitcher.sceneGroupTab.scenes.help="Select the scene group you want to modify on the left.\n\nSelect a scene to add to this scene group by selecting the scene above and clicking the plus symbol below.\n\nA scene can be added multiple times to the same scene group."
; Scene Trigger Tab
AdvSceneSwitcher.sceneTriggerTab.title="Scene Triggers"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.none="--select trigger--"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneActive="is active"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneInactive="is not active"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerType.sceneLeave="switched away from"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.none="--select action--"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startRecording="start recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.pauseRecording="pause recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unpauseRecording="unpause recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopRecording="stop recording"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopStreaming="stop streaming"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startStreaming="start streaming"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startReplayBuffer="start replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopReplayBuffer="stop replay buffer"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.muteSource="mute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.unmuteSource="unmute source"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startSwitcher="start the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopSwitcher="stop the scene switcher"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.startVirtualCamera="start virtual camera"
AdvSceneSwitcher.sceneTriggerTab.sceneTriggerAction.stopVirtualCamera="stop virtual camera"
AdvSceneSwitcher.sceneTriggerTab.entry="When{{scenes}}{{triggers}}{{actions}}{{audioSources}}after{{duration}}"
AdvSceneSwitcher.sceneTriggerTab.help="This tab allows you to trigger actions on scene changes, like stopping recording or streaming."

View File

@@ -122,12 +122,10 @@ AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Durante la tr
AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}"
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
AdvSceneSwitcher.condition.window="Ventana"
AdvSceneSwitcher.condition.window.entry.line1="{{windows}} existen y ..."
AdvSceneSwitcher.condition.window.entry.line2="... es {{fullscreen}} pantalla completa {{maximized}} maximizada {{focused}} enfocada {{windowFocusChanged}} ventana de primer plano cambiada"
AdvSceneSwitcher.condition.file="Archivo"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medios"
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
@@ -324,8 +322,8 @@ AdvSceneSwitcher.condition.stats.dockHint="Puede abrir el panel de \"Estadístic
AdvSceneSwitcher.condition.stats.entry="{{stats}} esta {{condition}} {{value}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="Cambiar escena"
AdvSceneSwitcher.action.scene.entry="Cambiar a la escena {{scenes}} usando {{transitions}} con una duración de {{duration}} segundos"
AdvSceneSwitcher.action.scene="Cambiar escena"
AdvSceneSwitcher.action.scene.entry="Cambiar a la{{sceneTypes}}escena{{scenes}}usando{{transitions}}con una duración de{{duration}} segundos"
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Espere hasta que se complete la transición a la escena de destino"
AdvSceneSwitcher.action.wait="Esperar"
AdvSceneSwitcher.action.wait.type.fixed="fijo"
@@ -362,10 +360,6 @@ AdvSceneSwitcher.action.streaming.type.stop="Detener transmisión"
AdvSceneSwitcher.action.streaming.type.start="Iniciar transmisión"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Ejecutar"
AdvSceneSwitcher.action.run.arguments="Argumentos:"
AdvSceneSwitcher.action.run.addArgument="Agregar argumento"
AdvSceneSwitcher.action.run.addArgumentDescription="Añadir nuevo argumento:"
AdvSceneSwitcher.action.run.entry="Ejecutar {{filePath}}"
AdvSceneSwitcher.action.sceneVisibility="Visibilidad del elemento de escena"
AdvSceneSwitcher.action.sceneVisibility.type.show="Mostrar"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Ocultar"
@@ -422,7 +416,6 @@ AdvSceneSwitcher.action.hotkey.leftMeta="Meta izquierda"
AdvSceneSwitcher.action.hotkey.rightMeta="Meta derecho"
AdvSceneSwitcher.action.hotkey.onlyOBS="Enviar pulsación de tecla solo a OBS"
AdvSceneSwitcher.action.hotkey.disabled="No se puede simular la pulsación de teclas: ¡funcionalidad desactivada!"
AdvSceneSwitcher.action.hotkey.entry="Presione {{keys}} durante {{duration}}"
AdvSceneSwitcher.action.sceneOrder="Orden de elementos de escena"
AdvSceneSwitcher.action.sceneOrder.type.moveUp="Mover hacia arriba"
AdvSceneSwitcher.action.sceneOrder.type.moveDown="Mover hacia abajo"

1209
data/locale/fr-FR.ini Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -86,10 +86,9 @@ AdvSceneSwitcher.condition.scene="Сцена"
AdvSceneSwitcher.condition.scene.type.current="Текущий"
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
AdvSceneSwitcher.condition.window="Окно"
AdvSceneSwitcher.condition.window.entry.line1="{{windows}} существует и ..."
AdvSceneSwitcher.condition.file="Файл"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Медиа"
AdvSceneSwitcher.condition.video="Видео"
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
@@ -116,8 +115,8 @@ AdvSceneSwitcher.condition.pluginState.state.sceneSwitched="Автоматиче
AdvSceneSwitcher.condition.pluginState.entry="{{condition}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="Переключить сцену"
AdvSceneSwitcher.action.scene.entry="Перейти к сцене {{scenes}} используя {{transitions}} с продолжительностью {{duration}} секунд"
AdvSceneSwitcher.action.scene="Переключить сцену"
AdvSceneSwitcher.action.scene.entry="Перейти к сцене{{sceneTypes}}{{scenes}}используя{{transitions}}с продолжительностью{{duration}}секунд"
AdvSceneSwitcher.action.wait="Подождать"
AdvSceneSwitcher.action.wait.type.fixed="фиксированный"
AdvSceneSwitcher.action.wait.type.random="случайный"

View File

@@ -112,12 +112,10 @@ AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Geçiş hedefi
AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}"
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
AdvSceneSwitcher.condition.window="Pencere"
AdvSceneSwitcher.condition.window.entry.line1="{{windows}} Varolan ve ..."
AdvSceneSwitcher.condition.window.entry.line2="... {{fullscreen}} Tam ekran {{maximized}} büyütüldü {{focused}} odaklanıldı {{windowFocusChanged}} ön plan penceresi değişikliği"
AdvSceneSwitcher.condition.file="Dosya"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medya"
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
@@ -261,8 +259,8 @@ AdvSceneSwitcher.condition.openvr.entry.line2="{{controls}}"
AdvSceneSwitcher.condition.openvr.entry.line3="HMD mevcut {{xPos}} x {{yPos}} x {{zPos}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="Sahne Degistirici"
AdvSceneSwitcher.action.scene.entry="Sahneyi {{scenes}} kullanarak {{transitions}} süresi olan {{duration}} saniye"
AdvSceneSwitcher.action.scene="Sahne Degistirici"
AdvSceneSwitcher.action.scene.entry="Sahneyi{{sceneTypes}}{{scenes}}kullanarak{{transitions}}süresi olan{{duration}}saniye"
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Hedef sahneye geçiş tamamlanana kadar bekleyin"
AdvSceneSwitcher.action.wait="Bekle"
AdvSceneSwitcher.action.wait.type.fixed="sabit"
@@ -292,10 +290,6 @@ AdvSceneSwitcher.action.streaming.type.stop="Yayın durdur"
AdvSceneSwitcher.action.streaming.type.start="Yayın başlat"
AdvSceneSwitcher.action.streaming.entry="{{actions}}{{keyFrameInterval}}{{stringValue}}{{showPassword}}"
AdvSceneSwitcher.action.run="Çalıştır"
AdvSceneSwitcher.action.run.arguments="Argümanlar:"
AdvSceneSwitcher.action.run.addArgument="Argüman ekle"
AdvSceneSwitcher.action.run.addArgumentDescription="Yeni Argüman ekle:"
AdvSceneSwitcher.action.run.entry="Çalıştır {{filePath}}"
AdvSceneSwitcher.action.sceneVisibility="Sahne öğesi görünürlüğü"
AdvSceneSwitcher.action.sceneVisibility.type.show="Göster"
AdvSceneSwitcher.action.sceneVisibility.type.hide="Gizle"
@@ -350,7 +344,6 @@ AdvSceneSwitcher.action.hotkey.leftMeta="Sol Meta"
AdvSceneSwitcher.action.hotkey.rightMeta="Sağ Meta"
AdvSceneSwitcher.action.hotkey.onlyOBS="Tuşa basımı yalnızca OBS'ye gönder"
AdvSceneSwitcher.action.hotkey.disabled="Tuşa basma simülasyonu yapılamıyor - işlevsellik devre dışı!"
AdvSceneSwitcher.action.hotkey.entry="Bas {{keys}} uygun {{duration}}"
AdvSceneSwitcher.action.sceneOrder="Sahne öğesi sırası"
AdvSceneSwitcher.action.sceneOrder.type.moveUp="Yukarı taşı"
AdvSceneSwitcher.action.sceneOrder.type.moveDown="Aşağı indir"

View File

@@ -178,9 +178,9 @@ AdvSceneSwitcher.condition.file.type.contentChange="内容已更改"
AdvSceneSwitcher.condition.file.type.dateChange="修改日期已更改"
AdvSceneSwitcher.condition.file.remote="远程文件"
AdvSceneSwitcher.condition.file.local="本地文件"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{useRegex}} {{checkModificationDate}} {{checkFileContent}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="媒体"
AdvSceneSwitcher.condition.media.source="源"
AdvSceneSwitcher.condition.media.anyOnScene="任何媒体源"
@@ -448,9 +448,9 @@ AdvSceneSwitcher.condition.display.type.displayCount="连接的显示器的数
AdvSceneSwitcher.condition.display.entry="{{conditions}}{{displays}}{{regex}}{{displayCount}}"
; Macro Actions
AdvSceneSwitcher.action.switchScene="切换场景"
AdvSceneSwitcher.action.scene.entry="切换场景 {{scenes}} 使用 {{transitions}} 时长 {{duration}} 秒"
AdvSceneSwitcher.action.scene.entry.noDuration="切换到场景{{scenes}}使用{{transitions}}"
AdvSceneSwitcher.action.scene="切换场景"
AdvSceneSwitcher.action.scene.entry="切换场景{{sceneTypes}}{{scenes}}使用{{transitions}}时长{{duration}}秒"
AdvSceneSwitcher.action.scene.entry.noDuration="切换到场景{{sceneTypes}}{{scenes}}使用{{transitions}}"
AdvSceneSwitcher.action.scene.blockUntilTransitionDone="等待目标场景的过渡完成"
AdvSceneSwitcher.action.wait="等待"
AdvSceneSwitcher.action.wait.type.fixed="固定数值"
@@ -559,7 +559,6 @@ AdvSceneSwitcher.action.hotkey.leftMeta="左 Meta"
AdvSceneSwitcher.action.hotkey.rightMeta="右 Meta"
AdvSceneSwitcher.action.hotkey.onlyOBS="仅向OBS发送按键"
AdvSceneSwitcher.action.hotkey.disabled="无法模拟按键-功能已禁用!"
AdvSceneSwitcher.action.hotkey.entry="按下 {{keys}} 在 {{duration}} 秒"
AdvSceneSwitcher.action.sceneOrder="场景项目顺序"
AdvSceneSwitcher.action.sceneOrder.type.moveUp="上移"
AdvSceneSwitcher.action.sceneOrder.type.moveDown="下移"
@@ -624,8 +623,6 @@ AdvSceneSwitcher.action.sequence.continueFrom="继续所选项目"
AdvSceneSwitcher.action.websocket="websocket"
AdvSceneSwitcher.action.websocket.type.request="request"
AdvSceneSwitcher.action.websocket.type.event="event"
AdvSceneSwitcher.action.websocket.entry.request="通过{{connection}} 发送场景切换器 {{type}}"
AdvSceneSwitcher.action.websocket.entry.event="发送场景切换器 {{type}} 到连接的客户端"
AdvSceneSwitcher.action.http="Http"
AdvSceneSwitcher.action.http.setHeaders="设置头信息(SET headers)"
AdvSceneSwitcher.action.http.headers="头信息(headers):"
@@ -655,7 +652,7 @@ AdvSceneSwitcher.action.variable.invalidSelection="无效选择!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="不支持从 %1 个操作获取变量值!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="不支持从 %1 条件中获取变量值!"
AdvSceneSwitcher.action.variable.currentSegmentValue="当前值:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="子字符串开始:{{subStringStart}} 子字符串大小:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="使用正则表达式为 {{regexMatchIdx}} 匹配的值:"
AdvSceneSwitcher.action.variable.entry.findAndReplace="{{findStr}}{{replaceStr}}"

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 87.803 77.194 L 68.212 57.602 c 9.5 -14.422 7.912 -34.054 -4.766 -46.732 c 0 0 -0.001 0 -0.001 0 c -14.495 -14.493 -38.08 -14.494 -52.574 0 c -14.494 14.495 -14.494 38.079 0 52.575 c 7.248 7.247 16.767 10.87 26.287 10.87 c 7.134 0 14.267 -2.035 20.445 -6.104 l 19.591 19.591 C 78.659 89.267 80.579 90 82.498 90 s 3.84 -0.733 5.305 -2.197 C 90.732 84.873 90.732 80.124 87.803 77.194 z M 21.48 52.837 c -8.645 -8.646 -8.645 -22.713 0 -31.358 c 4.323 -4.322 10 -6.483 15.679 -6.483 c 5.678 0 11.356 2.161 15.678 6.483 c 8.644 8.644 8.645 22.707 0.005 31.352 c -0.002 0.002 -0.004 0.003 -0.005 0.005 c -0.002 0.002 -0.003 0.003 -0.004 0.005 C 44.184 61.481 30.123 61.48 21.48 52.837 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #fefefe; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 87.803 77.194 L 68.212 57.602 c 9.5 -14.422 7.912 -34.054 -4.766 -46.732 c 0 0 -0.001 0 -0.001 0 c -14.495 -14.493 -38.08 -14.494 -52.574 0 c -14.494 14.495 -14.494 38.079 0 52.575 c 7.248 7.247 16.767 10.87 26.287 10.87 c 7.134 0 14.267 -2.035 20.445 -6.104 l 19.591 19.591 C 78.659 89.267 80.579 90 82.498 90 s 3.84 -0.733 5.305 -2.197 C 90.732 84.873 90.732 80.124 87.803 77.194 z M 21.48 52.837 c -8.645 -8.646 -8.645 -22.713 0 -31.358 c 4.323 -4.322 10 -6.483 15.679 -6.483 c 5.678 0 11.356 2.161 15.678 6.483 c 8.644 8.644 8.645 22.707 0.005 31.352 c -0.002 0.002 -0.004 0.003 -0.005 0.005 c -0.002 0.002 -0.003 0.003 -0.004 0.005 C 44.184 61.481 30.123 61.48 21.48 52.837 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: #202020; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
deps/cpp-httplib vendored Submodule

Submodule deps/cpp-httplib added at c7ed1796a7

View File

@@ -67,8 +67,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>957</width>
<height>905</height>
<width>962</width>
<height>1033</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_19">
@@ -256,6 +256,30 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QCheckBox" name="disableComboBoxFilter">
<property name="text">
<string>AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_26">
<item>
@@ -805,7 +829,7 @@
<property name="title">
<string>AdvSceneSwitcher.macroTab.edit</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_33">
<layout class="QVBoxLayout" name="verticalLayout_38">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_18" stretch="0,0,0,0,0,0,0,0,0">
<item>
@@ -819,7 +843,7 @@
<widget class="QLineEdit" name="macroName"/>
</item>
<item>
<widget class="QPushButton" name="runMacro">
<widget class="advss::MacroRunButton" name="runMacro">
<property name="text">
<string>AdvSceneSwitcher.macroTab.run</string>
</property>
@@ -912,6 +936,16 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="advss::MacroSegmentList" name="conditionsList">
<property name="minimumSize">
<size>
<width>0</width>
<height>1</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<property name="leftMargin">
@@ -1066,170 +1100,360 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="macroActions" native="true">
<layout class="QVBoxLayout" name="macroActionsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_21">
<property name="leftMargin">
<number>9</number>
</property>
<item>
<widget class="QPushButton" name="actionAdd">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">addIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionRemove">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">removeIconSmall</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_133">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_47">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_132">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="actionTop">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionUp">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">upArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionDown">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">downArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionBottom">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_14">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
<widget class="QSplitter" name="macroElseActionSplitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="macroActions" native="true">
<layout class="QVBoxLayout" name="macroActionsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="advss::MacroSegmentList" name="actionsList">
<property name="minimumSize">
<size>
<width>0</width>
<height>1</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_21">
<property name="leftMargin">
<number>9</number>
</property>
<item>
<widget class="QPushButton" name="actionAdd">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">addIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionRemove">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">removeIconSmall</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_133">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_47">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_132">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="actionTop">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionUp">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">upArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionDown">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">downArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="actionBottom">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_14">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="macroElseActions" native="true">
<layout class="QVBoxLayout" name="macroElseActionsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="advss::MacroSegmentList" name="elseActionsList">
<property name="minimumSize">
<size>
<width>0</width>
<height>1</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_33">
<property name="leftMargin">
<number>9</number>
</property>
<item>
<widget class="QPushButton" name="elseActionAdd">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">addIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="elseActionRemove">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">removeIconSmall</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_138">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_50">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_139">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="elseActionTop">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="elseActionUp">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">upArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="elseActionDown">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="themeID" stdset="0">
<string notr="true">downArrowIconSmall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="elseActionBottom">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_22">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
@@ -4619,6 +4843,17 @@
<extends>QListView</extends>
<header>macro-tree.hpp</header>
</customwidget>
<customwidget>
<class>advss::MacroSegmentList</class>
<extends>QScrollArea</extends>
<header>macro-segment-list.hpp</header>
<container>1</container>
</customwidget>
<customwidget>
<class>advss::MacroRunButton</class>
<extends>QPushButton</extends>
<header>macro-run-button.hpp</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -1,13 +1,3 @@
#include <QMainWindow>
#include <QAction>
#include <QFileDialog>
#include <QDirIterator>
#include <regex>
#include <filesystem>
#include <obs-module.h>
#include <obs-frontend-api.h>
#include "advanced-scene-switcher.hpp"
#include "switcher-data.hpp"
#include "status-control.hpp"
@@ -17,6 +7,15 @@
#include "utility.hpp"
#include "version.h"
#include <QMainWindow>
#include <QAction>
#include <QFileDialog>
#include <QDirIterator>
#include <regex>
#include <filesystem>
#include <obs-module.h>
#include <obs-frontend-api.h>
namespace advss {
AdvSceneSwitcher *AdvSceneSwitcher::window = nullptr;
@@ -328,12 +327,14 @@ void SwitcherData::SetPreconditions()
lastCursorPos = GetCursorPos();
}
void ClearWebsocketMessages();
void SwitcherData::ResetForNextInterval()
{
// Core reset functions
ClearWebsocketMessages();
// Plugin reset functions
for (const auto &func : resetForNextIntervalFuncs) {
for (const auto &func : resetIntervalSteps) {
func();
}
}

View File

@@ -63,6 +63,7 @@ public slots:
void on_saveWindowGeo_stateChanged(int state);
void on_showTrayNotifications_stateChanged(int state);
void on_uiHintsDisable_stateChanged(int state);
void on_disableComboBoxFilter_stateChanged(int state);
void on_warnPluginLoadFailure_stateChanged(int state);
void on_hideLegacyTabs_stateChanged(int state);
void on_priorityUp_clicked();
@@ -83,10 +84,13 @@ public:
void SetEditMacro(Macro &m);
void SetMacroEditAreaDisabled(bool);
void HighlightAction(int idx, QColor color = QColor(Qt::green));
void HighlightElseAction(int idx, QColor color = QColor(Qt::green));
void HighlightCondition(int idx, QColor color = QColor(Qt::green));
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 SetActionData(Macro &m);
void SetElseActionData(Macro &m);
void SetConditionData(Macro &m);
void SwapActions(Macro *m, int pos1, int pos2);
void SwapConditions(Macro *m, int pos1, int pos2);
@@ -97,7 +101,6 @@ public slots:
void on_macroUp_clicked();
void on_macroDown_clicked();
void on_macroName_editingFinished();
void on_runMacro_clicked();
void on_runMacroInParallel_stateChanged(int value);
void on_runMacroOnChange_stateChanged(int value);
void on_conditionAdd_clicked();
@@ -112,29 +115,50 @@ public slots:
void on_actionUp_clicked();
void on_actionDown_clicked();
void on_actionBottom_clicked();
void on_elseActionAdd_clicked();
void on_elseActionRemove_clicked();
void on_elseActionTop_clicked();
void on_elseActionUp_clicked();
void on_elseActionDown_clicked();
void on_elseActionBottom_clicked();
void MacroSelectionAboutToChange();
void MacroSelectionChanged();
void UpMacroSegementHotkey();
void DownMacroSegementHotkey();
void DeleteMacroSegementHotkey();
void ShowMacroContextMenu(const QPoint &);
void ShowMacroActionsContextMenu(const QPoint &);
void ShowMacroElseActionsContextMenu(const QPoint &);
void ShowMacroConditionsContextMenu(const QPoint &);
void CopyMacro();
void RenameCurrentMacro();
void ExportMacros();
void ImportMacros();
void ExpandAllActions();
void ExpandAllElseActions();
void ExpandAllConditions();
void CollapseAllActions();
void CollapseAllElseActions();
void CollapseAllConditions();
void MinimizeActions();
void MaximizeActions();
void MinimizeElseActions();
void MaximizeElseActions();
void MinimizeConditions();
void MaximizeConditions();
void MacroActionSelectionChanged(int idx);
void MacroActionReorder(int to, int target);
void AddMacroAction(int idx);
void RemoveMacroAction(int idx);
void MoveMacroActionUp(int idx);
void MoveMacroActionDown(int idx);
void MacroElseActionSelectionChanged(int idx);
void MacroElseActionReorder(int to, int target);
void AddMacroElseAction(int idx);
void RemoveMacroElseAction(int idx);
void SwapElseActions(Macro *m, int pos1, int pos2);
void MoveMacroElseActionUp(int idx);
void MoveMacroElseActionDown(int idx);
void MacroConditionSelectionChanged(int idx);
void MacroConditionReorder(int to, int target);
void AddMacroCondition(int idx);
@@ -156,6 +180,7 @@ signals:
void MacroSegmentOrderChanged();
void HighlightMacrosChanged(bool value);
void HighlightActionsChanged(bool value);
void HighlightElseActionsChanged(bool value);
void HighlightConditionsChanged(bool value);
void ConnectionAdded(const QString &);
@@ -166,19 +191,16 @@ signals:
void VariableRemoved(const QString &);
private:
enum class MacroSection { CONDITIONS, ACTIONS, ELSE_ACTIONS };
void SetupMacroSegmentSelection(MacroSection type, int idx);
bool ResolveMacroImportNameConflict(std::shared_ptr<Macro> &);
bool MacroTabIsInFocus();
MacroSegmentList *conditionsList = nullptr;
MacroSegmentList *actionsList = nullptr;
enum class MacroSection {
CONDITIONS,
ACTIONS,
};
MacroSection lastInteracted = MacroSection::CONDITIONS;
int currentConditionIdx = -1;
int currentActionIdx = -1;
int currentElseActionIdx = -1;
/* --- End of macro tab section --- */

View File

@@ -2,6 +2,7 @@
#include "switcher-data.hpp"
#include "status-control.hpp"
#include "file-selection.hpp"
#include "filter-combo-box.hpp"
#include "utility.hpp"
#include "version.h"
@@ -131,10 +132,9 @@ void AdvSceneSwitcher::closeEvent(QCloseEvent *)
}
switcher->windowPos = this->pos();
switcher->windowSize = this->size();
switcher->macroActionConditionSplitterPosition =
ui->macroActionConditionSplitter->sizes();
switcher->macroListMacroEditSplitterPosition =
ui->macroListMacroEditSplitter->sizes();
MacroSelectionAboutToChange(); // Trigger saving of splitter states
obs_frontend_save();
}
@@ -175,6 +175,16 @@ void AdvSceneSwitcher::on_uiHintsDisable_stateChanged(int state)
switcher->disableHints = state;
}
void AdvSceneSwitcher::on_disableComboBoxFilter_stateChanged(int state)
{
if (loading) {
return;
}
switcher->disableFilterComboboxFilter = state;
FilterComboBox::SetFilterBehaviourEnabled(!state);
}
void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
{
if (loading) {
@@ -494,6 +504,11 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
loadSceneGroups(obj);
LoadVariables(obj);
LoadConnections(obj);
for (const auto &func : loadSteps) {
func(obj);
}
LoadMacros(obj);
loadWindowTitleSwitches(obj);
loadScreenRegionSwitches(obj);
@@ -548,6 +563,10 @@ void SwitcherData::SaveSettings(obs_data_t *obj)
SaveHotkeys(obj);
SaveUISettings(obj);
SaveVersion(obj, g_GIT_SHA1);
for (const auto &func : saveSteps) {
func(obj);
}
}
void SwitcherData::SaveGeneralSettings(obs_data_t *obj)
@@ -575,6 +594,8 @@ void SwitcherData::SaveGeneralSettings(obs_data_t *obj)
obs_data_set_bool(obj, "showSystemTrayNotifications",
showSystemTrayNotifications);
obs_data_set_bool(obj, "disableHints", disableHints);
obs_data_set_bool(obj, "disableFilterComboboxFilter",
disableFilterComboboxFilter);
obs_data_set_bool(obj, "warnPluginLoadFailure", warnPluginLoadFailure);
obs_data_set_bool(obj, "hideLegacyTabs", hideLegacyTabs);
@@ -623,6 +644,8 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
showSystemTrayNotifications =
obs_data_get_bool(obj, "showSystemTrayNotifications");
disableHints = obs_data_get_bool(obj, "disableHints");
disableFilterComboboxFilter =
obs_data_get_bool(obj, "disableFilterComboboxFilter");
obs_data_set_default_bool(obj, "warnPluginLoadFailure", true);
warnPluginLoadFailure = obs_data_get_bool(obj, "warnPluginLoadFailure");
obs_data_set_default_bool(obj, "hideLegacyTabs", true);
@@ -652,34 +675,6 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
lastImportPath = obs_data_get_string(obj, "lastImportPath");
}
static void saveSplitterPos(QList<int> &sizes, obs_data_t *obj,
const std::string name)
{
auto array = obs_data_array_create();
for (int i = 0; i < sizes.count(); ++i) {
obs_data_t *array_obj = obs_data_create();
obs_data_set_int(array_obj, "pos", sizes[i]);
obs_data_array_push_back(array, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, name.c_str(), array);
obs_data_array_release(array);
}
static void loadSplitterPos(QList<int> &sizes, obs_data_t *obj,
const std::string name)
{
sizes.clear();
obs_data_array_t *array = obs_data_get_array(obj, name.c_str());
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
obs_data_t *item = obs_data_array_item(array, i);
sizes << obs_data_get_int(item, "pos");
obs_data_release(item);
}
obs_data_array_release(array);
}
void SwitcherData::SaveUISettings(obs_data_t *obj)
{
obs_data_set_int(obj, "generalTabPos", tabOrder[0]);
@@ -707,9 +702,7 @@ void SwitcherData::SaveUISettings(obs_data_t *obj)
obs_data_set_int(obj, "windowWidth", windowSize.width());
obs_data_set_int(obj, "windowHeight", windowSize.height());
saveSplitterPos(macroActionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
saveSplitterPos(macroListMacroEditSplitterPosition, obj,
SaveSplitterPos(macroListMacroEditSplitterPosition, obj,
"macroListMacroEditSplitterPosition");
}
@@ -763,9 +756,8 @@ void SwitcherData::LoadUISettings(obs_data_t *obj)
(int)obs_data_get_int(obj, "windowPosY")};
windowSize = {(int)obs_data_get_int(obj, "windowWidth"),
(int)obs_data_get_int(obj, "windowHeight")};
loadSplitterPos(macroActionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
loadSplitterPos(macroListMacroEditSplitterPosition, obj,
LoadSplitterPos(macroListMacroEditSplitterPosition, obj,
"macroListMacroEditSplitterPosition");
}
@@ -997,6 +989,10 @@ void AdvSceneSwitcher::SetupGeneralTab()
ui->showTrayNotifications->setChecked(
switcher->showSystemTrayNotifications);
ui->uiHintsDisable->setChecked(switcher->disableHints);
ui->disableComboBoxFilter->setChecked(
switcher->disableFilterComboboxFilter);
FilterComboBox::SetFilterBehaviourEnabled(
!switcher->disableFilterComboboxFilter);
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);

View File

@@ -332,7 +332,7 @@ bool IsFullscreen(const std::string &title)
return windowStatesAreSet(title, states);
}
std::optional<std::string> GetTextInWindow(const std::string &window)
std::optional<std::string> GetTextInWindow(const std::string &)
{
// Not implemented
return {};

View File

@@ -239,7 +239,7 @@ void AdvSceneSwitcher::AddMacroAction(int idx)
obs_data_release(data);
}
macro->UpdateActionIndices();
actionsList->Insert(
ui->actionsList->Insert(
idx,
new MacroActionEdit(this, &macro->Actions()[idx], id));
SetActionData(*macro);
@@ -263,7 +263,7 @@ void AdvSceneSwitcher::on_actionAdd_clicked()
if (currentActionIdx != -1) {
MacroActionSelectionChanged(currentActionIdx + 1);
}
actionsList->SetHelpMsgVisible(false);
ui->actionsList->SetHelpMsgVisible(false);
}
void AdvSceneSwitcher::RemoveMacroAction(int idx)
@@ -279,7 +279,7 @@ void AdvSceneSwitcher::RemoveMacroAction(int idx)
{
std::lock_guard<std::mutex> lock(switcher->m);
actionsList->Remove(idx);
ui->actionsList->Remove(idx);
macro->Actions().erase(macro->Actions().begin() + idx);
switcher->abortMacroWait = true;
switcher->macroWaitCv.notify_all();
@@ -322,10 +322,11 @@ void AdvSceneSwitcher::on_actionUp_clicked()
MoveMacroActionUp(currentActionIdx);
MacroActionSelectionChanged(currentActionIdx - 1);
}
void AdvSceneSwitcher::on_actionDown_clicked()
{
if (currentActionIdx == -1 ||
currentActionIdx == actionsList->ContentLayout()->count() - 1) {
currentActionIdx == ui->actionsList->ContentLayout()->count() - 1) {
return;
}
MoveMacroActionDown(currentActionIdx);
@@ -337,11 +338,82 @@ void AdvSceneSwitcher::on_actionBottom_clicked()
if (currentActionIdx == -1) {
return;
}
const int newIdx = actionsList->ContentLayout()->count() - 1;
const int newIdx = ui->actionsList->ContentLayout()->count() - 1;
MacroActionReorder(newIdx, currentActionIdx);
MacroActionSelectionChanged(newIdx);
}
void AdvSceneSwitcher::on_elseActionAdd_clicked()
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (currentElseActionIdx == -1) {
AddMacroElseAction((int)macro->ElseActions().size());
} else {
AddMacroElseAction(currentElseActionIdx + 1);
}
if (currentElseActionIdx != -1) {
MacroElseActionSelectionChanged(currentElseActionIdx + 1);
}
ui->elseActionsList->SetHelpMsgVisible(false);
}
void AdvSceneSwitcher::on_elseActionRemove_clicked()
{
if (currentElseActionIdx == -1) {
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
RemoveMacroElseAction((int)macro->Actions().size() - 1);
} else {
RemoveMacroElseAction(currentElseActionIdx);
}
MacroElseActionSelectionChanged(-1);
}
void AdvSceneSwitcher::on_elseActionTop_clicked()
{
if (currentElseActionIdx == -1) {
return;
}
MacroElseActionReorder(0, currentElseActionIdx);
MacroElseActionSelectionChanged(0);
}
void AdvSceneSwitcher::on_elseActionUp_clicked()
{
if (currentElseActionIdx == -1 || currentElseActionIdx == 0) {
return;
}
MoveMacroElseActionUp(currentElseActionIdx);
MacroElseActionSelectionChanged(currentElseActionIdx - 1);
}
void AdvSceneSwitcher::on_elseActionDown_clicked()
{
if (currentElseActionIdx == -1 ||
currentElseActionIdx ==
ui->elseActionsList->ContentLayout()->count() - 1) {
return;
}
MoveMacroElseActionDown(currentElseActionIdx);
MacroElseActionSelectionChanged(currentElseActionIdx + 1);
}
void AdvSceneSwitcher::on_elseActionBottom_clicked()
{
if (currentElseActionIdx == -1) {
return;
}
const int newIdx = ui->elseActionsList->ContentLayout()->count() - 1;
MacroElseActionReorder(newIdx, currentElseActionIdx);
MacroElseActionSelectionChanged(newIdx);
}
void AdvSceneSwitcher::SwapActions(Macro *m, int pos1, int pos2)
{
if (pos1 == pos2) {
@@ -355,11 +427,11 @@ void AdvSceneSwitcher::SwapActions(Macro *m, int pos1, int pos2)
iter_swap(m->Actions().begin() + pos1, m->Actions().begin() + pos2);
m->UpdateActionIndices();
auto widget1 = static_cast<MacroActionEdit *>(
actionsList->ContentLayout()->takeAt(pos1)->widget());
ui->actionsList->ContentLayout()->takeAt(pos1)->widget());
auto widget2 = static_cast<MacroActionEdit *>(
actionsList->ContentLayout()->takeAt(pos2 - 1)->widget());
actionsList->Insert(pos1, widget2);
actionsList->Insert(pos2, widget1);
ui->actionsList->ContentLayout()->takeAt(pos2 - 1)->widget());
ui->actionsList->Insert(pos1, widget2);
ui->actionsList->Insert(pos2, widget1);
SetActionData(*m);
emit(MacroSegmentOrderChanged());
}
@@ -394,24 +466,158 @@ void AdvSceneSwitcher::MoveMacroActionDown(int idx)
HighlightAction(idx + 1);
}
void AdvSceneSwitcher::MacroActionSelectionChanged(int idx)
void AdvSceneSwitcher::MacroElseActionSelectionChanged(int idx)
{
SetupMacroSegmentSelection(MacroSection::ELSE_ACTIONS, idx);
}
void AdvSceneSwitcher::MacroElseActionReorder(int to, int from)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
actionsList->SetSelection(idx);
conditionsList->SetSelection(-1);
if (idx < 0 || (unsigned)idx >= macro->Actions().size()) {
currentActionIdx = -1;
} else {
currentActionIdx = idx;
lastInteracted = MacroSection::ACTIONS;
if (to == from || from < 0 || from > (int)macro->ElseActions().size() ||
to < 0 || to > (int)macro->ElseActions().size()) {
return;
}
currentConditionIdx = -1;
HighlightControls();
{
std::lock_guard<std::mutex> lock(switcher->m);
auto action = macro->ElseActions().at(from);
macro->ElseActions().erase(macro->ElseActions().begin() + from);
macro->ElseActions().insert(macro->ElseActions().begin() + to,
action);
macro->UpdateElseActionIndices();
ui->elseActionsList->ContentLayout()->insertItem(
to, ui->elseActionsList->ContentLayout()->takeAt(from));
SetElseActionData(*macro);
}
HighlightElseAction(to);
emit(MacroSegmentOrderChanged());
}
void AdvSceneSwitcher::AddMacroElseAction(int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (idx < 0 || idx > (int)macro->ElseActions().size()) {
return;
}
std::string id;
if (idx - 1 >= 0) {
id = macro->ElseActions().at(idx - 1)->GetId();
} else {
MacroActionSwitchScene temp(nullptr);
id = temp.GetId();
}
{
std::lock_guard<std::mutex> lock(switcher->m);
macro->ElseActions().emplace(
macro->ElseActions().begin() + idx,
MacroActionFactory::Create(id, macro.get()));
if (idx - 1 >= 0) {
OBSDataAutoRelease data = obs_data_create();
macro->ElseActions().at(idx - 1)->Save(data);
macro->ElseActions().at(idx)->Load(data);
}
macro->UpdateElseActionIndices();
ui->elseActionsList->Insert(
idx, new MacroActionEdit(
this, &macro->ElseActions()[idx], id));
SetElseActionData(*macro);
}
HighlightElseAction(idx);
emit(MacroSegmentOrderChanged());
}
void AdvSceneSwitcher::RemoveMacroElseAction(int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (idx < 0 || idx >= (int)macro->ElseActions().size()) {
return;
}
{
std::lock_guard<std::mutex> lock(switcher->m);
ui->elseActionsList->Remove(idx);
macro->ElseActions().erase(macro->ElseActions().begin() + idx);
switcher->abortMacroWait = true;
switcher->macroWaitCv.notify_all();
macro->UpdateElseActionIndices();
SetActionData(*macro);
}
MacroElseActionSelectionChanged(-1);
lastInteracted = MacroSection::ELSE_ACTIONS;
emit(MacroSegmentOrderChanged());
}
void AdvSceneSwitcher::SwapElseActions(Macro *m, int pos1, int pos2)
{
if (pos1 == pos2) {
return;
}
if (pos1 > pos2) {
std::swap(pos1, pos2);
}
std::lock_guard<std::mutex> lock(switcher->m);
iter_swap(m->ElseActions().begin() + pos1,
m->ElseActions().begin() + pos2);
m->UpdateElseActionIndices();
auto widget1 = static_cast<MacroActionEdit *>(
ui->elseActionsList->ContentLayout()->takeAt(pos1)->widget());
auto widget2 = static_cast<MacroActionEdit *>(
ui->elseActionsList->ContentLayout()
->takeAt(pos2 - 1)
->widget());
ui->elseActionsList->Insert(pos1, widget2);
ui->elseActionsList->Insert(pos2, widget1);
SetElseActionData(*m);
emit(MacroSegmentOrderChanged());
}
void AdvSceneSwitcher::MoveMacroElseActionUp(int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (idx < 1 || idx >= (int)macro->ElseActions().size()) {
return;
}
SwapElseActions(macro.get(), idx, idx - 1);
HighlightElseAction(idx - 1);
}
void AdvSceneSwitcher::MoveMacroElseActionDown(int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (idx < 0 || idx >= (int)macro->ElseActions().size() - 1) {
return;
}
SwapElseActions(macro.get(), idx, idx + 1);
HighlightElseAction(idx + 1);
}
void AdvSceneSwitcher::MacroActionSelectionChanged(int idx)
{
SetupMacroSegmentSelection(MacroSection::ACTIONS, idx);
}
void AdvSceneSwitcher::MacroActionReorder(int to, int from)
@@ -431,8 +637,8 @@ void AdvSceneSwitcher::MacroActionReorder(int to, int from)
macro->Actions().erase(macro->Actions().begin() + from);
macro->Actions().insert(macro->Actions().begin() + to, action);
macro->UpdateActionIndices();
actionsList->ContentLayout()->insertItem(
to, actionsList->ContentLayout()->takeAt(from));
ui->actionsList->ContentLayout()->insertItem(
to, ui->actionsList->ContentLayout()->takeAt(from));
SetActionData(*macro);
}
HighlightAction(to);

View File

@@ -2,6 +2,7 @@
#include "platform-funcs.hpp"
#include "utility.hpp"
#include <thread>
#include <obs-interaction.h>
namespace advss {
@@ -723,7 +724,7 @@ static QString getHotkeyDescriptionByName(const std::string &name)
QString description = "";
} params;
auto func = [](void *param, obs_hotkey_id id, obs_hotkey_t *hotkey) {
auto func = [](void *param, obs_hotkey_id, obs_hotkey_t *hotkey) {
auto params = static_cast<Parameters *>(param);
std::string name = obs_hotkey_get_name(hotkey);
addNamePrefix(name, hotkey);

View File

@@ -49,7 +49,7 @@ bool MacroActionMacro::PerformAction()
break;
case Action::RUN:
if (!macro->Paused()) {
macro->PerformActions();
macro->PerformActions(true, false, true);
}
break;
case Action::STOP:

View File

@@ -57,6 +57,12 @@ bool MacroActionProjector::PerformAction()
break;
}
if (_fullscreen && _monitor == -1) {
blog(LOG_INFO, "refusing to open fullscreen projector"
" with invalid display selection");
return true;
}
obs_frontend_open_projector(type, _fullscreen ? _monitor : -1, "",
name.c_str());
@@ -85,6 +91,7 @@ bool MacroActionProjector::Save(obs_data_t *obj) const
MacroAction::Save(obj);
obs_data_set_int(obj, "type", static_cast<int>(_type));
obs_data_set_int(obj, "monitor", _monitor);
obs_data_set_string(obj, "monitorName", _monitorName.c_str());
obs_data_set_bool(obj, "fullscreen", _fullscreen);
_scene.Save(obj);
_source.Save(obj);
@@ -96,12 +103,50 @@ bool MacroActionProjector::Load(obs_data_t *obj)
MacroAction::Load(obj);
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
_monitor = obs_data_get_int(obj, "monitor");
_monitorName = obs_data_get_string(obj, "monitorName");
_fullscreen = obs_data_get_bool(obj, "fullscreen");
_scene.Load(obj);
_source.Load(obj);
if (MonitorSetupChanged()) {
blog(LOG_INFO, "monitor setup seems to have changed! "
"resetting projector action monitor selection!");
_monitor = -1;
}
return true;
}
void MacroActionProjector::SetMonitor(int idx)
{
_monitor = idx;
auto monitorNames = GetMonitorNames();
if (_monitor < 0 || _monitor >= monitorNames.size()) {
// Monitor setup changed while settings were selected?
_monitorName = "";
return;
}
_monitorName = monitorNames.at(_monitor).toStdString();
}
int MacroActionProjector::GetMonitor() const
{
return _monitor;
}
bool MacroActionProjector::MonitorSetupChanged()
{
if (_monitorName.empty()) {
return false;
}
auto monitorNames = GetMonitorNames();
if (_monitor < 0 || _monitor >= monitorNames.size()) {
return true;
}
return monitorNames.at(_monitor) !=
QString::fromStdString(_monitorName);
}
static inline void populateSelectionTypes(QComboBox *list)
{
for (auto entry : selectionTypes) {
@@ -134,6 +179,8 @@ MacroActionProjectorEdit::MacroActionProjectorEdit(
sources.sort();
_sources->SetSourceNameList(sources);
_monitors->addItems(GetMonitorNames());
_monitors->setPlaceholderText(
obs_module_text("AdvSceneSwitcher.selectDisplay"));
QWidget::connect(_windowTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(WindowTypeChanged(int)));
@@ -177,7 +224,7 @@ void MacroActionProjectorEdit::UpdateEntryData()
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
_scenes->SetScene(_entryData->_scene);
_sources->SetSource(_entryData->_source);
_monitors->setCurrentIndex(_entryData->_monitor);
_monitors->setCurrentIndex(_entryData->GetMonitor());
SetWidgetVisibility();
}
@@ -208,7 +255,7 @@ void MacroActionProjectorEdit::MonitorChanged(int value)
}
auto lock = LockContext();
_entryData->_monitor = value;
_entryData->SetMonitor(value);
}
void MacroActionProjectorEdit::WindowTypeChanged(int)

View File

@@ -17,6 +17,8 @@ public:
{
return std::make_shared<MacroActionProjector>(m);
}
void SetMonitor(int);
int GetMonitor() const;
enum class Type {
SOURCE,
@@ -29,10 +31,15 @@ public:
Type _type = Type::SCENE;
SourceSelection _source;
SceneSelection _scene;
int _monitor = 0;
bool _fullscreen = true;
private:
bool MonitorSetupChanged();
int _monitor = -1;
// Only used to detect display setup changes
std::string _monitorName = "";
static bool _registered;
static const std::string id;
};

View File

@@ -54,12 +54,12 @@ bool MacroActionRandom::PerformAction()
}
if (macros.size() == 1) {
lastRandomMacro = macros[0];
return macros[0]->PerformActions();
return macros[0]->PerformActions(true);
}
srand((unsigned int)time(0));
size_t idx = std::rand() % (macros.size());
lastRandomMacro = macros[idx];
return macros[idx]->PerformActions();
return macros[idx]->PerformActions(true);
}
void MacroActionRandom::LogAction() const

View File

@@ -12,7 +12,15 @@ const std::string MacroActionSwitchScene::id = "scene_switch";
bool MacroActionSwitchScene::_registered = MacroActionFactory::Register(
MacroActionSwitchScene::id,
{MacroActionSwitchScene::Create, MacroActionSwitchSceneEdit::Create,
"AdvSceneSwitcher.action.switchScene"});
"AdvSceneSwitcher.action.scene"});
const static std::map<MacroActionSwitchScene::SceneType, std::string>
sceneTypes = {
{MacroActionSwitchScene::SceneType::PROGRAM,
"AdvSceneSwitcher.action.scene.type.program"},
{MacroActionSwitchScene::SceneType::PREVIEW,
"AdvSceneSwitcher.action.scene.type.preview"},
};
static void waitForTransitionChange(OBSWeakSource &transition,
std::unique_lock<std::mutex> *lock,
@@ -126,6 +134,14 @@ bool MacroActionSwitchScene::WaitForTransition(OBSWeakSource &scene,
bool MacroActionSwitchScene::PerformAction()
{
auto scene = _scene.GetScene();
if (_sceneType == SceneType::PREVIEW) {
OBSSourceAutoRelease previewScneSource =
obs_weak_source_get_source(scene);
obs_frontend_set_current_preview_scene(previewScneSource);
return true;
}
auto transition = _transition.GetTransition();
SwitchScene({scene, transition, (int)(_duration.Milliseconds())},
obs_frontend_preview_program_mode_active());
@@ -137,24 +153,9 @@ bool MacroActionSwitchScene::PerformAction()
void MacroActionSwitchScene::LogAction() const
{
auto t = _scene.GetType();
auto sceneName = GetWeakSourceName(_scene.GetScene(false));
switch (t) {
case SceneSelection::Type::SCENE:
vblog(LOG_INFO, "switch to scene '%s'",
_scene.ToString(true).c_str());
break;
case SceneSelection::Type::GROUP:
vblog(LOG_INFO, "switch to scene '%s' (scene group '%s')",
sceneName.c_str(), _scene.ToString(true).c_str());
break;
case SceneSelection::Type::PREVIOUS:
vblog(LOG_INFO, "switch to previous scene '%s'",
sceneName.c_str());
break;
default:
break;
}
vblog(LOG_INFO, "switch%s scene to '%s'",
_sceneType == SceneType::PREVIEW ? " preview" : "",
_scene.ToString(true).c_str());
}
bool MacroActionSwitchScene::Save(obs_data_t *obj) const
@@ -165,6 +166,7 @@ bool MacroActionSwitchScene::Save(obs_data_t *obj) const
_duration.Save(obj);
obs_data_set_bool(obj, "blockUntilTransitionDone",
_blockUntilTransitionDone);
obs_data_set_int(obj, "sceneType", static_cast<int>(_sceneType));
return true;
}
@@ -176,6 +178,7 @@ bool MacroActionSwitchScene::Load(obs_data_t *obj)
_duration.Load(obj);
_blockUntilTransitionDone =
obs_data_get_bool(obj, "blockUntilTransitionDone");
_sceneType = static_cast<SceneType>(obs_data_get_int(obj, "sceneType"));
return true;
}
@@ -184,6 +187,13 @@ std::string MacroActionSwitchScene::GetShortDesc() const
return _scene.ToString();
}
static inline void populateTypeSelection(QComboBox *list)
{
for (const auto &[_, name] : sceneTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
MacroActionSwitchSceneEdit::MacroActionSwitchSceneEdit(
QWidget *parent, std::shared_ptr<MacroActionSwitchScene> entryData)
: QWidget(parent),
@@ -192,9 +202,11 @@ MacroActionSwitchSceneEdit::MacroActionSwitchSceneEdit(
_duration(new DurationSelection(parent, false)),
_blockUntilTransitionDone(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.scene.blockUntilTransitionDone"))),
_sceneTypes(new QComboBox()),
_entryLayout(new QHBoxLayout())
{
_duration->SpinBox()->setSpecialValueText("-");
populateTypeSelection(_sceneTypes);
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
@@ -206,28 +218,29 @@ MacroActionSwitchSceneEdit::MacroActionSwitchSceneEdit(
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(_blockUntilTransitionDone, SIGNAL(stateChanged(int)),
this, SLOT(BlockUntilTransitionDoneChanged(int)));
QWidget::connect(_sceneTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(SceneTypeChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", _scenes},
{"{{transitions}}", _transitions},
{"{{duration}}", _duration},
{"{{blockUntilTransitionDone}}", _blockUntilTransitionDone},
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.scene.entry"),
_entryLayout, widgetPlaceholders);
_entryLayout,
{{"{{scenes}}", _scenes},
{"{{transitions}}", _transitions},
{"{{duration}}", _duration},
{"{{sceneTypes}}", _sceneTypes}});
QVBoxLayout *mainLayout = new QVBoxLayout;
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(_entryLayout);
mainLayout->addWidget(_blockUntilTransitionDone);
setLayout(mainLayout);
_entryData = entryData;
_sceneTypes->setCurrentIndex(static_cast<int>(_entryData->_sceneType));
_scenes->SetScene(_entryData->_scene);
_transitions->SetTransition(_entryData->_transition);
_duration->SetDuration(_entryData->_duration);
_blockUntilTransitionDone->setChecked(
_entryData->_blockUntilTransitionDone);
SetDurationVisibility();
SetWidgetVisibility();
_loading = false;
}
@@ -251,8 +264,44 @@ void MacroActionSwitchSceneEdit::BlockUntilTransitionDoneChanged(int state)
_entryData->_blockUntilTransitionDone = state;
}
void MacroActionSwitchSceneEdit::SetDurationVisibility()
void MacroActionSwitchSceneEdit::SceneTypeChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_sceneType =
static_cast<MacroActionSwitchScene::SceneType>(value);
SetWidgetVisibility();
}
void MacroActionSwitchSceneEdit::SetWidgetVisibility()
{
_entryLayout->removeWidget(_scenes);
_entryLayout->removeWidget(_transitions);
_entryLayout->removeWidget(_duration);
_entryLayout->removeWidget(_sceneTypes);
ClearLayout(_entryLayout);
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", _scenes},
{"{{transitions}}", _transitions},
{"{{duration}}", _duration},
{"{{sceneTypes}}", _sceneTypes},
};
if (_entryData->_sceneType ==
MacroActionSwitchScene::SceneType::PREVIEW) {
_transitions->hide();
_duration->hide();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.scene.entry.preview"),
_entryLayout, widgetPlaceholders);
return;
}
_transitions->show();
if (_entryData->_transition.GetType() !=
TransitionSelection::Type::TRANSITION) {
_duration->show();
@@ -261,15 +310,6 @@ void MacroActionSwitchSceneEdit::SetDurationVisibility()
_entryData->_transition.GetTransition());
_duration->setVisible(!fixedDuration);
_entryLayout->removeWidget(_scenes);
_entryLayout->removeWidget(_transitions);
_entryLayout->removeWidget(_duration);
ClearLayout(_entryLayout);
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", _scenes},
{"{{transitions}}", _transitions},
{"{{duration}}", _duration},
};
if (fixedDuration) {
PlaceWidgets(
obs_module_text(
@@ -302,7 +342,7 @@ void MacroActionSwitchSceneEdit::TransitionChanged(const TransitionSelection &t)
auto lock = LockContext();
_entryData->_transition = t;
SetDurationVisibility();
SetWidgetVisibility();
}
} // namespace advss

View File

@@ -22,6 +22,9 @@ public:
{
return std::make_shared<MacroActionSwitchScene>(m);
}
enum class SceneType { PROGRAM, PREVIEW };
SceneType _sceneType = SceneType::PROGRAM;
SceneSelection _scene;
TransitionSelection _transition;
Duration _duration;
@@ -55,20 +58,21 @@ private slots:
void TransitionChanged(const TransitionSelection &);
void DurationChanged(const Duration &seconds);
void BlockUntilTransitionDoneChanged(int state);
void SceneTypeChanged(int);
signals:
void HeaderInfoChanged(const QString &);
protected:
private:
void SetWidgetVisibility();
SceneSelectionWidget *_scenes;
TransitionSelectionWidget *_transitions;
DurationSelection *_duration;
QCheckBox *_blockUntilTransitionDone;
QComboBox *_sceneTypes;
QHBoxLayout *_entryLayout;
std::shared_ptr<MacroActionSwitchScene> _entryData;
private:
void SetDurationVisibility();
bool _loading = true;
};

View File

@@ -66,7 +66,7 @@ bool MacroActionSequence::PerformAction()
return true;
}
return macro->PerformActions();
return macro->PerformActions(true);
}
void MacroActionSequence::LogAction() const

View File

@@ -27,6 +27,8 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.source.type.deinterlaceMode"},
{MacroActionSource::Action::DEINTERLACE_FIELD_ORDER,
"AdvSceneSwitcher.action.source.type.deinterlaceOrder"},
{MacroActionSource::Action::OPEN_INTERACTION_DIALOG,
"AdvSceneSwitcher.action.source.type.openInteractionDialog"},
};
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
@@ -110,6 +112,12 @@ static void refreshSourceSettings(obs_source_t *s)
}
}
static bool isInteractable(obs_source_t *source)
{
uint32_t flags = obs_source_get_output_flags(source);
return (flags & OBS_SOURCE_INTERACTION) != 0;
}
bool MacroActionSource::PerformAction()
{
auto s = obs_weak_source_get_source(_source.GetSource());
@@ -135,6 +143,16 @@ bool MacroActionSource::PerformAction()
case Action::DEINTERLACE_FIELD_ORDER:
obs_source_set_deinterlace_field_order(s, _deinterlaceOrder);
break;
case Action::OPEN_INTERACTION_DIALOG:
if (isInteractable(s)) {
obs_frontend_open_source_interaction(s);
} else {
blog(LOG_INFO,
"refusing to open interaction dialog "
"for non intractable source \"%s\"",
_source.ToString().c_str());
}
break;
default:
break;
}

View File

@@ -48,6 +48,7 @@ public:
SETTINGS_BUTTON,
DEINTERLACE_MODE,
DEINTERLACE_FIELD_ORDER,
OPEN_INTERACTION_DIALOG,
};
Action _action = Action::ENABLE;

View File

@@ -10,14 +10,14 @@ bool MacroActionSudioMode::_registered = MacroActionFactory::Register(
{MacroActionSudioMode::Create, MacroActionSudioModeEdit::Create,
"AdvSceneSwitcher.action.studioMode"});
const static std::map<StudioModeAction, std::string> actionTypes = {
{StudioModeAction::SWAP_SCENE,
const static std::map<MacroActionSudioMode::Action, std::string> actionTypes = {
{MacroActionSudioMode::Action::SWAP_SCENE,
"AdvSceneSwitcher.action.studioMode.type.swap"},
{StudioModeAction::SET_SCENE,
{MacroActionSudioMode::Action::SET_SCENE,
"AdvSceneSwitcher.action.studioMode.type.setScene"},
{StudioModeAction::ENABLE_STUDIO_MODE,
{MacroActionSudioMode::Action::ENABLE_STUDIO_MODE,
"AdvSceneSwitcher.action.studioMode.type.enable"},
{StudioModeAction::DISABLE_STUDIO_MODE,
{MacroActionSudioMode::Action::DISABLE_STUDIO_MODE,
"AdvSceneSwitcher.action.studioMode.type.disable"},
};
@@ -41,19 +41,19 @@ static void enableStudioMode(bool enable)
bool MacroActionSudioMode::PerformAction()
{
switch (_action) {
case StudioModeAction::SWAP_SCENE:
case Action::SWAP_SCENE:
obs_frontend_preview_program_trigger_transition();
break;
case StudioModeAction::SET_SCENE: {
case Action::SET_SCENE: {
auto s = obs_weak_source_get_source(_scene.GetScene());
obs_frontend_set_current_preview_scene(s);
obs_source_release(s);
break;
}
case StudioModeAction::ENABLE_STUDIO_MODE:
case Action::ENABLE_STUDIO_MODE:
enableStudioMode(true);
break;
case StudioModeAction::DISABLE_STUDIO_MODE:
case Action::DISABLE_STUDIO_MODE:
enableStudioMode(false);
break;
default:
@@ -86,15 +86,14 @@ bool MacroActionSudioMode::Save(obs_data_t *obj) const
bool MacroActionSudioMode::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action =
static_cast<StudioModeAction>(obs_data_get_int(obj, "action"));
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_scene.Load(obj);
return true;
}
std::string MacroActionSudioMode::GetShortDesc() const
{
if (_action == StudioModeAction::SET_SCENE) {
if (_action == Action::SET_SCENE) {
return _scene.ToString();
}
return "";
@@ -102,8 +101,9 @@ std::string MacroActionSudioMode::GetShortDesc() const
static inline void populateActionSelection(QComboBox *list)
{
for (auto entry : actionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[id, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(id));
}
}
@@ -139,9 +139,10 @@ void MacroActionSudioModeEdit::UpdateEntryData()
if (!_entryData) {
return;
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_scenes->SetScene(_entryData->_scene);
_scenes->setVisible(_entryData->_action == StudioModeAction::SET_SCENE);
SetWidgetVisibility();
}
void MacroActionSudioModeEdit::SceneChanged(const SceneSelection &s)
@@ -156,15 +157,27 @@ void MacroActionSudioModeEdit::SceneChanged(const SceneSelection &s)
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionSudioModeEdit::ActionChanged(int value)
void MacroActionSudioModeEdit::SetWidgetVisibility()
{
_scenes->setVisible(_entryData->_action ==
MacroActionSudioMode::Action::SET_SCENE);
if (_entryData->_action != MacroActionSudioMode::Action::SET_SCENE) {
_actions->removeItem(_actions->findData(static_cast<int>(
MacroActionSudioMode::Action::SET_SCENE)));
}
}
void MacroActionSudioModeEdit::ActionChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<StudioModeAction>(value);
_scenes->setVisible(_entryData->_action == StudioModeAction::SET_SCENE);
_entryData->_action = static_cast<MacroActionSudioMode::Action>(
_actions->itemData(index).toInt());
SetWidgetVisibility();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}

View File

@@ -4,13 +4,6 @@
namespace advss {
enum class StudioModeAction {
SWAP_SCENE,
SET_SCENE,
ENABLE_STUDIO_MODE,
DISABLE_STUDIO_MODE,
};
class MacroActionSudioMode : public MacroAction {
public:
MacroActionSudioMode(Macro *m) : MacroAction(m) {}
@@ -25,7 +18,14 @@ public:
return std::make_shared<MacroActionSudioMode>(m);
}
StudioModeAction _action = StudioModeAction::SWAP_SCENE;
enum class Action {
SWAP_SCENE,
SET_SCENE, // TODO: Remove in future version as the
// functionality moved to the scene switch action
ENABLE_STUDIO_MODE,
DISABLE_STUDIO_MODE,
};
Action _action = Action::SWAP_SCENE;
SceneSelection _scene;
private:
@@ -55,12 +55,13 @@ private slots:
signals:
void HeaderInfoChanged(const QString &);
protected:
private:
void SetWidgetVisibility();
QComboBox *_actions;
SceneSelectionWidget *_scenes;
std::shared_ptr<MacroActionSudioMode> _entryData;
private:
bool _loading = true;
};

View File

@@ -41,6 +41,10 @@ const static std::map<MacroActionVariable::Type, std::string> actionTypes = {
"AdvSceneSwitcher.action.variable.type.mathExpression"},
{MacroActionVariable::Type::USER_INPUT,
"AdvSceneSwitcher.action.variable.type.askForValue"},
{MacroActionVariable::Type::ENV_VARIABLE,
"AdvSceneSwitcher.action.variable.type.environmentVariable"},
{MacroActionVariable::Type::SCENE_ITEM_COUNT,
"AdvSceneSwitcher.action.variable.type.sceneItemCount"},
};
static void apppend(Variable &var, const std::string &value)
@@ -126,6 +130,7 @@ void MacroActionVariable::HandleMathExpression(Variable *var)
struct AskForInputParams {
QString prompt;
QString placeholder;
std::optional<std::string> result;
};
@@ -134,6 +139,7 @@ static void askForInput(void *param)
auto parameters = static_cast<AskForInputParams *>(param);
auto dialog = new NonModalMessageDialog(
parameters->prompt, NonModalMessageDialog::Type::INPUT);
dialog->SetInput(parameters->placeholder);
parameters->result = dialog->GetInput();
}
@@ -213,6 +219,9 @@ bool MacroActionVariable::PerformAction()
"AdvSceneSwitcher.action.variable.askForValuePromptDefault"))
.arg(QString::fromStdString(
var->Name())),
_useCustomPrompt && _useInputPlaceholder
? QString::fromStdString(_inputPlaceholder)
: "",
{}};
obs_queue_task(OBS_TASK_UI, askForInput, &params, true);
if (!params.result.has_value()) {
@@ -221,6 +230,14 @@ bool MacroActionVariable::PerformAction()
var->SetValue(*params.result);
return true;
}
case Type::ENV_VARIABLE: {
var->SetValue(std::getenv(_envVariableName.c_str()));
return true;
}
case Type::SCENE_ITEM_COUNT: {
var->SetValue(GetSceneItemCount(_scene.GetScene(false)));
return true;
}
}
return true;
@@ -247,6 +264,10 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
_mathExpression.Save(obj, "mathExpression");
obs_data_set_bool(obj, "useCustomPrompt", _useCustomPrompt);
_inputPrompt.Save(obj, "inputPrompt");
obs_data_set_bool(obj, "useInputPlaceholder", _useInputPlaceholder);
_inputPlaceholder.Save(obj, "inputPlaceholder");
_envVariableName.Save(obj, "environmentVariableName");
_scene.Save(obj);
return true;
}
@@ -271,6 +292,10 @@ bool MacroActionVariable::Load(obs_data_t *obj)
_mathExpression.Load(obj, "mathExpression");
_useCustomPrompt = obs_data_get_bool(obj, "useCustomPrompt");
_inputPrompt.Load(obj, "inputPrompt");
_useInputPlaceholder = obs_data_get_bool(obj, "useInputPlaceholder");
_inputPlaceholder.Load(obj, "inputPlaceholder");
_envVariableName.Load(obj, "environmentVariableName");
_scene.Load(obj);
return true;
}
@@ -392,7 +417,12 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_mathExpressionResult(new QLabel()),
_promptLayout(new QHBoxLayout()),
_useCustomPrompt(new QCheckBox()),
_inputPrompt(new VariableLineEdit(this))
_inputPrompt(new VariableLineEdit(this)),
_placeholderLayout(new QHBoxLayout()),
_useInputPlaceholder(new QCheckBox()),
_inputPlaceholder(new VariableLineEdit(this)),
_envVariable(new VariableLineEdit(this)),
_scenes(new SceneSelectionWidget(this, true, false, true, true, true))
{
_numValue->setMinimum(-9999999999);
_numValue->setMaximum(9999999999);
@@ -447,6 +477,14 @@ MacroActionVariableEdit::MacroActionVariableEdit(
SLOT(UseCustomPromptChanged(int)));
QWidget::connect(_inputPrompt, SIGNAL(editingFinished()), this,
SLOT(InputPromptChanged()));
QWidget::connect(_useInputPlaceholder, SIGNAL(stateChanged(int)), this,
SLOT(UseInputPlaceholderChanged(int)));
QWidget::connect(_inputPlaceholder, SIGNAL(editingFinished()), this,
SLOT(InputPlaceholderChanged()));
QWidget::connect(_envVariable, SIGNAL(editingFinished()), this,
SLOT(EnvVariableChanged()));
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{variables}}", _variables},
@@ -463,6 +501,10 @@ MacroActionVariableEdit::MacroActionVariableEdit(
{"{{mathExpression}}", _mathExpression},
{"{{useCustomPrompt}}", _useCustomPrompt},
{"{{inputPrompt}}", _inputPrompt},
{"{{useInputPlaceholder}}", _useInputPlaceholder},
{"{{inputPlaceholder}}", _inputPlaceholder},
{"{{envVariableName}}", _envVariable},
{"{{scenes}}", _scenes},
};
auto entryLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.variable.entry"),
@@ -485,8 +527,12 @@ MacroActionVariableEdit::MacroActionVariableEdit(
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.variable.entry.userInput"),
"AdvSceneSwitcher.action.variable.entry.userInput.customPrompt"),
_promptLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.variable.entry.userInput.placeholder"),
_placeholderLayout, widgetPlaceholders);
auto regexConfigLayout = new QHBoxLayout;
regexConfigLayout->addWidget(_regex);
@@ -505,6 +551,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
layout->addLayout(_findReplaceLayout);
layout->addWidget(_mathExpressionResult);
layout->addLayout(_promptLayout);
layout->addLayout(_placeholderLayout);
setLayout(layout);
_entryData = entryData;
@@ -547,6 +594,10 @@ void MacroActionVariableEdit::UpdateEntryData()
_mathExpression->setText(_entryData->_mathExpression);
_useCustomPrompt->setChecked(_entryData->_useCustomPrompt);
_inputPrompt->setText(_entryData->_inputPrompt);
_useInputPlaceholder->setChecked(_entryData->_useInputPlaceholder);
_inputPlaceholder->setText(_entryData->_inputPlaceholder);
_envVariable->setText(_entryData->_envVariableName);
_scenes->SetScene(_entryData->_scene);
SetWidgetVisibility();
}
@@ -818,15 +869,9 @@ void MacroActionVariableEdit::UseCustomPromptChanged(int value)
return;
}
_inputPrompt->setVisible(value);
if (value) {
RemoveStretchIfPresent(_promptLayout);
} else {
AddStretchIfNecessary(_promptLayout);
}
auto lock = LockContext();
_entryData->_useCustomPrompt = value;
SetWidgetVisibility();
}
void MacroActionVariableEdit::InputPromptChanged()
@@ -839,6 +884,47 @@ void MacroActionVariableEdit::InputPromptChanged()
_entryData->_inputPrompt = _inputPrompt->text().toStdString();
}
void MacroActionVariableEdit::UseInputPlaceholderChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_useInputPlaceholder = value;
SetWidgetVisibility();
}
void MacroActionVariableEdit::InputPlaceholderChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_inputPlaceholder = _inputPlaceholder->text().toStdString();
}
void MacroActionVariableEdit::EnvVariableChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_envVariableName = _envVariable->text().toStdString();
}
void MacroActionVariableEdit::SceneChanged(const SceneSelection &scene)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_scene = scene;
}
void MacroActionVariableEdit::SetWidgetVisibility()
{
if (!_entryData) {
@@ -887,7 +973,34 @@ void MacroActionVariableEdit::SetWidgetVisibility()
SetLayoutVisible(_promptLayout,
_entryData->_type ==
MacroActionVariable::Type::USER_INPUT);
_inputPrompt->setVisible(_entryData->_useCustomPrompt);
_inputPrompt->setVisible(
_entryData->_type == MacroActionVariable::Type::USER_INPUT &&
_entryData->_useCustomPrompt);
if (_entryData->_useCustomPrompt) {
RemoveStretchIfPresent(_promptLayout);
} else {
AddStretchIfNecessary(_promptLayout);
}
SetLayoutVisible(
_placeholderLayout,
_entryData->_type == MacroActionVariable::Type::USER_INPUT &&
_entryData->_useCustomPrompt);
_useInputPlaceholder->setVisible(
_entryData->_type == MacroActionVariable::Type::USER_INPUT &&
_entryData->_useCustomPrompt);
_inputPlaceholder->setVisible(
_entryData->_type == MacroActionVariable::Type::USER_INPUT &&
_entryData->_useCustomPrompt &&
_entryData->_useInputPlaceholder);
if (_entryData->_useInputPlaceholder) {
RemoveStretchIfPresent(_placeholderLayout);
} else {
AddStretchIfNecessary(_placeholderLayout);
}
_envVariable->setVisible(_entryData->_type ==
MacroActionVariable::Type::ENV_VARIABLE);
_scenes->setVisible(_entryData->_type ==
MacroActionVariable::Type::SCENE_ITEM_COUNT);
adjustSize();
updateGeometry();
}

View File

@@ -3,6 +3,7 @@
#include "macro-segment-selection.hpp"
#include "regex-config.hpp"
#include "resizing-text-edit.hpp"
#include "scene-selection.hpp"
#include "variable-line-edit.hpp"
namespace advss {
@@ -37,6 +38,8 @@ public:
FIND_AND_REPLACE,
MATH_EXPRESSION,
USER_INPUT,
ENV_VARIABLE,
SCENE_ITEM_COUNT,
};
Type _type = Type::SET_FIXED_VALUE;
@@ -58,6 +61,15 @@ public:
bool _useCustomPrompt = false;
StringVariable _inputPrompt = obs_module_text(
"AdvSceneSwitcher.action.variable.askForValuePrompt");
bool _useInputPlaceholder = false;
StringVariable _inputPlaceholder =
obs_module_text("AdvSceneSwitcher.enterText");
#ifdef _WIN32
StringVariable _envVariableName = "USERPROFILE";
#else
StringVariable _envVariableName = "HOME";
#endif
SceneSelection _scene;
private:
void DecrementCurrentSegmentVariableRef();
@@ -107,11 +119,18 @@ private slots:
void MathExpressionChanged();
void UseCustomPromptChanged(int);
void InputPromptChanged();
void UseInputPlaceholderChanged(int);
void InputPlaceholderChanged();
void EnvVariableChanged();
void SceneChanged(const SceneSelection &);
signals:
void HeaderInfoChanged(const QString &);
protected:
private:
void SetWidgetVisibility();
void SetSegmentValueError(const QString &);
VariableSelection *_variables;
VariableSelection *_variables2;
QComboBox *_actions;
@@ -136,12 +155,13 @@ protected:
QHBoxLayout *_promptLayout;
QCheckBox *_useCustomPrompt;
VariableLineEdit *_inputPrompt;
QHBoxLayout *_placeholderLayout;
QCheckBox *_useInputPlaceholder;
VariableLineEdit *_inputPlaceholder;
VariableLineEdit *_envVariable;
SceneSelectionWidget *_scenes;
std::shared_ptr<MacroActionVariable> _entryData;
private:
void SetWidgetVisibility();
void SetSegmentValueError(const QString &);
QTimer _timer;
bool _loading = true;
};

View File

@@ -385,7 +385,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
}
(*cond)->SetLogicType(logic);
macro->UpdateConditionIndices();
conditionsList->Insert(
ui->conditionsList->Insert(
idx,
new MacroConditionEdit(this, &macro->Conditions()[idx],
id, idx == 0));
@@ -410,7 +410,7 @@ void AdvSceneSwitcher::on_conditionAdd_clicked()
if (currentConditionIdx != -1) {
MacroConditionSelectionChanged(currentConditionIdx + 1);
}
conditionsList->SetHelpMsgVisible(false);
ui->conditionsList->SetHelpMsgVisible(false);
}
void AdvSceneSwitcher::RemoveMacroCondition(int idx)
@@ -426,14 +426,14 @@ void AdvSceneSwitcher::RemoveMacroCondition(int idx)
{
auto lock = LockContext();
conditionsList->Remove(idx);
ui->conditionsList->Remove(idx);
macro->Conditions().erase(macro->Conditions().begin() + idx);
macro->UpdateConditionIndices();
if (idx == 0 && macro->Conditions().size() > 0) {
auto newRoot = macro->Conditions().at(0);
newRoot->SetLogicType(LogicType::ROOT_NONE);
static_cast<MacroConditionEdit *>(
conditionsList->WidgetAt(0))
ui->conditionsList->WidgetAt(0))
->SetRootNode(true);
}
SetConditionData(*macro);
@@ -479,7 +479,7 @@ void AdvSceneSwitcher::on_conditionDown_clicked()
{
if (currentConditionIdx == -1 ||
currentConditionIdx ==
conditionsList->ContentLayout()->count() - 1) {
ui->conditionsList->ContentLayout()->count() - 1) {
return;
}
MoveMacroConditionDown(currentConditionIdx);
@@ -491,7 +491,7 @@ void AdvSceneSwitcher::on_conditionBottom_clicked()
if (currentConditionIdx == -1) {
return;
}
const int newIdx = conditionsList->ContentLayout()->count() - 1;
const int newIdx = ui->conditionsList->ContentLayout()->count() - 1;
MacroConditionReorder(newIdx, currentConditionIdx);
MacroConditionSelectionChanged(newIdx);
}
@@ -521,11 +521,11 @@ void AdvSceneSwitcher::SwapConditions(Macro *m, int pos1, int pos2)
}
auto widget1 = static_cast<MacroConditionEdit *>(
conditionsList->ContentLayout()->takeAt(pos1)->widget());
ui->conditionsList->ContentLayout()->takeAt(pos1)->widget());
auto widget2 = static_cast<MacroConditionEdit *>(
conditionsList->ContentLayout()->takeAt(pos2 - 1)->widget());
conditionsList->Insert(pos1, widget2);
conditionsList->Insert(pos2, widget1);
ui->conditionsList->ContentLayout()->takeAt(pos2 - 1)->widget());
ui->conditionsList->Insert(pos1, widget2);
ui->conditionsList->Insert(pos2, widget1);
SetConditionData(*m);
widget2->SetRootNode(root);
widget1->SetRootNode(false);
@@ -564,22 +564,7 @@ void AdvSceneSwitcher::MoveMacroConditionDown(int idx)
void AdvSceneSwitcher::MacroConditionSelectionChanged(int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
conditionsList->SetSelection(idx);
actionsList->SetSelection(-1);
if (idx < 0 || (unsigned)idx >= macro->Conditions().size()) {
currentConditionIdx = -1;
} else {
currentConditionIdx = idx;
lastInteracted = MacroSection::CONDITIONS;
}
currentActionIdx = -1;
HighlightControls();
SetupMacroSegmentSelection(MacroSection::CONDITIONS, idx);
}
void AdvSceneSwitcher::MacroConditionReorder(int to, int from)
@@ -599,30 +584,30 @@ void AdvSceneSwitcher::MacroConditionReorder(int to, int from)
if (to == 0) {
condition->SetLogicType(LogicType::ROOT_NONE);
static_cast<MacroConditionEdit *>(
conditionsList->WidgetAt(from))
ui->conditionsList->WidgetAt(from))
->SetRootNode(true);
macro->Conditions().at(0)->SetLogicType(LogicType::AND);
static_cast<MacroConditionEdit *>(
conditionsList->WidgetAt(0))
ui->conditionsList->WidgetAt(0))
->SetRootNode(false);
}
if (from == 0) {
condition->SetLogicType(LogicType::AND);
static_cast<MacroConditionEdit *>(
conditionsList->WidgetAt(from))
ui->conditionsList->WidgetAt(from))
->SetRootNode(false);
macro->Conditions().at(1)->SetLogicType(
LogicType::ROOT_NONE);
static_cast<MacroConditionEdit *>(
conditionsList->WidgetAt(1))
ui->conditionsList->WidgetAt(1))
->SetRootNode(true);
}
macro->Conditions().erase(macro->Conditions().begin() + from);
macro->Conditions().insert(macro->Conditions().begin() + to,
condition);
macro->UpdateConditionIndices();
conditionsList->ContentLayout()->insertItem(
to, conditionsList->ContentLayout()->takeAt(from));
ui->conditionsList->ContentLayout()->insertItem(
to, ui->conditionsList->ContentLayout()->takeAt(from));
SetConditionData(*macro);
}
HighlightCondition(to);

View File

@@ -18,8 +18,10 @@ const static std::map<MacroConditionFilter::Condition, std::string>
"AdvSceneSwitcher.condition.filter.type.active"},
{MacroConditionFilter::Condition::DISABLED,
"AdvSceneSwitcher.condition.filter.type.showing"},
{MacroConditionFilter::Condition::SETTINGS,
{MacroConditionFilter::Condition::SETTINGS_MATCH,
"AdvSceneSwitcher.condition.filter.type.settings"},
{MacroConditionFilter::Condition::SETTINGS_CHANGED,
"AdvSceneSwitcher.condition.filter.type.settingsChanged"},
};
bool MacroConditionFilter::CheckCondition()
@@ -38,13 +40,20 @@ bool MacroConditionFilter::CheckCondition()
case Condition::DISABLED:
ret = !obs_source_enabled(filterSource);
break;
case Condition::SETTINGS:
case Condition::SETTINGS_MATCH:
ret = CompareSourceSettings(filterWeakSource, _settings,
_regex);
if (IsReferencedInVars()) {
SetVariableValue(GetSourceSettings(filterWeakSource));
}
break;
case Condition::SETTINGS_CHANGED: {
std::string settings = GetSourceSettings(_source.GetSource());
ret = !_currentSettings.empty() && settings != _currentSettings;
_currentSettings = settings;
SetVariableValue(settings);
break;
}
default:
break;
}
@@ -95,8 +104,8 @@ std::string MacroConditionFilter::GetShortDesc() const
static inline void populateConditionSelection(QComboBox *list)
{
for (auto entry : filterConditionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[_, name] : filterConditionTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
@@ -194,8 +203,9 @@ void MacroConditionFilterEdit::ConditionChanged(int index)
auto lock = LockContext();
_entryData->_condition =
static_cast<MacroConditionFilter::Condition>(index);
SetSettingsSelectionVisible(_entryData->_condition ==
MacroConditionFilter::Condition::SETTINGS);
SetSettingsSelectionVisible(
_entryData->_condition ==
MacroConditionFilter::Condition::SETTINGS_MATCH);
}
void MacroConditionFilterEdit::GetSettingsClicked()
@@ -259,8 +269,9 @@ void MacroConditionFilterEdit::UpdateEntryData()
_conditions->setCurrentIndex(static_cast<int>(_entryData->_condition));
_settings->setPlainText(_entryData->_settings);
_regex->SetRegexConfig(_entryData->_regex);
SetSettingsSelectionVisible(_entryData->_condition ==
MacroConditionFilter::Condition::SETTINGS);
SetSettingsSelectionVisible(
_entryData->_condition ==
MacroConditionFilter::Condition::SETTINGS_MATCH);
adjustSize();
updateGeometry();

View File

@@ -27,7 +27,8 @@ public:
enum class Condition {
ENABLED,
DISABLED,
SETTINGS,
SETTINGS_MATCH,
SETTINGS_CHANGED,
};
SourceSelection _source;
@@ -37,6 +38,8 @@ public:
RegexConfig _regex;
private:
std::string _currentSettings;
static bool _registered;
static const std::string id;
};

View File

@@ -3,6 +3,7 @@
#include "process-config.hpp"
#include "duration-control.hpp"
#include <thread>
#include <QCheckBox>
#include <QSpinBox>

View File

@@ -16,6 +16,8 @@ const static std::map<MacroConditionScene::Type, std::string> sceneTypes = {
"AdvSceneSwitcher.condition.scene.type.current"},
{MacroConditionScene::Type::PREVIOUS,
"AdvSceneSwitcher.condition.scene.type.previous"},
{MacroConditionScene::Type::PREVIEW,
"AdvSceneSwitcher.condition.scene.type.preview"},
{MacroConditionScene::Type::CHANGED,
"AdvSceneSwitcher.condition.scene.type.changed"},
{MacroConditionScene::Type::NOT_CHANGED,
@@ -24,6 +26,8 @@ const static std::map<MacroConditionScene::Type, std::string> sceneTypes = {
"AdvSceneSwitcher.condition.scene.type.currentPattern"},
{MacroConditionScene::Type::PREVIOUS_PATTERN,
"AdvSceneSwitcher.condition.scene.type.previousPattern"},
{MacroConditionScene::Type::PREVIEW_PATTERN,
"AdvSceneSwitcher.condition.scene.type.previewPattern"},
};
static bool sceneNameMatchesRegex(const OBSWeakSource &scene,
@@ -79,6 +83,14 @@ bool MacroConditionScene::CheckCondition()
SetVariableValue(GetWeakSourceName(scene));
return scene == _scene.GetScene(false);
}
case Type::PREVIEW: {
OBSSourceAutoRelease source =
obs_frontend_get_current_preview_scene();
OBSWeakSourceAutoRelease scene =
obs_source_get_weak_source(source);
SetVariableValue(GetWeakSourceName(scene));
return scene == _scene.GetScene(false);
}
case Type::CHANGED:
SetVariableValue(GetWeakSourceName(switcher->currentScene));
return sceneChanged;
@@ -95,6 +107,14 @@ bool MacroConditionScene::CheckCondition()
SetVariableValue(GetWeakSourceName(scene));
return sceneNameMatchesRegex(scene, _pattern);
}
case Type::PREVIEW_PATTERN: {
OBSSourceAutoRelease source =
obs_frontend_get_current_preview_scene();
OBSWeakSourceAutoRelease scene =
obs_source_get_weak_source(source);
SetVariableValue(GetWeakSourceName(scene));
return sceneNameMatchesRegex(scene.Get(), _pattern);
}
}
return false;
@@ -108,6 +128,7 @@ bool MacroConditionScene::Save(obs_data_t *obj) const
obs_data_set_string(obj, "pattern", _pattern.c_str());
obs_data_set_bool(obj, "useTransitionTargetScene",
_useTransitionTargetScene);
obs_data_set_int(obj, "version", 1);
return true;
}
@@ -124,6 +145,46 @@ bool MacroConditionScene::Load(obs_data_t *obj)
_useTransitionTargetScene =
obs_data_get_bool(obj, "useTransitionTargetScene");
}
// TODO: Remove fallback in future version
if (!obs_data_has_user_value(obj, "version")) {
enum {
CURRENT,
PREVIOUS,
CHANGED,
NOT_CHANGED,
CURRENT_PATTERN,
PREVIOUS_PATTERN,
};
int oldType = obs_data_get_int(obj, "type");
switch (oldType) {
case CURRENT:
_type = Type::CURRENT;
break;
case PREVIOUS:
_type = Type::PREVIOUS;
break;
case CHANGED:
_type = Type::CHANGED;
break;
case NOT_CHANGED:
_type = Type::NOT_CHANGED;
break;
case CURRENT_PATTERN:
_type = Type::CURRENT_PATTERN;
break;
case PREVIOUS_PATTERN:
_type = Type::PREVIOUS_PATTERN;
break;
default:
blog(LOG_WARNING,
"failed to convert scene condition type (%d) for macro %s",
oldType, GetMacro()->Name().c_str());
_type = Type::CURRENT;
break;
}
}
return true;
}
@@ -137,8 +198,9 @@ std::string MacroConditionScene::GetShortDesc() const
static inline void populateTypeSelection(QComboBox *list)
{
for (auto entry : sceneTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[id, name] : sceneTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(id));
}
}
@@ -199,14 +261,15 @@ void MacroConditionSceneEdit::SceneChanged(const SceneSelection &s)
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroConditionSceneEdit::TypeChanged(int value)
void MacroConditionSceneEdit::TypeChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_type = static_cast<MacroConditionScene::Type>(value);
_entryData->_type = static_cast<MacroConditionScene::Type>(
_sceneType->itemData(index).toInt());
SetWidgetVisibility();
}
@@ -234,7 +297,8 @@ void MacroConditionSceneEdit::SetWidgetVisibility()
{
_scenes->setVisible(
_entryData->_type == MacroConditionScene::Type::CURRENT ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS);
_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
_entryData->_type == MacroConditionScene::Type::PREVIEW);
_useTransitionTargetScene->setVisible(
_entryData->_type == MacroConditionScene::Type::CURRENT ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
@@ -246,7 +310,9 @@ void MacroConditionSceneEdit::SetWidgetVisibility()
_entryData->_type ==
MacroConditionScene::Type::CURRENT_PATTERN ||
_entryData->_type ==
MacroConditionScene::Type::PREVIOUS_PATTERN);
MacroConditionScene::Type::PREVIOUS_PATTERN ||
_entryData->_type ==
MacroConditionScene::Type::PREVIEW_PATTERN);
if (_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS_PATTERN) {
@@ -259,6 +325,7 @@ void MacroConditionSceneEdit::SetWidgetVisibility()
"AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour"));
}
adjustSize();
updateGeometry();
}
void MacroConditionSceneEdit::UpdateEntryData()
@@ -268,7 +335,8 @@ void MacroConditionSceneEdit::UpdateEntryData()
}
_scenes->SetScene(_entryData->_scene);
_sceneType->setCurrentIndex(static_cast<int>(_entryData->_type));
_sceneType->setCurrentIndex(
_sceneType->findData(static_cast<int>(_entryData->_type)));
_pattern->setText(QString::fromStdString(_entryData->_pattern));
_useTransitionTargetScene->setChecked(
_entryData->_useTransitionTargetScene);

View File

@@ -23,12 +23,14 @@ public:
}
enum class Type {
CURRENT,
PREVIOUS,
CHANGED,
NOT_CHANGED,
CURRENT_PATTERN,
PREVIOUS_PATTERN,
CURRENT = 10,
PREVIOUS = 20,
PREVIEW = 30,
CHANGED = 40,
NOT_CHANGED = 50,
CURRENT_PATTERN = 60,
PREVIOUS_PATTERN = 70,
PREVIEW_PATTERN = 80,
};
SceneSelection _scene;

View File

@@ -6,10 +6,13 @@ namespace advss {
const std::string MacroConditionSlideshow::id = "slideshow";
bool MacroConditionSlideshow::_registered = MacroConditionFactory::Register(
MacroConditionSlideshow::id,
{MacroConditionSlideshow::Create, MacroConditionSlideshowEdit::Create,
"AdvSceneSwitcher.condition.slideshow"});
bool MacroConditionSlideshow::_registered =
obs_get_version() >= MAKE_SEMANTIC_VERSION(29, 1, 0) &&
MacroConditionFactory::Register(
MacroConditionSlideshow::id,
{MacroConditionSlideshow::Create,
MacroConditionSlideshowEdit::Create,
"AdvSceneSwitcher.condition.slideshow"});
static const std::map<MacroConditionSlideshow::Condition, std::string>
conditions = {
@@ -200,9 +203,9 @@ MacroConditionSlideshowEdit::MacroConditionSlideshowEdit(
QWidget *parent, std::shared_ptr<MacroConditionSlideshow> entryData)
: QWidget(parent),
_conditions(new QComboBox(this)),
_sources(new SourceSelectionWidget(this, QStringList(), true)),
_index(new VariableSpinBox(this)),
_path(new VariableLineEdit(this))
_path(new VariableLineEdit(this)),
_sources(new SourceSelectionWidget(this, QStringList(), true))
{
setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.slideshow.updateIntervalTooltip"));

View File

@@ -10,13 +10,16 @@ bool MacroConditionSource::_registered = MacroConditionFactory::Register(
{MacroConditionSource::Create, MacroConditionSourceEdit::Create,
"AdvSceneSwitcher.condition.source"});
const static std::map<SourceCondition, std::string> sourceConditionTypes = {
{SourceCondition::ACTIVE,
"AdvSceneSwitcher.condition.source.type.active"},
{SourceCondition::SHOWING,
"AdvSceneSwitcher.condition.source.type.showing"},
{SourceCondition::SETTINGS,
"AdvSceneSwitcher.condition.source.type.settings"},
const static std::map<MacroConditionSource::Condition, std::string>
sourceCnditionTypes = {
{MacroConditionSource::Condition::ACTIVE,
"AdvSceneSwitcher.condition.source.type.active"},
{MacroConditionSource::Condition::SHOWING,
"AdvSceneSwitcher.condition.source.type.showing"},
{MacroConditionSource::Condition::SETTINGS_MATCH,
"AdvSceneSwitcher.condition.source.type.settings"},
{MacroConditionSource::Condition::SETTINGS_CHANGED,
"AdvSceneSwitcher.condition.source.type.settingsChanged"},
};
bool MacroConditionSource::CheckCondition()
@@ -29,13 +32,13 @@ bool MacroConditionSource::CheckCondition()
auto s = obs_weak_source_get_source(_source.GetSource());
switch (_condition) {
case SourceCondition::ACTIVE:
case Condition::ACTIVE:
ret = obs_source_active(s);
break;
case SourceCondition::SHOWING:
case Condition::SHOWING:
ret = obs_source_showing(s);
break;
case SourceCondition::SETTINGS:
case Condition::SETTINGS_MATCH:
ret = CompareSourceSettings(_source.GetSource(), _settings,
_regex);
if (IsReferencedInVars()) {
@@ -43,6 +46,13 @@ bool MacroConditionSource::CheckCondition()
GetSourceSettings(_source.GetSource()));
}
break;
case Condition::SETTINGS_CHANGED: {
std::string settings = GetSourceSettings(_source.GetSource());
ret = !_currentSettings.empty() && settings != _currentSettings;
_currentSettings = settings;
SetVariableValue(settings);
break;
}
default:
break;
}
@@ -70,8 +80,7 @@ bool MacroConditionSource::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_source.Load(obj);
_condition = static_cast<SourceCondition>(
obs_data_get_int(obj, "condition"));
_condition = static_cast<Condition>(obs_data_get_int(obj, "condition"));
_settings.Load(obj, "settings");
_regex.Load(obj);
// TOOD: remove in future version
@@ -89,8 +98,8 @@ std::string MacroConditionSource::GetShortDesc() const
static inline void populateConditionSelection(QComboBox *list)
{
for (auto entry : sourceConditionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[_, name] : sourceCnditionTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
@@ -173,7 +182,8 @@ void MacroConditionSourceEdit::ConditionChanged(int index)
}
auto lock = LockContext();
_entryData->_condition = static_cast<SourceCondition>(index);
_entryData->_condition =
static_cast<MacroConditionSource::Condition>(index);
SetWidgetVisibility();
}
@@ -220,14 +230,18 @@ void MacroConditionSourceEdit::RegexChanged(RegexConfig conf)
void MacroConditionSourceEdit::SetWidgetVisibility()
{
_settings->setVisible(_entryData->_condition ==
SourceCondition::SETTINGS);
_getSettings->setVisible(_entryData->_condition ==
SourceCondition::SETTINGS);
_regex->setVisible(_entryData->_condition == SourceCondition::SETTINGS);
MacroConditionSource::Condition::SETTINGS_MATCH);
_getSettings->setVisible(
_entryData->_condition ==
MacroConditionSource::Condition::SETTINGS_MATCH);
_regex->setVisible(_entryData->_condition ==
MacroConditionSource::Condition::SETTINGS_MATCH);
setToolTip(
(_entryData->_condition == SourceCondition::ACTIVE ||
_entryData->_condition == SourceCondition::SHOWING)
(_entryData->_condition ==
MacroConditionSource::Condition::ACTIVE ||
_entryData->_condition ==
MacroConditionSource::Condition::SHOWING)
? obs_module_text(
"AdvSceneSwitcher.condition.source.sceneVisibilityHint")
: "");

View File

@@ -10,12 +10,6 @@
namespace advss {
enum class SourceCondition {
ACTIVE,
SHOWING,
SETTINGS,
};
class MacroConditionSource : public MacroCondition {
public:
MacroConditionSource(Macro *m) : MacroCondition(m, true) {}
@@ -29,12 +23,21 @@ public:
return std::make_shared<MacroConditionSource>(m);
}
enum class Condition {
ACTIVE,
SHOWING,
SETTINGS_MATCH,
SETTINGS_CHANGED,
};
SourceSelection _source;
SourceCondition _condition = SourceCondition::ACTIVE;
Condition _condition = Condition::ACTIVE;
StringVariable _settings = "";
RegexConfig _regex;
private:
std::string _currentSettings;
static bool _registered;
static const std::string id;
};

View File

@@ -155,6 +155,13 @@ void MacroConditionStudioModeEdit::SetWidgetVisibility()
_scenes->setVisible(_entryData->_condition ==
StudioModeCondition::PREVIEW_SCENE);
// TODO: Remove this workaround once the PREVIEW_SCENE condition type
// has been removed
if (_entryData->_condition != StudioModeCondition::PREVIEW_SCENE) {
_condition->removeItem(
static_cast<int>(StudioModeCondition::PREVIEW_SCENE));
}
}
} // namespace advss

View File

@@ -10,7 +10,8 @@ namespace advss {
enum class StudioModeCondition {
STUDIO_MODE_ACTIVE,
STUDIO_MODE_NOT_ACTIVE,
PREVIEW_SCENE,
PREVIEW_SCENE, // TODO: Remove in future version as the functionality
// moved to the scene condition
};
class MacroConditionStudioMode : public MacroCondition {

View File

@@ -6,7 +6,7 @@
namespace advss {
MacroDock::MacroDock(Macro *m, QWidget *parent,
MacroDock::MacroDock(std::weak_ptr<Macro> m, QWidget *parent,
const StringVariable &runButtonText,
const StringVariable &pauseButtonText,
const StringVariable &unpauseButtonText,
@@ -25,11 +25,12 @@ MacroDock::MacroDock(Macro *m, QWidget *parent,
_statusText(new QLabel(conditionsFalseText.c_str())),
_macro(m)
{
if (_macro) {
setWindowTitle(QString::fromStdString(_macro->Name()));
_run->setVisible(_macro->DockHasRunButton());
_pauseToggle->setVisible(_macro->DockHasPauseButton());
_statusText->setVisible(_macro->DockHasStatusLabel());
auto macro = _macro.lock();
if (macro) {
setWindowTitle(QString::fromStdString(macro->Name()));
_run->setVisible(macro->DockHasRunButton());
_pauseToggle->setVisible(macro->DockHasPauseButton());
_statusText->setVisible(macro->DockHasStatusLabel());
} else {
setWindowTitle("<deleted macro>");
}
@@ -120,49 +121,52 @@ void MacroDock::EnableHighlight(bool value)
void MacroDock::RunClicked()
{
if (!_macro) {
auto macro = _macro.lock();
if (!macro) {
return;
}
auto ret = _macro->PerformActions();
auto ret = macro->PerformActions(true);
if (!ret) {
QString err =
obs_module_text("AdvSceneSwitcher.macroTab.runFail");
DisplayMessage(err.arg(QString::fromStdString(_macro->Name())));
DisplayMessage(err.arg(QString::fromStdString(macro->Name())));
}
}
void MacroDock::PauseToggleClicked()
{
if (!_macro) {
auto macro = _macro.lock();
if (!macro) {
return;
}
_macro->SetPaused(!_macro->Paused());
macro->SetPaused(!macro->Paused());
UpdateText();
}
void MacroDock::UpdateText()
{
_run->setText(_runButtonText.c_str());
if (!_macro) {
auto macro = _macro.lock();
if (!macro) {
return;
}
_pauseToggle->setText(_macro->Paused() ? _unpauseButtonText.c_str()
: _pauseButtonText.c_str());
_statusText->setText(_macro->Matched() ? _conditionsTrueText.c_str()
: _conditionsFalseText.c_str());
_pauseToggle->setText(macro->Paused() ? _unpauseButtonText.c_str()
: _pauseButtonText.c_str());
_statusText->setText(macro->Matched() ? _conditionsTrueText.c_str()
: _conditionsFalseText.c_str());
}
void MacroDock::Highlight()
{
if (!_highlight || !_macro) {
auto macro = _macro.lock();
if (!_highlight || !macro) {
return;
}
if (_lastHighlightCheckTime.time_since_epoch().count() != 0 &&
_macro->ExecutedSince(_lastHighlightCheckTime)) {
macro->ExecutedSince(_lastHighlightCheckTime)) {
PulseWidget(this, Qt::green, QColor(0, 0, 0, 0), true);
}
_lastHighlightCheckTime = std::chrono::high_resolution_clock::now();

View File

@@ -16,7 +16,8 @@ class MacroDock : public OBSDock {
Q_OBJECT
public:
MacroDock(Macro *, QWidget *parent, const StringVariable &runButtonText,
MacroDock(std::weak_ptr<Macro>, QWidget *parent,
const StringVariable &runButtonText,
const StringVariable &pauseButtonText,
const StringVariable &unpauseButtonText,
const StringVariable &conditionsTrueText,
@@ -53,7 +54,7 @@ private:
QTimer _timer;
std::chrono::high_resolution_clock::time_point _lastHighlightCheckTime{};
Macro *_macro;
std::weak_ptr<Macro> _macro;
};
} // namespace advss

View File

@@ -253,6 +253,76 @@ void AdvSceneSwitcher::ExportMacros()
MacroExportImportDialog::ExportMacros(exportString);
}
static bool
isValidMacroSegmentIdx(const std::deque<std::shared_ptr<MacroSegment>> &list,
int idx)
{
return (idx > 0 || (unsigned)idx < list.size());
}
void AdvSceneSwitcher::SetupMacroSegmentSelection(MacroSection type, int idx)
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
MacroSegmentList *setList, *resetList1, *resetList2;
int *setIdx, *resetIdx1, *resetIdx2;
std::deque<std::shared_ptr<MacroSegment>> segements;
switch (type) {
case AdvSceneSwitcher::MacroSection::CONDITIONS:
setList = ui->conditionsList;
setIdx = &currentConditionIdx;
segements = {macro->Conditions().begin(),
macro->Conditions().end()};
resetList1 = ui->actionsList;
resetList2 = ui->elseActionsList;
resetIdx1 = &currentActionIdx;
resetIdx2 = &currentElseActionIdx;
break;
case AdvSceneSwitcher::MacroSection::ACTIONS:
setList = ui->actionsList;
setIdx = &currentActionIdx;
segements = {macro->Actions().begin(), macro->Actions().end()};
resetList1 = ui->conditionsList;
resetList2 = ui->elseActionsList;
resetIdx1 = &currentConditionIdx;
resetIdx2 = &currentElseActionIdx;
break;
case AdvSceneSwitcher::MacroSection::ELSE_ACTIONS:
setList = ui->elseActionsList;
setIdx = &currentElseActionIdx;
segements = {macro->ElseActions().begin(),
macro->ElseActions().end()};
resetList1 = ui->actionsList;
resetList2 = ui->conditionsList;
resetIdx1 = &currentActionIdx;
resetIdx2 = &currentConditionIdx;
break;
default:
break;
}
setList->SetSelection(idx);
resetList1->SetSelection(-1);
resetList2->SetSelection(-1);
if (isValidMacroSegmentIdx(segements, idx)) {
*setIdx = idx;
} else {
*setIdx = -1;
}
*resetIdx1 = -1;
*resetIdx2 = -1;
lastInteracted = type;
HighlightControls();
}
bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
std::shared_ptr<Macro> &macro)
{
@@ -381,21 +451,6 @@ void AdvSceneSwitcher::on_macroName_editingFinished()
RenameMacro(macro, newName);
}
void AdvSceneSwitcher::on_runMacro_clicked()
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
bool ret = macro->PerformActions(true, true);
if (!ret) {
QString err =
obs_module_text("AdvSceneSwitcher.macroTab.runFail");
DisplayMessage(err.arg(QString::fromStdString(macro->Name())));
}
}
void AdvSceneSwitcher::on_runMacroInParallel_stateChanged(int value)
{
auto macro = GetSelectedMacro();
@@ -422,9 +477,20 @@ void AdvSceneSwitcher::PopulateMacroActions(Macro &m, uint32_t afterIdx)
for (; afterIdx < actions.size(); afterIdx++) {
auto newEntry = new MacroActionEdit(this, &actions[afterIdx],
actions[afterIdx]->GetId());
actionsList->Add(newEntry);
ui->actionsList->Add(newEntry);
}
actionsList->SetHelpMsgVisible(actions.size() == 0);
ui->actionsList->SetHelpMsgVisible(actions.size() == 0);
}
void AdvSceneSwitcher::PopulateMacroElseActions(Macro &m, uint32_t afterIdx)
{
auto &actions = m.ElseActions();
for (; afterIdx < actions.size(); afterIdx++) {
auto newEntry = new MacroActionEdit(this, &actions[afterIdx],
actions[afterIdx]->GetId());
ui->elseActionsList->Add(newEntry);
}
ui->elseActionsList->SetHelpMsgVisible(actions.size() == 0);
}
void AdvSceneSwitcher::PopulateMacroConditions(Macro &m, uint32_t afterIdx)
@@ -435,17 +501,35 @@ void AdvSceneSwitcher::PopulateMacroConditions(Macro &m, uint32_t afterIdx)
auto newEntry = new MacroConditionEdit(
this, &conditions[afterIdx],
conditions[afterIdx]->GetId(), root);
conditionsList->Add(newEntry);
ui->conditionsList->Add(newEntry);
root = false;
}
conditionsList->SetHelpMsgVisible(conditions.size() == 0);
ui->conditionsList->SetHelpMsgVisible(conditions.size() == 0);
}
void AdvSceneSwitcher::SetActionData(Macro &m)
{
auto &actions = m.Actions();
for (int idx = 0; idx < actionsList->ContentLayout()->count(); idx++) {
auto item = actionsList->ContentLayout()->itemAt(idx);
for (int idx = 0; idx < ui->actionsList->ContentLayout()->count();
idx++) {
auto item = ui->actionsList->ContentLayout()->itemAt(idx);
if (!item) {
continue;
}
auto widget = static_cast<MacroActionEdit *>(item->widget());
if (!widget) {
continue;
}
widget->SetEntryData(&*(actions.begin() + idx));
}
}
void AdvSceneSwitcher::SetElseActionData(Macro &m)
{
auto &actions = m.ElseActions();
for (int idx = 0; idx < ui->elseActionsList->ContentLayout()->count();
idx++) {
auto item = ui->elseActionsList->ContentLayout()->itemAt(idx);
if (!item) {
continue;
}
@@ -460,9 +544,9 @@ void AdvSceneSwitcher::SetActionData(Macro &m)
void AdvSceneSwitcher::SetConditionData(Macro &m)
{
auto &conditions = m.Conditions();
for (int idx = 0; idx < conditionsList->ContentLayout()->count();
for (int idx = 0; idx < ui->conditionsList->ContentLayout()->count();
idx++) {
auto item = conditionsList->ContentLayout()->itemAt(idx);
auto item = ui->conditionsList->ContentLayout()->itemAt(idx);
if (!item) {
continue;
}
@@ -474,6 +558,21 @@ void AdvSceneSwitcher::SetConditionData(Macro &m)
}
}
static void maximizeFirstSplitterEntry(QSplitter *splitter)
{
QList<int> newSizes;
newSizes << 999999;
for (int i = 0; i < splitter->sizes().size() - 1; i++) {
newSizes << 0;
}
splitter->setSizes(newSizes);
}
static void centerSplitterPosition(QSplitter *splitter)
{
splitter->setSizes(QList<int>() << 999999 << 999999);
}
void AdvSceneSwitcher::SetEditMacro(Macro &m)
{
{
@@ -484,22 +583,40 @@ void AdvSceneSwitcher::SetEditMacro(Macro &m)
ui->runMacroInParallel->setChecked(m.RunInParallel());
ui->runMacroOnChange->setChecked(m.MatchOnChange());
}
conditionsList->Clear();
actionsList->Clear();
ui->conditionsList->Clear();
ui->actionsList->Clear();
ui->elseActionsList->Clear();
m.ResetUIHelpers();
PopulateMacroConditions(m);
PopulateMacroActions(m);
PopulateMacroElseActions(m);
SetMacroEditAreaDisabled(false);
currentActionIdx = -1;
currentElseActionIdx = -1;
currentConditionIdx = -1;
HighlightControls();
if (m.IsGroup()) {
SetMacroEditAreaDisabled(true);
ui->macroName->setEnabled(true);
centerSplitterPosition(ui->macroActionConditionSplitter);
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
return;
}
currentActionIdx = -1;
currentConditionIdx = -1;
HighlightControls();
if (!m.HasValidSplitterPositions()) {
centerSplitterPosition(ui->macroActionConditionSplitter);
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
return;
}
ui->macroActionConditionSplitter->setSizes(
m.GetActionConditionSplitterPosition());
ui->macroElseActionSplitter->setSizes(
m.GetElseActionSplitterPosition());
}
void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable)
@@ -515,12 +632,17 @@ void AdvSceneSwitcher::SetMacroEditAreaDisabled(bool disable)
void AdvSceneSwitcher::HighlightAction(int idx, QColor color)
{
actionsList->Highlight(idx, color);
ui->actionsList->Highlight(idx, color);
}
void AdvSceneSwitcher::HighlightElseAction(int idx, QColor color)
{
ui->elseActionsList->Highlight(idx, color);
}
void AdvSceneSwitcher::HighlightCondition(int idx, QColor color)
{
conditionsList->Highlight(idx, color);
ui->conditionsList->Highlight(idx, color);
}
std::shared_ptr<Macro> AdvSceneSwitcher::GetSelectedMacro()
@@ -533,6 +655,37 @@ std::vector<std::shared_ptr<Macro>> AdvSceneSwitcher::GetSelectedMacros()
return ui->macros->GetCurrentMacros();
}
void AdvSceneSwitcher::MacroSelectionAboutToChange()
{
if (loading) {
return;
}
if (!ui->macroName->isEnabled()) { // No macro is selected
return;
}
auto macro = GetMacroByQString(ui->macroName->text());
if (!macro) {
return;
}
macro->SetActionConditionSplitterPosition(
ui->macroActionConditionSplitter->sizes());
auto elsePos = ui->macroElseActionSplitter->sizes();
// If only conditions are visible maximize the actions to avoid neither
// actions nor elseActions being visible when the condition <-> action
// splitter is moved
if (elsePos[0] == 0 && elsePos[1] == 0) {
macro->SetElseActionSplitterPosition(QList<int>()
<< 999999 << 0);
return;
}
macro->SetElseActionSplitterPosition(
ui->macroElseActionSplitter->sizes());
}
void AdvSceneSwitcher::MacroSelectionChanged()
{
if (loading) {
@@ -542,10 +695,14 @@ void AdvSceneSwitcher::MacroSelectionChanged()
auto macro = GetSelectedMacro();
if (!macro) {
SetMacroEditAreaDisabled(true);
conditionsList->Clear();
actionsList->Clear();
conditionsList->SetHelpMsgVisible(true);
actionsList->SetHelpMsgVisible(true);
ui->conditionsList->Clear();
ui->actionsList->Clear();
ui->elseActionsList->Clear();
ui->conditionsList->SetHelpMsgVisible(true);
ui->actionsList->SetHelpMsgVisible(true);
ui->elseActionsList->SetHelpMsgVisible(true);
centerSplitterPosition(ui->macroActionConditionSplitter);
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
return;
}
SetEditMacro(*macro);
@@ -583,8 +740,19 @@ void AdvSceneSwitcher::on_macroProperties_clicked()
emit HighlightConditionsChanged(prop._highlightConditions);
}
// Don't restore splitter pos if an element is not visible at all
bool shouldResotreSplitterPos(const QList<int> &pos)
static void moveControlsToSplitter(QSplitter *splitter, int idx,
QLayoutItem *item)
{
static int splitterHandleWidth = 38;
auto handle = splitter->handle(idx);
auto layout = item->layout();
layout->setContentsMargins(7, 7, 7, 7);
handle->setLayout(layout);
splitter->setHandleWidth(splitterHandleWidth);
splitter->setStyleSheet("QSplitter::handle {background: transparent;}");
}
bool shouldRestoreSplitter(const QList<int> &pos)
{
if (pos.size() == 0) {
return false;
@@ -605,37 +773,44 @@ void AdvSceneSwitcher::SetupMacroTab()
}
ui->macros->Reset(switcher->macros,
switcher->macroProperties._highlightExecuted);
connect(ui->macros, SIGNAL(MacroSelectionAboutToChange()), this,
SLOT(MacroSelectionAboutToChange()));
connect(ui->macros, SIGNAL(MacroSelectionChanged()), this,
SLOT(MacroSelectionChanged()));
ui->runMacro->SetMacroTree(ui->macros);
delete conditionsList;
conditionsList = new MacroSegmentList(this);
conditionsList->SetHelpMsg(
ui->conditionsList->SetHelpMsg(
obs_module_text("AdvSceneSwitcher.macroTab.editConditionHelp"));
connect(conditionsList, &MacroSegmentList::SelectionChagned, this,
connect(ui->conditionsList, &MacroSegmentList::SelectionChagned, this,
&AdvSceneSwitcher::MacroConditionSelectionChanged);
connect(conditionsList, &MacroSegmentList::Reorder, this,
connect(ui->conditionsList, &MacroSegmentList::Reorder, this,
&AdvSceneSwitcher::MacroConditionReorder);
ui->macroConditionsLayout->insertWidget(0, conditionsList);
delete actionsList;
actionsList = new MacroSegmentList(this);
actionsList->SetHelpMsg(
ui->actionsList->SetHelpMsg(
obs_module_text("AdvSceneSwitcher.macroTab.editActionHelp"));
connect(actionsList, &MacroSegmentList::SelectionChagned, this,
connect(ui->actionsList, &MacroSegmentList::SelectionChagned, this,
&AdvSceneSwitcher::MacroActionSelectionChanged);
connect(actionsList, &MacroSegmentList::Reorder, this,
connect(ui->actionsList, &MacroSegmentList::Reorder, this,
&AdvSceneSwitcher::MacroActionReorder);
ui->macroActionsLayout->insertWidget(0, actionsList);
ui->elseActionsList->SetHelpMsg(obs_module_text(
"AdvSceneSwitcher.macroTab.editElseActionHelp"));
connect(ui->elseActionsList, &MacroSegmentList::SelectionChagned, this,
&AdvSceneSwitcher::MacroElseActionSelectionChanged);
connect(ui->elseActionsList, &MacroSegmentList::Reorder, this,
&AdvSceneSwitcher::MacroElseActionReorder);
ui->macros->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->macros, &QWidget::customContextMenuRequested, this,
&AdvSceneSwitcher::ShowMacroContextMenu);
actionsList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(actionsList, &QWidget::customContextMenuRequested, this,
ui->actionsList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->actionsList, &QWidget::customContextMenuRequested, this,
&AdvSceneSwitcher::ShowMacroActionsContextMenu);
conditionsList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(conditionsList, &QWidget::customContextMenuRequested, this,
ui->elseActionsList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->elseActionsList, &QWidget::customContextMenuRequested, this,
&AdvSceneSwitcher::ShowMacroElseActionsContextMenu);
ui->conditionsList->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->conditionsList, &QWidget::customContextMenuRequested, this,
&AdvSceneSwitcher::ShowMacroConditionsContextMenu);
SetMacroEditAreaDisabled(true);
@@ -648,16 +823,10 @@ void AdvSceneSwitcher::SetupMacroTab()
onChangeHighlightTimer.start();
// Move condition controls into splitter handle layout
auto handle = ui->macroActionConditionSplitter->handle(1);
auto item = ui->macroConditionsLayout->takeAt(1);
if (item) {
auto layout = item->layout();
layout->setContentsMargins(7, 7, 7, 7);
handle->setLayout(layout);
ui->macroActionConditionSplitter->setHandleWidth(38);
}
ui->macroActionConditionSplitter->setStyleSheet(
"QSplitter::handle {background: transparent;}");
moveControlsToSplitter(ui->macroActionConditionSplitter, 1,
ui->macroConditionsLayout->takeAt(1));
moveControlsToSplitter(ui->macroElseActionSplitter, 1,
ui->macroActionsLayout->takeAt(1));
// Set action and condition control icons
const std::string pathPrefix =
@@ -665,6 +834,9 @@ void AdvSceneSwitcher::SetupMacroTab()
SetButtonIcon(ui->actionTop, (pathPrefix + "DoubleUp.svg").c_str());
SetButtonIcon(ui->actionBottom,
(pathPrefix + "DoubleDown.svg").c_str());
SetButtonIcon(ui->elseActionTop, (pathPrefix + "DoubleUp.svg").c_str());
SetButtonIcon(ui->elseActionBottom,
(pathPrefix + "DoubleDown.svg").c_str());
SetButtonIcon(ui->conditionTop, (pathPrefix + "DoubleUp.svg").c_str());
SetButtonIcon(ui->conditionBottom,
(pathPrefix + "DoubleDown.svg").c_str());
@@ -673,13 +845,11 @@ void AdvSceneSwitcher::SetupMacroTab()
ui->macroListMacroEditSplitter->setStretchFactor(0, 1);
ui->macroListMacroEditSplitter->setStretchFactor(1, 4);
centerSplitterPosition(ui->macroActionConditionSplitter);
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
if (switcher->saveWindowGeo) {
if (shouldResotreSplitterPos(
switcher->macroActionConditionSplitterPosition)) {
ui->macroActionConditionSplitter->setSizes(
switcher->macroActionConditionSplitterPosition);
}
if (shouldResotreSplitterPos(
if (shouldRestoreSplitter(
switcher->macroListMacroEditSplitterPosition)) {
ui->macroListMacroEditSplitter->setSizes(
switcher->macroListMacroEditSplitterPosition);
@@ -731,41 +901,55 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
obs_module_text("AdvSceneSwitcher.macroTab.export"), this,
&AdvSceneSwitcher::ExportMacros);
exportAction->setDisabled(ui->macros->SelectionEmpty());
auto import = menu.addAction(
obs_module_text("AdvSceneSwitcher.macroTab.import"), this,
&AdvSceneSwitcher::ImportMacros);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.import"),
this, &AdvSceneSwitcher::ImportMacros);
menu.exec(globalPos);
}
static void setupConextMenu(AdvSceneSwitcher *ss, const QPoint &pos,
std::function<void(AdvSceneSwitcher *)> expand,
std::function<void(AdvSceneSwitcher *)> collapse,
std::function<void(AdvSceneSwitcher *)> maximize,
std::function<void(AdvSceneSwitcher *)> minimize)
{
QMenu menu;
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"),
ss, [ss, expand]() { expand(ss); });
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"),
ss, [ss, collapse]() { collapse(ss); });
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"),
ss, [ss, maximize]() { maximize(ss); });
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"),
ss, [ss, minimize]() { minimize(ss); });
menu.exec(pos);
}
void AdvSceneSwitcher::ShowMacroActionsContextMenu(const QPoint &pos)
{
QPoint globalPos = actionsList->mapToGlobal(pos);
QMenu menu;
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"),
this, &AdvSceneSwitcher::ExpandAllActions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"),
this, &AdvSceneSwitcher::CollapseAllActions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"),
this, &AdvSceneSwitcher::MinimizeConditions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"),
this, &AdvSceneSwitcher::MinimizeActions);
menu.exec(globalPos);
setupConextMenu(this, ui->actionsList->mapToGlobal(pos),
&AdvSceneSwitcher::ExpandAllActions,
&AdvSceneSwitcher::CollapseAllActions,
&AdvSceneSwitcher::MaximizeActions,
&AdvSceneSwitcher::MinimizeActions);
}
void AdvSceneSwitcher::ShowMacroElseActionsContextMenu(const QPoint &pos)
{
setupConextMenu(this, ui->elseActionsList->mapToGlobal(pos),
&AdvSceneSwitcher::ExpandAllElseActions,
&AdvSceneSwitcher::CollapseAllElseActions,
&AdvSceneSwitcher::MaximizeElseActions,
&AdvSceneSwitcher::MinimizeElseActions);
}
void AdvSceneSwitcher::ShowMacroConditionsContextMenu(const QPoint &pos)
{
QPoint globalPos = conditionsList->mapToGlobal(pos);
QMenu menu;
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.expandAll"),
this, &AdvSceneSwitcher::ExpandAllConditions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.collapseAll"),
this, &AdvSceneSwitcher::CollapseAllConditions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.maximize"),
this, &AdvSceneSwitcher::MinimizeActions);
menu.addAction(obs_module_text("AdvSceneSwitcher.macroTab.minimize"),
this, &AdvSceneSwitcher::MinimizeConditions);
menu.exec(globalPos);
setupConextMenu(this, ui->conditionsList->mapToGlobal(pos),
&AdvSceneSwitcher::ExpandAllConditions,
&AdvSceneSwitcher::CollapseAllConditions,
&AdvSceneSwitcher::MaximizeConditions,
&AdvSceneSwitcher::MinimizeConditions);
}
void AdvSceneSwitcher::CopyMacro()
@@ -795,60 +979,103 @@ void AdvSceneSwitcher::CopyMacro()
emit MacroAdded(QString::fromStdString(name));
}
void AdvSceneSwitcher::ExpandAllActions()
void setCollapsedHelper(const std::shared_ptr<Macro> &m, MacroSegmentList *list,
bool collapsed)
{
auto m = GetSelectedMacro();
if (!m) {
return;
}
actionsList->SetCollapsed(false);
list->SetCollapsed(collapsed);
}
void AdvSceneSwitcher::ExpandAllActions()
{
setCollapsedHelper(GetSelectedMacro(), ui->actionsList, false);
}
void AdvSceneSwitcher::ExpandAllElseActions()
{
setCollapsedHelper(GetSelectedMacro(), ui->elseActionsList, false);
}
void AdvSceneSwitcher::ExpandAllConditions()
{
auto m = GetSelectedMacro();
if (!m) {
return;
}
conditionsList->SetCollapsed(false);
setCollapsedHelper(GetSelectedMacro(), ui->conditionsList, false);
}
void AdvSceneSwitcher::CollapseAllActions()
{
auto m = GetSelectedMacro();
if (!m) {
return;
}
actionsList->SetCollapsed(true);
setCollapsedHelper(GetSelectedMacro(), ui->actionsList, true);
}
void AdvSceneSwitcher::CollapseAllElseActions()
{
setCollapsedHelper(GetSelectedMacro(), ui->elseActionsList, true);
}
void AdvSceneSwitcher::CollapseAllConditions()
{
auto m = GetSelectedMacro();
if (!m) {
return;
}
conditionsList->SetCollapsed(true);
setCollapsedHelper(GetSelectedMacro(), ui->conditionsList, true);
}
static void reduceSizeOfSplitterIdx(QSplitter *splitter, int idx)
{
auto sizes = splitter->sizes();
int sum = sizes[0] + sizes[1];
int reducedSize = sum / 10;
sizes[idx] = reducedSize;
sizes[(idx + 1) % 2] = sum - reducedSize;
splitter->setSizes(sizes);
}
void AdvSceneSwitcher::MinimizeActions()
{
QList<int> sizes = ui->macroActionConditionSplitter->sizes();
int sum = sizes[0] + sizes[1];
int actionsHeight = sum / 10;
sizes[1] = actionsHeight;
sizes[0] = sum - actionsHeight;
ui->macroActionConditionSplitter->setSizes(sizes);
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (macro->ElseActions().size() == 0) {
reduceSizeOfSplitterIdx(ui->macroActionConditionSplitter, 1);
} else {
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
reduceSizeOfSplitterIdx(ui->macroActionConditionSplitter, 1);
}
}
void AdvSceneSwitcher::MaximizeActions()
{
MinimizeElseActions();
MinimizeConditions();
}
void AdvSceneSwitcher::MinimizeElseActions()
{
auto macro = GetSelectedMacro();
if (!macro) {
return;
}
if (macro->ElseActions().size() == 0) {
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
} else {
reduceSizeOfSplitterIdx(ui->macroElseActionSplitter, 1);
}
}
void AdvSceneSwitcher::MaximizeElseActions()
{
MinimizeConditions();
reduceSizeOfSplitterIdx(ui->macroElseActionSplitter, 0);
}
void AdvSceneSwitcher::MinimizeConditions()
{
QList<int> sizes = ui->macroActionConditionSplitter->sizes();
int sum = sizes[0] + sizes[1];
int conditionsHeight = sum / 10;
sizes[0] = conditionsHeight;
sizes[1] = sum - conditionsHeight;
ui->macroActionConditionSplitter->setSizes(sizes);
reduceSizeOfSplitterIdx(ui->macroActionConditionSplitter, 0);
}
void AdvSceneSwitcher::MaximizeConditions()
{
MinimizeElseActions();
MinimizeActions();
}
bool AdvSceneSwitcher::MacroTabIsInFocus()

View File

@@ -609,7 +609,7 @@ bool MacroTreeModel::IsLastItem(std::shared_ptr<Macro> item) const
bool MacroTreeModel::IsInValidState()
{
// Check for reordering erros
for (int i = 0, j = 0; i < _macros.size(); i++) {
for (size_t i = 0, j = 0; i < _macros.size(); i++) {
const auto &m = _macros[i];
if (QString::fromStdString(m->Name()) !=
data(index(j, 0), Qt::AccessibleTextRole)) {
@@ -622,7 +622,7 @@ bool MacroTreeModel::IsInValidState()
}
// Check for group errors
for (int i = 0; i < _macros.size(); i++) {
for (size_t i = 0; i < _macros.size(); i++) {
const auto &m = _macros[i];
if (!m->IsGroup()) {
continue;
@@ -1260,6 +1260,7 @@ void MacroTree::UngroupSelectedGroups()
void MacroTree::SelectionChangedHelper(const QItemSelection &,
const QItemSelection &)
{
emit MacroSelectionAboutToChange();
emit MacroSelectionChanged();
}

View File

@@ -142,6 +142,7 @@ public slots:
const QItemSelection &);
signals:
void MacroSelectionAboutToChange();
void MacroSelectionChanged();
protected:

View File

@@ -176,24 +176,26 @@ bool Macro::CeckMatch()
}
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
bool matchedBeforeOnChangeCheck = _matched;
if (_matched && _matchOnChange && _lastMatched) {
vblog(LOG_INFO, "ignore match for Macro %s (on change)",
_name.c_str());
_matched = false;
SetOnChangeHighlight();
_conditionSateChanged = _lastMatched != _matched;
if (!_conditionSateChanged && _performActionsOnChange) {
_onPreventedActionExecution = true;
}
_lastMatched = matchedBeforeOnChangeCheck;
_lastMatched = _matched;
_lastCheckTime = std::chrono::high_resolution_clock::now();
return _matched;
}
bool Macro::PerformActions(bool forceParallel, bool ignorePause)
bool Macro::PerformActions(bool match, bool forceParallel, bool ignorePause)
{
if (!_done) {
vblog(LOG_INFO, "macro %s already running", _name.c_str());
return !forceParallel;
}
std::function<bool(bool)> runFunc =
match ? std::bind(&Macro::RunActions, this,
std::placeholders::_1)
: std::bind(&Macro::RunElseActions, this,
std::placeholders::_1);
_stop = false;
_done = false;
bool ret = true;
@@ -202,9 +204,9 @@ bool Macro::PerformActions(bool forceParallel, bool ignorePause)
_backgroundThread.join();
}
_backgroundThread = std::thread(
[this, ignorePause] { RunActions(ignorePause); });
[this, runFunc, ignorePause] { runFunc(ignorePause); });
} else {
RunActions(ret, ignorePause);
ret = runFunc(ignorePause);
}
_lastExecutionTime = std::chrono::high_resolution_clock::now();
auto group = _parent.lock();
@@ -223,6 +225,28 @@ bool Macro::ExecutedSince(
return _lastExecutionTime > time;
}
bool Macro::ShouldRunActions() const
{
const bool hasActionsToExecute =
(_matched || _elseActions.size() > 0) &&
(!_performActionsOnChange || _conditionSateChanged);
if (VerboseLoggingEnabled() && _performActionsOnChange &&
!_conditionSateChanged) {
if (_matched && _actions.size() > 0) {
blog(LOG_INFO, "skip actions for Macro %s (on change)",
_name.c_str());
}
if (!_matched && _elseActions.size() > 0) {
blog(LOG_INFO,
"skip else actions for Macro %s (on change)",
_name.c_str());
}
}
return hasActionsToExecute;
}
int64_t Macro::MsSinceLastCheck() const
{
if (_lastCheckTime.time_since_epoch().count() == 0) {
@@ -251,37 +275,43 @@ void Macro::ResetTimers()
_lastExecutionTime = {};
}
void Macro::RunActions(bool &retVal, bool ignorePause)
bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actions,
bool ignorePause)
{
bool ret = true;
for (auto &a : _actions) {
if (a->Enabled()) {
a->LogAction();
ret = ret && a->PerformAction();
bool actionsExecutedSuccessfully = true;
for (auto &action : actions) {
if (action->Enabled()) {
action->LogAction();
actionsExecutedSuccessfully =
actionsExecutedSuccessfully &&
action->PerformAction();
} else {
vblog(LOG_INFO, "skipping disabled action %s",
a->GetId().c_str());
action->GetId().c_str());
}
if (!ret || (_paused && !ignorePause) || _stop || _die) {
retVal = ret;
if (!actionsExecutedSuccessfully || (_paused && !ignorePause) ||
_stop || _die) {
break;
}
if (a->Enabled()) {
a->SetHighlight();
if (action->Enabled()) {
action->SetHighlight();
}
}
_done = true;
return actionsExecutedSuccessfully;
}
void Macro::RunActions(bool ignorePause)
bool Macro::RunActions(bool ignorePause)
{
bool unused;
RunActions(unused, ignorePause);
vblog(LOG_INFO, "running actions of %s", _name.c_str());
return RunActionsHelper(_actions, ignorePause);
}
void Macro::SetOnChangeHighlight()
bool Macro::RunElseActions(bool ignorePause)
{
_onChangeTriggered = true;
vblog(LOG_INFO, "running else actions of %s", _name.c_str());
return RunActionsHelper(_elseActions, ignorePause);
}
bool Macro::DockIsVisible() const
@@ -289,6 +319,11 @@ bool Macro::DockIsVisible() const
return _dock && _dockAction && _dock->isVisible();
}
void Macro::SetMatchOnChange(bool onChange)
{
_performActionsOnChange = onChange;
}
void Macro::SetPaused(bool pause)
{
if (_paused && !pause) {
@@ -332,22 +367,39 @@ std::deque<std::shared_ptr<MacroAction>> &Macro::Actions()
return _actions;
}
void Macro::UpdateActionIndices()
std::deque<std::shared_ptr<MacroAction>> &Macro::ElseActions()
{
return _elseActions;
}
static void updateIndicesHelper(std::deque<std::shared_ptr<MacroSegment>> &list)
{
int idx = 0;
for (auto a : _actions) {
a->SetIndex(idx);
for (auto segment : list) {
segment->SetIndex(idx);
idx++;
}
}
void Macro::UpdateActionIndices()
{
std::deque<std::shared_ptr<MacroSegment>> list(_actions.begin(),
_actions.end());
updateIndicesHelper(list);
}
void Macro::UpdateElseActionIndices()
{
std::deque<std::shared_ptr<MacroSegment>> list(_elseActions.begin(),
_elseActions.end());
updateIndicesHelper(list);
}
void Macro::UpdateConditionIndices()
{
int idx = 0;
for (auto c : _conditions) {
c->SetIndex(idx);
idx++;
}
std::deque<std::shared_ptr<MacroSegment>> list(_conditions.begin(),
_conditions.end());
updateIndicesHelper(list);
}
std::shared_ptr<Macro> Macro::Parent() const
@@ -360,56 +412,57 @@ bool Macro::Save(obs_data_t *obj) const
obs_data_set_string(obj, "name", _name.c_str());
obs_data_set_bool(obj, "pause", _paused);
obs_data_set_bool(obj, "parallel", _runInParallel);
obs_data_set_bool(obj, "onChange", _matchOnChange);
obs_data_set_bool(obj, "onChange", _performActionsOnChange);
obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart);
obs_data_set_bool(obj, "group", _isGroup);
if (_isGroup) {
auto groupData = obs_data_create();
OBSDataAutoRelease groupData = obs_data_create();
obs_data_set_bool(groupData, "collapsed", _isCollapsed);
obs_data_set_int(groupData, "size", _groupSize);
obs_data_set_obj(obj, "groupData", groupData);
obs_data_release(groupData);
return true;
}
SaveDockSettings(obj);
SaveSplitterPos(_actionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
SaveSplitterPos(_elseActionSplitterPosition, obj,
"macroElseActionSplitterPosition");
obs_data_set_bool(obj, "registerHotkeys", _registerHotkeys);
obs_data_array_t *pauseHotkey = obs_hotkey_save(_pauseHotkey);
OBSDataArrayAutoRelease pauseHotkey = obs_hotkey_save(_pauseHotkey);
obs_data_set_array(obj, "pauseHotkey", pauseHotkey);
obs_data_array_release(pauseHotkey);
obs_data_array_t *unpauseHotkey = obs_hotkey_save(_unpauseHotkey);
OBSDataArrayAutoRelease unpauseHotkey = obs_hotkey_save(_unpauseHotkey);
obs_data_set_array(obj, "unpauseHotkey", unpauseHotkey);
obs_data_array_release(unpauseHotkey);
obs_data_array_t *togglePauseHotkey =
OBSDataArrayAutoRelease togglePauseHotkey =
obs_hotkey_save(_togglePauseHotkey);
obs_data_set_array(obj, "togglePauseHotkey", togglePauseHotkey);
obs_data_array_release(togglePauseHotkey);
obs_data_array_t *conditions = obs_data_array_create();
OBSDataArrayAutoRelease conditions = obs_data_array_create();
for (auto &c : _conditions) {
obs_data_t *array_obj = obs_data_create();
c->Save(array_obj);
obs_data_array_push_back(conditions, array_obj);
obs_data_release(array_obj);
OBSDataAutoRelease arrayObj = obs_data_create();
c->Save(arrayObj);
obs_data_array_push_back(conditions, arrayObj);
}
obs_data_set_array(obj, "conditions", conditions);
obs_data_array_release(conditions);
obs_data_array_t *actions = obs_data_array_create();
OBSDataArrayAutoRelease actions = obs_data_array_create();
for (auto &a : _actions) {
obs_data_t *array_obj = obs_data_create();
a->Save(array_obj);
obs_data_array_push_back(actions, array_obj);
obs_data_release(array_obj);
OBSDataAutoRelease arrayObj = obs_data_create();
a->Save(arrayObj);
obs_data_array_push_back(actions, arrayObj);
}
obs_data_set_array(obj, "actions", actions);
obs_data_array_release(actions);
OBSDataArrayAutoRelease elseActions = obs_data_array_create();
for (auto &a : _elseActions) {
OBSDataAutoRelease arrayObj = obs_data_create();
a->Save(arrayObj);
obs_data_array_push_back(elseActions, arrayObj);
}
obs_data_set_array(obj, "elseActions", elseActions);
return true;
}
@@ -453,73 +506,69 @@ bool Macro::Load(obs_data_t *obj)
_name = obs_data_get_string(obj, "name");
_paused = obs_data_get_bool(obj, "pause");
_runInParallel = obs_data_get_bool(obj, "parallel");
_matchOnChange = obs_data_get_bool(obj, "onChange");
_performActionsOnChange = obs_data_get_bool(obj, "onChange");
_skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart");
_isGroup = obs_data_get_bool(obj, "group");
if (_isGroup) {
auto groupData = obs_data_get_obj(obj, "groupData");
OBSDataAutoRelease groupData =
obs_data_get_obj(obj, "groupData");
_isCollapsed = obs_data_get_bool(groupData, "collapsed");
_groupSize = obs_data_get_int(groupData, "size");
obs_data_release(groupData);
return true;
}
LoadDockSettings(obj);
LoadSplitterPos(_actionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
LoadSplitterPos(_elseActionSplitterPosition, obj,
"macroElseActionSplitterPosition");
obs_data_set_default_bool(obj, "registerHotkeys", true);
_registerHotkeys = obs_data_get_bool(obj, "registerHotkeys");
if (_registerHotkeys) {
SetupHotkeys();
}
obs_data_array_t *pauseHotkey = obs_data_get_array(obj, "pauseHotkey");
OBSDataArrayAutoRelease pauseHotkey =
obs_data_get_array(obj, "pauseHotkey");
obs_hotkey_load(_pauseHotkey, pauseHotkey);
obs_data_array_release(pauseHotkey);
obs_data_array_t *unpauseHotkey =
OBSDataArrayAutoRelease unpauseHotkey =
obs_data_get_array(obj, "unpauseHotkey");
obs_hotkey_load(_unpauseHotkey, unpauseHotkey);
obs_data_array_release(unpauseHotkey);
obs_data_array_t *togglePauseHotkey =
OBSDataArrayAutoRelease togglePauseHotkey =
obs_data_get_array(obj, "togglePauseHotkey");
obs_hotkey_load(_togglePauseHotkey, togglePauseHotkey);
obs_data_array_release(togglePauseHotkey);
SetHotkeysDesc();
bool root = true;
obs_data_array_t *conditions = obs_data_get_array(obj, "conditions");
OBSDataArrayAutoRelease conditions =
obs_data_get_array(obj, "conditions");
size_t count = obs_data_array_count(conditions);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(conditions, i);
std::string id = obs_data_get_string(array_obj, "id");
OBSDataAutoRelease arrayObj =
obs_data_array_item(conditions, i);
std::string id = obs_data_get_string(arrayObj, "id");
auto newEntry = MacroConditionFactory::Create(id, this);
if (newEntry) {
_conditions.emplace_back(newEntry);
auto c = _conditions.back().get();
c->Load(array_obj);
c->Load(arrayObj);
setValidLogic(c, root, _name);
} else {
blog(LOG_WARNING,
"discarding condition entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
obs_data_release(array_obj);
root = false;
}
obs_data_array_release(conditions);
UpdateConditionIndices();
obs_data_array_t *actions = obs_data_get_array(obj, "actions");
OBSDataArrayAutoRelease actions = obs_data_get_array(obj, "actions");
count = obs_data_array_count(actions);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(actions, i);
OBSDataAutoRelease array_obj = obs_data_array_item(actions, i);
std::string id = obs_data_get_string(array_obj, "id");
auto newEntry = MacroActionFactory::Create(id, this);
if (newEntry) {
_actions.emplace_back(newEntry);
@@ -529,11 +578,27 @@ bool Macro::Load(obs_data_t *obj)
"discarding action entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
obs_data_release(array_obj);
}
obs_data_array_release(actions);
UpdateActionIndices();
OBSDataArrayAutoRelease elseActions =
obs_data_get_array(obj, "elseActions");
count = obs_data_array_count(elseActions);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease array_obj =
obs_data_array_item(elseActions, i);
std::string id = obs_data_get_string(array_obj, "id");
auto newEntry = MacroActionFactory::Create(id, this);
if (newEntry) {
_elseActions.emplace_back(newEntry);
_elseActions.back()->Load(array_obj);
} else {
blog(LOG_WARNING,
"discarding elseAction entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
}
UpdateElseActionIndices();
return true;
}
@@ -552,7 +617,12 @@ bool Macro::SwitchesScene() const
{
MacroActionSwitchScene temp(nullptr);
auto sceneSwitchId = temp.GetId();
for (auto &a : _actions) {
for (const auto &a : _actions) {
if (a->GetId() == sceneSwitchId) {
return true;
}
}
for (const auto &a : _elseActions) {
if (a->GetId() == sceneSwitchId) {
return true;
}
@@ -560,18 +630,44 @@ bool Macro::SwitchesScene() const
return false;
}
const QList<int> &Macro::GetActionConditionSplitterPosition() const
{
return _actionConditionSplitterPosition;
}
void Macro::SetActionConditionSplitterPosition(const QList<int> sizes)
{
_actionConditionSplitterPosition = sizes;
}
const QList<int> &Macro::GetElseActionSplitterPosition() const
{
return _elseActionSplitterPosition;
}
void Macro::SetElseActionSplitterPosition(const QList<int> sizes)
{
_elseActionSplitterPosition = sizes;
}
bool Macro::HasValidSplitterPositions() const
{
return !_actionConditionSplitterPosition.empty() &&
!_elseActionSplitterPosition.empty();
}
bool Macro::OnChangePreventedActionsRecently()
{
if (_onChangeTriggered) {
_onChangeTriggered = false;
return true;
if (_onPreventedActionExecution) {
_onPreventedActionExecution = false;
return _matched ? _actions.size() > 0 : _elseActions.size() > 0;
}
return false;
}
void Macro::ResetUIHelpers()
{
_onChangeTriggered = false;
_onPreventedActionExecution = false;
for (auto c : _conditions) {
c->Highlight();
}
@@ -710,7 +806,8 @@ void Macro::EnableDock(bool value)
// Create new dock widget
auto window =
static_cast<QMainWindow *>(obs_frontend_get_main_window());
_dock = new MacroDock(this, window, _runButtonText, _pauseButtonText,
_dock = new MacroDock(GetWeakMacroByName(_name.c_str()), window,
_runButtonText, _pauseButtonText,
_unpauseButtonText, _conditionsTrueStatusText,
_conditionsFalseStatusText, _dockHighlight);
SetDockWidgetName(); // Used by OBS to restore position
@@ -1014,7 +1111,7 @@ bool SwitcherData::CheckMacros()
{
bool ret = false;
for (auto &m : macros) {
if (m->CeckMatch()) {
if (m->CeckMatch() || m->ElseActions().size() > 0) {
ret = true;
// This has to be performed here for now as actions are
// not performed immediately after checking conditions.
@@ -1049,7 +1146,7 @@ bool SwitcherData::RunMacros()
}
for (auto &m : runPhaseMacros) {
if (!m || !m->Matched()) {
if (!m || !m->ShouldRunActions()) {
continue;
}
if (firstInterval && m->SkipExecOnStart()) {
@@ -1059,7 +1156,7 @@ bool SwitcherData::RunMacros()
continue;
}
vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
if (!m->PerformActions()) {
if (!m->PerformActions(m->Matched())) {
blog(LOG_WARNING, "abort macro: %s", m->Name().c_str());
}
}

View File

@@ -25,9 +25,10 @@ public:
Macro(const std::string &name = "", const bool addHotkey = false);
virtual ~Macro();
bool CeckMatch();
bool PerformActions(bool forceParallel = false,
bool PerformActions(bool match, bool forceParallel = false,
bool ignorePause = false);
bool Matched() const { return _matched; }
bool ShouldRunActions() const;
int64_t MsSinceLastCheck() const;
std::string Name() const { return _name; }
void SetName(const std::string &name);
@@ -35,8 +36,8 @@ public:
bool RunInParallel() const { return _runInParallel; }
void SetPaused(bool pause = true);
bool Paused() const { return _paused; }
void SetMatchOnChange(bool onChange) { _matchOnChange = onChange; }
bool MatchOnChange() const { return _matchOnChange; }
void SetMatchOnChange(bool onChange);
bool MatchOnChange() const { return _performActionsOnChange; }
void SetSkipExecOnStart(bool skip) { _skipExecOnStart = skip; }
bool SkipExecOnStart() const { return _skipExecOnStart; }
int RunCount() const { return _runCount; };
@@ -48,7 +49,9 @@ public:
std::deque<std::shared_ptr<MacroCondition>> &Conditions();
std::deque<std::shared_ptr<MacroAction>> &Actions();
std::deque<std::shared_ptr<MacroAction>> &ElseActions();
void UpdateActionIndices();
void UpdateElseActionIndices();
void UpdateConditionIndices();
// Group controls
@@ -80,6 +83,11 @@ public:
bool SwitchesScene() const;
// UI helpers
const QList<int> &GetActionConditionSplitterPosition() const;
void SetActionConditionSplitterPosition(const QList<int>);
const QList<int> &GetElseActionSplitterPosition() const;
void SetElseActionSplitterPosition(const QList<int>);
bool HasValidSplitterPositions() const;
bool
ExecutedSince(const std::chrono::high_resolution_clock::time_point &);
bool OnChangePreventedActionsRecently();
@@ -113,9 +121,11 @@ private:
void SetupHotkeys();
void ClearHotkeys() const;
void SetHotkeysDesc() const;
void RunActions(bool &ret, bool ignorePause);
void RunActions(bool ignorePause);
void SetOnChangeHighlight();
bool RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actions,
bool ignorePause);
bool RunActions(bool ignorePause);
bool RunElseActions(bool ignorePause);
bool DockIsVisible() const;
void SetDockWidgetName() const;
void SaveDockSettings(obs_data_t *obj) const;
@@ -133,6 +143,7 @@ private:
std::deque<std::shared_ptr<MacroCondition>> _conditions;
std::deque<std::shared_ptr<MacroAction>> _actions;
std::deque<std::shared_ptr<MacroAction>> _elseActions;
std::weak_ptr<Macro> _parent;
uint32_t _groupSize = 0;
@@ -142,7 +153,8 @@ private:
bool _runInParallel = false;
bool _matched = false;
bool _lastMatched = false;
bool _matchOnChange = true;
bool _conditionSateChanged = false;
bool _performActionsOnChange = true;
bool _skipExecOnStart = false;
bool _paused = false;
int _runCount = 0;
@@ -151,7 +163,11 @@ private:
obs_hotkey_id _unpauseHotkey = OBS_INVALID_HOTKEY_ID;
obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID;
bool _onChangeTriggered = false;
// UI helpers
bool _onPreventedActionExecution = false;
QList<int> _actionConditionSplitterPosition;
QList<int> _elseActionSplitterPosition;
bool _registerDock = false;
bool _dockHasRunButton = true;

View File

@@ -27,6 +27,11 @@ if(ENABLE_OPENVR_PLUGIN)
add_subdirectory(openvr)
endif()
option(ENABLE_TWITCH_PLUGIN "Enable the twitch plugin" ON)
if(ENABLE_TWITCH_PLUGIN)
add_subdirectory(twitch)
endif()
option(ENABLE_VIDEO_PLUGIN "Enable the video plugin" ON)
if(ENABLE_VIDEO_PLUGIN)
add_subdirectory(video)

View File

@@ -5,7 +5,7 @@ project(advanced-scene-switcher-midi)
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
set(LIBREMIDI_DIR "${ADVSS_SOURCE_DIR}/deps/libremidi")
if(NOT EXISTS "${LIBREMIDI_DIR}")
if(NOT EXISTS "${LIBREMIDI_DIR}/CMakeLists.txt")
message(WARNING "libremidi directory \"${LIBREMIDI_DIR}\" not found!\n"
"MIDI support will be disabled!")
return()

View File

@@ -9,7 +9,7 @@ namespace advss {
static std::map<std::pair<MidiDeviceType, int>, MidiDeviceInstance *>
SetupMidiMessageVector()
{
GetSwitcher()->AddResetForNextIntervalFunction(
GetSwitcher()->AddIntervalResetStep(
MidiDeviceInstance::ClearMessageBuffersOfAllDevices);
return {};
}

View File

@@ -0,0 +1,79 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-twitch)
# --- Check requirements ---
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
set(CPP_HTTPLIB_DIR "${ADVSS_SOURCE_DIR}/deps/cpp-httplib")
if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
message(WARNING "cpp-httplib directory \"${CPP_HTTPLIB_DIR}\" not found!\n"
"Twitch support will be disabled!")
return()
endif()
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n"
"Twitch support will be disabled!\n\n")
return()
endif()
endif()
find_package(ZLIB)
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1)
if(OS_MACOS)
target_compile_definitions(
${PROJECT_NAME} PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN=1)
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreFoundation")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework Security")
endif()
target_sources(
${PROJECT_NAME}
PRIVATE category-selection.cpp
category-selection.hpp
macro-action-twitch.cpp
macro-action-twitch.hpp
token.cpp
token.hpp
twitch-helpers.cpp
twitch-helpers.hpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
"${OPENSSL_INCLUDE_DIR}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
install_advss_plugin(${PROJECT_NAME})
if(OS_WINDOWS)
# Couldn't really find a better way to install runtime dependencies for
# Windows TODO: Clean this up at some point
function(FIND_FILES_WITH_PATTERN result pattern dir)
execute_process(
COMMAND
powershell -Command
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
"Select-Object -First 1 |"
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
OUTPUT_VARIABLE files
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(${result}
${files}
PARENT_SCOPE)
endfunction()
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
endif()

View File

@@ -0,0 +1,364 @@
#include "category-selection.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <utility.hpp>
#include <name-dialog.hpp>
#include <obs-module-helper.hpp>
#include <obs.hpp>
#include <QVBoxLayout>
namespace advss {
void TwitchCategory::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "category");
id = obs_data_get_int(data, "id");
name = obs_data_get_string(data, "name");
}
void TwitchCategory::Save(obs_data_t *obj) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_int(data, "id", id);
obs_data_set_string(data, "name", name.c_str());
obs_data_set_obj(obj, "category", data);
}
bool TwitchCategorySelection::_fetchingCategoriesDone = false;
std::map<QString, int> TwitchCategorySelection::_streamingCategories;
void TwitchCategorySelection::PopulateCategorySelection()
{
auto token = _token.lock();
if (!token && _streamingCategories.empty()) {
return;
}
if (!_fetchingCategoriesDone && token) {
_categoryGrabber.Start(token);
if (_progressDialog->exec() == QDialog::Accepted) {
_fetchingCategoriesDone = true;
}
_categoryGrabber.Stop();
_categoryGrabber.wait();
}
UpdateCategoryList();
}
void TwitchCategorySelection::UpdateCategoryList()
{
_streamingCategories = _categoryGrabber.GetCategories();
QString currentSelection = currentText();
const QSignalBlocker b(this);
clear();
for (const auto &[name, id] : _streamingCategories) {
addItem(name, id);
}
setCurrentText(currentSelection);
}
TwitchCategorySelection::TwitchCategorySelection(QWidget *parent)
: FilterComboBox(
parent,
obs_module_text("AdvSceneSwitcher.twitchCategories.select")),
_progressDialog(new ProgressDialog(this))
{
_progressDialog->setWindowModality(Qt::WindowModal);
setSizeAdjustPolicy(QComboBox::AdjustToContents);
QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this,
SLOT(SelectionChanged(int)));
QWidget::connect(&_categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
_progressDialog, SLOT(CategoryCountUpdated(int)));
QWidget::connect(&_categoryGrabber, SIGNAL(Finished()), this,
SLOT(PopulateFinished()));
QWidget::connect(TwitchCategorySignalManager::Instance(),
SIGNAL(RepopulateRequired()), this,
SLOT(UpdateCategoryList()));
}
void TwitchCategorySelection::SetToken(const std::weak_ptr<TwitchToken> &token)
{
_token = token;
const bool expired = token.expired();
setDisabled(expired);
if (expired) {
setToolTip(obs_module_text(
"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
} else {
setToolTip("");
}
}
void TwitchCategorySelection::PopulateFinished()
{
_progressDialog->accept();
}
void TwitchCategorySelection::showPopup()
{
if (!IsPopulated()) {
PopulateCategorySelection();
}
adjustSize();
updateGeometry();
FilterComboBox::showPopup();
}
bool TwitchCategorySelection::IsPopulated()
{
return count() == _categoryGrabber.GetCategories().size() &&
_fetchingCategoriesDone;
}
void TwitchCategorySelection::SetCategory(const TwitchCategory &id)
{
// If the list is populated already try to find id ...
int index = findData(id.id);
if (index != -1) {
setCurrentIndex(index);
return;
}
if (id.id == -1) {
setCurrentIndex(-1);
return;
}
// ... otherwise just add a dummy entry with the category name
addItem(QString::fromStdString(id.name), id.id);
setCurrentIndex(findData(id.id));
}
void TwitchCategorySelection::SelectionChanged(int index)
{
TwitchCategory category{itemData(index).toInt(),
currentText().toStdString()};
emit CategoreyChanged(category);
}
std::map<QString, int> CategoryGrabber::_categoryMap = {};
std::mutex CategoryGrabber::_mtx = {};
void CategoryGrabber::Start(const std::shared_ptr<TwitchToken> &token,
const std::string search)
{
_searchString = search;
_token = token;
_stop = false;
start();
}
void CategoryGrabber::Stop()
{
_stop = true;
}
const std::map<QString, int> &CategoryGrabber::GetCategories()
{
return _categoryMap;
}
void CategoryGrabber::run()
{
if (!_token) {
return;
emit Failed();
}
{
std::lock_guard<std::mutex> lock(_mtx);
if (_searchString.empty()) {
GetAll();
} else {
Search(_searchString);
}
}
emit Finished();
}
void CategoryGrabber::Search(const std::string &)
{
static const std::string uri = "https://api.twitch.tv";
const std::string path = "/helix/search/categories";
int startCount = _categoryMap.size();
std::string cursor;
httplib::Params params = {
{"first", "100"}, {"after", cursor}, {"query", _searchString}};
auto response = SendGetRequest(uri, path, *_token, params);
while (response.status == 200 && !_stop) {
cursor = ParseReply(response.data);
if (cursor.empty()) {
break; // End of category list
}
params = {{"first", "100"},
{"after", cursor},
{"query", _searchString}};
response = SendGetRequest(uri, path, *_token, params);
emit CategoryCountUpdated(_categoryMap.size() - startCount);
}
}
void CategoryGrabber::GetAll()
{
static const std::string uri = "https://api.twitch.tv";
const std::string path = "/helix/games/top";
// Declare static to "save" progress in case of cancel
static std::string cursor;
httplib::Params params = {{"first", "100"}, {"after", cursor}};
auto response = SendGetRequest(uri, path, *_token, params);
while (response.status == 200 && !_stop) {
cursor = ParseReply(response.data);
if (cursor.empty()) {
break; // End of category list
}
params = {{"first", "100"}, {"after", cursor}};
response = SendGetRequest(uri, path, *_token, params);
emit CategoryCountUpdated(_categoryMap.size());
}
}
std::string CategoryGrabber::ParseReply(obs_data_t *data) const
{
OBSDataArrayAutoRelease array = obs_data_get_array(data, "data");
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
int id = std::stoi(obs_data_get_string(arrayObj, "id"));
QString name = obs_data_get_string(arrayObj, "name");
_categoryMap.emplace(name, id);
}
OBSDataAutoRelease pagination = obs_data_get_obj(data, "pagination");
return obs_data_get_string(pagination, "cursor");
}
ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip)
: QDialog(parent),
_skipFetchCheckBox(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.twitchCategories.fetchSkip"))),
_status(new QLabel(obs_module_text(
"AdvSceneSwitcher.twitchCategories.fetchStart")))
{
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
_skipFetchCheckBox->setVisible(showSkip);
auto layout = new QVBoxLayout(this);
layout->addWidget(_status);
auto cancelButton = new QPushButton(
obs_module_text("AdvSceneSwitcher.twitchCategories.fetchStop"),
this);
layout->addWidget(_skipFetchCheckBox);
layout->addWidget(cancelButton);
setLayout(layout);
QWidget::connect(_skipFetchCheckBox, &QCheckBox::stateChanged, this,
[this](int value) { _skipFetch = value; });
QWidget::connect(cancelButton, &QPushButton::clicked, this,
[this]() { _skipFetch ? accept() : reject(); });
if (_skipFetch) {
accept();
}
}
void ProgressDialog::CategoryCountUpdated(int value)
{
_status->setText(
QString(obs_module_text(
"AdvSceneSwitcher.twitchCategories.fetchStatus"))
.arg(value));
}
TwitchCategorySearchButton::TwitchCategorySearchButton()
{
setMaximumWidth(22);
const std::string pathPrefix =
GetDataFilePath("res/images/" + GetThemeTypeName());
SetButtonIcon(this, (pathPrefix + "Search.svg").c_str());
setToolTip(obs_module_text(
"AdvSceneSwitcher.twitchCategories.manualSearch"));
QWidget::connect(this, SIGNAL(clicked()), this,
SLOT(StartManualCategorySearch()));
QWidget::connect(this, SIGNAL(RequestRepopulate()),
TwitchCategorySignalManager::Instance(),
SIGNAL(RepopulateRequired()));
}
void TwitchCategorySearchButton::SetToken(
const std::weak_ptr<TwitchToken> &token)
{
_token = token;
const bool expired = token.expired();
setDisabled(expired);
if (expired) {
setToolTip(obs_module_text(
"AdvSceneSwitcher.action.twitch.categorySelectionDisabled"));
} else {
setToolTip(obs_module_text(
"AdvSceneSwitcher.twitchCategories.manualSearch"));
}
}
void TwitchCategorySearchButton::StartManualCategorySearch()
{
std::string category;
bool accepted = AdvSSNameDialog::AskForName(
this,
obs_module_text("AdvSceneSwitcher.twitchCategories.search"),
obs_module_text("AdvSceneSwitcher.twitchCategories.name"),
category);
if (!accepted) {
return;
}
CategoryGrabber categoryGrabber;
auto *progressDialog = new ProgressDialog(this, false);
QWidget::connect(&categoryGrabber, SIGNAL(CategoryCountUpdated(int)),
progressDialog, SLOT(CategoryCountUpdated(int)));
QWidget::connect(&categoryGrabber, &CategoryGrabber::Finished, this,
[progressDialog]() { progressDialog->accept(); });
QWidget::connect(&categoryGrabber, &CategoryGrabber::Failed, this,
[progressDialog]() { progressDialog->reject(); });
auto previousCategoryCount = categoryGrabber.GetCategories().size();
categoryGrabber.Start(_token.lock(), category);
progressDialog->exec();
categoryGrabber.Stop();
categoryGrabber.wait();
emit RequestRepopulate();
progressDialog->deleteLater();
auto newCategoryCount =
categoryGrabber.GetCategories().size() - previousCategoryCount;
if (newCategoryCount == 0) {
DisplayMessage(
QString(obs_module_text(
"AdvSceneSwitcher.twitchCategories.searchFailed"))
.arg(QString::fromStdString(category)));
} else {
DisplayMessage(
QString(obs_module_text(
"AdvSceneSwitcher.twitchCategories.searchSuccess"))
.arg(QString::number(newCategoryCount),
QString::fromStdString(category)));
}
}
TwitchCategorySignalManager *TwitchCategorySignalManager::Instance()
{
static TwitchCategorySignalManager manager;
return &manager;
}
} // namespace advss

View File

@@ -0,0 +1,129 @@
#pragma once
#include <filter-combo-box.hpp>
#include <string>
#include <obs-data.h>
#include <QThread>
#include <QDialog>
#include <QLabel>
#include <QCheckBox>
#include <QPushButton>
namespace advss {
class TwitchToken;
struct TwitchCategory {
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
int id = -1;
std::string name = "-";
};
class CategoryGrabber : public QThread {
Q_OBJECT
public:
void Start(const std::shared_ptr<TwitchToken> &token,
const std::string searchString = "");
void Stop();
const std::map<QString, int> &GetCategories();
private:
signals:
void CategoryCountUpdated(int value);
void Finished();
void Failed();
private:
void run() override;
void Search(const std::string &);
void GetAll();
std::string ParseReply(obs_data_t *) const;
std::shared_ptr<TwitchToken> _token;
static std::map<QString, int> _categoryMap;
std::string _searchString = "";
bool _stop = false;
// Don't allow parallel search requests to not spam Twitch API
static std::mutex _mtx;
};
class ProgressDialog : public QDialog {
Q_OBJECT
public:
ProgressDialog(QWidget *parent, bool showSkip = true);
private slots:
void CategoryCountUpdated(int);
private:
QCheckBox *_skipFetchCheckBox;
QLabel *_status;
bool _skipFetch = false;
};
class TwitchCategorySelection : public FilterComboBox {
Q_OBJECT
public:
TwitchCategorySelection(QWidget *parent);
// Will *not* verify if ID is still valid or populate the selection
// list as that would take too long
void SetCategory(const TwitchCategory &);
// Used for populating the category list
void SetToken(const std::weak_ptr<TwitchToken> &);
private slots:
void SelectionChanged(int);
void PopulateFinished();
void UpdateCategoryList();
signals:
void CategoreyChanged(const TwitchCategory &);
protected:
void showPopup() override;
private:
void PopulateCategorySelection();
bool IsPopulated();
ProgressDialog *_progressDialog;
CategoryGrabber _categoryGrabber;
std::weak_ptr<TwitchToken> _token;
static bool _fetchingCategoriesDone;
static std::map<QString, int> _streamingCategories;
};
class TwitchCategorySearchButton : public QPushButton {
Q_OBJECT
public:
TwitchCategorySearchButton();
void SetToken(const std::weak_ptr<TwitchToken> &);
private slots:
void StartManualCategorySearch();
signals:
void RequestRepopulate();
private:
std::weak_ptr<TwitchToken> _token;
};
// Helper class to ease singal / slot handling
class TwitchCategorySignalManager : public QObject {
Q_OBJECT
public:
static TwitchCategorySignalManager *Instance();
signals:
void RepopulateRequired();
};
} // namespace advss

View File

@@ -0,0 +1,433 @@
#include "macro-action-twitch.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <utility.hpp>
namespace advss {
const std::string MacroActionTwitch::id = "twitch";
bool MacroActionTwitch::_registered = MacroActionFactory::Register(
MacroActionTwitch::id,
{MacroActionTwitch::Create, MacroActionTwitchEdit::Create,
"AdvSceneSwitcher.action.twitch"});
const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
{MacroActionTwitch::Action::TITLE,
"AdvSceneSwitcher.action.twitch.type.title"},
{MacroActionTwitch::Action::CATEGORY,
"AdvSceneSwitcher.action.twitch.type.category"},
{MacroActionTwitch::Action::MARKER,
"AdvSceneSwitcher.action.twitch.type.marker"},
{MacroActionTwitch::Action::CLIP,
"AdvSceneSwitcher.action.twitch.type.clip"},
{MacroActionTwitch::Action::COMMERCIAL,
"AdvSceneSwitcher.action.twitch.type.commercial"},
};
void MacroActionTwitch::SetStreamTitle(
const std::shared_ptr<TwitchToken> &token) const
{
if (std::string(_streamTitle).empty()) {
return;
}
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "title", _streamTitle.c_str());
auto result = SendPatchRequest(
"https://api.twitch.tv",
std::string("/helix/channels?broadcaster_id=") +
token->GetUserID(),
*token, data.Get());
if (result.status != 204) {
blog(LOG_INFO, "Failed to set stream title! (%d)",
result.status);
}
}
void MacroActionTwitch::SetStreamCategory(
const std::shared_ptr<TwitchToken> &token) const
{
if (_category.id == -1) {
return;
}
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "game_id",
std::to_string(_category.id).c_str());
auto result = SendPatchRequest(
"https://api.twitch.tv",
std::string("/helix/channels?broadcaster_id=") +
token->GetUserID(),
*token, data.Get());
if (result.status != 204) {
blog(LOG_INFO, "Failed to set stream category! (%d)",
result.status);
}
}
void MacroActionTwitch::CreateStreamMarker(
const std::shared_ptr<TwitchToken> &token) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "user_id", token->GetUserID().c_str());
if (!std::string(_markerDescription).empty()) {
obs_data_set_string(data, "description",
_markerDescription.c_str());
}
auto result = SendPostRequest("https://api.twitch.tv",
"/helix/streams/markers", *token,
data.Get());
if (result.status != 200) {
blog(LOG_INFO, "Failed to create marker! (%d)", result.status);
}
}
void MacroActionTwitch::CreateStreamClip(
const std::shared_ptr<TwitchToken> &token) const
{
OBSDataAutoRelease data = obs_data_create();
auto hasDelay = _clipHasDelay ? "true" : "false";
auto result = SendPostRequest(
"https://api.twitch.tv",
"/helix/clips?broadcaster_id=" + token->GetUserID() +
"&has_delay=" + hasDelay,
*token, data.Get());
if (result.status != 202) {
blog(LOG_INFO, "Failed to create clip! (%d)", result.status);
}
}
void MacroActionTwitch::StartCommercial(
const std::shared_ptr<TwitchToken> &token) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "broadcaster_id", token->GetUserID().c_str());
obs_data_set_int(data, "length", _duration.Seconds());
auto result = SendPostRequest("https://api.twitch.tv",
"/helix/channels/commercial", *token,
data.Get());
if (result.status != 200) {
OBSDataArrayAutoRelease replyArray =
obs_data_get_array(result.data, "data");
OBSDataAutoRelease replyData =
obs_data_array_item(replyArray, 0);
blog(LOG_INFO,
"Failed to start commercial! (%d)\n"
"length: %d\n"
"message: %s\n"
"retry_after: %d\n",
result.status, obs_data_get_int(replyData, "length"),
obs_data_get_string(replyData, "message"),
obs_data_get_int(replyData, "retry_after"));
}
}
bool MacroActionTwitch::PerformAction()
{
auto token = _token.lock();
if (!token) {
return true;
}
switch (_action) {
case MacroActionTwitch::Action::TITLE:
SetStreamTitle(token);
break;
case MacroActionTwitch::Action::CATEGORY:
SetStreamCategory(token);
break;
case MacroActionTwitch::Action::MARKER:
CreateStreamMarker(token);
break;
case MacroActionTwitch::Action::CLIP:
CreateStreamClip(token);
break;
case MacroActionTwitch::Action::COMMERCIAL:
StartCommercial(token);
break;
default:
break;
}
return true;
}
void MacroActionTwitch::LogAction() const
{
auto it = actionTypes.find(_action);
if (it != actionTypes.end()) {
vblog(LOG_INFO, "performed action \"%s\" with token for \"%s\"",
it->second.c_str(),
GetWeakTwitchTokenName(_token).c_str());
} else {
blog(LOG_WARNING, "ignored unknown twitch action %d",
static_cast<int>(_action));
}
}
bool MacroActionTwitch::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_action));
obs_data_set_string(obj, "token",
GetWeakTwitchTokenName(_token).c_str());
_streamTitle.Save(obj, "streamTitle");
_category.Save(obj);
_markerDescription.Save(obj, "markerDescription");
obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay);
_duration.Save(obj);
return true;
}
bool MacroActionTwitch::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token"));
_streamTitle.Load(obj, "streamTitle");
_category.Load(obj);
_markerDescription.Load(obj, "markerDescription");
_clipHasDelay = obs_data_get_bool(obj, "clipHasDelay");
_duration.Load(obj);
return true;
}
std::string MacroActionTwitch::GetShortDesc() const
{
return GetWeakTwitchTokenName(_token);
}
bool MacroActionTwitch::ActionIsSupportedByToken()
{
static const std::unordered_map<Action, TokenOption> requiredOption = {
{Action::TITLE, {"channel:manage:broadcast"}},
{Action::CATEGORY, {"channel:manage:broadcast"}},
{Action::MARKER, {"channel:manage:broadcast"}},
{Action::CLIP, {"clips:edit"}},
{Action::COMMERCIAL, {"channel:edit:commercial"}},
};
auto token = _token.lock();
if (!token) {
return false;
}
auto option = requiredOption.find(_action);
assert(option != requiredOption.end());
return token->OptionIsEnabled(option->second);
}
static inline void populateActionSelection(QComboBox *list)
{
for (const auto &[_, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget *parent, std::shared_ptr<MacroActionTwitch> entryData)
: QWidget(parent),
_actions(new QComboBox()),
_tokens(new TwitchConnectionSelection()),
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategorySelection(this)),
_manualCategorySearch(new TwitchCategorySearchButton()),
_markerDescription(new VariableLineEdit(this)),
_clipHasDelay(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.twitch.clip.hasDelay"))),
_duration(new DurationSelection(this, false, 0)),
_layout(new QHBoxLayout()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient")))
{
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_streamTitle->setMaxLength(140);
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
auto spinBox = _duration->SpinBox();
spinBox->setSuffix("s");
spinBox->setMaximum(180);
populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(TwitchTokenChanged(const QString &)));
QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this,
SLOT(StreamTitleChanged()));
QWidget::connect(_category,
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
SLOT(CategoreyChanged(const TwitchCategory &)));
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
SLOT(HasClipDelayChanged(const Duration &)));
QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.twitch.entry"),
_layout,
{{"{{account}}", _tokens},
{"{{actions}}", _actions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{manualCategorySearch}}", _manualCategorySearch},
{"{{markerDescription}}", _markerDescription},
{"{{clipHasDelay}}", _clipHasDelay},
{"{{duration}}", _duration}});
_layout->setContentsMargins(0, 0, 0, 0);
auto mainLayout = new QVBoxLayout();
mainLayout->addLayout(_layout);
mainLayout->addWidget(_tokenPermissionWarning);
setLayout(mainLayout);
_tokenPermissionCheckTimer.start(1000);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_token = GetWeakTwitchTokenByQString(token);
_category->SetToken(_entryData->_token);
_manualCategorySearch->SetToken(_entryData->_token);
SetupWidgetVisibility();
emit(HeaderInfoChanged(token));
}
void MacroActionTwitchEdit::StreamTitleChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_streamTitle = _streamTitle->text().toStdString();
}
void MacroActionTwitchEdit::CategoreyChanged(const TwitchCategory &category)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_category = category;
}
void MacroActionTwitchEdit::MarkerDescriptionChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_markerDescription =
_markerDescription->text().toStdString();
}
void MacroActionTwitchEdit::ClipHasDelayChanged(int state)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_clipHasDelay = state;
}
void MacroActionTwitchEdit::DurationChanged(const Duration &duration)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_duration = duration;
}
void MacroActionTwitchEdit::CheckTokenPermissions()
{
_tokenPermissionWarning->setVisible(
_entryData && !_entryData->ActionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::SetupWidgetVisibility()
{
_streamTitle->setVisible(_entryData->_action ==
MacroActionTwitch::Action::TITLE);
_category->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CATEGORY);
_manualCategorySearch->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CATEGORY);
_markerDescription->setVisible(_entryData->_action ==
MacroActionTwitch::Action::MARKER);
_clipHasDelay->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CLIP);
_duration->setVisible(_entryData->_action ==
MacroActionTwitch::Action::COMMERCIAL);
if (_entryData->_action == MacroActionTwitch::Action::TITLE ||
_entryData->_action == MacroActionTwitch::Action::MARKER) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
}
_tokenPermissionWarning->setVisible(
!_entryData->ActionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_tokens->SetToken(_entryData->_token);
_streamTitle->setText(_entryData->_streamTitle);
_category->SetToken(_entryData->_token);
_manualCategorySearch->SetToken(_entryData->_token);
_category->SetCategory(_entryData->_category);
_markerDescription->setText(_entryData->_markerDescription);
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
_duration->SetDuration(_entryData->_duration);
SetupWidgetVisibility();
}
void MacroActionTwitchEdit::ActionChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionTwitch::Action>(value);
SetupWidgetVisibility();
}
} // namespace advss

View File

@@ -0,0 +1,104 @@
#pragma once
#include "macro-action-edit.hpp"
#include "token.hpp"
#include "category-selection.hpp"
#include <variable-line-edit.hpp>
#include <duration-control.hpp>
namespace advss {
class MacroActionTwitch : public MacroAction {
public:
MacroActionTwitch(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m)
{
return std::make_shared<MacroActionTwitch>(m);
}
bool ActionIsSupportedByToken();
enum class Action {
TITLE,
CATEGORY,
MARKER,
CLIP,
COMMERCIAL,
};
Action _action = Action::TITLE;
std::weak_ptr<TwitchToken> _token;
StringVariable _streamTitle =
obs_module_text("AdvSceneSwitcher.action.twitch.title.title");
TwitchCategory _category;
StringVariable _markerDescription = obs_module_text(
"AdvSceneSwitcher.action.twitch.marker.description");
bool _clipHasDelay = false;
Duration _duration = 60;
private:
void SetStreamTitle(const std::shared_ptr<TwitchToken> &) const;
void SetStreamCategory(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamMarker(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
static bool _registered;
static const std::string id;
};
class MacroActionTwitchEdit : public QWidget {
Q_OBJECT
public:
MacroActionTwitchEdit(
QWidget *parent,
std::shared_ptr<MacroActionTwitch> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionTwitchEdit(
parent,
std::dynamic_pointer_cast<MacroActionTwitch>(action));
}
private slots:
void ActionChanged(int);
void TwitchTokenChanged(const QString &);
void StreamTitleChanged();
void CategoreyChanged(const TwitchCategory &);
void MarkerDescriptionChanged();
void ClipHasDelayChanged(int state);
void DurationChanged(const Duration &);
void CheckTokenPermissions();
signals:
void HeaderInfoChanged(const QString &);
protected:
std::shared_ptr<MacroActionTwitch> _entryData;
private:
void SetupWidgetVisibility();
QComboBox *_actions;
TwitchConnectionSelection *_tokens;
VariableLineEdit *_streamTitle;
TwitchCategorySelection *_category;
TwitchCategorySearchButton *_manualCategorySearch;
VariableLineEdit *_markerDescription;
QCheckBox *_clipHasDelay;
DurationSelection *_duration;
QHBoxLayout *_layout;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
bool _loading = true;
};
} // namespace advss

View File

@@ -0,0 +1,640 @@
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <switcher-data.hpp>
#include <utility.hpp>
#include <QScrollArea>
#include <QDesktopServices>
namespace advss {
static std::deque<std::shared_ptr<Item>> twitchTokens;
const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
// Add necessary token permissions here
/*
{"analytics:read:extensions",
"AdvSceneSwitcher.twitchToken.analytics.readExtensions"},
{"analytics:read:games",
"AdvSceneSwitcher.twitchToken.analytics.readGames"},
{"bits:read", "AdvSceneSwitcher.twitchToken.bits.read"},
*/
{"channel:manage:broadcast",
"AdvSceneSwitcher.twitchToken.channel.manageBroadcast"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"},
{"channel:edit:commercial",
"AdvSceneSwitcher.twitchToken.channel.startCommercial"},
};
static void saveConnections(obs_data_t *obj);
static void loadConnections(obs_data_t *obj);
bool setupTwitchTokenSupport()
{
GetSwitcher()->AddSaveStep(saveConnections);
GetSwitcher()->AddLoadStep(loadConnections);
return true;
}
bool TwitchToken::_setup = setupTwitchTokenSupport();
static void saveConnections(obs_data_t *obj)
{
OBSDataArrayAutoRelease connectionArray = obs_data_array_create();
for (const auto &c : twitchTokens) {
OBSDataAutoRelease arrayObj = obs_data_create();
c->Save(arrayObj);
obs_data_array_push_back(connectionArray, arrayObj);
}
obs_data_set_array(obj, "twitchConnections", connectionArray);
}
static void loadConnections(obs_data_t *obj)
{
twitchTokens.clear();
OBSDataArrayAutoRelease connectionArray =
obs_data_get_array(obj, "twitchConnections");
size_t count = obs_data_array_count(connectionArray);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj =
obs_data_array_item(connectionArray, i);
auto con = TwitchToken::Create();
twitchTokens.emplace_back(con);
twitchTokens.back()->Load(arrayObj);
}
}
void TokenOption::Load(obs_data_t *obj)
{
apiId = obs_data_get_string(obj, "apiID");
}
void TokenOption::Save(obs_data_t *obj) const
{
obs_data_set_string(obj, "apiID", apiId.c_str());
}
std::string TokenOption::GetLocale() const
{
return apiIdToLocale.at(apiId);
}
const std::unordered_map<std::string, std::string> &
TokenOption::GetTokenOptionMap()
{
return apiIdToLocale;
}
bool TokenOption::operator<(const TokenOption &other) const
{
return apiId < other.apiId;
}
void TwitchToken::Load(obs_data_t *obj)
{
Item::Load(obj);
_token = obs_data_get_string(obj, "token");
_userID = obs_data_get_string(obj, "userID");
_tokenOptions.clear();
OBSDataArrayAutoRelease options = obs_data_get_array(obj, "options");
size_t count = obs_data_array_count(options);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(options, i);
TokenOption tokenOption;
tokenOption.Load(arrayObj);
_tokenOptions.insert(tokenOption);
}
}
void TwitchToken::Save(obs_data_t *obj) const
{
Item::Save(obj);
obs_data_set_string(obj, "token", _token.c_str());
obs_data_set_string(obj, "userID", _userID.c_str());
OBSDataArrayAutoRelease options = obs_data_array_create();
for (auto &option : _tokenOptions) {
OBSDataAutoRelease arrayObj = obs_data_create();
option.Save(arrayObj);
obs_data_array_push_back(options, arrayObj);
}
obs_data_set_array(obj, "options", options);
}
bool TwitchToken::OptionIsEnabled(const TokenOption &option) const
{
for (const auto &activeOption : _tokenOptions) {
if (activeOption.apiId == option.apiId) {
return true;
}
}
return false;
}
void TwitchToken::SetToken(const std::string &value)
{
_token = value;
auto res =
SendGetRequest("https://api.twitch.tv", "/helix/users", *this);
if (res.status != 200) {
blog(LOG_WARNING, "failed to get Twitch user id from token!");
_userID = -1;
return;
}
OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data");
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
_userID = obs_data_get_string(arrayObj, "id");
_name = obs_data_get_string(arrayObj, "display_name");
}
}
TwitchToken *GetTwitchTokenByName(const QString &name)
{
return GetTwitchTokenByName(name.toStdString());
}
TwitchToken *GetTwitchTokenByName(const std::string &name)
{
for (auto &t : twitchTokens) {
if (t->Name() == name) {
return dynamic_cast<TwitchToken *>(t.get());
}
}
return nullptr;
}
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByName(const std::string &name)
{
for (const auto &t : twitchTokens) {
if (t->Name() == name) {
std::weak_ptr<TwitchToken> wp =
std::dynamic_pointer_cast<TwitchToken>(t);
return wp;
}
}
return std::weak_ptr<TwitchToken>();
}
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByQString(const QString &name)
{
return GetWeakTwitchTokenByName(name.toStdString());
}
std::string GetWeakTwitchTokenName(std::weak_ptr<TwitchToken> token)
{
auto con = token.lock();
if (!con) {
return obs_module_text("AdvSceneSwitcher.twitchToken.invalid");
}
return con->Name();
}
static bool ConnectionNameAvailable(const QString &name)
{
return !GetTwitchTokenByName(name);
}
static bool ConnectionNameAvailable(const std::string &name)
{
return ConnectionNameAvailable(QString::fromStdString(name));
}
static bool AskForSettingsWrapper(QWidget *parent, Item &settings)
{
TwitchToken &ConnectionSettings = dynamic_cast<TwitchToken &>(settings);
return TwitchTokenSettingsDialog::AskForSettings(parent,
ConnectionSettings);
}
TwitchConnectionSelection::TwitchConnectionSelection(QWidget *parent)
: ItemSelection(twitchTokens, TwitchToken::Create,
AskForSettingsWrapper,
"AdvSceneSwitcher.twitchToken.select",
"AdvSceneSwitcher.twitchToken.add",
"AdvSceneSwitcher.twitchToken.nameNotAvailable",
"AdvSceneSwitcher.twitchToken.configure", parent)
{
ShowRenameContextMenu(false);
// Connect to slots
QWidget::connect(TwitchConnectionSignalManager::Instance(),
SIGNAL(Add(const QString &)), this,
SLOT(AddItem(const QString &)));
QWidget::connect(TwitchConnectionSignalManager::Instance(),
SIGNAL(Remove(const QString &)), this,
SLOT(RemoveItem(const QString &)));
// Forward signals
QWidget::connect(this, SIGNAL(ItemAdded(const QString &)),
TwitchConnectionSignalManager::Instance(),
SIGNAL(Add(const QString &)));
QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)),
TwitchConnectionSignalManager::Instance(),
SIGNAL(Remove(const QString &)));
}
void TwitchConnectionSelection::SetToken(const std::string &token)
{
const QSignalBlocker blocker(_selection);
if (!!GetTwitchTokenByName(token)) {
_selection->setCurrentText(QString::fromStdString(token));
} else {
_selection->setCurrentIndex(-1);
}
}
void TwitchConnectionSelection::SetToken(
const std::weak_ptr<TwitchToken> &token_)
{
const QSignalBlocker blocker(_selection);
auto token = token_.lock();
if (token) {
SetToken(token->Name());
} else {
_selection->setCurrentIndex(-1);
}
}
static QCheckBox *addOption(const TokenOption &option, const TwitchToken &token,
QGridLayout *layout, int &row)
{
auto label = new QLabel(obs_module_text(option.GetLocale().c_str()));
label->setWordWrap(true);
layout->addWidget(label, row, 1);
auto checkBox = new QCheckBox();
checkBox->setChecked(token.OptionIsEnabled(option));
layout->addWidget(checkBox, row, 0);
row++;
return checkBox;
}
TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
QWidget *parent, const TwitchToken &settings)
: ItemSettingsDialog(settings, twitchTokens,
"AdvSceneSwitcher.twitchToken.select",
"AdvSceneSwitcher.twitchToken.add",
"AdvSceneSwitcher.twitchToken.nameNotAvailable",
parent),
_requestToken(new QPushButton(
obs_module_text("AdvSceneSwitcher.twitchToken.request"))),
_showToken(new QPushButton()),
_currentTokenValue(new QLineEdit()),
_tokenStatus(new QLabel())
{
_showToken->setMaximumWidth(22);
_showToken->setFlat(true);
_showToken->setStyleSheet(
"QPushButton { background-color: transparent; border: 0px }");
_currentTokenValue->setReadOnly(true);
_currentTokenValue->setText(QString::fromStdString(settings._token));
_name->setReadOnly(true);
QWidget::connect(_requestToken, SIGNAL(clicked()), this,
SLOT(RequestToken()));
QWidget::connect(_showToken, SIGNAL(pressed()), this,
SLOT(ShowToken()));
QWidget::connect(_showToken, SIGNAL(released()), this,
SLOT(HideToken()));
QWidget::connect(&_tokenGrabber, &TokenGrabberThread::GotToken, this,
&TwitchTokenSettingsDialog::GotToken);
auto generalSettingsGrid = new QGridLayout();
int row = 0;
generalSettingsGrid->addWidget(
new QLabel(
obs_module_text("AdvSceneSwitcher.twitchToken.name")),
row, 0);
auto nameLayout = new QHBoxLayout;
nameLayout->addWidget(_name);
nameLayout->addWidget(_nameHint);
generalSettingsGrid->addLayout(nameLayout, row, 1);
++row;
generalSettingsGrid->addWidget(
new QLabel(
obs_module_text("AdvSceneSwitcher.twitchToken.value")),
row, 0);
auto tokenValueLayout = new QHBoxLayout;
tokenValueLayout->addWidget(_currentTokenValue);
tokenValueLayout->addWidget(_showToken);
generalSettingsGrid->addLayout(tokenValueLayout, row, 1);
++row;
generalSettingsGrid->addWidget(_requestToken, row, 0);
generalSettingsGrid->addWidget(_tokenStatus, row, 1);
auto optionsGrid = new QGridLayout();
row = 0;
auto optionsBox = new QGroupBox(
obs_module_text("AdvSceneSwitcher.twitchToken.permissions"));
for (const auto &[id, _] : TokenOption::GetTokenOptionMap()) {
auto checkBox = addOption({id}, settings, optionsGrid, row);
QWidget::connect(checkBox, SIGNAL(stateChanged(int)), this,
SLOT(TokenOptionChanged(int)));
_optionWidgets[id] = checkBox;
}
MinimizeSizeOfColumn(optionsGrid, 0);
optionsBox->setLayout(optionsGrid);
auto scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto contentWidget = new QWidget(scrollArea);
auto layout = new QVBoxLayout(contentWidget);
layout->addLayout(generalSettingsGrid);
layout->addWidget(optionsBox);
layout->setContentsMargins(0, 0, 0, 0);
scrollArea->setWidget(contentWidget);
auto dialogLayout = new QVBoxLayout();
dialogLayout->addWidget(scrollArea);
dialogLayout->addWidget(_buttonbox);
setLayout(dialogLayout);
_currentTokenValue->setText(QString::fromStdString(settings._token));
if (settings._token.empty()) {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.notSet"));
}
HideToken();
if (_name->text().isEmpty()) {
PulseWidget(_requestToken, Qt::green, QColor(0, 0, 0, 0), true);
}
_currentToken = settings;
}
void TwitchTokenSettingsDialog::ShowToken()
{
SetButtonIcon(_showToken, ":res/images/visible.svg");
_currentTokenValue->setEchoMode(QLineEdit::Normal);
}
void TwitchTokenSettingsDialog::HideToken()
{
SetButtonIcon(_showToken, ":res/images/invisible.svg");
_currentTokenValue->setEchoMode(QLineEdit::PasswordEchoOnEdit);
}
void TwitchTokenSettingsDialog::TokenOptionChanged(int)
{
if (!_name->text().isEmpty()) {
PulseWidget(_requestToken, Qt::green, QColor(0, 0, 0, 0), true);
}
_name->setText("");
QMetaObject::invokeMethod(this, "NameChanged",
Q_ARG(const QString &, ""));
_currentTokenValue->setText("");
}
static std::string generateStateString()
{
const char *chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
const size_t stateStringLen = 32;
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<size_t> dis(0, sizeof(chars) - 2);
std::string state;
state.reserve(stateStringLen);
for (size_t i = 0; i < stateStringLen; ++i) {
state += chars[dis(gen)];
}
return state;
}
static std::string generateScopeString(const std::set<TokenOption> &options)
{
if (options.empty()) {
return "";
}
std::string scope;
for (const auto &option : options) {
scope += option.apiId + "+";
}
scope.pop_back(); // Remove trailing +
return scope;
}
static std::string getHtml(const QString &redirect)
{
const char *html = R"(
<html>
<head>
<title>Advanced scene switcher</title>
</head>
<body>
<div id="output">Please click this link to continue if not automatically redirected</div>
<p><a href="%1">Login with Twitch</a></a></p>
<script type="text/javascript">
if (document.location.hash && document.location.hash != '') {
var parsedHash = new URLSearchParams(window.location.hash.slice(1));
if (parsedHash.get('access_token')) {
window.location.replace(`http://localhost:8080/token?access_token=${parsedHash.get('access_token')}&state=${parsedHash.get('state')}`);
output.textContent = 'It is safe to close this window';
}
} else {
window.location.replace('%1');
}
</script>
</body>
</html>)";
return QString(html).arg(redirect).toStdString();
}
void TwitchTokenSettingsDialog::RequestToken()
{
// Don't allow parallel RequestToken() calls
_requestToken->setDisabled(true);
auto scope = QString::fromStdString(
generateScopeString(GetEnabledOptions()));
_tokenGrabber.SetTokenScope(scope);
_tokenGrabber.start();
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.waiting"));
}
void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
{
_currentTokenValue->setText(value.value_or(""));
if (value.has_value()) {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.success"));
_currentToken.SetToken(value.value().toStdString());
auto name = QString::fromStdString(_currentToken._name);
_name->setText(name);
_name->textEdited(name);
} else {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.fail"));
_name->setText("");
}
_requestToken->setEnabled(true);
}
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()
{
std::set<TokenOption> result;
for (const auto &[id, checkBox] : _optionWidgets) {
if (checkBox->isChecked()) {
TokenOption option;
option.apiId = id;
result.emplace(option);
}
}
return result;
}
bool TwitchTokenSettingsDialog::AskForSettings(QWidget *parent,
TwitchToken &settings)
{
TwitchTokenSettingsDialog dialog(parent, settings);
dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
if (dialog.exec() != DialogCode::Accepted) {
return false;
}
settings = dialog._currentToken;
settings._tokenOptions = dialog.GetEnabledOptions();
return true;
}
int TokenGrabberThread::_timeout = 15;
TokenGrabberThread::~TokenGrabberThread()
{
_stopWaiting = true;
_cv.notify_all();
Stop();
_server.stop();
}
static std::string getAuthErrorString(const char *errDetail)
{
QString err = obs_module_text(
"AdvSceneSwitcher.twitchToken.request.fail.browser");
return err.arg(obs_module_text(errDetail)).toStdString();
}
void TokenGrabberThread::run()
{
// Reset
_server.stop();
_server.~Server();
new (&_server) httplib::Server();
if (_serverThread.joinable()) {
_serverThread.join();
}
_stopWaiting = {false};
// Generate URI to request token
auto state = generateStateString();
auto getTokenURI = "https://id.twitch.tv/oauth2/authorize"
"?response_type=token"
"&client_id=" +
QString(GetClientID()) +
"&redirect_uri=http://localhost:8080/auth"
"&scope=" +
_scope + "&state=" + QString::fromStdString(state);
// Setup server receiving token string
auto html = getHtml(getTokenURI);
_server.Get("/auth", [html, state](const httplib::Request &req,
httplib::Response &res) {
// Check for errors
if (req.has_param("error")) {
auto recvState = req.get_param_value("state");
if (recvState != state) {
blog(LOG_WARNING,
"state string does not match in error handling?! "
"Got \"%s\" - expected \"%s\"\n"
"ignoring error ...",
recvState.c_str(), state.c_str());
return;
}
auto errorStr =
req.get_param_value("error_description");
res.set_content(getAuthErrorString(errorStr.c_str()),
"text/plain");
return;
}
// Parse fragments and redirect to /token with
// corresponding parameters.
res.set_content(html, "text/html");
});
_server.Get("/token", [&](const httplib::Request &req,
httplib::Response &res) {
// Check if valid request and grab the token string
std::lock_guard<std::mutex> lk(_mutex);
auto recvState = req.get_param_value("state");
if (recvState != state) {
blog(LOG_WARNING,
"state string does not match! "
"Got \"%s\" - expected \"%s\"",
recvState.c_str(), state.c_str());
res.set_content(
getAuthErrorString(
"AdvSceneSwitcher.twitchToken.request.fail.stateMismatch"),
"text/plain");
} else {
_tokenString = QString::fromStdString(
req.get_param_value("access_token"));
res.set_content(
obs_module_text(
"AdvSceneSwitcher.twitchToken.request.success.browser"),
"text/plain");
}
_stopWaiting = true;
_cv.notify_all();
});
// Request user to grant token
QDesktopServices::openUrl(getTokenURI);
// Start the server and wait
std::unique_lock<std::mutex> lock(_mutex);
_serverThread =
std::thread([this]() { _server.listen("localhost", 8080); });
auto time = std::chrono::high_resolution_clock::now() +
std::chrono::seconds(_timeout);
while (!_stopWaiting) {
if (_cv.wait_until(lock, time) == std::cv_status::timeout) {
break;
}
}
_server.stop();
emit GotToken(_tokenString);
}
void TokenGrabberThread::Stop()
{
if (_server.is_running()) {
_server.stop();
}
if (_serverThread.joinable()) {
_serverThread.join();
}
wait();
}
TwitchConnectionSignalManager *TwitchConnectionSignalManager::Instance()
{
static TwitchConnectionSignalManager manager;
return &manager;
}
} // namespace advss

View File

@@ -0,0 +1,141 @@
#pragma once
#include <item-selection-helpers.hpp>
#include <httplib.h>
#include <set>
#include <QCheckBox>
#include <QThread>
#include <optional>
namespace advss {
class TwitchConnectionSelection;
class TwitchTokenSettingsDialog;
class TokenOption {
public:
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
std::string GetLocale() const;
static const std::unordered_map<std::string, std::string> &
GetTokenOptionMap();
bool operator<(const TokenOption &other) const;
std::string apiId = "";
private:
const static std::unordered_map<std::string, std::string> apiIdToLocale;
};
class TwitchToken : public Item {
public:
static std::shared_ptr<Item> Create()
{
return std::make_shared<TwitchToken>();
}
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
std::string GetName() { return _name; }
bool OptionIsEnabled(const TokenOption &) const;
void SetToken(const std::string &);
bool IsEmpty() const { return _token.empty(); }
std::string GetToken() const { return _token; }
std::string GetUserID() const { return _userID; }
private:
std::string _token;
std::string _userID;
std::set<TokenOption> _tokenOptions = {{"channel:manage:broadcast"}};
static bool _setup;
friend TwitchConnectionSelection;
friend TwitchTokenSettingsDialog;
};
class TokenGrabberThread : public QThread {
Q_OBJECT
public:
~TokenGrabberThread();
void SetTokenScope(const QString &value) { _scope = value; }
signals:
void GotToken(const std::optional<QString> &);
protected:
void run() override;
private:
void Stop();
QString _scope;
std::optional<QString> _tokenString;
static int _timeout;
std::mutex _mutex;
std::atomic_bool _stopWaiting = {false};
std::condition_variable _cv;
std::thread _serverThread;
httplib::Server _server;
};
class TwitchTokenSettingsDialog : public ItemSettingsDialog {
Q_OBJECT
public:
TwitchTokenSettingsDialog(QWidget *parent, const TwitchToken &);
static bool AskForSettings(QWidget *parent, TwitchToken &settings);
private slots:
void ShowToken();
void HideToken();
void TokenOptionChanged(int);
void RequestToken();
void GotToken(const std::optional<QString> &);
private:
std::set<TokenOption> GetEnabledOptions();
QPushButton *_requestToken;
QPushButton *_showToken;
QLineEdit *_currentTokenValue;
QLabel *_tokenStatus;
TokenGrabberThread _tokenGrabber;
TwitchToken _currentToken;
std::unordered_map<std::string, QCheckBox *> _optionWidgets;
};
class TwitchConnectionSelection : public ItemSelection {
Q_OBJECT
public:
TwitchConnectionSelection(QWidget *parent = 0);
void SetToken(const std::string &);
void SetToken(const std::weak_ptr<TwitchToken> &);
};
// Helper class so that it is not required to add signals to the
// AdvSceneSwitcher class for handling adding and removing Twitch connections
class TwitchConnectionSignalManager : public QObject {
Q_OBJECT
public:
static TwitchConnectionSignalManager *Instance();
private:
signals:
// Rename signal not required as name is based on Twitch account name
// and item name cannot be manually changed
void Add(const QString &);
void Remove(const QString &);
};
TwitchToken *GetTwitchTokenByName(const QString &);
TwitchToken *GetTwitchTokenByName(const std::string &);
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByName(const std::string &name);
std::weak_ptr<TwitchToken> GetWeakTwitchTokenByQString(const QString &name);
std::string GetWeakTwitchTokenName(std::weak_ptr<TwitchToken>);
} // namespace advss
Q_DECLARE_METATYPE(advss::TwitchToken *);

View File

@@ -0,0 +1,95 @@
#include "twitch-helpers.hpp"
#include "token.hpp"
namespace advss {
static constexpr std::string_view clientID = "ds5tt4ogliifsqc04mz3d3etnck3e5";
static httplib::Headers getTokenRequestHeaders(const TwitchToken &token)
{
return {
{"Authorization", "Bearer " + token.GetToken()},
{"Client-Id", clientID.data()},
};
}
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const TwitchToken &token,
const httplib::Params &params)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto response = cli.Get(path, params, headers);
if (!response) {
auto err = response.error();
blog(LOG_INFO, "%s failed - %s", __func__,
httplib::to_string(err).c_str());
return {};
}
RequestResult result;
result.status = response->status;
if (response->body.empty()) {
return result;
}
OBSDataAutoRelease replyData =
obs_data_create_from_json(response->body.c_str());
result.data = replyData;
return result;
}
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto json = obs_data_get_json(data);
std::string body = json ? json : "";
auto response = cli.Post(path, headers, body, "application/json");
if (!response) {
auto err = response.error();
blog(LOG_INFO, "%s failed - %s", __func__,
httplib::to_string(err).c_str());
return {};
}
RequestResult result;
result.status = response->status;
if (response->body.empty()) {
return result;
}
OBSDataAutoRelease replyData =
obs_data_create_from_json(response->body.c_str());
result.data = replyData;
return result;
}
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto json = obs_data_get_json(data);
std::string body = json ? json : "";
auto response = cli.Patch(path, headers, body, "application/json");
if (!response) {
auto err = response.error();
blog(LOG_INFO, "%s failed - %s", __func__,
httplib::to_string(err).c_str());
return {};
}
RequestResult result;
result.status = response->status;
if (response->body.empty()) {
return result;
}
OBSDataAutoRelease replyData =
obs_data_create_from_json(response->body.c_str());
result.data = replyData;
return result;
}
const char *GetClientID()
{
return clientID.data();
}
} // namespace advss

View File

@@ -0,0 +1,24 @@
#pragma once
#include <httplib.h>
#include <obs.hpp>
#include <string>
namespace advss {
class TwitchToken;
struct RequestResult {
int status = 0;
OBSData data = nullptr;
};
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const TwitchToken &token,
const httplib::Params & = {});
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data);
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data);
const char *GetClientID();
} // namespace advss

View File

@@ -104,7 +104,9 @@ static bool requiresFileInput(VideoCondition t)
bool MacroConditionVideo::CheckShouldBeSkipped()
{
if (_condition != VideoCondition::PATTERN &&
_condition != VideoCondition::OBJECT) {
_condition != VideoCondition::OBJECT &&
_condition != VideoCondition::HAS_CHANGED &&
_condition != VideoCondition::HAS_NOT_CHANGED) {
return false;
}
@@ -315,19 +317,17 @@ bool MacroConditionVideo::CheckOCR()
auto text = RunOCR(_ocrParameters.GetOCR(), _screenshotData.image,
_ocrParameters.color, _ocrParameters.colorThreshold);
if (_ocrParameters.regex.Enabled()) {
auto expr = _ocrParameters.regex.GetRegularExpression(
_ocrParameters.text);
if (!expr.isValid()) {
return false;
}
auto match = expr.match(QString::fromStdString(text));
return match.hasMatch();
}
SetVariableValue(text);
return text == std::string(_ocrParameters.text);
if (!_ocrParameters.regex.Enabled()) {
return text == std::string(_ocrParameters.text);
}
auto expr =
_ocrParameters.regex.GetRegularExpression(_ocrParameters.text);
if (!expr.isValid()) {
return false;
}
auto match = expr.match(QString::fromStdString(text));
return match.hasMatch();
}
bool MacroConditionVideo::CheckColor()
@@ -1454,7 +1454,9 @@ static bool needsShowMatch(VideoCondition cond)
static bool needsThrottleControls(VideoCondition cond)
{
return cond == VideoCondition::PATTERN ||
cond == VideoCondition::OBJECT;
cond == VideoCondition::OBJECT ||
cond == VideoCondition::HAS_CHANGED ||
cond == VideoCondition::HAS_NOT_CHANGED;
}
static bool needsThreshold(VideoCondition cond)

View File

@@ -168,11 +168,22 @@ void SwitcherData::SaveVersion(obs_data_t *obj,
obs_data_set_string(obj, "version", currentVersion.c_str());
}
void SwitcherData::AddResetForNextIntervalFunction(
std::function<void()> function)
void SwitcherData::AddIntervalResetStep(std::function<void()> function)
{
std::lock_guard<std::mutex> lock(switcher->m);
resetForNextIntervalFuncs.emplace_back(function);
resetIntervalSteps.emplace_back(function);
}
void SwitcherData::AddSaveStep(std::function<void(obs_data_t *)> function)
{
std::lock_guard<std::mutex> lock(switcher->m);
saveSteps.emplace_back(function);
}
void SwitcherData::AddLoadStep(std::function<void(obs_data_t *)> function)
{
std::lock_guard<std::mutex> lock(switcher->m);
loadSteps.emplace_back(function);
}
} // namespace advss

View File

@@ -64,7 +64,9 @@ public:
void SetPreconditions();
void ResetForNextInterval();
void AddResetForNextIntervalFunction(std::function<void()>);
void AddSaveStep(std::function<void(obs_data_t *)>);
void AddLoadStep(std::function<void(obs_data_t *)>);
void AddIntervalResetStep(std::function<void()>);
bool CheckForMatch(OBSWeakSource &scene, OBSWeakSource &transition,
int &linger, bool &setPreviousSceneAsMatch,
bool &macroMatch);
@@ -112,7 +114,9 @@ public:
std::atomic_bool abortMacroWait = {false};
std::condition_variable macroTransitionCv;
std::vector<std::function<void()>> resetForNextIntervalFuncs;
std::vector<std::function<void(obs_data_t *)>> saveSteps;
std::vector<std::function<void(obs_data_t *)>> loadSteps;
std::vector<std::function<void()>> resetIntervalSteps;
bool firstBoot = true;
bool transitionActive = false;
@@ -204,12 +208,12 @@ public:
QStringList loadFailureLibs;
bool warnPluginLoadFailure = true;
bool disableHints = false;
bool disableFilterComboboxFilter = false;
bool hideLegacyTabs = true;
std::vector<int> tabOrder = std::vector<int>(tab_count);
bool saveWindowGeo = false;
QPoint windowPos = {};
QSize windowSize = {};
QList<int> macroActionConditionSplitterPosition;
QList<int> macroListMacroEditSplitterPosition;
/* --- End of UI section --- */

View File

@@ -221,7 +221,7 @@ std::string GetWeakConnectionName(std::weak_ptr<Connection> connection)
{
auto con = connection.lock();
if (!con) {
return "invalid connection selection";
return obs_module_text("AdvSceneSwitcher.connection.invalid");
}
return con->Name();
}
@@ -248,6 +248,7 @@ ConnectionSelection::ConnectionSelection(QWidget *parent)
AskForSettingsWrapper,
"AdvSceneSwitcher.connection.select",
"AdvSceneSwitcher.connection.add",
"AdvSceneSwitcher.item.nameNotAvailable",
"AdvSceneSwitcher.connection.configure", parent)
{
// Connect to slots
@@ -297,7 +298,8 @@ ConnectionSettingsDialog::ConnectionSettingsDialog(QWidget *parent,
const Connection &settings)
: ItemSettingsDialog(settings, switcher->connections,
"AdvSceneSwitcher.connection.select",
"AdvSceneSwitcher.connection.add", parent),
"AdvSceneSwitcher.connection.add",
"AdvSceneSwitcher.item.nameNotAvailable", parent),
_useCustomURI(new QCheckBox()),
_customUri(new QLineEdit()),
_address(new QLineEdit()),

View File

@@ -0,0 +1,8 @@
#pragma once
// Helpers helper to enable exporting and importing custom Qt widget symbols
#ifdef ADVSS_EXPORT_SYMBOLS
#define ADVSS_EXPORT Q_DECL_EXPORT
#else
#define ADVSS_EXPORT Q_DECL_IMPORT
#endif

View File

@@ -7,15 +7,41 @@
namespace advss {
bool FilterComboBox::_filteringEnabled = false;
FilterComboBox::FilterComboBox(QWidget *parent, const QString &placehodler)
: QComboBox(parent)
{
// If the filtering behaviour of the FilterComboBox is disabled it is
// just a regular QComboBox with the option to set a placeholder so exit
// the constructor early.
if (!_filteringEnabled) {
if (!placehodler.isEmpty()) {
setPlaceholderText(placehodler);
}
return;
}
// Allow edit for completer but don't add new entries on pressing enter
setEditable(true);
setInsertPolicy(InsertPolicy::NoInsert);
if (!placehodler.isEmpty()) {
lineEdit()->setPlaceholderText(placehodler);
// Make sure that the placeholder text is visible
QFontMetrics fontMetrics(font());
int textWidth = fontMetrics.boundingRect(placehodler).width();
QStyleOptionComboBox comboBoxOption;
comboBoxOption.initFrom(this);
int buttonWidth =
style()->subControlRect(QStyle::CC_ComboBox,
&comboBoxOption,
QStyle::SC_ComboBoxArrow, this)
.width();
setMinimumWidth(buttonWidth + textWidth);
}
setMaxVisibleItems(30);
@@ -31,6 +57,11 @@ FilterComboBox::FilterComboBox(QWidget *parent, const QString &placehodler)
&FilterComboBox::TextChagned);
}
void FilterComboBox::SetFilterBehaviourEnabled(bool value)
{
FilterComboBox::_filteringEnabled = value;
}
void FilterComboBox::focusOutEvent(QFocusEvent *event)
{
// Reset on invalid selection

View File

@@ -1,15 +1,18 @@
#pragma once
#include "export-symbol-helper.hpp"
#include <QComboBox>
namespace advss {
// Helper class which enables user to filter possible selections by typing
class FilterComboBox : public QComboBox {
class ADVSS_EXPORT FilterComboBox : public QComboBox {
Q_OBJECT
public:
FilterComboBox(QWidget *parent = nullptr,
const QString &placehodler = "");
static void SetFilterBehaviourEnabled(bool);
protected:
void focusOutEvent(QFocusEvent *event) override;
@@ -20,6 +23,7 @@ private slots:
private:
int _lastCompleterHighlightRow = -1;
static bool _filteringEnabled;
};
} // namespace advss

View File

@@ -46,6 +46,7 @@ static bool ItemNameAvailable(const std::string &name,
ItemSelection::ItemSelection(std::deque<std::shared_ptr<Item>> &items,
CreateItemFunc create, SettingsCallback callback,
std::string_view select, std::string_view add,
std::string_view conflict,
std::string_view configureTooltip, QWidget *parent)
: QWidget(parent),
_selection(new FilterComboBox(this, obs_module_text(select.data()))),
@@ -54,7 +55,8 @@ ItemSelection::ItemSelection(std::deque<std::shared_ptr<Item>> &items,
_askForSettings(callback),
_items(items),
_selectStr(select),
_addStr(add)
_addStr(add),
_conflictStr(conflict)
{
_modify->setMaximumWidth(22);
SetButtonIcon(_modify, ":/settings/images/settings/general.svg");
@@ -94,6 +96,11 @@ void ItemSelection::SetItem(const std::string &item)
}
}
void ItemSelection::ShowRenameContextMenu(bool value)
{
_showRenameContextMenu = value;
}
void ItemSelection::ChangeSelection(const QString &sel)
{
if (sel == obs_module_text(_addStr.data())) {
@@ -139,12 +146,14 @@ void ItemSelection::ModifyButtonClicked()
};
QMenu menu(this);
QAction *action = new QAction(
obs_module_text("AdvSceneSwitcher.item.rename"), &menu);
connect(action, SIGNAL(triggered()), this, SLOT(RenameItem()));
action->setProperty("connetion", QVariant::fromValue(item));
menu.addAction(action);
QAction *action;
if (_showRenameContextMenu) {
action = new QAction(
obs_module_text("AdvSceneSwitcher.item.rename"), &menu);
connect(action, SIGNAL(triggered()), this, SLOT(RenameItem()));
action->setProperty("item", QVariant::fromValue(item));
menu.addAction(action);
}
action = new QAction(obs_module_text("AdvSceneSwitcher.item.remove"),
&menu);
@@ -162,7 +171,7 @@ void ItemSelection::ModifyButtonClicked()
void ItemSelection::RenameItem()
{
QAction *action = reinterpret_cast<QAction *>(sender());
QVariant variant = action->property("connetion");
QVariant variant = action->property("item");
Item *item = variant.value<Item *>();
std::string name;
@@ -174,12 +183,13 @@ void ItemSelection::RenameItem()
return;
}
if (name.empty()) {
DisplayMessage("AdvSceneSwitcher.item.emptyName");
DisplayMessage(
obs_module_text("AdvSceneSwitcher.item.emptyName"));
return;
}
if (_selection->currentText().toStdString() != name &&
!ItemNameAvailable(name, _items)) {
DisplayMessage("AdvSceneSwitcher.item.nameNotAvailable");
DisplayMessage(obs_module_text(_conflictStr.data()));
return;
}
@@ -245,7 +255,9 @@ Item *ItemSelection::GetCurrentItem()
ItemSettingsDialog::ItemSettingsDialog(const Item &settings,
std::deque<std::shared_ptr<Item>> &items,
std::string_view select,
std::string_view add, QWidget *parent)
std::string_view add,
std::string_view nameConflict,
QWidget *parent)
: QDialog(parent),
_name(new QLineEdit()),
_nameHint(new QLabel),
@@ -253,18 +265,20 @@ ItemSettingsDialog::ItemSettingsDialog(const Item &settings,
QDialogButtonBox::Cancel)),
_items(items),
_selectStr(select),
_addStr(add)
_addStr(add),
_conflictStr(nameConflict)
{
setModal(true);
setWindowModality(Qt::WindowModality::WindowModal);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setFixedWidth(555);
setMinimumWidth(555);
setMinimumHeight(100);
_buttonbox->setCenterButtons(true);
_buttonbox->button(QDialogButtonBox::Ok)->setDisabled(true);
_name->setText(QString::fromStdString(settings._name));
_originalName = QString::fromStdString(settings._name);
_name->setText(_originalName);
QWidget::connect(_name, SIGNAL(textEdited(const QString &)), this,
SLOT(NameChanged(const QString &)));
@@ -279,9 +293,8 @@ ItemSettingsDialog::ItemSettingsDialog(const Item &settings,
void ItemSettingsDialog::NameChanged(const QString &text)
{
if (text != _name->text() && !ItemNameAvailable(text, _items)) {
SetNameWarning(obs_module_text(
"AdvSceneSwitcher.item.nameNotAvailable"));
if (text != _originalName && !ItemNameAvailable(text, _items)) {
SetNameWarning(obs_module_text(_conflictStr.data()));
return;
}
if (text.isEmpty()) {

View File

@@ -1,24 +1,19 @@
#pragma once
#include "filter-combo-box.hpp"
#include "export-symbol-helper.hpp"
#include <QPushButton>
#include <QDialog>
#include <QLineEdit>
#include <QCheckBox>
#include <QSpinBox>
#include <QLabel>
#include <QDialogButtonBox>
#include <QSpinBox>
#include <QTimer>
#include <QWidget>
#include <deque>
#include <obs.hpp>
#include <websocket-helpers.hpp>
#include <obs-data.h>
namespace advss {
class ItemSelection;
class ItemSettingsDialog;
class ADVSS_EXPORT ItemSelection;
class ADVSS_EXPORT ItemSettingsDialog;
class Item {
public:
@@ -41,10 +36,13 @@ class ItemSettingsDialog : public QDialog {
Q_OBJECT
public:
ItemSettingsDialog(const Item &, std::deque<std::shared_ptr<Item>> &,
std::string_view = "AdvSceneSwitcher.item.select",
std::string_view = "AdvSceneSwitcher.item.select",
QWidget *parent = 0);
ItemSettingsDialog(
const Item &, std::deque<std::shared_ptr<Item>> &,
std::string_view selectString = "AdvSceneSwitcher.item.select",
std::string_view addString = "AdvSceneSwitcher.item.add",
std::string_view conflictString =
"AdvSceneSwitcher.item.nameNotAvailable",
QWidget *parent = 0);
virtual ~ItemSettingsDialog() = default;
private slots:
@@ -59,6 +57,8 @@ protected:
std::deque<std::shared_ptr<Item>> &_items;
std::string_view _selectStr;
std::string_view _addStr;
std::string_view _conflictStr;
QString _originalName;
};
typedef bool (*SettingsCallback)(QWidget *, Item &);
@@ -73,9 +73,12 @@ public:
SettingsCallback,
std::string_view selectString = "AdvSceneSwitcher.item.select",
std::string_view addString = "AdvSceneSwitcher.item.add",
std::string_view conflictString =
"AdvSceneSwitcher.item.nameNotAvailable",
std::string_view configureTooltip = "", QWidget *parent = 0);
virtual ~ItemSelection() = default;
void SetItem(const std::string &);
void ShowRenameContextMenu(bool value);
private slots:
void ModifyButtonClicked();
@@ -101,6 +104,8 @@ protected:
std::deque<std::shared_ptr<Item>> &_items;
std::string_view _selectStr;
std::string_view _addStr;
std::string_view _conflictStr;
bool _showRenameContextMenu = true;
};
} // namespace advss

View File

@@ -0,0 +1,82 @@
#include "macro-run-button.hpp"
#include "macro-tree.hpp"
#include "macro.hpp"
#include "obs-module-helper.hpp"
#include "utility.hpp"
#include <QKeyEvent>
namespace advss {
MacroRunButton::MacroRunButton(QWidget *parent) : QPushButton(parent)
{
if (window()) {
window()->installEventFilter(this);
}
QWidget::connect(this, SIGNAL(pressed()), this, SLOT(Pressed()));
}
void MacroRunButton::SetMacroTree(MacroTree *macros)
{
_macros = macros;
QWidget::connect(macros, SIGNAL(MacroSelectionChanged()), this,
SLOT(MacroSelectionChanged()));
QWidget::connect(&_timer, &QTimer::timeout, this,
[this]() { MacroSelectionChanged(); });
_timer.start(1000);
}
void MacroRunButton::MacroSelectionChanged()
{
auto macro = _macros->GetCurrentMacro();
if (!macro) {
_macroHasElseActions = false;
return;
}
_macroHasElseActions = macro->ElseActions().size() > 0;
}
bool MacroRunButton::eventFilter(QObject *obj, QEvent *event)
{
if (!_macroHasElseActions) {
setText(obs_module_text("AdvSceneSwitcher.macroTab.run"));
_runElseActionsKeyHeld = false;
return QPushButton::eventFilter(obj, event);
}
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Control) {
setText(obs_module_text(
"AdvSceneSwitcher.macroTab.runElse"));
_runElseActionsKeyHeld = true;
}
} else if (event->type() == QEvent::KeyRelease) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Control) {
setText(obs_module_text(
"AdvSceneSwitcher.macroTab.run"));
_runElseActionsKeyHeld = false;
}
}
return QPushButton::eventFilter(obj, event);
}
void MacroRunButton::Pressed()
{
auto macro = _macros->GetCurrentMacro();
if (!macro) {
return;
}
bool ret = _runElseActionsKeyHeld
? macro->PerformActions(false, true, true)
: macro->PerformActions(true, true, true);
if (!ret) {
QString err =
obs_module_text("AdvSceneSwitcher.macroTab.runFail");
DisplayMessage(err.arg(QString::fromStdString(macro->Name())));
}
}
} // namespace advss

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QPushButton>
#include <QTimer>
namespace advss {
class MacroTree;
class MacroRunButton : public QPushButton {
Q_OBJECT
public:
MacroRunButton(QWidget *parent = nullptr);
void SetMacroTree(MacroTree *);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private slots:
void MacroSelectionChanged();
void Pressed();
private:
MacroTree *_macros = nullptr;
bool _macroHasElseActions = false;
bool _runElseActionsKeyHeld = false;
QTimer _timer;
};
} // namespace advss

View File

@@ -75,7 +75,7 @@ std::optional<std::string> NonModalMessageDialog::GetInput()
show();
// Trigger resize
_inputEdit->setPlainText("");
_inputEdit->setPlainText(_inputEdit->toPlainText());
exec();
this->deleteLater();
@@ -85,6 +85,12 @@ std::optional<std::string> NonModalMessageDialog::GetInput()
return {};
}
void NonModalMessageDialog::SetInput(const QString &input)
{
assert(_type == Type::INPUT);
_inputEdit->setPlainText(input);
}
void NonModalMessageDialog::YesClicked()
{
_answer = QMessageBox::Yes;

View File

@@ -19,7 +19,8 @@ public:
NonModalMessageDialog(const QString &message, bool question);
QMessageBox::StandardButton ShowMessage();
std::optional<std::string> GetInput();
Type GetType() { return _type; }
Type GetType() const { return _type; }
void SetInput(const QString &);
private slots:
void YesClicked();

View File

@@ -5,6 +5,12 @@
#include <string.h>
#include <QGroupBox>
#ifdef WIN32
#include <winsock.h>
#else
#include <arpa/inet.h>
#endif
namespace advss {
std::unordered_map<size_t, OSCMessageElement::TypeInfo>
@@ -360,7 +366,7 @@ OSCMessageElementEdit::OSCMessageElementEdit(QWidget *parent)
_text->hide();
_binaryText->hide();
for (int i = 0; i < OSCMessageElement::_typeNames.size() - 1; i++) {
for (size_t i = 0; i < OSCMessageElement::_typeNames.size() - 1; i++) {
_type->addItem(obs_module_text(
OSCMessageElement::_typeNames.at(i).localizedName));
}

View File

@@ -162,29 +162,6 @@ static bool getSceneItemAtIdx(obs_scene_t *, obs_sceneitem_t *item, void *ptr)
return true;
}
static bool getTotalSceneItemCountHelper(obs_scene_t *, obs_sceneitem_t *item,
void *ptr)
{
auto count = reinterpret_cast<int *>(ptr);
if (obs_sceneitem_is_group(item)) {
obs_scene_t *scene = obs_sceneitem_group_get_scene(item);
obs_scene_enum_items(scene, getTotalSceneItemCountHelper, ptr);
}
*count = *count + 1;
return true;
}
static int getTotalSceneItemCountOnScene(const OBSWeakSource &sceneWeakSource)
{
auto s = obs_weak_source_get_source(sceneWeakSource);
auto scene = obs_scene_from_source(s);
int count = 0;
obs_scene_enum_items(scene, getTotalSceneItemCountHelper, &count);
obs_source_release(s);
return count;
}
struct GroupData {
std::string type;
std::vector<OBSSceneItem> items = {};
@@ -367,7 +344,6 @@ std::vector<OBSSceneItem> SceneItemSelection::GetSceneItemsByName(
} else {
name = GetWeakSourceName(_source);
}
int count = getCountOfSceneItemOccurance(sceneSelection, name, false);
auto items = getSceneItemsWithName(scene, name);
ReduceBadedOnIndexSelection(items);
return items;
@@ -409,7 +385,7 @@ std::vector<OBSSceneItem> SceneItemSelection::GetSceneItemsByIdx(
}
auto sceneWeakSource = sceneSelection.GetScene(false);
int count = getTotalSceneItemCountOnScene(sceneWeakSource);
int count = GetSceneItemCount(sceneWeakSource);
if (count == 0) {
return {};
}
@@ -751,15 +727,17 @@ void SceneItemSelectionWidget::SetNameConflictVisibility()
case SceneItemSelection::Type::SOURCE_NAME_PATTERN:
case SceneItemSelection::Type::SOURCE_GROUP:
sceneItemCount =
getTotalSceneItemCountOnScene(_scene.GetScene(false));
sceneItemCount = GetSceneItemCount(_scene.GetScene(false));
break;
case SceneItemSelection::Type::INDEX:
case SceneItemSelection::Type::INDEX_RANGE:
case SceneItemSelection::Type::ALL:
break;
}
if (_currentSelection._type ==
SceneItemSelection::Type::SOURCE_NAME_PATTERN) {
int sceneItemCount =
getTotalSceneItemCountOnScene(_scene.GetScene(false));
int sceneItemCount = GetSceneItemCount(_scene.GetScene(false));
if (sceneItemCount == 0) {
_nameConflictIndex->hide();
return;

View File

@@ -3,6 +3,7 @@
#include "utility.hpp"
#include <QLayout>
#include <QTimer>
namespace advss {

View File

@@ -502,6 +502,29 @@ bool SaveTransformState(obs_data_t *obj, const struct obs_transform_info &info,
return true;
}
static bool getTotalSceneItemCountHelper(obs_scene_t *, obs_sceneitem_t *item,
void *ptr)
{
auto count = reinterpret_cast<int *>(ptr);
if (obs_sceneitem_is_group(item)) {
obs_scene_t *scene = obs_sceneitem_group_get_scene(item);
obs_scene_enum_items(scene, getTotalSceneItemCountHelper, ptr);
}
*count = *count + 1;
return true;
}
int GetSceneItemCount(const OBSWeakSource &sceneWeakSource)
{
auto s = obs_weak_source_get_source(sceneWeakSource);
auto scene = obs_scene_from_source(s);
int count = 0;
obs_scene_enum_items(scene, getTotalSceneItemCountHelper, &count);
obs_source_release(s);
return count;
}
bool DisplayMessage(const QString &msg, bool question, bool modal)
{
if (!modal) {
@@ -605,11 +628,11 @@ bool IsValidMacroSegmentIndex(Macro *m, const int idx, bool isCondition)
return false;
}
if (isCondition) {
if (idx >= m->Conditions().size()) {
if (idx >= (int)m->Conditions().size()) {
return false;
}
} else {
if (idx >= m->Actions().size()) {
if (idx >= (int)m->Actions().size()) {
return false;
}
}
@@ -651,6 +674,33 @@ QString GetMacroSegmentDescription(Macro *macro, int idx, bool isCondition)
return result;
}
void SaveSplitterPos(const QList<int> &sizes, obs_data_t *obj,
const std::string name)
{
auto array = obs_data_array_create();
for (int i = 0; i < sizes.count(); ++i) {
obs_data_t *array_obj = obs_data_create();
obs_data_set_int(array_obj, "pos", sizes[i]);
obs_data_array_push_back(array, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, name.c_str(), array);
obs_data_array_release(array);
}
void LoadSplitterPos(QList<int> &sizes, obs_data_t *obj, const std::string name)
{
sizes.clear();
obs_data_array_t *array = obs_data_get_array(obj, name.c_str());
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
obs_data_t *item = obs_data_array_item(array, i);
sizes << obs_data_get_int(item, "pos");
obs_data_release(item);
}
obs_data_array_release(array);
}
QStringList GetSourceNames()
{
auto sourceEnum = [](void *param, obs_source_t *source) -> bool /* -- */

View File

@@ -38,6 +38,7 @@ void LoadTransformState(obs_data_t *obj, struct obs_transform_info &info,
struct obs_sceneitem_crop &crop);
bool SaveTransformState(obs_data_t *obj, const struct obs_transform_info &info,
const struct obs_sceneitem_crop &crop);
int GetSceneItemCount(const OBSWeakSource &);
/* Scene item helpers */
@@ -129,6 +130,10 @@ std::string GetPathInProfileDir(const char *filePath);
QStringList GetMonitorNames();
bool IsValidMacroSegmentIndex(Macro *m, const int idx, bool isCondition);
QString GetMacroSegmentDescription(Macro *, int idx, bool isCondition);
void SaveSplitterPos(const QList<int> &sizes, obs_data_t *obj,
const std::string name);
void LoadSplitterPos(QList<int> &sizes, obs_data_t *obj,
const std::string name);
/* Legacy helpers */

View File

@@ -116,7 +116,7 @@ std::string GetWeakVariableName(std::weak_ptr<Variable> var_)
{
auto var = var_.lock();
if (!var) {
return "invalid variable selection";
return obs_module_text("AdvSceneSwitcher.variable.invalid");
}
return var->Name();
}
@@ -164,7 +164,8 @@ VariableSettingsDialog::VariableSettingsDialog(QWidget *parent,
const Variable &settings)
: ItemSettingsDialog(settings, switcher->variables,
"AdvSceneSwitcher.variable.select",
"AdvSceneSwitcher.variable.add", parent),
"AdvSceneSwitcher.variable.add",
"AdvSceneSwitcher.item.nameNotAvailable", parent),
_value(new ResizingPlainTextEdit(this)),
_defaultValue(new ResizingPlainTextEdit(this)),
_save(new QComboBox())
@@ -248,6 +249,7 @@ VariableSelection::VariableSelection(QWidget *parent)
AskForSettingsWrapper,
"AdvSceneSwitcher.variable.select",
"AdvSceneSwitcher.variable.add",
"AdvSceneSwitcher.item.nameNotAvailable",
"AdvSceneSwitcher.variable.configure", parent)
{
// Connect to slots

View File

@@ -15,6 +15,9 @@
#include <obs-frontend-api.h>
#include <QAbstractEventDispatcher>
#include <QAbstractNativeEventFilter>
#include <QApplication>
#include <QWidget>
#include <mutex>
namespace advss {
@@ -102,10 +105,61 @@ static VOID EnumWindowsWithMetro(__in WNDENUMPROC lpEnumFunc,
}
}
// Asynchronously updates the OBS window list and returns the state of the last
// successful update
const std::vector<std::string> getOBSWindows()
{
struct OBSWindowListHelper {
std::vector<std::string> windows;
std::atomic_bool done;
};
static OBSWindowListHelper obsWindowListHelper1 = {{}, {true}};
static OBSWindowListHelper obsWindowListHelper2 = {{}, {false}};
auto getQtWindowList = [](void *param) {
auto list = reinterpret_cast<OBSWindowListHelper *>(param);
for (auto w : QApplication::topLevelWidgets()) {
auto title = w->windowTitle();
if (!title.isEmpty()) {
list->windows.emplace_back(title.toStdString());
}
}
list->done = true;
};
static OBSWindowListHelper *lastDoneHelper = &obsWindowListHelper2;
static OBSWindowListHelper *pendingHelper = &obsWindowListHelper1;
static std::mutex mutex;
/* ------------------------------------------------------------------ */
std::lock_guard<std::mutex> lock(mutex);
if (pendingHelper->done) { // Check if swap is needed
auto temp = lastDoneHelper;
lastDoneHelper = pendingHelper;
pendingHelper = temp;
pendingHelper->done = false;
pendingHelper->windows.clear();
obs_queue_task(OBS_TASK_UI, getQtWindowList, pendingHelper,
false);
}
return lastDoneHelper->windows;
}
void GetWindowList(std::vector<std::string> &windows)
{
windows.resize(0);
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
// Also add OBS windows
for (const auto &window : getOBSWindows()) {
if (!window.empty()) {
windows.emplace_back(window);
}
}
// Add entry for OBS Studio itself - see GetCurrentWindowTitle()
windows.emplace_back("OBS");
}
void GetWindowList(QStringList &windows)
@@ -117,9 +171,6 @@ void GetWindowList(QStringList &windows)
for (auto window : w) {
windows << QString::fromStdString(window);
}
// Add entry for OBS Studio itself, see GetCurrentWindowTitle
windows << QString("OBS");
}
void GetCurrentWindowTitle(std::string &title)
@@ -128,12 +179,24 @@ void GetCurrentWindowTitle(std::string &title)
DWORD pid;
DWORD thid;
thid = GetWindowThreadProcessId(window, &pid);
// GetWindowText will freeze if the control it is reading was created in another thread.
// It does not directly read the control.
// Instead it waits for the thread that created the control to process a WM_GETTEXT message.
// So if that thread is frozen in a WaitFor... call you have a deadlock.
// Calling GetWindowTitle() on the OBS windows might cause a deadlock in
// the following scenario:
//
// The thread using GetWindowTitle() will send a WM_GETTEXT message to
// the main thread that created the window and wait for it to be
// processed.
// The main thread itself might be blocked from processing the message,
// however, when it itself is waiting for the thread using
// GetWindowTitle() to return.
//
// So instead rely on Qt to get the title of the active window.
if (GetCurrentProcessId() == pid) {
title = "OBS";
auto window = QApplication::activeWindow();
if (window) {
title = window->windowTitle().toStdString();
} else {
title = "OBS";
}
return;
}
GetWindowTitle(window, title);