mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 04:36:21 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8174004eda | ||
|
|
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 |
@@ -19,14 +19,9 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup cmake
|
|
||||||
uses: jwlawson/actions-setup-cmake@v1.13
|
|
||||||
with:
|
|
||||||
cmake-version: '3.x.x'
|
|
||||||
|
|
||||||
- name: Restore cached dependencies
|
- name: Restore cached dependencies
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.DEP_DIR }}
|
path: ${{ env.DEP_DIR }}
|
||||||
key: ${{ env.DEP_DIR }}-${{ runner.os }}-${{ inputs.target }}
|
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)"
|
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||||
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
|
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
|
||||||
echo "/home/linuxbrew/.linuxbrew/opt/clang-format@16/bin" >> $GITHUB_PATH
|
echo "/home/linuxbrew/.linuxbrew/opt/clang-format@16/bin" >> $GITHUB_PATH
|
||||||
|
brew update
|
||||||
brew install --quiet zsh
|
brew install --quiet zsh
|
||||||
echo ::endgroup::
|
echo ::endgroup::
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ runs:
|
|||||||
|
|
||||||
if (( ${changes[(I)(*.c|*.h|*.cpp|*.hpp|*.m|*.mm)]} )) {
|
if (( ${changes[(I)(*.c|*.h|*.cpp|*.hpp|*.m|*.mm)]} )) {
|
||||||
echo ::group::Install clang-format-16
|
echo ::group::Install clang-format-16
|
||||||
|
brew update
|
||||||
brew install --quiet obsproject/tools/clang-format@16
|
brew install --quiet obsproject/tools/clang-format@16
|
||||||
echo ::endgroup::
|
echo ::endgroup::
|
||||||
|
|
||||||
|
|||||||
2
.github/scripts/.Wingetfile
vendored
2
.github/scripts/.Wingetfile
vendored
@@ -1,4 +1,4 @@
|
|||||||
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
|
package 'cmake', path: 'Cmake\bin', bin: 'cmake'
|
||||||
package 'innosetup', path: 'Inno Setup 6', bin: 'iscc'
|
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'
|
package 'Microsoft.VisualStudio.Locator', path: 'vswhere', bin: 'vswhere'
|
||||||
35
.github/scripts/Package-Windows.ps1
vendored
35
.github/scripts/Package-Windows.ps1
vendored
@@ -68,14 +68,44 @@ function Package {
|
|||||||
|
|
||||||
Remove-Item @RemoveArgs
|
Remove-Item @RemoveArgs
|
||||||
|
|
||||||
|
# Build a staging directory with two subfolders:
|
||||||
|
# recommended/ - new layout, extract to %ProgramData%\obs-studio\
|
||||||
|
# legacy/ - old layout, extract to the OBS install directory
|
||||||
|
$ReleasePath = "${ProjectRoot}/release/${Configuration}"
|
||||||
|
$StagingPath = "${ProjectRoot}/release/zip-staging"
|
||||||
|
$NewBinPath = "${ReleasePath}/${ProductName}/bin/64bit"
|
||||||
|
$NewDataPath = "${ReleasePath}/${ProductName}/data"
|
||||||
|
|
||||||
|
Remove-Item -Path $StagingPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Force -Path $StagingPath | Out-Null
|
||||||
|
Copy-Item -Path "${ProjectRoot}/build-aux/CI/windows/README.txt" -Destination "${StagingPath}/README.txt"
|
||||||
|
|
||||||
|
if ( Test-Path -Path $NewBinPath ) {
|
||||||
|
$RecBinPath = "${StagingPath}/recommended/${ProductName}/bin/64bit"
|
||||||
|
$LegacyBinPath = "${StagingPath}/legacy/obs-plugins/64bit"
|
||||||
|
New-Item -ItemType Directory -Force -Path $RecBinPath | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path $LegacyBinPath | Out-Null
|
||||||
|
Copy-Item -Path "${NewBinPath}/*" -Destination $RecBinPath -Recurse -Force
|
||||||
|
Copy-Item -Path "${NewBinPath}/*" -Destination $LegacyBinPath -Recurse -Force
|
||||||
|
}
|
||||||
|
if ( Test-Path -Path $NewDataPath ) {
|
||||||
|
$RecDataPath = "${StagingPath}/recommended/${ProductName}/data"
|
||||||
|
$LegacyDataPath = "${StagingPath}/legacy/data/obs-plugins/${ProductName}"
|
||||||
|
New-Item -ItemType Directory -Force -Path $RecDataPath | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path $LegacyDataPath | Out-Null
|
||||||
|
Copy-Item -Path "${NewDataPath}/*" -Destination $RecDataPath -Recurse -Force
|
||||||
|
Copy-Item -Path "${NewDataPath}/*" -Destination $LegacyDataPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
Log-Group "Archiving ${ProductName}..."
|
Log-Group "Archiving ${ProductName}..."
|
||||||
$CompressArgs = @{
|
$CompressArgs = @{
|
||||||
Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*")
|
Path = (Get-ChildItem -Path $StagingPath)
|
||||||
CompressionLevel = 'Optimal'
|
CompressionLevel = 'Optimal'
|
||||||
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
|
DestinationPath = "${ProjectRoot}/release/${OutputName}.zip"
|
||||||
Verbose = ($Env:CI -ne $null)
|
Verbose = ($null -ne $Env:CI)
|
||||||
}
|
}
|
||||||
Compress-Archive -Force @CompressArgs
|
Compress-Archive -Force @CompressArgs
|
||||||
|
Remove-Item -Path $StagingPath -Recurse -Force
|
||||||
Log-Group
|
Log-Group
|
||||||
|
|
||||||
if ( ( $BuildInstaller ) ) {
|
if ( ( $BuildInstaller ) ) {
|
||||||
@@ -89,6 +119,7 @@ function Package {
|
|||||||
Log-Information 'Creating InnoSetup installer...'
|
Log-Information 'Creating InnoSetup installer...'
|
||||||
Push-Location -Stack BuildTemp
|
Push-Location -Stack BuildTemp
|
||||||
Ensure-Location -Path "${ProjectRoot}/release"
|
Ensure-Location -Path "${ProjectRoot}/release"
|
||||||
|
Remove-Item -Path Package -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Copy-Item -Path ${Configuration} -Destination Package -Recurse
|
Copy-Item -Path ${Configuration} -Destination Package -Recurse
|
||||||
Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer"
|
Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer"
|
||||||
Remove-Item -Path Package -Recurse
|
Remove-Item -Path Package -Recurse
|
||||||
|
|||||||
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
|
brew bundle --file ${SCRIPT_HOME}/.Brewfile
|
||||||
|
rehash
|
||||||
log_group
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
- name: check_libobs_revision
|
- name: check_libobs_revision
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
mv ../*.deb .
|
mv ../*.deb .
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "obs-scene-switcher.deb"
|
name: "obs-scene-switcher.deb"
|
||||||
path: ${{ github.workspace }}/*.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"
|
description: "Project name detected by parsing build spec file"
|
||||||
value: ${{ jobs.check-event.outputs.pluginName }}
|
value: ${{ jobs.check-event.outputs.pluginName }}
|
||||||
env:
|
env:
|
||||||
DEP_DIR: .deps/advss-build-dependencies-4
|
DEP_DIR: .deps/advss-build-dependencies-5
|
||||||
jobs:
|
jobs:
|
||||||
check-event:
|
check-event:
|
||||||
name: Check GitHub Event Data 🔎
|
name: Check GitHub Event Data 🔎
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
commitHash: ${{ steps.setup.outputs.commitHash }}
|
commitHash: ${{ steps.setup.outputs.commitHash }}
|
||||||
pluginName: ${{ steps.setup.outputs.pluginName }}
|
pluginName: ${{ steps.setup.outputs.pluginName }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Check Event Data ☑️
|
- name: Check Event Data ☑️
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: zsh --no-rcs --errexit --pipefail {0}
|
shell: zsh --no-rcs --errexit --pipefail {0}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer
|
sudo xcode-select --switch /Applications/Xcode_16.1.0.app/Contents/Developer
|
||||||
print '::endgroup::'
|
print '::endgroup::'
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
id: ccache-cache
|
id: ccache-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/.ccache
|
path: ${{ github.workspace }}/.ccache
|
||||||
@@ -163,13 +163,13 @@ jobs:
|
|||||||
codesignPass: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
|
codesignPass: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload Artifacts 📡
|
- name: Upload Artifacts 📡
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}
|
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.*
|
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-macos-universal.*
|
||||||
|
|
||||||
- name: Upload Debug Symbol Artifacts 🪲
|
- name: Upload Debug Symbol Artifacts 🪲
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ needs.check-event.outputs.config == 'Release' }}
|
if: ${{ needs.check-event.outputs.config == 'Release' }}
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}-dSYMs
|
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-macos-universal-${{ needs.check-event.outputs.commitHash }}-dSYMs
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -202,7 +202,7 @@ jobs:
|
|||||||
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
|
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
|
||||||
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
|
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
id: ccache-cache
|
id: ccache-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/.ccache
|
path: ${{ github.workspace }}/.ccache
|
||||||
@@ -210,13 +210,8 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-ccache-x86_64-
|
${{ 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 🍺
|
- name: Set up Homebrew 🍺
|
||||||
uses: Homebrew/actions/setup-homebrew@master
|
uses: Homebrew/actions/setup-homebrew@main
|
||||||
|
|
||||||
- name: Build Plugin 🧱
|
- name: Build Plugin 🧱
|
||||||
uses: ./.github/actions/build-plugin
|
uses: ./.github/actions/build-plugin
|
||||||
@@ -238,23 +233,24 @@ jobs:
|
|||||||
config: ${{ needs.check-event.outputs.config }}
|
config: ${{ needs.check-event.outputs.config }}
|
||||||
|
|
||||||
- name: Upload Source Tarball 🗜️
|
- name: Upload Source Tarball 🗜️
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
|
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
|
||||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
|
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
|
||||||
|
|
||||||
- name: Upload Artifacts 📡
|
- name: Upload Artifacts 📡
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}
|
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*.*
|
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*.*
|
||||||
|
|
||||||
- name: Upload debug symbol artifacts 🪲
|
- name: Upload debug symbol artifacts 🪲
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ fromJSON(needs.check-event.outputs.package) }}
|
if: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-24.04-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
|
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
|
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
windows-build:
|
windows-build:
|
||||||
name: Build for Windows 🪟
|
name: Build for Windows 🪟
|
||||||
@@ -264,7 +260,7 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -311,7 +307,7 @@ jobs:
|
|||||||
package: ${{ fromJSON(needs.check-event.outputs.package) }}
|
package: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||||
|
|
||||||
- name: Upload Artifacts 📡
|
- name: Upload Artifacts 📡
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-windows-x64-${{ needs.check-event.outputs.commitHash }}
|
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*.*
|
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:
|
clang-format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: clang-format check 🐉
|
- name: clang-format check 🐉
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
cmake-format:
|
cmake-format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: cmake-format check 🎛️
|
- 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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check locale files
|
- name: Check locale files
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.github/workflows/push.yaml
vendored
4
.github/workflows/push.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Download Build Artifacts 📥
|
- name: Download Build Artifacts 📥
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
if: fromJSON(steps.check.outputs.validTag)
|
if: fromJSON(steps.check.outputs.validTag)
|
||||||
id: download
|
id: download
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
- name: Create Release 🛫
|
- name: Create Release 🛫
|
||||||
if: fromJSON(steps.check.outputs.validTag)
|
if: fromJSON(steps.check.outputs.validTag)
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@d4e8205d7e959a9107da6396278b2f1f07af0f9b
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }}
|
prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }}
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ target_sources(
|
|||||||
lib/macro/macro-dock-window.hpp
|
lib/macro/macro-dock-window.hpp
|
||||||
lib/macro/macro-edit.cpp
|
lib/macro/macro-edit.cpp
|
||||||
lib/macro/macro-edit.hpp
|
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.cpp
|
||||||
lib/macro/macro-export-import-dialog.hpp
|
lib/macro/macro-export-import-dialog.hpp
|
||||||
lib/macro/macro-helpers.cpp
|
lib/macro/macro-helpers.cpp
|
||||||
|
|||||||
37
build-aux/CI/windows/README.txt
Normal file
37
build-aux/CI/windows/README.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
Advanced Scene Switcher - Windows Installation
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
For full installation instructions visit:
|
||||||
|
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
|
||||||
|
|
||||||
|
|
||||||
|
Quick guide
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This archive contains two installation methods.
|
||||||
|
Choose ONE depending on your OBS version.
|
||||||
|
|
||||||
|
|
||||||
|
recommended\ (preferred)
|
||||||
|
---------------------------------------------
|
||||||
|
Extract the CONTENTS of this folder into:
|
||||||
|
|
||||||
|
C:\ProgramData\obs-studio\
|
||||||
|
|
||||||
|
Tip: press Win+R and type %ProgramData%\obs-studio\plugins to open that folder.
|
||||||
|
|
||||||
|
After extracting, the path should look like:
|
||||||
|
C:\ProgramData\obs-studio\plugins\advanced-scene-switcher\bin\64bit\advanced-scene-switcher.dll
|
||||||
|
|
||||||
|
|
||||||
|
legacy\ (older OBS versions, portable OBS installs, or if the recommended method does not work)
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
Extract the CONTENTS of this folder into your OBS installation directory,
|
||||||
|
typically:
|
||||||
|
|
||||||
|
C:\Program Files\obs-studio\
|
||||||
|
|
||||||
|
Merge the data\ and obs-plugins\ folders with the existing ones when prompted.
|
||||||
|
|
||||||
|
Steam users: right-click OBS in your library -> Manage -> Browse local files,
|
||||||
|
then extract the contents of legacy\ into that directory.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
if(BUILD_OUT_OF_TREE)
|
if(BUILD_OUT_OF_TREE)
|
||||||
if(OS_WINDOWS)
|
if(OS_WINDOWS)
|
||||||
set(OBS_PLUGIN_DESTINATION "obs-plugins/64bit")
|
set(OBS_PLUGIN_DESTINATION "${CMAKE_PROJECT_NAME}/bin/64bit")
|
||||||
else()
|
else()
|
||||||
set(OBS_PLUGIN_DESTINATION "${CMAKE_INSTALL_LIBDIR}/obs-plugins")
|
set(OBS_PLUGIN_DESTINATION "${CMAKE_INSTALL_LIBDIR}/obs-plugins")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ set(CPACK_PACKAGE_FILE_NAME
|
|||||||
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}"
|
"${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_SHLIBDEPS ON)
|
||||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
|
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
|
||||||
set(CPACK_SET_DESTDIR ON)
|
set(CPACK_SET_DESTDIR ON)
|
||||||
@@ -46,6 +48,19 @@ set(CPACK_SOURCE_PACKAGE_FILE_NAME
|
|||||||
set(CPACK_ARCHIVE_THREADS 0)
|
set(CPACK_ARCHIVE_THREADS 0)
|
||||||
|
|
||||||
include(CPack)
|
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)
|
find_package(libobs QUIET)
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,9 @@ include_guard(GLOBAL)
|
|||||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
|
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE)
|
||||||
|
|
||||||
include(buildspec)
|
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(
|
install(
|
||||||
TARGETS ${target}
|
TARGETS ${target}
|
||||||
RUNTIME DESTINATION bin/64bit
|
RUNTIME DESTINATION "${target}/bin/64bit"
|
||||||
LIBRARY DESTINATION obs-plugins/64bit)
|
LIBRARY DESTINATION "${target}/bin/64bit")
|
||||||
|
|
||||||
install(
|
install(
|
||||||
FILES "$<TARGET_PDB_FILE:${target}>"
|
FILES "$<TARGET_PDB_FILE:${target}>"
|
||||||
CONFIGURATIONS RelWithDebInfo Debug Release
|
CONFIGURATIONS RelWithDebInfo Debug Release
|
||||||
DESTINATION obs-plugins/64bit
|
DESTINATION "${target}/bin/64bit"
|
||||||
OPTIONAL)
|
OPTIONAL)
|
||||||
|
|
||||||
if(OBS_BUILD_DIR)
|
if(OBS_BUILD_DIR)
|
||||||
@@ -44,11 +44,11 @@ function(set_target_properties_plugin target)
|
|||||||
TARGET ${target}
|
TARGET ${target}
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
||||||
"${OBS_BUILD_DIR}/obs-plugins/64bit"
|
"${OBS_BUILD_DIR}/plugins/${target}/bin/64bit"
|
||||||
COMMAND
|
COMMAND
|
||||||
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>"
|
"${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:${target}>"
|
||||||
"$<$<CONFIG:Debug,RelWithDebInfo,Release>:$<TARGET_PDB_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}"
|
COMMENT "Copy ${target} to obs-studio directory ${OBS_BUILD_DIR}"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
endif()
|
endif()
|
||||||
@@ -105,7 +105,7 @@ function(target_install_resources target)
|
|||||||
|
|
||||||
install(
|
install(
|
||||||
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
|
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/"
|
||||||
DESTINATION data/obs-plugins/${target}
|
DESTINATION "${target}/data"
|
||||||
USE_SOURCE_PERMISSIONS)
|
USE_SOURCE_PERMISSIONS)
|
||||||
|
|
||||||
if(OBS_BUILD_DIR)
|
if(OBS_BUILD_DIR)
|
||||||
@@ -113,11 +113,11 @@ function(target_install_resources target)
|
|||||||
TARGET ${target}
|
TARGET ${target}
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
||||||
"${OBS_BUILD_DIR}/data/obs-plugins/${target}"
|
"${OBS_BUILD_DIR}/plugins/${target}/data"
|
||||||
COMMAND
|
COMMAND
|
||||||
"${CMAKE_COMMAND}" -E copy_directory
|
"${CMAKE_COMMAND}" -E copy_directory
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/data"
|
"${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"
|
COMMENT "Copy ${target} resources to data directory"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
endif()
|
endif()
|
||||||
@@ -133,7 +133,7 @@ function(target_add_resource target resource)
|
|||||||
|
|
||||||
install(
|
install(
|
||||||
FILES "${resource}"
|
FILES "${resource}"
|
||||||
DESTINATION data/obs-plugins/${target}
|
DESTINATION "${target}/data"
|
||||||
COMPONENT Runtime)
|
COMPONENT Runtime)
|
||||||
|
|
||||||
if(OBS_BUILD_DIR)
|
if(OBS_BUILD_DIR)
|
||||||
@@ -141,9 +141,9 @@ function(target_add_resource target resource)
|
|||||||
TARGET ${target}
|
TARGET ${target}
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND "${CMAKE_COMMAND}" -E make_directory
|
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}"
|
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"
|
COMMENT "Copy ${target} resource ${resource} to library directory"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
|
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
|
||||||
#define MyAppPublisher "@PLUGIN_AUTHOR@"
|
#define MyAppPublisher "@PLUGIN_AUTHOR@"
|
||||||
#define MyAppURL "@PLUGIN_WEBSITE@"
|
#define MyAppURL "@PLUGIN_WEBSITE@"
|
||||||
|
#define MyAppId "@UUID_APP@"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
; NOTE: The value of AppId uniquely identifies this application.
|
; NOTE: The value of AppId uniquely identifies this application.
|
||||||
; Do not use the same AppId value in installers for other applications.
|
; Do not use the same AppId value in installers for other applications.
|
||||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
AppId={{@UUID_APP@}
|
AppId={{{#MyAppId}}
|
||||||
AppName={#MyAppName}
|
AppName={#MyAppName}
|
||||||
AppVersion={#MyAppVersion}
|
AppVersion={#MyAppVersion}
|
||||||
AppPublisher={#MyAppPublisher}
|
AppPublisher={#MyAppPublisher}
|
||||||
@@ -20,8 +21,9 @@ OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer
|
|||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
DirExistsWarning=no
|
DirExistsWarning=no
|
||||||
|
UsePreviousAppDir=no
|
||||||
SetupIconFile=installer.ico
|
SetupIconFile=installer.ico
|
||||||
UninstallDisplayIcon={app}\data\obs-plugins\advanced-scene-switcher\res\images\logo.ico
|
UninstallDisplayIcon={app}\advanced-scene-switcher\data\res\images\logo.ico
|
||||||
DisableDirPage=no
|
DisableDirPage=no
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
@@ -51,17 +53,34 @@ begin
|
|||||||
);
|
);
|
||||||
end;
|
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;
|
function GetDirName(Value: string): string;
|
||||||
var
|
|
||||||
InstallPath: string;
|
|
||||||
begin
|
begin
|
||||||
// initialize default path, which will be returned when the following registry
|
Result := ExpandConstant('{commonappdata}\obs-studio\plugins');
|
||||||
// key queries fail due to missing keys or for some different reason
|
end;
|
||||||
Result := '{autopf}\obs-studio';
|
|
||||||
// query the first registry value; if this succeeds, return the obtained value
|
// Remove files placed by the legacy installer (obs-plugins/64bit and
|
||||||
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
|
// data/obs-plugins layout) so the plugin is not loaded twice by OBS.
|
||||||
Result := InstallPath
|
// We read InstallLocation from our own previous uninstall entry, which is
|
||||||
|
// the exact directory the old installer used regardless of OBS install path.
|
||||||
|
procedure RemoveLegacyFiles();
|
||||||
|
var
|
||||||
|
OldInstallPath: string;
|
||||||
|
UninstallKey: string;
|
||||||
|
begin
|
||||||
|
UninstallKey := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' + '{' + '{#MyAppId}' + '}_is1';
|
||||||
|
if not RegQueryStringValue(HKLM, UninstallKey, 'InstallLocation', OldInstallPath) then
|
||||||
|
Exit;
|
||||||
|
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.dll');
|
||||||
|
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher.pdb');
|
||||||
|
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.dll');
|
||||||
|
DeleteFile(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-lib.pdb');
|
||||||
|
DelTree(OldInstallPath + '\obs-plugins\64bit\advanced-scene-switcher-plugins', True, True, True);
|
||||||
|
DelTree(OldInstallPath + '\data\obs-plugins\advanced-scene-switcher', True, True, True);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
begin
|
||||||
|
if CurStep = ssInstall then
|
||||||
|
RemoveLegacyFiles();
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ AdvSceneSwitcher.macroList.deleted="gelöscht"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" ist bereits ausgewählt!"
|
||||||
|
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Eintrag ignorieren"
|
|
||||||
AdvSceneSwitcher.logic.and="Und"
|
AdvSceneSwitcher.logic.and="Und"
|
||||||
AdvSceneSwitcher.logic.or="Oder"
|
AdvSceneSwitcher.logic.or="Oder"
|
||||||
AdvSceneSwitcher.logic.andNot="Und nicht"
|
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.alwaysShowMacroSearch="Always show macro search"
|
||||||
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowFeatureTabs="Always show all feature tabs"
|
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.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.matchBehavior="Match behavior"
|
||||||
AdvSceneSwitcher.generalTab.priority="Priority"
|
AdvSceneSwitcher.generalTab.priority="Priority"
|
||||||
AdvSceneSwitcher.generalTab.priority.description="Switching methods priority (Highest priority is at the top)"
|
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="Export"
|
||||||
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
|
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.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="Import"
|
||||||
AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:"
|
AdvSceneSwitcher.macroTab.import.info="Paste the export string into the below text box to import macros:"
|
||||||
AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!"
|
AdvSceneSwitcher.macroTab.import.invalid="Invalid import data provided!"
|
||||||
@@ -306,7 +320,6 @@ AdvSceneSwitcher.macroList.deleted="deleted"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" is alreay selected!"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" is alreay selected!"
|
||||||
|
|
||||||
# Macro Logic
|
# Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Ignore entry"
|
|
||||||
AdvSceneSwitcher.logic.and="And"
|
AdvSceneSwitcher.logic.and="And"
|
||||||
AdvSceneSwitcher.logic.or="Or"
|
AdvSceneSwitcher.logic.or="Or"
|
||||||
AdvSceneSwitcher.logic.andNot="And not"
|
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.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="Game capture"
|
||||||
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
|
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
|
||||||
|
|
||||||
AdvSceneSwitcher.condition.screenshot="Screenshot"
|
AdvSceneSwitcher.condition.screenshot="Screenshot"
|
||||||
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
|
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
|
||||||
AdvSceneSwitcher.condition.mqtt="MQTT"
|
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.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.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.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="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.\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.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.regular="actions"
|
||||||
AdvSceneSwitcher.action.macro.type.run.actionType.else="else-actions"
|
AdvSceneSwitcher.action.macro.type.run.actionType.else="else-actions"
|
||||||
AdvSceneSwitcher.action.macro.type.run.skipWhenPaused="Skip execution when macro is paused"
|
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="Window title"
|
||||||
AdvSceneSwitcher.tempVar.window.window.description="The window title of the matching window."
|
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="Window class"
|
||||||
AdvSceneSwitcher.tempVar.window.windowClass.description="The window class of the matching window."
|
AdvSceneSwitcher.tempVar.window.windowClass.description="The window class of the matching window."
|
||||||
AdvSceneSwitcher.tempVar.window.windowText="Window text"
|
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.similarity.description="Values range from 0 to 1, where 0 means dissimilar and 1 means highly similar."
|
||||||
AdvSceneSwitcher.tempVar.video.patternCount="Pattern count"
|
AdvSceneSwitcher.tempVar.video.patternCount="Pattern count"
|
||||||
AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame."
|
AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame."
|
||||||
|
AdvSceneSwitcher.tempVar.video.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="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.objectCount.description="The number of objects the given model has identified in a given video input frame."
|
||||||
AdvSceneSwitcher.tempVar.video.brightness="Average brightness"
|
AdvSceneSwitcher.tempVar.video.brightness="Average brightness"
|
||||||
@@ -2538,6 +2568,9 @@ AdvSceneSwitcher.selectDisplay="--select display--"
|
|||||||
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
|
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
|
||||||
AdvSceneSwitcher.selectWindowTip="Use \"OBS\" to specify OBS window\nUse \"Task Switching\"to specify ALT + TAB"
|
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.invalid=" (Invalid)"
|
||||||
AdvSceneSwitcher.settings.suffix.type.bool=" (Bool)"
|
AdvSceneSwitcher.settings.suffix.type.bool=" (Bool)"
|
||||||
AdvSceneSwitcher.settings.suffix.type.int=" (Int)"
|
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.FPSInteger="Integer FPS Value"
|
||||||
Basic.Settings.Video.FPSFraction="Fractional 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 :)
|
# Legacy tabs below - please don't waste your time adding translations for these :)
|
||||||
# Transition Tab
|
# Transition Tab
|
||||||
AdvSceneSwitcher.transitionTab.title="Transition"
|
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"
|
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
|
||||||
|
|
||||||
; Lógica de macros
|
; Lógica de macros
|
||||||
AdvSceneSwitcher.logic.none="Omitir entrada"
|
|
||||||
AdvSceneSwitcher.logic.and="Y"
|
AdvSceneSwitcher.logic.and="Y"
|
||||||
AdvSceneSwitcher.logic.or="O"
|
AdvSceneSwitcher.logic.or="O"
|
||||||
AdvSceneSwitcher.logic.andNot="Y no"
|
AdvSceneSwitcher.logic.andNot="Y no"
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ AdvSceneSwitcher.macroList.deleted="supprimé"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" est déjà sélectionné !"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" est déjà sélectionné !"
|
||||||
|
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Ignorer l'entrée"
|
|
||||||
AdvSceneSwitcher.logic.and="Et"
|
AdvSceneSwitcher.logic.and="Et"
|
||||||
AdvSceneSwitcher.logic.or="Ou"
|
AdvSceneSwitcher.logic.or="Ou"
|
||||||
AdvSceneSwitcher.logic.andNot="Et pas"
|
AdvSceneSwitcher.logic.andNot="Et pas"
|
||||||
|
|||||||
@@ -275,7 +275,6 @@ AdvSceneSwitcher.macroList.deleted="削除"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" はすでに選択されています!"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" はすでに選択されています!"
|
||||||
|
|
||||||
# Macro Logic
|
# Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="入力無視"
|
|
||||||
; AdvSceneSwitcher.logic.and="And"
|
; AdvSceneSwitcher.logic.and="And"
|
||||||
; AdvSceneSwitcher.logic.or="Or"
|
; AdvSceneSwitcher.logic.or="Or"
|
||||||
; AdvSceneSwitcher.logic.andNot="And not"
|
; AdvSceneSwitcher.logic.andNot="And not"
|
||||||
|
|||||||
@@ -239,7 +239,6 @@ AdvSceneSwitcher.macroList.deleted="excluída"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" já está selecionada!"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" já está selecionada!"
|
||||||
|
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Ignorar entrada"
|
|
||||||
AdvSceneSwitcher.logic.and="E"
|
AdvSceneSwitcher.logic.and="E"
|
||||||
AdvSceneSwitcher.logic.or="Ou"
|
AdvSceneSwitcher.logic.or="Ou"
|
||||||
AdvSceneSwitcher.logic.andNot="E não"
|
AdvSceneSwitcher.logic.andNot="E não"
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ AdvSceneSwitcher.macroTab.name="Имя:"
|
|||||||
AdvSceneSwitcher.macroTab.defaultname="Макрос %1"
|
AdvSceneSwitcher.macroTab.defaultname="Макрос %1"
|
||||||
AdvSceneSwitcher.macroTab.copy="Создать копию"
|
AdvSceneSwitcher.macroTab.copy="Создать копию"
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Игнорировать вход"
|
|
||||||
AdvSceneSwitcher.logic.and="И"
|
AdvSceneSwitcher.logic.and="И"
|
||||||
AdvSceneSwitcher.logic.or="Или"
|
AdvSceneSwitcher.logic.or="Или"
|
||||||
AdvSceneSwitcher.logic.andNot="И не"
|
AdvSceneSwitcher.logic.andNot="И не"
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ AdvSceneSwitcher.macroTab.expandAll="Hepsini Genişlet"
|
|||||||
AdvSceneSwitcher.macroTab.collapseAll="Hepsini Küçült"
|
AdvSceneSwitcher.macroTab.collapseAll="Hepsini Küçült"
|
||||||
|
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="Girişi yoksay"
|
|
||||||
AdvSceneSwitcher.logic.and="Ve"
|
AdvSceneSwitcher.logic.and="Ve"
|
||||||
AdvSceneSwitcher.logic.or="Ya da"
|
AdvSceneSwitcher.logic.or="Ya da"
|
||||||
AdvSceneSwitcher.logic.andNot="ve değil"
|
AdvSceneSwitcher.logic.andNot="ve değil"
|
||||||
|
|||||||
@@ -259,7 +259,6 @@ AdvSceneSwitcher.macroList.deleted="删除"
|
|||||||
AdvSceneSwitcher.macroList.duplicate="\"%1\" 已选择!"
|
AdvSceneSwitcher.macroList.duplicate="\"%1\" 已选择!"
|
||||||
|
|
||||||
; Macro Logic
|
; Macro Logic
|
||||||
AdvSceneSwitcher.logic.none="忽略条件"
|
|
||||||
AdvSceneSwitcher.logic.and="且"
|
AdvSceneSwitcher.logic.and="且"
|
||||||
AdvSceneSwitcher.logic.or="或"
|
AdvSceneSwitcher.logic.or="或"
|
||||||
AdvSceneSwitcher.logic.andNot="且不"
|
AdvSceneSwitcher.logic.andNot="且不"
|
||||||
|
|||||||
2
deps/cpp-httplib
vendored
2
deps/cpp-httplib
vendored
Submodule deps/cpp-httplib updated: c7ed1796a7...811dd0b6f2
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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="settingsLock">
|
||||||
|
<property name="text">
|
||||||
|
<string>AdvSceneSwitcher.generalTab.generalBehavior.settingsLock</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
#include <obs-frontend-api.h>
|
#include <obs-frontend-api.h>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QLibrary>
|
#include <QLibrary>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
@@ -41,9 +43,12 @@ AdvSceneSwitcher::AdvSceneSwitcher(QWidget *parent)
|
|||||||
{
|
{
|
||||||
switcher->settingsWindowOpened = true;
|
switcher->settingsWindowOpened = true;
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
std::lock_guard<std::mutex> lock(switcher->m);
|
{
|
||||||
switcher->Prune();
|
std::lock_guard<std::mutex> lock(switcher->m);
|
||||||
LoadUI();
|
switcher->Prune();
|
||||||
|
LoadUI();
|
||||||
|
}
|
||||||
|
CheckFirstTimeSetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
AdvSceneSwitcher::~AdvSceneSwitcher()
|
AdvSceneSwitcher::~AdvSceneSwitcher()
|
||||||
@@ -139,7 +144,6 @@ void AdvSceneSwitcher::LoadUI()
|
|||||||
SetTabOrder(ui->tabWidget);
|
SetTabOrder(ui->tabWidget);
|
||||||
SetCurrentTab(ui->tabWidget);
|
SetCurrentTab(ui->tabWidget);
|
||||||
RestoreWindowGeo();
|
RestoreWindowGeo();
|
||||||
CheckFirstTimeSetup();
|
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -506,6 +510,8 @@ bool SwitcherData::AnySceneTransitionStarted()
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
extern "C" EXPORT void FreeSceneSwitcher()
|
extern "C" EXPORT void FreeSceneSwitcher()
|
||||||
{
|
{
|
||||||
|
switcher->Stop();
|
||||||
|
|
||||||
PlatformCleanup();
|
PlatformCleanup();
|
||||||
RunPluginCleanupSteps();
|
RunPluginCleanupSteps();
|
||||||
|
|
||||||
@@ -714,19 +720,54 @@ static void LoadPlugins()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool settingsPasswordIsCorrect(QWidget *parent)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
QString input = QInputDialog::getText(
|
||||||
|
parent,
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel"),
|
||||||
|
QLineEdit::Password, QString(), &ok);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.toStdString() == switcher->settingsLockPassword) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox::warning(
|
||||||
|
parent,
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void OpenSettingsWindow()
|
void OpenSettingsWindow()
|
||||||
{
|
{
|
||||||
if (switcher->settingsWindowOpened) {
|
if (switcher->settingsWindowOpened) {
|
||||||
AdvSceneSwitcher::window->show();
|
AdvSceneSwitcher::window->show();
|
||||||
AdvSceneSwitcher::window->raise();
|
AdvSceneSwitcher::window->raise();
|
||||||
AdvSceneSwitcher::window->activateWindow();
|
AdvSceneSwitcher::window->activateWindow();
|
||||||
} else {
|
return;
|
||||||
AdvSceneSwitcher::window =
|
|
||||||
new AdvSceneSwitcher(static_cast<QMainWindow *>(
|
|
||||||
obs_frontend_get_main_window()));
|
|
||||||
AdvSceneSwitcher::window->setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
AdvSceneSwitcher::window->show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
void AdvSceneSwitcher::HighlightMacroSettingsButton(bool enable)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public slots:
|
|||||||
void on_warnPluginLoadFailure_stateChanged(int state);
|
void on_warnPluginLoadFailure_stateChanged(int state);
|
||||||
void on_suppressCrashRecoveryDialog_stateChanged(int state);
|
void on_suppressCrashRecoveryDialog_stateChanged(int state);
|
||||||
void on_hideLegacyTabs_stateChanged(int state);
|
void on_hideLegacyTabs_stateChanged(int state);
|
||||||
|
void on_settingsLock_stateChanged(int state);
|
||||||
void on_priorityUp_clicked();
|
void on_priorityUp_clicked();
|
||||||
void on_priorityDown_clicked();
|
void on_priorityDown_clicked();
|
||||||
void on_threadPriority_currentTextChanged(const QString &text);
|
void on_threadPriority_currentTextChanged(const QString &text);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include <obs-frontend-api.h>
|
#include <obs-frontend-api.h>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -243,6 +245,64 @@ void AdvSceneSwitcher::on_hideLegacyTabs_stateChanged(int state)
|
|||||||
ui->priorityBox->setVisible(!switcher->hideLegacyTabs);
|
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()
|
void AdvSceneSwitcher::SetDeprecationWarnings()
|
||||||
{
|
{
|
||||||
QString toolTip =
|
QString toolTip =
|
||||||
@@ -391,12 +451,16 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto macro = FirstRunWizard::ShowWizard(this);
|
bool wasSkipped = false;
|
||||||
|
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
|
||||||
if (macro) {
|
if (macro) {
|
||||||
renameMacroIfNecessary(macro);
|
renameMacroIfNecessary(macro);
|
||||||
QTimer::singleShot(0, this,
|
QTimer::singleShot(0, this,
|
||||||
[this, macro]() { ui->macros->Add(macro); });
|
[this, macro]() { ui->macros->Add(macro); });
|
||||||
}
|
}
|
||||||
|
if (wasSkipped) {
|
||||||
|
ui->alwaysShowFeatureTabs->setChecked(true);
|
||||||
|
}
|
||||||
switcher->Start();
|
switcher->Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +627,9 @@ void SwitcherData::SaveGeneralSettings(obs_data_t *obj)
|
|||||||
disableFilterComboboxFilter);
|
disableFilterComboboxFilter);
|
||||||
obs_data_set_bool(obj, "warnPluginLoadFailure", warnPluginLoadFailure);
|
obs_data_set_bool(obj, "warnPluginLoadFailure", warnPluginLoadFailure);
|
||||||
obs_data_set_bool(obj, "hideLegacyTabs", hideLegacyTabs);
|
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);
|
SaveFunctionPriorities(obj, functionNamesByPriority);
|
||||||
|
|
||||||
@@ -634,6 +701,8 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
|
|||||||
warnPluginLoadFailure = obs_data_get_bool(obj, "warnPluginLoadFailure");
|
warnPluginLoadFailure = obs_data_get_bool(obj, "warnPluginLoadFailure");
|
||||||
obs_data_set_default_bool(obj, "hideLegacyTabs", true);
|
obs_data_set_default_bool(obj, "hideLegacyTabs", true);
|
||||||
hideLegacyTabs = obs_data_get_bool(obj, "hideLegacyTabs");
|
hideLegacyTabs = obs_data_get_bool(obj, "hideLegacyTabs");
|
||||||
|
settingsLockEnabled = obs_data_get_bool(obj, "settingsLockEnabled");
|
||||||
|
settingsLockPassword = obs_data_get_string(obj, "settingsLockPassword");
|
||||||
|
|
||||||
SetDefaultFunctionPriorities(obj);
|
SetDefaultFunctionPriorities(obj);
|
||||||
LoadFunctionPriorities(obj, functionNamesByPriority);
|
LoadFunctionPriorities(obj, functionNamesByPriority);
|
||||||
@@ -956,6 +1025,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
|
|||||||
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
|
||||||
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
|
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
|
||||||
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
|
||||||
|
ui->settingsLock->setChecked(switcher->settingsLockEnabled);
|
||||||
|
|
||||||
populatePriorityFunctionList(ui->priorityList);
|
populatePriorityFunctionList(ui->priorityList);
|
||||||
populateThreadPriorityList(ui->threadPriority);
|
populateThreadPriorityList(ui->threadPriority);
|
||||||
|
|||||||
@@ -178,47 +178,19 @@ void AdvSceneSwitcher::on_ignoreWindows_currentRowChanged(int idx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkWindowTitleSwitchDirect(WindowSwitch &s,
|
static bool windowInfoMatchesSwitch(const WindowInfo &info,
|
||||||
std::string ¤tWindowTitle, bool &match,
|
const WindowSwitch &s)
|
||||||
OBSWeakSource &scene,
|
|
||||||
OBSWeakSource &transition)
|
|
||||||
{
|
{
|
||||||
bool focus = (!s.focus || s.window == currentWindowTitle);
|
if (s.focus && !info.focused) {
|
||||||
bool fullscreen = (!s.fullscreen || IsFullscreen(s.window));
|
return false;
|
||||||
bool max = (!s.maximized || IsMaximized(s.window));
|
|
||||||
|
|
||||||
if (focus && fullscreen && max) {
|
|
||||||
match = true;
|
|
||||||
scene = s.getScene();
|
|
||||||
transition = s.transition;
|
|
||||||
}
|
}
|
||||||
}
|
if (s.fullscreen && !info.fullscreen) {
|
||||||
|
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.maximized && !info.maximized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
|
bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
|
||||||
@@ -228,23 +200,37 @@ bool SwitcherData::checkWindowTitleSwitch(OBSWeakSource &scene,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string currentWindowTitle = switcher->currentTitle;
|
|
||||||
bool match = false;
|
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) {
|
for (WindowSwitch &s : windowSwitches) {
|
||||||
if (!s.initialized()) {
|
if (!s.initialized()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::find(windowList.begin(), windowList.end(), s.window) !=
|
for (const auto &info : windows) {
|
||||||
windowList.end()) {
|
bool titleMatch = false;
|
||||||
checkWindowTitleSwitchDirect(s, currentWindowTitle,
|
try {
|
||||||
match, scene, transition);
|
std::regex expr(s.window);
|
||||||
} else {
|
titleMatch = info.title == s.window ||
|
||||||
checkWindowTitleSwitchRegex(s, currentWindowTitle,
|
std::regex_match(info.title, expr);
|
||||||
windowList, match, scene,
|
} catch (const std::regex_error &) {
|
||||||
transition);
|
titleMatch = info.title == s.window;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!titleMatch || !windowInfoMatchesSwitch(info, s)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = true;
|
||||||
|
scene = s.getScene();
|
||||||
|
transition = s.transition;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|||||||
@@ -236,17 +236,65 @@ std::string getWindowName(Window window)
|
|||||||
return windowTitle;
|
return windowTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> GetWindowList()
|
std::vector<WindowInfo> GetWindows(const WindowQueryOptions &options)
|
||||||
{
|
{
|
||||||
std::vector<std::string> windows;
|
const std::string foregroundTitle = GetCurrentWindowTitle();
|
||||||
for (auto window : getTopLevelWindows()) {
|
const auto topLevel = getTopLevelWindows();
|
||||||
|
auto display = disp();
|
||||||
|
|
||||||
|
std::vector<WindowInfo> result;
|
||||||
|
result.reserve(topLevel.size());
|
||||||
|
|
||||||
|
for (auto window : topLevel) {
|
||||||
auto name = getWindowName(window);
|
auto name = getWindowName(window);
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
continue;
|
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;
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getActiveWindow(Window *&window)
|
int getActiveWindow(Window *&window)
|
||||||
@@ -290,68 +338,6 @@ std::string GetCurrentWindowTitle()
|
|||||||
return name;
|
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)
|
static void getProcessListProcps(QStringList &processes)
|
||||||
{
|
{
|
||||||
#ifdef PROCPS_AVAILABLE
|
#ifdef PROCPS_AVAILABLE
|
||||||
|
|||||||
@@ -871,6 +871,8 @@ void MacroActionMacroEdit::SetWidgetVisibility()
|
|||||||
} else {
|
} else {
|
||||||
_noConditionsWarning->setVisible(false);
|
_noConditionsWarning->setVisible(false);
|
||||||
}
|
}
|
||||||
|
_runMacroHelp->setVisible(action ==
|
||||||
|
MacroActionMacro::Action::RUN_MACRO);
|
||||||
|
|
||||||
_nestedMacro->setVisible(action ==
|
_nestedMacro->setVisible(action ==
|
||||||
MacroActionMacro::Action::NESTED_MACRO);
|
MacroActionMacro::Action::NESTED_MACRO);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#include "macro-condition-edit.hpp"
|
#include "macro-condition-edit.hpp"
|
||||||
#include "advanced-scene-switcher.hpp"
|
#include "advanced-scene-switcher.hpp"
|
||||||
|
#include "macro-signals.hpp"
|
||||||
#include "macro-settings.hpp"
|
#include "macro-settings.hpp"
|
||||||
#include "macro.hpp"
|
#include "macro.hpp"
|
||||||
#include "path-helpers.hpp"
|
#include "path-helpers.hpp"
|
||||||
#include "plugin-state-helpers.hpp"
|
#include "plugin-state-helpers.hpp"
|
||||||
#include "section.hpp"
|
#include "section.hpp"
|
||||||
|
#include "switch-button.hpp"
|
||||||
#include "ui-helpers.hpp"
|
#include "ui-helpers.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
|
|
||||||
@@ -102,12 +104,15 @@ MacroConditionEdit::MacroConditionEdit(
|
|||||||
QWidget *parent, std::shared_ptr<MacroCondition> *entryData,
|
QWidget *parent, std::shared_ptr<MacroCondition> *entryData,
|
||||||
bool isRootCondition)
|
bool isRootCondition)
|
||||||
: MacroSegmentEdit(parent),
|
: MacroSegmentEdit(parent),
|
||||||
|
_enable(new SwitchButton()),
|
||||||
_logicSelection(new QComboBox()),
|
_logicSelection(new QComboBox()),
|
||||||
_conditionSelection(new FilterComboBox()),
|
_conditionSelection(new FilterComboBox()),
|
||||||
_dur(new DurationModifierEdit()),
|
_dur(new DurationModifierEdit()),
|
||||||
_entryData(entryData),
|
_entryData(entryData),
|
||||||
_isRoot(isRootCondition)
|
_isRoot(isRootCondition)
|
||||||
{
|
{
|
||||||
|
QWidget::connect(_enable, SIGNAL(checked(bool)), this,
|
||||||
|
SLOT(ConditionEnableChanged(bool)));
|
||||||
QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)),
|
QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)),
|
||||||
this, SLOT(LogicSelectionChanged(int)));
|
this, SLOT(LogicSelectionChanged(int)));
|
||||||
QWidget::connect(_conditionSelection,
|
QWidget::connect(_conditionSelection,
|
||||||
@@ -122,6 +127,7 @@ MacroConditionEdit::MacroConditionEdit(
|
|||||||
Logic::PopulateLogicTypeSelection(_logicSelection, isRootCondition);
|
Logic::PopulateLogicTypeSelection(_logicSelection, isRootCondition);
|
||||||
populateConditionSelection(_conditionSelection);
|
populateConditionSelection(_conditionSelection);
|
||||||
|
|
||||||
|
_section->AddHeaderWidget(_enable);
|
||||||
_section->AddHeaderWidget(_logicSelection);
|
_section->AddHeaderWidget(_logicSelection);
|
||||||
_section->AddHeaderWidget(_conditionSelection);
|
_section->AddHeaderWidget(_conditionSelection);
|
||||||
_section->AddHeaderWidget(_headerInfo);
|
_section->AddHeaderWidget(_headerInfo);
|
||||||
@@ -154,8 +160,17 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
|
|||||||
const auto logic = static_cast<Logic::Type>(
|
const auto logic = static_cast<Logic::Type>(
|
||||||
_logicSelection->itemData(idx).toInt());
|
_logicSelection->itemData(idx).toInt());
|
||||||
(*_entryData)->SetLogicType(logic);
|
(*_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
|
bool MacroConditionEdit::IsRootNode() const
|
||||||
@@ -168,7 +183,9 @@ void MacroConditionEdit::SetLogicSelection()
|
|||||||
const auto logic = (*_entryData)->GetLogicType();
|
const auto logic = (*_entryData)->GetLogicType();
|
||||||
_logicSelection->setCurrentIndex(
|
_logicSelection->setCurrentIndex(
|
||||||
_logicSelection->findData(static_cast<int>(logic)));
|
_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)
|
void MacroConditionEdit::SetRootNode(bool root)
|
||||||
@@ -239,10 +256,12 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
|
|||||||
{
|
{
|
||||||
auto lock = LockContext();
|
auto lock = LockContext();
|
||||||
auto logic = (*_entryData)->GetLogicType();
|
auto logic = (*_entryData)->GetLogicType();
|
||||||
|
const bool enabled = (*_entryData)->Enabled();
|
||||||
_entryData->reset();
|
_entryData->reset();
|
||||||
*_entryData = MacroConditionFactory::Create(id, macro);
|
*_entryData = MacroConditionFactory::Create(id, macro);
|
||||||
(*_entryData)->SetIndex(idx);
|
(*_entryData)->SetIndex(idx);
|
||||||
(*_entryData)->SetLogicType(logic);
|
(*_entryData)->SetLogicType(logic);
|
||||||
|
(*_entryData)->SetEnabled(enabled);
|
||||||
(*_entryData)->PostLoad();
|
(*_entryData)->PostLoad();
|
||||||
RunAndClearPostLoadSteps();
|
RunAndClearPostLoadSteps();
|
||||||
}
|
}
|
||||||
@@ -256,6 +275,9 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
|
|||||||
SetupVarMappings((*_entryData).get());
|
SetupVarMappings((*_entryData).get());
|
||||||
_dur->setVisible(MacroConditionFactory::UsesDurationModifier(id));
|
_dur->setVisible(MacroConditionFactory::UsesDurationModifier(id));
|
||||||
SetFocusPolicyOfWidgets();
|
SetFocusPolicyOfWidgets();
|
||||||
|
|
||||||
|
emit MacroSignalManager::Instance()->ConditionTypeCreated(
|
||||||
|
QString::fromStdString(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroConditionEdit::DurationChanged(const Duration &seconds)
|
void MacroConditionEdit::DurationChanged(const Duration &seconds)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
|
class SwitchButton;
|
||||||
|
|
||||||
class DurationModifierEdit : public QWidget {
|
class DurationModifierEdit : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@@ -47,11 +49,13 @@ private slots:
|
|||||||
void ConditionSelectionChanged(const QString &text);
|
void ConditionSelectionChanged(const QString &text);
|
||||||
void DurationChanged(const Duration &value);
|
void DurationChanged(const Duration &value);
|
||||||
void DurationModifierChanged(DurationModifier::Type m);
|
void DurationModifierChanged(DurationModifier::Type m);
|
||||||
|
void ConditionEnableChanged(bool);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetLogicSelection();
|
void SetLogicSelection();
|
||||||
std::shared_ptr<MacroSegment> Data() const;
|
std::shared_ptr<MacroSegment> Data() const;
|
||||||
|
|
||||||
|
SwitchButton *_enable;
|
||||||
QComboBox *_logicSelection;
|
QComboBox *_logicSelection;
|
||||||
FilterComboBox *_conditionSelection;
|
FilterComboBox *_conditionSelection;
|
||||||
DurationModifierEdit *_dur;
|
DurationModifierEdit *_dur;
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ bool MacroCondition::Load(obs_data_t *obj)
|
|||||||
{
|
{
|
||||||
MacroSegment::Load(obj);
|
MacroSegment::Load(obj);
|
||||||
_logic.Load(obj, "logic");
|
_logic.Load(obj, "logic");
|
||||||
|
if (_logic.GetType() == Logic::Type::NONE) {
|
||||||
|
SetEnabled(false);
|
||||||
|
_logic.SetType(Logic::Type::AND);
|
||||||
|
}
|
||||||
_durationModifier.Load(obj);
|
_durationModifier.Load(obj);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,8 @@ void MacroCondition::ValidateLogicSelection(bool isRootCondition,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logic.SetType(Logic::Type::NONE);
|
_logic.SetType(Logic::Type::AND);
|
||||||
|
SetEnabled(false);
|
||||||
blog(LOG_WARNING,
|
blog(LOG_WARNING,
|
||||||
"setting invalid logic selection to 'ignore' for macro %s",
|
"setting invalid logic selection to 'ignore' for macro %s",
|
||||||
context);
|
context);
|
||||||
|
|||||||
@@ -1118,6 +1118,9 @@ void MacroEdit::AddMacroAction(int idx)
|
|||||||
macro->Actions().at(idx - 1)->Save(data);
|
macro->Actions().at(idx - 1)->Save(data);
|
||||||
}
|
}
|
||||||
AddMacroAction(macro.get(), idx, id, data);
|
AddMacroAction(macro.get(), idx, id, data);
|
||||||
|
if (idx < (int)macro->Actions().size()) {
|
||||||
|
macro->Actions().at(idx)->FixupVarMappingRefs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroEdit::on_actionAdd_clicked()
|
void MacroEdit::on_actionAdd_clicked()
|
||||||
@@ -1426,6 +1429,9 @@ void MacroEdit::AddMacroElseAction(int idx)
|
|||||||
macro->ElseActions().at(idx - 1)->Save(data);
|
macro->ElseActions().at(idx - 1)->Save(data);
|
||||||
}
|
}
|
||||||
AddMacroElseAction(macro.get(), idx, id, data);
|
AddMacroElseAction(macro.get(), idx, id, data);
|
||||||
|
if (idx < (int)macro->ElseActions().size()) {
|
||||||
|
macro->ElseActions().at(idx)->FixupVarMappingRefs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroEdit::RemoveMacroElseAction(int idx)
|
void MacroEdit::RemoveMacroElseAction(int idx)
|
||||||
@@ -1570,6 +1576,9 @@ void MacroEdit::AddMacroCondition(int idx)
|
|||||||
macro->Conditions().at(idx - 1)->Save(data);
|
macro->Conditions().at(idx - 1)->Save(data);
|
||||||
}
|
}
|
||||||
AddMacroCondition(macro.get(), idx, id, data.Get(), logic);
|
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,
|
void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
|
||||||
|
|||||||
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,160 @@
|
|||||||
#include "macro-export-import-dialog.hpp"
|
#include "macro-export-import-dialog.hpp"
|
||||||
|
#include "macro-export-extensions.hpp"
|
||||||
#include "obs-module-helper.hpp"
|
#include "obs-module-helper.hpp"
|
||||||
|
|
||||||
#include <obs.hpp>
|
#include <obs.hpp>
|
||||||
#include <QLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
static bool usePlainText = false;
|
static bool usePlainText = false;
|
||||||
|
|
||||||
MacroExportImportDialog::MacroExportImportDialog(Type type)
|
// ---------------------------------------------------------------------------
|
||||||
|
// Compress / decompress helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static QString compressMacroString(const QString &input)
|
||||||
|
{
|
||||||
|
QByteArray inputData = input.toUtf8();
|
||||||
|
auto compressedData = qCompress(inputData);
|
||||||
|
QByteArray encodedData = compressedData.toBase64();
|
||||||
|
return QString::fromUtf8(encodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString decompressMacroString(const QString &input)
|
||||||
|
{
|
||||||
|
QByteArray encodedData = input.toUtf8();
|
||||||
|
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
||||||
|
auto outputData = qUncompress(compressedData);
|
||||||
|
return QString::fromUtf8(outputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isValidData(const QString &json)
|
||||||
|
{
|
||||||
|
OBSDataAutoRelease data =
|
||||||
|
obs_data_create_from_json(json.toStdString().c_str());
|
||||||
|
return !!data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extension section
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Section *MacroExportImportDialog::BuildExtensionSection()
|
||||||
|
{
|
||||||
|
auto outerSection = new Section(300, this);
|
||||||
|
outerSection->AddHeaderWidget(new QLabel(obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroTab.export.additionalContent")));
|
||||||
|
|
||||||
|
auto outerContent = new QWidget();
|
||||||
|
auto outerLayout = new QVBoxLayout(outerContent);
|
||||||
|
outerLayout->setContentsMargins(4, 4, 4, 4);
|
||||||
|
outerLayout->setSpacing(4);
|
||||||
|
|
||||||
|
const auto &extensions = GetMacroExportExtensions();
|
||||||
|
|
||||||
|
// Build a sorted index list (alphabetical by translated display name).
|
||||||
|
std::vector<int> order(extensions.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
||||||
|
return QString::localeAwareCompare(
|
||||||
|
obs_module_text(extensions[a].displayNameKey),
|
||||||
|
obs_module_text(extensions[b].displayNameKey)) <
|
||||||
|
0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int extIdx : order) {
|
||||||
|
const auto &ext = extensions[extIdx];
|
||||||
|
ExtensionUI ui;
|
||||||
|
|
||||||
|
// Simple all-or-nothing checkbox (no sub-items).
|
||||||
|
if (!ext.getExportItems) {
|
||||||
|
ui.mainCheck = new QCheckBox(
|
||||||
|
obs_module_text(ext.displayNameKey),
|
||||||
|
outerContent);
|
||||||
|
ui.mainCheck->setChecked(false);
|
||||||
|
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||||
|
&MacroExportImportDialog::UpdateExportString);
|
||||||
|
outerLayout->addWidget(ui.mainCheck);
|
||||||
|
_extensionUIs.append(std::move(ui));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions with per-item selection get their own inner
|
||||||
|
// Section: the main checkbox sits in the header (next to the
|
||||||
|
// toggle arrow) and the per-item checkboxes live in the
|
||||||
|
// collapsible content area.
|
||||||
|
const auto items = ext.getExportItems();
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto innerSection = new Section(200, outerContent);
|
||||||
|
|
||||||
|
ui.mainCheck =
|
||||||
|
new QCheckBox(obs_module_text(ext.displayNameKey));
|
||||||
|
ui.mainCheck->setChecked(false);
|
||||||
|
innerSection->AddHeaderWidget(ui.mainCheck);
|
||||||
|
|
||||||
|
auto subWidget = new QWidget();
|
||||||
|
auto subLayout = new QVBoxLayout(subWidget);
|
||||||
|
subLayout->setContentsMargins(4, 2, 4, 2);
|
||||||
|
subLayout->setSpacing(2);
|
||||||
|
|
||||||
|
for (const auto &[id, displayName] : items) {
|
||||||
|
auto cb = new QCheckBox(displayName, subWidget);
|
||||||
|
cb->setChecked(false);
|
||||||
|
subLayout->addWidget(cb);
|
||||||
|
ui.itemChecks.append({id, cb});
|
||||||
|
connect(cb, &QCheckBox::stateChanged, this,
|
||||||
|
&MacroExportImportDialog::UpdateExportString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main checkbox toggles all sub-items.
|
||||||
|
// Capture uiIdx (position in _extensionUIs after append).
|
||||||
|
const int uiIdx = _extensionUIs.size();
|
||||||
|
connect(ui.mainCheck, &QCheckBox::stateChanged, this,
|
||||||
|
[this, uiIdx](int state) {
|
||||||
|
if (state == Qt::PartiallyChecked)
|
||||||
|
return;
|
||||||
|
const bool checked = (state == Qt::Checked);
|
||||||
|
if (uiIdx < _extensionUIs.size()) {
|
||||||
|
for (auto &[id, cb] :
|
||||||
|
_extensionUIs[uiIdx].itemChecks) {
|
||||||
|
QSignalBlocker b(cb);
|
||||||
|
cb->setChecked(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateExportString();
|
||||||
|
});
|
||||||
|
|
||||||
|
innerSection->SetContent(subWidget, false);
|
||||||
|
outerLayout->addWidget(innerSection);
|
||||||
|
_extensionUIs.append(std::move(ui));
|
||||||
|
}
|
||||||
|
|
||||||
|
outerSection->SetContent(outerContent, true);
|
||||||
|
return outerSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MacroExportImportDialog::MacroExportImportDialog(Type type,
|
||||||
|
const QString &baseJson)
|
||||||
: QDialog(nullptr),
|
: QDialog(nullptr),
|
||||||
|
_baseJson(baseJson),
|
||||||
_importExportString(new QPlainTextEdit(this)),
|
_importExportString(new QPlainTextEdit(this)),
|
||||||
_usePlainText(new QCheckBox(obs_module_text(
|
_usePlainText(new QCheckBox(obs_module_text(
|
||||||
"AdvSceneSwitcher.macroTab.export.usePlainText")))
|
"AdvSceneSwitcher.macroTab.export.usePlainText")))
|
||||||
{
|
{
|
||||||
_importExportString->setReadOnly(type == Type::EXPORT_MACRO);
|
_importExportString->setReadOnly(type == Type::EXPORT_MACRO);
|
||||||
|
|
||||||
auto label = new QLabel(obs_module_text(
|
auto label = new QLabel(obs_module_text(
|
||||||
type == Type::EXPORT_MACRO
|
type == Type::EXPORT_MACRO
|
||||||
? "AdvSceneSwitcher.macroTab.export.info"
|
? "AdvSceneSwitcher.macroTab.export.info"
|
||||||
@@ -39,7 +177,12 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
|
|||||||
connect(_usePlainText, &QCheckBox::stateChanged, this,
|
connect(_usePlainText, &QCheckBox::stateChanged, this,
|
||||||
&MacroExportImportDialog::UsePlainTextChanged);
|
&MacroExportImportDialog::UsePlainTextChanged);
|
||||||
|
|
||||||
auto layout = new QVBoxLayout;
|
auto layout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
if (type == Type::EXPORT_MACRO && !GetMacroExportExtensions().empty()) {
|
||||||
|
layout->addWidget(BuildExtensionSection());
|
||||||
|
}
|
||||||
|
|
||||||
layout->addWidget(label);
|
layout->addWidget(label);
|
||||||
layout->addWidget(_importExportString);
|
layout->addWidget(_importExportString);
|
||||||
layout->addWidget(_usePlainText);
|
layout->addWidget(_usePlainText);
|
||||||
@@ -47,32 +190,57 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
|
|||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
||||||
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
|
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();
|
OBSDataAutoRelease data =
|
||||||
auto compressedData = qCompress(inputData);
|
obs_data_create_from_json(_baseJson.toStdString().c_str());
|
||||||
QByteArray encodedData = compressedData.toBase64();
|
if (!data) {
|
||||||
return QString::fromUtf8(encodedData);
|
return _baseJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &extensions = GetMacroExportExtensions();
|
||||||
|
for (int i = 0;
|
||||||
|
i < (int)extensions.size() && i < (int)_extensionUIs.size(); ++i) {
|
||||||
|
if (!_extensionUIs[i].mainCheck->isChecked()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList selectedIds;
|
||||||
|
for (const auto &[id, cb] : _extensionUIs[i].itemChecks) {
|
||||||
|
if (cb->isChecked()) {
|
||||||
|
selectedIds << id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extensions[i].save(data, selectedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *json = obs_data_get_json(data);
|
||||||
|
return json ? QString::fromUtf8(json) : QString{};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString decompressMacroString(const QString &input)
|
void MacroExportImportDialog::RefreshExportText()
|
||||||
{
|
{
|
||||||
QByteArray encodedData = input.toUtf8();
|
const QString json = BuildExportJson();
|
||||||
QByteArray compressedData = QByteArray::fromBase64(encodedData);
|
if (usePlainText) {
|
||||||
auto outputData = qUncompress(compressedData);
|
_importExportString->setPlainText(json);
|
||||||
return QString::fromUtf8(outputData);
|
} else {
|
||||||
|
_importExportString->setPlainText(compressMacroString(json));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroExportImportDialog::ExportMacros(const QString &json)
|
void MacroExportImportDialog::UpdateExportString()
|
||||||
{
|
{
|
||||||
MacroExportImportDialog dialog(
|
RefreshExportText();
|
||||||
MacroExportImportDialog::Type::EXPORT_MACRO);
|
|
||||||
dialog._importExportString->setPlainText(compressMacroString(json));
|
|
||||||
dialog.adjustSize();
|
|
||||||
dialog.updateGeometry();
|
|
||||||
dialog.exec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroExportImportDialog::UsePlainTextChanged(int value)
|
void MacroExportImportDialog::UsePlainTextChanged(int value)
|
||||||
@@ -90,26 +258,45 @@ void MacroExportImportDialog::UsePlainTextChanged(int value)
|
|||||||
usePlainText = value;
|
usePlainText = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isValidData(const QString &json)
|
// ---------------------------------------------------------------------------
|
||||||
|
// Static entry points
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void MacroExportImportDialog::ExportMacros(obs_data_t *macroData)
|
||||||
{
|
{
|
||||||
OBSDataAutoRelease data =
|
const char *rawJson = obs_data_get_json(macroData);
|
||||||
obs_data_create_from_json(json.toStdString().c_str());
|
const QString baseJson = rawJson ? QString::fromUtf8(rawJson)
|
||||||
return !!data;
|
: QString{};
|
||||||
|
|
||||||
|
MacroExportImportDialog dialog(Type::EXPORT_MACRO, baseJson);
|
||||||
|
dialog.adjustSize();
|
||||||
|
dialog.updateGeometry();
|
||||||
|
dialog.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MacroExportImportDialog::ImportMacros(QString &json)
|
bool MacroExportImportDialog::ImportMacros(QString &json)
|
||||||
{
|
{
|
||||||
MacroExportImportDialog dialog(
|
MacroExportImportDialog dialog(Type::IMPORT_MACRO);
|
||||||
MacroExportImportDialog::Type::IMPORT_MACRO);
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
return false;
|
||||||
json = decompressMacroString(
|
|
||||||
dialog._importExportString->toPlainText());
|
|
||||||
if (!isValidData(json)) { // Fallback to support raw json format
|
|
||||||
json = dialog._importExportString->toPlainText();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
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
|
} // namespace advss
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "section.hpp"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QList>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
|
|
||||||
|
#include <obs-data.h>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
class MacroExportImportDialog : public QDialog {
|
class MacroExportImportDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum class Type { EXPORT_MACRO, IMPORT_MACRO };
|
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);
|
static bool ImportMacros(QString &json);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void UsePlainTextChanged(int);
|
void UsePlainTextChanged(int);
|
||||||
|
void UpdateExportString();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
explicit MacroExportImportDialog(Type type,
|
||||||
|
const QString &baseJson = {});
|
||||||
|
|
||||||
|
// Per-extension widgets
|
||||||
|
struct ExtensionUI {
|
||||||
|
QCheckBox *mainCheck = nullptr;
|
||||||
|
// (itemId, checkbox) pairs - empty when no per-item selection.
|
||||||
|
QList<QPair<QString, QCheckBox *>> itemChecks;
|
||||||
|
};
|
||||||
|
|
||||||
|
Section *BuildExtensionSection();
|
||||||
|
QString BuildExportJson() const;
|
||||||
|
void RefreshExportText();
|
||||||
|
|
||||||
|
QString _baseJson;
|
||||||
QPlainTextEdit *_importExportString;
|
QPlainTextEdit *_importExportString;
|
||||||
QCheckBox *_usePlainText;
|
QCheckBox *_usePlainText;
|
||||||
|
QList<ExtensionUI> _extensionUIs;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -214,6 +214,20 @@ bool MacroIsPaused(const Macro *macro)
|
|||||||
return macro ? macro->Paused() : true;
|
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(
|
bool MacroWasPausedSince(
|
||||||
const Macro *macro,
|
const Macro *macro,
|
||||||
const std::chrono::high_resolution_clock::time_point &time)
|
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));
|
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)
|
void ResetMacroConditionTimers(Macro *macro)
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ LastMacroConditionCheckTime(const Macro *);
|
|||||||
|
|
||||||
EXPORT bool MacroIsStopped(const Macro *);
|
EXPORT bool MacroIsStopped(const Macro *);
|
||||||
EXPORT bool MacroIsPaused(const Macro *);
|
EXPORT bool MacroIsPaused(const Macro *);
|
||||||
|
EXPORT void SetMacroPaused(Macro *, bool paused);
|
||||||
|
EXPORT void StopMacro(Macro *);
|
||||||
EXPORT bool
|
EXPORT bool
|
||||||
MacroWasPausedSince(const Macro *,
|
MacroWasPausedSince(const Macro *,
|
||||||
const std::chrono::high_resolution_clock::time_point &);
|
const std::chrono::high_resolution_clock::time_point &);
|
||||||
@@ -66,10 +68,14 @@ EXPORT bool MacroWasCheckedSinceLastStart(const Macro *);
|
|||||||
EXPORT void AddMacroHelperThread(Macro *, std::thread &&);
|
EXPORT void AddMacroHelperThread(Macro *, std::thread &&);
|
||||||
|
|
||||||
EXPORT bool CheckMacros();
|
EXPORT bool CheckMacros();
|
||||||
|
EXPORT bool CheckMacroConditions(Macro *, bool ignorePause = false);
|
||||||
|
|
||||||
EXPORT bool RunMacroActions(Macro *);
|
EXPORT bool RunMacroActions(Macro *, bool forceParallel = false,
|
||||||
bool RunMacroElseActions(Macro *);
|
bool ignorePause = false);
|
||||||
|
EXPORT bool RunMacroElseActions(Macro *, bool forceParallel = false,
|
||||||
|
bool ignorePause = false);
|
||||||
EXPORT bool RunMacros();
|
EXPORT bool RunMacros();
|
||||||
|
|
||||||
void StopAllMacros();
|
void StopAllMacros();
|
||||||
|
|
||||||
EXPORT void LoadMacros(obs_data_t *obj);
|
EXPORT void LoadMacros(obs_data_t *obj);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "macro-ref.hpp"
|
#include "macro-ref.hpp"
|
||||||
#include "macro.hpp"
|
#include "macro.hpp"
|
||||||
|
|
||||||
|
#include "plugin-state-helpers.hpp"
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
MacroRef::MacroRef(std::string name)
|
MacroRef::MacroRef(std::string name)
|
||||||
@@ -19,6 +21,7 @@ void MacroRef::Load(obs_data_t *obj)
|
|||||||
auto name = obs_data_get_string(obj, "macro");
|
auto name = obs_data_get_string(obj, "macro");
|
||||||
_postLoadName = name;
|
_postLoadName = name;
|
||||||
_macro = GetWeakMacroByName(name);
|
_macro = GetWeakMacroByName(name);
|
||||||
|
AddPostLoadStep([this]() { MacroRef::PostLoad(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroRef::PostLoad()
|
void MacroRef::PostLoad()
|
||||||
|
|||||||
@@ -72,15 +72,24 @@ void MacroEdit::PasteMacroSegment()
|
|||||||
}
|
}
|
||||||
AddMacroCondition(macro.get(), macro->Conditions().size(),
|
AddMacroCondition(macro.get(), macro->Conditions().size(),
|
||||||
copyInfo.segment->GetId(), data.Get(), logic);
|
copyInfo.segment->GetId(), data.Get(), logic);
|
||||||
|
if (!macro->Conditions().empty()) {
|
||||||
|
macro->Conditions().back()->FixupVarMappingRefs();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MacroSegmentCopyInfo::Type::ACTION:
|
case MacroSegmentCopyInfo::Type::ACTION:
|
||||||
AddMacroAction(macro.get(), macro->Actions().size(),
|
AddMacroAction(macro.get(), macro->Actions().size(),
|
||||||
copyInfo.segment->GetId(), data.Get());
|
copyInfo.segment->GetId(), data.Get());
|
||||||
|
if (!macro->Actions().empty()) {
|
||||||
|
macro->Actions().back()->FixupVarMappingRefs();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MacroSegmentCopyInfo::Type::ELSE:
|
case MacroSegmentCopyInfo::Type::ELSE:
|
||||||
AddMacroElseAction(macro.get(), macro->ElseActions().size(),
|
AddMacroElseAction(macro.get(), macro->ElseActions().size(),
|
||||||
copyInfo.segment->GetId(), data.Get());
|
copyInfo.segment->GetId(), data.Get());
|
||||||
|
if (!macro->ElseActions().empty()) {
|
||||||
|
macro->ElseActions().back()->FixupVarMappingRefs();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ const std::vector<VarMapping> &MacroSegment::GetVarMappings() const
|
|||||||
void MacroSegment::SetVarMappings(const std::vector<VarMapping> &mappings)
|
void MacroSegment::SetVarMappings(const std::vector<VarMapping> &mappings)
|
||||||
{
|
{
|
||||||
_varMappings = 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
|
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_bool(data, "useCustomLabel", _useCustomLabel);
|
||||||
obs_data_set_string(data, "customLabel", _customLabel.c_str());
|
obs_data_set_string(data, "customLabel", _customLabel.c_str());
|
||||||
obs_data_set_bool(data, "enabled", _enabled);
|
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();
|
OBSDataArrayAutoRelease mappingsArray = obs_data_array_create();
|
||||||
for (const auto &mapping : _varMappings) {
|
for (const auto &mapping : _varMappings) {
|
||||||
@@ -112,6 +129,12 @@ bool MacroSegment::Load(obs_data_t *obj)
|
|||||||
_enabled = obs_data_get_bool(obj, "enabled");
|
_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();
|
_varMappings.clear();
|
||||||
OBSDataArrayAutoRelease mappingsArray =
|
OBSDataArrayAutoRelease mappingsArray =
|
||||||
obs_data_get_array(data, "varMappings");
|
obs_data_get_array(data, "varMappings");
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public:
|
|||||||
const std::vector<VarMapping> &GetVarMappings() const;
|
const std::vector<VarMapping> &GetVarMappings() const;
|
||||||
void SetVarMappings(const std::vector<VarMapping> &mappings);
|
void SetVarMappings(const std::vector<VarMapping> &mappings);
|
||||||
std::vector<TempVariable> GetOwnTempVars() const;
|
std::vector<TempVariable> GetOwnTempVars() const;
|
||||||
|
void FixupVarMappingRefs();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend bool SupportsVariableValue(MacroSegment *);
|
friend bool SupportsVariableValue(MacroSegment *);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ signals:
|
|||||||
void Remove(const QString &);
|
void Remove(const QString &);
|
||||||
void SegmentOrderChanged();
|
void SegmentOrderChanged();
|
||||||
void HighlightChanged(bool value);
|
void HighlightChanged(bool value);
|
||||||
|
void ConditionTypeCreated(const QString &typeId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MacroSignalManager(QObject *parent = nullptr);
|
MacroSignalManager(QObject *parent = nullptr);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "advanced-scene-switcher.hpp"
|
#include "advanced-scene-switcher.hpp"
|
||||||
#include "action-queue.hpp"
|
|
||||||
#include "macro-action-edit.hpp"
|
#include "macro-action-edit.hpp"
|
||||||
#include "macro-condition-edit.hpp"
|
#include "macro-condition-edit.hpp"
|
||||||
#include "macro-export-import-dialog.hpp"
|
#include "macro-export-import-dialog.hpp"
|
||||||
@@ -316,13 +315,9 @@ void AdvSceneSwitcher::ExportMacros() const
|
|||||||
obs_data_array_push_back(macroArray, obj);
|
obs_data_array_push_back(macroArray, obj);
|
||||||
}
|
}
|
||||||
obs_data_set_array(data, "macros", macroArray);
|
obs_data_set_array(data, "macros", macroArray);
|
||||||
SaveVariables(data);
|
|
||||||
SaveActionQueues(data);
|
|
||||||
obs_data_set_string(data, "version", g_GIT_TAG);
|
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(
|
bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
|
||||||
@@ -384,8 +379,6 @@ void AdvSceneSwitcher::ImportMacros()
|
|||||||
ImportMacros();
|
ImportMacros();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImportVariables(data);
|
|
||||||
ImportQueues(data);
|
|
||||||
|
|
||||||
auto version = obs_data_get_string(data, "version");
|
auto version = obs_data_get_string(data, "version");
|
||||||
if (strcmp(version, g_GIT_TAG) != 0) {
|
if (strcmp(version, g_GIT_TAG) != 0) {
|
||||||
@@ -557,6 +550,10 @@ void AdvSceneSwitcher::HighlightOnChange() const
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (macro->Paused()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (macro->ActionTriggerModePreventedActionsSince(
|
if (macro->ActionTriggerModePreventedActionsSince(
|
||||||
lastOnChangeHighlightCheckTime)) {
|
lastOnChangeHighlightCheckTime)) {
|
||||||
HighlightWidget(ui->actionTriggerMode, Qt::yellow,
|
HighlightWidget(ui->actionTriggerMode, Qt::yellow,
|
||||||
|
|||||||
@@ -129,12 +129,11 @@ static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
|
|||||||
const auto timeSpent = endTime - startTime;
|
const auto timeSpent = endTime - startTime;
|
||||||
|
|
||||||
if (timeSpent >= perfLogThreshold) {
|
if (timeSpent >= perfLogThreshold) {
|
||||||
const long int ms =
|
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
timeSpent)
|
|
||||||
.count();
|
|
||||||
blog(LOG_WARNING,
|
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->GetId().c_str(),
|
||||||
condition->GetMacro()->Name().c_str());
|
condition->GetMacro()->Name().c_str());
|
||||||
}
|
}
|
||||||
@@ -157,6 +156,15 @@ bool Macro::CheckConditionHelper(
|
|||||||
return conditionMatched;
|
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();
|
const auto logicType = condition->GetLogicType();
|
||||||
if (logicType == Logic::Type::NONE) {
|
if (logicType == Logic::Type::NONE) {
|
||||||
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
|
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
|
||||||
@@ -281,7 +289,7 @@ bool Macro::CheckConditions(bool ignorePause)
|
|||||||
|
|
||||||
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
|
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
|
||||||
: (_elseActions.size() > 0);
|
: (_elseActions.size() > 0);
|
||||||
if (!_actionModeMatch && hasActionsToExecute) {
|
if (!_actionModeMatch && hasActionsToExecute && !_paused) {
|
||||||
_lastActionRunModePreventTime =
|
_lastActionRunModePreventTime =
|
||||||
std::chrono::high_resolution_clock::now();
|
std::chrono::high_resolution_clock::now();
|
||||||
}
|
}
|
||||||
@@ -380,7 +388,7 @@ bool Macro::ShouldRunActions() const
|
|||||||
!_paused && (_matched || _elseActions.size() > 0) &&
|
!_paused && (_matched || _elseActions.size() > 0) &&
|
||||||
_actionModeMatch;
|
_actionModeMatch;
|
||||||
|
|
||||||
if (VerboseLoggingEnabled() && !_actionModeMatch) {
|
if (VerboseLoggingEnabled() && !_actionModeMatch && !_paused) {
|
||||||
if (_matched && _actions.size() > 0) {
|
if (_matched && _actions.size() > 0) {
|
||||||
blog(LOG_INFO, "skip actions for Macro %s (on change)",
|
blog(LOG_INFO, "skip actions for Macro %s (on change)",
|
||||||
_name.c_str());
|
_name.c_str());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#import <Carbon/Carbon.h>
|
#import <Carbon/Carbon.h>
|
||||||
#include <Carbon/Carbon.h>
|
#include <Carbon/Carbon.h>
|
||||||
#include <util/platform.h>
|
#include <util/platform.h>
|
||||||
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
@@ -14,41 +15,13 @@
|
|||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
std::vector<std::string> GetWindowList()
|
static std::string nsStringToStdString(NSString *str)
|
||||||
{
|
{
|
||||||
std::vector<std::string> windows;
|
if (!str) {
|
||||||
@autoreleasepool {
|
return "";
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return windows;
|
const char *utf8 = [str UTF8String];
|
||||||
|
return utf8 ? utf8 : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetCurrentWindowTitle()
|
std::string GetCurrentWindowTitle()
|
||||||
@@ -100,100 +73,49 @@ std::string GetCurrentWindowTitle()
|
|||||||
return title;
|
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)
|
bool isWindowMaximizedOnScreen(NSDictionary *app, NSScreen *screen)
|
||||||
{
|
{
|
||||||
double maximizedTolerance = 0.99;
|
|
||||||
NSRect screenFrame = [screen frame];
|
|
||||||
NSRect windowBounds;
|
NSRect windowBounds;
|
||||||
CGRectMakeWithDictionaryRepresentation(
|
CGRectMakeWithDictionaryRepresentation(
|
||||||
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
|
(CFDictionaryRef)[app objectForKey:@"kCGWindowBounds"],
|
||||||
&windowBounds);
|
&windowBounds);
|
||||||
|
|
||||||
int sumX = windowBounds.origin.x + windowBounds.size.width;
|
// CGWindowList: flipped coords, origin at top-left of main display.
|
||||||
int sumY = windowBounds.origin.y + windowBounds.size.height;
|
// 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
|
// Convert visible area to CGWindow coordinates.
|
||||||
if (sumX > screenFrame.size.width) {
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
if (sumY > screenFrame.size.height) {
|
if (fabs((windowBounds.origin.x + windowBounds.size.width) -
|
||||||
|
screenRight) > tolerance) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Bottom edge must align with the visible bottom (above dock).
|
||||||
return ((double)sumX / (double)screenFrame.size.width) >
|
if (fabs((windowBounds.origin.y + windowBounds.size.height) -
|
||||||
maximizedTolerance &&
|
screenBottom) > tolerance) {
|
||||||
((double)sumY / (double)screenFrame.size.height) >
|
return false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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)
|
bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
|
||||||
@@ -207,52 +129,176 @@ bool isWindowFullscreenOnScreen(NSDictionary *app, NSScreen *screen)
|
|||||||
return NSEqualSizes(windowBounds.size, screenFrame.size);
|
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 {
|
@autoreleasepool {
|
||||||
NSArray *screens = [NSScreen screens];
|
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);
|
CFArrayRef cfApps = CGWindowListCopyWindowInfo(
|
||||||
bool matches = nameMachesPattern(name, title) ||
|
kCGWindowListExcludeDesktopElements, kCGNullWindowID);
|
||||||
nameMachesPattern(owner, title);
|
NSMutableArray *apps = (__bridge NSMutableArray *)cfApps;
|
||||||
if (equals || matches) {
|
|
||||||
|
// 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) {
|
for (NSScreen *screen in screens) {
|
||||||
if (isWindowOriginOnScreen(app, screen,
|
if (isWindowFullscreenOnScreen(app,
|
||||||
true) &&
|
screen)) {
|
||||||
isWindowFullscreenOnScreen(
|
int pid = [[app objectForKey:
|
||||||
app, screen)) {
|
@"kCGWindowOwnerPID"]
|
||||||
apps = nil;
|
intValue];
|
||||||
CFRelease(cfApps);
|
fullscreenPIDs.insert(pid);
|
||||||
return true;
|
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;
|
apps = nil;
|
||||||
CFRelease(cfApps);
|
CFRelease(cfApps);
|
||||||
}
|
}
|
||||||
return false;
|
return result;
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::string> GetTextInWindow(const std::string &)
|
|
||||||
{
|
|
||||||
// Not implemented
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int SecondsSinceLastInput()
|
int SecondsSinceLastInput()
|
||||||
@@ -341,8 +387,7 @@ QStringList GetProcessPathsFromName(const QString &name)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const char *nameStr = appName.UTF8String;
|
const char *nameStr = appName.UTF8String;
|
||||||
if (!nameStr ||
|
if (!nameStr || name != QString::fromUtf8(nameStr)) {
|
||||||
name != QString::fromUtf8(nameStr)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NSURL *url = app.executableURL;
|
NSURL *url = app.executableURL;
|
||||||
|
|||||||
@@ -11,11 +11,31 @@ namespace advss {
|
|||||||
|
|
||||||
enum class HotkeyType;
|
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 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 int SecondsSinceLastInput();
|
||||||
EXPORT QStringList GetProcessList();
|
EXPORT QStringList GetProcessList();
|
||||||
EXPORT std::string GetForegroundProcessName();
|
EXPORT std::string GetForegroundProcessName();
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ public:
|
|||||||
bool disableHints = false;
|
bool disableHints = false;
|
||||||
bool disableFilterComboboxFilter = false;
|
bool disableFilterComboboxFilter = false;
|
||||||
bool hideLegacyTabs = true;
|
bool hideLegacyTabs = true;
|
||||||
|
bool settingsLockEnabled = false;
|
||||||
|
std::string settingsLockPassword;
|
||||||
bool saveWindowGeo = false;
|
bool saveWindowGeo = false;
|
||||||
QPoint windowPos = {};
|
QPoint windowPos = {};
|
||||||
QSize windowSize = {};
|
QSize windowSize = {};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
const std::map<Logic::Type, const char *> Logic::localeMap = {
|
const std::map<Logic::Type, const char *> Logic::localeMap = {
|
||||||
{Logic::Type::NONE, {"AdvSceneSwitcher.logic.none"}},
|
|
||||||
{Logic::Type::AND, {"AdvSceneSwitcher.logic.and"}},
|
{Logic::Type::AND, {"AdvSceneSwitcher.logic.and"}},
|
||||||
{Logic::Type::OR, {"AdvSceneSwitcher.logic.or"}},
|
{Logic::Type::OR, {"AdvSceneSwitcher.logic.or"}},
|
||||||
{Logic::Type::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
|
{Logic::Type::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
|
||||||
@@ -67,7 +66,7 @@ void Logic::PopulateLogicTypeSelection(QComboBox *list, bool isRootCondition)
|
|||||||
return typeValue < rootOffset;
|
return typeValue < rootOffset;
|
||||||
}}
|
}}
|
||||||
: std::function<bool(int)>{[](int typeValue) {
|
: std::function<bool(int)>{[](int typeValue) {
|
||||||
return typeValue >= rootOffset;
|
return typeValue > rootOffset;
|
||||||
}};
|
}};
|
||||||
for (const auto &[type, name] : localeMap) {
|
for (const auto &[type, name] : localeMap) {
|
||||||
const int typeValue = static_cast<int>(type);
|
const int typeValue = static_cast<int>(type);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ static void populateUnits(QComboBox *list)
|
|||||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"));
|
list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"));
|
||||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.minutes"));
|
list->addItem(obs_module_text("AdvSceneSwitcher.unit.minutes"));
|
||||||
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
|
list->addItem(obs_module_text("AdvSceneSwitcher.unit.hours"));
|
||||||
|
list->addItem(obs_module_text("AdvSceneSwitcher.unit.days"));
|
||||||
}
|
}
|
||||||
|
|
||||||
DurationSelection::DurationSelection(QWidget *parent, bool showUnitSelection,
|
DurationSelection::DurationSelection(QWidget *parent, bool showUnitSelection,
|
||||||
@@ -66,6 +67,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
|||||||
return 60;
|
return 60;
|
||||||
case Duration::Unit::HOURS:
|
case Duration::Unit::HOURS:
|
||||||
return 3600;
|
return 3600;
|
||||||
|
case Duration::Unit::DAYS:
|
||||||
|
return 86400;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ static std::string durationUnitToString(Duration::Unit u)
|
|||||||
return obs_module_text("AdvSceneSwitcher.unit.minutes");
|
return obs_module_text("AdvSceneSwitcher.unit.minutes");
|
||||||
case Duration::Unit::HOURS:
|
case Duration::Unit::HOURS:
|
||||||
return obs_module_text("AdvSceneSwitcher.unit.hours");
|
return obs_module_text("AdvSceneSwitcher.unit.hours");
|
||||||
|
case Duration::Unit::DAYS:
|
||||||
|
return obs_module_text("AdvSceneSwitcher.unit.days");
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -151,6 +153,8 @@ static int durationUnitToMultiplier(Duration::Unit u)
|
|||||||
return 60;
|
return 60;
|
||||||
case Duration::Unit::HOURS:
|
case Duration::Unit::HOURS:
|
||||||
return 3600;
|
return 3600;
|
||||||
|
case Duration::Unit::DAYS:
|
||||||
|
return 86400;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public:
|
|||||||
SECONDS,
|
SECONDS,
|
||||||
MINUTES,
|
MINUTES,
|
||||||
HOURS,
|
HOURS,
|
||||||
|
DAYS,
|
||||||
};
|
};
|
||||||
EXPORT Unit GetUnit() const { return _unit; }
|
EXPORT Unit GetUnit() const { return _unit; }
|
||||||
|
|
||||||
|
|||||||
@@ -398,10 +398,14 @@ void FirstRunWizard::markFirstRunComplete()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent)
|
std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent,
|
||||||
|
bool *wasSkipped)
|
||||||
{
|
{
|
||||||
auto wizard = new FirstRunWizard(parent);
|
auto wizard = new FirstRunWizard(parent);
|
||||||
wizard->exec();
|
wizard->exec();
|
||||||
|
if (wasSkipped) {
|
||||||
|
*wasSkipped = (wizard->result() == QDialog::Rejected);
|
||||||
|
}
|
||||||
wizard->deleteLater();
|
wizard->deleteLater();
|
||||||
return wizard->_macro;
|
return wizard->_macro;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ class FirstRunWizard : public QWizard {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit FirstRunWizard(QWidget *parent = nullptr);
|
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
|
static bool
|
||||||
CreateMacro(std::shared_ptr<Macro> ¯o, const std::string ¯oName,
|
CreateMacro(std::shared_ptr<Macro> ¯o, const std::string ¯oName,
|
||||||
const std::string &conditionId, obs_data_t *conditionData,
|
const std::string &conditionId, obs_data_t *conditionData,
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ static std::vector<std::function<void()>> &getStopSteps()
|
|||||||
return steps;
|
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 *)>> &getSaveSteps()
|
||||||
{
|
{
|
||||||
static std::vector<std::function<void(obs_data_t *)>> steps;
|
static std::vector<std::function<void(obs_data_t *)>> steps;
|
||||||
@@ -109,6 +121,18 @@ void LoadPluginSettings(obs_data_t *obj)
|
|||||||
GetSwitcher()->LoadSettings(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)
|
void AddSaveStep(std::function<void(obs_data_t *)> step)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
@@ -136,6 +160,9 @@ void AddIntervalResetStep(std::function<void()> step)
|
|||||||
void RunSaveSteps(obs_data_t *obj)
|
void RunSaveSteps(obs_data_t *obj)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
for (const auto &func : getEarlySaveSteps()) {
|
||||||
|
func(obj);
|
||||||
|
}
|
||||||
for (const auto &func : getSaveSteps()) {
|
for (const auto &func : getSaveSteps()) {
|
||||||
func(obj);
|
func(obj);
|
||||||
}
|
}
|
||||||
@@ -144,6 +171,9 @@ void RunSaveSteps(obs_data_t *obj)
|
|||||||
void RunLoadSteps(obs_data_t *obj)
|
void RunLoadSteps(obs_data_t *obj)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
for (const auto &func : getEarlyLoadSteps()) {
|
||||||
|
func(obj);
|
||||||
|
}
|
||||||
for (const auto &func : getLoadSteps()) {
|
for (const auto &func : getLoadSteps()) {
|
||||||
func(obj);
|
func(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace advss {
|
|||||||
void SavePluginSettings(obs_data_t *);
|
void SavePluginSettings(obs_data_t *);
|
||||||
EXPORT void LoadPluginSettings(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 AddSaveStep(std::function<void(obs_data_t *)>);
|
||||||
EXPORT void AddLoadStep(std::function<void(obs_data_t *)>);
|
EXPORT void AddLoadStep(std::function<void(obs_data_t *)>);
|
||||||
EXPORT void AddPostLoadStep(std::function<void()>);
|
EXPORT void AddPostLoadStep(std::function<void()>);
|
||||||
|
|||||||
@@ -173,10 +173,10 @@ void PopulateTransitionSelection(QComboBox *sel, bool addCurrent, bool addAny,
|
|||||||
void PopulateWindowSelection(QComboBox *sel, bool addSelect)
|
void PopulateWindowSelection(QComboBox *sel, bool addSelect)
|
||||||
{
|
{
|
||||||
|
|
||||||
const auto windows = GetWindowList();
|
const auto windows = GetWindows();
|
||||||
|
|
||||||
for (const std::string &window : windows) {
|
for (const auto &info : windows) {
|
||||||
sel->addItem(window.c_str());
|
sel->addItem(info.title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
sel->model()->sort(0);
|
sel->model()->sort(0);
|
||||||
|
|||||||
@@ -162,4 +162,48 @@ bool IsMediaSource(obs_source_t *source)
|
|||||||
return (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) != 0;
|
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
|
} // namespace advss
|
||||||
|
|||||||
@@ -20,4 +20,25 @@ EXPORT OBSWeakSource GetWeakFilterByQString(OBSWeakSource source,
|
|||||||
EXPORT int GetSceneItemCount(const OBSWeakSource &);
|
EXPORT int GetSceneItemCount(const OBSWeakSource &);
|
||||||
EXPORT bool IsMediaSource(obs_source_t *source);
|
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
|
} // namespace advss
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public:
|
|||||||
EXPORT std::optional<const TempVariable> GetTempVariable(Macro *) const;
|
EXPORT std::optional<const TempVariable> GetTempVariable(Macro *) const;
|
||||||
EXPORT bool operator==(const TempVariableRef &other) const;
|
EXPORT bool operator==(const TempVariableRef &other) const;
|
||||||
bool HasValidID() const { return !_id.empty(); }
|
bool HasValidID() const { return !_id.empty(); }
|
||||||
|
std::string GetID() const { return _id; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
|
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ static void save(obs_data_t *data)
|
|||||||
{
|
{
|
||||||
tabSettings.Save(data, "tabSettings");
|
tabSettings.Save(data, "tabSettings");
|
||||||
dockSettings.Save(data, "dockSettings");
|
dockSettings.Save(data, "dockSettings");
|
||||||
|
|
||||||
obs_data_set_bool(data, "addVariablesDock", addDock);
|
obs_data_set_bool(data, "addVariablesDock", addDock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,6 @@ static void load(obs_data_t *data)
|
|||||||
{
|
{
|
||||||
tabSettings.Load(data, "tabSettings");
|
tabSettings.Load(data, "tabSettings");
|
||||||
dockSettings.Load(data, "dockSettings");
|
dockSettings.Load(data, "dockSettings");
|
||||||
|
|
||||||
enableDock(obs_data_get_bool(data, "addVariablesDock"));
|
enableDock(obs_data_get_bool(data, "addVariablesDock"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
|||||||
|
|
||||||
static bool setup()
|
static bool setup()
|
||||||
{
|
{
|
||||||
AddSaveStep(SaveVariables);
|
AddEarlySaveStep(SaveVariables);
|
||||||
AddLoadStep(LoadVariables);
|
AddEarlyLoadStep(LoadVariables);
|
||||||
AddPluginCleanupStep([]() { variables.clear(); });
|
AddPluginCleanupStep([]() { variables.clear(); });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -454,7 +454,6 @@ void LoadVariables(obs_data_t *obj)
|
|||||||
auto var = Variable::Create();
|
auto var = Variable::Create();
|
||||||
variables.emplace_back(var);
|
variables.emplace_back(var);
|
||||||
variables.back()->Load(array_obj);
|
variables.back()->Load(array_obj);
|
||||||
obs_data_release(array_obj);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#include "platform-funcs.hpp"
|
#include "platform-funcs.hpp"
|
||||||
|
#include "plugin-state-helpers.hpp"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <UIAutomation.h>
|
#include <UIAutomation.h>
|
||||||
#include <util/platform.h>
|
#include <util/platform.h>
|
||||||
#include <TlHelp32.h>
|
#include <TlHelp32.h>
|
||||||
#include <Psapi.h>
|
#include <Psapi.h>
|
||||||
|
#include <memory>
|
||||||
#include <locale>
|
#include <locale>
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -16,6 +18,9 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
|
#define ADVSS_WIDEN_(x) L##x
|
||||||
|
#define ADVSS_WIDEN(x) ADVSS_WIDEN_(x)
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
#define MAX_SEARCH 1000
|
#define MAX_SEARCH 1000
|
||||||
@@ -133,7 +138,7 @@ const std::vector<std::string> getOBSWindows()
|
|||||||
return lastDoneHelper->windows;
|
return lastDoneHelper->windows;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> GetWindowList()
|
static std::vector<std::string> getAllWindowTitles()
|
||||||
{
|
{
|
||||||
std::vector<std::string> windows;
|
std::vector<std::string> windows;
|
||||||
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
|
EnumWindowsWithMetro(GetTitleCB, reinterpret_cast<LPARAM>(&windows));
|
||||||
@@ -150,6 +155,129 @@ std::vector<std::string> GetWindowList()
|
|||||||
return windows;
|
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()
|
std::string GetCurrentWindowTitle()
|
||||||
{
|
{
|
||||||
HWND window = GetForegroundWindow();
|
HWND window = GetForegroundWindow();
|
||||||
@@ -188,36 +316,6 @@ static HWND getHWNDfromTitle(const std::string &title)
|
|||||||
return hwnd;
|
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)
|
static std::wstring GetControlText(HWND hwnd, IUIAutomationElement *element)
|
||||||
{
|
{
|
||||||
VARIANT var;
|
VARIANT var;
|
||||||
@@ -244,7 +342,7 @@ static std::wstring GetControlText(HWND hwnd, IUIAutomationElement *element)
|
|||||||
return text;
|
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);
|
HWND hwnd = getHWNDfromTitle(window);
|
||||||
if (!hwnd) {
|
if (!hwnd) {
|
||||||
@@ -311,35 +409,6 @@ std::optional<std::string> GetTextInWindow(const std::string &window)
|
|||||||
return tmp;
|
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 GetProcessList()
|
||||||
{
|
{
|
||||||
QStringList processes;
|
QStringList processes;
|
||||||
@@ -500,9 +569,86 @@ int SecondsSinceLastInput()
|
|||||||
return (getTime() - getLastInputTime()) / 1000;
|
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()
|
void PlatformInit()
|
||||||
{
|
{
|
||||||
CoInitialize(NULL);
|
CoInitialize(NULL);
|
||||||
|
AddFinishedLoadingStep(addPluginFolderToSymbolPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlatformCleanup()
|
void PlatformCleanup()
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
#include <obs-module.h>
|
#include <obs-module.h>
|
||||||
#include <obs-frontend-api.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_DECLARE_MODULE()
|
||||||
OBS_MODULE_USE_DEFAULT_LOCALE("advanced-scene-switcher", "en-US")
|
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)
|
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);
|
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||||
InitSceneSwitcher(obs_current_module(), obs_module_text);
|
InitSceneSwitcher(obs_current_module(), obs_module_text);
|
||||||
return true;
|
return true;
|
||||||
@@ -25,4 +42,5 @@ bool obs_module_load(void)
|
|||||||
void obs_module_unload(void)
|
void obs_module_unload(void)
|
||||||
{
|
{
|
||||||
FreeSceneSwitcher();
|
FreeSceneSwitcher();
|
||||||
|
ADVSS_UNSETENV(ADVSS_INSTANCE_ENV);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ endmacro()
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
add_subdirectory(base)
|
|
||||||
|
|
||||||
# Add plugins below, which have dependencies to external libraries or other
|
# Add plugins below, which have dependencies to external libraries or other
|
||||||
# components which might potentially not be fulfilled.
|
# components which might potentially not be fulfilled.
|
||||||
|
|
||||||
@@ -28,10 +26,12 @@ install_advss_plugin_dependency(...)
|
|||||||
... to install the plugin and its dependencies.
|
... to install the plugin and its dependencies.
|
||||||
#]]
|
#]]
|
||||||
|
|
||||||
|
add_plugin(base)
|
||||||
add_plugin(http)
|
add_plugin(http)
|
||||||
add_plugin(midi)
|
add_plugin(midi)
|
||||||
add_plugin(mqtt)
|
add_plugin(mqtt)
|
||||||
add_plugin(openvr)
|
add_plugin(openvr)
|
||||||
|
add_plugin(schedule)
|
||||||
add_plugin(scripting)
|
add_plugin(scripting)
|
||||||
add_plugin(stream-deck)
|
add_plugin(stream-deck)
|
||||||
add_plugin(twitch)
|
add_plugin(twitch)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public:
|
|||||||
|
|
||||||
} // namespace
|
} // 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) {
|
if (event != OBS_FRONTEND_EVENT_EXIT) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ void MacroActionSourceInteractionEdit::AcceptRecorded(
|
|||||||
_stepList->SetSteps(_entryData->_steps);
|
_stepList->SetSteps(_entryData->_steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
|
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *)
|
||||||
{
|
{
|
||||||
const QSignalBlocker b(this);
|
const QSignalBlocker b(this);
|
||||||
UpdateEntryData();
|
UpdateEntryData();
|
||||||
|
|||||||
@@ -42,19 +42,20 @@ void CloseWindow(const std::string &) {}
|
|||||||
|
|
||||||
std::optional<std::string> MacroActionWindow::GetMatchingWindow() const
|
std::optional<std::string> MacroActionWindow::GetMatchingWindow() const
|
||||||
{
|
{
|
||||||
const auto windowList = GetWindowList();
|
const auto windows = GetWindows();
|
||||||
|
|
||||||
if (!_regex.Enabled()) {
|
if (!_regex.Enabled()) {
|
||||||
if (std::find(windowList.begin(), windowList.end(),
|
for (const auto &info : windows) {
|
||||||
std::string(_window)) == windowList.end()) {
|
if (info.title == std::string(_window)) {
|
||||||
return {};
|
return info.title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _window;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &window : windowList) {
|
for (const auto &info : windows) {
|
||||||
if (_regex.Matches(window, _window)) {
|
if (_regex.Matches(info.title, _window)) {
|
||||||
return window;
|
return info.title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ void MacroConditionDateEdit::UpdateEntryData()
|
|||||||
SetWidgetStatus();
|
SetWidgetStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroConditionDateEdit::showEvent(QShowEvent *event)
|
void MacroConditionDateEdit::showEvent(QShowEvent *)
|
||||||
{
|
{
|
||||||
const QSignalBlocker b(this);
|
const QSignalBlocker b(this);
|
||||||
UpdateEntryData();
|
UpdateEntryData();
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ bool MacroConditionGameCapture::CheckCondition()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_activeKeeper.SetActive(_keepActive);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(_mtx);
|
std::lock_guard<std::mutex> lock(_mtx);
|
||||||
if (_hooked) {
|
if (_hooked) {
|
||||||
SetTempVarValue("title", _title);
|
SetTempVarValue("title", _title);
|
||||||
@@ -38,6 +40,7 @@ bool MacroConditionGameCapture::Save(obs_data_t *obj) const
|
|||||||
{
|
{
|
||||||
MacroCondition::Save(obj);
|
MacroCondition::Save(obj);
|
||||||
_source.Save(obj);
|
_source.Save(obj);
|
||||||
|
obs_data_set_bool(obj, "keepActive", _keepActive);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ bool MacroConditionGameCapture::Load(obs_data_t *obj)
|
|||||||
{
|
{
|
||||||
MacroCondition::Load(obj);
|
MacroCondition::Load(obj);
|
||||||
_source.Load(obj);
|
_source.Load(obj);
|
||||||
|
_keepActive = obs_data_get_bool(obj, "keepActive");
|
||||||
SetupSignalHandler(OBSGetStrongRef(_source.GetSource()));
|
SetupSignalHandler(OBSGetStrongRef(_source.GetSource()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,9 @@ void MacroConditionGameCapture::SetupSignalHandler(obs_source_t *source)
|
|||||||
_unhookSignal = OBSSignal(sh, "unhooked", UnhookedSignalReceived, this);
|
_unhookSignal = OBSSignal(sh, "unhooked", UnhookedSignalReceived, this);
|
||||||
_lastSource = source;
|
_lastSource = source;
|
||||||
|
|
||||||
|
_activeKeeper.SetActive(_keepActive);
|
||||||
|
_activeKeeper.SetSource(source);
|
||||||
|
|
||||||
SetupInitialState(source);
|
SetupInitialState(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,17 +171,33 @@ MacroConditionGameCaptureEdit::MacroConditionGameCaptureEdit(
|
|||||||
QWidget *parent, std::shared_ptr<MacroConditionGameCapture> entryData)
|
QWidget *parent, std::shared_ptr<MacroConditionGameCapture> entryData)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
_sources(new SourceSelectionWidget(this, getGameCaptureSourcesList,
|
_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,
|
QWidget::connect(_sources,
|
||||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||||
SLOT(SourceChanged(const SourceSelection &)));
|
SLOT(SourceChanged(const SourceSelection &)));
|
||||||
|
QWidget::connect(_keepActive, SIGNAL(stateChanged(int)), this,
|
||||||
|
SLOT(KeepActiveChanged(int)));
|
||||||
|
|
||||||
auto layout = new QHBoxLayout;
|
auto entryLayout = new QHBoxLayout;
|
||||||
PlaceWidgets(
|
PlaceWidgets(
|
||||||
obs_module_text("AdvSceneSwitcher.condition.gameCapture.entry"),
|
obs_module_text("AdvSceneSwitcher.condition.gameCapture.entry"),
|
||||||
layout, {{"{{sources}}", _sources}});
|
entryLayout, {{"{{sources}}", _sources}});
|
||||||
setLayout(layout);
|
|
||||||
|
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;
|
_entryData = entryData;
|
||||||
UpdateEntryData();
|
UpdateEntryData();
|
||||||
@@ -187,9 +210,16 @@ void MacroConditionGameCaptureEdit::SourceChanged(const SourceSelection &source)
|
|||||||
_entryData->_source = source;
|
_entryData->_source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MacroConditionGameCaptureEdit::KeepActiveChanged(int state)
|
||||||
|
{
|
||||||
|
GUARD_LOADING_AND_LOCK();
|
||||||
|
_entryData->_keepActive = state;
|
||||||
|
}
|
||||||
|
|
||||||
void MacroConditionGameCaptureEdit::UpdateEntryData()
|
void MacroConditionGameCaptureEdit::UpdateEntryData()
|
||||||
{
|
{
|
||||||
_sources->SetSource(_entryData->_source);
|
_sources->SetSource(_entryData->_source);
|
||||||
|
_keepActive->setChecked(_entryData->_keepActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace advss
|
} // namespace advss
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include "help-icon.hpp"
|
||||||
#include "macro-condition-edit.hpp"
|
#include "macro-condition-edit.hpp"
|
||||||
|
#include "source-helpers.hpp"
|
||||||
#include "source-selection.hpp"
|
#include "source-selection.hpp"
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QCheckBox>
|
||||||
|
|
||||||
namespace advss {
|
namespace advss {
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
SourceSelection _source;
|
SourceSelection _source;
|
||||||
|
bool _keepActive = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void HookedSignalReceived(void *data, calldata_t *);
|
static void HookedSignalReceived(void *data, calldata_t *);
|
||||||
@@ -31,6 +35,7 @@ private:
|
|||||||
void GetCalldataInfo(calldata_t *cd);
|
void GetCalldataInfo(calldata_t *cd);
|
||||||
|
|
||||||
obs_source_t *_lastSource = nullptr;
|
obs_source_t *_lastSource = nullptr;
|
||||||
|
SourceActiveKeeper _activeKeeper;
|
||||||
OBSSignal _hookSignal;
|
OBSSignal _hookSignal;
|
||||||
OBSSignal _unhookSignal;
|
OBSSignal _unhookSignal;
|
||||||
|
|
||||||
@@ -63,9 +68,12 @@ public:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void SourceChanged(const SourceSelection &);
|
void SourceChanged(const SourceSelection &);
|
||||||
|
void KeepActiveChanged(int);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SourceSelectionWidget *_sources;
|
SourceSelectionWidget *_sources;
|
||||||
|
QCheckBox *_keepActive;
|
||||||
|
HelpIcon *_keepActiveHelp;
|
||||||
std::shared_ptr<MacroConditionGameCapture> _entryData;
|
std::shared_ptr<MacroConditionGameCapture> _entryData;
|
||||||
|
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|||||||
@@ -158,8 +158,21 @@ void MacroConditionTransition::TransitionStarted(void *data, calldata_t *cd)
|
|||||||
|
|
||||||
OBSSourceAutoRelease startSource = obs_transition_get_source(
|
OBSSourceAutoRelease startSource = obs_transition_get_source(
|
||||||
transitionSource, OBS_TRANSITION_SOURCE_A);
|
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) {
|
||||||
|
OBSSourceAutoRelease startSourceByName = obs_get_source_by_name(
|
||||||
|
obs_source_get_name(startSource));
|
||||||
|
if (startSourceByName) {
|
||||||
|
startSource = std::move(startSourceByName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OBSSourceAutoRelease endSource =
|
OBSSourceAutoRelease endSource =
|
||||||
obs_frontend_preview_program_mode_active()
|
studioModeActive
|
||||||
? obs_frontend_get_current_scene()
|
? obs_frontend_get_current_scene()
|
||||||
: obs_transition_get_source(transitionSource,
|
: obs_transition_get_source(transitionSource,
|
||||||
OBS_TRANSITION_SOURCE_B);
|
OBS_TRANSITION_SOURCE_B);
|
||||||
@@ -357,6 +370,7 @@ void MacroConditionTransitionEdit::SceneChanged(const SceneSelection &s)
|
|||||||
{
|
{
|
||||||
GUARD_LOADING_AND_LOCK();
|
GUARD_LOADING_AND_LOCK();
|
||||||
_entryData->_scene = s;
|
_entryData->_scene = s;
|
||||||
|
_entryData->ConnectToTransitionSignals();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroConditionTransitionEdit::DurationChanged(const Duration &dur)
|
void MacroConditionTransitionEdit::DurationChanged(const Duration &dur)
|
||||||
|
|||||||
@@ -14,21 +14,6 @@ bool MacroConditionWindow::_registered = MacroConditionFactory::Register(
|
|||||||
{MacroConditionWindow::Create, MacroConditionWindowEdit::Create,
|
{MacroConditionWindow::Create, MacroConditionWindowEdit::Create,
|
||||||
"AdvSceneSwitcher.condition.window"});
|
"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)
|
void MacroConditionWindow::SetCheckText(bool value)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -46,83 +31,76 @@ bool MacroConditionWindow::GetCheckText()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MacroConditionWindow::WindowMatchesRequirements(
|
bool MacroConditionWindow::WindowMatchesRequirements(
|
||||||
const std::string &window) const
|
const WindowInfo &info) const
|
||||||
{
|
{
|
||||||
const bool focusCheckOK =
|
if (_focus && !info.focused) {
|
||||||
(!_focus || window == ForegroundWindowTitle());
|
|
||||||
if (!focusCheckOK) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool fullscreenCheckOK = (!_fullscreen || IsFullscreen(window));
|
if (_fullscreen && !info.fullscreen) {
|
||||||
if (!fullscreenCheckOK) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool maxCheckOK = (!_maximized || IsMaximized(window));
|
if (_maximized && !info.maximized) {
|
||||||
if (!maxCheckOK) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool textCheckOK =
|
if (_checkText) {
|
||||||
(!_checkText || windowContainsText(window, _text, _textRegex));
|
if (!info.text.has_value()) {
|
||||||
if (!textCheckOK) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
if (_textRegex.Enabled()) {
|
||||||
|
if (!_textRegex.Matches(*info.text, _text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (*info.text != std::string(_text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MacroConditionWindow::WindowMatches(
|
bool MacroConditionWindow::FindMatch(const std::vector<WindowInfo> &windows)
|
||||||
const std::vector<std::string> &windowList)
|
|
||||||
{
|
{
|
||||||
bool match = !_checkTitle ||
|
// When regex is enabled the title check is always active (the backend
|
||||||
std::find(windowList.begin(), windowList.end(),
|
// uses ".*" when the user disables the title check), so we can use a
|
||||||
std::string(_window)) != windowList.end();
|
// single predicate for both modes.
|
||||||
match = match && WindowMatchesRequirements(_window);
|
for (const auto &info : windows) {
|
||||||
SetVariableValueBasedOnMatch(_window);
|
const bool titleOK =
|
||||||
return match;
|
_windowRegex.Enabled()
|
||||||
}
|
? _windowRegex.Matches(info.title, _window)
|
||||||
|
: (!_checkTitle ||
|
||||||
#ifdef _WIN32
|
info.title == std::string(_window));
|
||||||
std::string GetWindowClassByWindowTitle(const std::string &window);
|
if (!titleOK || !WindowMatchesRequirements(info)) {
|
||||||
#endif
|
continue;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
SetVariableValueBasedOnMatch(&info);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
SetVariableValueBasedOnMatch("");
|
SetVariableValueBasedOnMatch(nullptr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MacroConditionWindow::SetVariableValueBasedOnMatch(
|
void MacroConditionWindow::SetVariableValueBasedOnMatch(const WindowInfo *info)
|
||||||
const std::string &matchWindow)
|
|
||||||
{
|
{
|
||||||
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
|
#ifdef _WIN32
|
||||||
SetTempVarValue("windowClass",
|
SetTempVarValue("windowClass", info ? info->windowClass : "");
|
||||||
GetWindowClassByWindowTitle(matchWindow));
|
|
||||||
if (_checkText) {
|
if (_checkText) {
|
||||||
const auto text = GetTextInWindow(matchWindow);
|
SetTempVarValue("windowText",
|
||||||
if (text) {
|
(info && info->text) ? *info->text : "");
|
||||||
SetTempVarValue("windowText", *text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!IsReferencedInVars()) {
|
if (!IsReferencedInVars()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_checkText) {
|
if (_checkText) {
|
||||||
const auto text = GetTextInWindow(matchWindow);
|
SetVariableValue((info && info->text) ? *info->text : "");
|
||||||
SetVariableValue(text.value_or(""));
|
|
||||||
} else {
|
} else {
|
||||||
SetVariableValue(ForegroundWindowTitle());
|
SetVariableValue(ForegroundWindowTitle());
|
||||||
}
|
}
|
||||||
@@ -135,13 +113,18 @@ static bool foregroundWindowChanged()
|
|||||||
|
|
||||||
bool MacroConditionWindow::CheckCondition()
|
bool MacroConditionWindow::CheckCondition()
|
||||||
{
|
{
|
||||||
const auto windowList = GetWindowList();
|
WindowQueryOptions options;
|
||||||
bool match = false;
|
options.focus = _focus;
|
||||||
if (_windowRegex.Enabled()) {
|
options.fullscreen = _fullscreen;
|
||||||
match = WindowRegexMatches(windowList);
|
options.maximized = _maximized;
|
||||||
} else {
|
options.geometry = true;
|
||||||
match = WindowMatches(windowList);
|
#ifdef _WIN32
|
||||||
}
|
options.windowClass = true;
|
||||||
|
options.text = _checkText;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const auto windows = GetWindows(options);
|
||||||
|
bool match = FindMatch(windows);
|
||||||
match = match && (!_windowFocusChanged || foregroundWindowChanged());
|
match = match && (!_windowFocusChanged || foregroundWindowChanged());
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
@@ -201,6 +184,26 @@ void MacroConditionWindow::SetupTempVars()
|
|||||||
obs_module_text("AdvSceneSwitcher.tempVar.window.window"),
|
obs_module_text("AdvSceneSwitcher.tempVar.window.window"),
|
||||||
obs_module_text(
|
obs_module_text(
|
||||||
"AdvSceneSwitcher.tempVar.window.window.description"));
|
"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
|
#ifdef _WIN32
|
||||||
AddTempvar(
|
AddTempvar(
|
||||||
"windowClass",
|
"windowClass",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "macro-condition-edit.hpp"
|
#include "macro-condition-edit.hpp"
|
||||||
|
#include "platform-funcs.hpp"
|
||||||
#include "variable-text-edit.hpp"
|
#include "variable-text-edit.hpp"
|
||||||
#include "regex-config.hpp"
|
#include "regex-config.hpp"
|
||||||
#include "window-selection.hpp"
|
#include "window-selection.hpp"
|
||||||
@@ -37,10 +38,9 @@ public:
|
|||||||
RegexConfig _textRegex = RegexConfig::PartialMatchRegexConfig();
|
RegexConfig _textRegex = RegexConfig::PartialMatchRegexConfig();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool WindowMatchesRequirements(const std::string &window) const;
|
bool WindowMatchesRequirements(const WindowInfo &info) const;
|
||||||
bool WindowMatches(const std::vector<std::string> &windowList);
|
bool FindMatch(const std::vector<WindowInfo> &windows);
|
||||||
bool WindowRegexMatches(const std::vector<std::string> &windowList);
|
void SetVariableValueBasedOnMatch(const WindowInfo *info);
|
||||||
void SetVariableValueBasedOnMatch(const std::string &matchWindow);
|
|
||||||
void SetupTempVars();
|
void SetupTempVars();
|
||||||
|
|
||||||
// For now only supported on Windows
|
// For now only supported on Windows
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "connection-manager.hpp"
|
#include "connection-manager.hpp"
|
||||||
#include "layout-helpers.hpp"
|
#include "layout-helpers.hpp"
|
||||||
|
#include "macro-export-extensions.hpp"
|
||||||
#include "name-dialog.hpp"
|
#include "name-dialog.hpp"
|
||||||
#include "obs-module-helper.hpp"
|
#include "obs-module-helper.hpp"
|
||||||
#include "plugin-state-helpers.hpp"
|
#include "plugin-state-helpers.hpp"
|
||||||
@@ -24,6 +25,51 @@ bool setup()
|
|||||||
AddSaveStep(saveConnections);
|
AddSaveStep(saveConnections);
|
||||||
AddLoadStep(loadConnections);
|
AddLoadStep(loadConnections);
|
||||||
AddPluginCleanupStep([]() { connections.clear(); });
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ void PopulateProfileSelection(QComboBox *box)
|
|||||||
std::string GetPathInProfileDir(const char *filePath)
|
std::string GetPathInProfileDir(const char *filePath)
|
||||||
{
|
{
|
||||||
auto path = obs_frontend_get_current_profile_path();
|
auto path = obs_frontend_get_current_profile_path();
|
||||||
|
if (!path) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
std::string result(path);
|
std::string result(path);
|
||||||
bfree(path);
|
bfree(path);
|
||||||
return result + "/" + filePath;
|
return result + "/" + filePath;
|
||||||
|
|||||||
@@ -205,26 +205,6 @@ static std::vector<HWND> getHWNDfromTitle(const std::string &title)
|
|||||||
return hwnds;
|
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)
|
void SetFocusWindow(const std::string &title)
|
||||||
{
|
{
|
||||||
auto hwnds = getHWNDfromTitle(title);
|
auto hwnds = getHWNDfromTitle(title);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "http-server.hpp"
|
#include "http-server.hpp"
|
||||||
#include "layout-helpers.hpp"
|
#include "layout-helpers.hpp"
|
||||||
#include "log-helper.hpp"
|
#include "log-helper.hpp"
|
||||||
|
#include "macro-export-extensions.hpp"
|
||||||
#include "obs-module-helper.hpp"
|
#include "obs-module-helper.hpp"
|
||||||
#include "plugin-state-helpers.hpp"
|
#include "plugin-state-helpers.hpp"
|
||||||
|
|
||||||
@@ -32,6 +33,49 @@ bool setup()
|
|||||||
}
|
}
|
||||||
httpServers.clear();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ else()
|
|||||||
-Wno-error=unused-parameter -Wno-error=unused-variable
|
-Wno-error=unused-parameter -Wno-error=unused-variable
|
||||||
-Wno-error=sign-compare)
|
-Wno-error=sign-compare)
|
||||||
if(OS_MACOS)
|
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()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
27
plugins/schedule/CMakeLists.txt
Normal file
27
plugins/schedule/CMakeLists.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(advanced-scene-switcher-macro-schedule)
|
||||||
|
|
||||||
|
add_library(${PROJECT_NAME} MODULE)
|
||||||
|
|
||||||
|
target_sources(
|
||||||
|
${PROJECT_NAME}
|
||||||
|
PRIVATE macro-schedule.cpp
|
||||||
|
macro-schedule.hpp
|
||||||
|
macro-schedule-entry-dialog.cpp
|
||||||
|
macro-schedule-entry-dialog.hpp
|
||||||
|
macro-schedule-tab.cpp
|
||||||
|
macro-schedule-tab.hpp
|
||||||
|
calendar/calendar-event.hpp
|
||||||
|
calendar/calendar-view.hpp
|
||||||
|
calendar/calendar-month-view.hpp
|
||||||
|
calendar/calendar-month-view.cpp
|
||||||
|
calendar/calendar-day-view.hpp
|
||||||
|
calendar/calendar-day-view.cpp
|
||||||
|
calendar/calendar-week-view.hpp
|
||||||
|
calendar/calendar-week-view.cpp
|
||||||
|
calendar/calendar-widget.hpp
|
||||||
|
calendar/calendar-widget.cpp)
|
||||||
|
|
||||||
|
setup_advss_plugin(${PROJECT_NAME})
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||||
|
install_advss_plugin(${PROJECT_NAME})
|
||||||
493
plugins/schedule/calendar/calendar-day-view.cpp
Normal file
493
plugins/schedule/calendar/calendar-day-view.cpp
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
#include "calendar-day-view.hpp"
|
||||||
|
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QTimerEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarDayHeader – fixed date strip above the scroll area
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class CalendarDayHeader : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit CalendarDayHeader(QWidget *parent = nullptr);
|
||||||
|
void SetDate(const QDate &date);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDate _date;
|
||||||
|
};
|
||||||
|
|
||||||
|
CalendarDayHeader::CalendarDayHeader(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
setFixedHeight(CalendarDayView::DAY_HEADER_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayHeader::SetDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_date = date;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayHeader::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
const bool isToday = (_date == today);
|
||||||
|
QLocale locale;
|
||||||
|
|
||||||
|
// Time-axis placeholder
|
||||||
|
p.fillRect(0, 0, CalendarDayView::TIME_AXIS_WIDTH, height(),
|
||||||
|
palette().button());
|
||||||
|
|
||||||
|
// Day column
|
||||||
|
const int colX = CalendarDayView::TIME_AXIS_WIDTH;
|
||||||
|
const int colW = width() - colX;
|
||||||
|
|
||||||
|
QColor bg = palette().button().color();
|
||||||
|
if (isToday) {
|
||||||
|
bg = palette().highlight().color().lighter(165);
|
||||||
|
}
|
||||||
|
p.fillRect(colX, 0, colW, height(), bg);
|
||||||
|
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawLine(colX, 0, colX, height());
|
||||||
|
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(13);
|
||||||
|
f.setBold(isToday);
|
||||||
|
p.setFont(f);
|
||||||
|
p.setPen(isToday ? palette().highlight().color()
|
||||||
|
: palette().buttonText().color());
|
||||||
|
|
||||||
|
const QString label =
|
||||||
|
locale.dayName(_date.dayOfWeek(), QLocale::LongFormat) + " " +
|
||||||
|
QString::number(_date.day()) + " " +
|
||||||
|
locale.monthName(_date.month(), QLocale::ShortFormat) + " " +
|
||||||
|
QString::number(_date.year());
|
||||||
|
p.drawText(colX, 0, colW, height(), Qt::AlignCenter, label);
|
||||||
|
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawLine(0, height() - 1, width(), height() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarDayTimeGrid – scrollable single-day painted time grid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class CalendarDayTimeGrid : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarDayTimeGrid(QWidget *parent = nullptr);
|
||||||
|
void SetDate(const QDate &date);
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events);
|
||||||
|
void ScrollToCurrentTime(QScrollArea *sa);
|
||||||
|
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void SlotClicked(const QDateTime &startTime);
|
||||||
|
void EventClicked(const QString &id);
|
||||||
|
void EventDoubleClicked(const QString &id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
void mousePressEvent(QMouseEvent *) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||||
|
void timerEvent(QTimerEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct EventLayout {
|
||||||
|
CalendarEvent event;
|
||||||
|
QTime drawStart; // clipped to the day's [00:00, 23:59:59]
|
||||||
|
QTime drawEnd;
|
||||||
|
int col;
|
||||||
|
int numCols;
|
||||||
|
};
|
||||||
|
|
||||||
|
int TimeToY(const QTime &t) const;
|
||||||
|
QTime YToTime(int y) const;
|
||||||
|
int ColWidth() const;
|
||||||
|
|
||||||
|
QList<EventLayout> LayoutDay() const;
|
||||||
|
QString EventIdAtPoint(const QPoint &pos) const;
|
||||||
|
QDateTime SlotAtPoint(const QPoint &pos) const;
|
||||||
|
|
||||||
|
void DrawEvent(QPainter &p, const EventLayout &layout, int colW);
|
||||||
|
|
||||||
|
QDate _date;
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
int _refreshTimerId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
CalendarDayTimeGrid::CalendarDayTimeGrid(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
_refreshTimerId = startTimer(60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize CalendarDayTimeGrid::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(CalendarDayView::TIME_AXIS_WIDTH + 200,
|
||||||
|
24 * CalendarDayView::PIXELS_PER_HOUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::timerEvent(QTimerEvent *e)
|
||||||
|
{
|
||||||
|
if (e->timerId() == _refreshTimerId) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int CalendarDayTimeGrid::TimeToY(const QTime &t) const
|
||||||
|
{
|
||||||
|
return (t.hour() * 60 + t.minute()) * CalendarDayView::PIXELS_PER_HOUR /
|
||||||
|
60;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTime CalendarDayTimeGrid::YToTime(int y) const
|
||||||
|
{
|
||||||
|
int totalMin = y * 60 / CalendarDayView::PIXELS_PER_HOUR;
|
||||||
|
totalMin = qBound(0, totalMin, 24 * 60 - 1);
|
||||||
|
totalMin = (totalMin / 15) * 15; // snap to 15-min increments
|
||||||
|
return QTime(totalMin / 60, totalMin % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CalendarDayTimeGrid::ColWidth() const
|
||||||
|
{
|
||||||
|
return qMax(0, width() - CalendarDayView::TIME_AXIS_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::SetDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_date = date;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::ScrollToCurrentTime(QScrollArea *sa)
|
||||||
|
{
|
||||||
|
if (!sa) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int y = qMax(0, TimeToY(QTime::currentTime().addSecs(-3600)));
|
||||||
|
sa->verticalScrollBar()->setValue(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ClipEventToDayD(const CalendarEvent &ev, const QDate &date,
|
||||||
|
QTime &drawStart, QTime &drawEnd)
|
||||||
|
{
|
||||||
|
if (!ev.start.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QDateTime dayStart(date, QTime(0, 0, 0));
|
||||||
|
const QDateTime dayEnd(date.addDays(1), QTime(0, 0, 0));
|
||||||
|
const QDateTime evEnd = ev.EffectiveEnd();
|
||||||
|
|
||||||
|
if (ev.start >= dayEnd || evEnd <= dayStart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawStart = (ev.start < dayStart) ? QTime(0, 0, 0) : ev.start.time();
|
||||||
|
drawEnd = (evEnd >= dayEnd) ? QTime(23, 59, 59) : evEnd.time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<CalendarDayTimeGrid::EventLayout> CalendarDayTimeGrid::LayoutDay() const
|
||||||
|
{
|
||||||
|
struct DayEvent {
|
||||||
|
CalendarEvent ev;
|
||||||
|
QTime drawStart;
|
||||||
|
QTime drawEnd;
|
||||||
|
};
|
||||||
|
QList<DayEvent> dayEvents;
|
||||||
|
for (const auto &ev : _events) {
|
||||||
|
QTime ds, de;
|
||||||
|
if (ClipEventToDayD(ev, _date, ds, de)) {
|
||||||
|
dayEvents.append({ev, ds, de});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(dayEvents.begin(), dayEvents.end(),
|
||||||
|
[](const DayEvent &a, const DayEvent &b) {
|
||||||
|
return a.drawStart < b.drawStart;
|
||||||
|
});
|
||||||
|
|
||||||
|
QList<EventLayout> result;
|
||||||
|
QList<QTime> columnEnds;
|
||||||
|
|
||||||
|
for (const auto &de : dayEvents) {
|
||||||
|
int col = -1;
|
||||||
|
for (int i = 0; i < columnEnds.size(); ++i) {
|
||||||
|
if (columnEnds[i] <= de.drawStart) {
|
||||||
|
col = i;
|
||||||
|
columnEnds[i] = de.drawEnd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (col == -1) {
|
||||||
|
col = columnEnds.size();
|
||||||
|
columnEnds.append(de.drawEnd);
|
||||||
|
}
|
||||||
|
result.append({de.ev, de.drawStart, de.drawEnd, col, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
const int totalCols = qMax(1, (int)columnEnds.size());
|
||||||
|
for (auto &layout : result) {
|
||||||
|
layout.numCols = totalCols;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CalendarDayTimeGrid::EventIdAtPoint(const QPoint &pos) const
|
||||||
|
{
|
||||||
|
if (pos.x() < CalendarDayView::TIME_AXIS_WIDTH) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int colW = ColWidth();
|
||||||
|
for (const auto &layout : LayoutDay()) {
|
||||||
|
const int subW = colW / layout.numCols;
|
||||||
|
const int x = CalendarDayView::TIME_AXIS_WIDTH +
|
||||||
|
layout.col * subW + 2;
|
||||||
|
const int w = subW - 4;
|
||||||
|
const int y = TimeToY(layout.drawStart);
|
||||||
|
const int h = qMax(TimeToY(layout.drawEnd) - y, 20);
|
||||||
|
if (QRect(x, y, w, h).contains(pos)) {
|
||||||
|
return layout.event.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime CalendarDayTimeGrid::SlotAtPoint(const QPoint &pos) const
|
||||||
|
{
|
||||||
|
if (pos.x() < CalendarDayView::TIME_AXIS_WIDTH) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QDateTime(_date, YToTime(pos.y()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::DrawEvent(QPainter &p, const EventLayout &layout,
|
||||||
|
int colW)
|
||||||
|
{
|
||||||
|
const auto &ev = layout.event;
|
||||||
|
const int subW = colW / layout.numCols;
|
||||||
|
const int x = CalendarDayView::TIME_AXIS_WIDTH + layout.col * subW + 2;
|
||||||
|
const int w = subW - 4;
|
||||||
|
const int y = TimeToY(layout.drawStart);
|
||||||
|
const int endY = TimeToY(layout.drawEnd);
|
||||||
|
const int h = qMax(endY - y, 20);
|
||||||
|
const QRect rect(x, y, w, h);
|
||||||
|
|
||||||
|
p.setBrush(ev.color);
|
||||||
|
p.setPen(ev.color.darker(130));
|
||||||
|
p.drawRoundedRect(rect, 3, 3);
|
||||||
|
|
||||||
|
p.fillRect(x, y + 1, 3, h - 2, ev.color.darker(160));
|
||||||
|
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(10);
|
||||||
|
f.setBold(true);
|
||||||
|
p.setFont(f);
|
||||||
|
|
||||||
|
const QRect textRect = rect.adjusted(6, 2, -2, -2);
|
||||||
|
if (h >= 34) {
|
||||||
|
QFont tf = f;
|
||||||
|
tf.setBold(false);
|
||||||
|
p.setFont(tf);
|
||||||
|
p.drawText(textRect, Qt::AlignTop | Qt::AlignLeft,
|
||||||
|
ev.start.toString("HH:mm"));
|
||||||
|
p.setFont(f);
|
||||||
|
p.drawText(textRect.adjusted(0, 14, 0, 0),
|
||||||
|
Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap,
|
||||||
|
ev.title);
|
||||||
|
} else {
|
||||||
|
p.drawText(textRect,
|
||||||
|
Qt::AlignVCenter | Qt::AlignLeft |
|
||||||
|
Qt::TextSingleLine,
|
||||||
|
ev.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const int w = width();
|
||||||
|
const int h = height();
|
||||||
|
const int colW = ColWidth();
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
|
||||||
|
p.fillRect(0, 0, w, h, palette().base());
|
||||||
|
p.fillRect(0, 0, CalendarDayView::TIME_AXIS_WIDTH, h,
|
||||||
|
palette().button());
|
||||||
|
|
||||||
|
// Day column background
|
||||||
|
const int colX = CalendarDayView::TIME_AXIS_WIDTH;
|
||||||
|
if (_date == today) {
|
||||||
|
p.fillRect(colX, 0, colW, h,
|
||||||
|
palette().highlight().color().lighter(190));
|
||||||
|
} else if (_date.dayOfWeek() >= 6) {
|
||||||
|
p.fillRect(colX, 0, colW, h, palette().alternateBase().color());
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont labelFont = p.font();
|
||||||
|
labelFont.setPixelSize(10);
|
||||||
|
p.setFont(labelFont);
|
||||||
|
|
||||||
|
for (int hour = 0; hour < 24; ++hour) {
|
||||||
|
const int y = hour * CalendarDayView::PIXELS_PER_HOUR;
|
||||||
|
|
||||||
|
p.setPen(palette().text().color());
|
||||||
|
p.drawText(2, y, CalendarDayView::TIME_AXIS_WIDTH - 6,
|
||||||
|
CalendarDayView::PIXELS_PER_HOUR,
|
||||||
|
Qt::AlignTop | Qt::AlignRight,
|
||||||
|
QTime(hour, 0).toString("HH:mm"));
|
||||||
|
|
||||||
|
p.setPen(QPen(palette().mid().color(), 1));
|
||||||
|
p.drawLine(CalendarDayView::TIME_AXIS_WIDTH, y, w, y);
|
||||||
|
|
||||||
|
QPen halfPen(palette().midlight().color(), 1, Qt::DotLine);
|
||||||
|
p.setPen(halfPen);
|
||||||
|
const int yHalf = y + CalendarDayView::PIXELS_PER_HOUR / 2;
|
||||||
|
p.drawLine(CalendarDayView::TIME_AXIS_WIDTH, yHalf, w, yHalf);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(QPen(palette().mid().color(), 1));
|
||||||
|
p.drawLine(colX, 0, colX, h);
|
||||||
|
|
||||||
|
for (const auto &layout : LayoutDay()) {
|
||||||
|
DrawEvent(p, layout, colW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current-time indicator
|
||||||
|
if (_date == today) {
|
||||||
|
const int y = TimeToY(QTime::currentTime());
|
||||||
|
p.setPen(QPen(QColor(220, 30, 30), 2));
|
||||||
|
p.drawLine(colX, y, w, y);
|
||||||
|
p.setBrush(QColor(220, 30, 30));
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawEllipse(colX - 4, y - 4, 8, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::mousePressEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventClicked(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QDateTime slot = SlotAtPoint(e->pos());
|
||||||
|
if (slot.isValid()) {
|
||||||
|
emit SlotClicked(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayTimeGrid::mouseDoubleClickEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventDoubleClicked(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarDayView
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CalendarDayView::CalendarDayView(QWidget *parent)
|
||||||
|
: CalendarView(parent),
|
||||||
|
_header(new CalendarDayHeader(this)),
|
||||||
|
_timeGrid(new CalendarDayTimeGrid(this)),
|
||||||
|
_scrollArea(new QScrollArea(this))
|
||||||
|
{
|
||||||
|
_timeGrid->setMinimumHeight(24 * CalendarDayView::PIXELS_PER_HOUR);
|
||||||
|
|
||||||
|
_scrollArea->setWidget(_timeGrid);
|
||||||
|
_scrollArea->setWidgetResizable(true);
|
||||||
|
_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
|
|
||||||
|
auto layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
layout->addWidget(_header);
|
||||||
|
layout->addWidget(_scrollArea);
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
connect(_timeGrid, &CalendarDayTimeGrid::SlotClicked, this,
|
||||||
|
&CalendarDayView::SlotClicked);
|
||||||
|
connect(_timeGrid, &CalendarDayTimeGrid::EventClicked, this,
|
||||||
|
&CalendarDayView::EventClicked);
|
||||||
|
connect(_timeGrid, &CalendarDayTimeGrid::EventDoubleClicked, this,
|
||||||
|
&CalendarDayView::EventDoubleClicked);
|
||||||
|
|
||||||
|
SetDate(QDate::currentDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayView::SetDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_date = date;
|
||||||
|
UpdateViews();
|
||||||
|
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayView::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
_timeGrid->SetEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarDayView::CurrentDate() const
|
||||||
|
{
|
||||||
|
return _date;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarDayView::RangeStart() const
|
||||||
|
{
|
||||||
|
return _date;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarDayView::RangeEnd() const
|
||||||
|
{
|
||||||
|
return _date;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarDayView::UpdateViews()
|
||||||
|
{
|
||||||
|
_header->SetDate(_date);
|
||||||
|
_timeGrid->SetDate(_date);
|
||||||
|
_timeGrid->ScrollToCurrentTime(_scrollArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
|
|
||||||
|
// Required for CalendarDayTimeGrid defined in this file
|
||||||
|
#include "calendar-day-view.moc"
|
||||||
44
plugins/schedule/calendar/calendar-day-view.hpp
Normal file
44
plugins/schedule/calendar/calendar-day-view.hpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "calendar-view.hpp"
|
||||||
|
|
||||||
|
#include <QScrollArea>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
class CalendarDayHeader;
|
||||||
|
class CalendarDayTimeGrid;
|
||||||
|
|
||||||
|
// Displays a single day column with a vertical time axis (00:00 - 24:00).
|
||||||
|
// Events are rendered as colored blocks sized to their duration.
|
||||||
|
// Overlapping events are arranged in side-by-side sub-columns.
|
||||||
|
// A red indicator marks the current time.
|
||||||
|
class CalendarDayView : public CalendarView {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarDayView(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void SetDate(const QDate &date) override;
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||||
|
|
||||||
|
QDate CurrentDate() const override;
|
||||||
|
QDate RangeStart() const override;
|
||||||
|
QDate RangeEnd() const override;
|
||||||
|
|
||||||
|
// Shared layout constants (used by DayHeader and TimeGrid)
|
||||||
|
static constexpr int TIME_AXIS_WIDTH = 56;
|
||||||
|
static constexpr int PIXELS_PER_HOUR = 64;
|
||||||
|
static constexpr int DAY_HEADER_HEIGHT = 36;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void UpdateViews();
|
||||||
|
|
||||||
|
CalendarDayHeader *_header;
|
||||||
|
CalendarDayTimeGrid *_timeGrid;
|
||||||
|
QScrollArea *_scrollArea;
|
||||||
|
|
||||||
|
QDate _date;
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
29
plugins/schedule/calendar/calendar-event.hpp
Normal file
29
plugins/schedule/calendar/calendar-event.hpp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
// Generic calendar event. All fields except id/start are optional.
|
||||||
|
struct CalendarEvent {
|
||||||
|
QString id;
|
||||||
|
QString title;
|
||||||
|
QDateTime start;
|
||||||
|
QDateTime end;
|
||||||
|
QColor color{70, 130, 180};
|
||||||
|
QVariant userData; // Caller-defined payload, returned on click signals
|
||||||
|
|
||||||
|
// Returns end if valid and > start, otherwise start + 30 minutes.
|
||||||
|
QDateTime EffectiveEnd() const
|
||||||
|
{
|
||||||
|
static constexpr qint64 defaultDuration = 1800;
|
||||||
|
return (end.isValid() && end > start)
|
||||||
|
? end
|
||||||
|
: start.addSecs(defaultDuration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
284
plugins/schedule/calendar/calendar-month-view.cpp
Normal file
284
plugins/schedule/calendar/calendar-month-view.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#include "calendar-month-view.hpp"
|
||||||
|
#include "obs-module-helper.hpp"
|
||||||
|
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
CalendarMonthView::CalendarMonthView(QWidget *parent) : CalendarView(parent)
|
||||||
|
{
|
||||||
|
setMinimumSize(320, 240);
|
||||||
|
SetDate(QDate::currentDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarMonthView::SetDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_currentDate = date;
|
||||||
|
|
||||||
|
// Grid starts on the Monday of the week that contains the 1st of the month.
|
||||||
|
const QDate first(date.year(), date.month(), 1);
|
||||||
|
// Qt: dayOfWeek() -> 1=Mon … 7=Sun
|
||||||
|
_gridStart = first.addDays(-(first.dayOfWeek() - 1));
|
||||||
|
|
||||||
|
update();
|
||||||
|
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarMonthView::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Geometry helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QDate CalendarMonthView::CellDate(int row, int col) const
|
||||||
|
{
|
||||||
|
return _gridStart.addDays(row * COLS + col);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect CalendarMonthView::CellRect(int row, int col) const
|
||||||
|
{
|
||||||
|
const int cellW = width() / COLS;
|
||||||
|
const int cellH = (height() - HEADER_HEIGHT) / ROWS;
|
||||||
|
return QRect(col * cellW, HEADER_HEIGHT + row * cellH, cellW, cellH);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CalendarMonthView::CellFromPoint(const QPoint &pos, int &row,
|
||||||
|
int &col) const
|
||||||
|
{
|
||||||
|
if (pos.y() < HEADER_HEIGHT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int cellW = width() / COLS;
|
||||||
|
const int cellH = (height() - HEADER_HEIGHT) / ROWS;
|
||||||
|
col = pos.x() / cellW;
|
||||||
|
row = (pos.y() - HEADER_HEIGHT) / cellH;
|
||||||
|
return col >= 0 && col < COLS && row >= 0 && row < ROWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Event helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QList<CalendarEvent> CalendarMonthView::EventsForDate(const QDate &date) const
|
||||||
|
{
|
||||||
|
QList<CalendarEvent> result;
|
||||||
|
for (const auto &event : _events) {
|
||||||
|
if (!event.start.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QDate evStart = event.start.date();
|
||||||
|
const QDate evEnd = event.EffectiveEnd().date();
|
||||||
|
if (date >= evStart && date <= evEnd) {
|
||||||
|
result.append(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CalendarMonthView::EventIdAtPoint(const QPoint &pos) const
|
||||||
|
{
|
||||||
|
int row, col;
|
||||||
|
if (!CellFromPoint(pos, row, col)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRect cell = CellRect(row, col);
|
||||||
|
const int evTop = cell.top() + DAY_NUM_HEIGHT + CELL_PAD;
|
||||||
|
const auto events = EventsForDate(CellDate(row, col));
|
||||||
|
|
||||||
|
for (int i = 0; i < qMin((int)events.size(), MAX_VISIBLE_EVENTS); ++i) {
|
||||||
|
const QRect evRect(cell.left() + CELL_PAD,
|
||||||
|
evTop + i * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||||
|
cell.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||||
|
if (evRect.contains(pos)) {
|
||||||
|
return events[i].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Painting
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarMonthView::PaintDayNameHeader(QPainter &p)
|
||||||
|
{
|
||||||
|
p.fillRect(0, 0, width(), HEADER_HEIGHT, palette().button());
|
||||||
|
|
||||||
|
const int cellW = width() / COLS;
|
||||||
|
QLocale locale;
|
||||||
|
|
||||||
|
p.setPen(palette().buttonText().color());
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(11);
|
||||||
|
p.setFont(f);
|
||||||
|
|
||||||
|
for (int col = 0; col < COLS; ++col) {
|
||||||
|
// dayOfWeek: 1=Mon … 7=Sun (ISO 8601 order matches our columns)
|
||||||
|
const QString name =
|
||||||
|
locale.dayName(col + 1, QLocale::ShortFormat);
|
||||||
|
p.drawText(col * cellW, 0, cellW, HEADER_HEIGHT,
|
||||||
|
Qt::AlignCenter, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom border
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawLine(0, HEADER_HEIGHT - 1, width(), HEADER_HEIGHT - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarMonthView::PaintCell(QPainter &p, int row, int col)
|
||||||
|
{
|
||||||
|
const QRect rect = CellRect(row, col);
|
||||||
|
const QDate date = CellDate(row, col);
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
const bool isToday = (date == today);
|
||||||
|
const bool inCurrentMonth = (date.month() == _currentDate.month());
|
||||||
|
|
||||||
|
// --- Background ---
|
||||||
|
QColor bg;
|
||||||
|
if (isToday) {
|
||||||
|
bg = palette().highlight().color().lighter(185);
|
||||||
|
} else if (!inCurrentMonth) {
|
||||||
|
bg = palette().alternateBase().color();
|
||||||
|
} else {
|
||||||
|
bg = palette().base().color();
|
||||||
|
}
|
||||||
|
// Weekend tint
|
||||||
|
if (col >= 5) {
|
||||||
|
bg = bg.darker(104);
|
||||||
|
}
|
||||||
|
p.fillRect(rect, bg);
|
||||||
|
|
||||||
|
// --- Grid border ---
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawRect(rect.adjusted(0, 0, -1, -1));
|
||||||
|
|
||||||
|
// --- Day number ---
|
||||||
|
const QRect numArea(rect.left(), rect.top() + CELL_PAD,
|
||||||
|
rect.width() - CELL_PAD, DAY_NUM_HEIGHT);
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// Filled circle behind the number
|
||||||
|
const int dia = DAY_NUM_HEIGHT - 4;
|
||||||
|
const QRect circleRect(numArea.right() - dia - 2,
|
||||||
|
numArea.top() + 1, dia, dia);
|
||||||
|
p.setBrush(palette().highlight());
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawEllipse(circleRect);
|
||||||
|
p.setPen(palette().highlightedText().color());
|
||||||
|
} else {
|
||||||
|
p.setPen(inCurrentMonth ? palette().text().color()
|
||||||
|
: palette().placeholderText().color());
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(11);
|
||||||
|
f.setBold(isToday);
|
||||||
|
p.setFont(f);
|
||||||
|
p.drawText(numArea, Qt::AlignRight | Qt::AlignTop,
|
||||||
|
QString::number(date.day()));
|
||||||
|
|
||||||
|
// --- Event bars ---
|
||||||
|
const auto events = EventsForDate(date);
|
||||||
|
const int evTop = rect.top() + DAY_NUM_HEIGHT + CELL_PAD;
|
||||||
|
const int maxFit = (rect.height() - DAY_NUM_HEIGHT - CELL_PAD * 2) /
|
||||||
|
(EVENT_HEIGHT + EVENT_MARGIN);
|
||||||
|
const int visible =
|
||||||
|
qMin(qMin((int)events.size(), maxFit), MAX_VISIBLE_EVENTS);
|
||||||
|
|
||||||
|
QFont evFont = p.font();
|
||||||
|
evFont.setPixelSize(10);
|
||||||
|
evFont.setBold(false);
|
||||||
|
p.setFont(evFont);
|
||||||
|
|
||||||
|
for (int i = 0; i < visible; ++i) {
|
||||||
|
const auto &ev = events[i];
|
||||||
|
const QRect evRect(rect.left() + CELL_PAD,
|
||||||
|
evTop + i * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||||
|
rect.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||||
|
|
||||||
|
p.setBrush(ev.color);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawRoundedRect(evRect, 2, 2);
|
||||||
|
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
p.drawText(evRect.adjusted(4, 0, -2, 0),
|
||||||
|
Qt::AlignVCenter | Qt::AlignLeft |
|
||||||
|
Qt::TextSingleLine,
|
||||||
|
ev.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "+N more" overflow label
|
||||||
|
if ((int)events.size() > visible) {
|
||||||
|
const int extra = events.size() - visible;
|
||||||
|
const QRect moreRect(
|
||||||
|
rect.left() + CELL_PAD,
|
||||||
|
evTop + visible * (EVENT_HEIGHT + EVENT_MARGIN),
|
||||||
|
rect.width() - CELL_PAD * 2, EVENT_HEIGHT);
|
||||||
|
p.setPen(palette().placeholderText().color());
|
||||||
|
p.drawText(
|
||||||
|
moreRect,
|
||||||
|
Qt::AlignVCenter | Qt::AlignLeft | Qt::TextSingleLine,
|
||||||
|
QString(obs_module_text(
|
||||||
|
"AdvSceneSwitcher.calendar.moreEvents"))
|
||||||
|
.arg(extra));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarMonthView::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
PaintDayNameHeader(p);
|
||||||
|
|
||||||
|
for (int row = 0; row < ROWS; ++row) {
|
||||||
|
for (int col = 0; col < COLS; ++col) {
|
||||||
|
PaintCell(p, row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mouse events
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarMonthView::mousePressEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventClicked(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int row, col;
|
||||||
|
if (CellFromPoint(e->pos(), row, col)) {
|
||||||
|
emit SlotClicked(QDateTime(CellDate(row, col), QTime(0, 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarMonthView::mouseDoubleClickEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventDoubleClicked(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
57
plugins/schedule/calendar/calendar-month-view.hpp
Normal file
57
plugins/schedule/calendar/calendar-month-view.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "calendar-view.hpp"
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
// Displays a traditional month grid (6 weeks × 7 days).
|
||||||
|
// Events are rendered as small colored bars inside each day cell.
|
||||||
|
// Up to three events are shown per cell; additional events are
|
||||||
|
// indicated by a "+N more" label.
|
||||||
|
class CalendarMonthView : public CalendarView {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarMonthView(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void SetDate(const QDate &date) override;
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||||
|
|
||||||
|
QDate CurrentDate() const override { return _currentDate; }
|
||||||
|
QDate RangeStart() const override { return _gridStart; }
|
||||||
|
QDate RangeEnd() const override { return _gridStart.addDays(41); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
void mousePressEvent(QMouseEvent *) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Geometry helpers
|
||||||
|
QDate CellDate(int row, int col) const;
|
||||||
|
QRect CellRect(int row, int col) const;
|
||||||
|
bool CellFromPoint(const QPoint &pos, int &row, int &col) const;
|
||||||
|
|
||||||
|
// Event helpers
|
||||||
|
QList<CalendarEvent> EventsForDate(const QDate &date) const;
|
||||||
|
QString EventIdAtPoint(const QPoint &pos) const;
|
||||||
|
|
||||||
|
// Painting
|
||||||
|
void PaintDayNameHeader(QPainter &p);
|
||||||
|
void PaintCell(QPainter &p, int row, int col);
|
||||||
|
|
||||||
|
static constexpr int ROWS = 6;
|
||||||
|
static constexpr int COLS = 7;
|
||||||
|
static constexpr int HEADER_HEIGHT = 28; // day-name row
|
||||||
|
static constexpr int DAY_NUM_HEIGHT =
|
||||||
|
22; // space reserved for day number
|
||||||
|
static constexpr int EVENT_HEIGHT = 16;
|
||||||
|
static constexpr int EVENT_MARGIN = 2;
|
||||||
|
static constexpr int CELL_PAD = 3;
|
||||||
|
static constexpr int MAX_VISIBLE_EVENTS = 3;
|
||||||
|
|
||||||
|
QDate _currentDate;
|
||||||
|
QDate _gridStart; // Monday of the week that contains the 1st of the month
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
49
plugins/schedule/calendar/calendar-view.hpp
Normal file
49
plugins/schedule/calendar/calendar-view.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "calendar-event.hpp"
|
||||||
|
|
||||||
|
#include <QDate>
|
||||||
|
#include <QList>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
// Abstract base class for calendar view modes (month, week, …).
|
||||||
|
// Subclasses must implement the pure virtual interface and emit the
|
||||||
|
// signals defined here via Q_SIGNALS so that CalendarWidget can
|
||||||
|
// connect to them uniformly regardless of the active view.
|
||||||
|
class CalendarView : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarView(QWidget *parent = nullptr) : QWidget(parent) {}
|
||||||
|
|
||||||
|
// Navigate to show the given date. The visible range is determined
|
||||||
|
// by the concrete view (e.g. the whole month, or the week).
|
||||||
|
virtual void SetDate(const QDate &date) = 0;
|
||||||
|
|
||||||
|
// Replace the full set of events shown in this view.
|
||||||
|
virtual void SetEvents(const QList<CalendarEvent> &events) = 0;
|
||||||
|
|
||||||
|
// A representative date for the current position (used for navigation).
|
||||||
|
virtual QDate CurrentDate() const = 0;
|
||||||
|
|
||||||
|
// Inclusive range of dates currently rendered.
|
||||||
|
virtual QDate RangeStart() const = 0;
|
||||||
|
virtual QDate RangeEnd() const = 0;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// User clicked an empty time slot.
|
||||||
|
void SlotClicked(const QDateTime &startTime);
|
||||||
|
|
||||||
|
// User single-clicked an event (id = CalendarEvent::id).
|
||||||
|
void EventClicked(const QString &eventId);
|
||||||
|
|
||||||
|
// User double-clicked an event.
|
||||||
|
void EventDoubleClicked(const QString &eventId);
|
||||||
|
|
||||||
|
// The visible date range changed; the owner should refresh events.
|
||||||
|
void VisibleRangeChanged(const QDate &rangeStart,
|
||||||
|
const QDate &rangeEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
542
plugins/schedule/calendar/calendar-week-view.cpp
Normal file
542
plugins/schedule/calendar/calendar-week-view.cpp
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
#include "calendar-week-view.hpp"
|
||||||
|
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QTimerEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarWeekDayHeader – fixed day-name strip above the scroll area
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class CalendarWeekDayHeader : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit CalendarWeekDayHeader(QWidget *parent = nullptr);
|
||||||
|
void SetStartOfWeek(const QDate &date);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDate _startOfWeek;
|
||||||
|
};
|
||||||
|
|
||||||
|
CalendarWeekDayHeader::CalendarWeekDayHeader(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
setFixedHeight(CalendarWeekView::DAY_HEADER_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekDayHeader::SetStartOfWeek(const QDate &date)
|
||||||
|
{
|
||||||
|
_startOfWeek = date;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekDayHeader::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const int dayW =
|
||||||
|
std::max(CalendarWeekView::MIN_DAY_WIDTH,
|
||||||
|
(width() - CalendarWeekView::TIME_AXIS_WIDTH) / 7);
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
QLocale locale;
|
||||||
|
|
||||||
|
// Time-axis placeholder
|
||||||
|
p.fillRect(0, 0, CalendarWeekView::TIME_AXIS_WIDTH, height(),
|
||||||
|
palette().button());
|
||||||
|
|
||||||
|
for (int d = 0; d < 7; ++d) {
|
||||||
|
const QDate date = _startOfWeek.addDays(d);
|
||||||
|
const int x = CalendarWeekView::TIME_AXIS_WIDTH + d * dayW;
|
||||||
|
const bool isToday = (date == today);
|
||||||
|
|
||||||
|
QColor bg = palette().button().color();
|
||||||
|
if (isToday) {
|
||||||
|
bg = palette().highlight().color().lighter(165);
|
||||||
|
} else if (date.dayOfWeek() >= 6) {
|
||||||
|
bg = bg.darker(106);
|
||||||
|
}
|
||||||
|
p.fillRect(x, 0, dayW, height(), bg);
|
||||||
|
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawLine(x, 0, x, height());
|
||||||
|
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(12);
|
||||||
|
f.setBold(isToday);
|
||||||
|
p.setFont(f);
|
||||||
|
p.setPen(isToday ? palette().highlight().color()
|
||||||
|
: palette().buttonText().color());
|
||||||
|
|
||||||
|
const QString label =
|
||||||
|
locale.dayName(date.dayOfWeek(), QLocale::ShortFormat) +
|
||||||
|
" " + QString::number(date.day());
|
||||||
|
p.drawText(x, 0, dayW, height(), Qt::AlignCenter, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawLine(0, height() - 1, width(), height() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarWeekTimeGrid – scrollable painted time grid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class CalendarWeekTimeGrid : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarWeekTimeGrid(QWidget *parent = nullptr);
|
||||||
|
void SetStartOfWeek(const QDate &date);
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events);
|
||||||
|
void ScrollToCurrentTime(QScrollArea *sa);
|
||||||
|
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void SlotClicked(const QDateTime &startTime);
|
||||||
|
void EventClicked(const QString &id);
|
||||||
|
void EventDoubleClicked(const QString &id);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
void mousePressEvent(QMouseEvent *) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *) override;
|
||||||
|
void timerEvent(QTimerEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct EventLayout {
|
||||||
|
CalendarEvent event;
|
||||||
|
QTime drawStart; // clipped to the day's [00:00, 23:59:59]
|
||||||
|
QTime drawEnd;
|
||||||
|
int col;
|
||||||
|
int numCols;
|
||||||
|
};
|
||||||
|
|
||||||
|
int TimeToY(const QTime &t) const;
|
||||||
|
QTime YToTime(int y) const;
|
||||||
|
int DayWidth() const;
|
||||||
|
int DayColumnX(int dayIndex) const;
|
||||||
|
|
||||||
|
QList<EventLayout> LayoutDay(int dayIndex) const;
|
||||||
|
QString EventIdAtPoint(const QPoint &pos) const;
|
||||||
|
QDateTime SlotAtPoint(const QPoint &pos) const;
|
||||||
|
|
||||||
|
void DrawEvent(QPainter &p, const EventLayout &layout, int dayX,
|
||||||
|
int dayW);
|
||||||
|
|
||||||
|
QDate _startOfWeek;
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
int _refreshTimerId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
CalendarWeekTimeGrid::CalendarWeekTimeGrid(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
_refreshTimerId = startTimer(60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize CalendarWeekTimeGrid::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(CalendarWeekView::TIME_AXIS_WIDTH +
|
||||||
|
7 * CalendarWeekView::MIN_DAY_WIDTH,
|
||||||
|
24 * CalendarWeekView::PIXELS_PER_HOUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::timerEvent(QTimerEvent *e)
|
||||||
|
{
|
||||||
|
if (e->timerId() == _refreshTimerId) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int CalendarWeekTimeGrid::TimeToY(const QTime &t) const
|
||||||
|
{
|
||||||
|
return (t.hour() * 60 + t.minute()) *
|
||||||
|
CalendarWeekView::PIXELS_PER_HOUR / 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTime CalendarWeekTimeGrid::YToTime(int y) const
|
||||||
|
{
|
||||||
|
int totalMin = y * 60 / CalendarWeekView::PIXELS_PER_HOUR;
|
||||||
|
totalMin = qBound(0, totalMin, 24 * 60 - 1);
|
||||||
|
totalMin = (totalMin / 15) * 15; // snap to 15-min increments
|
||||||
|
return QTime(totalMin / 60, totalMin % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CalendarWeekTimeGrid::DayWidth() const
|
||||||
|
{
|
||||||
|
return std::max(CalendarWeekView::MIN_DAY_WIDTH,
|
||||||
|
(width() - CalendarWeekView::TIME_AXIS_WIDTH) / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CalendarWeekTimeGrid::DayColumnX(int dayIndex) const
|
||||||
|
{
|
||||||
|
return CalendarWeekView::TIME_AXIS_WIDTH + dayIndex * DayWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::SetStartOfWeek(const QDate &date)
|
||||||
|
{
|
||||||
|
_startOfWeek = date;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::ScrollToCurrentTime(QScrollArea *sa)
|
||||||
|
{
|
||||||
|
if (!sa) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int y = qMax(0, TimeToY(QTime::currentTime().addSecs(-3600)));
|
||||||
|
sa->verticalScrollBar()->setValue(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the portion of an event visible within [date 00:00, date+1 00:00).
|
||||||
|
// drawStart / drawEnd are times in that day's coordinate space.
|
||||||
|
static bool ClipEventToDay(const CalendarEvent &ev, const QDate &date,
|
||||||
|
QTime &drawStart, QTime &drawEnd)
|
||||||
|
{
|
||||||
|
if (!ev.start.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QDateTime dayStart(date, QTime(0, 0, 0));
|
||||||
|
const QDateTime dayEnd(date.addDays(1), QTime(0, 0, 0));
|
||||||
|
const QDateTime evEnd = ev.EffectiveEnd();
|
||||||
|
|
||||||
|
if (ev.start >= dayEnd || evEnd <= dayStart) {
|
||||||
|
return false; // no overlap
|
||||||
|
}
|
||||||
|
|
||||||
|
drawStart = (ev.start < dayStart) ? QTime(0, 0, 0) : ev.start.time();
|
||||||
|
drawEnd = (evEnd >= dayEnd) ? QTime(23, 59, 59) : evEnd.time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<CalendarWeekTimeGrid::EventLayout>
|
||||||
|
CalendarWeekTimeGrid::LayoutDay(int dayIndex) const
|
||||||
|
{
|
||||||
|
const QDate date = _startOfWeek.addDays(dayIndex);
|
||||||
|
|
||||||
|
// Collect events that overlap this day, sorted by their clipped start.
|
||||||
|
struct DayEvent {
|
||||||
|
CalendarEvent ev;
|
||||||
|
QTime drawStart;
|
||||||
|
QTime drawEnd;
|
||||||
|
};
|
||||||
|
QList<DayEvent> dayEvents;
|
||||||
|
for (const auto &ev : _events) {
|
||||||
|
QTime ds, de;
|
||||||
|
if (ClipEventToDay(ev, date, ds, de)) {
|
||||||
|
dayEvents.append({ev, ds, de});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(dayEvents.begin(), dayEvents.end(),
|
||||||
|
[](const DayEvent &a, const DayEvent &b) {
|
||||||
|
return a.drawStart < b.drawStart;
|
||||||
|
});
|
||||||
|
|
||||||
|
QList<EventLayout> result;
|
||||||
|
QList<QTime> columnEnds;
|
||||||
|
|
||||||
|
for (const auto &de : dayEvents) {
|
||||||
|
int col = -1;
|
||||||
|
for (int i = 0; i < columnEnds.size(); ++i) {
|
||||||
|
if (columnEnds[i] <= de.drawStart) {
|
||||||
|
col = i;
|
||||||
|
columnEnds[i] = de.drawEnd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (col == -1) {
|
||||||
|
col = columnEnds.size();
|
||||||
|
columnEnds.append(de.drawEnd);
|
||||||
|
}
|
||||||
|
result.append({de.ev, de.drawStart, de.drawEnd, col, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
const int totalCols = qMax(1, (int)columnEnds.size());
|
||||||
|
for (auto &layout : result) {
|
||||||
|
layout.numCols = totalCols;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CalendarWeekTimeGrid::EventIdAtPoint(const QPoint &pos) const
|
||||||
|
{
|
||||||
|
if (pos.x() < CalendarWeekView::TIME_AXIS_WIDTH) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int dayW = DayWidth();
|
||||||
|
const int dayIdx = (pos.x() - CalendarWeekView::TIME_AXIS_WIDTH) / dayW;
|
||||||
|
if (dayIdx < 0 || dayIdx >= 7) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const int dayX = DayColumnX(dayIdx);
|
||||||
|
for (const auto &layout : LayoutDay(dayIdx)) {
|
||||||
|
const int colW = dayW / layout.numCols;
|
||||||
|
const int x = dayX + layout.col * colW + 2;
|
||||||
|
const int w = colW - 4;
|
||||||
|
const int y = TimeToY(layout.drawStart);
|
||||||
|
const int h = qMax(TimeToY(layout.drawEnd) - y, 20);
|
||||||
|
if (QRect(x, y, w, h).contains(pos)) {
|
||||||
|
return layout.event.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime CalendarWeekTimeGrid::SlotAtPoint(const QPoint &pos) const
|
||||||
|
{
|
||||||
|
if (pos.x() < CalendarWeekView::TIME_AXIS_WIDTH) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int dayW = DayWidth();
|
||||||
|
const int dayIdx = (pos.x() - CalendarWeekView::TIME_AXIS_WIDTH) / dayW;
|
||||||
|
if (dayIdx < 0 || dayIdx >= 7) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QDateTime(_startOfWeek.addDays(dayIdx), YToTime(pos.y()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::DrawEvent(QPainter &p, const EventLayout &layout,
|
||||||
|
int dayX, int dayW)
|
||||||
|
{
|
||||||
|
const auto &ev = layout.event;
|
||||||
|
const int colW = dayW / layout.numCols;
|
||||||
|
const int x = dayX + layout.col * colW + 2;
|
||||||
|
const int w = colW - 4;
|
||||||
|
const int y = TimeToY(layout.drawStart);
|
||||||
|
const int endY = TimeToY(layout.drawEnd);
|
||||||
|
const int h = qMax(endY - y, 20);
|
||||||
|
const QRect rect(x, y, w, h);
|
||||||
|
|
||||||
|
p.setBrush(ev.color);
|
||||||
|
p.setPen(ev.color.darker(130));
|
||||||
|
p.drawRoundedRect(rect, 3, 3);
|
||||||
|
|
||||||
|
p.fillRect(x, y + 1, 3, h - 2, ev.color.darker(160));
|
||||||
|
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
|
||||||
|
QFont f = p.font();
|
||||||
|
f.setPixelSize(10);
|
||||||
|
f.setBold(true);
|
||||||
|
p.setFont(f);
|
||||||
|
|
||||||
|
const QRect textRect = rect.adjusted(6, 2, -2, -2);
|
||||||
|
if (h >= 34) {
|
||||||
|
QFont tf = f;
|
||||||
|
tf.setBold(false);
|
||||||
|
p.setFont(tf);
|
||||||
|
p.drawText(textRect, Qt::AlignTop | Qt::AlignLeft,
|
||||||
|
ev.start.toString("HH:mm"));
|
||||||
|
p.setFont(f);
|
||||||
|
p.drawText(textRect.adjusted(0, 14, 0, 0),
|
||||||
|
Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap,
|
||||||
|
ev.title);
|
||||||
|
} else {
|
||||||
|
p.drawText(textRect,
|
||||||
|
Qt::AlignVCenter | Qt::AlignLeft |
|
||||||
|
Qt::TextSingleLine,
|
||||||
|
ev.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const int w = width();
|
||||||
|
const int h = height();
|
||||||
|
const int dayW = DayWidth();
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
|
||||||
|
p.fillRect(0, 0, w, h, palette().base());
|
||||||
|
p.fillRect(0, 0, CalendarWeekView::TIME_AXIS_WIDTH, h,
|
||||||
|
palette().button());
|
||||||
|
|
||||||
|
for (int d = 0; d < 7; ++d) {
|
||||||
|
const QDate date = _startOfWeek.addDays(d);
|
||||||
|
const int x = DayColumnX(d);
|
||||||
|
if (date == today) {
|
||||||
|
p.fillRect(x, 0, dayW, h,
|
||||||
|
palette().highlight().color().lighter(190));
|
||||||
|
} else if (date.dayOfWeek() >= 6) {
|
||||||
|
p.fillRect(x, 0, dayW, h,
|
||||||
|
palette().alternateBase().color());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont labelFont = p.font();
|
||||||
|
labelFont.setPixelSize(10);
|
||||||
|
p.setFont(labelFont);
|
||||||
|
|
||||||
|
for (int hour = 0; hour < 24; ++hour) {
|
||||||
|
const int y = hour * CalendarWeekView::PIXELS_PER_HOUR;
|
||||||
|
|
||||||
|
p.setPen(palette().text().color());
|
||||||
|
p.drawText(2, y, CalendarWeekView::TIME_AXIS_WIDTH - 6,
|
||||||
|
CalendarWeekView::PIXELS_PER_HOUR,
|
||||||
|
Qt::AlignTop | Qt::AlignRight,
|
||||||
|
QTime(hour, 0).toString("HH:mm"));
|
||||||
|
|
||||||
|
p.setPen(QPen(palette().mid().color(), 1));
|
||||||
|
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, y, w, y);
|
||||||
|
|
||||||
|
QPen halfPen(palette().midlight().color(), 1, Qt::DotLine);
|
||||||
|
p.setPen(halfPen);
|
||||||
|
const int yHalf = y + CalendarWeekView::PIXELS_PER_HOUR / 2;
|
||||||
|
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, yHalf, w, yHalf);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(QPen(palette().mid().color(), 1));
|
||||||
|
for (int d = 0; d <= 7; ++d) {
|
||||||
|
const int x = DayColumnX(d);
|
||||||
|
p.drawLine(x, 0, x, h);
|
||||||
|
}
|
||||||
|
p.drawLine(CalendarWeekView::TIME_AXIS_WIDTH, 0,
|
||||||
|
CalendarWeekView::TIME_AXIS_WIDTH, h);
|
||||||
|
|
||||||
|
for (int d = 0; d < 7; ++d) {
|
||||||
|
const int dayX = DayColumnX(d);
|
||||||
|
for (const auto &layout : LayoutDay(d)) {
|
||||||
|
DrawEvent(p, layout, dayX, dayW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current-time indicator
|
||||||
|
if (_startOfWeek.isValid() && _startOfWeek <= today &&
|
||||||
|
today <= _startOfWeek.addDays(6)) {
|
||||||
|
const int dayIdx = _startOfWeek.daysTo(today);
|
||||||
|
const int dayX = DayColumnX(dayIdx);
|
||||||
|
const int y = TimeToY(QTime::currentTime());
|
||||||
|
|
||||||
|
p.setPen(QPen(QColor(220, 30, 30), 2));
|
||||||
|
p.drawLine(dayX, y, dayX + dayW, y);
|
||||||
|
|
||||||
|
p.setBrush(QColor(220, 30, 30));
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawEllipse(dayX - 4, y - 4, 8, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::mousePressEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventClicked(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QDateTime slot = SlotAtPoint(e->pos());
|
||||||
|
if (slot.isValid()) {
|
||||||
|
emit SlotClicked(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekTimeGrid::mouseDoubleClickEvent(QMouseEvent *e)
|
||||||
|
{
|
||||||
|
if (e->button() != Qt::LeftButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString id = EventIdAtPoint(e->pos());
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
emit EventDoubleClicked(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CalendarWeekView
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CalendarWeekView::CalendarWeekView(QWidget *parent) : CalendarView(parent)
|
||||||
|
{
|
||||||
|
_header = new CalendarWeekDayHeader(this);
|
||||||
|
_timeGrid = new CalendarWeekTimeGrid(this);
|
||||||
|
|
||||||
|
// Fix the minimum height so the scroll area cannot shrink the grid
|
||||||
|
// below the full 24-hour span — which would eliminate the scrollbar.
|
||||||
|
_timeGrid->setMinimumHeight(24 * CalendarWeekView::PIXELS_PER_HOUR);
|
||||||
|
|
||||||
|
_scrollArea = new QScrollArea(this);
|
||||||
|
_scrollArea->setWidget(_timeGrid);
|
||||||
|
// widgetResizable(true) lets the grid expand horizontally to fill the
|
||||||
|
// viewport width while the fixed minimumHeight enforces vertical scrolling.
|
||||||
|
_scrollArea->setWidgetResizable(true);
|
||||||
|
_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
_scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
|
|
||||||
|
auto layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
layout->addWidget(_header);
|
||||||
|
layout->addWidget(_scrollArea);
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
connect(_timeGrid, &CalendarWeekTimeGrid::SlotClicked, this,
|
||||||
|
&CalendarWeekView::SlotClicked);
|
||||||
|
connect(_timeGrid, &CalendarWeekTimeGrid::EventClicked, this,
|
||||||
|
&CalendarWeekView::EventClicked);
|
||||||
|
connect(_timeGrid, &CalendarWeekTimeGrid::EventDoubleClicked, this,
|
||||||
|
&CalendarWeekView::EventDoubleClicked);
|
||||||
|
|
||||||
|
SetDate(QDate::currentDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekView::SetDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_startOfWeek = date.addDays(-(date.dayOfWeek() - 1));
|
||||||
|
UpdateViews();
|
||||||
|
emit VisibleRangeChanged(RangeStart(), RangeEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekView::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
_timeGrid->SetEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarWeekView::CurrentDate() const
|
||||||
|
{
|
||||||
|
return _startOfWeek.addDays(3); // Wednesday - stable mid-point
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarWeekView::RangeStart() const
|
||||||
|
{
|
||||||
|
return _startOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarWeekView::RangeEnd() const
|
||||||
|
{
|
||||||
|
return _startOfWeek.addDays(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWeekView::UpdateViews()
|
||||||
|
{
|
||||||
|
_header->SetStartOfWeek(_startOfWeek);
|
||||||
|
_timeGrid->SetStartOfWeek(_startOfWeek);
|
||||||
|
_timeGrid->ScrollToCurrentTime(_scrollArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
|
|
||||||
|
// Required for CalendarWeekTimeGrid defined in this file
|
||||||
|
#include "calendar-week-view.moc"
|
||||||
45
plugins/schedule/calendar/calendar-week-view.hpp
Normal file
45
plugins/schedule/calendar/calendar-week-view.hpp
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "calendar-view.hpp"
|
||||||
|
|
||||||
|
#include <QScrollArea>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
class CalendarWeekDayHeader;
|
||||||
|
class CalendarWeekTimeGrid;
|
||||||
|
|
||||||
|
// Displays 7 day columns with a vertical time axis (00:00 - 24:00).
|
||||||
|
// Events are rendered as colored blocks sized to their duration.
|
||||||
|
// Overlapping events within the same day are arranged in side-by-side
|
||||||
|
// sub-columns. A red indicator marks the current time.
|
||||||
|
class CalendarWeekView : public CalendarView {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CalendarWeekView(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void SetDate(const QDate &date) override;
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events) override;
|
||||||
|
|
||||||
|
QDate CurrentDate() const override;
|
||||||
|
QDate RangeStart() const override;
|
||||||
|
QDate RangeEnd() const override;
|
||||||
|
|
||||||
|
// Shared layout constants (used by DayHeader and TimeGrid)
|
||||||
|
static constexpr int TIME_AXIS_WIDTH = 56;
|
||||||
|
static constexpr int PIXELS_PER_HOUR = 64;
|
||||||
|
static constexpr int DAY_HEADER_HEIGHT = 36;
|
||||||
|
static constexpr int MIN_DAY_WIDTH = 80;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void UpdateViews();
|
||||||
|
|
||||||
|
CalendarWeekDayHeader *_header;
|
||||||
|
CalendarWeekTimeGrid *_timeGrid;
|
||||||
|
QScrollArea *_scrollArea;
|
||||||
|
|
||||||
|
QDate _startOfWeek;
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
308
plugins/schedule/calendar/calendar-widget.cpp
Normal file
308
plugins/schedule/calendar/calendar-widget.cpp
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
#include "calendar-widget.hpp"
|
||||||
|
|
||||||
|
#include "obs-module-helper.hpp"
|
||||||
|
#include "ui-helpers.hpp"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
CalendarWidget::CalendarWidget(QWidget *parent)
|
||||||
|
: QWidget(parent),
|
||||||
|
_prevBtn(new QPushButton(this)),
|
||||||
|
_nextBtn(new QPushButton(this)),
|
||||||
|
_todayBtn(new QPushButton(
|
||||||
|
obs_module_text("AdvSceneSwitcher.calendar.today"), this)),
|
||||||
|
_navLabel(new QLabel(this)),
|
||||||
|
_monthBtn(new QPushButton(
|
||||||
|
obs_module_text("AdvSceneSwitcher.calendar.month"), this)),
|
||||||
|
_weekBtn(new QPushButton(
|
||||||
|
obs_module_text("AdvSceneSwitcher.calendar.week"), this)),
|
||||||
|
_dayBtn(new QPushButton(
|
||||||
|
obs_module_text("AdvSceneSwitcher.calendar.day"), this)),
|
||||||
|
_viewStack(new QStackedWidget(this)),
|
||||||
|
_monthView(new CalendarMonthView(this)),
|
||||||
|
_weekView(new CalendarWeekView(this)),
|
||||||
|
_dayView(new CalendarDayView(this))
|
||||||
|
{
|
||||||
|
// --- Navigation bar ---
|
||||||
|
SetButtonIcon(_prevBtn, GetThemeTypeName() == "Light"
|
||||||
|
? "theme:Light/left.svg"
|
||||||
|
: "theme:Dark/left.svg");
|
||||||
|
SetButtonIcon(_nextBtn, GetThemeTypeName() == "Light"
|
||||||
|
? "theme:Light/right.svg"
|
||||||
|
: "theme:Dark/right.svg");
|
||||||
|
_navLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
_monthBtn->setCheckable(true);
|
||||||
|
_weekBtn->setCheckable(true);
|
||||||
|
_dayBtn->setCheckable(true);
|
||||||
|
_weekBtn->setChecked(true);
|
||||||
|
|
||||||
|
auto navBar = new QHBoxLayout();
|
||||||
|
navBar->addWidget(_prevBtn);
|
||||||
|
navBar->addWidget(_nextBtn);
|
||||||
|
navBar->addWidget(_todayBtn);
|
||||||
|
navBar->addStretch();
|
||||||
|
navBar->addWidget(_navLabel, 1);
|
||||||
|
navBar->addStretch();
|
||||||
|
navBar->addWidget(_monthBtn);
|
||||||
|
navBar->addWidget(_weekBtn);
|
||||||
|
navBar->addWidget(_dayBtn);
|
||||||
|
|
||||||
|
// --- Views ---
|
||||||
|
_viewStack->addWidget(_monthView);
|
||||||
|
_viewStack->addWidget(_weekView);
|
||||||
|
_viewStack->addWidget(_dayView);
|
||||||
|
|
||||||
|
// --- Main layout ---
|
||||||
|
auto mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
mainLayout->addLayout(navBar);
|
||||||
|
mainLayout->addWidget(_viewStack, 1);
|
||||||
|
setLayout(mainLayout);
|
||||||
|
|
||||||
|
// --- Connections ---
|
||||||
|
connect(_prevBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnPrevClicked);
|
||||||
|
connect(_nextBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnNextClicked);
|
||||||
|
connect(_todayBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnTodayClicked);
|
||||||
|
connect(_monthBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnMonthModeClicked);
|
||||||
|
connect(_weekBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnWeekModeClicked);
|
||||||
|
connect(_dayBtn, &QPushButton::clicked, this,
|
||||||
|
&CalendarWidget::OnDayModeClicked);
|
||||||
|
|
||||||
|
ConnectView(_monthView);
|
||||||
|
ConnectView(_weekView);
|
||||||
|
ConnectView(_dayView);
|
||||||
|
|
||||||
|
// Start in week view
|
||||||
|
SwitchToView(_weekView);
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Events
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarWidget::SetEvents(const QList<CalendarEvent> &events)
|
||||||
|
{
|
||||||
|
_events = events;
|
||||||
|
_monthView->SetEvents(events);
|
||||||
|
_weekView->SetEvents(events);
|
||||||
|
_dayView->SetEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::AddEvent(const CalendarEvent &event)
|
||||||
|
{
|
||||||
|
_events.append(event);
|
||||||
|
_monthView->SetEvents(_events);
|
||||||
|
_weekView->SetEvents(_events);
|
||||||
|
_dayView->SetEvents(_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::RemoveEvent(const QString &id)
|
||||||
|
{
|
||||||
|
_events.erase(std::remove_if(_events.begin(), _events.end(),
|
||||||
|
[&id](const CalendarEvent &e) {
|
||||||
|
return e.id == id;
|
||||||
|
}),
|
||||||
|
_events.end());
|
||||||
|
_monthView->SetEvents(_events);
|
||||||
|
_weekView->SetEvents(_events);
|
||||||
|
_dayView->SetEvents(_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::ClearEvents()
|
||||||
|
{
|
||||||
|
_events.clear();
|
||||||
|
_monthView->SetEvents(_events);
|
||||||
|
_weekView->SetEvents(_events);
|
||||||
|
_dayView->SetEvents(_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Navigation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarWidget::GoToDate(const QDate &date)
|
||||||
|
{
|
||||||
|
_activeView->SetDate(date);
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::GoToToday()
|
||||||
|
{
|
||||||
|
GoToDate(QDate::currentDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarWidget::VisibleRangeStart() const
|
||||||
|
{
|
||||||
|
return _activeView ? _activeView->RangeStart() : QDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDate CalendarWidget::VisibleRangeEnd() const
|
||||||
|
{
|
||||||
|
return _activeView ? _activeView->RangeEnd() : QDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnPrevClicked()
|
||||||
|
{
|
||||||
|
if (_viewMode == ViewMode::Month) {
|
||||||
|
_activeView->SetDate(_activeView->CurrentDate().addMonths(-1));
|
||||||
|
} else if (_viewMode == ViewMode::Week) {
|
||||||
|
_activeView->SetDate(_activeView->RangeStart().addDays(-7));
|
||||||
|
} else {
|
||||||
|
_activeView->SetDate(_activeView->CurrentDate().addDays(-1));
|
||||||
|
}
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnNextClicked()
|
||||||
|
{
|
||||||
|
if (_viewMode == ViewMode::Month) {
|
||||||
|
_activeView->SetDate(_activeView->CurrentDate().addMonths(1));
|
||||||
|
} else if (_viewMode == ViewMode::Week) {
|
||||||
|
_activeView->SetDate(_activeView->RangeStart().addDays(7));
|
||||||
|
} else {
|
||||||
|
_activeView->SetDate(_activeView->CurrentDate().addDays(1));
|
||||||
|
}
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnTodayClicked()
|
||||||
|
{
|
||||||
|
GoToToday();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnMonthModeClicked()
|
||||||
|
{
|
||||||
|
SetViewMode(ViewMode::Month);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnWeekModeClicked()
|
||||||
|
{
|
||||||
|
SetViewMode(ViewMode::Week);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnDayModeClicked()
|
||||||
|
{
|
||||||
|
SetViewMode(ViewMode::Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::OnViewRangeChanged(const QDate &start, const QDate &end)
|
||||||
|
{
|
||||||
|
emit VisibleRangeChanged(start, end);
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void CalendarWidget::ConnectView(CalendarView *view)
|
||||||
|
{
|
||||||
|
connect(view, &CalendarView::SlotClicked, this,
|
||||||
|
&CalendarWidget::SlotClicked);
|
||||||
|
connect(view, &CalendarView::EventClicked, this,
|
||||||
|
&CalendarWidget::EventClicked);
|
||||||
|
connect(view, &CalendarView::EventDoubleClicked, this,
|
||||||
|
&CalendarWidget::EventDoubleClicked);
|
||||||
|
connect(view, &CalendarView::VisibleRangeChanged, this,
|
||||||
|
&CalendarWidget::OnViewRangeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::SwitchToView(CalendarView *view)
|
||||||
|
{
|
||||||
|
_activeView = view;
|
||||||
|
_viewStack->setCurrentWidget(view);
|
||||||
|
view->SetEvents(_events);
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::SetViewMode(ViewMode mode)
|
||||||
|
{
|
||||||
|
if (_viewMode == mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_viewMode = mode;
|
||||||
|
|
||||||
|
// Preserve the current date when switching views
|
||||||
|
const QDate current = _activeView->CurrentDate();
|
||||||
|
|
||||||
|
_monthBtn->setChecked(mode == ViewMode::Month);
|
||||||
|
_weekBtn->setChecked(mode == ViewMode::Week);
|
||||||
|
_dayBtn->setChecked(mode == ViewMode::Day);
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case ViewMode::Month:
|
||||||
|
SwitchToView(_monthView);
|
||||||
|
_monthView->SetDate(current);
|
||||||
|
break;
|
||||||
|
case ViewMode::Week:
|
||||||
|
SwitchToView(_weekView);
|
||||||
|
_weekView->SetDate(current);
|
||||||
|
break;
|
||||||
|
case ViewMode::Day: {
|
||||||
|
SwitchToView(_dayView);
|
||||||
|
const QDate today = QDate::currentDate();
|
||||||
|
const QDate rangeStart = _activeView->RangeStart();
|
||||||
|
const QDate rangeEnd = _activeView->RangeEnd();
|
||||||
|
const QDate target = (today >= rangeStart && today <= rangeEnd)
|
||||||
|
? today
|
||||||
|
: rangeStart;
|
||||||
|
_dayView->SetDate(target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateNavLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalendarWidget::UpdateNavLabel()
|
||||||
|
{
|
||||||
|
if (!_activeView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QLocale locale;
|
||||||
|
const QDate cur = _activeView->CurrentDate();
|
||||||
|
|
||||||
|
if (_viewMode == ViewMode::Month) {
|
||||||
|
_navLabel->setText(locale.monthName(cur.month()) + " " +
|
||||||
|
QString::number(cur.year()));
|
||||||
|
} else if (_viewMode == ViewMode::Day) {
|
||||||
|
// Day view: "Monday, April 13, 2026"
|
||||||
|
_navLabel->setText(locale.dayName(cur.dayOfWeek()) + ", " +
|
||||||
|
locale.monthName(cur.month()) + " " +
|
||||||
|
QString::number(cur.day()) + ", " +
|
||||||
|
QString::number(cur.year()));
|
||||||
|
} else {
|
||||||
|
// Week view: "Apr 7 – Apr 13, 2026"
|
||||||
|
const QDate s = _activeView->RangeStart();
|
||||||
|
const QDate e = _activeView->RangeEnd();
|
||||||
|
if (s.month() == e.month()) {
|
||||||
|
_navLabel->setText(
|
||||||
|
locale.monthName(s.month(),
|
||||||
|
QLocale::ShortFormat) +
|
||||||
|
" " + QString::number(s.day()) + " - " +
|
||||||
|
QString::number(e.day()) + ", " +
|
||||||
|
QString::number(s.year()));
|
||||||
|
} else {
|
||||||
|
_navLabel->setText(
|
||||||
|
locale.monthName(s.month(),
|
||||||
|
QLocale::ShortFormat) +
|
||||||
|
" " + QString::number(s.day()) + " - " +
|
||||||
|
locale.monthName(e.month(),
|
||||||
|
QLocale::ShortFormat) +
|
||||||
|
" " + QString::number(e.day()) + ", " +
|
||||||
|
QString::number(s.year()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
90
plugins/schedule/calendar/calendar-widget.hpp
Normal file
90
plugins/schedule/calendar/calendar-widget.hpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "calendar-day-view.hpp"
|
||||||
|
#include "calendar-event.hpp"
|
||||||
|
#include "calendar-month-view.hpp"
|
||||||
|
#include "calendar-week-view.hpp"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
class CalendarWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class ViewMode { Month, Week, Day };
|
||||||
|
|
||||||
|
explicit CalendarWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
// --- View ---
|
||||||
|
void SetViewMode(ViewMode mode);
|
||||||
|
ViewMode GetViewMode() const { return _viewMode; }
|
||||||
|
|
||||||
|
// --- Events ---
|
||||||
|
void SetEvents(const QList<CalendarEvent> &events);
|
||||||
|
void AddEvent(const CalendarEvent &event);
|
||||||
|
void RemoveEvent(const QString &id);
|
||||||
|
void ClearEvents();
|
||||||
|
const QList<CalendarEvent> &GetEvents() const { return _events; }
|
||||||
|
|
||||||
|
// --- Navigation ---
|
||||||
|
void GoToDate(const QDate &date);
|
||||||
|
void GoToToday();
|
||||||
|
|
||||||
|
// --- Visible range ---
|
||||||
|
QDate VisibleRangeStart() const;
|
||||||
|
QDate VisibleRangeEnd() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// User clicked an empty slot in the active view.
|
||||||
|
void SlotClicked(const QDateTime &startTime);
|
||||||
|
|
||||||
|
// User single-clicked an event.
|
||||||
|
void EventClicked(const QString &eventId);
|
||||||
|
|
||||||
|
// User double-clicked an event (typically: open edit dialog).
|
||||||
|
void EventDoubleClicked(const QString &eventId);
|
||||||
|
|
||||||
|
// The visible date range changed; reload your events for [start, end].
|
||||||
|
void VisibleRangeChanged(const QDate &rangeStart,
|
||||||
|
const QDate &rangeEnd);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void OnPrevClicked();
|
||||||
|
void OnNextClicked();
|
||||||
|
void OnTodayClicked();
|
||||||
|
void OnMonthModeClicked();
|
||||||
|
void OnWeekModeClicked();
|
||||||
|
void OnDayModeClicked();
|
||||||
|
void OnViewRangeChanged(const QDate &start, const QDate &end);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ConnectView(CalendarView *view);
|
||||||
|
void SwitchToView(CalendarView *view);
|
||||||
|
void UpdateNavLabel();
|
||||||
|
|
||||||
|
// Navigation bar widgets
|
||||||
|
QPushButton *_prevBtn;
|
||||||
|
QPushButton *_nextBtn;
|
||||||
|
QPushButton *_todayBtn;
|
||||||
|
QLabel *_navLabel;
|
||||||
|
QPushButton *_monthBtn;
|
||||||
|
QPushButton *_weekBtn;
|
||||||
|
QPushButton *_dayBtn;
|
||||||
|
|
||||||
|
// View stack
|
||||||
|
QStackedWidget *_viewStack;
|
||||||
|
CalendarMonthView *_monthView;
|
||||||
|
CalendarWeekView *_weekView;
|
||||||
|
CalendarDayView *_dayView;
|
||||||
|
CalendarView *_activeView = nullptr;
|
||||||
|
|
||||||
|
ViewMode _viewMode = ViewMode::Week;
|
||||||
|
QList<CalendarEvent> _events;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
373
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
373
plugins/schedule/macro-schedule-entry-dialog.cpp
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
#include "macro-schedule-entry-dialog.hpp"
|
||||||
|
|
||||||
|
#include "help-icon.hpp"
|
||||||
|
#include "obs-module-helper.hpp"
|
||||||
|
#include "ui-helpers.hpp"
|
||||||
|
|
||||||
|
#include <QColorDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace advss {
|
||||||
|
|
||||||
|
bool MacroScheduleEntryDialog::AskForSettings(QWidget *parent,
|
||||||
|
MacroScheduleEntry &entry,
|
||||||
|
bool isNew)
|
||||||
|
{
|
||||||
|
MacroScheduleEntryDialog dialog(parent, entry, isNew);
|
||||||
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dialog.ApplyToEntry(entry);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MacroScheduleEntryDialog::MacroScheduleEntryDialog(
|
||||||
|
QWidget *parent, const MacroScheduleEntry &entry, bool isNew)
|
||||||
|
: QDialog(parent),
|
||||||
|
_name(new QLineEdit(this)),
|
||||||
|
_macroSel(new MacroSelection(this)),
|
||||||
|
_startDateTime(new QDateTimeEdit(this)),
|
||||||
|
_hasEndDate(new QCheckBox(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.endDate"),
|
||||||
|
this)),
|
||||||
|
_endDate(new QDateTimeEdit(this)),
|
||||||
|
_endDateAction(new QComboBox(this)),
|
||||||
|
_doesRepeat(new QCheckBox(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.repeat"),
|
||||||
|
this)),
|
||||||
|
_repeatInterval(new DurationSelection(this, true, 0.1)),
|
||||||
|
_repeatEndGroup(new QGroupBox(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd"),
|
||||||
|
this)),
|
||||||
|
_endNever(new QRadioButton(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.never"),
|
||||||
|
_repeatEndGroup)),
|
||||||
|
_endAfterN(new QRadioButton(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes"),
|
||||||
|
_repeatEndGroup)),
|
||||||
|
_endCount(new QSpinBox(_repeatEndGroup)),
|
||||||
|
_endUntil(new QRadioButton(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.untilDate"),
|
||||||
|
_repeatEndGroup)),
|
||||||
|
_endUntilDate(new QDateEdit(_repeatEndGroup)),
|
||||||
|
_colorBtn(new QPushButton(this)),
|
||||||
|
_checkConditions(new QCheckBox(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.checkConditions"),
|
||||||
|
this)),
|
||||||
|
_runElseActionsOnConditionFailure(new QCheckBox(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.runElseActionsOnConditionFailure"),
|
||||||
|
this)),
|
||||||
|
_enabled(new QCheckBox(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.enabled"),
|
||||||
|
this))
|
||||||
|
{
|
||||||
|
setWindowTitle(obs_module_text(
|
||||||
|
isNew ? "AdvSceneSwitcher.macroScheduleEntry.dialog.title.add"
|
||||||
|
: "AdvSceneSwitcher.macroScheduleEntry.dialog.title.edit"));
|
||||||
|
setMinimumWidth(420);
|
||||||
|
|
||||||
|
auto form = new QFormLayout();
|
||||||
|
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||||
|
|
||||||
|
// --- Name ---
|
||||||
|
_name->setPlaceholderText(obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.name.placeholder"));
|
||||||
|
form->addRow(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.name"),
|
||||||
|
_name);
|
||||||
|
|
||||||
|
// --- Macro ---
|
||||||
|
form->addRow(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.macro"),
|
||||||
|
_macroSel);
|
||||||
|
|
||||||
|
// --- Start date/time ---
|
||||||
|
_startDateTime->setDisplayFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
_startDateTime->setCalendarPopup(true);
|
||||||
|
_startDateTime->setDateTime(QDateTime::currentDateTime());
|
||||||
|
form->addRow(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.startDateTime"),
|
||||||
|
_startDateTime);
|
||||||
|
|
||||||
|
// --- Optional end date ---
|
||||||
|
|
||||||
|
_endDate->setDisplayFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
_endDate->setCalendarPopup(true);
|
||||||
|
_endDate->setDateTime(QDateTime::currentDateTime().addSecs(60 * 60));
|
||||||
|
_endDate->setEnabled(false);
|
||||||
|
auto endDateHelp = new HelpIcon(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.endDate.help"),
|
||||||
|
this);
|
||||||
|
_endDateAction->addItem(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.none"),
|
||||||
|
static_cast<int>(MacroScheduleEntry::EndDateAction::NONE));
|
||||||
|
_endDateAction->addItem(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.disableEntry"),
|
||||||
|
static_cast<int>(
|
||||||
|
MacroScheduleEntry::EndDateAction::DISABLE_ENTRY));
|
||||||
|
_endDateAction->addItem(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.pauseMacro"),
|
||||||
|
static_cast<int>(
|
||||||
|
MacroScheduleEntry::EndDateAction::PAUSE_MACRO));
|
||||||
|
_endDateAction->addItem(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.endDate.action.stopMacro"),
|
||||||
|
static_cast<int>(
|
||||||
|
MacroScheduleEntry::EndDateAction::STOP_MACRO));
|
||||||
|
_endDateAction->setEnabled(false);
|
||||||
|
|
||||||
|
auto endDateRow = new QHBoxLayout();
|
||||||
|
endDateRow->addWidget(_hasEndDate);
|
||||||
|
endDateRow->addWidget(_endDate);
|
||||||
|
endDateRow->addWidget(_endDateAction);
|
||||||
|
endDateRow->addWidget(endDateHelp);
|
||||||
|
endDateRow->addStretch();
|
||||||
|
form->addRow(QString(), endDateRow);
|
||||||
|
|
||||||
|
connect(_hasEndDate, &QCheckBox::toggled, _endDate,
|
||||||
|
&QDateTimeEdit::setEnabled);
|
||||||
|
connect(_hasEndDate, &QCheckBox::toggled, _endDateAction,
|
||||||
|
&QComboBox::setEnabled);
|
||||||
|
|
||||||
|
// Keep end date minimum in sync with the start date.
|
||||||
|
_endDate->setMinimumDateTime(_startDateTime->dateTime());
|
||||||
|
connect(_startDateTime, &QDateTimeEdit::dateTimeChanged, this,
|
||||||
|
[this](const QDateTime &dt) {
|
||||||
|
_endDate->setMinimumDateTime(dt);
|
||||||
|
if (_endDate->dateTime() < dt) {
|
||||||
|
_endDate->setDateTime(dt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Repeat ---
|
||||||
|
form->addRow(QString(), _doesRepeat);
|
||||||
|
|
||||||
|
_repeatInterval->setVisible(false);
|
||||||
|
form->addRow(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeat.interval"),
|
||||||
|
_repeatInterval);
|
||||||
|
if (auto *label = form->labelForField(_repeatInterval)) {
|
||||||
|
label->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(_doesRepeat, &QCheckBox::toggled, this,
|
||||||
|
[form, this](bool checked) {
|
||||||
|
_repeatInterval->setVisible(checked);
|
||||||
|
if (auto *label =
|
||||||
|
form->labelForField(_repeatInterval)) {
|
||||||
|
label->setVisible(checked);
|
||||||
|
}
|
||||||
|
_repeatEndGroup->setVisible(checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Repeat end group (hidden when NONE) ---
|
||||||
|
|
||||||
|
auto repeatEndLayout = new QVBoxLayout(_repeatEndGroup);
|
||||||
|
|
||||||
|
_endNever->setChecked(true);
|
||||||
|
repeatEndLayout->addWidget(_endNever);
|
||||||
|
|
||||||
|
auto afterNRow = new QHBoxLayout();
|
||||||
|
_endCount->setMinimum(1);
|
||||||
|
_endCount->setMaximum(99999);
|
||||||
|
_endCount->setValue(1);
|
||||||
|
_endCount->setEnabled(false);
|
||||||
|
auto afterNSuffix = new QLabel(
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.repeatEnd.afterNTimes.suffix"),
|
||||||
|
_repeatEndGroup);
|
||||||
|
afterNRow->addWidget(_endAfterN);
|
||||||
|
afterNRow->addWidget(_endCount);
|
||||||
|
afterNRow->addWidget(afterNSuffix);
|
||||||
|
afterNRow->addStretch();
|
||||||
|
repeatEndLayout->addLayout(afterNRow);
|
||||||
|
|
||||||
|
auto untilRow = new QHBoxLayout();
|
||||||
|
_endUntilDate->setCalendarPopup(true);
|
||||||
|
_endUntilDate->setDate(QDate::currentDate().addMonths(1));
|
||||||
|
_endUntilDate->setEnabled(false);
|
||||||
|
untilRow->addWidget(_endUntil);
|
||||||
|
untilRow->addWidget(_endUntilDate);
|
||||||
|
untilRow->addStretch();
|
||||||
|
repeatEndLayout->addLayout(untilRow);
|
||||||
|
|
||||||
|
_repeatEndGroup->setVisible(false);
|
||||||
|
form->addRow(_repeatEndGroup);
|
||||||
|
|
||||||
|
// Enable/disable child widgets based on radio selection
|
||||||
|
connect(_endAfterN, &QRadioButton::toggled, _endCount,
|
||||||
|
&QSpinBox::setEnabled);
|
||||||
|
connect(_endUntil, &QRadioButton::toggled, _endUntilDate,
|
||||||
|
&QDateEdit::setEnabled);
|
||||||
|
|
||||||
|
// --- Color ---
|
||||||
|
_colorBtn->setFixedSize(48, 22);
|
||||||
|
_colorBtn->setFlat(false);
|
||||||
|
connect(_colorBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
const QColor picked = QColorDialog::getColor(
|
||||||
|
_color, this,
|
||||||
|
obs_module_text(
|
||||||
|
"AdvSceneSwitcher.macroScheduleEntry.color.dialog"));
|
||||||
|
if (picked.isValid()) {
|
||||||
|
_color = picked;
|
||||||
|
UpdateColorButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
form->addRow(
|
||||||
|
obs_module_text("AdvSceneSwitcher.macroScheduleEntry.color"),
|
||||||
|
_colorBtn);
|
||||||
|
|
||||||
|
// --- Options ---
|
||||||
|
form->addRow(QString(), _checkConditions);
|
||||||
|
|
||||||
|
_runElseActionsOnConditionFailure->setVisible(false);
|
||||||
|
form->addRow(QString(), _runElseActionsOnConditionFailure);
|
||||||
|
connect(_checkConditions, &QCheckBox::toggled,
|
||||||
|
_runElseActionsOnConditionFailure, &QCheckBox::setVisible);
|
||||||
|
|
||||||
|
_enabled->setChecked(true);
|
||||||
|
form->addRow(QString(), _enabled);
|
||||||
|
|
||||||
|
// --- Buttons ---
|
||||||
|
auto buttons = new QDialogButtonBox(
|
||||||
|
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
|
// --- Main layout ---
|
||||||
|
auto mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->addLayout(form);
|
||||||
|
mainLayout->addWidget(buttons);
|
||||||
|
setLayout(mainLayout);
|
||||||
|
|
||||||
|
PopulateFromEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MacroScheduleEntryDialog::PopulateFromEntry(const MacroScheduleEntry &entry)
|
||||||
|
{
|
||||||
|
_name->setText(QString::fromStdString(entry.name));
|
||||||
|
|
||||||
|
MacroRef ref = entry.macro;
|
||||||
|
_macroSel->SetCurrentMacro(ref);
|
||||||
|
|
||||||
|
if (entry.startDateTime.isValid()) {
|
||||||
|
_startDateTime->setDateTime(entry.startDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasEndDate->setChecked(entry.hasEndDate);
|
||||||
|
if (entry.hasEndDate && entry.endDate.isValid()) {
|
||||||
|
_endDate->setDateTime(entry.endDate);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const int idx = _endDateAction->findData(
|
||||||
|
static_cast<int>(entry.endDateAction));
|
||||||
|
if (idx >= 0) {
|
||||||
|
_endDateAction->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_doesRepeat->setChecked(entry.doesRepeat);
|
||||||
|
_repeatInterval->SetDuration(entry.repeatInterval);
|
||||||
|
_doesRepeat->toggled(entry.doesRepeat);
|
||||||
|
|
||||||
|
switch (entry.repeatEndType) {
|
||||||
|
case MacroScheduleEntry::RepeatEndType::NEVER:
|
||||||
|
_endNever->setChecked(true);
|
||||||
|
break;
|
||||||
|
case MacroScheduleEntry::RepeatEndType::AFTER_N_TIMES:
|
||||||
|
_endAfterN->setChecked(true);
|
||||||
|
_endCount->setValue(entry.repeatMaxCount);
|
||||||
|
break;
|
||||||
|
case MacroScheduleEntry::RepeatEndType::UNTIL_DATE:
|
||||||
|
_endUntil->setChecked(true);
|
||||||
|
if (entry.repeatUntilDate.isValid()) {
|
||||||
|
_endUntilDate->setDate(entry.repeatUntilDate.date());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_color = entry.color.isValid() ? entry.color : QColor(70, 130, 180);
|
||||||
|
UpdateColorButton();
|
||||||
|
_checkConditions->setChecked(entry.checkConditions);
|
||||||
|
_runElseActionsOnConditionFailure->setChecked(
|
||||||
|
entry.runElseActionsOnConditionFailure);
|
||||||
|
_runElseActionsOnConditionFailure->setVisible(entry.checkConditions);
|
||||||
|
_enabled->setChecked(entry.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MacroScheduleEntryDialog::ApplyToEntry(MacroScheduleEntry &entry) const
|
||||||
|
{
|
||||||
|
entry.name = _name->text().toStdString();
|
||||||
|
|
||||||
|
const QString macroName = _macroSel->currentText();
|
||||||
|
entry.macro = macroName;
|
||||||
|
|
||||||
|
const QDateTime newStartDateTime = _startDateTime->dateTime();
|
||||||
|
const bool startChanged = (newStartDateTime != entry.startDateTime);
|
||||||
|
entry.startDateTime = newStartDateTime;
|
||||||
|
if (startChanged) {
|
||||||
|
entry.FastForwardTo(QDateTime::currentDateTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.hasEndDate = _hasEndDate->isChecked();
|
||||||
|
if (entry.hasEndDate) {
|
||||||
|
entry.endDate = _endDate->dateTime();
|
||||||
|
entry.endDateAction =
|
||||||
|
static_cast<MacroScheduleEntry::EndDateAction>(
|
||||||
|
_endDateAction->currentData().toInt());
|
||||||
|
} else {
|
||||||
|
entry.endDate = QDateTime();
|
||||||
|
entry.endDateAction = MacroScheduleEntry::EndDateAction::NONE;
|
||||||
|
}
|
||||||
|
entry.endDateActionApplied = false;
|
||||||
|
|
||||||
|
entry.doesRepeat = _doesRepeat->isChecked();
|
||||||
|
entry.repeatInterval = _repeatInterval->GetDuration();
|
||||||
|
|
||||||
|
if (_endNever->isChecked()) {
|
||||||
|
entry.repeatEndType = MacroScheduleEntry::RepeatEndType::NEVER;
|
||||||
|
entry.repeatUntilDate = QDateTime();
|
||||||
|
} else if (_endAfterN->isChecked()) {
|
||||||
|
entry.repeatEndType =
|
||||||
|
MacroScheduleEntry::RepeatEndType::AFTER_N_TIMES;
|
||||||
|
entry.repeatMaxCount = _endCount->value();
|
||||||
|
entry.repeatUntilDate = QDateTime();
|
||||||
|
} else if (_endUntil->isChecked()) {
|
||||||
|
entry.repeatEndType =
|
||||||
|
MacroScheduleEntry::RepeatEndType::UNTIL_DATE;
|
||||||
|
entry.repeatUntilDate =
|
||||||
|
QDateTime(_endUntilDate->date(), QTime(23, 59, 59));
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.color = _color;
|
||||||
|
entry.checkConditions = _checkConditions->isChecked();
|
||||||
|
entry.runElseActionsOnConditionFailure =
|
||||||
|
_runElseActionsOnConditionFailure->isChecked();
|
||||||
|
entry.enabled = _enabled->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MacroScheduleEntryDialog::UpdateColorButton()
|
||||||
|
{
|
||||||
|
// Fill the button with the chosen color so it acts as a swatch
|
||||||
|
const QString style =
|
||||||
|
QString("background-color: %1; border: 1px solid #888;")
|
||||||
|
.arg(_color.name());
|
||||||
|
_colorBtn->setStyleSheet(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace advss
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user