mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 04:36:21 -05:00
Compare commits
82 Commits
test
...
nested-que
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba7886c8c1 | ||
|
|
801268d08b | ||
|
|
7dfd9ddcb5 | ||
|
|
6215c39024 | ||
|
|
1c856911e8 | ||
|
|
ed17c2ac6d | ||
|
|
1ce332a91d | ||
|
|
2aa27fc051 | ||
|
|
31b239d5dc | ||
|
|
7c3779e287 | ||
|
|
3f13c33b10 | ||
|
|
8727a65513 | ||
|
|
baf3d0e63c | ||
|
|
ea2d1e7e9d | ||
|
|
f9f26ad50b | ||
|
|
9911c41a35 | ||
|
|
d590bc11f8 | ||
|
|
31c4c1f5d7 | ||
|
|
42ff48d637 | ||
|
|
d44376e869 | ||
|
|
4b1fd9e700 | ||
|
|
d61099f9c0 | ||
|
|
c2b73a02ca | ||
|
|
49a681bcfe | ||
|
|
4cedb29123 | ||
|
|
d80b3aaf69 | ||
|
|
b1cc15386f | ||
|
|
748744d1b7 | ||
|
|
912ff98e27 | ||
|
|
a9614c5306 | ||
|
|
cb2f97f11d | ||
|
|
017ea233ec | ||
|
|
e8206173ff | ||
|
|
680f72e4f9 | ||
|
|
930e8b38d4 | ||
|
|
ee1d0136d9 | ||
|
|
20c276f4ea | ||
|
|
bea4f66eb1 | ||
|
|
c5185765ee | ||
|
|
90130c119e | ||
|
|
a7ca06d25c | ||
|
|
5a5d590c51 | ||
|
|
336b398bc3 | ||
|
|
56c8f335db | ||
|
|
b6a4d48948 | ||
|
|
774ccb7fbd | ||
|
|
eda9dde23d | ||
|
|
bbf91efd0d | ||
|
|
b022d59391 | ||
|
|
cf46a010a7 | ||
|
|
595bab0d83 | ||
|
|
6fc819d72b | ||
|
|
0553a9b2ee | ||
|
|
3caa3a4735 | ||
|
|
2469057ad6 | ||
|
|
b230ec5bc5 | ||
|
|
1669b4cb72 | ||
|
|
2841fcaadb | ||
|
|
91810d9c40 | ||
|
|
ccb5c8ba2c | ||
|
|
265daebdf3 | ||
|
|
a149a11c57 | ||
|
|
d7cc7a4c7b | ||
|
|
41866bcb24 | ||
|
|
c73a5866d7 | ||
|
|
18aaf1dd31 | ||
|
|
d523a2e674 | ||
|
|
0f7eed6257 | ||
|
|
1f5a346190 | ||
|
|
7fd2979e86 | ||
|
|
2077e1971d | ||
|
|
873e073eb6 | ||
|
|
4f3d9e3a00 | ||
|
|
c02896ea17 | ||
|
|
4bfd40219b | ||
|
|
6a3de18069 | ||
|
|
8c51b44fbb | ||
|
|
10daa38921 | ||
|
|
6e0f7ab38f | ||
|
|
86061b3cf1 | ||
|
|
c3669cae3e | ||
|
|
a116360f87 |
@@ -19,14 +19,9 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup cmake
|
||||
uses: jwlawson/actions-setup-cmake@v1.13
|
||||
with:
|
||||
cmake-version: '3.x.x'
|
||||
|
||||
- name: Restore cached dependencies
|
||||
id: restore-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.DEP_DIR }}
|
||||
key: ${{ env.DEP_DIR }}-${{ runner.os }}-${{ inputs.target }}
|
||||
|
||||
2
.github/actions/run-clang-format/action.yaml
vendored
2
.github/actions/run-clang-format/action.yaml
vendored
@@ -29,6 +29,7 @@ runs:
|
||||
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
|
||||
echo "/home/linuxbrew/.linuxbrew/opt/clang-format@16/bin" >> $GITHUB_PATH
|
||||
brew update
|
||||
brew install --quiet zsh
|
||||
echo ::endgroup::
|
||||
|
||||
@@ -52,6 +53,7 @@ runs:
|
||||
|
||||
if (( ${changes[(I)(*.c|*.h|*.cpp|*.hpp|*.m|*.mm)]} )) {
|
||||
echo ::group::Install clang-format-16
|
||||
brew update
|
||||
brew install --quiet obsproject/tools/clang-format@16
|
||||
echo ::endgroup::
|
||||
|
||||
|
||||
2
.github/scripts/.Wingetfile
vendored
2
.github/scripts/.Wingetfile
vendored
@@ -1,4 +1,4 @@
|
||||
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
|
||||
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'
|
||||
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl'
|
||||
package 'OpenSSL', path: 'OpenSSL', bin: 'openssl', version: '3.6.2'
|
||||
package 'Microsoft.VisualStudio.Locator', path: 'vswhere', bin: 'vswhere'
|
||||
73
.github/scripts/Package-Windows.ps1
vendored
73
.github/scripts/Package-Windows.ps1
vendored
@@ -68,14 +68,53 @@ function Package {
|
||||
|
||||
Remove-Item @RemoveArgs
|
||||
|
||||
Log-Group "Archiving ${ProductName}..."
|
||||
$CompressArgs = @{
|
||||
Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*")
|
||||
CompressionLevel = 'Optimal'
|
||||
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
|
||||
Verbose = ($Env:CI -ne $null)
|
||||
$ReleasePath = "${ProjectRoot}/release/${Configuration}"
|
||||
$NewBinPath = "${ReleasePath}/${ProductName}/bin/64bit"
|
||||
$NewDataPath = "${ReleasePath}/${ProductName}/data"
|
||||
$CIWindowsDir = "${ProjectRoot}/build-aux/CI/windows"
|
||||
|
||||
# --- Recommended zip (new layout, extract to %ProgramData%\obs-studio\) ---
|
||||
Log-Group "Archiving ${ProductName} (recommended)..."
|
||||
$RecStaging = "${ProjectRoot}/release/zip-staging-rec"
|
||||
Remove-Item -Path $RecStaging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $RecStaging | Out-Null
|
||||
Copy-Item -Path "${CIWindowsDir}/README.txt" -Destination "${RecStaging}/README.txt"
|
||||
if ( Test-Path -Path $NewBinPath ) {
|
||||
$RecBinPath = "${RecStaging}/${ProductName}/bin/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $RecBinPath | Out-Null
|
||||
Copy-Item -Path "${NewBinPath}/*" -Destination $RecBinPath -Recurse -Force
|
||||
}
|
||||
Compress-Archive -Force @CompressArgs
|
||||
if ( Test-Path -Path $NewDataPath ) {
|
||||
$RecDataPath = "${RecStaging}/${ProductName}/data"
|
||||
New-Item -ItemType Directory -Force -Path $RecDataPath | Out-Null
|
||||
Copy-Item -Path "${NewDataPath}/*" -Destination $RecDataPath -Recurse -Force
|
||||
}
|
||||
Compress-Archive -Force -Path (Get-ChildItem -Path $RecStaging) `
|
||||
-CompressionLevel Optimal `
|
||||
-DestinationPath "${ProjectRoot}/release/${OutputName}.zip"
|
||||
Remove-Item -Path $RecStaging -Recurse -Force
|
||||
Log-Group
|
||||
|
||||
# --- Legacy zip (old layout, extract to OBS install directory) ---
|
||||
Log-Group "Archiving ${ProductName} (legacy)..."
|
||||
$LegStaging = "${ProjectRoot}/release/zip-staging-leg"
|
||||
Remove-Item -Path $LegStaging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $LegStaging | Out-Null
|
||||
Copy-Item -Path "${CIWindowsDir}/README-legacy.txt" -Destination "${LegStaging}/README.txt"
|
||||
if ( Test-Path -Path $NewBinPath ) {
|
||||
$LegBinPath = "${LegStaging}/obs-plugins/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $LegBinPath | Out-Null
|
||||
Copy-Item -Path "${NewBinPath}/*" -Destination $LegBinPath -Recurse -Force
|
||||
}
|
||||
if ( Test-Path -Path $NewDataPath ) {
|
||||
$LegDataPath = "${LegStaging}/data/obs-plugins/${ProductName}"
|
||||
New-Item -ItemType Directory -Force -Path $LegDataPath | Out-Null
|
||||
Copy-Item -Path "${NewDataPath}/*" -Destination $LegDataPath -Recurse -Force
|
||||
}
|
||||
Compress-Archive -Force -Path (Get-ChildItem -Path $LegStaging) `
|
||||
-CompressionLevel Optimal `
|
||||
-DestinationPath "${ProjectRoot}/release/${OutputName}-legacy.zip"
|
||||
Remove-Item -Path $LegStaging -Recurse -Force
|
||||
Log-Group
|
||||
|
||||
if ( ( $BuildInstaller ) ) {
|
||||
@@ -89,7 +128,25 @@ function Package {
|
||||
Log-Information 'Creating InnoSetup installer...'
|
||||
Push-Location -Stack BuildTemp
|
||||
Ensure-Location -Path "${ProjectRoot}/release"
|
||||
Copy-Item -Path ${Configuration} -Destination Package -Recurse
|
||||
Remove-Item -Path Package -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Recommended layout (for %ProgramData%\obs-studio\plugins\)
|
||||
$PkgRec = "Package/recommended"
|
||||
New-Item -ItemType Directory -Force -Path $PkgRec | Out-Null
|
||||
Copy-Item -Path "${Configuration}/*" -Destination $PkgRec -Recurse -Force
|
||||
|
||||
# Legacy layout (for OBS installation directory)
|
||||
if ( Test-Path "${Configuration}/${ProductName}/bin/64bit" ) {
|
||||
$PkgLegBin = "Package/legacy/obs-plugins/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $PkgLegBin | Out-Null
|
||||
Copy-Item -Path "${Configuration}/${ProductName}/bin/64bit/*" -Destination $PkgLegBin -Recurse -Force
|
||||
}
|
||||
if ( Test-Path "${Configuration}/${ProductName}/data" ) {
|
||||
$PkgLegData = "Package/legacy/data/obs-plugins/${ProductName}"
|
||||
New-Item -ItemType Directory -Force -Path $PkgLegData | Out-Null
|
||||
Copy-Item -Path "${Configuration}/${ProductName}/data/*" -Destination $PkgLegData -Recurse -Force
|
||||
}
|
||||
|
||||
Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer"
|
||||
Remove-Item -Path Package -Recurse
|
||||
Pop-Location -Stack BuildTemp
|
||||
|
||||
1
.github/scripts/utils.zsh/check_macos
vendored
1
.github/scripts/utils.zsh/check_macos
vendored
@@ -18,4 +18,5 @@ if (( ! ${+commands[brew]} )) {
|
||||
}
|
||||
|
||||
brew bundle --file ${SCRIPT_HOME}/.Brewfile
|
||||
rehash
|
||||
log_group
|
||||
|
||||
4
.github/workflows/build-debian.yml
vendored
4
.github/workflows/build-debian.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: check_libobs_revision
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
mv ../*.deb .
|
||||
- name: Publish
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "obs-scene-switcher.deb"
|
||||
path: ${{ github.workspace }}/*.deb
|
||||
|
||||
34
.github/workflows/build-project.yaml
vendored
34
.github/workflows/build-project.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
description: "Project name detected by parsing build spec file"
|
||||
value: ${{ jobs.check-event.outputs.pluginName }}
|
||||
env:
|
||||
DEP_DIR: .deps/advss-build-dependencies-4
|
||||
DEP_DIR: .deps/advss-build-dependencies-5
|
||||
jobs:
|
||||
check-event:
|
||||
name: Check GitHub Event Data 🔎
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
commitHash: ${{ steps.setup.outputs.commitHash }}
|
||||
pluginName: ${{ steps.setup.outputs.pluginName }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check Event Data ☑️
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
run:
|
||||
shell: zsh --no-rcs --errexit --pipefail {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer
|
||||
print '::endgroup::'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: ccache-cache
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
@@ -163,13 +163,13 @@ jobs:
|
||||
codesignPass: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-macos-universal.*
|
||||
|
||||
- name: Upload Debug Symbol Artifacts 🪲
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ needs.check-event.outputs.config == 'Release' }}
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}-dSYMs
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
|
||||
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: ccache-cache
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
@@ -210,13 +210,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-x86_64-
|
||||
|
||||
- name: Set up CMake 🏗️
|
||||
uses: jwlawson/actions-setup-cmake@v1.13
|
||||
with:
|
||||
cmake-version: '3.x.x'
|
||||
|
||||
- name: Set up Homebrew 🍺
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
@@ -238,23 +233,24 @@ jobs:
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Upload Source Tarball 🗜️
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*.*
|
||||
|
||||
- name: Upload debug symbol artifacts 🪲
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||
if-no-files-found: ignore
|
||||
|
||||
windows-build:
|
||||
name: Build for Windows 🪟
|
||||
@@ -264,7 +260,7 @@ jobs:
|
||||
run:
|
||||
shell: pwsh
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -311,7 +307,7 @@ jobs:
|
||||
package: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-windows-x64-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-windows-x64*.*
|
||||
|
||||
4
.github/workflows/check-format.yaml
vendored
4
.github/workflows/check-format.yaml
vendored
@@ -5,7 +5,7 @@ jobs:
|
||||
clang-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: clang-format check 🐉
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
cmake-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: cmake-format check 🎛️
|
||||
|
||||
2
.github/workflows/locale-check.yml
vendored
2
.github/workflows/locale-check.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check locale files
|
||||
run: |
|
||||
|
||||
4
.github/workflows/push.yaml
vendored
4
.github/workflows/push.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
esac
|
||||
|
||||
- name: Download Build Artifacts 📥
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
if: fromJSON(steps.check.outputs.validTag)
|
||||
id: download
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
- name: Create Release 🛫
|
||||
if: fromJSON(steps.check.outputs.validTag)
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@d4e8205d7e959a9107da6396278b2f1f07af0f9b
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }}
|
||||
|
||||
@@ -134,6 +134,9 @@ target_sources(
|
||||
lib/macro/macro-dock-window.hpp
|
||||
lib/macro/macro-edit.cpp
|
||||
lib/macro/macro-edit.hpp
|
||||
lib/macro/macro-export-builtin-extensions.cpp
|
||||
lib/macro/macro-export-extensions.cpp
|
||||
lib/macro/macro-export-extensions.hpp
|
||||
lib/macro/macro-export-import-dialog.cpp
|
||||
lib/macro/macro-export-import-dialog.hpp
|
||||
lib/macro/macro-helpers.cpp
|
||||
|
||||
23
build-aux/CI/windows/README-legacy.txt
Normal file
23
build-aux/CI/windows/README-legacy.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
Advanced Scene Switcher - Windows Installation (Legacy)
|
||||
========================================================
|
||||
|
||||
For full installation instructions visit:
|
||||
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
|
||||
|
||||
|
||||
This archive uses the legacy plugin layout for older OBS versions,
|
||||
portable OBS installs, and Steam.
|
||||
|
||||
Extract the CONTENTS of this archive into your OBS installation directory,
|
||||
typically:
|
||||
|
||||
C:\Program Files\obs-studio\
|
||||
|
||||
Merge the data\ and obs-plugins\ folders with the existing ones when prompted.
|
||||
|
||||
Steam users: right-click OBS in your library -> Manage -> Browse local files,
|
||||
then extract the contents of this archive into that directory.
|
||||
|
||||
|
||||
For OBS 28 and newer the recommended archive is preferred:
|
||||
advanced-scene-switcher-<version>-windows-x64.zip
|
||||
21
build-aux/CI/windows/README.txt
Normal file
21
build-aux/CI/windows/README.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
Advanced Scene Switcher - Windows Installation
|
||||
===============================================
|
||||
|
||||
For full installation instructions visit:
|
||||
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
|
||||
|
||||
|
||||
This archive uses the recommended plugin layout for OBS 28 and newer.
|
||||
|
||||
Extract the CONTENTS of this archive into:
|
||||
|
||||
C:\ProgramData\obs-studio\plugins\
|
||||
|
||||
Tip: press Win+R and type %ProgramData%\obs-studio\plugins to open that folder.
|
||||
|
||||
After extracting, the path should look like:
|
||||
C:\ProgramData\obs-studio\plugins\advanced-scene-switcher\bin\64bit\advanced-scene-switcher.dll
|
||||
|
||||
|
||||
If the plugin does not appear in OBS, try the legacy archive instead:
|
||||
advanced-scene-switcher-<version>-windows-x64-legacy.zip
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
if(BUILD_OUT_OF_TREE)
|
||||
if(OS_WINDOWS)
|
||||
set(OBS_PLUGIN_DESTINATION "obs-plugins/64bit")
|
||||
set(OBS_PLUGIN_DESTINATION "${CMAKE_PROJECT_NAME}/bin/64bit")
|
||||
else()
|
||||
set(OBS_PLUGIN_DESTINATION "${CMAKE_INSTALL_LIBDIR}/obs-plugins")
|
||||
endif()
|
||||
@@ -157,7 +157,9 @@ else()
|
||||
set_target_properties(${target} PROPERTIES INSTALL_RPATH
|
||||
"$ORIGIN:$ORIGIN/..")
|
||||
endif()
|
||||
message(STATUS "ADVSS: ENABLED PLUGIN ${target}")
|
||||
get_filename_component(_plugin_dir_name "${CMAKE_CURRENT_SOURCE_DIR}" NAME)
|
||||
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_ENABLED
|
||||
${_plugin_dir_name})
|
||||
endfunction()
|
||||
|
||||
function(install_advss_plugin_dependency_target target dep)
|
||||
|
||||
@@ -18,7 +18,9 @@ set(CPACK_PACKAGE_FILE_NAME
|
||||
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}"
|
||||
)
|
||||
|
||||
set(CPACK_GENERATOR "DEB")
|
||||
set(CPACK_GENERATOR
|
||||
"DEB"
|
||||
CACHE STRING "CPack generator to use")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
|
||||
set(CPACK_SET_DESTDIR ON)
|
||||
@@ -46,6 +48,19 @@ set(CPACK_SOURCE_PACKAGE_FILE_NAME
|
||||
set(CPACK_ARCHIVE_THREADS 0)
|
||||
|
||||
include(CPack)
|
||||
# CPack.cmake has a side effect that leaves binary packaging variables in the
|
||||
# state intended for source packaging when it finishes. Specifically it sets
|
||||
# CPACK_GENERATOR to CPACK_SOURCE_GENERATOR ("TXZ"), sets
|
||||
# CPACK_INSTALLED_DIRECTORIES to the source root (which would cause CPackDeb to
|
||||
# scan the entire source tree, including ccache object files that lack a Build
|
||||
# ID), and clears CPACK_INSTALL_CMAKE_PROJECTS (preventing the cmake install
|
||||
# mechanism from being used). In some cmake versions these overrides happen
|
||||
# before CPackConfig.cmake is written. Reset all three so that binary DEB
|
||||
# packaging works correctly regardless of cmake version.
|
||||
set(CPACK_GENERATOR "DEB")
|
||||
set(CPACK_INSTALLED_DIRECTORIES "")
|
||||
set(CPACK_INSTALL_CMAKE_PROJECTS
|
||||
"${CMAKE_BINARY_DIR};${CMAKE_PROJECT_NAME};ALL;/")
|
||||
|
||||
find_package(libobs QUIET)
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ include(buildspec_common)
|
||||
|
||||
# _check_dependencies_windows: Set up Windows slice for _check_dependencies
|
||||
function(_check_dependencies_windows)
|
||||
set(arch ${CMAKE_GENERATOR_PLATFORM})
|
||||
if(CMAKE_GENERATOR_PLATFORM)
|
||||
set(arch ${CMAKE_GENERATOR_PLATFORM})
|
||||
else()
|
||||
set(arch x64)
|
||||
endif()
|
||||
set(platform windows-${arch})
|
||||
|
||||
set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps")
|
||||
|
||||
@@ -6,3 +6,9 @@ include_guard(GLOBAL)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
|
||||
|
||||
include(buildspec)
|
||||
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX
|
||||
"$ENV{ALLUSERSPROFILE}/obs-studio/plugins"
|
||||
CACHE STRING "Default plugin installation directory" FORCE)
|
||||
endif()
|
||||
|
||||
@@ -30,13 +30,13 @@ function(set_target_properties_plugin target)
|
||||
|
||||
install(
|
||||
TARGETS ${target}
|
||||
RUNTIME DESTINATION bin/64bit
|
||||
LIBRARY DESTINATION obs-plugins/64bit)
|
||||
RUNTIME DESTINATION "${target}/bin/64bit"
|
||||
LIBRARY DESTINATION "${target}/bin/64bit")
|
||||
|
||||
install(
|
||||
FILES "$<TARGET_PDB_FILE:${target}>"
|
||||
CONFIGURATIONS RelWithDebInfo Debug Release
|
||||
DESTINATION obs-plugins/64bit
|
||||
DESTINATION "${target}/bin/64bit"
|
||||
OPTIONAL)
|
||||
|
||||
if(OBS_BUILD_DIR)
|
||||
@@ -44,11 +44,11 @@ function(set_target_properties_plugin target)
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
||||
"${OBS_BUILD_DIR}/obs-plugins/64bit"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/bin/64bit"
|
||||
COMMAND
|
||||
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>"
|
||||
"$<$<CONFIG:Debug,RelWithDebInfo,Release>:$<TARGET_PDB_FILE:${target}>>"
|
||||
"${OBS_BUILD_DIR}/obs-plugins/64bit"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/bin/64bit"
|
||||
COMMENT "Copy ${target} to obs-studio directory ${OBS_BUILD_DIR}"
|
||||
VERBATIM)
|
||||
endif()
|
||||
@@ -105,7 +105,7 @@ function(target_install_resources target)
|
||||
|
||||
install(
|
||||
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
|
||||
DESTINATION data/obs-plugins/${target}
|
||||
DESTINATION "${target}/data"
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
|
||||
if(OBS_BUILD_DIR)
|
||||
@@ -113,11 +113,11 @@ function(target_install_resources target)
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
||||
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/data"
|
||||
COMMAND
|
||||
"${CMAKE_COMMAND}" -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/data"
|
||||
COMMENT "Copy ${target} resources to data directory"
|
||||
VERBATIM)
|
||||
endif()
|
||||
@@ -133,7 +133,7 @@ function(target_add_resource target resource)
|
||||
|
||||
install(
|
||||
FILES "${resource}"
|
||||
DESTINATION data/obs-plugins/${target}
|
||||
DESTINATION "${target}/data"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(OBS_BUILD_DIR)
|
||||
@@ -141,9 +141,9 @@ function(target_add_resource target resource)
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
||||
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/data"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy "${resource}"
|
||||
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
|
||||
"${OBS_BUILD_DIR}/plugins/${target}/data"
|
||||
COMMENT "Copy ${target} resource ${resource} to library directory"
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
|
||||
#define MyAppPublisher "@PLUGIN_AUTHOR@"
|
||||
#define MyAppURL "@PLUGIN_WEBSITE@"
|
||||
#define MyAppId "@UUID_APP@"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{@UUID_APP@}
|
||||
AppId={{{#MyAppId}}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
@@ -20,15 +21,19 @@ OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
DirExistsWarning=no
|
||||
UsePreviousAppDir=no
|
||||
SetupIconFile=installer.ico
|
||||
UninstallDisplayIcon={app}\data\obs-plugins\advanced-scene-switcher\res\images\logo.ico
|
||||
UninstallDisplayIcon={code:GetUninstallIconPath}
|
||||
DisableDirPage=no
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; Recommended layout - used when installing to %ProgramData%\obs-studio\plugins\
|
||||
Source: "..\release\Package\recommended\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsRecommendedLayout
|
||||
; Legacy layout - used when installing to the OBS installation directory
|
||||
Source: "..\release\Package\legacy\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsLegacyLayout
|
||||
Source: "..\LICENSE"; Flags: dontcopy
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
@@ -51,17 +56,64 @@ begin
|
||||
);
|
||||
end;
|
||||
|
||||
// credit where it's due :
|
||||
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
|
||||
function GetDirName(Value: string): string;
|
||||
var
|
||||
InstallPath: string;
|
||||
// Returns true when the selected install directory is under %ProgramData%,
|
||||
// meaning the recommended plugin layout should be used.
|
||||
function IsRecommendedLayout(): Boolean;
|
||||
begin
|
||||
// initialize default path, which will be returned when the following registry
|
||||
// key queries fail due to missing keys or for some different reason
|
||||
Result := '{autopf}\obs-studio';
|
||||
// query the first registry value; if this succeeds, return the obtained value
|
||||
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
|
||||
Result := InstallPath
|
||||
Result := Pos(LowerCase(ExpandConstant('{commonappdata}')),
|
||||
LowerCase(WizardDirValue())) = 1;
|
||||
end;
|
||||
|
||||
function IsLegacyLayout(): Boolean;
|
||||
begin
|
||||
Result := not IsRecommendedLayout();
|
||||
end;
|
||||
|
||||
function GetUninstallIconPath(Param: string): string;
|
||||
begin
|
||||
if IsRecommendedLayout() then
|
||||
Result := ExpandConstant('{app}\advanced-scene-switcher\data\res\images\logo.ico')
|
||||
else
|
||||
Result := ExpandConstant('{app}\data\obs-plugins\advanced-scene-switcher\res\images\logo.ico');
|
||||
end;
|
||||
|
||||
function GetDirName(Value: string): string;
|
||||
var
|
||||
PrevPath: string;
|
||||
UninstallKey: string;
|
||||
begin
|
||||
Result := ExpandConstant('{commonappdata}\obs-studio\plugins');
|
||||
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
|
||||
if RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', PrevPath) and (PrevPath <> '') then
|
||||
Result := PrevPath;
|
||||
end;
|
||||
|
||||
// Remove files placed by the legacy installer (obs-plugins/64bit and
|
||||
// data/obs-plugins layout) so the plugin is not loaded twice by OBS.
|
||||
// Only runs when upgrading from a legacy location to the recommended one.
|
||||
procedure RemoveLegacyFiles();
|
||||
var
|
||||
OldInstallPath: string;
|
||||
UninstallKey: string;
|
||||
begin
|
||||
if not IsRecommendedLayout() then
|
||||
Exit;
|
||||
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
|
||||
if not RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', OldInstallPath) then
|
||||
Exit;
|
||||
// Skip if the previous install was already in the recommended location
|
||||
if Pos(LowerCase(ExpandConstant('{commonappdata}')), LowerCase(OldInstallPath)) = 1 then
|
||||
Exit;
|
||||
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.dll');
|
||||
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.pdb');
|
||||
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.dll');
|
||||
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.pdb');
|
||||
DelTree(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-plugins', True, True, True);
|
||||
DelTree(OldInstallPath + '\data\obs-plugins\advanced-scene-switcher', True, True, True);
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssInstall then
|
||||
RemoveLegacyFiles();
|
||||
end;
|
||||
|
||||
@@ -99,7 +99,6 @@ AdvSceneSwitcher.macroList.deleted="gelöscht"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Eintrag ignorieren"
|
||||
AdvSceneSwitcher.logic.and="Und"
|
||||
AdvSceneSwitcher.logic.or="Oder"
|
||||
AdvSceneSwitcher.logic.andNot="Und nicht"
|
||||
|
||||
@@ -47,6 +47,13 @@ AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowMacroSearch="Always show macro search"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs="Always show all feature tabs"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs.tooltip="Show tabs that are normally hidden until first used in a macro (e.g. Action Queues, Variables, Twitch, Websockets)."
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock="Lock settings dialog with password"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title="Settings Locked"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel="Enter password:"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword="Wrong password!"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.newPasswordLabel="New password:"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.confirmPasswordLabel="Confirm password:"
|
||||
AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordMismatch="Passwords do not match!"
|
||||
AdvSceneSwitcher.generalTab.matchBehavior="Match behavior"
|
||||
AdvSceneSwitcher.generalTab.priority="Priority"
|
||||
AdvSceneSwitcher.generalTab.priority.description="Switching methods priority (Highest priority is at the top)"
|
||||
@@ -202,6 +209,13 @@ AdvSceneSwitcher.macroTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroTab.export="Export"
|
||||
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
|
||||
AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text"
|
||||
AdvSceneSwitcher.macroTab.export.additionalContent="Export Additional Content"
|
||||
AdvSceneSwitcher.macroTab.export.variables="Variables"
|
||||
AdvSceneSwitcher.macroTab.export.actionQueues="Action Queues"
|
||||
AdvSceneSwitcher.macroTab.export.httpServers="HTTP Servers"
|
||||
AdvSceneSwitcher.macroTab.export.macroScheduleEntries="Macro Schedule"
|
||||
AdvSceneSwitcher.macroTab.export.twitchConnections="Twitch Connections"
|
||||
AdvSceneSwitcher.macroTab.export.websocketConnections="Websocket Connections"
|
||||
AdvSceneSwitcher.macroTab.import="Import"
|
||||
AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:"
|
||||
AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!"
|
||||
@@ -306,7 +320,6 @@ AdvSceneSwitcher.macroList.deleted="deleted"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" is alreay selected!"
|
||||
|
||||
# Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Ignore entry"
|
||||
AdvSceneSwitcher.logic.and="And"
|
||||
AdvSceneSwitcher.logic.or="Or"
|
||||
AdvSceneSwitcher.logic.andNot="And not"
|
||||
@@ -856,6 +869,7 @@ AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
|
||||
AdvSceneSwitcher.condition.streamDeck.pluginDownload="<html><head/><body><p>The Stream Deck plugin can be found <a href=\"https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin/releases\"><span style=\" text-decoration: underline; color:#268bd2;\">here on GitHub</span></a>.</p></body></html>"
|
||||
AdvSceneSwitcher.condition.gameCapture="Game capture"
|
||||
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
|
||||
|
||||
AdvSceneSwitcher.condition.screenshot="Screenshot"
|
||||
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
|
||||
AdvSceneSwitcher.condition.mqtt="MQTT"
|
||||
@@ -1076,8 +1090,8 @@ AdvSceneSwitcher.action.macro.type.runActions="Run macro actions"
|
||||
AdvSceneSwitcher.action.macro.type.run.conditions.ignore="Do not consider condition state"
|
||||
AdvSceneSwitcher.action.macro.type.run.conditions.true="Only if conditions evaluate to true"
|
||||
AdvSceneSwitcher.action.macro.type.run.conditions.false="Only if conditions evaluate to false"
|
||||
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Reevaluate the condition state before executing this action"
|
||||
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nCheck this option if you want those side effects to be taken into account when evaluating the condition state."
|
||||
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState="Check conditions now, even if macro is paused"
|
||||
AdvSceneSwitcher.action.macro.type.run.updateConditionMatchState.help="The plugin operates in phases:\n * The phase evaluating the macro conditions\n * The phase running macro actions\nMacros executed before this particular action might have side effects on the condition state of macros.\nAdditionally, paused macros are assumed to not match their conditions.\nCheck this option to force a fresh condition evaluation before running this action, regardless of whether the macro is paused."
|
||||
AdvSceneSwitcher.action.macro.type.run.actionType.regular="actions"
|
||||
AdvSceneSwitcher.action.macro.type.run.actionType.else="else-actions"
|
||||
AdvSceneSwitcher.action.macro.type.run.skipWhenPaused="Skip execution when macro is paused"
|
||||
@@ -2280,6 +2294,14 @@ AdvSceneSwitcher.tempVar.scene.position="Position in scenes list"
|
||||
|
||||
AdvSceneSwitcher.tempVar.window.window="Window title"
|
||||
AdvSceneSwitcher.tempVar.window.window.description="The window title of the matching window."
|
||||
AdvSceneSwitcher.tempVar.window.windowX="Window X position"
|
||||
AdvSceneSwitcher.tempVar.window.windowX.description="The X position (left edge) of the matching window in screen coordinates."
|
||||
AdvSceneSwitcher.tempVar.window.windowY="Window Y position"
|
||||
AdvSceneSwitcher.tempVar.window.windowY.description="The Y position (top edge) of the matching window in screen coordinates."
|
||||
AdvSceneSwitcher.tempVar.window.windowWidth="Window width"
|
||||
AdvSceneSwitcher.tempVar.window.windowWidth.description="The width of the matching window in pixels."
|
||||
AdvSceneSwitcher.tempVar.window.windowHeight="Window height"
|
||||
AdvSceneSwitcher.tempVar.window.windowHeight.description="The height of the matching window in pixels."
|
||||
AdvSceneSwitcher.tempVar.window.windowClass="Window class"
|
||||
AdvSceneSwitcher.tempVar.window.windowClass.description="The window class of the matching window."
|
||||
AdvSceneSwitcher.tempVar.window.windowText="Window text"
|
||||
@@ -2345,6 +2367,14 @@ AdvSceneSwitcher.tempVar.video.similarity="Similarity Rating"
|
||||
AdvSceneSwitcher.tempVar.video.similarity.description="Values range from 0 to 1, where 0 means dissimilar and 1 means highly similar."
|
||||
AdvSceneSwitcher.tempVar.video.patternCount="Pattern count"
|
||||
AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame."
|
||||
AdvSceneSwitcher.tempVar.video.matchX="Match X coordinate"
|
||||
AdvSceneSwitcher.tempVar.video.matchX.description="The X coordinate of the top-left corner of the best pattern match.\nIf the 'check area' option is enabled, the coordinate is relative to the video source, not the area.\nSet to -1 if no match was found."
|
||||
AdvSceneSwitcher.tempVar.video.matchY="Match Y coordinate"
|
||||
AdvSceneSwitcher.tempVar.video.matchY.description="The Y coordinate of the top-left corner of the best pattern match.\nIf the 'check area' option is enabled, the coordinate is relative to the video source, not the area.\nSet to -1 if no match was found."
|
||||
AdvSceneSwitcher.tempVar.video.matchWidth="Match width"
|
||||
AdvSceneSwitcher.tempVar.video.matchWidth.description="The width of the matched pattern in pixels."
|
||||
AdvSceneSwitcher.tempVar.video.matchHeight="Match height"
|
||||
AdvSceneSwitcher.tempVar.video.matchHeight.description="The height of the matched pattern in pixels."
|
||||
AdvSceneSwitcher.tempVar.video.objectCount="Object count"
|
||||
AdvSceneSwitcher.tempVar.video.objectCount.description="The number of objects the given model has identified in a given video input frame."
|
||||
AdvSceneSwitcher.tempVar.video.brightness="Average brightness"
|
||||
@@ -2538,6 +2568,9 @@ AdvSceneSwitcher.selectDisplay="--select display--"
|
||||
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
|
||||
AdvSceneSwitcher.selectWindowTip="Use \"OBS\" to specify OBS window\nUse \"Task Switching\"to specify ALT + TAB"
|
||||
|
||||
AdvSceneSwitcher.keepSourceActive="Keep source active"
|
||||
AdvSceneSwitcher.keepSourceActive.help="When a source is not visible in the active scene it becomes inactive, which can cause it to stop producing output (e.g. video frames or game capture hooks).\nEnabling this option forces the source to remain active at all times, ensuring it continues to produce output even when not displayed.\n\nNote: Keeping a source permanently active may increase CPU and GPU usage, as the source continues to run even when not shown."
|
||||
|
||||
AdvSceneSwitcher.settings.suffix.type.invalid=" (Invalid)"
|
||||
AdvSceneSwitcher.settings.suffix.type.bool=" (Bool)"
|
||||
AdvSceneSwitcher.settings.suffix.type.int=" (Int)"
|
||||
@@ -2705,6 +2738,64 @@ Basic.Settings.Video.FPSCommon="Common FPS Values"
|
||||
Basic.Settings.Video.FPSInteger="Integer FPS Value"
|
||||
Basic.Settings.Video.FPSFraction="Fractional FPS Value"
|
||||
|
||||
# Calendar widget
|
||||
AdvSceneSwitcher.calendar.today="Today"
|
||||
AdvSceneSwitcher.calendar.month="Month"
|
||||
AdvSceneSwitcher.calendar.week="Week"
|
||||
AdvSceneSwitcher.calendar.day="Day"
|
||||
AdvSceneSwitcher.calendar.moreEvents="+%1 more"
|
||||
|
||||
# Macro Schedule tab
|
||||
AdvSceneSwitcher.macroScheduleTab.title="Macro Schedule"
|
||||
AdvSceneSwitcher.macroScheduleTab.help="No scheduled entries.\nClick '+' to schedule a macro."
|
||||
AdvSceneSwitcher.macroScheduleTab.add="Add"
|
||||
AdvSceneSwitcher.macroScheduleTab.add.tooltip="Add schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleTab.edit="Edit"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.tooltip="Remove schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleTab.enable="Enable"
|
||||
AdvSceneSwitcher.macroScheduleTab.disable="Disable"
|
||||
AdvSceneSwitcher.macroScheduleTab.showAll="Show All"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.name="Name"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.macro="Macro"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.schedule="Schedule"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.nextTrigger="Next Trigger"
|
||||
AdvSceneSwitcher.macroScheduleTab.column.status="Status"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.confirm="Are you sure you want to remove \"%1\"?"
|
||||
AdvSceneSwitcher.macroScheduleTab.remove.confirmMultiple="Are you sure you want to remove %1 entries?"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.active="Active"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.disabled="Disabled"
|
||||
AdvSceneSwitcher.macroScheduleTab.status.expired="Expired"
|
||||
|
||||
# Macro Schedule entry dialog
|
||||
AdvSceneSwitcher.macroScheduleEntry.dialog.title.add="Add Schedule Entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.dialog.title.edit="Edit Schedule Entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.name="Name (optional):"
|
||||
AdvSceneSwitcher.macroScheduleEntry.name.placeholder="e.g. Daily stream start"
|
||||
AdvSceneSwitcher.macroScheduleEntry.macro="Macro:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.startDateTime="Start:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate="End date:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.help="Controls how far the entry spans visually in the calendar and optionally triggers an action when the date is reached."
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.none="No action"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.disableEntry="Disable schedule entry"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.pauseMacro="Pause macro"
|
||||
AdvSceneSwitcher.macroScheduleEntry.endDate.action.stopMacro="Stop macro"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat="Repeat"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.once="Once"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.every="Every %1"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeat.interval="Repeat interval:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.noMacro="(no macro)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd="End repeat:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.never="Never"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes="After"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes.suffix="occurrence(s)"
|
||||
AdvSceneSwitcher.macroScheduleEntry.repeatEnd.untilDate="Until"
|
||||
AdvSceneSwitcher.macroScheduleEntry.color="Color:"
|
||||
AdvSceneSwitcher.macroScheduleEntry.color.dialog="Pick entry color"
|
||||
AdvSceneSwitcher.macroScheduleEntry.checkConditions="Check macro conditions before running"
|
||||
AdvSceneSwitcher.macroScheduleEntry.runElseActionsOnConditionFailure="Run else actions if conditions are not met"
|
||||
AdvSceneSwitcher.macroScheduleEntry.enabled="Enabled"
|
||||
|
||||
# Legacy tabs below - please don't waste your time adding translations for these :)
|
||||
# Transition Tab
|
||||
AdvSceneSwitcher.transitionTab.title="Transition"
|
||||
|
||||
@@ -83,7 +83,6 @@ AdvSceneSwitcher.macroTab.highlightTrueConditions="Resaltar condiciones de la ma
|
||||
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
|
||||
|
||||
; Lógica de macros
|
||||
AdvSceneSwitcher.logic.none="Omitir entrada"
|
||||
AdvSceneSwitcher.logic.and="Y"
|
||||
AdvSceneSwitcher.logic.or="O"
|
||||
AdvSceneSwitcher.logic.andNot="Y no"
|
||||
|
||||
@@ -131,7 +131,6 @@ AdvSceneSwitcher.macroList.deleted="supprimé"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" est déjà sélectionné !"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Ignorer l'entrée"
|
||||
AdvSceneSwitcher.logic.and="Et"
|
||||
AdvSceneSwitcher.logic.or="Ou"
|
||||
AdvSceneSwitcher.logic.andNot="Et pas"
|
||||
|
||||
@@ -275,7 +275,6 @@ AdvSceneSwitcher.macroList.deleted="削除"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" はすでに選択されています!"
|
||||
|
||||
# Macro Logic
|
||||
AdvSceneSwitcher.logic.none="入力無視"
|
||||
; AdvSceneSwitcher.logic.and="And"
|
||||
; AdvSceneSwitcher.logic.or="Or"
|
||||
; AdvSceneSwitcher.logic.andNot="And not"
|
||||
|
||||
@@ -239,7 +239,6 @@ AdvSceneSwitcher.macroList.deleted="excluída"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" já está selecionada!"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Ignorar entrada"
|
||||
AdvSceneSwitcher.logic.and="E"
|
||||
AdvSceneSwitcher.logic.or="Ou"
|
||||
AdvSceneSwitcher.logic.andNot="E não"
|
||||
|
||||
@@ -70,7 +70,6 @@ AdvSceneSwitcher.macroTab.name="Имя:"
|
||||
AdvSceneSwitcher.macroTab.defaultname="Макрос %1"
|
||||
AdvSceneSwitcher.macroTab.copy="Создать копию"
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Игнорировать вход"
|
||||
AdvSceneSwitcher.logic.and="И"
|
||||
AdvSceneSwitcher.logic.or="Или"
|
||||
AdvSceneSwitcher.logic.andNot="И не"
|
||||
|
||||
@@ -78,7 +78,6 @@ AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet"
|
||||
AdvSceneSwitcher.macroTab.collapseAll="Hepsini Küçült"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="Girişi yoksay"
|
||||
AdvSceneSwitcher.logic.and="Ve"
|
||||
AdvSceneSwitcher.logic.or="Ya da"
|
||||
AdvSceneSwitcher.logic.andNot="ve değil"
|
||||
|
||||
@@ -259,7 +259,6 @@ AdvSceneSwitcher.macroList.deleted="删除"
|
||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" 已选择!"
|
||||
|
||||
; Macro Logic
|
||||
AdvSceneSwitcher.logic.none="忽略条件"
|
||||
AdvSceneSwitcher.logic.and="且"
|
||||
AdvSceneSwitcher.logic.or="或"
|
||||
AdvSceneSwitcher.logic.andNot="且不"
|
||||
|
||||
2
deps/cpp-httplib
vendored
2
deps/cpp-httplib
vendored
Submodule deps/cpp-httplib updated: c7ed1796a7...008e107d0f
2
deps/leptonica
vendored
2
deps/leptonica
vendored
Submodule deps/leptonica updated: b667978e86...13275a278e
2
deps/libusb
vendored
2
deps/libusb
vendored
Submodule deps/libusb updated: 15a7ebb4d4...bc368fb133
@@ -258,6 +258,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="settingsLock">
|
||||
<property name="text">
|
||||
<string>AdvSceneSwitcher.generalTab.generalBehavior.settingsLock</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QAction>
|
||||
#include <QDirIterator>
|
||||
#include <QInputDialog>
|
||||
#include <QLibrary>
|
||||
#include <QMainWindow>
|
||||
#include <QMessageBox>
|
||||
#include <QTextStream>
|
||||
#include <regex>
|
||||
|
||||
@@ -41,9 +43,12 @@ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
|
||||
{
|
||||
switcher->settingsWindowOpened = true;
|
||||
ui->setupUi(this);
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
switcher->Prune();
|
||||
LoadUI();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
switcher->Prune();
|
||||
LoadUI();
|
||||
}
|
||||
CheckFirstTimeSetup();
|
||||
}
|
||||
|
||||
AdvSceneSwitcher::~AdvSceneSwitcher()
|
||||
@@ -139,7 +144,6 @@ void AdvSceneSwitcher::LoadUI()
|
||||
SetTabOrder(ui->tabWidget);
|
||||
SetCurrentTab(ui->tabWidget);
|
||||
RestoreWindowGeo();
|
||||
CheckFirstTimeSetup();
|
||||
|
||||
loading = false;
|
||||
}
|
||||
@@ -457,12 +461,13 @@ void SwitcherData::Stop()
|
||||
SetMacroAbortWait(true);
|
||||
GetMacroWaitCV().notify_all();
|
||||
GetMacroTransitionCV().notify_all();
|
||||
StopAllMacros();
|
||||
SignalStopAllMacros();
|
||||
StopAndClearAllActionQueues();
|
||||
CloseAllInputDialogs();
|
||||
th->wait();
|
||||
delete th;
|
||||
th = nullptr;
|
||||
WaitForAllMacros();
|
||||
RunStopSteps();
|
||||
}
|
||||
|
||||
@@ -506,6 +511,8 @@ bool SwitcherData::AnySceneTransitionStarted()
|
||||
******************************************************************************/
|
||||
extern "C" EXPORT void FreeSceneSwitcher()
|
||||
{
|
||||
switcher->Stop();
|
||||
|
||||
PlatformCleanup();
|
||||
RunPluginCleanupSteps();
|
||||
|
||||
@@ -627,13 +634,20 @@ static void handleSceneCollectionCleanup()
|
||||
return;
|
||||
}
|
||||
|
||||
// OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP is also called on
|
||||
// shutdown.
|
||||
// Here we also don't want to clear the settings.
|
||||
if (switcher->obsIsShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
SaveSceneSwitcher(nullptr, false, nullptr);
|
||||
}
|
||||
|
||||
// Note to future self:
|
||||
// be careful using switcher->m here as there is potential for deadlocks when using
|
||||
// frontend functions such as obs_frontend_set_current_scene()
|
||||
static void OBSEvent(enum obs_frontend_event event, void *switcher)
|
||||
static void OBSEvent(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (!switcher) {
|
||||
return;
|
||||
@@ -707,19 +721,54 @@ static void LoadPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
static bool settingsPasswordIsCorrect(QWidget *parent)
|
||||
{
|
||||
bool ok;
|
||||
QString input = QInputDialog::getText(
|
||||
parent,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel"),
|
||||
QLineEdit::Password, QString(), &ok);
|
||||
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.toStdString() == switcher->settingsLockPassword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QMessageBox::warning(
|
||||
parent,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword"));
|
||||
return false;
|
||||
}
|
||||
|
||||
void OpenSettingsWindow()
|
||||
{
|
||||
if (switcher->settingsWindowOpened) {
|
||||
AdvSceneSwitcher::window->show();
|
||||
AdvSceneSwitcher::window->raise();
|
||||
AdvSceneSwitcher::window->activateWindow();
|
||||
} else {
|
||||
AdvSceneSwitcher::window =
|
||||
new AdvSceneSwitcher(static_cast<QMainWindow *>(
|
||||
obs_frontend_get_main_window()));
|
||||
AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
|
||||
AdvSceneSwitcher::window->show();
|
||||
return;
|
||||
}
|
||||
|
||||
auto parent =
|
||||
static_cast<QMainWindow *>(obs_frontend_get_main_window());
|
||||
if (switcher->settingsLockEnabled &&
|
||||
!switcher->settingsLockPassword.empty() &&
|
||||
!settingsPasswordIsCorrect(parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AdvSceneSwitcher::window = new AdvSceneSwitcher(parent);
|
||||
AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
|
||||
AdvSceneSwitcher::window->show();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::HighlightMacroSettingsButton(bool enable)
|
||||
@@ -767,8 +816,6 @@ void HighlightMacroSettingsButton(bool enable)
|
||||
enable);
|
||||
}
|
||||
|
||||
void SetupActionQueues();
|
||||
|
||||
extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
|
||||
translateFunc translate)
|
||||
{
|
||||
@@ -780,12 +827,11 @@ extern "C" EXPORT void InitSceneSwitcher(obs_module_t *module,
|
||||
PlatformInit();
|
||||
LoadPlugins();
|
||||
SetupDock();
|
||||
SetupActionQueues();
|
||||
|
||||
RunPluginInitSteps();
|
||||
|
||||
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
|
||||
obs_frontend_add_event_callback(OBSEvent, switcher);
|
||||
obs_frontend_add_event_callback(OBSEvent, nullptr);
|
||||
|
||||
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
|
||||
obs_module_text("AdvSceneSwitcher.pluginName"));
|
||||
|
||||
@@ -68,6 +68,7 @@ public slots:
|
||||
void on_warnPluginLoadFailure_stateChanged(int state);
|
||||
void on_suppressCrashRecoveryDialog_stateChanged(int state);
|
||||
void on_hideLegacyTabs_stateChanged(int state);
|
||||
void on_settingsLock_stateChanged(int state);
|
||||
void on_priorityUp_clicked();
|
||||
void on_priorityDown_clicked();
|
||||
void on_threadPriority_currentTextChanged(const QString &text);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -243,6 +245,64 @@ void AdvSceneSwitcher::on_hideLegacyTabs_stateChanged(int state)
|
||||
ui->priorityBox->setVisible(!switcher->hideLegacyTabs);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_settingsLock_stateChanged(int state)
|
||||
{
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
switcher->settingsLockEnabled = false;
|
||||
switcher->settingsLockPassword = "";
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString newPassword = QInputDialog::getText(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.newPasswordLabel"),
|
||||
QLineEdit::Password, QString(), &ok);
|
||||
|
||||
const auto disablePassword = [this]() {
|
||||
QSignalBlocker blocker(ui->settingsLock);
|
||||
ui->settingsLock->setChecked(false);
|
||||
ui->settingsLock->setCheckState(Qt::Unchecked);
|
||||
switcher->settingsLockEnabled = false;
|
||||
switcher->settingsLockPassword = "";
|
||||
};
|
||||
|
||||
if (!ok || newPassword.isEmpty()) {
|
||||
disablePassword();
|
||||
return;
|
||||
}
|
||||
|
||||
QString confirm = QInputDialog::getText(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.confirmPasswordLabel"),
|
||||
QLineEdit::Password, QString(), &ok);
|
||||
if (!ok || newPassword != confirm) {
|
||||
if (ok) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordMismatch"));
|
||||
}
|
||||
disablePassword();
|
||||
return;
|
||||
}
|
||||
|
||||
switcher->settingsLockEnabled = true;
|
||||
switcher->settingsLockPassword = newPassword.toStdString();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::SetDeprecationWarnings()
|
||||
{
|
||||
QString toolTip =
|
||||
@@ -391,12 +451,16 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
|
||||
return;
|
||||
}
|
||||
|
||||
auto macro = FirstRunWizard::ShowWizard(this);
|
||||
bool wasSkipped = false;
|
||||
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
|
||||
if (macro) {
|
||||
renameMacroIfNecessary(macro);
|
||||
QTimer::singleShot(0, this,
|
||||
[this, macro]() { ui->macros->Add(macro); });
|
||||
}
|
||||
if (wasSkipped) {
|
||||
ui->alwaysShowFeatureTabs->setChecked(true);
|
||||
}
|
||||
switcher->Start();
|
||||
}
|
||||
|
||||
@@ -468,7 +532,6 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
|
||||
// Needs to be loaded before any entries which might rely on scene group
|
||||
// selections to be available.
|
||||
loadSceneGroups(obj);
|
||||
LoadVariables(obj);
|
||||
|
||||
RunLoadSteps(obj);
|
||||
|
||||
@@ -507,7 +570,6 @@ void SwitcherData::SaveSettings(obs_data_t *obj)
|
||||
saveSceneGroups(obj);
|
||||
SaveMacros(obj);
|
||||
SaveGlobalMacroSettings(obj);
|
||||
SaveVariables(obj);
|
||||
saveWindowTitleSwitches(obj);
|
||||
saveScreenRegionSwitches(obj);
|
||||
savePauseSwitches(obj);
|
||||
@@ -565,6 +627,9 @@ void SwitcherData::SaveGeneralSettings(obs_data_t *obj)
|
||||
disableFilterComboboxFilter);
|
||||
obs_data_set_bool(obj, "warnPluginLoadFailure", warnPluginLoadFailure);
|
||||
obs_data_set_bool(obj, "hideLegacyTabs", hideLegacyTabs);
|
||||
obs_data_set_bool(obj, "settingsLockEnabled", settingsLockEnabled);
|
||||
obs_data_set_string(obj, "settingsLockPassword",
|
||||
settingsLockPassword.c_str());
|
||||
|
||||
SaveFunctionPriorities(obj, functionNamesByPriority);
|
||||
|
||||
@@ -636,6 +701,8 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
|
||||
warnPluginLoadFailure = obs_data_get_bool(obj, "warnPluginLoadFailure");
|
||||
obs_data_set_default_bool(obj, "hideLegacyTabs", true);
|
||||
hideLegacyTabs = obs_data_get_bool(obj, "hideLegacyTabs");
|
||||
settingsLockEnabled = obs_data_get_bool(obj, "settingsLockEnabled");
|
||||
settingsLockPassword = obs_data_get_string(obj, "settingsLockPassword");
|
||||
|
||||
SetDefaultFunctionPriorities(obj);
|
||||
LoadFunctionPriorities(obj, functionNamesByPriority);
|
||||
@@ -958,6 +1025,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
|
||||
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
||||
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
|
||||
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
||||
ui->settingsLock->setChecked(switcher->settingsLockEnabled);
|
||||
|
||||
populatePriorityFunctionList(ui->priorityList);
|
||||
populateThreadPriorityList(ui->threadPriority);
|
||||
|
||||
@@ -178,47 +178,19 @@ void AdvSceneSwitcher::on_ignoreWindows_currentRowChanged(int idx)
|
||||
}
|
||||
}
|
||||
|
||||
void checkWindowTitleSwitchDirect(WindowSwitch &s,
|
||||
std::string ¤tWindowTitle, bool &match,
|
||||
OBSWeakSource &scene,
|
||||
OBSWeakSource &transition)
|
||||
static bool windowInfoMatchesSwitch(const WindowInfo &info,
|
||||
const WindowSwitch &s)
|
||||
{
|
||||
bool focus = (!s.focus || s.window == currentWindowTitle);
|
||||
bool fullscreen = (!s.fullscreen || IsFullscreen(s.window));
|
||||
bool max = (!s.maximized || IsMaximized(s.window));
|
||||
|
||||
if (focus && fullscreen && max) {
|
||||
match = true;
|
||||
scene = s.getScene();
|
||||
transition = s.transition;
|
||||
if (s.focus && !info.focused) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void checkWindowTitleSwitchRegex(WindowSwitch &s,
|
||||
std::string ¤tWindowTitle,
|
||||
std::vector<std::string> windowList,
|
||||
bool &match, OBSWeakSource &scene,
|
||||
OBSWeakSource &transition)
|
||||
{
|
||||
for (auto &window : windowList) {
|
||||
try {
|
||||
std::regex expr(s.window);
|
||||
if (!std::regex_match(window, expr)) {
|
||||
continue;
|
||||
}
|
||||
} catch (const std::regex_error &) {
|
||||
}
|
||||
|
||||
bool focus = (!s.focus || window == currentWindowTitle);
|
||||
bool fullscreen = (!s.fullscreen || IsFullscreen(window));
|
||||
bool max = (!s.maximized || IsMaximized(window));
|
||||
|
||||
if (focus && fullscreen && max) {
|
||||
match = true;
|
||||
scene = s.getScene();
|
||||
transition = s.transition;
|
||||
}
|
||||
if (s.fullscreen && !info.fullscreen) {
|
||||
return false;
|
||||
}
|
||||
if (s.maximized && !info.maximized) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
|
||||
@@ -228,23 +200,37 @@ bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string currentWindowTitle = switcher->currentTitle;
|
||||
bool match = false;
|
||||
const auto windowList = GetWindowList();
|
||||
|
||||
WindowQueryOptions options;
|
||||
options.focus = true;
|
||||
options.fullscreen = true;
|
||||
options.maximized = true;
|
||||
const auto windows = GetWindows(options);
|
||||
|
||||
for (WindowSwitch &s : windowSwitches) {
|
||||
if (!s.initialized()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::find(windowList.begin(), windowList.end(), s.window) !=
|
||||
windowList.end()) {
|
||||
checkWindowTitleSwitchDirect(s, currentWindowTitle,
|
||||
match, scene, transition);
|
||||
} else {
|
||||
checkWindowTitleSwitchRegex(s, currentWindowTitle,
|
||||
windowList, match, scene,
|
||||
transition);
|
||||
for (const auto &info : windows) {
|
||||
bool titleMatch = false;
|
||||
try {
|
||||
std::regex expr(s.window);
|
||||
titleMatch = info.title == s.window ||
|
||||
std::regex_match(info.title, expr);
|
||||
} catch (const std::regex_error &) {
|
||||
titleMatch = info.title == s.window;
|
||||
}
|
||||
|
||||
if (!titleMatch || !windowInfoMatchesSwitch(info, s)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match = true;
|
||||
scene = s.getScene();
|
||||
transition = s.transition;
|
||||
break;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
|
||||
@@ -236,17 +236,89 @@ std::string getWindowName(Window window)
|
||||
return windowTitle;
|
||||
}
|
||||
|
||||
std::vector<std::string> GetWindowList()
|
||||
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
|
||||
{
|
||||
std::vector<std::string> windows;
|
||||
for (auto window : getTopLevelWindows()) {
|
||||
const std::string foregroundTitle = GetCurrentWindowTitle();
|
||||
const auto topLevel = getTopLevelWindows();
|
||||
auto display = disp();
|
||||
|
||||
std::vector<WindowInfo> result;
|
||||
result.reserve(topLevel.size());
|
||||
|
||||
for (auto window : topLevel) {
|
||||
auto name = getWindowName(window);
|
||||
if (name.empty()) {
|
||||
continue;
|
||||
}
|
||||
windows.emplace_back(name);
|
||||
|
||||
WindowInfo info;
|
||||
info.title = name;
|
||||
|
||||
if (options.focus) {
|
||||
info.focused = (name == foregroundTitle);
|
||||
}
|
||||
|
||||
if (options.fullscreen || options.maximized) {
|
||||
QStringList states = getStates(window);
|
||||
if (options.fullscreen) {
|
||||
info.fullscreen = states.contains(
|
||||
"_NET_WM_STATE_FULLSCREEN");
|
||||
}
|
||||
if (options.maximized) {
|
||||
info.maximized =
|
||||
states.contains(
|
||||
"_NET_WM_STATE_MAXIMIZED_VERT") &&
|
||||
states.contains(
|
||||
"_NET_WM_STATE_MAXIMIZED_HORZ");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.geometry && display) {
|
||||
XWindowAttributes attrs;
|
||||
if (XGetWindowAttributes(display, window, &attrs)) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
Window child;
|
||||
XTranslateCoordinates(
|
||||
display, window,
|
||||
DefaultRootWindow(display), 0, 0, &x,
|
||||
&y, &child);
|
||||
info.x = x;
|
||||
info.y = y;
|
||||
info.width = attrs.width;
|
||||
info.height = attrs.height;
|
||||
}
|
||||
}
|
||||
|
||||
// windowClass and text not implemented on Linux
|
||||
result.emplace_back(std::move(info));
|
||||
}
|
||||
return windows;
|
||||
|
||||
// When KWin compat is active, native Wayland windows are not tracked by
|
||||
// X11 and therefore do not appear in the list above. Merge in any window
|
||||
// reported by KWin that is not already present (matched by title) so that
|
||||
// title and focus conditions work for native Wayland windows.
|
||||
if (KWin) {
|
||||
for (const auto &[pid, title] :
|
||||
FocusNotifier::getWindowList()) {
|
||||
bool found = false;
|
||||
for (const auto &info : result) {
|
||||
if (info.title == title) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
WindowInfo waylandWindow;
|
||||
waylandWindow.title = title;
|
||||
waylandWindow.focused =
|
||||
(title == foregroundTitle);
|
||||
result.emplace_back(std::move(waylandWindow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int getActiveWindow(Window *&window)
|
||||
@@ -290,68 +362,6 @@ std::string GetCurrentWindowTitle()
|
||||
return name;
|
||||
}
|
||||
|
||||
bool windowStatesAreSet(const std::string &windowTitle,
|
||||
std::vector<QString> &expectedStates)
|
||||
{
|
||||
if (!ewmhIsSupported()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Window> windows = getTopLevelWindows();
|
||||
for (auto &window : windows) {
|
||||
auto name = getWindowName(window);
|
||||
if (name.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool equals = windowTitle == name;
|
||||
bool matches = QString::fromStdString(name).contains(
|
||||
QRegularExpression(
|
||||
QString::fromStdString(windowTitle)));
|
||||
|
||||
if (!(equals || matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QStringList states = getStates(window);
|
||||
if (states.isEmpty()) {
|
||||
if (expectedStates.empty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto &state : expectedStates) {
|
||||
if (!states.contains(state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsMaximized(const std::string &title)
|
||||
{
|
||||
std::vector<QString> states;
|
||||
states.emplace_back("_NET_WM_STATE_MAXIMIZED_VERT");
|
||||
states.emplace_back("_NET_WM_STATE_MAXIMIZED_HORZ");
|
||||
return windowStatesAreSet(title, states);
|
||||
}
|
||||
|
||||
bool IsFullscreen(const std::string &title)
|
||||
{
|
||||
std::vector<QString> states;
|
||||
states.emplace_back("_NET_WM_STATE_FULLSCREEN");
|
||||
return windowStatesAreSet(title, states);
|
||||
}
|
||||
|
||||
std::optional<std::string> GetTextInWindow(const std::string &)
|
||||
{
|
||||
// Not implemented
|
||||
return {};
|
||||
}
|
||||
|
||||
static void getProcessListProcps(QStringList &processes)
|
||||
{
|
||||
#ifdef PROCPS_AVAILABLE
|
||||
|
||||
@@ -12,35 +12,82 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
std::mutex FocusNotifier::_mutex;
|
||||
int FocusNotifier::activePID = -1;
|
||||
std::string FocusNotifier::activeTitle = {};
|
||||
std::map<std::string, std::pair<int, std::string>> FocusNotifier::_windows;
|
||||
|
||||
int FocusNotifier::getActiveWindowPID()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return activePID;
|
||||
}
|
||||
|
||||
std::string FocusNotifier::getActiveWindowTitle()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return activeTitle;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, std::string>> FocusNotifier::getWindowList()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::vector<std::pair<int, std::string>> result;
|
||||
result.reserve(_windows.size());
|
||||
for (const auto &entry : _windows) {
|
||||
result.emplace_back(entry.second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void FocusNotifier::focusChanged(const int pid)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
activePID = pid;
|
||||
}
|
||||
|
||||
void FocusNotifier::focusTitle(const QString &title)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
activeTitle = title.toStdString();
|
||||
}
|
||||
|
||||
void FocusNotifier::focusUpdate(const int pid, const QString &title)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
activePID = pid;
|
||||
activeTitle = title.toStdString();
|
||||
}
|
||||
|
||||
void FocusNotifier::windowAdded(const QString &id, const int pid,
|
||||
const QString &title)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_windows[id.toStdString()] = {pid, title.toStdString()};
|
||||
}
|
||||
|
||||
void FocusNotifier::windowRemoved(const QString &id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_windows.erase(id.toStdString());
|
||||
}
|
||||
|
||||
void FocusNotifier::windowTitleChanged(const QString &id, const QString &title)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
auto it = _windows.find(id.toStdString());
|
||||
if (it != _windows.end()) {
|
||||
it->second.second = title.toStdString();
|
||||
}
|
||||
}
|
||||
|
||||
bool isKWinAvailable()
|
||||
{
|
||||
const QDBusConnectionInterface *interface =
|
||||
QDBusConnection::sessionBus().interface();
|
||||
if (!interface)
|
||||
if (!interface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList services =
|
||||
interface->registeredServiceNames().value();
|
||||
@@ -53,25 +100,32 @@ bool startKWinScript(QString &scriptObjectPath)
|
||||
"/tmp/AdvancedSceneSwitcher/KWinFocusNotifier.js";
|
||||
|
||||
const QString script =
|
||||
R"(workspace.windowActivated.connect(function(client) {
|
||||
if (!client) return;
|
||||
if (!client.pid) return;
|
||||
if (!client.caption) return;
|
||||
R"(var adss = "com.github.AdvancedSceneSwitcher";
|
||||
var adssPath = "/com/github/AdvancedSceneSwitcher";
|
||||
|
||||
callDBus(
|
||||
"com.github.AdvancedSceneSwitcher",
|
||||
"/com/github/AdvancedSceneSwitcher",
|
||||
"com.github.AdvancedSceneSwitcher",
|
||||
"focusChanged",
|
||||
client.pid
|
||||
);
|
||||
callDBus(
|
||||
"com.github.AdvancedSceneSwitcher",
|
||||
"/com/github/AdvancedSceneSwitcher",
|
||||
"com.github.AdvancedSceneSwitcher",
|
||||
"focusTitle",
|
||||
client.caption
|
||||
);
|
||||
function trackWindow(window) {
|
||||
var id = window.internalId.toString();
|
||||
callDBus(adss, adssPath, adss, "windowAdded", id, window.pid, window.caption);
|
||||
window.captionChanged.connect(function() {
|
||||
callDBus(adss, adssPath, adss, "windowTitleChanged", id, window.caption);
|
||||
});
|
||||
}
|
||||
|
||||
var windows = workspace.windowList();
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
trackWindow(windows[i]);
|
||||
}
|
||||
|
||||
workspace.windowAdded.connect(trackWindow);
|
||||
|
||||
workspace.windowRemoved.connect(function(window) {
|
||||
callDBus(adss, adssPath, adss, "windowRemoved", window.internalId.toString());
|
||||
});
|
||||
|
||||
workspace.windowActivated.connect(function(client) {
|
||||
callDBus(adss, adssPath, adss, "focusUpdate",
|
||||
client ? client.pid : 0,
|
||||
client ? client.caption : "");
|
||||
}))";
|
||||
|
||||
if (const QDir dir; !dir.mkpath(QFileInfo(scriptPath).absolutePath())) {
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -10,18 +14,27 @@ class FocusNotifier final : public QObject {
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "com.github.AdvancedSceneSwitcher")
|
||||
|
||||
static std::mutex _mutex;
|
||||
static int activePID;
|
||||
static std::string activeTitle;
|
||||
// Maps KWin internal window ID to {pid, title}
|
||||
static std::map<std::string, std::pair<int, std::string>> _windows;
|
||||
|
||||
public:
|
||||
using QObject::QObject;
|
||||
|
||||
static int getActiveWindowPID();
|
||||
static std::string getActiveWindowTitle();
|
||||
static std::vector<std::pair<int, std::string>> getWindowList();
|
||||
|
||||
public slots:
|
||||
void focusChanged(const int pid);
|
||||
void focusTitle(const QString &title);
|
||||
void focusUpdate(const int pid, const QString &title);
|
||||
void windowAdded(const QString &id, const int pid,
|
||||
const QString &title);
|
||||
void windowRemoved(const QString &id);
|
||||
void windowTitleChanged(const QString &id, const QString &title);
|
||||
};
|
||||
|
||||
bool isKWinAvailable();
|
||||
|
||||
@@ -164,4 +164,9 @@ std::shared_ptr<MacroSegment> MacroActionEdit::Data() const
|
||||
return *_entryData;
|
||||
}
|
||||
|
||||
void MacroActionEdit::FocusTypeSelection()
|
||||
{
|
||||
_actionSelection->setFocus();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
std::shared_ptr<MacroAction> * = nullptr);
|
||||
void SetupWidgets(bool basicSetup = false);
|
||||
void SetEntryData(std::shared_ptr<MacroAction> *);
|
||||
void FocusTypeSelection() override;
|
||||
|
||||
private slots:
|
||||
void ActionSelectionChanged(const QString &text);
|
||||
|
||||
@@ -263,6 +263,7 @@ bool MacroActionMacro::Load(obs_data_t *obj)
|
||||
if (obs_data_has_user_value(obj, "nestedMacro")) {
|
||||
OBSDataAutoRelease nestedMacroData =
|
||||
obs_data_get_obj(obj, "nestedMacro");
|
||||
_nestedMacro = std::make_shared<Macro>();
|
||||
_nestedMacro->Load(nestedMacroData);
|
||||
}
|
||||
|
||||
@@ -309,6 +310,15 @@ void MacroActionMacro::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_actionIndex.ResolveVariables();
|
||||
_label.ResolveVariables();
|
||||
|
||||
if (_action == Action::NESTED_MACRO) {
|
||||
for (auto &action : _nestedMacro->Actions()) {
|
||||
action->ResolveVariablesToFixedValues();
|
||||
}
|
||||
for (auto &action : _nestedMacro->ElseActions()) {
|
||||
action->ResolveVariablesToFixedValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void runActionsHelper(Macro *macro, bool runElseActions, bool setInputs,
|
||||
@@ -871,6 +881,8 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
||||
} else {
|
||||
_noConditionsWarning->setVisible(false);
|
||||
}
|
||||
_runMacroHelp->setVisible(action ==
|
||||
MacroActionMacro::Action::RUN_MACRO);
|
||||
|
||||
_nestedMacro->setVisible(action ==
|
||||
MacroActionMacro::Action::NESTED_MACRO);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "section.hpp"
|
||||
#include "switch-button.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
@@ -102,12 +104,15 @@ MacroConditionEdit::MacroConditionEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroCondition> *entryData,
|
||||
bool isRootCondition)
|
||||
: MacroSegmentEdit(parent),
|
||||
_enable(new SwitchButton()),
|
||||
_logicSelection(new QComboBox()),
|
||||
_conditionSelection(new FilterComboBox()),
|
||||
_dur(new DurationModifierEdit()),
|
||||
_entryData(entryData),
|
||||
_isRoot(isRootCondition)
|
||||
{
|
||||
QWidget::connect(_enable, SIGNAL(checked(bool)), this,
|
||||
SLOT(ConditionEnableChanged(bool)));
|
||||
QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(LogicSelectionChanged(int)));
|
||||
QWidget::connect(_conditionSelection,
|
||||
@@ -122,6 +127,7 @@ MacroConditionEdit::MacroConditionEdit(
|
||||
Logic::PopulateLogicTypeSelection(_logicSelection, isRootCondition);
|
||||
populateConditionSelection(_conditionSelection);
|
||||
|
||||
_section->AddHeaderWidget(_enable);
|
||||
_section->AddHeaderWidget(_logicSelection);
|
||||
_section->AddHeaderWidget(_conditionSelection);
|
||||
_section->AddHeaderWidget(_headerInfo);
|
||||
@@ -154,8 +160,17 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
|
||||
const auto logic = static_cast<Logic::Type>(
|
||||
_logicSelection->itemData(idx).toInt());
|
||||
(*_entryData)->SetLogicType(logic);
|
||||
}
|
||||
|
||||
SetEnableAppearance(logic != Logic::Type::NONE);
|
||||
void MacroConditionEdit::ConditionEnableChanged(bool value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
(*_entryData)->SetEnabled(value);
|
||||
SetDisableEffect(!value);
|
||||
}
|
||||
|
||||
bool MacroConditionEdit::IsRootNode() const
|
||||
@@ -168,7 +183,9 @@ void MacroConditionEdit::SetLogicSelection()
|
||||
const auto logic = (*_entryData)->GetLogicType();
|
||||
_logicSelection->setCurrentIndex(
|
||||
_logicSelection->findData(static_cast<int>(logic)));
|
||||
SetEnableAppearance(logic != Logic::Type::NONE);
|
||||
const bool enabled = (*_entryData)->Enabled();
|
||||
_enable->setChecked(enabled);
|
||||
SetEnableAppearance(enabled);
|
||||
}
|
||||
|
||||
void MacroConditionEdit::SetRootNode(bool root)
|
||||
@@ -239,10 +256,12 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
|
||||
{
|
||||
auto lock = LockContext();
|
||||
auto logic = (*_entryData)->GetLogicType();
|
||||
const bool enabled = (*_entryData)->Enabled();
|
||||
_entryData->reset();
|
||||
*_entryData = MacroConditionFactory::Create(id, macro);
|
||||
(*_entryData)->SetIndex(idx);
|
||||
(*_entryData)->SetLogicType(logic);
|
||||
(*_entryData)->SetEnabled(enabled);
|
||||
(*_entryData)->PostLoad();
|
||||
RunAndClearPostLoadSteps();
|
||||
}
|
||||
@@ -256,6 +275,9 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
|
||||
SetupVarMappings((*_entryData).get());
|
||||
_dur->setVisible(MacroConditionFactory::UsesDurationModifier(id));
|
||||
SetFocusPolicyOfWidgets();
|
||||
|
||||
emit MacroSignalManager::Instance()->ConditionTypeCreated(
|
||||
QString::fromStdString(id));
|
||||
}
|
||||
|
||||
void MacroConditionEdit::DurationChanged(const Duration &seconds)
|
||||
@@ -283,4 +305,9 @@ std::shared_ptr<MacroSegment> MacroConditionEdit::Data() const
|
||||
return *_entryData;
|
||||
}
|
||||
|
||||
void MacroConditionEdit::FocusTypeSelection()
|
||||
{
|
||||
_conditionSelection->setFocus();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
class SwitchButton;
|
||||
|
||||
class DurationModifierEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -41,17 +43,20 @@ public:
|
||||
void SetRootNode(bool);
|
||||
void SetupWidgets(bool basicSetup = false);
|
||||
void SetEntryData(std::shared_ptr<MacroCondition> *);
|
||||
void FocusTypeSelection() override;
|
||||
|
||||
private slots:
|
||||
void LogicSelectionChanged(int idx);
|
||||
void ConditionSelectionChanged(const QString &text);
|
||||
void DurationChanged(const Duration &value);
|
||||
void DurationModifierChanged(DurationModifier::Type m);
|
||||
void ConditionEnableChanged(bool);
|
||||
|
||||
private:
|
||||
void SetLogicSelection();
|
||||
std::shared_ptr<MacroSegment> Data() const;
|
||||
|
||||
SwitchButton *_enable;
|
||||
QComboBox *_logicSelection;
|
||||
FilterComboBox *_conditionSelection;
|
||||
DurationModifierEdit *_dur;
|
||||
|
||||
@@ -31,6 +31,10 @@ bool MacroCondition::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroSegment::Load(obj);
|
||||
_logic.Load(obj, "logic");
|
||||
if (_logic.GetType() == Logic::Type::NONE) {
|
||||
SetEnabled(false);
|
||||
_logic.SetType(Logic::Type::AND);
|
||||
}
|
||||
_durationModifier.Load(obj);
|
||||
return true;
|
||||
}
|
||||
@@ -50,7 +54,8 @@ void MacroCondition::ValidateLogicSelection(bool isRootCondition,
|
||||
return;
|
||||
}
|
||||
|
||||
_logic.SetType(Logic::Type::NONE);
|
||||
_logic.SetType(Logic::Type::AND);
|
||||
SetEnabled(false);
|
||||
blog(LOG_WARNING,
|
||||
"setting invalid logic selection to 'ignore' for macro %s",
|
||||
context);
|
||||
|
||||
@@ -650,6 +650,18 @@ void MacroEdit::HighlightCondition(int idx, QColor color) const
|
||||
ui->conditionsList->Highlight(idx, color);
|
||||
}
|
||||
|
||||
void MacroEdit::ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
|
||||
int *currentIdx)
|
||||
{
|
||||
QTimer::singleShot(0, this, [this, list, newIdx, currentIdx]() {
|
||||
list->ensureWidgetVisible(list->WidgetAt(*currentIdx));
|
||||
auto *widget = list->WidgetAt(newIdx);
|
||||
if (widget) {
|
||||
widget->FocusTypeSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void resetSegmentHighlights(MacroSegmentList *list)
|
||||
{
|
||||
MacroSegmentEdit *widget = nullptr;
|
||||
@@ -1087,10 +1099,7 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
|
||||
ui->actionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->actionsList->ensureWidgetVisible(
|
||||
ui->actionsList->WidgetAt(currentActionIdx));
|
||||
});
|
||||
ScrollAndFocusNewSegment(ui->actionsList, idx, ¤tActionIdx);
|
||||
}
|
||||
|
||||
void MacroEdit::AddMacroAction(int idx)
|
||||
@@ -1118,6 +1127,9 @@ void MacroEdit::AddMacroAction(int idx)
|
||||
macro->Actions().at(idx - 1)->Save(data);
|
||||
}
|
||||
AddMacroAction(macro.get(), idx, id, data);
|
||||
if (idx < (int)macro->Actions().size()) {
|
||||
macro->Actions().at(idx)->FixupVarMappingRefs();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroEdit::on_actionAdd_clicked()
|
||||
@@ -1395,10 +1407,8 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
|
||||
ui->elseActionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->elseActionsList->ensureWidgetVisible(
|
||||
ui->elseActionsList->WidgetAt(currentElseActionIdx));
|
||||
});
|
||||
ScrollAndFocusNewSegment(ui->elseActionsList, idx,
|
||||
¤tElseActionIdx);
|
||||
}
|
||||
|
||||
void MacroEdit::AddMacroElseAction(int idx)
|
||||
@@ -1426,6 +1436,9 @@ void MacroEdit::AddMacroElseAction(int idx)
|
||||
macro->ElseActions().at(idx - 1)->Save(data);
|
||||
}
|
||||
AddMacroElseAction(macro.get(), idx, id, data);
|
||||
if (idx < (int)macro->ElseActions().size()) {
|
||||
macro->ElseActions().at(idx)->FixupVarMappingRefs();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroEdit::RemoveMacroElseAction(int idx)
|
||||
@@ -1570,6 +1583,9 @@ void MacroEdit::AddMacroCondition(int idx)
|
||||
macro->Conditions().at(idx - 1)->Save(data);
|
||||
}
|
||||
AddMacroCondition(macro.get(), idx, id, data.Get(), logic);
|
||||
if (idx < (int)macro->Conditions().size()) {
|
||||
macro->Conditions().at(idx)->FixupVarMappingRefs();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
|
||||
@@ -1602,10 +1618,7 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
|
||||
ui->conditionsList->SetHelpMsgVisible(false);
|
||||
emit(MacroSegmentOrderChanged());
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
ui->conditionsList->ensureWidgetVisible(
|
||||
ui->conditionsList->WidgetAt(currentConditionIdx));
|
||||
});
|
||||
ScrollAndFocusNewSegment(ui->conditionsList, idx, ¤tConditionIdx);
|
||||
}
|
||||
|
||||
void MacroEdit::on_conditionAdd_clicked()
|
||||
|
||||
@@ -116,6 +116,8 @@ private:
|
||||
void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0);
|
||||
void PopulateMacroElseActions(Macro &m, uint32_t afterIdx = 0);
|
||||
void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0);
|
||||
void ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
|
||||
int *currentIdx);
|
||||
void SetupMacroSegmentSelection(MacroSection type, int idx);
|
||||
void
|
||||
SetupContextMenu(const QPoint &pos,
|
||||
|
||||
89
lib/macro/macro-export-builtin-extensions.cpp
Normal file
89
lib/macro/macro-export-builtin-extensions.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "macro-export-extensions.hpp"
|
||||
|
||||
#include "action-queue.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool setup();
|
||||
static bool setupDone = setup();
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
// --- Variables ---
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.variables", "variables",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
if (selectedIds.isEmpty()) {
|
||||
SaveVariables(data);
|
||||
return;
|
||||
}
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &v : GetVariables()) {
|
||||
const QString name =
|
||||
QString::fromStdString(v->Name());
|
||||
if (!selectedIds.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
v->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "variables", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
ImportVariables(data);
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &v : GetVariables()) {
|
||||
const QString name =
|
||||
QString::fromStdString(v->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
|
||||
// --- Action Queues ---
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.actionQueues",
|
||||
"actionQueues",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
if (selectedIds.isEmpty()) {
|
||||
SaveActionQueues(data);
|
||||
return;
|
||||
}
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &q : GetActionQueues()) {
|
||||
const QString name =
|
||||
QString::fromStdString(q->Name());
|
||||
if (!selectedIds.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
q->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "actionQueues", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
ImportQueues(data);
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &q : GetActionQueues()) {
|
||||
const QString name =
|
||||
QString::fromStdString(q->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
17
lib/macro/macro-export-extensions.cpp
Normal file
17
lib/macro/macro-export-extensions.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "macro-export-extensions.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::vector<MacroExportExtension> extensions;
|
||||
|
||||
void AddMacroExportExtension(const MacroExportExtension &ext)
|
||||
{
|
||||
extensions.emplace_back(ext);
|
||||
}
|
||||
|
||||
const std::vector<MacroExportExtension> &GetMacroExportExtensions()
|
||||
{
|
||||
return extensions;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
41
lib/macro/macro-export-extensions.hpp
Normal file
41
lib/macro/macro-export-extensions.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <obs-data.h>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct MacroExportExtension {
|
||||
const char *displayNameKey;
|
||||
const char *jsonKey;
|
||||
|
||||
// Export callback.
|
||||
// Write content into 'data'. 'selectedIds' contains the item IDs the
|
||||
// user explicitly checked; if empty (no per-item widget provided) save
|
||||
// everything.
|
||||
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
|
||||
save;
|
||||
|
||||
// Import callback.
|
||||
// Read content from 'data'. 'selectedIds' contains the item IDs the
|
||||
// user explicitly checked; if empty import every item found in 'data'.
|
||||
std::function<void(obs_data_t *data, const QStringList &selectedIds)>
|
||||
load;
|
||||
|
||||
// Optional: enumerate items available for export as (id, displayName)
|
||||
// pairs. When nullptr the extension appears as a single all-or-nothing
|
||||
// checkbox in the export dialog.
|
||||
std::function<QList<QPair<QString, QString>>()> getExportItems =
|
||||
nullptr;
|
||||
};
|
||||
|
||||
EXPORT void AddMacroExportExtension(const MacroExportExtension &ext);
|
||||
const std::vector<MacroExportExtension> &GetMacroExportExtensions();
|
||||
|
||||
} // namespace advss
|
||||
@@ -1,22 +1,183 @@
|
||||
#include "macro-export-import-dialog.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "section.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <QLayout>
|
||||
#include <QLabel>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QSplitter>
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool usePlainText = false;
|
||||
|
||||
MacroExportImportDialog::MacroExportImportDialog(Type type)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compress / decompress helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static QString compressMacroString(const QString &input)
|
||||
{
|
||||
QByteArray inputData = input.toUtf8();
|
||||
auto compressedData = qCompress(inputData);
|
||||
QByteArray encodedData = compressedData.toBase64();
|
||||
return QString::fromUtf8(encodedData);
|
||||
}
|
||||
|
||||
static QString decompressMacroString(const QString &input)
|
||||
{
|
||||
QByteArray encodedData = input.toUtf8();
|
||||
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
||||
auto outputData = qUncompress(compressedData);
|
||||
return QString::fromUtf8(outputData);
|
||||
}
|
||||
|
||||
static bool isValidData(const QString &json)
|
||||
{
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
return !!data;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension section
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QWidget *MacroExportImportDialog::BuildExtensionWidget()
|
||||
{
|
||||
auto wrapper = new QWidget(this);
|
||||
auto wrapperLayout = new QVBoxLayout(wrapper);
|
||||
wrapperLayout->setContentsMargins(0, 0, 0, 0);
|
||||
wrapperLayout->setSpacing(4);
|
||||
|
||||
wrapperLayout->addWidget(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.export.additionalContent")));
|
||||
|
||||
auto outerContent = new QWidget();
|
||||
auto outerLayout = new QVBoxLayout(outerContent);
|
||||
outerLayout->setContentsMargins(4, 4, 4, 4);
|
||||
outerLayout->setSpacing(4);
|
||||
|
||||
const auto &extensions = GetMacroExportExtensions();
|
||||
|
||||
std::vector<int> order(extensions.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
||||
return QString::localeAwareCompare(
|
||||
obs_module_text(extensions[a].displayNameKey),
|
||||
obs_module_text(extensions[b].displayNameKey)) <
|
||||
0;
|
||||
});
|
||||
|
||||
for (int extIdx : order) {
|
||||
const auto &ext = extensions[extIdx];
|
||||
ExtensionUI ui;
|
||||
|
||||
// Simple all-or-nothing checkbox (no sub-items).
|
||||
if (!ext.getExportItems) {
|
||||
ui.mainCheck = new QCheckBox(
|
||||
obs_module_text(ext.displayNameKey),
|
||||
outerContent);
|
||||
ui.mainCheck->setChecked(false);
|
||||
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UpdateExportString);
|
||||
outerLayout->addWidget(ui.mainCheck);
|
||||
_extensionUIs.append(std::move(ui));
|
||||
_extensionOrder.push_back(extIdx);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extensions with per-item selection get their own inner
|
||||
// Section: the main checkbox sits in the header (next to the
|
||||
// toggle arrow) and the per-item checkboxes live in the
|
||||
// collapsible content area.
|
||||
auto items = ext.getExportItems();
|
||||
if (items.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const QPair<QString, QString> &a,
|
||||
const QPair<QString, QString> &b) {
|
||||
return QString::localeAwareCompare(
|
||||
a.second, b.second) < 0;
|
||||
});
|
||||
|
||||
auto innerSection = new Section(200, outerContent);
|
||||
|
||||
ui.mainCheck =
|
||||
new QCheckBox(obs_module_text(ext.displayNameKey));
|
||||
ui.mainCheck->setChecked(false);
|
||||
innerSection->AddHeaderWidget(ui.mainCheck);
|
||||
|
||||
auto subWidget = new QWidget();
|
||||
auto subLayout = new QVBoxLayout(subWidget);
|
||||
subLayout->setContentsMargins(4, 2, 4, 2);
|
||||
subLayout->setSpacing(2);
|
||||
|
||||
for (const auto &[id, displayName] : items) {
|
||||
auto cb = new QCheckBox(displayName, subWidget);
|
||||
cb->setChecked(false);
|
||||
subLayout->addWidget(cb);
|
||||
ui.itemChecks.append({id, cb});
|
||||
connect(cb, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UpdateExportString);
|
||||
}
|
||||
|
||||
// Main checkbox toggles all sub-items.
|
||||
// Capture uiIdx (position in _extensionUIs after append).
|
||||
const int uiIdx = _extensionUIs.size();
|
||||
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||
[this, uiIdx](int state) {
|
||||
if (state == Qt::PartiallyChecked)
|
||||
return;
|
||||
const bool checked = (state == Qt::Checked);
|
||||
if (uiIdx < _extensionUIs.size()) {
|
||||
for (auto &[id, cb] :
|
||||
_extensionUIs[uiIdx].itemChecks) {
|
||||
QSignalBlocker b(cb);
|
||||
cb->setChecked(checked);
|
||||
}
|
||||
}
|
||||
UpdateExportString();
|
||||
});
|
||||
|
||||
innerSection->SetContent(subWidget, true);
|
||||
outerLayout->addWidget(innerSection);
|
||||
_extensionUIs.append(std::move(ui));
|
||||
_extensionOrder.push_back(extIdx);
|
||||
}
|
||||
|
||||
outerLayout->addStretch();
|
||||
|
||||
auto scrollArea = new QScrollArea(wrapper);
|
||||
scrollArea->setWidget(outerContent);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
wrapperLayout->addWidget(scrollArea);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacroExportImportDialog::MacroExportImportDialog(Type type,
|
||||
const QString &baseJson)
|
||||
: QDialog(nullptr),
|
||||
_baseJson(baseJson),
|
||||
_importExportString(new QPlainTextEdit(this)),
|
||||
_usePlainText(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.export.usePlainText")))
|
||||
{
|
||||
_importExportString->setReadOnly(type == Type::EXPORT_MACRO);
|
||||
|
||||
auto label = new QLabel(obs_module_text(
|
||||
type == Type::EXPORT_MACRO
|
||||
? "AdvSceneSwitcher.macroTab.export.info"
|
||||
@@ -39,40 +200,90 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
|
||||
connect(_usePlainText, &QCheckBox::stateChanged, this,
|
||||
&MacroExportImportDialog::UsePlainTextChanged);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(_importExportString);
|
||||
layout->addWidget(_usePlainText);
|
||||
auto layout = new QVBoxLayout(this);
|
||||
|
||||
if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) {
|
||||
auto splitter = new QSplitter(Qt::Vertical, this);
|
||||
splitter->setChildrenCollapsible(false);
|
||||
splitter->addWidget(BuildExtensionWidget());
|
||||
|
||||
auto textWidget = new QWidget(splitter);
|
||||
auto textLayout = new QVBoxLayout(textWidget);
|
||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||
textLayout->addWidget(label);
|
||||
textLayout->addWidget(_importExportString);
|
||||
textLayout->addWidget(_usePlainText);
|
||||
splitter->addWidget(textWidget);
|
||||
|
||||
layout->addWidget(splitter);
|
||||
} else {
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(_importExportString);
|
||||
layout->addWidget(_usePlainText);
|
||||
}
|
||||
|
||||
layout->addWidget(buttons);
|
||||
setLayout(layout);
|
||||
|
||||
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
||||
|
||||
if (type == Type::EXPORT_MACRO) {
|
||||
RefreshExportText();
|
||||
}
|
||||
}
|
||||
|
||||
QString compressMacroString(const QString &input)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export string helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QString MacroExportImportDialog::BuildExportJson() const
|
||||
{
|
||||
QByteArray inputData = input.toUtf8();
|
||||
auto compressedData = qCompress(inputData);
|
||||
QByteArray encodedData = compressedData.toBase64();
|
||||
return QString::fromUtf8(encodedData);
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(_baseJson.toStdString().c_str());
|
||||
if (!data) {
|
||||
return _baseJson;
|
||||
}
|
||||
|
||||
const auto &extensions = GetMacroExportExtensions();
|
||||
for (int i = 0; i < (int)_extensionUIs.size(); ++i) {
|
||||
const auto &ui = _extensionUIs[i];
|
||||
|
||||
QStringList selectedIds;
|
||||
if (ui.itemChecks.isEmpty()) {
|
||||
if (!ui.mainCheck->isChecked()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
for (const auto &[id, cb] : ui.itemChecks) {
|
||||
if (cb->isChecked()) {
|
||||
selectedIds << id;
|
||||
}
|
||||
}
|
||||
if (selectedIds.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
extensions[_extensionOrder[i]].save(data, selectedIds);
|
||||
}
|
||||
|
||||
const char *json = obs_data_get_json(data);
|
||||
return json ? QString::fromUtf8(json) : QString{};
|
||||
}
|
||||
|
||||
QString decompressMacroString(const QString &input)
|
||||
void MacroExportImportDialog::RefreshExportText()
|
||||
{
|
||||
QByteArray encodedData = input.toUtf8();
|
||||
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
||||
auto outputData = qUncompress(compressedData);
|
||||
return QString::fromUtf8(outputData);
|
||||
const QString json = BuildExportJson();
|
||||
if (usePlainText) {
|
||||
_importExportString->setPlainText(json);
|
||||
} else {
|
||||
_importExportString->setPlainText(compressMacroString(json));
|
||||
}
|
||||
}
|
||||
|
||||
void MacroExportImportDialog::ExportMacros(const QString &json)
|
||||
void MacroExportImportDialog::UpdateExportString()
|
||||
{
|
||||
MacroExportImportDialog dialog(
|
||||
MacroExportImportDialog::Type::EXPORT_MACRO);
|
||||
dialog._importExportString->setPlainText(compressMacroString(json));
|
||||
dialog.adjustSize();
|
||||
dialog.updateGeometry();
|
||||
dialog.exec();
|
||||
RefreshExportText();
|
||||
}
|
||||
|
||||
void MacroExportImportDialog::UsePlainTextChanged(int value)
|
||||
@@ -90,26 +301,45 @@ void MacroExportImportDialog::UsePlainTextChanged(int value)
|
||||
usePlainText = value;
|
||||
}
|
||||
|
||||
static bool isValidData(const QString &json)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void MacroExportImportDialog::ExportMacros(obs_data_t *macroData)
|
||||
{
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
return !!data;
|
||||
const char *rawJson = obs_data_get_json(macroData);
|
||||
const QString baseJson = rawJson ? QString::fromUtf8(rawJson)
|
||||
: QString{};
|
||||
|
||||
MacroExportImportDialog dialog(Type::EXPORT_MACRO, baseJson);
|
||||
dialog.adjustSize();
|
||||
dialog.updateGeometry();
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
bool MacroExportImportDialog::ImportMacros(QString &json)
|
||||
{
|
||||
MacroExportImportDialog dialog(
|
||||
MacroExportImportDialog::Type::IMPORT_MACRO);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
json = decompressMacroString(
|
||||
dialog._importExportString->toPlainText());
|
||||
if (!isValidData(json)) { // Fallback to support raw json format
|
||||
json = dialog._importExportString->toPlainText();
|
||||
}
|
||||
return true;
|
||||
MacroExportImportDialog dialog(Type::IMPORT_MACRO);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
json = decompressMacroString(dialog._importExportString->toPlainText());
|
||||
if (!isValidData(json)) {
|
||||
// Fallback: support raw (uncompressed) JSON pasted directly.
|
||||
json = dialog._importExportString->toPlainText();
|
||||
}
|
||||
|
||||
// Invoke all extension load callbacks.
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_create_from_json(json.toStdString().c_str());
|
||||
if (data) {
|
||||
for (const auto &ext : GetMacroExportExtensions()) {
|
||||
ext.load(data, {});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroExportImportDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Type { EXPORT_MACRO, IMPORT_MACRO };
|
||||
MacroExportImportDialog(Type type);
|
||||
|
||||
static void ExportMacros(const QString &json);
|
||||
static void ExportMacros(obs_data_t *macroData);
|
||||
static bool ImportMacros(QString &json);
|
||||
|
||||
private slots:
|
||||
void UsePlainTextChanged(int);
|
||||
void UpdateExportString();
|
||||
|
||||
private:
|
||||
explicit MacroExportImportDialog(Type type,
|
||||
const QString &baseJson = {});
|
||||
|
||||
// Per-extension widgets
|
||||
struct ExtensionUI {
|
||||
QCheckBox *mainCheck = nullptr;
|
||||
// (itemId, checkbox) pairs - empty when no per-item selection.
|
||||
QList<QPair<QString, QCheckBox *>> itemChecks;
|
||||
};
|
||||
|
||||
QWidget *BuildExtensionWidget();
|
||||
QString BuildExportJson() const;
|
||||
void RefreshExportText();
|
||||
|
||||
QString _baseJson;
|
||||
QPlainTextEdit *_importExportString;
|
||||
QCheckBox *_usePlainText;
|
||||
QList<ExtensionUI> _extensionUIs;
|
||||
std::vector<int> _extensionOrder;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -214,6 +214,20 @@ bool MacroIsPaused(const Macro *macro)
|
||||
return macro ? macro->Paused() : true;
|
||||
}
|
||||
|
||||
void SetMacroPaused(Macro *macro, bool paused)
|
||||
{
|
||||
if (macro) {
|
||||
macro->SetPaused(paused);
|
||||
}
|
||||
}
|
||||
|
||||
void StopMacro(Macro *macro)
|
||||
{
|
||||
if (macro) {
|
||||
macro->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroWasPausedSince(
|
||||
const Macro *macro,
|
||||
const std::chrono::high_resolution_clock::time_point &time)
|
||||
@@ -237,14 +251,20 @@ void AddMacroHelperThread(Macro *macro, std::thread &&newThread)
|
||||
macro->AddHelperThread(std::move(newThread));
|
||||
}
|
||||
|
||||
bool RunMacroActions(Macro *macro)
|
||||
bool RunMacroActions(Macro *macro, bool forceParallel, bool ignorePause)
|
||||
{
|
||||
return macro && macro->PerformActions(true);
|
||||
return macro && macro->PerformActions(true, forceParallel, ignorePause);
|
||||
}
|
||||
|
||||
bool RunMacroElseActions(Macro *macro)
|
||||
bool RunMacroElseActions(Macro *macro, bool forceParallel, bool ignorePause)
|
||||
{
|
||||
return macro && macro->PerformActions(false);
|
||||
return macro &&
|
||||
macro->PerformActions(false, forceParallel, ignorePause);
|
||||
}
|
||||
|
||||
bool CheckMacroConditions(Macro *macro, bool ignorePause)
|
||||
{
|
||||
return macro && macro->CheckConditions(ignorePause);
|
||||
}
|
||||
|
||||
void ResetMacroConditionTimers(Macro *macro)
|
||||
|
||||
@@ -58,6 +58,8 @@ LastMacroConditionCheckTime(const Macro *);
|
||||
|
||||
EXPORT bool MacroIsStopped(const Macro *);
|
||||
EXPORT bool MacroIsPaused(const Macro *);
|
||||
EXPORT void SetMacroPaused(Macro *, bool paused);
|
||||
EXPORT void StopMacro(Macro *);
|
||||
EXPORT bool
|
||||
MacroWasPausedSince(const Macro *,
|
||||
const std::chrono::high_resolution_clock::time_point &);
|
||||
@@ -66,11 +68,16 @@ EXPORT bool MacroWasCheckedSinceLastStart(const Macro *);
|
||||
EXPORT void AddMacroHelperThread(Macro *, std::thread &&);
|
||||
|
||||
EXPORT bool CheckMacros();
|
||||
EXPORT bool CheckMacroConditions(Macro *, bool ignorePause = false);
|
||||
|
||||
EXPORT bool RunMacroActions(Macro *);
|
||||
bool RunMacroElseActions(Macro *);
|
||||
EXPORT bool RunMacroActions(Macro *, bool forceParallel = false,
|
||||
bool ignorePause = false);
|
||||
EXPORT bool RunMacroElseActions(Macro *, bool forceParallel = false,
|
||||
bool ignorePause = false);
|
||||
EXPORT bool RunMacros();
|
||||
void StopAllMacros();
|
||||
|
||||
void SignalStopAllMacros();
|
||||
void WaitForAllMacros();
|
||||
|
||||
EXPORT void LoadMacros(obs_data_t *obj);
|
||||
EXPORT void SaveMacros(obs_data_t *obj);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
MacroRef::MacroRef(std::string name)
|
||||
@@ -19,6 +21,7 @@ void MacroRef::Load(obs_data_t *obj)
|
||||
auto name = obs_data_get_string(obj, "macro");
|
||||
_postLoadName = name;
|
||||
_macro = GetWeakMacroByName(name);
|
||||
AddPostLoadStep([this]() { MacroRef::PostLoad(); });
|
||||
}
|
||||
|
||||
void MacroRef::PostLoad()
|
||||
@@ -71,11 +74,11 @@ void LoadMacroList(obs_data_t *obj, std::vector<MacroRef> ¯os,
|
||||
{
|
||||
obs_data_array_t *array = obs_data_get_array(obj, name.c_str());
|
||||
size_t count = obs_data_array_count(array);
|
||||
macros.reserve(macros.size() + count);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
obs_data_t *array_obj = obs_data_array_item(array, i);
|
||||
MacroRef ref;
|
||||
ref.Load(array_obj);
|
||||
macros.push_back(ref);
|
||||
macros.emplace_back();
|
||||
macros.back().Load(array_obj);
|
||||
obs_data_release(array_obj);
|
||||
}
|
||||
obs_data_array_release(array);
|
||||
|
||||
@@ -72,15 +72,24 @@ void MacroEdit::PasteMacroSegment()
|
||||
}
|
||||
AddMacroCondition(macro.get(), macro->Conditions().size(),
|
||||
copyInfo.segment->GetId(), data.Get(), logic);
|
||||
if (!macro->Conditions().empty()) {
|
||||
macro->Conditions().back()->FixupVarMappingRefs();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MacroSegmentCopyInfo::Type::ACTION:
|
||||
AddMacroAction(macro.get(), macro->Actions().size(),
|
||||
copyInfo.segment->GetId(), data.Get());
|
||||
if (!macro->Actions().empty()) {
|
||||
macro->Actions().back()->FixupVarMappingRefs();
|
||||
}
|
||||
break;
|
||||
case MacroSegmentCopyInfo::Type::ELSE:
|
||||
AddMacroElseAction(macro.get(), macro->ElseActions().size(),
|
||||
copyInfo.segment->GetId(), data.Get());
|
||||
if (!macro->ElseActions().empty()) {
|
||||
macro->ElseActions().back()->FixupVarMappingRefs();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -54,6 +54,23 @@ const std::vector<VarMapping> &MacroSegment::GetVarMappings() const
|
||||
void MacroSegment::SetVarMappings(const std::vector<VarMapping> &mappings)
|
||||
{
|
||||
_varMappings = mappings;
|
||||
IncrementTempVarInUseGeneration();
|
||||
}
|
||||
|
||||
void MacroSegment::FixupVarMappingRefs()
|
||||
{
|
||||
for (auto &mapping : _varMappings) {
|
||||
if (!mapping.tempVar.HasValidID()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &var : _tempVariables) {
|
||||
if (var.ID() == mapping.tempVar.GetID()) {
|
||||
mapping.tempVar = var.GetRef();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
NotifyUIAboutTempVarChange(this);
|
||||
}
|
||||
|
||||
std::vector<TempVariable> MacroSegment::GetOwnTempVars() const
|
||||
@@ -80,7 +97,7 @@ bool MacroSegment::Save(obs_data_t *obj) const
|
||||
obs_data_set_bool(data, "useCustomLabel", _useCustomLabel);
|
||||
obs_data_set_string(data, "customLabel", _customLabel.c_str());
|
||||
obs_data_set_bool(data, "enabled", _enabled);
|
||||
obs_data_set_int(data, "version", 1);
|
||||
obs_data_set_int(data, "version", 2);
|
||||
|
||||
OBSDataArrayAutoRelease mappingsArray = obs_data_array_create();
|
||||
for (const auto &mapping : _varMappings) {
|
||||
@@ -112,6 +129,12 @@ bool MacroSegment::Load(obs_data_t *obj)
|
||||
_enabled = obs_data_get_bool(obj, "enabled");
|
||||
}
|
||||
|
||||
// Reset the previously unused "enabled" value for conditions to "true"
|
||||
if (obs_data_get_int(data, "version") < 2 &&
|
||||
obs_data_has_user_value(obj, "logic")) {
|
||||
_enabled = true;
|
||||
}
|
||||
|
||||
_varMappings.clear();
|
||||
OBSDataArrayAutoRelease mappingsArray =
|
||||
obs_data_get_array(data, "varMappings");
|
||||
|
||||
@@ -57,6 +57,7 @@ public:
|
||||
const std::vector<VarMapping> &GetVarMappings() const;
|
||||
void SetVarMappings(const std::vector<VarMapping> &mappings);
|
||||
std::vector<TempVariable> GetOwnTempVars() const;
|
||||
void FixupVarMappingRefs();
|
||||
|
||||
protected:
|
||||
friend bool SupportsVariableValue(MacroSegment *);
|
||||
@@ -133,6 +134,7 @@ public:
|
||||
void SetSelected(bool);
|
||||
virtual std::shared_ptr<MacroSegment> Data() const = 0;
|
||||
virtual void SetupWidgets(bool basicSetup = false) = 0;
|
||||
virtual void FocusTypeSelection() {}
|
||||
|
||||
void SetupVarMappings(MacroSegment *segment);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ signals:
|
||||
void Remove(const QString &);
|
||||
void SegmentOrderChanged();
|
||||
void HighlightChanged(bool value);
|
||||
void ConditionTypeCreated(const QString &typeId);
|
||||
|
||||
private:
|
||||
MacroSignalManager(QObject *parent = nullptr);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "action-queue.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-export-import-dialog.hpp"
|
||||
@@ -316,13 +315,9 @@ void AdvSceneSwitcher::ExportMacros() const
|
||||
obs_data_array_push_back(macroArray, obj);
|
||||
}
|
||||
obs_data_set_array(data, "macros", macroArray);
|
||||
SaveVariables(data);
|
||||
SaveActionQueues(data);
|
||||
obs_data_set_string(data, "version", g_GIT_TAG);
|
||||
auto json = obs_data_get_json(data);
|
||||
QString exportString(json);
|
||||
|
||||
MacroExportImportDialog::ExportMacros(exportString);
|
||||
MacroExportImportDialog::ExportMacros(data);
|
||||
}
|
||||
|
||||
bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
|
||||
@@ -384,8 +379,6 @@ void AdvSceneSwitcher::ImportMacros()
|
||||
ImportMacros();
|
||||
return;
|
||||
}
|
||||
ImportVariables(data);
|
||||
ImportQueues(data);
|
||||
|
||||
auto version = obs_data_get_string(data, "version");
|
||||
if (strcmp(version, g_GIT_TAG) != 0) {
|
||||
@@ -402,7 +395,7 @@ void AdvSceneSwitcher::ImportMacros()
|
||||
std::vector<std::shared_ptr<Macro>> importedMacros;
|
||||
auto &tempMacros = GetTemporaryMacros();
|
||||
|
||||
auto lock = LockContext();
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
tempMacros.clear();
|
||||
|
||||
@@ -412,10 +405,14 @@ void AdvSceneSwitcher::ImportMacros()
|
||||
macro->Load(array_obj);
|
||||
RunAndClearPostLoadSteps();
|
||||
|
||||
if (macroNameExists(macro->Name()) &&
|
||||
!ResolveMacroImportNameConflict(macro)) {
|
||||
groupSize--;
|
||||
continue;
|
||||
if (macroNameExists(macro->Name())) {
|
||||
lock.unlock();
|
||||
bool resolved = ResolveMacroImportNameConflict(macro);
|
||||
lock.lock();
|
||||
if (!resolved) {
|
||||
groupSize--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
importedMacros.emplace_back(macro);
|
||||
@@ -557,6 +554,10 @@ void AdvSceneSwitcher::HighlightOnChange() const
|
||||
return;
|
||||
}
|
||||
|
||||
if (macro->Paused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (macro->ActionTriggerModePreventedActionsSince(
|
||||
lastOnChangeHighlightCheckTime)) {
|
||||
HighlightWidget(ui->actionTriggerMode, Qt::yellow,
|
||||
|
||||
@@ -17,6 +17,17 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddPluginCleanupStep([]() {
|
||||
GetTopLevelMacros().clear();
|
||||
GetTemporaryMacros().clear();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool setupDone = setup();
|
||||
|
||||
Macro::Macro(const std::string &name) : _dockSettings(this)
|
||||
{
|
||||
SetName(name);
|
||||
@@ -118,12 +129,11 @@ static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
|
||||
const auto timeSpent = endTime - startTime;
|
||||
|
||||
if (timeSpent >= perfLogThreshold) {
|
||||
const long int ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
timeSpent)
|
||||
.count();
|
||||
blog(LOG_WARNING,
|
||||
"spent %ld ms in %s condition check of macro '%s'!", ms,
|
||||
"spent %lld ms in %s condition check of macro '%s'!",
|
||||
(long long)std::chrono::duration_cast<
|
||||
std::chrono::milliseconds>(timeSpent)
|
||||
.count(),
|
||||
condition->GetId().c_str(),
|
||||
condition->GetMacro()->Name().c_str());
|
||||
}
|
||||
@@ -146,6 +156,15 @@ bool Macro::CheckConditionHelper(
|
||||
return conditionMatched;
|
||||
};
|
||||
|
||||
if (!condition->Enabled()) {
|
||||
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
|
||||
condition->GetId().c_str(), _name.c_str());
|
||||
if (!_useShortCircuitEvaluation) {
|
||||
(void)evaluateCondition();
|
||||
}
|
||||
return _matched;
|
||||
}
|
||||
|
||||
const auto logicType = condition->GetLogicType();
|
||||
if (logicType == Logic::Type::NONE) {
|
||||
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
|
||||
@@ -270,7 +289,7 @@ bool Macro::CheckConditions(bool ignorePause)
|
||||
|
||||
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
|
||||
: (_elseActions.size() > 0);
|
||||
if (!_actionModeMatch && hasActionsToExecute) {
|
||||
if (!_actionModeMatch && hasActionsToExecute && !_paused) {
|
||||
_lastActionRunModePreventTime =
|
||||
std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
@@ -369,7 +388,7 @@ bool Macro::ShouldRunActions() const
|
||||
!_paused && (_matched || _elseActions.size() > 0) &&
|
||||
_actionModeMatch;
|
||||
|
||||
if (VerboseLoggingEnabled() && !_actionModeMatch) {
|
||||
if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
|
||||
if (_matched && _actions.size() > 0) {
|
||||
blog(LOG_INFO, "skip actions for Macro %s (on change)",
|
||||
_name.c_str());
|
||||
@@ -1297,7 +1316,14 @@ bool RunMacros()
|
||||
return true;
|
||||
}
|
||||
|
||||
void StopAllMacros()
|
||||
void SignalStopAllMacros()
|
||||
{
|
||||
for (const auto &m : GetAllMacros()) {
|
||||
m->SignalStop();
|
||||
}
|
||||
}
|
||||
|
||||
void WaitForAllMacros()
|
||||
{
|
||||
for (const auto &m : GetAllMacros()) {
|
||||
m->Stop();
|
||||
|
||||
@@ -61,6 +61,7 @@ public:
|
||||
PauseStateSaveBehavior GetPauseStateSaveBehavior() const;
|
||||
|
||||
void Stop();
|
||||
void SignalStop() { _stop = true; }
|
||||
bool GetStop() const { return _stop; }
|
||||
void ResetTimers();
|
||||
|
||||
@@ -228,7 +229,7 @@ void LoadMacros(obs_data_t *obj);
|
||||
void SaveMacros(obs_data_t *obj);
|
||||
bool CheckMacros();
|
||||
bool RunMacros();
|
||||
void StopAllMacros();
|
||||
void WaitForAllMacros();
|
||||
void InvalidateMacroTempVarValues();
|
||||
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#import <Carbon/Carbon.h>
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <util/platform.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
@@ -14,41 +15,13 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
std::vector<std::string> GetWindowList()
|
||||
static std::string nsStringToStdString(NSString *str)
|
||||
{
|
||||
std::vector<std::string> windows;
|
||||
@autoreleasepool {
|
||||
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
|
||||
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
|
||||
for (NSDictionary *app in apps) {
|
||||
std::string name([[app objectForKey:@"kCGWindowName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
std::string owner(
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
|
||||
if (!name.empty() &&
|
||||
find(windows.begin(), windows.end(), name) ==
|
||||
windows.end()) {
|
||||
windows.emplace_back(name);
|
||||
}
|
||||
if (!owner.empty() &&
|
||||
find(windows.begin(), windows.end(), owner) ==
|
||||
windows.end()) {
|
||||
windows.emplace_back(owner);
|
||||
}
|
||||
}
|
||||
apps = nil;
|
||||
CFRelease(cfApps);
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
return windows;
|
||||
const char *utf8 = [str UTF8String];
|
||||
return utf8 ? utf8 : "";
|
||||
}
|
||||
|
||||
std::string GetCurrentWindowTitle()
|
||||
@@ -100,100 +73,49 @@ std::string GetCurrentWindowTitle()
|
||||
return title;
|
||||
}
|
||||
|
||||
bool isWindowOriginOnScreen(NSDictionary *app, NSScreen *screen,
|
||||
bool fullscreen = false)
|
||||
{
|
||||
NSArray *screens = [NSScreen screens];
|
||||
NSRect mainScreenFrame = [screens[0] frame];
|
||||
NSRect screenFrame;
|
||||
if (fullscreen) {
|
||||
screenFrame = [screen frame];
|
||||
} else {
|
||||
screenFrame = [screen visibleFrame];
|
||||
}
|
||||
NSRect windowBounds;
|
||||
CGRectMakeWithDictionaryRepresentation(
|
||||
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
|
||||
&windowBounds);
|
||||
|
||||
return (windowBounds.origin.x == screenFrame.origin.x &&
|
||||
(mainScreenFrame.size.height - screenFrame.size.height -
|
||||
windowBounds.origin.y ==
|
||||
screenFrame.origin.y));
|
||||
}
|
||||
|
||||
bool isWindowMaximizedOnScreen(NSDictionary *app, NSScreen *screen)
|
||||
{
|
||||
double maximizedTolerance = 0.99;
|
||||
NSRect screenFrame = [screen frame];
|
||||
NSRect windowBounds;
|
||||
CGRectMakeWithDictionaryRepresentation(
|
||||
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
|
||||
&windowBounds);
|
||||
|
||||
int sumX = windowBounds.origin.x + windowBounds.size.width;
|
||||
int sumY = windowBounds.origin.y + windowBounds.size.height;
|
||||
// CGWindowList: flipped coords, origin at top-left of main display.
|
||||
// NSScreen: Cocoa coords, origin at bottom-left of main display.
|
||||
NSRect mainFrame = [[NSScreen screens][0] frame];
|
||||
NSRect visFrame = [screen visibleFrame];
|
||||
|
||||
// Return false if window spans over multiple screens
|
||||
if (sumX > screenFrame.size.width) {
|
||||
// Convert visible area to CGWindow coordinates.
|
||||
// screenTop is just below the menubar; screenBottom is just above the
|
||||
// dock.
|
||||
CGFloat screenLeft = visFrame.origin.x;
|
||||
CGFloat screenTop =
|
||||
mainFrame.size.height - visFrame.origin.y - visFrame.size.height;
|
||||
CGFloat screenRight = screenLeft + visFrame.size.width;
|
||||
CGFloat screenBottom = screenTop + visFrame.size.height;
|
||||
|
||||
const double tolerance = 4.0;
|
||||
|
||||
// Width must span the full visible width.
|
||||
if (fabs(windowBounds.origin.x - screenLeft) > tolerance) {
|
||||
return false;
|
||||
}
|
||||
if (sumY > screenFrame.size.height) {
|
||||
if (fabs((windowBounds.origin.x + windowBounds.size.width) -
|
||||
screenRight) > tolerance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((double)sumX / (double)screenFrame.size.width) >
|
||||
maximizedTolerance &&
|
||||
((double)sumY / (double)screenFrame.size.height) >
|
||||
maximizedTolerance;
|
||||
}
|
||||
|
||||
bool nameMachesPattern(std::string windowName, std::string pattern)
|
||||
{
|
||||
return QString::fromStdString(windowName)
|
||||
.contains(QRegularExpression(QString::fromStdString(pattern)));
|
||||
}
|
||||
|
||||
bool IsMaximized(const std::string &title)
|
||||
{
|
||||
@autoreleasepool {
|
||||
NSArray *screens = [NSScreen screens];
|
||||
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
|
||||
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
|
||||
for (NSDictionary *app in apps) {
|
||||
std::string name([[app objectForKey:@"kCGWindowName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
std::string owner(
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
|
||||
bool equals = (title == name || title == owner);
|
||||
bool matches = nameMachesPattern(name, title) ||
|
||||
nameMachesPattern(owner, title);
|
||||
if (equals || matches) {
|
||||
for (NSScreen *screen in screens) {
|
||||
if (isWindowOriginOnScreen(app,
|
||||
screen) &&
|
||||
isWindowMaximizedOnScreen(app,
|
||||
screen)) {
|
||||
apps = nil;
|
||||
CFRelease(cfApps);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
apps = nil;
|
||||
CFRelease(cfApps);
|
||||
// Bottom edge must align with the visible bottom (above dock).
|
||||
if (fabs((windowBounds.origin.y + windowBounds.size.height) -
|
||||
screenBottom) > tolerance) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
// Top edge must be at or above the menubar line (some apps extend
|
||||
// their window frame behind the menubar).
|
||||
if (windowBounds.origin.y > screenTop + tolerance) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
|
||||
@@ -207,52 +129,176 @@ bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
|
||||
return NSEqualSizes(windowBounds.size, screenFrame.size);
|
||||
}
|
||||
|
||||
bool IsFullscreen(const std::string &title)
|
||||
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
|
||||
{
|
||||
std::vector<WindowInfo> result;
|
||||
const std::string foregroundTitle = GetCurrentWindowTitle();
|
||||
|
||||
@autoreleasepool {
|
||||
NSArray *screens = [NSScreen screens];
|
||||
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
|
||||
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
|
||||
for (NSDictionary *app in apps) {
|
||||
std::string name([[app objectForKey:@"kCGWindowName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
std::string owner(
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
UTF8String],
|
||||
[[app objectForKey:@"kCGWindowOwnerName"]
|
||||
lengthOfBytesUsingEncoding:
|
||||
NSUTF8StringEncoding]);
|
||||
|
||||
bool equals = (title == name || title == owner);
|
||||
bool matches = nameMachesPattern(name, title) ||
|
||||
nameMachesPattern(owner, title);
|
||||
if (equals || matches) {
|
||||
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListExcludeDesktopElements, kCGNullWindowID);
|
||||
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
|
||||
|
||||
// Pre-compute which PIDs own at least one fullscreen-sized window.
|
||||
// A fullscreen app reports a small chrome/title bar window first and
|
||||
// its full-size content window second; checking only the first
|
||||
// window's bounds gives a false negative.
|
||||
std::set<int> fullscreenPIDs;
|
||||
if (options.fullscreen) {
|
||||
for (NSDictionary *app in apps) {
|
||||
int layer = [[app objectForKey:@"kCGWindowLayer"]
|
||||
intValue];
|
||||
if (layer != 0) {
|
||||
continue;
|
||||
}
|
||||
for (NSScreen *screen in screens) {
|
||||
if (isWindowOriginOnScreen(app, screen,
|
||||
true) &&
|
||||
isWindowFullscreenOnScreen(
|
||||
app, screen)) {
|
||||
apps = nil;
|
||||
CFRelease(cfApps);
|
||||
return true;
|
||||
if (isWindowFullscreenOnScreen(app,
|
||||
screen)) {
|
||||
int pid = [[app objectForKey:
|
||||
@"kCGWindowOwnerPID"]
|
||||
intValue];
|
||||
fullscreenPIDs.insert(pid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::set<int> maximizedPIDs;
|
||||
if (options.maximized) {
|
||||
for (NSDictionary *app in apps) {
|
||||
int layer = [[app objectForKey:@"kCGWindowLayer"]
|
||||
intValue];
|
||||
if (layer != 0) {
|
||||
continue;
|
||||
}
|
||||
for (NSScreen *screen in screens) {
|
||||
if (isWindowMaximizedOnScreen(app,
|
||||
screen)) {
|
||||
int pid = [[app objectForKey:
|
||||
@"kCGWindowOwnerPID"]
|
||||
intValue];
|
||||
maximizedPIDs.insert(pid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-compute the largest-area window bounds per PID.
|
||||
// CGWindowList often lists small auxiliary windows (toolbars,
|
||||
// chrome) before the main content window; using the largest area
|
||||
// avoids reporting those wrong bounds for geometry.
|
||||
std::map<int, NSRect> largestBoundsPerPID;
|
||||
if (options.geometry || options.maximized) {
|
||||
for (NSDictionary *app in apps) {
|
||||
int layer = [[app objectForKey:@"kCGWindowLayer"]
|
||||
intValue];
|
||||
if (layer != 0) {
|
||||
continue;
|
||||
}
|
||||
NSRect b = NSZeroRect;
|
||||
CGRectMakeWithDictionaryRepresentation(
|
||||
(CFDictionaryRef)[app
|
||||
objectForKey:@"kCGWindowBounds"],
|
||||
&b);
|
||||
int pid = [[app objectForKey:@"kCGWindowOwnerPID"]
|
||||
intValue];
|
||||
auto it = largestBoundsPerPID.find(pid);
|
||||
if (it == largestBoundsPerPID.end() ||
|
||||
b.size.width * b.size.height >
|
||||
it->second.size.width *
|
||||
it->second.size.height) {
|
||||
largestBoundsPerPID[pid] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track titles already added to avoid duplicates (name + owner)
|
||||
std::vector<std::string> seen;
|
||||
|
||||
for (NSDictionary *app in apps) {
|
||||
int layer =
|
||||
[[app objectForKey:@"kCGWindowLayer"] intValue];
|
||||
if (layer != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string name = nsStringToStdString(
|
||||
[app objectForKey:@"kCGWindowName"]);
|
||||
std::string owner = nsStringToStdString(
|
||||
[app objectForKey:@"kCGWindowOwnerName"]);
|
||||
|
||||
// Collect geometry once (used for multiple fields)
|
||||
NSRect bounds = NSZeroRect;
|
||||
if (options.geometry || options.fullscreen ||
|
||||
options.maximized) {
|
||||
CGRectMakeWithDictionaryRepresentation(
|
||||
(CFDictionaryRef)[app
|
||||
objectForKey:@"kCGWindowBounds"],
|
||||
&bounds);
|
||||
}
|
||||
|
||||
// Process both the window name and the owner name as
|
||||
// separate entries, matching the old GetWindowList() behaviour
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
const std::string &title = (pass == 0) ? name
|
||||
: owner;
|
||||
if (title.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (std::find(seen.begin(), seen.end(),
|
||||
title) != seen.end()) {
|
||||
continue;
|
||||
}
|
||||
seen.emplace_back(title);
|
||||
|
||||
WindowInfo info;
|
||||
info.title = title;
|
||||
|
||||
if (options.focus) {
|
||||
info.focused =
|
||||
(title == foregroundTitle);
|
||||
}
|
||||
|
||||
if (options.geometry || options.fullscreen ||
|
||||
options.maximized) {
|
||||
int pid = [[app objectForKey:
|
||||
@"kCGWindowOwnerPID"]
|
||||
intValue];
|
||||
auto it = largestBoundsPerPID.find(pid);
|
||||
const NSRect &geoBounds =
|
||||
(it != largestBoundsPerPID.end())
|
||||
? it->second
|
||||
: bounds;
|
||||
info.x = (int)geoBounds.origin.x;
|
||||
info.y = (int)geoBounds.origin.y;
|
||||
info.width = (int)geoBounds.size.width;
|
||||
info.height = (int)geoBounds.size.height;
|
||||
|
||||
if (options.fullscreen) {
|
||||
info.fullscreen =
|
||||
fullscreenPIDs.count(pid) >
|
||||
0;
|
||||
}
|
||||
|
||||
if (options.maximized) {
|
||||
info.maximized =
|
||||
maximizedPIDs.count(pid) >
|
||||
0;
|
||||
}
|
||||
}
|
||||
|
||||
// windowClass and text not implemented on macOS
|
||||
result.emplace_back(std::move(info));
|
||||
}
|
||||
}
|
||||
apps = nil;
|
||||
CFRelease(cfApps);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<std::string> GetTextInWindow(const std::string &)
|
||||
{
|
||||
// Not implemented
|
||||
return {};
|
||||
return result;
|
||||
}
|
||||
|
||||
int SecondsSinceLastInput()
|
||||
@@ -341,8 +387,7 @@ QStringList GetProcessPathsFromName(const QString &name)
|
||||
continue;
|
||||
}
|
||||
const char *nameStr = appName.UTF8String;
|
||||
if (!nameStr ||
|
||||
name != QString::fromUtf8(nameStr)) {
|
||||
if (!nameStr || name != QString::fromUtf8(nameStr)) {
|
||||
continue;
|
||||
}
|
||||
NSURL *url = app.executableURL;
|
||||
|
||||
@@ -11,11 +11,31 @@ namespace advss {
|
||||
|
||||
enum class HotkeyType;
|
||||
|
||||
EXPORT std::vector<std::string> GetWindowList();
|
||||
struct WindowQueryOptions {
|
||||
bool geometry = false;
|
||||
bool focus = false;
|
||||
bool fullscreen = false;
|
||||
bool maximized = false;
|
||||
bool windowClass = false; // Windows only, no-op on other platforms
|
||||
bool text = false; // Windows only, expensive, no-op on other platforms
|
||||
};
|
||||
|
||||
struct WindowInfo {
|
||||
std::string title;
|
||||
bool focused = false;
|
||||
bool fullscreen = false;
|
||||
bool maximized = false;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::string windowClass;
|
||||
std::optional<std::string> text;
|
||||
};
|
||||
|
||||
EXPORT std::vector<WindowInfo>
|
||||
GetWindows(const WindowQueryOptions &options = {});
|
||||
EXPORT std::string GetCurrentWindowTitle();
|
||||
EXPORT bool IsFullscreen(const std::string &title);
|
||||
EXPORT bool IsMaximized(const std::string &title);
|
||||
EXPORT std::optional<std::string> GetTextInWindow(const std::string &window);
|
||||
EXPORT int SecondsSinceLastInput();
|
||||
EXPORT QStringList GetProcessList();
|
||||
EXPORT std::string GetForegroundProcessName();
|
||||
|
||||
@@ -16,14 +16,17 @@ static void setupTab(QTabWidget *);
|
||||
|
||||
static ActionQueueTable *tabWidget = nullptr;
|
||||
|
||||
void RegisterActionQueueTab()
|
||||
static bool setup()
|
||||
{
|
||||
AddPluginInitStep([]() {
|
||||
AddPluginInitStep([] {
|
||||
AddSetupTabCallback("actionQueueTab", ActionQueueTable::Create,
|
||||
setupTab);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool setupDone = setup();
|
||||
|
||||
static void setTabVisible(QTabWidget *tabWidget, bool visible)
|
||||
{
|
||||
SetTabVisibleByName(
|
||||
|
||||
@@ -12,20 +12,16 @@ std::deque<std::shared_ptr<Item>> &GetActionQueues()
|
||||
return queues;
|
||||
}
|
||||
|
||||
void RegisterActionQueueTab();
|
||||
|
||||
void SetupActionQueues()
|
||||
static bool setup()
|
||||
{
|
||||
static bool done = false;
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
AddSaveStep(SaveActionQueues);
|
||||
AddLoadStep(LoadActionQueues);
|
||||
RegisterActionQueueTab();
|
||||
done = true;
|
||||
AddPluginCleanupStep([]() { queues.clear(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool setupDone = setup();
|
||||
|
||||
ActionQueue::ActionQueue() : Item()
|
||||
{
|
||||
_lastEmpty = std::chrono::high_resolution_clock::now();
|
||||
|
||||
@@ -101,7 +101,6 @@ signals:
|
||||
};
|
||||
|
||||
std::deque<std::shared_ptr<Item>> &GetActionQueues();
|
||||
void SetupActionQueues();
|
||||
void SaveActionQueues(obs_data_t *);
|
||||
void LoadActionQueues(obs_data_t *);
|
||||
void ImportQueues(obs_data_t *);
|
||||
|
||||
@@ -163,6 +163,8 @@ public:
|
||||
bool disableHints = false;
|
||||
bool disableFilterComboboxFilter = false;
|
||||
bool hideLegacyTabs = true;
|
||||
bool settingsLockEnabled = false;
|
||||
std::string settingsLockPassword;
|
||||
bool saveWindowGeo = false;
|
||||
QPoint windowPos = {};
|
||||
QSize windowSize = {};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
namespace advss {
|
||||
|
||||
const std::map<Logic::Type, const char *> Logic::localeMap = {
|
||||
{Logic::Type::NONE, {"AdvSceneSwitcher.logic.none"}},
|
||||
{Logic::Type::AND, {"AdvSceneSwitcher.logic.and"}},
|
||||
{Logic::Type::OR, {"AdvSceneSwitcher.logic.or"}},
|
||||
{Logic::Type::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
|
||||
@@ -67,7 +66,7 @@ void Logic::PopulateLogicTypeSelection(QComboBox *list, bool isRootCondition)
|
||||
return typeValue < rootOffset;
|
||||
}}
|
||||
: std::function<bool(int)>{[](int typeValue) {
|
||||
return typeValue >= rootOffset;
|
||||
return typeValue > rootOffset;
|
||||
}};
|
||||
for (const auto &[type, name] : localeMap) {
|
||||
const int typeValue = static_cast<int>(type);
|
||||
|
||||
@@ -28,6 +28,8 @@ static constexpr bool handleUncleanShutdown = true;
|
||||
static bool wasCleanShutdown = false;
|
||||
static bool suppressCrashDialog = false;
|
||||
|
||||
static char *sentinelFile = nullptr;
|
||||
|
||||
bool GetSuppressCrashDialog()
|
||||
{
|
||||
return suppressCrashDialog;
|
||||
@@ -58,7 +60,6 @@ static void handleShutdown(enum obs_frontend_event event, void *)
|
||||
return;
|
||||
}
|
||||
|
||||
char *sentinelFile = obs_module_config_path(sentinel.data());
|
||||
if (!sentinelFile) {
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +79,8 @@ static void handleShutdown(enum obs_frontend_event event, void *)
|
||||
|
||||
static void setup()
|
||||
{
|
||||
char *sentinelFile = obs_module_config_path(sentinel.data());
|
||||
// Freed in handleShutdown()
|
||||
sentinelFile = obs_module_config_path(sentinel.data());
|
||||
if (!sentinelFile) {
|
||||
return;
|
||||
}
|
||||
@@ -106,7 +108,6 @@ static void setup()
|
||||
|
||||
file.write("running");
|
||||
file.close();
|
||||
bfree(sentinelFile);
|
||||
|
||||
obs_frontend_add_event_callback(handleShutdown, nullptr);
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,7 @@ static void populateUnits(QComboBox *list)
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.minutes"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
|
||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.days"));
|
||||
}
|
||||
|
||||
DurationSelection::DurationSelection(QWidget *parent, bool showUnitSelection,
|
||||
@@ -66,6 +67,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
||||
return 60;
|
||||
case Duration::Unit::HOURS:
|
||||
return 3600;
|
||||
case Duration::Unit::DAYS:
|
||||
return 86400;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ static std::string durationUnitToString(Duration::Unit u)
|
||||
return obs_module_text("AdvSceneSwitcher.unit.minutes");
|
||||
case Duration::Unit::HOURS:
|
||||
return obs_module_text("AdvSceneSwitcher.unit.hours");
|
||||
case Duration::Unit::DAYS:
|
||||
return obs_module_text("AdvSceneSwitcher.unit.days");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -151,6 +153,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
||||
return 60;
|
||||
case Duration::Unit::HOURS:
|
||||
return 3600;
|
||||
case Duration::Unit::DAYS:
|
||||
return 86400;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
SECONDS,
|
||||
MINUTES,
|
||||
HOURS,
|
||||
DAYS,
|
||||
};
|
||||
EXPORT Unit GetUnit() const { return _unit; }
|
||||
|
||||
|
||||
@@ -398,10 +398,14 @@ void FirstRunWizard::markFirstRunComplete()
|
||||
}
|
||||
|
||||
// static
|
||||
std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent)
|
||||
std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent,
|
||||
bool *wasSkipped)
|
||||
{
|
||||
auto wizard = new FirstRunWizard(parent);
|
||||
wizard->exec();
|
||||
if (wasSkipped) {
|
||||
*wasSkipped = (wizard->result() == QDialog::Rejected);
|
||||
}
|
||||
wizard->deleteLater();
|
||||
return wizard->_macro;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ class FirstRunWizard : public QWizard {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FirstRunWizard(QWidget *parent = nullptr);
|
||||
static std::shared_ptr<Macro> ShowWizard(QWidget *parent);
|
||||
static std::shared_ptr<Macro> ShowWizard(QWidget *parent,
|
||||
bool *wasSkipped = nullptr);
|
||||
static bool
|
||||
CreateMacro(std::shared_ptr<Macro> ¯o, const std::string ¯oName,
|
||||
const std::string &conditionId, obs_data_t *conditionData,
|
||||
|
||||
@@ -75,6 +75,18 @@ static std::vector<std::function<void()>> &getStopSteps()
|
||||
return steps;
|
||||
}
|
||||
|
||||
static std::vector<std::function<void(obs_data_t *)>> &getEarlySaveSteps()
|
||||
{
|
||||
static std::vector<std::function<void(obs_data_t *)>> steps;
|
||||
return steps;
|
||||
}
|
||||
|
||||
static std::vector<std::function<void(obs_data_t *)>> &getEarlyLoadSteps()
|
||||
{
|
||||
static std::vector<std::function<void(obs_data_t *)>> steps;
|
||||
return steps;
|
||||
}
|
||||
|
||||
static std::vector<std::function<void(obs_data_t *)>> &getSaveSteps()
|
||||
{
|
||||
static std::vector<std::function<void(obs_data_t *)>> steps;
|
||||
@@ -109,6 +121,18 @@ void LoadPluginSettings(obs_data_t *obj)
|
||||
GetSwitcher()->LoadSettings(obj);
|
||||
}
|
||||
|
||||
void AddEarlySaveStep(std::function<void(obs_data_t *)> step)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
getEarlySaveSteps().emplace_back(step);
|
||||
}
|
||||
|
||||
void AddEarlyLoadStep(std::function<void(obs_data_t *)> step)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
getEarlyLoadSteps().emplace_back(step);
|
||||
}
|
||||
|
||||
void AddSaveStep(std::function<void(obs_data_t *)> step)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
@@ -136,6 +160,9 @@ void AddIntervalResetStep(std::function<void()> step)
|
||||
void RunSaveSteps(obs_data_t *obj)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
for (const auto &func : getEarlySaveSteps()) {
|
||||
func(obj);
|
||||
}
|
||||
for (const auto &func : getSaveSteps()) {
|
||||
func(obj);
|
||||
}
|
||||
@@ -144,6 +171,9 @@ void RunSaveSteps(obs_data_t *obj)
|
||||
void RunLoadSteps(obs_data_t *obj)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
for (const auto &func : getEarlyLoadSteps()) {
|
||||
func(obj);
|
||||
}
|
||||
for (const auto &func : getLoadSteps()) {
|
||||
func(obj);
|
||||
}
|
||||
@@ -310,7 +340,7 @@ bool HighlightUIElementsEnabled()
|
||||
|
||||
bool OBSIsShuttingDown()
|
||||
{
|
||||
return GetSwitcher() && GetSwitcher()->obsIsShuttingDown;
|
||||
return !GetSwitcher() || GetSwitcher()->obsIsShuttingDown;
|
||||
}
|
||||
|
||||
bool InitialLoadIsComplete()
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace advss {
|
||||
void SavePluginSettings(obs_data_t *);
|
||||
EXPORT void LoadPluginSettings(obs_data_t *);
|
||||
|
||||
void AddEarlySaveStep(std::function<void(obs_data_t *)>);
|
||||
void AddEarlyLoadStep(std::function<void(obs_data_t *)>);
|
||||
EXPORT void AddSaveStep(std::function<void(obs_data_t *)>);
|
||||
EXPORT void AddLoadStep(std::function<void(obs_data_t *)>);
|
||||
EXPORT void AddPostLoadStep(std::function<void()>);
|
||||
|
||||
@@ -173,10 +173,10 @@ void PopulateTransitionSelection(QComboBox *sel, bool addCurrent, bool addAny,
|
||||
void PopulateWindowSelection(QComboBox *sel, bool addSelect)
|
||||
{
|
||||
|
||||
const auto windows = GetWindowList();
|
||||
const auto windows = GetWindows();
|
||||
|
||||
for (const std::string &window : windows) {
|
||||
sel->addItem(window.c_str());
|
||||
for (const auto &info : windows) {
|
||||
sel->addItem(info.title.c_str());
|
||||
}
|
||||
|
||||
sel->model()->sort(0);
|
||||
|
||||
@@ -162,4 +162,48 @@ bool IsMediaSource(obs_source_t *source)
|
||||
return (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) != 0;
|
||||
}
|
||||
|
||||
SourceActiveKeeper::~SourceActiveKeeper()
|
||||
{
|
||||
ReleaseRef();
|
||||
}
|
||||
|
||||
void SourceActiveKeeper::SetActive(bool active)
|
||||
{
|
||||
if (_active == active) {
|
||||
return;
|
||||
}
|
||||
if (active) {
|
||||
_active = true;
|
||||
AcquireRef();
|
||||
} else {
|
||||
ReleaseRef();
|
||||
_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SourceActiveKeeper::SetSource(obs_source_t *source)
|
||||
{
|
||||
if (_source == source) {
|
||||
return;
|
||||
}
|
||||
ReleaseRef();
|
||||
_source = source;
|
||||
AcquireRef();
|
||||
}
|
||||
|
||||
void SourceActiveKeeper::AcquireRef()
|
||||
{
|
||||
if (_active && _source) {
|
||||
obs_source_inc_active(_source);
|
||||
}
|
||||
}
|
||||
|
||||
void SourceActiveKeeper::ReleaseRef()
|
||||
{
|
||||
if (_active && _source) {
|
||||
obs_source_dec_active(_source);
|
||||
_source = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -20,4 +20,25 @@ EXPORT OBSWeakSource GetWeakFilterByQString(OBSWeakSource source,
|
||||
EXPORT int GetSceneItemCount(const OBSWeakSource &);
|
||||
EXPORT bool IsMediaSource(obs_source_t *source);
|
||||
|
||||
// RAII helper that keeps an OBS source active (via obs_source_inc_active /
|
||||
// obs_source_dec_active) for as long as the keeper is enabled.
|
||||
class EXPORT SourceActiveKeeper {
|
||||
public:
|
||||
SourceActiveKeeper() = default;
|
||||
~SourceActiveKeeper();
|
||||
|
||||
SourceActiveKeeper(const SourceActiveKeeper &) = delete;
|
||||
SourceActiveKeeper &operator=(const SourceActiveKeeper &) = delete;
|
||||
|
||||
void SetActive(bool active);
|
||||
void SetSource(obs_source_t *source);
|
||||
|
||||
private:
|
||||
void AcquireRef();
|
||||
void ReleaseRef();
|
||||
|
||||
OBSSource _source;
|
||||
bool _active = false;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -74,6 +74,7 @@ public:
|
||||
EXPORT std::optional<const TempVariable> GetTempVariable(Macro *) const;
|
||||
EXPORT bool operator==(const TempVariableRef &other) const;
|
||||
bool HasValidID() const { return !_id.empty(); }
|
||||
std::string GetID() const { return _id; }
|
||||
|
||||
private:
|
||||
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
|
||||
|
||||
@@ -29,6 +29,7 @@ static void save(obs_data_t *data)
|
||||
{
|
||||
tabSettings.Save(data, "tabSettings");
|
||||
dockSettings.Save(data, "dockSettings");
|
||||
|
||||
obs_data_set_bool(data, "addVariablesDock", addDock);
|
||||
}
|
||||
|
||||
@@ -63,7 +64,6 @@ static void load(obs_data_t *data)
|
||||
{
|
||||
tabSettings.Load(data, "tabSettings");
|
||||
dockSettings.Load(data, "dockSettings");
|
||||
|
||||
enableDock(obs_data_get_bool(data, "addVariablesDock"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "variable.hpp"
|
||||
#include "math-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <QGridLayout>
|
||||
|
||||
namespace advss {
|
||||
@@ -15,6 +18,15 @@ static std::deque<std::shared_ptr<Item>> variables;
|
||||
static std::mutex lastVariableChangeMutex;
|
||||
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddEarlySaveStep(SaveVariables);
|
||||
AddEarlyLoadStep(LoadVariables);
|
||||
AddPluginCleanupStep([]() { variables.clear(); });
|
||||
return true;
|
||||
}
|
||||
static bool setupDone = setup();
|
||||
|
||||
static void setLastVariableChangeTime()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
|
||||
@@ -418,34 +430,31 @@ static bool variableWithNameExists(const std::string &name)
|
||||
|
||||
void SaveVariables(obs_data_t *obj)
|
||||
{
|
||||
obs_data_array_t *variablesArray = obs_data_array_create();
|
||||
OBSDataArrayAutoRelease variablesArray = obs_data_array_create();
|
||||
for (const auto &v : variables) {
|
||||
obs_data_t *array_obj = obs_data_create();
|
||||
OBSDataAutoRelease array_obj = obs_data_create();
|
||||
v->Save(array_obj);
|
||||
obs_data_array_push_back(variablesArray, array_obj);
|
||||
obs_data_release(array_obj);
|
||||
}
|
||||
|
||||
obs_data_set_array(obj, "variables", variablesArray);
|
||||
obs_data_array_release(variablesArray);
|
||||
}
|
||||
|
||||
void LoadVariables(obs_data_t *obj)
|
||||
{
|
||||
variables.clear();
|
||||
|
||||
obs_data_array_t *variablesArray = obs_data_get_array(obj, "variables");
|
||||
OBSDataArrayAutoRelease variablesArray =
|
||||
obs_data_get_array(obj, "variables");
|
||||
size_t count = obs_data_array_count(variablesArray);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
obs_data_t *array_obj = obs_data_array_item(variablesArray, i);
|
||||
OBSDataAutoRelease array_obj =
|
||||
obs_data_array_item(variablesArray, i);
|
||||
auto var = Variable::Create();
|
||||
variables.emplace_back(var);
|
||||
variables.back()->Load(array_obj);
|
||||
obs_data_release(array_obj);
|
||||
}
|
||||
|
||||
obs_data_array_release(variablesArray);
|
||||
}
|
||||
|
||||
static void signalImportedVariables(void *varsPtr)
|
||||
@@ -460,16 +469,15 @@ static void signalImportedVariables(void *varsPtr)
|
||||
|
||||
void ImportVariables(obs_data_t *data)
|
||||
{
|
||||
obs_data_array_t *array = obs_data_get_array(data, "variables");
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(data, "variables");
|
||||
size_t count = obs_data_array_count(array);
|
||||
|
||||
auto importedVars = new std::vector<std::shared_ptr<Item>>;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
obs_data_t *arrayElement = obs_data_array_item(array, i);
|
||||
OBSDataAutoRelease arrayElement = obs_data_array_item(array, i);
|
||||
auto var = Variable::Create();
|
||||
var->Load(arrayElement);
|
||||
obs_data_release(arrayElement);
|
||||
|
||||
if (variableWithNameExists(var->Name())) {
|
||||
continue;
|
||||
@@ -479,8 +487,6 @@ void ImportVariables(obs_data_t *data)
|
||||
importedVars->emplace_back(var);
|
||||
}
|
||||
|
||||
obs_data_array_release(array);
|
||||
|
||||
QueueUITask(signalImportedVariables, importedVars);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "platform-funcs.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <UIAutomation.h>
|
||||
#include <util/platform.h>
|
||||
#include <TlHelp32.h>
|
||||
#include <Psapi.h>
|
||||
#include <memory>
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
#include <string>
|
||||
@@ -16,6 +18,9 @@
|
||||
#include <QWidget>
|
||||
#include <mutex>
|
||||
|
||||
#define ADVSS_WIDEN_(x) L##x
|
||||
#define ADVSS_WIDEN(x) ADVSS_WIDEN_(x)
|
||||
|
||||
namespace advss {
|
||||
|
||||
#define MAX_SEARCH 1000
|
||||
@@ -133,7 +138,7 @@ const std::vector<std::string> getOBSWindows()
|
||||
return lastDoneHelper->windows;
|
||||
}
|
||||
|
||||
std::vector<std::string> GetWindowList()
|
||||
static std::vector<std::string> getAllWindowTitles()
|
||||
{
|
||||
std::vector<std::string> windows;
|
||||
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
|
||||
@@ -150,6 +155,129 @@ std::vector<std::string> GetWindowList()
|
||||
return windows;
|
||||
}
|
||||
|
||||
static bool isWindowMaximized(HWND hwnd)
|
||||
{
|
||||
if (!hwnd || hwnd == GetDesktopWindow() || hwnd == GetShellWindow()) {
|
||||
return false;
|
||||
}
|
||||
if (IsZoomed(hwnd)) {
|
||||
return true;
|
||||
}
|
||||
RECT appBounds;
|
||||
MONITORINFO monitorInfo = {0};
|
||||
monitorInfo.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
|
||||
&monitorInfo);
|
||||
GetWindowRect(hwnd, &appBounds);
|
||||
return monitorInfo.rcMonitor.bottom == appBounds.bottom &&
|
||||
monitorInfo.rcMonitor.top == appBounds.top &&
|
||||
monitorInfo.rcMonitor.left == appBounds.left &&
|
||||
monitorInfo.rcMonitor.right == appBounds.right;
|
||||
}
|
||||
|
||||
static bool isWindowFullscreen(HWND hwnd)
|
||||
{
|
||||
if (!hwnd || hwnd == GetDesktopWindow() || hwnd == GetShellWindow()) {
|
||||
return false;
|
||||
}
|
||||
RECT appBounds;
|
||||
MONITORINFO monitorInfo = {0};
|
||||
monitorInfo.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
|
||||
&monitorInfo);
|
||||
GetWindowRect(hwnd, &appBounds);
|
||||
return monitorInfo.rcMonitor.bottom == appBounds.bottom &&
|
||||
monitorInfo.rcMonitor.top == appBounds.top &&
|
||||
monitorInfo.rcMonitor.left == appBounds.left &&
|
||||
monitorInfo.rcMonitor.right == appBounds.right;
|
||||
}
|
||||
|
||||
static std::string getWindowClass(HWND hwnd)
|
||||
{
|
||||
std::wstring wClass;
|
||||
wClass.resize(256);
|
||||
int len = GetClassNameW(hwnd, &wClass[0], (int)wClass.size());
|
||||
if (len <= 0) {
|
||||
return "";
|
||||
}
|
||||
wClass.resize(len);
|
||||
size_t utf8len = os_wcs_to_utf8(wClass.c_str(), 0, nullptr, 0);
|
||||
std::string result;
|
||||
result.resize(utf8len);
|
||||
os_wcs_to_utf8(wClass.c_str(), 0, &result[0], utf8len + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
static HWND getHWNDfromTitle(const std::string &title);
|
||||
static std::optional<std::string> GetTextInWindow(const std::string &window);
|
||||
|
||||
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
|
||||
{
|
||||
const std::string foregroundTitle = GetCurrentWindowTitle();
|
||||
const auto titles = getAllWindowTitles();
|
||||
|
||||
std::vector<WindowInfo> result;
|
||||
result.reserve(titles.size());
|
||||
|
||||
for (const auto &title : titles) {
|
||||
WindowInfo info;
|
||||
info.title = title;
|
||||
|
||||
const bool needHwnd = options.geometry || options.fullscreen ||
|
||||
options.maximized ||
|
||||
options.windowClass || options.text ||
|
||||
options.focus;
|
||||
|
||||
if (options.focus) {
|
||||
info.focused = (title == foregroundTitle);
|
||||
}
|
||||
|
||||
if (!needHwnd || (!options.geometry && !options.fullscreen &&
|
||||
!options.maximized && !options.windowClass &&
|
||||
!options.text)) {
|
||||
result.emplace_back(std::move(info));
|
||||
continue;
|
||||
}
|
||||
|
||||
HWND hwnd = getHWNDfromTitle(title);
|
||||
if (!hwnd) {
|
||||
result.emplace_back(std::move(info));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.geometry || options.fullscreen ||
|
||||
options.maximized) {
|
||||
RECT rect;
|
||||
if (GetWindowRect(hwnd, &rect)) {
|
||||
info.x = rect.left;
|
||||
info.y = rect.top;
|
||||
info.width = rect.right - rect.left;
|
||||
info.height = rect.bottom - rect.top;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maximized) {
|
||||
info.maximized = isWindowMaximized(hwnd);
|
||||
}
|
||||
|
||||
if (options.fullscreen) {
|
||||
info.fullscreen = isWindowFullscreen(hwnd);
|
||||
}
|
||||
|
||||
if (options.windowClass) {
|
||||
info.windowClass = getWindowClass(hwnd);
|
||||
}
|
||||
|
||||
if (options.text) {
|
||||
info.text = GetTextInWindow(title);
|
||||
}
|
||||
|
||||
result.emplace_back(std::move(info));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string GetCurrentWindowTitle()
|
||||
{
|
||||
HWND window = GetForegroundWindow();
|
||||
@@ -188,36 +316,6 @@ static HWND getHWNDfromTitle(const std::string &title)
|
||||
return hwnd;
|
||||
}
|
||||
|
||||
bool IsMaximized(const std::string &title)
|
||||
{
|
||||
RECT appBounds;
|
||||
MONITORINFO monitorInfo = {0};
|
||||
HWND hwnd = NULL;
|
||||
|
||||
hwnd = getHWNDfromTitle(title);
|
||||
if (!hwnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
monitorInfo.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
|
||||
&monitorInfo);
|
||||
|
||||
if (hwnd && hwnd != GetDesktopWindow() && hwnd != GetShellWindow()) {
|
||||
if (IsZoomed(hwnd)) {
|
||||
return true;
|
||||
}
|
||||
GetWindowRect(hwnd, &appBounds);
|
||||
if (monitorInfo.rcMonitor.bottom == appBounds.bottom &&
|
||||
monitorInfo.rcMonitor.top == appBounds.top &&
|
||||
monitorInfo.rcMonitor.left == appBounds.left &&
|
||||
monitorInfo.rcMonitor.right == appBounds.right) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::wstring GetControlText(HWND hwnd, IUIAutomationElement *element)
|
||||
{
|
||||
VARIANT var;
|
||||
@@ -244,7 +342,7 @@ static std::wstring GetControlText(HWND hwnd, IUIAutomationElement *element)
|
||||
return text;
|
||||
}
|
||||
|
||||
std::optional<std::string> GetTextInWindow(const std::string &window)
|
||||
static std::optional<std::string> GetTextInWindow(const std::string &window)
|
||||
{
|
||||
HWND hwnd = getHWNDfromTitle(window);
|
||||
if (!hwnd) {
|
||||
@@ -311,35 +409,6 @@ std::optional<std::string> GetTextInWindow(const std::string &window)
|
||||
return tmp;
|
||||
}
|
||||
|
||||
bool IsFullscreen(const std::string &title)
|
||||
{
|
||||
RECT appBounds;
|
||||
MONITORINFO monitorInfo = {0};
|
||||
|
||||
HWND hwnd = NULL;
|
||||
hwnd = getHWNDfromTitle(title);
|
||||
|
||||
if (!hwnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
monitorInfo.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST),
|
||||
&monitorInfo);
|
||||
|
||||
if (hwnd && hwnd != GetDesktopWindow() && hwnd != GetShellWindow()) {
|
||||
GetWindowRect(hwnd, &appBounds);
|
||||
if (monitorInfo.rcMonitor.bottom == appBounds.bottom &&
|
||||
monitorInfo.rcMonitor.top == appBounds.top &&
|
||||
monitorInfo.rcMonitor.left == appBounds.left &&
|
||||
monitorInfo.rcMonitor.right == appBounds.right) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList GetProcessList()
|
||||
{
|
||||
QStringList processes;
|
||||
@@ -500,9 +569,86 @@ int SecondsSinceLastInput()
|
||||
return (getTime() - getLastInputTime()) / 1000;
|
||||
}
|
||||
|
||||
static void addPluginFolderToSymbolPath()
|
||||
{
|
||||
// This runs after OBS_FRONTEND_EVENT_FINISHED_LOADING, which fires after
|
||||
// obs_load_all_modules() completes. By that point OBS has already called
|
||||
// reset_win32_symbol_paths() -> SymInitializeW(), so DbgHelp is
|
||||
// initialized and we can append our plugins subfolder (where the PDB
|
||||
// files live) to the existing search path.
|
||||
HMODULE dbghelp = LoadLibraryW(L"DbgHelp");
|
||||
if (!dbghelp) {
|
||||
return;
|
||||
}
|
||||
|
||||
typedef BOOL(WINAPI * SymGetSearchPathW_t)(HANDLE, PWSTR, DWORD);
|
||||
typedef BOOL(WINAPI * SymSetSearchPathW_t)(HANDLE, PCWSTR);
|
||||
typedef BOOL(WINAPI * SymRefreshModuleList_t)(HANDLE);
|
||||
|
||||
auto symGetSearchPathW = reinterpret_cast<SymGetSearchPathW_t>(
|
||||
GetProcAddress(dbghelp, "SymGetSearchPathW"));
|
||||
auto symSetSearchPathW = reinterpret_cast<SymSetSearchPathW_t>(
|
||||
GetProcAddress(dbghelp, "SymSetSearchPathW"));
|
||||
auto symRefreshModuleList = reinterpret_cast<SymRefreshModuleList_t>(
|
||||
GetProcAddress(dbghelp, "SymRefreshModuleList"));
|
||||
|
||||
if (!symGetSearchPathW || !symSetSearchPathW || !symRefreshModuleList) {
|
||||
FreeLibrary(dbghelp);
|
||||
return;
|
||||
}
|
||||
|
||||
HMODULE hModule = NULL;
|
||||
if (!GetModuleHandleExW(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(addPluginFolderToSymbolPath),
|
||||
&hModule)) {
|
||||
FreeLibrary(dbghelp);
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t dllDir[MAX_PATH];
|
||||
if (!GetModuleFileNameW(hModule, dllDir, MAX_PATH)) {
|
||||
FreeLibrary(dbghelp);
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t *lastSep = wcsrchr(dllDir, L'\\');
|
||||
if (!lastSep) {
|
||||
FreeLibrary(dbghelp);
|
||||
return;
|
||||
}
|
||||
*lastSep = L'\0';
|
||||
|
||||
wchar_t pluginsPath[MAX_PATH];
|
||||
wcsncpy_s(pluginsPath, MAX_PATH, dllDir, _TRUNCATE);
|
||||
wcsncat_s(pluginsPath, MAX_PATH, L"\\" ADVSS_WIDEN(ADVSS_PLUGIN_FOLDER),
|
||||
_TRUNCATE);
|
||||
|
||||
constexpr DWORD currentPathLen = 4096;
|
||||
constexpr DWORD newPathLen = 8192;
|
||||
auto currentPath = std::make_unique<wchar_t[]>(currentPathLen);
|
||||
auto newPath = std::make_unique<wchar_t[]>(newPathLen);
|
||||
|
||||
symGetSearchPathW(GetCurrentProcess(), currentPath.get(),
|
||||
currentPathLen);
|
||||
|
||||
if (currentPath[0] != L'\0') {
|
||||
_snwprintf_s(newPath.get(), newPathLen, _TRUNCATE, L"%s;%s",
|
||||
currentPath.get(), pluginsPath);
|
||||
} else {
|
||||
wcsncpy_s(newPath.get(), newPathLen, pluginsPath, _TRUNCATE);
|
||||
}
|
||||
|
||||
symSetSearchPathW(GetCurrentProcess(), newPath.get());
|
||||
symRefreshModuleList(GetCurrentProcess());
|
||||
FreeLibrary(dbghelp);
|
||||
}
|
||||
|
||||
void PlatformInit()
|
||||
{
|
||||
CoInitialize(NULL);
|
||||
AddFinishedLoadingStep(addPluginFolderToSymbolPath);
|
||||
}
|
||||
|
||||
void PlatformCleanup()
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
#include <obs-module.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define ADVSS_SETENV(k, v) _putenv_s(k, v)
|
||||
#define ADVSS_UNSETENV(k) _putenv_s(k, "")
|
||||
#else
|
||||
#define ADVSS_SETENV(k, v) setenv(k, v, 1)
|
||||
#define ADVSS_UNSETENV(k) unsetenv(k)
|
||||
#endif
|
||||
|
||||
#define ADVSS_INSTANCE_ENV "ADVSS_INSTANCE_LOADED"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("advanced-scene-switcher", "en-US")
|
||||
@@ -17,6 +28,12 @@ void obs_module_post_load(void)
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
if (getenv(ADVSS_INSTANCE_ENV)) {
|
||||
blog(LOG_WARNING,
|
||||
"[adv-ss] Another instance is already loaded - skipping.");
|
||||
return false;
|
||||
}
|
||||
ADVSS_SETENV(ADVSS_INSTANCE_ENV, "1");
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
InitSceneSwitcher(obs_current_module(), obs_module_text);
|
||||
return true;
|
||||
@@ -25,4 +42,5 @@ bool obs_module_load(void)
|
||||
void obs_module_unload(void)
|
||||
{
|
||||
FreeSceneSwitcher();
|
||||
ADVSS_UNSETENV(ADVSS_INSTANCE_ENV);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
macro(add_plugin plugin)
|
||||
option(ENABLE_${plugin}_PLUGIN "Enable the ${plugin} plugin" ON)
|
||||
if(ENABLE_${plugin}_PLUGIN)
|
||||
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_PENDING ${plugin})
|
||||
add_subdirectory(${plugin})
|
||||
else()
|
||||
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_DISABLED_BY_OPTION
|
||||
${plugin})
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
|
||||
add_subdirectory(base)
|
||||
|
||||
# Add plugins below, which have dependencies to external libraries or other
|
||||
# components which might potentially not be fulfilled.
|
||||
|
||||
@@ -28,12 +30,43 @@ install_advss_plugin_dependency(...)
|
||||
... to install the plugin and its dependencies.
|
||||
#]]
|
||||
|
||||
add_plugin(base)
|
||||
add_plugin(http)
|
||||
add_plugin(midi)
|
||||
add_plugin(mqtt)
|
||||
add_plugin(openvr)
|
||||
add_plugin(schedule)
|
||||
add_plugin(scripting)
|
||||
add_plugin(stream-deck)
|
||||
add_plugin(twitch)
|
||||
add_plugin(usb)
|
||||
add_plugin(video)
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
|
||||
# Print plugin summary
|
||||
get_property(_advss_enabled GLOBAL PROPERTY ADVSS_PLUGINS_ENABLED)
|
||||
get_property(_advss_pending GLOBAL PROPERTY ADVSS_PLUGINS_PENDING)
|
||||
get_property(_advss_disabled_opt GLOBAL
|
||||
PROPERTY ADVSS_PLUGINS_DISABLED_BY_OPTION)
|
||||
|
||||
set(_advss_disabled_deps)
|
||||
foreach(_p ${_advss_pending})
|
||||
if(NOT _p IN_LIST _advss_enabled)
|
||||
list(APPEND _advss_disabled_deps ${_p})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
message(STATUS "")
|
||||
message(STATUS "ADVSS Plugin Summary")
|
||||
message(STATUS "====================")
|
||||
foreach(_p ${_advss_enabled})
|
||||
message(STATUS " ENABLED: ${_p}")
|
||||
endforeach()
|
||||
foreach(_p ${_advss_disabled_deps})
|
||||
message(STATUS " DISABLED: ${_p} (missing dependencies)")
|
||||
endforeach()
|
||||
foreach(_p ${_advss_disabled_opt})
|
||||
message(STATUS " DISABLED: ${_p} (ENABLE_${_p}_PLUGIN=OFF)")
|
||||
endforeach()
|
||||
message(STATUS "")
|
||||
|
||||
@@ -46,7 +46,7 @@ public:
|
||||
|
||||
} // namespace
|
||||
|
||||
static void handleShutdown(enum obs_frontend_event event, void *private_data)
|
||||
static void handleShutdown(enum obs_frontend_event event, void *)
|
||||
{
|
||||
if (event != OBS_FRONTEND_EVENT_EXIT) {
|
||||
return;
|
||||
|
||||
@@ -259,7 +259,7 @@ void MacroActionSourceInteractionEdit::AcceptRecorded(
|
||||
_stepList->SetSteps(_entryData->_steps);
|
||||
}
|
||||
|
||||
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
|
||||
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *)
|
||||
{
|
||||
const QSignalBlocker b(this);
|
||||
UpdateEntryData();
|
||||
|
||||
@@ -42,19 +42,20 @@ void CloseWindow(const std::string &) {}
|
||||
|
||||
std::optional<std::string> MacroActionWindow::GetMatchingWindow() const
|
||||
{
|
||||
const auto windowList = GetWindowList();
|
||||
const auto windows = GetWindows();
|
||||
|
||||
if (!_regex.Enabled()) {
|
||||
if (std::find(windowList.begin(), windowList.end(),
|
||||
std::string(_window)) == windowList.end()) {
|
||||
return {};
|
||||
for (const auto &info : windows) {
|
||||
if (info.title == std::string(_window)) {
|
||||
return info.title;
|
||||
}
|
||||
}
|
||||
return _window;
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const auto &window : windowList) {
|
||||
if (_regex.Matches(window, _window)) {
|
||||
return window;
|
||||
for (const auto &info : windows) {
|
||||
if (_regex.Matches(info.title, _window)) {
|
||||
return info.title;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -710,7 +710,7 @@ void MacroConditionDateEdit::UpdateEntryData()
|
||||
SetWidgetStatus();
|
||||
}
|
||||
|
||||
void MacroConditionDateEdit::showEvent(QShowEvent *event)
|
||||
void MacroConditionDateEdit::showEvent(QShowEvent *)
|
||||
{
|
||||
const QSignalBlocker b(this);
|
||||
UpdateEntryData();
|
||||
|
||||
@@ -24,6 +24,8 @@ bool MacroConditionGameCapture::CheckCondition()
|
||||
return false;
|
||||
}
|
||||
|
||||
_activeKeeper.SetActive(_keepActive);
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mtx);
|
||||
if (_hooked) {
|
||||
SetTempVarValue("title", _title);
|
||||
@@ -38,6 +40,7 @@ bool MacroConditionGameCapture::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroCondition::Save(obj);
|
||||
_source.Save(obj);
|
||||
obs_data_set_bool(obj, "keepActive", _keepActive);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -45,6 +48,7 @@ bool MacroConditionGameCapture::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroCondition::Load(obj);
|
||||
_source.Load(obj);
|
||||
_keepActive = obs_data_get_bool(obj, "keepActive");
|
||||
SetupSignalHandler(OBSGetStrongRef(_source.GetSource()));
|
||||
return true;
|
||||
}
|
||||
@@ -111,6 +115,9 @@ void MacroConditionGameCapture::SetupSignalHandler(obs_source_t *source)
|
||||
_unhookSignal = OBSSignal(sh, "unhooked", UnhookedSignalReceived, this);
|
||||
_lastSource = source;
|
||||
|
||||
_activeKeeper.SetActive(_keepActive);
|
||||
_activeKeeper.SetSource(source);
|
||||
|
||||
SetupInitialState(source);
|
||||
}
|
||||
|
||||
@@ -164,17 +171,33 @@ MacroConditionGameCaptureEdit::MacroConditionGameCaptureEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionGameCapture> entryData)
|
||||
: QWidget(parent),
|
||||
_sources(new SourceSelectionWidget(this, getGameCaptureSourcesList,
|
||||
true))
|
||||
true)),
|
||||
_keepActive(new QCheckBox(
|
||||
obs_module_text("AdvSceneSwitcher.keepSourceActive"), this)),
|
||||
_keepActiveHelp(new HelpIcon(
|
||||
obs_module_text("AdvSceneSwitcher.keepSourceActive.help"),
|
||||
this))
|
||||
{
|
||||
QWidget::connect(_sources,
|
||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
QWidget::connect(_keepActive, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(KeepActiveChanged(int)));
|
||||
|
||||
auto layout = new QHBoxLayout;
|
||||
auto entryLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.condition.gameCapture.entry"),
|
||||
layout, {{"{{sources}}", _sources}});
|
||||
setLayout(layout);
|
||||
entryLayout, {{"{{sources}}", _sources}});
|
||||
|
||||
auto keepActiveLayout = new QHBoxLayout;
|
||||
keepActiveLayout->addWidget(_keepActive);
|
||||
keepActiveLayout->addWidget(_keepActiveHelp);
|
||||
keepActiveLayout->addStretch();
|
||||
|
||||
auto mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(entryLayout);
|
||||
mainLayout->addLayout(keepActiveLayout);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
@@ -187,9 +210,16 @@ void MacroConditionGameCaptureEdit::SourceChanged(const SourceSelection &source)
|
||||
_entryData->_source = source;
|
||||
}
|
||||
|
||||
void MacroConditionGameCaptureEdit::KeepActiveChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_keepActive = state;
|
||||
}
|
||||
|
||||
void MacroConditionGameCaptureEdit::UpdateEntryData()
|
||||
{
|
||||
_sources->SetSource(_entryData->_source);
|
||||
_keepActive->setChecked(_entryData->_keepActive);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
#include "help-icon.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "source-helpers.hpp"
|
||||
#include "source-selection.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -20,6 +23,7 @@ public:
|
||||
}
|
||||
|
||||
SourceSelection _source;
|
||||
bool _keepActive = false;
|
||||
|
||||
private:
|
||||
static void HookedSignalReceived(void *data, calldata_t *);
|
||||
@@ -31,6 +35,7 @@ private:
|
||||
void GetCalldataInfo(calldata_t *cd);
|
||||
|
||||
obs_source_t *_lastSource = nullptr;
|
||||
SourceActiveKeeper _activeKeeper;
|
||||
OBSSignal _hookSignal;
|
||||
OBSSignal _unhookSignal;
|
||||
|
||||
@@ -63,9 +68,12 @@ public:
|
||||
|
||||
private slots:
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void KeepActiveChanged(int);
|
||||
|
||||
private:
|
||||
SourceSelectionWidget *_sources;
|
||||
QCheckBox *_keepActive;
|
||||
HelpIcon *_keepActiveHelp;
|
||||
std::shared_ptr<MacroConditionGameCapture> _entryData;
|
||||
|
||||
bool _loading = true;
|
||||
|
||||
@@ -158,8 +158,21 @@ void MacroConditionTransition::TransitionStarted(void *data, calldata_t *cd)
|
||||
|
||||
OBSSourceAutoRelease startSource = obs_transition_get_source(
|
||||
transitionSource, OBS_TRANSITION_SOURCE_A);
|
||||
|
||||
// In studio mode, source A may be a private copy of the scene.
|
||||
// Look up the original by name to get a comparable reference.
|
||||
const bool studioModeActive =
|
||||
obs_frontend_preview_program_mode_active();
|
||||
if (studioModeActive && startSource) {
|
||||
OBSSourceAutoRelease startSourceByName = obs_get_source_by_name(
|
||||
obs_source_get_name(startSource));
|
||||
if (startSourceByName) {
|
||||
startSource = std::move(startSourceByName);
|
||||
}
|
||||
}
|
||||
|
||||
OBSSourceAutoRelease endSource =
|
||||
obs_frontend_preview_program_mode_active()
|
||||
studioModeActive
|
||||
? obs_frontend_get_current_scene()
|
||||
: obs_transition_get_source(transitionSource,
|
||||
OBS_TRANSITION_SOURCE_B);
|
||||
@@ -357,6 +370,7 @@ void MacroConditionTransitionEdit::SceneChanged(const SceneSelection &s)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_scene = s;
|
||||
_entryData->ConnectToTransitionSignals();
|
||||
}
|
||||
|
||||
void MacroConditionTransitionEdit::DurationChanged(const Duration &dur)
|
||||
|
||||
@@ -14,21 +14,6 @@ bool MacroConditionWindow::_registered = MacroConditionFactory::Register(
|
||||
{MacroConditionWindow::Create, MacroConditionWindowEdit::Create,
|
||||
"AdvSceneSwitcher.condition.window"});
|
||||
|
||||
static bool windowContainsText(const std::string &window,
|
||||
const std::string &matchText,
|
||||
const RegexConfig ®ex)
|
||||
{
|
||||
auto text = GetTextInWindow(window);
|
||||
if (!text.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (regex.Enabled()) {
|
||||
return regex.Matches(*text, matchText);
|
||||
}
|
||||
return text == matchText;
|
||||
}
|
||||
|
||||
void MacroConditionWindow::SetCheckText(bool value)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
@@ -46,83 +31,76 @@ bool MacroConditionWindow::GetCheckText()
|
||||
}
|
||||
|
||||
bool MacroConditionWindow::WindowMatchesRequirements(
|
||||
const std::string &window) const
|
||||
const WindowInfo &info) const
|
||||
{
|
||||
const bool focusCheckOK =
|
||||
(!_focus || window == ForegroundWindowTitle());
|
||||
if (!focusCheckOK) {
|
||||
if (_focus && !info.focused) {
|
||||
return false;
|
||||
}
|
||||
const bool fullscreenCheckOK = (!_fullscreen || IsFullscreen(window));
|
||||
if (!fullscreenCheckOK) {
|
||||
if (_fullscreen && !info.fullscreen) {
|
||||
return false;
|
||||
}
|
||||
const bool maxCheckOK = (!_maximized || IsMaximized(window));
|
||||
if (!maxCheckOK) {
|
||||
if (_maximized && !info.maximized) {
|
||||
return false;
|
||||
}
|
||||
const bool textCheckOK =
|
||||
(!_checkText || windowContainsText(window, _text, _textRegex));
|
||||
if (!textCheckOK) {
|
||||
return false;
|
||||
if (_checkText) {
|
||||
if (!info.text.has_value()) {
|
||||
return false;
|
||||
}
|
||||
if (_textRegex.Enabled()) {
|
||||
if (!_textRegex.Matches(*info.text, _text)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (*info.text != std::string(_text)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroConditionWindow::WindowMatches(
|
||||
const std::vector<std::string> &windowList)
|
||||
bool MacroConditionWindow::FindMatch(const std::vector<WindowInfo> &windows)
|
||||
{
|
||||
bool match = !_checkTitle ||
|
||||
std::find(windowList.begin(), windowList.end(),
|
||||
std::string(_window)) != windowList.end();
|
||||
match = match && WindowMatchesRequirements(_window);
|
||||
SetVariableValueBasedOnMatch(_window);
|
||||
return match;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string GetWindowClassByWindowTitle(const std::string &window);
|
||||
#endif
|
||||
|
||||
bool MacroConditionWindow::WindowRegexMatches(
|
||||
const std::vector<std::string> &windowList)
|
||||
{
|
||||
// No need to test if checking for window title is required as if the
|
||||
// user has disabled window title matching the option will always be
|
||||
// enabled in the backend and use the regular expression ".*".
|
||||
|
||||
for (const auto &window : windowList) {
|
||||
if (_windowRegex.Matches(window, _window) &&
|
||||
WindowMatchesRequirements(window)) {
|
||||
SetVariableValueBasedOnMatch(window);
|
||||
return true;
|
||||
// When regex is enabled the title check is always active (the backend
|
||||
// uses ".*" when the user disables the title check), so we can use a
|
||||
// single predicate for both modes.
|
||||
for (const auto &info : windows) {
|
||||
const bool titleOK =
|
||||
_windowRegex.Enabled()
|
||||
? _windowRegex.Matches(info.title, _window)
|
||||
: (!_checkTitle ||
|
||||
info.title == std::string(_window));
|
||||
if (!titleOK || !WindowMatchesRequirements(info)) {
|
||||
continue;
|
||||
}
|
||||
SetVariableValueBasedOnMatch(&info);
|
||||
return true;
|
||||
}
|
||||
SetVariableValueBasedOnMatch("");
|
||||
SetVariableValueBasedOnMatch(nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
void MacroConditionWindow::SetVariableValueBasedOnMatch(
|
||||
const std::string &matchWindow)
|
||||
void MacroConditionWindow::SetVariableValueBasedOnMatch(const WindowInfo *info)
|
||||
{
|
||||
SetTempVarValue("window", matchWindow);
|
||||
const std::string title = info ? info->title : "";
|
||||
SetTempVarValue("window", title);
|
||||
SetTempVarValue("windowX", info ? std::to_string(info->x) : "");
|
||||
SetTempVarValue("windowY", info ? std::to_string(info->y) : "");
|
||||
SetTempVarValue("windowWidth", info ? std::to_string(info->width) : "");
|
||||
SetTempVarValue("windowHeight",
|
||||
info ? std::to_string(info->height) : "");
|
||||
#ifdef _WIN32
|
||||
SetTempVarValue("windowClass",
|
||||
GetWindowClassByWindowTitle(matchWindow));
|
||||
SetTempVarValue("windowClass", info ? info->windowClass : "");
|
||||
if (_checkText) {
|
||||
const auto text = GetTextInWindow(matchWindow);
|
||||
if (text) {
|
||||
SetTempVarValue("windowText", *text);
|
||||
}
|
||||
SetTempVarValue("windowText",
|
||||
(info && info->text) ? *info->text : "");
|
||||
}
|
||||
#endif
|
||||
if (!IsReferencedInVars()) {
|
||||
return;
|
||||
}
|
||||
if (_checkText) {
|
||||
const auto text = GetTextInWindow(matchWindow);
|
||||
SetVariableValue(text.value_or(""));
|
||||
SetVariableValue((info && info->text) ? *info->text : "");
|
||||
} else {
|
||||
SetVariableValue(ForegroundWindowTitle());
|
||||
}
|
||||
@@ -135,13 +113,18 @@ static bool foregroundWindowChanged()
|
||||
|
||||
bool MacroConditionWindow::CheckCondition()
|
||||
{
|
||||
const auto windowList = GetWindowList();
|
||||
bool match = false;
|
||||
if (_windowRegex.Enabled()) {
|
||||
match = WindowRegexMatches(windowList);
|
||||
} else {
|
||||
match = WindowMatches(windowList);
|
||||
}
|
||||
WindowQueryOptions options;
|
||||
options.focus = _focus;
|
||||
options.fullscreen = _fullscreen;
|
||||
options.maximized = _maximized;
|
||||
options.geometry = true;
|
||||
#ifdef _WIN32
|
||||
options.windowClass = true;
|
||||
options.text = _checkText;
|
||||
#endif
|
||||
|
||||
const auto windows = GetWindows(options);
|
||||
bool match = FindMatch(windows);
|
||||
match = match && (!_windowFocusChanged || foregroundWindowChanged());
|
||||
return match;
|
||||
}
|
||||
@@ -201,6 +184,26 @@ void MacroConditionWindow::SetupTempVars()
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.window.window"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.window.window.description"));
|
||||
AddTempvar(
|
||||
"windowX",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.window.windowX"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.window.windowX.description"));
|
||||
AddTempvar(
|
||||
"windowY",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.window.windowY"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.window.windowY.description"));
|
||||
AddTempvar(
|
||||
"windowWidth",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.window.windowWidth"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.window.windowWidth.description"));
|
||||
AddTempvar(
|
||||
"windowHeight",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.window.windowHeight"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.window.windowHeight.description"));
|
||||
#ifdef _WIN32
|
||||
AddTempvar(
|
||||
"windowClass",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "platform-funcs.hpp"
|
||||
#include "variable-text-edit.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "window-selection.hpp"
|
||||
@@ -37,10 +38,9 @@ public:
|
||||
RegexConfig _textRegex = RegexConfig::PartialMatchRegexConfig();
|
||||
|
||||
private:
|
||||
bool WindowMatchesRequirements(const std::string &window) const;
|
||||
bool WindowMatches(const std::vector<std::string> &windowList);
|
||||
bool WindowRegexMatches(const std::vector<std::string> &windowList);
|
||||
void SetVariableValueBasedOnMatch(const std::string &matchWindow);
|
||||
bool WindowMatchesRequirements(const WindowInfo &info) const;
|
||||
bool FindMatch(const std::vector<WindowInfo> &windows);
|
||||
void SetVariableValueBasedOnMatch(const WindowInfo *info);
|
||||
void SetupTempVars();
|
||||
|
||||
// For now only supported on Windows
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "connection-manager.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "name-dialog.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
@@ -23,6 +24,52 @@ bool setup()
|
||||
{
|
||||
AddSaveStep(saveConnections);
|
||||
AddLoadStep(loadConnections);
|
||||
AddPluginCleanupStep([]() { connections.clear(); });
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.websocketConnections",
|
||||
"websocketConnections",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &c : connections) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(c->Name())))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
c->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "websocketConnections",
|
||||
array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(
|
||||
data, "websocketConnections");
|
||||
const size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item =
|
||||
obs_data_array_item(array, i);
|
||||
auto con = WSConnection::Create();
|
||||
con->Load(item);
|
||||
if (!GetWeakConnectionByName(con->Name())
|
||||
.expired())
|
||||
continue;
|
||||
connections.emplace_back(con);
|
||||
ConnectionSelectionSignalManager::Instance()
|
||||
->Add(QString::fromStdString(
|
||||
con->Name()));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &c : connections) {
|
||||
const QString name =
|
||||
QString::fromStdString(c->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ void PopulateProfileSelection(QComboBox *box)
|
||||
std::string GetPathInProfileDir(const char *filePath)
|
||||
{
|
||||
auto path = obs_frontend_get_current_profile_path();
|
||||
if (!path) {
|
||||
return "";
|
||||
}
|
||||
std::string result(path);
|
||||
bfree(path);
|
||||
return result + "/" + filePath;
|
||||
|
||||
@@ -205,26 +205,6 @@ static std::vector<HWND> getHWNDfromTitle(const std::string &title)
|
||||
return hwnds;
|
||||
}
|
||||
|
||||
std::string GetWindowClassByWindowTitle(const std::string &window)
|
||||
{
|
||||
auto hwnds = getHWNDfromTitle(window);
|
||||
if (hwnds.empty()) {
|
||||
return "";
|
||||
}
|
||||
auto hwnd = hwnds.at(0);
|
||||
std::wstring wClass;
|
||||
wClass.resize(1024);
|
||||
if (!GetClassNameW(hwnd, &wClass[0], wClass.capacity())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t len = os_wcs_to_utf8(wClass.c_str(), 0, nullptr, 0);
|
||||
std::string className;
|
||||
className.resize(len);
|
||||
os_wcs_to_utf8(wClass.c_str(), 0, &className[0], len + 1);
|
||||
return className;
|
||||
}
|
||||
|
||||
void SetFocusWindow(const std::string &title)
|
||||
{
|
||||
auto hwnds = getHWNDfromTitle(title);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "http-server.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
@@ -32,6 +33,49 @@ bool setup()
|
||||
}
|
||||
httpServers.clear();
|
||||
});
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.httpServers", "httpServers",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
for (const auto &s : httpServers) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(s->Name())))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
s->Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "httpServers", array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_get_array(data, "httpServers");
|
||||
const size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item =
|
||||
obs_data_array_item(array, i);
|
||||
auto server = HttpServer::Create();
|
||||
server->Load(item);
|
||||
if (!GetWeakHttpServerByName(server->Name())
|
||||
.expired())
|
||||
continue;
|
||||
httpServers.emplace_back(server);
|
||||
HttpServerSignalManager::Instance()->Add(
|
||||
QString::fromStdString(
|
||||
server->Name()));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
for (const auto &s : httpServers) {
|
||||
const QString name =
|
||||
QString::fromStdString(s->Name());
|
||||
items.append({name, name});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ else()
|
||||
-Wno-error=unused-parameter -Wno-error=unused-variable
|
||||
-Wno-error=sign-compare)
|
||||
if(OS_MACOS)
|
||||
target_compile_options(libremidi PUBLIC -Wno-error=vla-extension)
|
||||
target_compile_options(
|
||||
libremidi PUBLIC -Wno-error=vla-extension -Wno-vla-extension
|
||||
-Wno-shorten-64-to-32)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user