Compare commits

...

21 Commits

Author SHA1 Message Date
WarmUpTill
ba7886c8c1 Fix variables of nested macros' actions not being resolved 2026-06-16 20:43:09 +02:00
WarmUpTill
801268d08b Fix nested macro actions being executed twice when added to a queue 2026-06-16 20:29:11 +02:00
WarmUpTill
7dfd9ddcb5 Print plugin enabled/disabled summary during CMake configure
Some checks are pending
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-06-16 17:40:22 +02:00
WarmUpTill
6215c39024 Don't fail cmake configure step if openvr deps are not met 2026-06-16 17:40:22 +02:00
WarmUpTill
1c856911e8 Set default value for CMAKE_GENERATOR_PLATFORM on Windows 2026-06-16 17:40:22 +02:00
WarmUpTill
ed17c2ac6d Disconnect Twitch chat and event connection on plugin stop
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-06-13 22:05:32 +02:00
WarmUpTill
1ce332a91d Track all kWin wayland windows 2026-06-13 22:01:52 +02:00
WarmUpTill
2aa27fc051 Fix window conditions not triggering for native Wayland windows
When KWin compat is active, GetWindows() only returned XWayland windows
since it relied on X11's _NET_CLIENT_LIST. Native Wayland windows were
invisible to conditions even though GetCurrentWindowTitle() reported them
correctly via the KWin DBus notifier.
2026-06-13 22:01:52 +02:00
WarmUpTill
31b239d5dc Focus segment type selection when new segment is added
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-06-11 18:25:29 +02:00
WarmUpTill
7c3779e287 Sort macro export dialog resources alphabetically and add scrollbars 2026-06-11 18:25:29 +02:00
WarmUpTill
3f13c33b10 Fix crash on transition start in studio mode 2026-06-11 18:25:29 +02:00
WarmUpTill
8727a65513 Support both recommended and legacy layouts in Windows installer
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Default to %ProgramData%\obs-studio\plugins\ on fresh installs.
Restore the previously recorded install path on upgrades, so users
who installed to a custom location are not silently moved.
2026-05-30 16:01:11 +02:00
WarmUpTill
baf3d0e63c Fix race condition in Twitch event sub reconnection logic 2026-05-30 16:01:11 +02:00
WarmUpTill
ea2d1e7e9d Fix crash when importing macros is slow
Probably not a real issue.
Noticed during debug of another problem.
2026-05-30 16:01:11 +02:00
WarmUpTill
f9f26ad50b Fix crash on shutdown
StopAllMacros was called before th->wait(), allowing Macro::Stop()
to run concurrently with the other threads still inside
CheckConditions. This caused races on _conditionCheckFuture (called
from both threads) and _helperThreads.

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

Requested in:
https://github.com/WarmUpTill/SceneSwitcher/discussions/1628
2026-05-30 16:01:11 +02:00
WarmUpTill
d590bc11f8 Fix freeze when choosing invalid OCR model 2026-05-30 16:01:11 +02:00
WarmUpTill
31c4c1f5d7 Convert "Program Files/" paths to "ProgramData/" paths on Windows
Affects OCR and object detection functionality of the video condition
2026-05-30 16:01:11 +02:00
WarmUpTill
42ff48d637 Add option to lock settings dialog with simple password
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
This is meant as a simple tampering protection.
The settings are not encrypted and the password is stored in plaintext.
2026-05-28 21:50:00 +02:00
WarmUpTill
d44376e869 deps: Update cpp-httplib to v0.46.0
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-05-27 11:38:26 +02:00
WarmUpTill
4b1fd9e700 Fix crash on startup
Resize of vector could invalidate the this pointers passed to the
PostLoad step lambdas
2026-05-27 11:32:30 +02:00
42 changed files with 667 additions and 164 deletions

View File

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

View File

@@ -0,0 +1,23 @@
Advanced Scene Switcher - Windows Installation (Legacy)
========================================================
For full installation instructions visit:
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
This archive uses the legacy plugin layout for older OBS versions,
portable OBS installs, and Steam.
Extract the CONTENTS of this archive into your OBS installation directory,
typically:
C:\Program Files\obs-studio\
Merge the data\ and obs-plugins\ folders with the existing ones when prompted.
Steam users: right-click OBS in your library -> Manage -> Browse local files,
then extract the contents of this archive into that directory.
For OBS 28 and newer the recommended archive is preferred:
advanced-scene-switcher-<version>-windows-x64.zip

View File

@@ -1,22 +1,15 @@
Advanced Scene Switcher - Windows Installation Advanced Scene Switcher - Windows Installation
============================================== ===============================================
For full installation instructions visit: For full installation instructions visit:
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
Quick guide This archive uses the recommended plugin layout for OBS 28 and newer.
-----------
This archive contains two installation methods. Extract the CONTENTS of this archive into:
Choose ONE depending on your OBS version.
C:\ProgramData\obs-studio\plugins\
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. Tip: press Win+R and type %ProgramData%\obs-studio\plugins to open that folder.
@@ -24,14 +17,5 @@ After extracting, the path should look like:
C:\ProgramData\obs-studio\plugins\advanced-scene-switcher\bin\64bit\advanced-scene-switcher.dll 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) If the plugin does not appear in OBS, try the legacy archive instead:
--------------------------------------------------------------------------- advanced-scene-switcher-<version>-windows-x64-legacy.zip
Extract the CONTENTS of this folder into your OBS installation directory,
typically:
C:\Program Files\obs-studio\
Merge the data\ and obs-plugins\ folders with the existing ones when prompted.
Steam users: right-click OBS in your library -> Manage -> Browse local files,
then extract the contents of legacy\ into that directory.

View File

@@ -157,7 +157,9 @@ else()
set_target_properties(${target} PROPERTIES INSTALL_RPATH set_target_properties(${target} PROPERTIES INSTALL_RPATH
"$ORIGIN:$ORIGIN/..") "$ORIGIN:$ORIGIN/..")
endif() endif()
message(STATUS "ADVSS: ENABLED PLUGIN ${target}") get_filename_component(_plugin_dir_name "${CMAKE_CURRENT_SOURCE_DIR}" NAME)
set_property(GLOBAL APPEND PROPERTY ADVSS_PLUGINS_ENABLED
${_plugin_dir_name})
endfunction() endfunction()
function(install_advss_plugin_dependency_target target dep) function(install_advss_plugin_dependency_target target dep)

View File

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

View File

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

View File

@@ -47,6 +47,13 @@ AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can
AdvSceneSwitcher.generalTab.generalBehavior.alwaysShowMacroSearch="Always show macro search" AdvSceneSwitcher.generalTab.generalBehavior.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)"

View File

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

View File

@@ -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>
@@ -459,12 +461,13 @@ void SwitcherData::Stop()
SetMacroAbortWait(true); SetMacroAbortWait(true);
GetMacroWaitCV().notify_all(); GetMacroWaitCV().notify_all();
GetMacroTransitionCV().notify_all(); GetMacroTransitionCV().notify_all();
StopAllMacros(); SignalStopAllMacros();
StopAndClearAllActionQueues(); StopAndClearAllActionQueues();
CloseAllInputDialogs(); CloseAllInputDialogs();
th->wait(); th->wait();
delete th; delete th;
th = nullptr; th = nullptr;
WaitForAllMacros();
RunStopSteps(); RunStopSteps();
} }
@@ -718,19 +721,54 @@ static void LoadPlugins()
} }
} }
static bool settingsPasswordIsCorrect(QWidget *parent)
{
bool ok;
QString input = QInputDialog::getText(
parent,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.passwordLabel"),
QLineEdit::Password, QString(), &ok);
if (!ok) {
return false;
}
if (input.toStdString() == switcher->settingsLockPassword) {
return true;
}
QMessageBox::warning(
parent,
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.title"),
obs_module_text(
"AdvSceneSwitcher.generalTab.generalBehavior.settingsLock.wrongPassword"));
return false;
}
void OpenSettingsWindow() 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)

View File

@@ -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);

View File

@@ -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 =
@@ -567,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);
@@ -638,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);
@@ -960,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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -263,6 +263,7 @@ bool MacroActionMacro::Load(obs_data_t *obj)
if (obs_data_has_user_value(obj, "nestedMacro")) { if (obs_data_has_user_value(obj, "nestedMacro")) {
OBSDataAutoRelease nestedMacroData = OBSDataAutoRelease nestedMacroData =
obs_data_get_obj(obj, "nestedMacro"); obs_data_get_obj(obj, "nestedMacro");
_nestedMacro = std::make_shared<Macro>();
_nestedMacro->Load(nestedMacroData); _nestedMacro->Load(nestedMacroData);
} }
@@ -309,6 +310,15 @@ void MacroActionMacro::ResolveVariablesToFixedValues()
{ {
_actionIndex.ResolveVariables(); _actionIndex.ResolveVariables();
_label.ResolveVariables(); _label.ResolveVariables();
if (_action == Action::NESTED_MACRO) {
for (auto &action : _nestedMacro->Actions()) {
action->ResolveVariablesToFixedValues();
}
for (auto &action : _nestedMacro->ElseActions()) {
action->ResolveVariablesToFixedValues();
}
}
} }
static void runActionsHelper(Macro *macro, bool runElseActions, bool setInputs, static void runActionsHelper(Macro *macro, bool runElseActions, bool setInputs,

View File

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

View File

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

View File

@@ -650,6 +650,18 @@ void MacroEdit::HighlightCondition(int idx, QColor color) const
ui->conditionsList->Highlight(idx, color); ui->conditionsList->Highlight(idx, color);
} }
void MacroEdit::ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
int *currentIdx)
{
QTimer::singleShot(0, this, [this, list, newIdx, currentIdx]() {
list->ensureWidgetVisible(list->WidgetAt(*currentIdx));
auto *widget = list->WidgetAt(newIdx);
if (widget) {
widget->FocusTypeSelection();
}
});
}
static void resetSegmentHighlights(MacroSegmentList *list) static void resetSegmentHighlights(MacroSegmentList *list)
{ {
MacroSegmentEdit *widget = nullptr; MacroSegmentEdit *widget = nullptr;
@@ -1087,10 +1099,7 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
ui->actionsList->SetHelpMsgVisible(false); ui->actionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() { ScrollAndFocusNewSegment(ui->actionsList, idx, &currentActionIdx);
ui->actionsList->ensureWidgetVisible(
ui->actionsList->WidgetAt(currentActionIdx));
});
} }
void MacroEdit::AddMacroAction(int idx) void MacroEdit::AddMacroAction(int idx)
@@ -1398,10 +1407,8 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
ui->elseActionsList->SetHelpMsgVisible(false); ui->elseActionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() { ScrollAndFocusNewSegment(ui->elseActionsList, idx,
ui->elseActionsList->ensureWidgetVisible( &currentElseActionIdx);
ui->elseActionsList->WidgetAt(currentElseActionIdx));
});
} }
void MacroEdit::AddMacroElseAction(int idx) void MacroEdit::AddMacroElseAction(int idx)
@@ -1611,10 +1618,7 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
ui->conditionsList->SetHelpMsgVisible(false); ui->conditionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged()); emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() { ScrollAndFocusNewSegment(ui->conditionsList, idx, &currentConditionIdx);
ui->conditionsList->ensureWidgetVisible(
ui->conditionsList->WidgetAt(currentConditionIdx));
});
} }
void MacroEdit::on_conditionAdd_clicked() void MacroEdit::on_conditionAdd_clicked()

View File

@@ -116,6 +116,8 @@ private:
void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0); void PopulateMacroActions(Macro &m, uint32_t afterIdx = 0);
void PopulateMacroElseActions(Macro &m, uint32_t afterIdx = 0); void PopulateMacroElseActions(Macro &m, uint32_t afterIdx = 0);
void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0); void PopulateMacroConditions(Macro &m, uint32_t afterIdx = 0);
void ScrollAndFocusNewSegment(MacroSegmentList *list, int newIdx,
int *currentIdx);
void SetupMacroSegmentSelection(MacroSection type, int idx); void SetupMacroSegmentSelection(MacroSection type, int idx);
void void
SetupContextMenu(const QPoint &pos, SetupContextMenu(const QPoint &pos,

View File

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

View File

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

View File

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

View File

@@ -74,11 +74,11 @@ void LoadMacroList(obs_data_t *obj, std::vector<MacroRef> &macros,
{ {
obs_data_array_t *array = obs_data_get_array(obj, name.c_str()); obs_data_array_t *array = obs_data_get_array(obj, name.c_str());
size_t count = obs_data_array_count(array); size_t count = obs_data_array_count(array);
macros.reserve(macros.size() + count);
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(array, i); obs_data_t *array_obj = obs_data_array_item(array, i);
MacroRef ref; macros.emplace_back();
ref.Load(array_obj); macros.back().Load(array_obj);
macros.push_back(ref);
obs_data_release(array_obj); obs_data_release(array_obj);
} }
obs_data_array_release(array); obs_data_array_release(array);

View File

@@ -134,6 +134,7 @@ public:
void SetSelected(bool); void SetSelected(bool);
virtual std::shared_ptr<MacroSegment> Data() const = 0; virtual std::shared_ptr<MacroSegment> Data() const = 0;
virtual void SetupWidgets(bool basicSetup = false) = 0; virtual void SetupWidgets(bool basicSetup = false) = 0;
virtual void FocusTypeSelection() {}
void SetupVarMappings(MacroSegment *segment); void SetupVarMappings(MacroSegment *segment);

View File

@@ -395,7 +395,7 @@ void AdvSceneSwitcher::ImportMacros()
std::vector<std::shared_ptr<Macro>> importedMacros; std::vector<std::shared_ptr<Macro>> importedMacros;
auto &tempMacros = GetTemporaryMacros(); auto &tempMacros = GetTemporaryMacros();
auto lock = LockContext(); std::unique_lock<std::mutex> lock(*GetMutex());
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
tempMacros.clear(); tempMacros.clear();
@@ -405,10 +405,14 @@ void AdvSceneSwitcher::ImportMacros()
macro->Load(array_obj); macro->Load(array_obj);
RunAndClearPostLoadSteps(); RunAndClearPostLoadSteps();
if (macroNameExists(macro->Name()) && if (macroNameExists(macro->Name())) {
!ResolveMacroImportNameConflict(macro)) { lock.unlock();
groupSize--; bool resolved = ResolveMacroImportNameConflict(macro);
continue; lock.lock();
if (!resolved) {
groupSize--;
continue;
}
} }
importedMacros.emplace_back(macro); importedMacros.emplace_back(macro);

View File

@@ -1316,7 +1316,14 @@ bool RunMacros()
return true; return true;
} }
void StopAllMacros() void SignalStopAllMacros()
{
for (const auto &m : GetAllMacros()) {
m->SignalStop();
}
}
void WaitForAllMacros()
{ {
for (const auto &m : GetAllMacros()) { for (const auto &m : GetAllMacros()) {
m->Stop(); m->Stop();

View File

@@ -61,6 +61,7 @@ public:
PauseStateSaveBehavior GetPauseStateSaveBehavior() const; PauseStateSaveBehavior GetPauseStateSaveBehavior() const;
void Stop(); void Stop();
void SignalStop() { _stop = true; }
bool GetStop() const { return _stop; } bool GetStop() const { return _stop; }
void ResetTimers(); void ResetTimers();
@@ -228,7 +229,7 @@ void LoadMacros(obs_data_t *obj);
void SaveMacros(obs_data_t *obj); void SaveMacros(obs_data_t *obj);
bool CheckMacros(); bool CheckMacros();
bool RunMacros(); bool RunMacros();
void StopAllMacros(); void WaitForAllMacros();
void InvalidateMacroTempVarValues(); void InvalidateMacroTempVarValues();
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval(); std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval();

View File

@@ -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 = {};

View File

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

View File

@@ -163,7 +163,7 @@ void MacroConditionTransition::TransitionStarted(void *data, calldata_t *cd)
// Look up the original by name to get a comparable reference. // Look up the original by name to get a comparable reference.
const bool studioModeActive = const bool studioModeActive =
obs_frontend_preview_program_mode_active(); obs_frontend_preview_program_mode_active();
if (studioModeActive) { if (studioModeActive && startSource) {
OBSSourceAutoRelease startSourceByName = obs_get_source_by_name( OBSSourceAutoRelease startSourceByName = obs_get_source_by_name(
obs_source_get_name(startSource)); obs_source_get_name(startSource));
if (startSourceByName) { if (startSourceByName) {

View File

@@ -26,7 +26,7 @@ if(NOT OpenVR_DIR)
set(OpenVR_DIR ${ADVSS_SOURCE_DIR}/deps/openvr) set(OpenVR_DIR ${ADVSS_SOURCE_DIR}/deps/openvr)
endif() endif()
if(EXISTS ${OpenVR_DIR}) if(EXISTS "${OpenVR_DIR}/CMakeLists.txt")
set(SIZEOF_VOIDP ${CMAKE_SIZEOF_VOID_P}) set(SIZEOF_VOIDP ${CMAKE_SIZEOF_VOID_P})
if((NOT APPLE) AND (CMAKE_SIZEOF_VOID_P EQUAL 8)) if((NOT APPLE) AND (CMAKE_SIZEOF_VOID_P EQUAL 8))
set(PROCESSOR_ARCH "64") set(PROCESSOR_ARCH "64")

View File

@@ -3,6 +3,7 @@
#include "twitch-helpers.hpp" #include "twitch-helpers.hpp"
#include <log-helper.hpp> #include <log-helper.hpp>
#include <plugin-state-helpers.hpp>
#undef DispatchMessage #undef DispatchMessage
@@ -339,6 +340,24 @@ static constexpr std::string_view defaultURL =
std::map<TwitchChatConnection::ChatMapKey, std::weak_ptr<TwitchChatConnection>> std::map<TwitchChatConnection::ChatMapKey, std::weak_ptr<TwitchChatConnection>>
TwitchChatConnection::_chatMap = {}; TwitchChatConnection::_chatMap = {};
void TwitchChatConnection::DisconnectAll()
{
for (auto &[key, weakConn] : _chatMap) {
auto conn = weakConn.lock();
if (conn) {
conn->Disconnect();
}
}
}
static bool setupChatConnectionSupport()
{
AddStopStep(TwitchChatConnection::DisconnectAll);
return true;
}
static bool _chatSetup = setupChatConnectionSupport();
TwitchChatConnection::TwitchChatConnection(const TwitchToken &token, TwitchChatConnection::TwitchChatConnection(const TwitchToken &token,
const TwitchChannel &channel) const TwitchChannel &channel)
: QObject(nullptr), : QObject(nullptr),

View File

@@ -84,6 +84,7 @@ public:
static std::shared_ptr<TwitchChatConnection> static std::shared_ptr<TwitchChatConnection>
GetChatConnection(const TwitchToken &token, GetChatConnection(const TwitchToken &token,
const TwitchChannel &channel); const TwitchChannel &channel);
static void DisconnectAll();
[[nodiscard]] ChatMessageBuffer RegisterForMessages(); [[nodiscard]] ChatMessageBuffer RegisterForMessages();
[[nodiscard]] ChatMessageBuffer RegisterForWhispers(); [[nodiscard]] ChatMessageBuffer RegisterForWhispers();
void SendChatMessage(const std::string &message); void SendChatMessage(const std::string &message);

View File

@@ -3,6 +3,7 @@
#include "twitch-helpers.hpp" #include "twitch-helpers.hpp"
#include <log-helper.hpp> #include <log-helper.hpp>
#include <plugin-state-helpers.hpp>
#ifdef VERIFY_TIMESTAMPS #ifdef VERIFY_TIMESTAMPS
#include "date/tz.h" #include "date/tz.h"
@@ -71,6 +72,22 @@ void EventSub::UnregisterInstance()
_instances.erase(it, _instances.end()); _instances.erase(it, _instances.end());
} }
void EventSub::DisconnectAll()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
for (auto *instance : _instances) {
instance->Disconnect();
}
}
static bool setupEventSubSupport()
{
AddStopStep(EventSub::DisconnectAll);
return true;
}
static bool _eventSubSetup = setupEventSubSupport();
void EventSub::ConnectThread() void EventSub::ConnectThread()
{ {
_client->reset(); _client->reset();
@@ -262,12 +279,21 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
return ""; return "";
} }
// Capture the session ID and release the lock before making the HTTP
// request. Holding the mutex during a network call would block
// SubscriptionIsActive() on the condition-check thread for the entire
// duration of the request, causing visibly slow macro evaluations.
const std::string sessionID = eventSub->_sessionID;
lock.unlock();
OBSDataAutoRelease postData = copyData(subscription.data); OBSDataAutoRelease postData = copyData(subscription.data);
setTransportData(postData.Get(), eventSub->_sessionID); setTransportData(postData.Get(), sessionID);
auto result = SendPostRequest(*token, registerSubscriptionURL.data(), auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
registerSubscriptionPath.data(), {}, registerSubscriptionPath.data(), {},
postData.Get()); postData.Get());
lock.lock();
if (result.status != 202) { if (result.status != 202) {
const char *error = obs_data_get_string(result.data, "error"); const char *error = obs_data_get_string(result.data, "error");
const char *message = const char *message =
@@ -280,6 +306,24 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
return ""; return "";
} }
// Verify the session has not been replaced while the HTTP request was
// in flight (e.g. a reconnect occurred). If it was, the subscription
// we just created belongs to a dead session and must be discarded so
// the caller will retry with the new session ID.
if (eventSub->_sessionID != sessionID) {
blog(LOG_INFO,
"discarding Twitch EventSub registration - session ID "
"changed during HTTP request");
return "";
}
// Another thread may have registered the same subscription while the
// lock was released.
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
eventSub->LogActiveSubscriptions();
return eventSub->_activeSubscriptions.find(subscription)->id;
}
OBSDataArrayAutoRelease replyArray = OBSDataArrayAutoRelease replyArray =
obs_data_get_array(result.data, "data"); obs_data_get_array(result.data, "data");
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0); OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
@@ -439,9 +483,10 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
void EventSub::HandleWelcome(obs_data_t *data) void EventSub::HandleWelcome(obs_data_t *data)
{ {
std::lock_guard<std::mutex> lock(_subscriptionMtx);
OBSDataAutoRelease session = obs_data_get_obj(data, "session"); OBSDataAutoRelease session = obs_data_get_obj(data, "session");
_sessionID = obs_data_get_string(session, "id"); _sessionID = obs_data_get_string(session, "id");
ClearActiveSubscriptions(); _activeSubscriptions.clear();
blog(LOG_INFO, "Twitch EventSub connected"); blog(LOG_INFO, "Twitch EventSub connected");
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str()); vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
} }

View File

@@ -53,6 +53,7 @@ public:
void Connect(); void Connect();
void Disconnect(); void Disconnect();
static void DisconnectAll();
[[nodiscard]] EventSubMessageBuffer RegisterForEvents(); [[nodiscard]] EventSubMessageBuffer RegisterForEvents();
bool SubscriptionIsActive(const std::string &id); bool SubscriptionIsActive(const std::string &id);
static std::string AddEventSubscription(std::shared_ptr<TwitchToken>, static std::string AddEventSubscription(std::shared_ptr<TwitchToken>,

View File

@@ -547,16 +547,18 @@ bool MacroConditionTwitch::CheckChatMessageRemove(TwitchToken &token)
bool MacroConditionTwitch::ChatConnectionIsSetup(TwitchToken &token) bool MacroConditionTwitch::ChatConnectionIsSetup(TwitchToken &token)
{ {
if (_chatConnection) {
_chatConnection->ConnectToChat();
return true;
}
_chatConnection =
TwitchChatConnection::GetChatConnection(token, _channel);
if (!_chatConnection) { if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection(
token, _channel);
if (!_chatConnection) {
return false;
}
_chatBuffer = _chatConnection->RegisterForMessages();
return false; return false;
} }
return true; _chatBuffer = _chatConnection->RegisterForMessages();
return false;
} }
bool MacroConditionTwitch::HandleChatEvents( bool MacroConditionTwitch::HandleChatEvents(

View File

@@ -403,11 +403,12 @@ bool MacroConditionVideo::CheckBrightnessThreshold()
bool MacroConditionVideo::CheckOCR() bool MacroConditionVideo::CheckOCR()
{ {
if (!_ocrParameters.Initialized()) { auto *ocr = _ocrParameters.GetOCR();
if (!ocr) {
return false; return false;
} }
auto text = RunOCR(_ocrParameters.GetOCR(), _screenshotData.GetImage(), auto text = RunOCR(ocr, _screenshotData.GetImage(),
_ocrParameters.color, _ocrParameters.colorThreshold); _ocrParameters.color, _ocrParameters.colorThreshold);
if (!text) { if (!text) {
return false; return false;

View File

@@ -82,7 +82,7 @@ bool ObjDetectParameters::Save(obs_data_t *obj) const
minSize.Save(data, "minSize"); minSize.Save(data, "minSize");
maxSize.Save(data, "maxSize"); maxSize.Save(data, "maxSize");
obs_data_set_obj(obj, "objectMatchData", data); obs_data_set_obj(obj, "objectMatchData", data);
obs_data_set_int(data, "version", 1); obs_data_set_int(data, "version", 2);
obs_data_release(data); obs_data_release(data);
return true; return true;
} }
@@ -121,6 +121,21 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
// TODO: Remove this fallback in a future version // TODO: Remove this fallback in a future version
if (!obs_data_has_user_value(data, "version")) { if (!obs_data_has_user_value(data, "version")) {
scaleFactor = obs_data_get_double(data, "scaleFactor"); scaleFactor = obs_data_get_double(data, "scaleFactor");
} else if (obs_data_get_int(data, "version") == 1) {
#ifdef _WIN32
// On Windows the plugin directory moved from
// "Program Files/.../data/obs-plugins/advanced-scene-switcher"
// to "ProgramData/.../plugins/advanced-scene-switcher/data",
// which invalidates previously saved default model paths.
const std::string oldPrefix =
"../../data/obs-plugins/advanced-scene-switcher/res/cascadeClassifiers/";
if (modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
modelPath = std::string(obs_get_module_data_path(
obs_current_module())) +
"/res/cascadeClassifiers/" +
modelPath.substr(oldPrefix.size());
}
#endif
} }
if (scaleFactor.IsFixedType() && !isScaleFactorValid(scaleFactor)) { if (scaleFactor.IsFixedType() && !isScaleFactorValid(scaleFactor)) {
scaleFactor = 1.1; scaleFactor = 1.1;
@@ -303,9 +318,6 @@ OCRParameters::OCRParameters(const OCRParameters &other)
useConfig(other.useConfig), useConfig(other.useConfig),
configFile(other.configFile) configFile(other.configFile)
{ {
if (!initDone) {
Setup();
}
} }
OCRParameters &OCRParameters::operator=(const OCRParameters &other) OCRParameters &OCRParameters::operator=(const OCRParameters &other)
@@ -319,9 +331,7 @@ OCRParameters &OCRParameters::operator=(const OCRParameters &other)
languageCode = other.languageCode; languageCode = other.languageCode;
useConfig = other.useConfig; useConfig = other.useConfig;
configFile = other.configFile; configFile = other.configFile;
if (!initDone) { initDone = false;
Setup();
}
return *this; return *this;
} }
@@ -337,7 +347,7 @@ bool OCRParameters::Save(obs_data_t *obj) const
SaveColor(data, "textColor", color); SaveColor(data, "textColor", color);
colorThreshold.Save(data, "colorThreshold"); colorThreshold.Save(data, "colorThreshold");
obs_data_set_int(data, "pageSegMode", static_cast<int>(pageSegMode)); obs_data_set_int(data, "pageSegMode", static_cast<int>(pageSegMode));
obs_data_set_int(data, "version", 2); obs_data_set_int(data, "version", 3);
obs_data_set_obj(obj, "ocrData", data); obs_data_set_obj(obj, "ocrData", data);
obs_data_release(data); obs_data_release(data);
return true; return true;
@@ -356,6 +366,23 @@ bool OCRParameters::Load(obs_data_t *obj)
tesseractBasePath = std::string(obs_get_module_data_path( tesseractBasePath = std::string(obs_get_module_data_path(
obs_current_module())) + obs_current_module())) +
"/res/ocr/"; "/res/ocr/";
} else if (obs_data_get_int(data, "version") == 2) {
#ifdef _WIN32
// On Windows the plugin directory moved from
// "Program Files/.../data/obs-plugins/advanced-scene-switcher"
// to "ProgramData/.../plugins/advanced-scene-switcher/data",
// which invalidates the previously saved default OCR path.
const std::string oldPath = tesseractBasePath;
if (oldPath ==
"../../data/obs-plugins/advanced-scene-switcher/res/ocr/" ||
oldPath ==
"../../data/obs-plugins/advanced-scene-switcher/res/ocr") {
tesseractBasePath =
std::string(obs_get_module_data_path(
obs_current_module())) +
"/res/ocr/";
}
#endif
} }
useConfig = obs_data_get_bool(data, "useConfig"); useConfig = obs_data_get_bool(data, "useConfig");
configFile = obs_data_get_string(data, "configFile"); configFile = obs_data_get_string(data, "configFile");
@@ -380,6 +407,14 @@ void OCRParameters::SetPageMode(tesseract::PageSegMode mode)
} }
} }
tesseract::TessBaseAPI *OCRParameters::GetOCR()
{
if (!initDone) {
Setup();
}
return initDone ? ocr.get() : nullptr;
}
bool OCRParameters::SetLanguageCode(const std::string &value) bool OCRParameters::SetLanguageCode(const std::string &value)
{ {
const auto dataPath = QString::fromStdString(tesseractBasePath) + "/" + const auto dataPath = QString::fromStdString(tesseractBasePath) + "/" +
@@ -389,7 +424,7 @@ bool OCRParameters::SetLanguageCode(const std::string &value)
return false; return false;
} }
languageCode = value; languageCode = value;
Setup(); initDone = false;
return true; return true;
} }
@@ -408,7 +443,7 @@ bool OCRParameters::SetTesseractBasePath(const std::string &value)
return false; return false;
} }
tesseractBasePath = value; tesseractBasePath = value;
Setup(); initDone = false;
return true; return true;
} }
@@ -420,13 +455,13 @@ std::string OCRParameters::GetTesseractBasePath() const
void OCRParameters::EnableCustomConfig(bool enable) void OCRParameters::EnableCustomConfig(bool enable)
{ {
useConfig = enable; useConfig = enable;
Setup(); initDone = false;
} }
void OCRParameters::SetCustomConfigFile(const std::string &filename) void OCRParameters::SetCustomConfigFile(const std::string &filename)
{ {
configFile = filename; configFile = filename;
Setup(); initDone = false;
} }
void OCRParameters::Setup() void OCRParameters::Setup()

View File

@@ -108,10 +108,7 @@ public:
void SetCustomConfigFile(const std::string &); void SetCustomConfigFile(const std::string &);
std::string GetCustomConfigFile() const { return configFile; } std::string GetCustomConfigFile() const { return configFile; }
tesseract::PageSegMode GetPageMode() const { return pageSegMode; } tesseract::PageSegMode GetPageMode() const { return pageSegMode; }
tesseract::TessBaseAPI *GetOCR() const tesseract::TessBaseAPI *GetOCR();
{
return initDone ? ocr.get() : nullptr;
}
StringVariable text = obs_module_text("AdvSceneSwitcher.enterText"); StringVariable text = obs_module_text("AdvSceneSwitcher.enterText");
RegexConfig regex = RegexConfig::PartialMatchRegexConfig(); RegexConfig regex = RegexConfig::PartialMatchRegexConfig();