Compare commits

...

28 Commits

Author SHA1 Message Date
WarmUpTill
a0e6e6f528 Add options to close source dialogs
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
2025-11-14 13:56:03 +01:00
WarmUpTill
e30d5845fa Run macro via dock button in parallel and ignore pause 2025-11-14 13:56:03 +01:00
WarmUpTill
d12911cd02 Fix typo 2025-11-14 13:56:03 +01:00
WarmUpTill
b8ecc40e8c Add search clear button to the variable tab 2025-11-14 12:38:24 +01:00
WarmUpTill
8ec4849b1d Add option copy value of another variable 2025-11-14 12:38:24 +01:00
WarmUpTill
e6e9f3a831 Add temp var for last replay buffer save path
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2025-11-13 21:06:29 +01:00
WarmUpTill
555f7c1381 Add option to enable dock containing variable tab
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
2025-11-10 18:45:06 +01:00
WarmUpTill
f0f8b0fd92 Add option to search in variable tab 2025-11-10 18:45:06 +01:00
WarmUpTill
661e83162f Cleanup 2025-11-10 18:45:06 +01:00
WarmUpTill
4606f80a9d Enable filtering in "Hotkey" action 2025-11-10 18:45:06 +01:00
WarmUpTill
9c109742fb Add option to change action state based on label and action type
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
2025-11-05 20:49:24 +01:00
WarmUpTill
b8b0682aaf Add support for else actions to MacroSegmentSelection 2025-11-05 20:49:24 +01:00
WarmUpTill
f93175db77 Sort scenes alphabetically
Prior to implementing canvas suppor they were in the order they are in
the scenes dock.
After switching from obs_frontend_get_scene_names() to
obs_canvas_enum_scenes() this seems to not always be the case.
2025-11-05 20:49:24 +01:00
WarmUpTill
8f92ba3ffa Increase slide index selection maximum to 9999 2025-11-05 20:49:24 +01:00
WarmUpTill
0d56de11d1 Refactor to support loading macro properties on import 2025-11-05 20:49:24 +01:00
WarmUpTill
e1020a1909 Fix poad load steps being executed too frequently
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
2025-10-30 20:48:33 +01:00
WarmUpTill
8b0bd4193b Fix temp var save / loading not working 2025-10-30 20:48:33 +01:00
WarmUpTill
d55bb6bc86 Fall back to obs_frontend_get_current_scene()
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
If there wasn't any scene change yet GetCurrentScene() would always
return nullptr and break various scene checks.
For example, this could happen when startup up a fresh OBS install for
the first time.
2025-10-29 12:26:44 +01:00
WarmUpTill
0583331bfd Fix scene selection not working without secondary canvases 2025-10-29 12:26:44 +01:00
WarmUpTill
6932de866d Refactor Twitch event server migration and reconnect handling
Some checks failed
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
Check locale / ubuntu64 (push) Has been cancelled
This should avoid any events being lost due to server migration.
2025-10-28 19:20:52 +01:00
WarmUpTill
00db0cf7c4 CI: Refactor OpenSSL handling on MacOS to support MQTT SSL 2025-10-28 19:20:52 +01:00
WarmUpTill
e9baf27ca2 Add helper to find recent versions of OpenSSL on Windows 2025-10-28 19:20:52 +01:00
WarmUpTill
b1a5db0c9c Fix crash when switching macros after deleting one containing temp refs 2025-10-28 19:20:52 +01:00
WarmUpTill
8f3b868fd9 Limit projector action to main canvas and improve layout 2025-10-28 19:20:52 +01:00
WarmUpTill
b3bf89840b Add GetPath() 2025-10-28 19:20:52 +01:00
WarmUpTill
84f7d0d214 Add SSL support to MQTT connections
Also fixes crash on startup if SSL was used while there was no support
for encrypted connections yet
2025-10-28 19:20:52 +01:00
WarmUpTill
e1164c4fa3 Refactor help icon usage 2025-10-28 19:20:52 +01:00
WarmUpTill
4534b23bad Enable Windows and MacOS build with OBS versions older than 31.1.1 2025-10-28 19:20:52 +01:00
68 changed files with 1577 additions and 466 deletions

View File

@@ -22,7 +22,7 @@ runs:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.28.x'
cmake-version: '3.x.x'
- name: Restore cached dependencies
id: restore-cache

View File

@@ -394,8 +394,11 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
popd
pushd ${advss_dep_path}
log_info "Prepare openssl ..."
rm -rf ${advss_dep_path}/openssl ${advss_dep_path}/openssl_build
mkdir ${advss_dep_path}/openssl_build
pushd ${advss_dep_path}/openssl_build
rm -rf openssl
git clone https://github.com/openssl/openssl.git --branch openssl-3.1.2 --depth 1
mv openssl openssl_x86
@@ -403,25 +406,27 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
log_info "Building openssl x86 ..."
export MACOSX_DEPLOYMENT_TARGET=10.9
cd openssl_x86
./Configure darwin64-x86_64-cc shared
make
pushd openssl_x86
./Configure darwin64-x86_64-cc no-shared no-module no-zlib --prefix=${advss_dep_path}
make -j$(nproc)
popd
log_info "Building openssl arm ..."
export MACOSX_DEPLOYMENT_TARGET=10.15
cd ../openssl_arm
./Configure enable-rc5 zlib darwin64-arm64-cc no-asm
make
pushd openssl_arm
./Configure enable-rc5 darwin64-arm64-cc no-shared no-module no-asm no-zlib --prefix=${advss_dep_path}
make -j$(nproc)
log_info "Install openssl ..."
make install
popd
log_info "Combine arm and x86 openssl binaries ..."
cd ..
mkdir openssl-combined
lipo -create openssl_x86/libcrypto.a openssl_arm/libcrypto.a -output openssl-combined/libcrypto.a
lipo -create openssl_x86/libssl.a openssl_arm/libssl.a -output openssl-combined/libssl.a
lipo -create openssl_x86/libcrypto.a openssl_arm/libcrypto.a -output ${advss_dep_path}/lib/libcrypto.a
lipo -create openssl_x86/libssl.a openssl_arm/libssl.a -output ${advss_dep_path}/lib/libssl.a
log_info "Clean up openssl dir ..."
mv openssl_x86 openssl
rm -rf openssl_arm
rm -rf openssl_x86 openssl_arm
popd
pushd ${project_root}/deps/libusb
@@ -439,14 +444,16 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
mkdir ${project_root}/deps/libusb/out_x86
./autogen.sh
./configure --host=x86_64-apple-darwin --prefix=${advss_dep_path}
make && make install
make -j$(nproc)
make install
log_info "Configure libusb arm ..."
make clean
rm -r ${project_root}/deps/libusb/out_x86
mkdir ${project_root}/deps/libusb/out_x86
./configure --host=aarch64-apple-darwin --prefix=${project_root}/deps/libusb/out_x86
make && make install
make -j$(nproc)
make install
log_info "Building libusb arm ..."
make clean
@@ -459,7 +466,8 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
export MACOSX_DEPLOYMENT_TARGET=10.15
mkdir ${project_root}/deps/libusb/out_arm
./configure --host=aarch64-apple-darwin --prefix=${project_root}/deps/libusb/out_arm
make && make install
make -j$(nproc)
make install
log_info "Combine arm and x86 libusb binaries ..."
lipo -create ${project_root}/deps/libusb/out_x86/lib/libusb-1.0.0.dylib \
@@ -491,8 +499,8 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
-DPAHO_BUILD_SHARED=OFF
-DPAHO_BUILD_STATIC=ON
-DPAHO_WITH_MQTT_C=ON
-DOPENSSL_ROOT_DIR="${advss_dep_path}"
-DPAHO_WITH_SSL=OFF # TODO: figure out linking issues with openssl
-DPAHO_WITH_SSL=ON
-DOPENSSL_USE_STATIC_LIBS=ON
)
pushd ${mqtt_dir}

View File

@@ -253,12 +253,8 @@ ${_usage_host:-}"
macos-*)
if (( ${+CI} )) typeset -gx NSUnbufferedIO=YES
local openssl_lib_dir="${advss_deps_path}/openssl-combined/"
local openssl_include_dir="${advss_deps_path}/openssl/include"
cmake_args+=(
-DOPENSSL_INCLUDE_DIR="${openssl_include_dir}"
-DOPENSSL_LIBRARIES="${openssl_lib_dir}/libcrypto.a;${openssl_lib_dir}/libssl.a"
-DCMAKE_PREFIX_PATH="${advss_deps_path}"
--preset ${_preset}
)

View File

@@ -234,8 +234,24 @@ function Build {
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DPAHO_WITH_MQTT_C=ON"
"-DPAHO_WITH_SSL=ON"
)
# Try to find OpenSSL installed via winget
$pf64 = Join-Path $Env:ProgramFiles "OpenSSL-Win64"
$pf = Join-Path $Env:ProgramFiles "OpenSSL"
$possibleDirs = @($pf64, $pf)
$opensslDir = $possibleDirs | Where-Object { Test-Path (Join-Path $_ "include\openssl\ssl.h") } | Select-Object -First 1
if ($opensslDir) {
Write-Host "Detected OpenSSL at: $opensslDir"
$MqttCmakeArgs += "-DOPENSSL_ROOT_DIR=$opensslDir"
$MqttCmakeArgs += "-DOPENSSL_CRYPTO_LIBRARY=$opensslDir\lib\VC\x64\MD\libcrypto.lib"
$MqttCmakeArgs += "-DOPENSSL_SSL_LIBRARY=$opensslDir\lib\VC\x64\MD\libssl.lib"
} else {
Write-Warning "OpenSSL not found - maybe cmake will find it ..."
}
Log-Information "Configuring paho.mqtt.cpp..."
Invoke-External cmake -S ${MqttPath} -B ${MqttBuildPath} @MqttCmakeArgs

View File

@@ -18,5 +18,7 @@ if (( ! ${+commands[brew]} )) {
}
brew bundle --file ${SCRIPT_HOME}/.Brewfile
rehash
# Workaround to make sure locally built openssl is picked up by cmake
brew uninstall --ignore-dependencies openssl@3 || true
rehash || true
log_group

View File

@@ -6,7 +6,7 @@ on:
description: "Project name detected by parsing build spec file"
value: ${{ jobs.check-event.outputs.pluginName }}
env:
DEP_DIR: .deps/advss-build-dependencies-2
DEP_DIR: .deps/advss-build-dependencies-3
jobs:
check-event:
name: Check GitHub Event Data 🔎
@@ -213,7 +213,7 @@ jobs:
- name: Set up CMake 🏗️
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.24.x'
cmake-version: '3.x.x'
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@master

View File

@@ -34,6 +34,11 @@ include(cmake/common/get_git_revision_description.cmake)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
git_describe(GIT_TAG)
# Helper for OpenSSL
if(OS_WINDOWS)
include(cmake/windows/wingetssl.cmake)
endif()
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
set(GIT_TAG ${PROJECT_VERSION})
endif()

View File

@@ -0,0 +1,78 @@
# ---------------------------------------------------------------------------
# Detects OpenSSL installed via winget or other common Windows locations,
# without requiring the user to manually set OPENSSL_ROOT_DIR.
# ---------------------------------------------------------------------------
if(WIN32 AND (NOT OpenSSL_FOUND))
set(_openssl_roots
"$ENV{ProgramFiles}/OpenSSL-Win64" "$ENV{ProgramFiles}/OpenSSL"
"$ENV{ProgramW6432}/OpenSSL-Win64")
set(_openssl_lib_suffixes "lib/VC/x64/MD" "lib/VC/x64/MDd" "lib/VC/x64/MT"
"lib/VC/x64/MTd" "lib")
# Determine which configuration we're building
if(CMAKE_BUILD_TYPE MATCHES "Debug")
set(_is_debug TRUE)
else()
set(_is_debug FALSE)
endif()
# Determine which runtime we use Default to /MD (shared CRT)
set(_crt_kind "MD")
if(MSVC)
if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "MultiThreaded")
if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "Debug")
set(_crt_kind "MTd")
else()
set(_crt_kind "MT")
endif()
else()
if(_is_debug)
set(_crt_kind "MDd")
else()
set(_crt_kind "MD")
endif()
endif()
endif()
message(STATUS "Looking for OpenSSL built with CRT variant: ${_crt_kind}")
# Try to find the root and corresponding lib path
foreach(_root ${_openssl_roots})
if(EXISTS "${_root}/include/openssl/ssl.h")
foreach(_suffix ${_openssl_lib_suffixes})
if(_suffix MATCHES "${_crt_kind}$"
AND EXISTS "${_root}/${_suffix}/libcrypto.lib")
set(OPENSSL_ROOT_DIR
"${_root}"
CACHE PATH "Path to OpenSSL root")
set(OPENSSL_CRYPTO_LIBRARY
"${_root}/${_suffix}/libcrypto.lib"
CACHE FILEPATH "OpenSSL crypto lib")
set(OPENSSL_SSL_LIBRARY
"${_root}/${_suffix}/libssl.lib"
CACHE FILEPATH "OpenSSL ssl lib")
set(OPENSSL_INCLUDE_DIR
"${_root}/include"
CACHE PATH "OpenSSL include dir")
set(OpenSSL_FOUND
TRUE
CACHE BOOL "Whether OpenSSL was found")
message(STATUS "Found OpenSSL at: ${_root}/${_suffix}")
return()
endif()
endforeach()
endif()
if(OpenSSL_FOUND)
break()
endif()
endforeach()
if(NOT OpenSSL_FOUND)
message(WARNING "Could not auto-detect OpenSSL under Program Files. "
"Might have to set OPENSSL_ROOT_DIR manually.")
endif()
endif()

View File

@@ -91,6 +91,12 @@ AdvSceneSwitcher.variableTab.lastUsed.text.never="Never"
AdvSceneSwitcher.variableTab.lastChanged.header="Last changed"
AdvSceneSwitcher.variableTab.lastChanged.text.none="No change since launch"
AdvSceneSwitcher.variableTab.lastChanged.tooltip="Times changed: %1\n\nPrevious value: %2"
AdvSceneSwitcher.variableTab.search.placeholder="Search ..."
AdvSceneSwitcher.variableTab.search.all="All columns"
AdvSceneSwitcher.variableTab.search.name="Name column"
AdvSceneSwitcher.variableTab.search.value="Value column"
AdvSceneSwitcher.variableTab.addDock="Add dock"
AdvSceneSwitcher.variableTab.clear="Clear"
# Action Queue Tab
AdvSceneSwitcher.actionQueueTab.title="Action Queues"
@@ -928,6 +934,9 @@ AdvSceneSwitcher.action.source.type.deinterlaceOrder="Set deinterlace field orde
AdvSceneSwitcher.action.source.type.openInteractionDialog="Open interaction dialog"
AdvSceneSwitcher.action.source.type.openFilterDialog="Open filter dialog"
AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog"
AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog"
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties dialog"
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}"
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
@@ -949,6 +958,7 @@ AdvSceneSwitcher.action.source.inputMethod.individualTempvar="Set to macro prope
AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.source.refresh="Refresh"
AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value."
AdvSceneSwitcher.action.source.dialog.accept="Accept changes"
AdvSceneSwitcher.action.media="Media"
AdvSceneSwitcher.action.media.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause"
@@ -984,12 +994,16 @@ AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
AdvSceneSwitcher.action.macro.actionSelectionType.id="of action type"
AdvSceneSwitcher.action.macro.type.nestedMacro.conditionHelp="This section allows you to define macro conditions.\n\nClick the plus button below to add a new condition."
AdvSceneSwitcher.action.macro.type.nestedMacro.actionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are met.\n\nClick the plus button below to add a new action."
AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are *not* met.\n\nClick the plus button below to add a new action."
AdvSceneSwitcher.action.macro.entry.run="{{actions}}{{actionTypes}}of{{macros}}"
AdvSceneSwitcher.action.macro.entry.run.condition="{{conditionBehaviors}}of{{conditionMacros}}"
AdvSceneSwitcher.action.macro.entry.other="{{actions}}{{actionIndex}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.run="{{actions}}{{actionSections}}of{{macros}}"
AdvSceneSwitcher.action.macro.layout.run.condition="{{conditionBehaviors}}of{{conditionMacros}}"
AdvSceneSwitcher.action.macro.layout.actionState="{{actions}}{{actionSelectionType}}{{actionIndex}}{{label}}{{regex}}{{actionTypes}}in{{actionSections}}section of{{macros}}"
AdvSceneSwitcher.action.macro.layout.other="{{actions}}{{macros}}"
AdvSceneSwitcher.action.pluginState="Plugin state"
AdvSceneSwitcher.action.pluginState.type.stop="Stop the Advanced Scene Switcher plugin"
AdvSceneSwitcher.action.pluginState.type.noMatch="Change the no-match behaviour:"
@@ -1141,6 +1155,7 @@ AdvSceneSwitcher.action.http.entry.line2="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.variable="Variable"
AdvSceneSwitcher.action.variable.type.set="Set to value"
AdvSceneSwitcher.action.variable.type.append="Append"
AdvSceneSwitcher.action.variable.type.copy="Copy variable"
AdvSceneSwitcher.action.variable.type.appendVar="Append variable"
AdvSceneSwitcher.action.variable.type.increment="Increment"
AdvSceneSwitcher.action.variable.type.decrement="Decrement"
@@ -1514,6 +1529,13 @@ AdvSceneSwitcher.mqttConnection.name="Name:"
AdvSceneSwitcher.mqttConnection.address="Address:"
AdvSceneSwitcher.mqttConnection.username="Username:"
AdvSceneSwitcher.mqttConnection.password="Password:"
AdvSceneSwitcher.mqttConnection.trustStore="Trust store:"
AdvSceneSwitcher.mqttConnection.trustStore.help="The file in PEM format containing the public digital certificates trusted by the client."
AdvSceneSwitcher.mqttConnection.keyStore="Key store:"
AdvSceneSwitcher.mqttConnection.keyStore.help="The file in PEM format containing the public certificate chain of the client.\nIt may also include the client's private key."
AdvSceneSwitcher.mqttConnection.privateKey="Private key:"
AdvSceneSwitcher.mqttConnection.privateKey.help="If not included in the key store, this is the file in PEM format containing the client's private key."
AdvSceneSwitcher.mqttConnection.verifyServerCert="Verify server certificate"
AdvSceneSwitcher.mqttConnection.reconnect="Reconnect automatically:"
AdvSceneSwitcher.mqttConnection.reconnectDelay="Automatically reconnect after:"
AdvSceneSwitcher.mqttConnection.connectOnStart="Connect on startup:"
@@ -2301,6 +2323,9 @@ AdvSceneSwitcher.tempVar.mqtt.message="Message"
AdvSceneSwitcher.tempVar.cursor.x="Cursor position (X)"
AdvSceneSwitcher.tempVar.cursor.y="Cursor position (Y)"
AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path"
AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved."
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectCanvas="--select canvas--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"

View File

@@ -861,9 +861,9 @@ AdvSceneSwitcher.action.macro.type.stop="Parar ações"
AdvSceneSwitcher.action.macro.type.disableAction="Desabilitar ação"
AdvSceneSwitcher.action.macro.type.enableAction="Habilitar ação"
AdvSceneSwitcher.action.macro.type.toggleAction="Alternar ação"
AdvSceneSwitcher.action.macro.entry.run="{{actions}}{{actionTypes}}de{{macros}}"
AdvSceneSwitcher.action.macro.entry.run.condition="{{conditionBehaviors}}de{{conditionMacros}}"
AdvSceneSwitcher.action.macro.entry.other="{{actions}}{{actionIndex}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.run="{{actions}}{{actionSections}}de{{macros}}"
AdvSceneSwitcher.action.macro.layout.run.condition="{{conditionBehaviors}}de{{conditionMacros}}"
AdvSceneSwitcher.action.macro.layout.other="{{actions}}{{macros}}"
AdvSceneSwitcher.action.pluginState="Estado do plugin"
AdvSceneSwitcher.action.pluginState.type.stop="Parar o plugin Advanced Scene Switcher"
AdvSceneSwitcher.action.pluginState.type.noMatch="Alterar o comportamento em caso de não correspondência:"

View File

@@ -920,9 +920,9 @@ AdvSceneSwitcher.action.macro.type.stop="停止操作"
AdvSceneSwitcher.action.macro.type.disableAction="停用操作"
AdvSceneSwitcher.action.macro.type.enableAction="启用操作"
AdvSceneSwitcher.action.macro.type.toggleAction="切换操作开关"
AdvSceneSwitcher.action.macro.entry.run="{{actions}}{{actionTypes}}{{macros}}"
AdvSceneSwitcher.action.macro.entry.run.condition="{{conditionBehaviors}}{{conditionMacros}}"
AdvSceneSwitcher.action.macro.entry.other="{{actions}}{{actionIndex}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.run="{{actions}}{{actionSections}}{{macros}}"
AdvSceneSwitcher.action.macro.layout.run.condition="{{conditionBehaviors}}{{conditionMacros}}"
AdvSceneSwitcher.action.macro.layout.other="{{actions}}{{macros}}"
AdvSceneSwitcher.action.pluginState="插件状态"
AdvSceneSwitcher.action.pluginState.type.stop="停止高级场景切换插件"
AdvSceneSwitcher.action.pluginState.type.noMatch="没有匹配项时:"

View File

@@ -421,7 +421,7 @@ bool SwitcherData::CheckForMatch(OBSWeakSource &scene,
static void ResetMacros()
{
for (auto &m : GetMacros()) {
for (auto &m : GetTopLevelMacros()) {
ResetMacroRunCount(m.get());
ResetMacroConditionTimers(m.get());
}

View File

@@ -459,7 +459,7 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
LoadHotkeys(obj);
LoadUISettings(obj);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
// Reset on startup and scene collection change
ResetLastOpenedTab();

View File

@@ -117,7 +117,7 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text)
*_entryData = MacroActionFactory::Create(id, macro);
(*_entryData)->SetIndex(idx);
(*_entryData)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
}
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),

View File

@@ -2,6 +2,7 @@
#include "help-icon.hpp"
#include "layout-helpers.hpp"
#include "macro.hpp"
#include "macro-action-factory.hpp"
namespace advss {
@@ -12,6 +13,74 @@ bool MacroActionMacro::_registered = MacroActionFactory::Register(
{MacroActionMacro::Create, MacroActionMacroEdit::Create,
"AdvSceneSwitcher.action.macro"});
void MacroActionMacro::AdjustActionState(Macro *macro) const
{
const auto &macroActions = _useElseSection ? macro->ElseActions()
: macro->Actions();
std::vector<std::shared_ptr<MacroAction>> actionsToModify;
switch (_actionSelectionType) {
case SelectionType::INDEX: {
const bool isValidAction =
(_useElseSection &&
IsValidElseActionIndex(macro, _actionIndex - 1)) ||
(!_useElseSection &&
IsValidActionIndex(macro, _actionIndex - 1));
if (isValidAction) {
actionsToModify.emplace_back(
macroActions.at(_actionIndex - 1));
}
break;
}
case SelectionType::LABEL:
for (const auto &action : macroActions) {
if (!action->GetUseCustomLabel()) {
continue;
}
const auto label = action->GetCustomLabel();
if (_regex.Enabled()) {
if (_regex.Matches(label, _label)) {
actionsToModify.emplace_back(action);
}
continue;
}
if (label == std::string(_label)) {
actionsToModify.emplace_back(action);
}
}
break;
case SelectionType::ID:
for (const auto &action : macroActions) {
if (action->GetId() == _actionId) {
actionsToModify.emplace_back(action);
}
}
break;
default:
break;
}
for (const auto &action : actionsToModify) {
switch (_action) {
case Action::DISABLE_ACTION:
action->SetEnabled(false);
break;
case Action::ENABLE_ACTION:
action->SetEnabled(true);
break;
case Action::TOGGLE_ACTION:
action->SetEnabled(!action->Enabled());
break;
default:
break;
}
}
}
bool MacroActionMacro::PerformAction()
{
if (_action == Action::NESTED_MACRO) {
@@ -44,23 +113,9 @@ bool MacroActionMacro::PerformAction()
macro->Stop();
break;
case Action::DISABLE_ACTION:
if (IsValidMacroSegmentIndex(macro.get(), _actionIndex - 1,
false)) {
macro->Actions().at(_actionIndex - 1)->SetEnabled(false);
}
break;
case Action::ENABLE_ACTION:
if (IsValidMacroSegmentIndex(macro.get(), _actionIndex - 1,
false)) {
macro->Actions().at(_actionIndex - 1)->SetEnabled(true);
}
break;
case Action::TOGGLE_ACTION:
if (IsValidMacroSegmentIndex(macro.get(), _actionIndex - 1,
false)) {
auto action = macro->Actions().at(_actionIndex - 1);
action->SetEnabled(!action->Enabled());
}
AdjustActionState(macro.get());
break;
default:
break;
@@ -115,9 +170,14 @@ void MacroActionMacro::LogAction() const
bool MacroActionMacro::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_macro.Save(obj);
_actionIndex.Save(obj, "actionIndex");
obs_data_set_int(obj, "action", static_cast<int>(_action));
_macro.Save(obj);
obs_data_set_int(obj, "actionSelectionType",
static_cast<int>(_actionSelectionType));
_actionIndex.Save(obj, "actionIndex");
_label.Save(obj, "label");
obs_data_set_string(obj, "actionId", _actionId.c_str());
_regex.Save(obj);
_runOptions.Save(obj);
OBSDataAutoRelease nestedMacroData = obs_data_create();
_nestedMacro->Save(nestedMacroData);
@@ -129,10 +189,15 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
bool MacroActionMacro::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_macro.Load(obj);
_actionIndex.Load(obj, "actionIndex");
_action = static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action"));
_macro.Load(obj);
_actionSelectionType = static_cast<SelectionType>(
obs_data_get_int(obj, "actionSelectionType"));
_actionIndex.Load(obj, "actionIndex");
_label.Load(obj, "label");
_actionId = obs_data_get_string(obj, "actionId");
_regex.Load(obj);
_runOptions.Load(obj);
if (obs_data_has_user_value(obj, "nestedMacro")) {
@@ -183,6 +248,7 @@ std::shared_ptr<MacroAction> MacroActionMacro::Copy() const
void MacroActionMacro::ResolveVariablesToFixedValues()
{
_actionIndex.ResolveVariables();
_label.ResolveVariables();
}
static void runActionsHelper(Macro *macro, bool runElseActions, bool setInputs,
@@ -266,7 +332,7 @@ static void populateConditionBehaviorSelection(QComboBox *list)
"AdvSceneSwitcher.action.macro.type.run.conditions.false"));
}
static void populateActionTypeSelection(QComboBox *list)
static void populateActionSectionSelection(QComboBox *list)
{
list->addItem(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.actionType.regular"));
@@ -274,19 +340,47 @@ static void populateActionTypeSelection(QComboBox *list)
"AdvSceneSwitcher.action.macro.type.run.actionType.else"));
}
static void populateActionTypes(QComboBox *list)
{
for (const auto &[id, info] : MacroActionFactory::GetActionTypes()) {
list->addItem(obs_module_text(info._name.c_str()),
QString::fromStdString(id));
}
}
static void populateActionSelectionTypes(QComboBox *list)
{
list->addItem(
obs_module_text(
"AdvSceneSwitcher.action.macro.actionSelectionType.index"),
static_cast<int>(MacroActionMacro::SelectionType::INDEX));
list->addItem(
obs_module_text(
"AdvSceneSwitcher.action.macro.actionSelectionType.label"),
static_cast<int>(MacroActionMacro::SelectionType::LABEL));
list->addItem(
obs_module_text(
"AdvSceneSwitcher.action.macro.actionSelectionType.id"),
static_cast<int>(MacroActionMacro::SelectionType::ID));
}
MacroActionMacroEdit::MacroActionMacroEdit(
QWidget *parent, std::shared_ptr<MacroActionMacro> entryData)
: ResizableWidget(parent),
_actions(new QComboBox()),
_macros(new MacroSelection(parent)),
_actionSelectionType(new QComboBox(this)),
_actionIndex(new MacroSegmentSelection(
this, MacroSegmentSelection::Type::ACTION)),
_actions(new QComboBox()),
_label(new VariableLineEdit(this)),
_actionTypes(new FilterComboBox(this)),
_regex(new RegexConfigWidget(this)),
_conditionMacros(new MacroSelection(parent)),
_conditionBehaviors(new QComboBox()),
_reevaluateConditionState(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.macro.type.run."
"updateConditionMatchState"))),
_actionTypes(new QComboBox()),
_actionSections(new QComboBox(this)),
_skipWhenPaused(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.macro.type.run.skipWhenPaused"))),
_setInputs(new QCheckBox(obs_module_text(
@@ -305,7 +399,9 @@ MacroActionMacroEdit::MacroActionMacroEdit(
{
populateActionSelection(_actions);
populateConditionBehaviorSelection(_conditionBehaviors);
populateActionTypeSelection(_actionTypes);
populateActionSectionSelection(_actionSections);
populateActionSelectionTypes(_actionSelectionType);
populateActionTypes(_actionTypes);
_conditionMacros->HideSelectedMacro();
@@ -313,16 +409,25 @@ MacroActionMacroEdit::MacroActionMacroEdit(
this, SLOT(MacroChanged(const QString &)));
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_actionSelectionType, SIGNAL(currentIndexChanged(int)),
this, SLOT(ActionSelectionTypeChanged(int)));
QWidget::connect(_actionIndex,
SIGNAL(SelectionChanged(const IntVariable &)), this,
SLOT(ActionIndexChanged(const IntVariable &)));
QWidget::connect(_label, SIGNAL(editingFinished()), this,
SLOT(LabelChanged()));
QWidget::connect(_actionTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionTypeChanged(int)));
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_conditionMacros,
SIGNAL(currentTextChanged(const QString &)), this,
SLOT(ConditionMacroChanged(const QString &)));
QWidget::connect(_conditionBehaviors, SIGNAL(currentIndexChanged(int)),
this, SLOT(ConditionBehaviorChanged(int)));
QWidget::connect(_actionTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionTypeChanged(int)));
QWidget::connect(_actionSections, SIGNAL(currentIndexChanged(int)),
this, SLOT(ActionSectionChanged(int)));
QWidget::connect(_skipWhenPaused, SIGNAL(stateChanged(int)), this,
SLOT(SkipWhenPausedChanged(int)));
QWidget::connect(_setInputs, SIGNAL(stateChanged(int)), this,
@@ -375,17 +480,24 @@ void MacroActionMacroEdit::UpdateEntryData()
if (!_entryData) {
return;
}
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
static_cast<int>(_entryData->_actionSelectionType)));
_actionIndex->SetValue(_entryData->_actionIndex);
_actionIndex->SetMacro(_entryData->_macro.GetMacro());
_label->setText(_entryData->_label);
_actionTypes->setCurrentIndex(_actionTypes->findData(
QString::fromStdString(_entryData->_actionId)));
_regex->SetRegexConfig(_entryData->_regex);
_macros->SetCurrentMacro(_entryData->_macro);
_conditionMacros->SetCurrentMacro(_entryData->_runOptions.macro);
_conditionBehaviors->setCurrentIndex(
static_cast<int>(_entryData->_runOptions.logic));
_reevaluateConditionState->setChecked(
_entryData->_runOptions.reevaluateConditionState);
_actionTypes->setCurrentIndex(
_actionSections->setCurrentIndex(
_entryData->_runOptions.runElseActions ? 1 : 0);
_skipWhenPaused->setChecked(_entryData->_runOptions.skipWhenPaused);
_setInputs->setChecked(_entryData->_runOptions.setInputs);
@@ -424,12 +536,40 @@ void MacroActionMacroEdit::ActionChanged(int idx)
SetWidgetVisibility();
}
void MacroActionMacroEdit::ActionSelectionTypeChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_actionSelectionType =
static_cast<MacroActionMacro::SelectionType>(
_actionSelectionType->itemData(idx).toInt());
SetWidgetVisibility();
}
void MacroActionMacroEdit::ActionIndexChanged(const IntVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_actionIndex = value;
}
void MacroActionMacroEdit::LabelChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_label = _label->text().toStdString();
}
void MacroActionMacroEdit::ActionTypeChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_actionId =
_actionTypes->itemData(idx).toString().toStdString();
}
void MacroActionMacroEdit::RegexChanged(const RegexConfig &regex)
{
GUARD_LOADING_AND_LOCK();
_entryData->_regex = regex;
}
void MacroActionMacroEdit::ConditionMacroChanged(const QString &text)
{
GUARD_LOADING_AND_LOCK();
@@ -451,10 +591,13 @@ void MacroActionMacroEdit::ReevaluateConditionStateChanged(int value)
SetWidgetVisibility();
}
void MacroActionMacroEdit::ActionTypeChanged(int value)
void MacroActionMacroEdit::ActionSectionChanged(int useElse)
{
GUARD_LOADING_AND_LOCK();
_entryData->_runOptions.runElseActions = value;
_entryData->_runOptions.runElseActions = useElse;
_entryData->_useElseSection = useElse;
_actionIndex->SetType(useElse ? MacroSegmentSelection::Type::ELSE_ACTION
: MacroSegmentSelection::Type::ACTION);
}
void MacroActionMacroEdit::SkipWhenPausedChanged(int value)
@@ -483,7 +626,11 @@ void MacroActionMacroEdit::SetWidgetVisibility()
_entryLayout->removeWidget(_actions);
_entryLayout->removeWidget(_actionIndex);
_entryLayout->removeWidget(_macros);
_entryLayout->removeWidget(_actionSections);
_entryLayout->removeWidget(_label);
_entryLayout->removeWidget(_regex);
_entryLayout->removeWidget(_actionTypes);
_entryLayout->removeWidget(_actionSelectionType);
_conditionLayout->removeWidget(_conditionBehaviors);
_conditionLayout->removeWidget(_conditionMacros);
@@ -494,19 +641,40 @@ void MacroActionMacroEdit::SetWidgetVisibility()
{"{{actions}}", _actions},
{"{{actionIndex}}", _actionIndex},
{"{{macros}}", _macros},
{"{{actionTypes}}", _actionTypes},
{"{{actionSections}}", _actionSections},
{"{{conditionBehaviors}}", _conditionBehaviors},
{"{{conditionMacros}}", _conditionMacros},
{"{{actionSelectionType}}", _actionSelectionType},
{"{{label}}", _label},
{"{{regex}}", _regex},
{"{{actionTypes}}", _actionTypes},
};
PlaceWidgets(
obs_module_text(
_entryData->_action ==
MacroActionMacro::Action::RUN_ACTIONS
? "AdvSceneSwitcher.action.macro.entry.run"
: "AdvSceneSwitcher.action.macro.entry.other"),
_entryLayout, placeholders);
const auto action = _entryData->_action;
const char *layoutText = "";
switch (action) {
case MacroActionMacro::Action::PAUSE:
case MacroActionMacro::Action::UNPAUSE:
case MacroActionMacro::Action::TOGGLE_PAUSE:
case MacroActionMacro::Action::RESET_COUNTER:
case MacroActionMacro::Action::STOP:
case MacroActionMacro::Action::NESTED_MACRO:
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
break;
case MacroActionMacro::Action::RUN_ACTIONS:
layoutText = "AdvSceneSwitcher.action.macro.layout.run";
break;
case MacroActionMacro::Action::DISABLE_ACTION:
case MacroActionMacro::Action::ENABLE_ACTION:
case MacroActionMacro::Action::TOGGLE_ACTION:
layoutText = "AdvSceneSwitcher.action.macro.layout.actionState";
break;
default:
break;
}
PlaceWidgets(obs_module_text(layoutText), _entryLayout, placeholders);
if (_entryData->_runOptions.logic ==
MacroActionMacro::RunOptions::Logic::IGNORE_CONDITIONS) {
@@ -515,56 +683,63 @@ void MacroActionMacroEdit::SetWidgetVisibility()
} else {
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.macro.entry.run.condition"),
"AdvSceneSwitcher.action.macro.layout.run.condition"),
_conditionLayout, placeholders);
}
if (_entryData->_action == MacroActionMacro::Action::RUN_ACTIONS ||
_entryData->_action == MacroActionMacro::Action::STOP) {
if (action == MacroActionMacro::Action::RUN_ACTIONS ||
action == MacroActionMacro::Action::STOP) {
_macros->HideSelectedMacro();
} else {
_macros->ShowAllMacros();
}
const auto actionSelectionType = _entryData->_actionSelectionType;
const bool isModifyingActionState =
_entryData->_action ==
MacroActionMacro::Action::DISABLE_ACTION ||
_entryData->_action ==
MacroActionMacro::Action::ENABLE_ACTION ||
_entryData->_action == MacroActionMacro::Action::TOGGLE_ACTION;
_actionIndex->setVisible(isModifyingActionState);
action == MacroActionMacro::Action::DISABLE_ACTION ||
action == MacroActionMacro::Action::ENABLE_ACTION ||
action == MacroActionMacro::Action::TOGGLE_ACTION;
_actionSelectionType->setVisible(isModifyingActionState);
_actionIndex->setVisible(
isModifyingActionState &&
actionSelectionType == MacroActionMacro::SelectionType::INDEX);
_label->setVisible(isModifyingActionState &&
actionSelectionType ==
MacroActionMacro::SelectionType::LABEL);
_regex->setVisible(isModifyingActionState &&
actionSelectionType ==
MacroActionMacro::SelectionType::LABEL);
_actionTypes->setVisible(isModifyingActionState &&
actionSelectionType ==
MacroActionMacro::SelectionType::ID);
SetLayoutVisible(_conditionLayout,
_entryData->_action ==
MacroActionMacro::Action::RUN_ACTIONS);
action == MacroActionMacro::Action::RUN_ACTIONS);
const bool needsAdditionalConditionWidgets =
_entryData->_action == MacroActionMacro::Action::RUN_ACTIONS &&
action == MacroActionMacro::Action::RUN_ACTIONS &&
_entryData->_runOptions.logic !=
MacroActionMacro::RunOptions::Logic::IGNORE_CONDITIONS;
_conditionMacros->setVisible(needsAdditionalConditionWidgets);
SetLayoutVisible(_reevaluateConditionStateLayout,
needsAdditionalConditionWidgets);
SetLayoutVisible(_setInputsLayout,
_entryData->_action ==
MacroActionMacro::Action::RUN_ACTIONS);
_inputs->setVisible(_entryData->_action ==
MacroActionMacro::Action::RUN_ACTIONS &&
action == MacroActionMacro::Action::RUN_ACTIONS);
_inputs->setVisible(action == MacroActionMacro::Action::RUN_ACTIONS &&
_entryData->_runOptions.setInputs);
HighlightMacroSettingsButton(
_entryData->_action == MacroActionMacro::Action::RUN_ACTIONS &&
action == MacroActionMacro::Action::RUN_ACTIONS &&
_entryData->_runOptions.setInputs &&
!_inputs->HasInputsToSet());
_actionTypes->setVisible(_entryData->_action ==
MacroActionMacro::Action::RUN_ACTIONS);
_skipWhenPaused->setVisible(_entryData->_action ==
_actionSections->setVisible(
action == MacroActionMacro::Action::RUN_ACTIONS ||
isModifyingActionState);
_skipWhenPaused->setVisible(action ==
MacroActionMacro::Action::RUN_ACTIONS);
_nestedMacro->setVisible(_entryData->_action ==
_nestedMacro->setVisible(action ==
MacroActionMacro::Action::NESTED_MACRO);
_macros->setVisible(_entryData->_action !=
MacroActionMacro::Action::NESTED_MACRO);
SetResizingEnabled(_entryData->_action ==
MacroActionMacro::Action::NESTED_MACRO);
_macros->setVisible(action != MacroActionMacro::Action::NESTED_MACRO);
SetResizingEnabled(action == MacroActionMacro::Action::NESTED_MACRO);
if (_nestedMacro->IsEmpty()) {
_nestedMacro->ShowAllMacroSections();

View File

@@ -5,7 +5,9 @@
#include "macro-input.hpp"
#include "macro-selection.hpp"
#include "macro-segment-selection.hpp"
#include "regex-config.hpp"
#include "resizable-widget.hpp"
#include "variable-line-edit.hpp"
#include <QCheckBox>
#include <QHBoxLayout>
@@ -55,14 +57,23 @@ public:
TOGGLE_PAUSE,
NESTED_MACRO,
};
enum class SelectionType { INDEX, LABEL, ID };
Action _action = Action::NESTED_MACRO;
SelectionType _actionSelectionType = SelectionType::INDEX;
bool _useElseSection = false;
IntVariable _actionIndex = 1;
StringVariable _label = "Custom label";
std::string _actionId;
RegexConfig _regex;
RunOptions _runOptions = {};
std::shared_ptr<Macro> _nestedMacro = std::make_shared<Macro>();
int _customWidgetHeight = 0;
private:
void RunActions(Macro *actionMacro) const;
void AdjustActionState(Macro *) const;
static bool _registered;
static const std::string id;
@@ -82,11 +93,15 @@ public:
private slots:
void MacroChanged(const QString &text);
void ActionChanged(int value);
void ActionSelectionTypeChanged(int value);
void ActionIndexChanged(const IntVariable &value);
void LabelChanged();
void ActionTypeChanged(int value);
void RegexChanged(const RegexConfig &);
void ConditionMacroChanged(const QString &text);
void ConditionBehaviorChanged(int value);
void ReevaluateConditionStateChanged(int value);
void ActionTypeChanged(int value);
void ActionSectionChanged(int value);
void SkipWhenPausedChanged(int value);
void SetInputsChanged(int value);
void InputsChanged(const StringList &);
@@ -98,13 +113,17 @@ private:
void SetWidgetVisibility();
void SetupMacroInput(Macro *) const;
MacroSelection *_macros;
MacroSegmentSelection *_actionIndex;
QComboBox *_actions;
MacroSelection *_macros;
QComboBox *_actionSelectionType;
MacroSegmentSelection *_actionIndex;
VariableLineEdit *_label;
FilterComboBox *_actionTypes;
RegexConfigWidget *_regex;
MacroSelection *_conditionMacros;
QComboBox *_conditionBehaviors;
QCheckBox *_reevaluateConditionState;
QComboBox *_actionTypes;
QComboBox *_actionSections;
QCheckBox *_skipWhenPaused;
QCheckBox *_setInputs;
MacroInputEdit *_inputs;

View File

@@ -1,4 +1,5 @@
#include "macro-action-variable.hpp"
#include "help-icon.hpp"
#include "json-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-condition-edit.hpp"
@@ -285,6 +286,14 @@ bool MacroActionVariable::PerformAction()
case Action::APPEND:
apppend(*var, _strValue);
break;
case Action::COPY_VAR: {
auto var2 = _variable2.lock();
if (!var2) {
return true;
}
var->SetValue(var2->Value());
break;
}
case Action::APPEND_VAR: {
auto var2 = _variable2.lock();
if (!var2) {
@@ -669,6 +678,8 @@ static inline void populateActionSelection(QComboBox *list)
actions = {
{MacroActionVariable::Action::SET_VALUE,
"AdvSceneSwitcher.action.variable.type.set"},
{MacroActionVariable::Action::COPY_VAR,
"AdvSceneSwitcher.action.variable.type.copy"},
{MacroActionVariable::Action::APPEND,
"AdvSceneSwitcher.action.variable.type.append"},
{MacroActionVariable::Action::PAD,
@@ -828,7 +839,10 @@ MacroActionVariableEdit::MacroActionVariableEdit(
this)),
_randomLayout(new QVBoxLayout()),
_jsonQuery(new VariableLineEdit(this)),
_jsonQueryHelp(new QLabel(this)),
_jsonQueryHelp(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.info"),
this)),
_jsonIndex(new VariableSpinBox(this)),
_entryLayout(new QHBoxLayout())
{
@@ -859,14 +873,6 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_randomNumberStart->setMaximum(9999999999);
_randomNumberEnd->setMinimum(-9999999999);
_randomNumberEnd->setMaximum(9999999999);
const QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
const QIcon icon(path);
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
_jsonQueryHelp->setPixmap(pixmap);
_jsonQueryHelp->setToolTip(obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.info"));
_jsonIndex->setMaximum(999);
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),
@@ -1525,6 +1531,7 @@ void MacroActionVariableEdit::SetWidgetVisibility()
}
_variables2->setVisible(
_entryData->_action == MacroActionVariable::Action::COPY_VAR ||
_entryData->_action ==
MacroActionVariable::Action::APPEND_VAR ||
_entryData->_action ==

View File

@@ -55,6 +55,7 @@ public:
RANDOM_NUMBER,
QUERY_JSON,
ARRAY_JSON,
COPY_VAR,
};
Action _action = Action::SET_VALUE;

View File

@@ -239,7 +239,7 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
(*_entryData)->SetIndex(idx);
(*_entryData)->SetLogicType(logic);
(*_entryData)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
}
auto widget =
MacroConditionFactory::CreateWidget(id, this, *_entryData);

View File

@@ -102,7 +102,7 @@ bool MacroConditionMacro::CheckActionStateCondition()
if (!macro) {
return false;
}
if (!IsValidMacroSegmentIndex(macro.get(), _actionIndex - 1, false)) {
if (!IsValidActionIndex(macro.get(), _actionIndex - 1)) {
return false;
}
if (_type == Type::ACTION_DISABLED) {

View File

@@ -110,7 +110,7 @@ void MacroDock::RunClicked()
return;
}
auto ret = macro->PerformActions(true);
auto ret = macro->PerformActions(true, true, true);
if (!ret) {
QString err =
obs_module_text("AdvSceneSwitcher.macroTab.runFail");

View File

@@ -1080,7 +1080,7 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
macro->Actions().at(idx)->Load(data);
}
macro->Actions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
macro->UpdateActionIndices();
ui->actionsList->Insert(
idx,
@@ -1383,7 +1383,7 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
macro->ElseActions().at(idx)->Load(data);
}
macro->ElseActions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
macro->UpdateElseActionIndices();
ui->elseActionsList->Insert(
idx, new MacroActionEdit(
@@ -1583,7 +1583,7 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
macro->Conditions().at(idx)->Load(data);
}
macro->Conditions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
(*cond)->SetLogicType(logic);
macro->UpdateConditionIndices();
ui->conditionsList->Insert(

View File

@@ -1,5 +1,6 @@
#include "macro-helpers.hpp"
#include "macro.hpp"
#include "macro-action-macro.hpp"
#include "plugin-state-helpers.hpp"
namespace advss {
@@ -8,6 +9,60 @@ static std::atomic_bool abortMacroWait = {false};
static std::atomic_bool macroSceneSwitched = {false};
static std::atomic_int shutdownConditionCount = {0};
static void appendNestedMacros(std::deque<std::shared_ptr<Macro>> &macros,
Macro *macro)
{
if (!macro) {
return;
}
const auto iterate = [&macros](const std::deque<
std::shared_ptr<MacroAction>> &actions) {
for (const auto &action : actions) {
const auto nestedMacroAction =
dynamic_cast<MacroActionMacro *>(action.get());
if (!nestedMacroAction) {
continue;
}
macros.push_back(nestedMacroAction->_nestedMacro);
appendNestedMacros(
macros, nestedMacroAction->_nestedMacro.get());
}
};
iterate(macro->Actions());
iterate(macro->ElseActions());
}
std::deque<std::shared_ptr<Macro>> &GetTopLevelMacros()
{
static std::deque<std::shared_ptr<Macro>> macros;
return macros;
}
std::deque<std::shared_ptr<Macro>> &GetTemporaryMacros()
{
static std::deque<std::shared_ptr<Macro>> tempMacros;
return tempMacros;
}
std::deque<std::shared_ptr<Macro>> GetAllMacros()
{
auto macros = GetTopLevelMacros();
for (const auto &topLevelMacro : macros) {
appendNestedMacros(macros, topLevelMacro.get());
}
const auto &tempMacros = GetTemporaryMacros();
macros.insert(macros.end(), tempMacros.begin(), tempMacros.end());
for (const auto &tempMacro : tempMacros) {
appendNestedMacros(macros, tempMacro.get());
}
return macros;
}
std::optional<std::deque<std::shared_ptr<MacroAction>>>
GetMacroActions(Macro *macro)
{
@@ -17,6 +72,15 @@ GetMacroActions(Macro *macro)
return macro->Actions();
}
std::optional<std::deque<std::shared_ptr<MacroAction>>>
GetMacroElseActions(Macro *macro)
{
if (!macro) {
return {};
}
return macro->ElseActions();
}
std::optional<std::deque<std::shared_ptr<MacroCondition>>>
GetMacroConditions(Macro *macro)
{
@@ -144,20 +208,42 @@ void ResetMacroRunCount(Macro *macro)
macro->ResetRunCount();
}
bool IsValidMacroSegmentIndex(const Macro *m, const int idx, bool isCondition)
bool IsValidActionIndex(const Macro *m, const int idx)
{
if (!m || idx < 0) {
return false;
}
if (isCondition) {
if (idx >= (int)m->Conditions().size()) {
return false;
}
} else {
if (idx >= (int)m->Actions().size()) {
return false;
}
if (idx >= (int)m->Actions().size()) {
return false;
}
return true;
}
bool IsValidElseActionIndex(const Macro *m, const int idx)
{
if (!m || idx < 0) {
return false;
}
if (idx >= (int)m->ElseActions().size()) {
return false;
}
return true;
}
bool IsValidConditionIndex(const Macro *m, const int idx)
{
if (!m || idx < 0) {
return false;
}
if (idx >= (int)m->Conditions().size()) {
return false;
}
return true;
}

View File

@@ -18,10 +18,14 @@ class Macro;
class MacroAction;
class MacroCondition;
EXPORT std::deque<std::shared_ptr<Macro>> &GetMacros();
EXPORT std::deque<std::shared_ptr<Macro>> &GetTopLevelMacros();
std::deque<std::shared_ptr<Macro>> &GetTemporaryMacros();
EXPORT std::deque<std::shared_ptr<Macro>> GetAllMacros();
EXPORT std::optional<std::deque<std::shared_ptr<MacroAction>>>
GetMacroActions(Macro *);
EXPORT std::optional<std::deque<std::shared_ptr<MacroAction>>>
GetMacroElseActions(Macro *);
EXPORT std::optional<std::deque<std::shared_ptr<MacroCondition>>>
GetMacroConditions(Macro *);
@@ -68,6 +72,8 @@ EXPORT void InvalidateMacroTempVarValues();
EXPORT void ResetMacroConditionTimers(Macro *);
EXPORT void ResetMacroRunCount(Macro *);
bool IsValidMacroSegmentIndex(const Macro *m, const int idx, bool isCondition);
bool IsValidActionIndex(const Macro *m, const int idx);
bool IsValidElseActionIndex(const Macro *m, const int idx);
bool IsValidConditionIndex(const Macro *m, const int idx);
} // namespace advss

View File

@@ -73,35 +73,46 @@ void MacroSegmentSelection::MacroSegmentOrderChanged()
}
static QString GetMacroSegmentDescription(Macro *macro, int idx,
bool isCondition)
MacroSegmentSelection::Type type)
{
if (!macro) {
return "";
}
if (!IsValidMacroSegmentIndex(macro, idx, isCondition)) {
MacroSegment *segment;
switch (type) {
case MacroSegmentSelection::Type::CONDITION:
if (!IsValidConditionIndex(macro, idx)) {
return "";
}
segment = GetMacroConditions(macro)->at(idx).get();
break;
case MacroSegmentSelection::Type::ACTION:
if (!IsValidActionIndex(macro, idx)) {
return "";
}
segment = GetMacroActions(macro)->at(idx).get();
break;
case MacroSegmentSelection::Type::ELSE_ACTION:
if (!IsValidElseActionIndex(macro, idx)) {
return "";
}
segment = GetMacroElseActions(macro)->at(idx).get();
break;
default:
return "";
}
MacroSegment *segment;
if (isCondition) {
segment = GetMacroConditions(macro)->at(idx).get();
} else {
segment = GetMacroActions(macro)->at(idx).get();
}
const auto idToName = type == MacroSegmentSelection::Type::CONDITION
? MacroConditionFactory::GetConditionName
: MacroActionFactory::GetActionName;
const QString typeStr =
obs_module_text(idToName(segment->GetId()).c_str());
const QString description =
QString::fromStdString(segment->GetShortDesc());
QString description = QString::fromStdString(segment->GetShortDesc());
QString type;
if (isCondition) {
type = obs_module_text(MacroConditionFactory::GetConditionName(
segment->GetId())
.c_str());
} else {
type = obs_module_text(
MacroActionFactory::GetActionName(segment->GetId())
.c_str());
}
QString result = type;
QString result = typeStr;
if (!description.isEmpty()) {
result += ": " + description;
}
@@ -123,11 +134,25 @@ void MacroSegmentSelection::SetupDescription() const
return;
}
bool validIndex = false;
switch (_type) {
case Type::CONDITION:
validIndex = IsValidConditionIndex(_macro, index - 1);
break;
case Type::ACTION:
validIndex = IsValidActionIndex(_macro, index - 1);
break;
case Type::ELSE_ACTION:
validIndex = IsValidElseActionIndex(_macro, index - 1);
break;
default:
break;
}
QString description;
if (IsValidMacroSegmentIndex(_macro, index - 1,
_type == Type::CONDITION)) {
description = GetMacroSegmentDescription(
_macro, index - 1, _type == Type::CONDITION);
if (validIndex) {
description =
GetMacroSegmentDescription(_macro, index - 1, _type);
} else {
description = obs_module_text(
"AdvSceneSwitcher.macroSegmentSelection.invalid");
@@ -165,8 +190,23 @@ void MacroSegmentSelection::MarkSelectedSegment()
if (index.GetType() == IntVariable::Type::VARIABLE) {
return;
}
if (!IsValidMacroSegmentIndex(_macro, index - 1,
_type == Type::CONDITION)) {
bool validIndex = false;
switch (_type) {
case Type::CONDITION:
validIndex = IsValidConditionIndex(_macro, index - 1);
break;
case Type::ACTION:
validIndex = IsValidActionIndex(_macro, index - 1);
break;
case Type::ELSE_ACTION:
validIndex = IsValidElseActionIndex(_macro, index - 1);
break;
default:
break;
}
if (!validIndex) {
return;
}

View File

@@ -8,7 +8,7 @@ class Macro;
class MacroSegmentSelection : public QWidget {
Q_OBJECT
public:
enum class Type { CONDITION, ACTION };
enum class Type { CONDITION, ACTION, ELSE_ACTION };
MacroSegmentSelection(QWidget *parent, Type type,
bool allowVariables = true);

View File

@@ -14,7 +14,7 @@ MacroSelection::MacroSelection(QWidget *parent)
: FilterComboBox(parent,
obs_module_text("AdvSceneSwitcher.selectMacro"))
{
for (const auto &m : GetMacros()) {
for (const auto &m : GetTopLevelMacros()) {
if (m->IsGroup()) {
continue;
}

View File

@@ -150,7 +150,7 @@ static void addGroupSubitems(std::vector<std::shared_ptr<Macro>> &macros,
subitems.reserve(group->GroupSize());
// Find all subitems
auto allMacros = GetMacros();
auto allMacros = GetTopLevelMacros();
for (auto it = allMacros.begin(); it < allMacros.end(); it++) {
if ((*it)->Name() != group->Name()) {
continue;
@@ -410,13 +410,17 @@ void AdvSceneSwitcher::ImportMacros()
int groupSize = 0;
std::shared_ptr<Macro> group;
std::vector<std::shared_ptr<Macro>> importedMacros;
auto &tempMacros = GetTemporaryMacros();
auto lock = LockContext();
for (size_t i = 0; i < count; i++) {
tempMacros.clear();
OBSDataAutoRelease array_obj = obs_data_array_item(array, i);
auto macro = std::make_shared<Macro>();
tempMacros.emplace_back(macro);
macro->Load(array_obj);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
if (macroNameExists(macro->Name()) &&
!ResolveMacroImportNameConflict(macro)) {
@@ -425,7 +429,7 @@ void AdvSceneSwitcher::ImportMacros()
}
importedMacros.emplace_back(macro);
GetMacros().emplace_back(macro);
GetTopLevelMacros().emplace_back(macro);
if (groupSize > 0 && !macro->IsGroup()) {
Macro::PrepareMoveToGroup(group, macro);
groupSize--;
@@ -444,9 +448,10 @@ void AdvSceneSwitcher::ImportMacros()
for (const auto &macro : importedMacros) {
macro->PostLoad();
}
RunPostLoadSteps();
RunAndClearPostLoadSteps();
tempMacros.clear();
ui->macros->Reset(GetMacros(),
ui->macros->Reset(GetTopLevelMacros(),
GetGlobalMacroSettings()._highlightExecuted);
disableAddButtonHighlight();
}
@@ -633,7 +638,7 @@ void AdvSceneSwitcher::SetupMacroTab()
{
ui->macros->installEventFilter(this);
if (GetMacros().size() == 0 && !switcher->disableHints) {
if (GetTopLevelMacros().size() == 0 && !switcher->disableHints) {
addPulse = HighlightWidget(ui->macroAdd, QColor(Qt::green));
}
@@ -641,7 +646,7 @@ void AdvSceneSwitcher::SetupMacroTab()
{ui->macroUp, ui->macroDown}});
ui->macroControlLayout->addWidget(macroControls);
ui->macros->Reset(GetMacros(),
ui->macros->Reset(GetTopLevelMacros(),
GetGlobalMacroSettings()._highlightExecuted);
connect(ui->macros, SIGNAL(MacroSelectionChanged()), this,
SLOT(MacroSelectionChanged()));
@@ -752,7 +757,7 @@ void AdvSceneSwitcher::CopyMacro()
newMacro->Load(data);
newMacro->PostLoad();
newMacro->SetName(name);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
Macro::PrepareMoveToGroup(macro->Parent(), newMacro);
ui->macros->Add(newMacro, macro);

View File

@@ -1184,7 +1184,7 @@ bool MacroTree::SelectionEmpty() const
bool MacroTree::GroupsExist() const
{
for (const auto &macro : GetMacros()) {
for (const auto &macro : GetTopLevelMacros()) {
if (macro->IsGroup()) {
return true;
}
@@ -1306,7 +1306,7 @@ void MacroTree::UngroupSelectedGroups()
void MacroTree::ExpandAll()
{
for (const auto &macro : GetMacros()) {
for (const auto &macro : GetTopLevelMacros()) {
if (!macro->IsGroup()) {
continue;
}
@@ -1316,7 +1316,7 @@ void MacroTree::ExpandAll()
void MacroTree::CollapseAll()
{
for (const auto &macro : GetMacros()) {
for (const auto &macro : GetTopLevelMacros()) {
if (!macro->IsGroup()) {
continue;
}

View File

@@ -17,8 +17,6 @@
namespace advss {
static std::deque<std::shared_ptr<Macro>> macros;
Macro::Macro(const std::string &name) : _dockSettings(this)
{
SetName(name);
@@ -59,6 +57,7 @@ Macro::CreateGroup(const std::string &name,
void Macro::RemoveGroup(std::shared_ptr<Macro> group)
{
auto &macros = GetTopLevelMacros();
auto it = std::find(macros.begin(), macros.end(), group);
if (it == macros.end()) {
return;
@@ -75,6 +74,7 @@ void Macro::RemoveGroup(std::shared_ptr<Macro> group)
void Macro::PrepareMoveToGroup(Macro *group, std::shared_ptr<Macro> item)
{
auto &macros = GetTopLevelMacros();
for (const auto &m : macros) {
if (m.get() == group) {
PrepareMoveToGroup(m, item);
@@ -1114,7 +1114,7 @@ void Macro::SetHotkeysDesc() const
void SaveMacros(obs_data_t *obj)
{
obs_data_array_t *macroArray = obs_data_array_create();
for (const auto &m : macros) {
for (const auto &m : GetTopLevelMacros()) {
obs_data_t *array_obj = obs_data_create();
m->Save(array_obj);
@@ -1128,7 +1128,9 @@ void SaveMacros(obs_data_t *obj)
void LoadMacros(obs_data_t *obj)
{
auto &macros = GetTopLevelMacros();
macros.clear();
obs_data_array_t *macroArray = obs_data_get_array(obj, "macros");
size_t count = obs_data_array_count(macroArray);
@@ -1178,15 +1180,10 @@ void LoadMacros(obs_data_t *obj)
}
}
std::deque<std::shared_ptr<Macro>> &GetMacros()
{
return macros;
}
bool CheckMacros()
{
bool matchFound = false;
for (const auto &m : macros) {
for (const auto &m : GetTopLevelMacros()) {
if (!m->ConditionsShouldBeChecked()) {
vblog(LOG_INFO,
"skipping condition check for macro \"%s\" "
@@ -1213,7 +1210,7 @@ bool RunMacros()
// reordered while macros are currently being executed.
// For example, this can happen if a macro is performing a wait action,
// as the main lock will be unlocked during this time.
auto runPhaseMacros = macros;
auto runPhaseMacros = GetTopLevelMacros();
// Avoid deadlocks when opening settings window and calling frontend
// API functions at the same time.
@@ -1253,14 +1250,14 @@ bool RunMacros()
void StopAllMacros()
{
for (const auto &m : macros) {
for (const auto &m : GetAllMacros()) {
m->Stop();
}
}
Macro *GetMacroByName(const char *name)
{
for (const auto &m : macros) {
for (const auto &m : GetTopLevelMacros()) {
if (m->Name() == name) {
return m.get();
}
@@ -1276,7 +1273,7 @@ Macro *GetMacroByQString(const QString &name)
std::weak_ptr<Macro> GetWeakMacroByName(const char *name)
{
for (const auto &m : macros) {
for (const auto &m : GetTopLevelMacros()) {
if (m->Name() == name) {
return m;
}
@@ -1287,7 +1284,7 @@ std::weak_ptr<Macro> GetWeakMacroByName(const char *name)
void InvalidateMacroTempVarValues()
{
for (const auto &m : macros) {
for (const auto &m : GetTopLevelMacros()) {
// Do not invalidate the temp vars set during condition checks
// or action executions running in parallel to the "main" macro
// loop, as otherwise access to the information stored in those
@@ -1302,6 +1299,8 @@ void InvalidateMacroTempVarValues()
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval()
{
auto &macros = GetTopLevelMacros();
if (macros.empty()) {
return {};
}

View File

@@ -217,7 +217,6 @@ private:
void LoadMacros(obs_data_t *obj);
void SaveMacros(obs_data_t *obj);
std::deque<std::shared_ptr<Macro>> &GetMacros();
bool CheckMacros();
bool RunMacros();
void StopAllMacros();

View File

@@ -113,7 +113,7 @@ void ActionQueue::Add(const std::shared_ptr<MacroAction> &action)
action->Save(data);
copy->Load(data);
copy->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
copy->ResolveVariablesToFixedValues();
_actions.emplace_back(copy);
} else {
@@ -423,7 +423,7 @@ void ImportQueues(obs_data_t *data)
importedQueues->emplace_back(queue);
}
QeueUITask(signalImportedQueues, importedQueues);
QueueUITask(signalImportedQueues, importedQueues);
}
std::weak_ptr<ActionQueue> GetWeakActionQueueByName(const std::string &name)

View File

@@ -2,31 +2,31 @@
namespace advss {
AutoUpdateTooltipLabel::AutoUpdateTooltipLabel(
AutoUpdateHelpIcon::AutoUpdateHelpIcon(
QWidget *parent, const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs)
: QLabel(parent),
: HelpIcon("", parent),
_callback(updateTooltipCallback),
_timer(new QTimer(this)),
_updateIntervalMs(updateIntervalMs)
{
connect(_timer, &QTimer::timeout, this,
&AutoUpdateTooltipLabel::UpdateTooltip);
&AutoUpdateHelpIcon::UpdateTooltip);
}
void AutoUpdateTooltipLabel::enterEvent(QEnterEvent *event)
void AutoUpdateHelpIcon::enterEvent(QEnterEvent *event)
{
_timer->start(_updateIntervalMs);
QLabel::enterEvent(event);
}
void AutoUpdateTooltipLabel::leaveEvent(QEvent *event)
void AutoUpdateHelpIcon::leaveEvent(QEvent *event)
{
_timer->stop();
QLabel::leaveEvent(event);
}
void AutoUpdateTooltipLabel::UpdateTooltip()
void AutoUpdateHelpIcon::UpdateTooltip()
{
setToolTip(_callback());
}

View File

@@ -1,20 +1,19 @@
#pragma once
#include "export-symbol-helper.hpp"
#include "help-icon.hpp"
#include <functional>
#include <QLabel>
#include <QTimer>
namespace advss {
class ADVSS_EXPORT AutoUpdateTooltipLabel : public QLabel {
class ADVSS_EXPORT AutoUpdateHelpIcon : public HelpIcon {
Q_OBJECT
public:
AutoUpdateTooltipLabel(
QWidget *parent,
const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs = 300);
AutoUpdateHelpIcon(QWidget *parent,
const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs = 300);
protected:
void enterEvent(QEnterEvent *event) override;

View File

@@ -35,6 +35,11 @@ void FileSelection::SetPath(const QString &path)
_filePath->setText(path);
}
QString FileSelection::GetPath() const
{
return _filePath->text();
}
QString FileSelection::ValidPathOrDesktop(const QString &path)
{
QFileInfo fileInfo(path);

View File

@@ -22,6 +22,7 @@ public:
QWidget *parent = 0);
EXPORT void SetPath(const StringVariable &);
EXPORT void SetPath(const QString &);
EXPORT QString GetPath() const;
EXPORT QPushButton *Button() { return _browseButton; }
EXPORT static QString ValidPathOrDesktop(const QString &path);

View File

@@ -113,12 +113,13 @@ void RunLoadSteps(obs_data_t *obj)
}
}
void RunPostLoadSteps()
void RunAndClearPostLoadSteps()
{
std::lock_guard<std::mutex> lock(postLoadMutex);
for (const auto &func : getPostLoadSteps()) {
func();
}
getPostLoadSteps().clear();
}
void ClearPostLoadSteps()

View File

@@ -14,7 +14,7 @@ EXPORT void AddPostLoadStep(std::function<void()>);
EXPORT void AddIntervalResetStep(std::function<void()>);
void RunSaveSteps(obs_data_t *);
void RunLoadSteps(obs_data_t *);
EXPORT void RunPostLoadSteps();
EXPORT void RunAndClearPostLoadSteps();
void ClearPostLoadSteps();
EXPORT void AddPluginInitStep(std::function<void()>);

View File

@@ -8,7 +8,7 @@
namespace advss {
ResourceTable::ResourceTable(QTabWidget *parent, const QString &help,
ResourceTable::ResourceTable(QWidget *parent, const QString &help,
const QString &addToolTip,
const QString &removeToolTip,
const QStringList &headers,

View File

@@ -14,7 +14,7 @@ class ADVSS_EXPORT ResourceTable : public QWidget {
Q_OBJECT
public:
ResourceTable(QTabWidget *parent, const QString &help,
ResourceTable(QWidget *parent, const QString &help,
const QString &addToolTip, const QString &removeToolTip,
const QStringList &headers,
const std::function<void()> &openSettings);

View File

@@ -239,22 +239,26 @@ void SceneSelection::ResolveVariables()
_type = Type::SCENE;
}
SceneSelection SceneSelectionWidget::CurrentSelection()
static obs_weak_canvas_t *getWeakRefToMainCanvas()
{
SceneSelection s;
static auto mainCanvas = obs_get_main_canvas();
static auto mainCanvasWeak = obs_canvas_get_weak_canvas(mainCanvas);
static auto canvas = obs_get_main_canvas();
static auto weakCanvas = obs_canvas_get_weak_canvas(canvas);
[[maybe_unused]] static const bool _ = []() {
// Let's just hope we don't have to deal with selecting scenes
// when the OBS main canvas gets deleted and release the
// references here already to avoid reporting leaks on shutdown
obs_canvas_release(mainCanvas);
obs_weak_canvas_release(mainCanvasWeak);
obs_canvas_release(canvas);
obs_weak_canvas_release(weakCanvas);
return true;
}();
return weakCanvas;
}
s._canvas = _forceMainCanvas ? OBSWeakCanvas(mainCanvasWeak)
SceneSelection SceneSelectionWidget::CurrentSelection()
{
SceneSelection s;
s._canvas = _forceMainCanvas ? OBSWeakCanvas(getWeakRefToMainCanvas())
: _canvas->GetCanvas();
const int idx = _scenes->currentIndex();
@@ -330,6 +334,7 @@ static QStringList getCanvasScenesList(obs_weak_canvas_t *weakCanvas)
QStringList list;
obs_canvas_enum_scenes(canvas, enumCanvasScenes, &list);
list.sort();
return list;
}
@@ -342,6 +347,10 @@ void SceneSelectionWidget::Reset()
void SceneSelectionWidget::PopulateSceneSelection(obs_weak_canvas_t *canvas)
{
if (_forceMainCanvas) {
canvas = getWeakRefToMainCanvas();
}
_scenes->clear();
if ((_current || _previous)) {
const bool isMain = IsMainCanvas(canvas);
@@ -418,7 +427,7 @@ SceneSelectionWidget::SceneSelectionWidget(QWidget *parent, bool variables,
layout->setContentsMargins(0, 0, 0, 0);
if (GetCanvasCount() <= 1) {
_canvas->hide();
LockToMainCanvas();
}
Resize();

View File

@@ -46,7 +46,7 @@ public:
bool previous = false, bool current = false,
bool preview = false);
EXPORT void SetScene(const SceneSelection &);
void LockToMainCanvas();
EXPORT void LockToMainCanvas();
protected:
void showEvent(QShowEvent *event) override;

View File

@@ -199,7 +199,17 @@ std::chrono::high_resolution_clock::time_point GetLastSceneChangeTime()
OBSWeakSource GetCurrentScene()
{
return switcher->currentScene;
if (switcher->currentScene) {
return switcher->currentScene;
}
// If there wasn't any scene switch yet switcher->currentScene will be
// null and we must use obs_frontend_get_current_scene() instead
OBSSourceAutoRelease currentSceneSource =
obs_frontend_get_current_scene();
OBSWeakSourceAutoRelease currentSceneWeakSource =
obs_source_get_weak_source(currentSceneSource);
return currentSceneWeakSource.Get();
}
OBSWeakSource GetPreviousScene()

View File

@@ -190,29 +190,6 @@ static bool isMatchingNestedMacroAction(MacroAction *action, const Macro *macro)
return nestedMacroAction->_nestedMacro.get() == macro;
}
static void appendNestedMacros(std::deque<std::shared_ptr<Macro>> &macros,
Macro *macro)
{
if (!macro) {
return;
}
for (const auto &action : macro->Actions()) {
const auto nestedMacroAction =
dynamic_cast<MacroActionMacro *>(action.get());
if (nestedMacroAction) {
macros.push_back(nestedMacroAction->_nestedMacro);
}
}
for (const auto &action : macro->ElseActions()) {
const auto nestedMacroAction =
dynamic_cast<MacroActionMacro *>(action.get());
if (nestedMacroAction) {
macros.push_back(nestedMacroAction->_nestedMacro);
}
}
}
static Macro *getParentMacro(const Macro *targetMacro,
const std::vector<Macro *> &macros)
{
@@ -263,8 +240,10 @@ static Macro *getParentMacro(const Macro *targetMacro,
static Macro *getParentMacro(const Macro *macro)
{
const auto &topLevelMacros = GetTopLevelMacros();
std::vector<Macro *> macros;
for (const auto &macro : GetMacros()) {
macros.reserve(topLevelMacros.size());
for (const auto &macro : topLevelMacros) {
macros.emplace_back(macro.get());
}
return getParentMacro(macro, macros);
@@ -299,9 +278,18 @@ void TempVariableRef::Save(obs_data_t *obj, Macro *macro,
obs_data_set_obj(obj, name, data);
}
void TempVariableRef::Load(obs_data_t *obj, Macro *macro, const char *name)
void TempVariableRef::Load(obs_data_t *obj, Macro *macroPtr, const char *name)
{
if (!macro) {
const auto macros = GetAllMacros();
std::weak_ptr<Macro> macro;
for (const auto &macroShared : macros) {
if (macroShared.get() == macroPtr) {
macro = macroShared;
break;
}
}
if (macro.expired()) {
_segment.reset();
return;
}
@@ -324,8 +312,15 @@ void TempVariableRef::Load(obs_data_t *obj, Macro *macro, const char *name)
});
}
void TempVariableRef::PostLoad(int idx, SegmentType type, Macro *macro)
void TempVariableRef::PostLoad(int idx, SegmentType type,
const std::weak_ptr<Macro> &weakMacro)
{
auto childMacro = weakMacro.lock();
if (!childMacro) {
return;
}
auto macro = childMacro.get();
for (int i = 0; i < _depth; i++) {
macro = getParentMacro(macro);
}
@@ -404,9 +399,8 @@ TempVariableSelection::TempVariableSelection(QWidget *parent)
: QWidget(parent),
_selection(new FilterComboBox(
this, obs_module_text("AdvSceneSwitcher.tempVar.select"))),
_info(new AutoUpdateTooltipLabel(this, [this]() {
return SetupInfoLabel();
}))
_info(new AutoUpdateHelpIcon(this,
[this]() { return SetupInfoLabel(); }))
{
MacroEdit *edit = findMacroEditParent(parent);
@@ -416,12 +410,6 @@ TempVariableSelection::TempVariableSelection(QWidget *parent)
_macroEdits.push_back(edit);
}
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_info->setPixmap(pixmap);
_info->hide();
_selection->setSizeAdjustPolicy(QComboBox::AdjustToContents);
@@ -478,15 +466,9 @@ void TempVariableSelection::MacroSegmentsChanged()
SetVariable(currentSelection);
}
void TempVariableSelection::SegmentTempVarsChanged(MacroSegment *segment)
void TempVariableSelection::SegmentTempVarsChanged(MacroSegment *)
{
const auto currentSegment = GetSegment();
const auto currentMacro = currentSegment ? currentSegment->GetMacro()
: nullptr;
const auto changeMacro = segment ? segment->GetMacro() : nullptr;
if (currentMacro == changeMacro) {
MacroSegmentsChanged();
}
MacroSegmentsChanged();
}
void TempVariableSelection::HighlightChanged(int idx)

View File

@@ -72,10 +72,10 @@ private:
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
SegmentType GetType() const;
int GetIdx() const;
void PostLoad(int idx, SegmentType, Macro *);
void PostLoad(int idx, SegmentType, const std::weak_ptr<Macro> &);
std::string _id = "";
std::weak_ptr<MacroSegment> _segment;
std::weak_ptr<MacroSegment> _segment = {};
int _depth = 0;
friend TempVariable;
@@ -105,7 +105,7 @@ private:
MacroSegment *GetSegment() const;
FilterComboBox *_selection;
AutoUpdateTooltipLabel *_info;
AutoUpdateHelpIcon *_info;
std::vector<MacroEdit *> _macroEdits;
};

View File

@@ -178,7 +178,7 @@ std::string GetThemeTypeName()
#endif
}
void QeueUITask(void (*task)(void *param), void *param)
void QueueUITask(void (*task)(void *param), void *param)
{
obs_queue_task(OBS_TASK_UI, task, param, false);
}

View File

@@ -35,7 +35,7 @@ EXPORT void DisplayTrayMessage(const QString &title, const QString &msg,
EXPORT std::string GetThemeTypeName();
EXPORT QWidget *GetSettingsWindow();
void QeueUITask(void (*task)(void *param), void *param);
EXPORT void QueueUITask(void (*task)(void *param), void *param);
bool IsCursorInWidgetArea(QWidget *widget);

View File

@@ -8,6 +8,9 @@
#include "ui-helpers.hpp"
#include "variable.hpp"
#include <obs-frontend-api.h>
#include <QLayout>
#include <QTimer>
namespace advss {
@@ -16,14 +19,62 @@ static bool registerTab();
static void setupTab(QTabWidget *);
static bool registerTabDone = registerTab();
static VariableTable *tabWidget = nullptr;
static VariableTable *dockWidget = nullptr;
static bool addDock = false;
static VariableTable::Settings tabSettings;
static VariableTable::Settings dockSettings;
static void save(obs_data_t *data)
{
tabSettings.Save(data, "tabSettings");
dockSettings.Save(data, "dockSettings");
obs_data_set_bool(data, "addVariablesDock", addDock);
}
static void enableDock(bool enable)
{
if (OBSIsShuttingDown()) {
return;
}
obs_frontend_remove_dock("advss-variable-dock");
addDock = enable;
if (!addDock) {
return;
}
dockWidget = new VariableTable(dockSettings);
dockWidget->HideDockOptions();
if (obs_frontend_add_dock_by_id(
"advss-variable-dock",
obs_module_text("AdvSceneSwitcher.variableTab.title"),
dockWidget)) {
return;
}
blog(LOG_INFO, "failed to register variable dock!");
dockWidget->deleteLater();
dockWidget = nullptr;
}
static void load(obs_data_t *data)
{
tabSettings.Load(data, "tabSettings");
dockSettings.Load(data, "dockSettings");
enableDock(obs_data_get_bool(data, "addVariablesDock"));
}
static bool registerTab()
{
AddPluginInitStep([]() {
AddSetupTabCallback("variableTab", VariableTable::Create,
setupTab);
AddSetupTabCallback("variableTab",
VariableTable::CreateTabTable, setupTab);
});
AddSaveStep(save);
AddLoadStep(load);
return true;
}
@@ -34,10 +85,14 @@ static void setTabVisible(QTabWidget *tabWidget, bool visible)
obs_module_text("AdvSceneSwitcher.variableTab.title"));
}
VariableTable *VariableTable::Create()
VariableTable *VariableTable::CreateTabTable()
{
tabWidget = new VariableTable();
return tabWidget;
return new VariableTable(tabSettings);
}
void VariableTable::HideDockOptions() const
{
_addDock->hide();
}
void VariableTable::Add()
@@ -163,15 +218,14 @@ static void updateVariableStatus(QTableWidget *table)
}
}
static void openSettingsDialog()
static void openSettingsDialog(VariableTable *table)
{
auto selectedRows =
tabWidget->Table()->selectionModel()->selectedRows();
auto selectedRows = table->Table()->selectionModel()->selectedRows();
if (selectedRows.empty()) {
return;
}
auto cell = tabWidget->Table()->item(selectedRows.last().row(), 0);
auto cell = table->Table()->item(selectedRows.last().row(), 0);
if (!cell) {
return;
}
@@ -194,15 +248,14 @@ static void openSettingsDialog()
void VariableTable::Remove()
{
auto selectedRows =
tabWidget->Table()->selectionModel()->selectedRows();
auto selectedRows = Table()->selectionModel()->selectedRows();
if (selectedRows.empty()) {
return;
}
QStringList varNames;
for (const auto &row : selectedRows) {
auto cell = tabWidget->Table()->item(row.row(), 0);
auto cell = Table()->item(row.row(), 0);
if (!cell) {
continue;
}
@@ -235,7 +288,52 @@ void VariableTable::Remove()
}
}
VariableTable::VariableTable(QTabWidget *parent)
void VariableTable::Filter()
{
const auto itemMatches = [&](const QTableWidgetItem *item) {
if (!item) {
return false;
}
if (_settings.searchString.empty()) {
return true;
}
const auto text = item->text();
if (_settings.regex.Enabled()) {
return _settings.regex.Matches(text.toStdString(),
_settings.searchString);
}
return text.contains(
QString::fromStdString(_settings.searchString),
Qt::CaseInsensitive);
};
for (int row = 0; row < Table()->rowCount(); ++row) {
bool match = false;
if (_settings.searchType == Settings::SearchType::ALL) {
for (int col = 0; col < Table()->columnCount(); ++col) {
if (itemMatches(Table()->item(row, col))) {
match = true;
break;
}
}
} else {
if (itemMatches(Table()->item(
row,
static_cast<int>(_settings.searchType)))) {
match = true;
}
}
Table()->setRowHidden(row, !match);
}
}
VariableTable::VariableTable(Settings &settings, QWidget *parent)
: ResourceTable(
parent, obs_module_text("AdvSceneSwitcher.variableTab.help"),
obs_module_text(
@@ -253,13 +351,113 @@ VariableTable::VariableTable(QTabWidget *parent)
"AdvSceneSwitcher.variableTab.lastUsed.header")
<< obs_module_text(
"AdvSceneSwitcher.variableTab.lastChanged.header"),
openSettingsDialog)
[this]() { openSettingsDialog(this); }),
_searchField(new QLineEdit(this)),
_clear(new QPushButton(this)),
_searchType(new QComboBox(this)),
_regexWidget(new RegexConfigWidget(this)),
_addDock(new QCheckBox(
obs_module_text("AdvSceneSwitcher.variableTab.addDock"))),
_settings(settings)
{
_clear->setMaximumWidth(22);
SetButtonIcon(_clear, GetThemeTypeName() == "Light"
? "theme:Light/close.svg"
: "theme:Dark/close.svg");
_clear->setToolTip(
obs_module_text("AdvSceneSwitcher.variableTab.clear"));
_clear->setDisabled(_settings.searchString.empty());
connect(_clear, &QPushButton::clicked, this, [this]() {
_searchField->setText("");
_clear->setDisabled(true);
});
for (const auto &variable : GetVariables()) {
auto v = std::static_pointer_cast<Variable>(variable);
AddItemTableRow(Table(), getCellLabels(v.get()));
}
connect(Table(), &QTableWidget::itemChanged, this,
[this]() { Filter(); });
_searchField->setPlaceholderText(obs_module_text(
("AdvSceneSwitcher.variableTab.search.placeholder")));
_searchField->setText(QString::fromStdString(_settings.searchString));
connect(_searchField, &QLineEdit::textChanged, this,
[this](const QString &text) {
_settings.searchString = text.toStdString();
_clear->setDisabled(text.isEmpty());
Filter();
});
_searchType->addItem(
obs_module_text("AdvSceneSwitcher.variableTab.search.all"),
Settings::SearchType::ALL);
_searchType->addItem(
obs_module_text("AdvSceneSwitcher.variableTab.search.name"),
Settings::SearchType::NAME);
_searchType->addItem(
obs_module_text("AdvSceneSwitcher.variableTab.search.value"),
Settings::SearchType::VALUE);
_searchType->setCurrentIndex(
_searchType->findData(_settings.searchType));
connect(_searchType, &QComboBox::currentIndexChanged, this, [this]() {
_settings.searchType = static_cast<Settings::SearchType>(
_searchType->currentData().toInt());
});
_regexWidget->SetRegexConfig(_settings.regex);
connect(_regexWidget, &RegexConfigWidget::RegexConfigChanged, this,
[this](const RegexConfig &regex) {
_settings.regex = regex;
Filter();
});
_addDock->setChecked(addDock);
connect(_addDock, &QCheckBox::stateChanged, this,
[this](int checked) { enableDock(checked); });
QWidget::connect(VariableSignalManager::Instance(),
&VariableSignalManager::Rename, this,
[this](const QString &oldName,
const QString &newName) {
RenameItemTableRow(Table(), oldName, newName);
});
QWidget::connect(
VariableSignalManager::Instance(), &VariableSignalManager::Add,
this, [this](const QString &name) {
AddItemTableRow(
Table(),
getCellLabels(GetVariableByQString(name)));
SetHelpVisible(false);
HighlightAddButton(false);
});
QWidget::connect(VariableSignalManager::Instance(),
&VariableSignalManager::Remove, this,
[this](const QString &name) {
RemoveItemTableRow(Table(), name);
if (Table()->rowCount() == 0) {
SetHelpVisible(true);
HighlightAddButton(true);
}
});
auto timer = new QTimer(this);
timer->setInterval(1000);
QWidget::connect(timer, &QTimer::timeout,
[this]() { updateVariableStatus(Table()); });
timer->start();
auto searchLayout = new QHBoxLayout();
searchLayout->addWidget(_searchField);
searchLayout->addWidget(_clear);
searchLayout->addWidget(_searchType);
searchLayout->addWidget(_regexWidget);
searchLayout->addWidget(_addDock);
qobject_cast<QVBoxLayout *>(layout())->insertLayout(0, searchLayout);
SetHelpVisible(GetVariables().empty());
}
@@ -269,37 +467,27 @@ static void setupTab(QTabWidget *tab)
setTabVisible(tab, false);
}
QWidget::connect(VariableSignalManager::Instance(),
&VariableSignalManager::Rename, tab,
[](const QString &oldName, const QString &newName) {
RenameItemTableRow(tabWidget->Table(), oldName,
newName);
});
QWidget::connect(
VariableSignalManager::Instance(), &VariableSignalManager::Add,
tab, [tab](const QString &name) {
AddItemTableRow(
tabWidget->Table(),
getCellLabels(GetVariableByQString(name)));
tabWidget->SetHelpVisible(false);
tabWidget->HighlightAddButton(false);
setTabVisible(tab, true);
});
QWidget::connect(VariableSignalManager::Instance(),
&VariableSignalManager::Remove, tab,
[](const QString &name) {
RemoveItemTableRow(tabWidget->Table(), name);
if (tabWidget->Table()->rowCount() == 0) {
tabWidget->SetHelpVisible(true);
tabWidget->HighlightAddButton(true);
}
});
tab, [tab](const QString &name) { setTabVisible(tab, true); });
}
auto timer = new QTimer(tabWidget);
timer->setInterval(1000);
QWidget::connect(timer, &QTimer::timeout,
[]() { updateVariableStatus(tabWidget->Table()); });
timer->start();
void VariableTable::Settings::Save(obs_data_t *data, const char *name)
{
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_int(settings, "searchType", static_cast<int>(searchType));
obs_data_set_string(settings, "searchString", searchString.c_str());
regex.Save(settings);
obs_data_set_obj(data, name, settings);
}
void VariableTable::Settings::Load(obs_data_t *data, const char *name)
{
OBSDataAutoRelease settings = obs_data_get_obj(data, name);
searchType = static_cast<SearchType>(
obs_data_get_int(settings, "searchType"));
searchString = obs_data_get_string(settings, "searchString");
regex.Load(settings);
}
} // namespace advss

View File

@@ -1,5 +1,8 @@
#pragma once
#include "resource-table.hpp"
#include "regex-config.hpp"
#include <QComboBox>
namespace advss {
@@ -7,14 +10,34 @@ class VariableTable final : public ResourceTable {
Q_OBJECT
public:
static VariableTable *Create();
struct Settings {
void Save(obs_data_t *data, const char *name);
void Load(obs_data_t *data, const char *name);
enum SearchType { ALL = -1, NAME = 0, VALUE = 1 };
SearchType searchType = SearchType::ALL;
std::string searchString;
RegexConfig regex;
};
VariableTable(Settings &settings, QWidget *parent = nullptr);
static VariableTable *CreateTabTable();
void HideDockOptions() const;
private slots:
void Add();
void Remove();
void Filter();
private:
VariableTable(QTabWidget *parent = nullptr);
QLineEdit *_searchField;
QPushButton *_clear;
QComboBox *_searchType;
RegexConfigWidget *_regexWidget;
QCheckBox *_addDock;
Settings &_settings;
};
} // namespace advss

View File

@@ -456,7 +456,7 @@ void ImportVariables(obs_data_t *data)
obs_data_array_release(array);
QeueUITask(signalImportedVariables, importedVars);
QueueUITask(signalImportedVariables, importedVars);
}
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()

View File

@@ -601,10 +601,10 @@ static void populateHotkeyNames(QComboBox *list,
MacroActionHotkeyEdit::MacroActionHotkeyEdit(
QWidget *parent, std::shared_ptr<MacroActionHotkey> entryData)
: QWidget(parent),
_actionType(new QComboBox()),
_hotkeyType(new QComboBox()),
_obsHotkeys(new QComboBox()),
_keys(new QComboBox()),
_actionType(new FilterComboBox()),
_hotkeyType(new FilterComboBox()),
_obsHotkeys(new FilterComboBox()),
_keys(new FilterComboBox()),
_leftShift(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.hotkey.leftShift"))),
_rightShift(new QCheckBox(obs_module_text(

View File

@@ -85,11 +85,14 @@ private slots:
void DurationChanged(const Duration &);
void OnlySendToOBSChanged(int state);
protected:
QComboBox *_actionType;
QComboBox *_hotkeyType;
QComboBox *_obsHotkeys;
QComboBox *_keys;
private:
void RepopulateOBSHotkeySelection();
void SetWidgetVisibility();
FilterComboBox *_actionType;
FilterComboBox *_hotkeyType;
FilterComboBox *_obsHotkeys;
FilterComboBox *_keys;
QCheckBox *_leftShift;
QCheckBox *_rightShift;
QCheckBox *_leftCtrl;
@@ -101,15 +104,10 @@ protected:
DurationSelection *_duration;
QCheckBox *_onlySendToOBS;
QLabel *_noKeyPressSimulationWarning;
std::shared_ptr<MacroActionHotkey> _entryData;
private:
void RepopulateOBSHotkeySelection();
void SetWidgetVisibility();
QHBoxLayout *_entryLayout;
QHBoxLayout *_keyConfigLayout;
std::shared_ptr<MacroActionHotkey> _entryData;
bool _loading = true;
};

View File

@@ -250,6 +250,10 @@ MacroActionProjectorEdit::MacroActionProjectorEdit(
_regex(new RegexConfigWidget(this)),
_layout(new QHBoxLayout(this))
{
// The obs_frontend_open_projector() function does not seem to support
// scenes of secondary canvases
_scenes->LockToMainCanvas();
populateActionSelection(_actions);
populateWindowTypes(_windowTypes);
populateSelectionTypes(_types);
@@ -396,26 +400,29 @@ void MacroActionProjectorEdit::SetWidgetVisibility()
return;
}
_projectorWindowName->setVisible(_entryData->_action ==
const auto &action = _entryData->_action;
const auto &type = _entryData->_type;
_projectorWindowName->setVisible(action ==
MacroActionProjector::Action::CLOSE);
_regex->setVisible(_entryData->_action ==
MacroActionProjector::Action::CLOSE);
_types->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_windowTypes->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_scenes->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SCENE);
_sources->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SOURCE);
_monitors->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN &&
_regex->setVisible(action == MacroActionProjector::Action::CLOSE);
_types->setVisible(action == MacroActionProjector::Action::OPEN);
_windowTypes->setVisible(action == MacroActionProjector::Action::OPEN);
_scenes->setVisible(action == MacroActionProjector::Action::OPEN &&
type == MacroActionProjector::Type::SCENE);
_sources->setVisible(action == MacroActionProjector::Action::OPEN &&
type == MacroActionProjector::Type::SOURCE);
_monitors->setVisible(action == MacroActionProjector::Action::OPEN &&
_entryData->_fullscreen);
adjustSize();
updateGeometry();
if (action == MacroActionProjector::Action::CLOSE) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
}
}
} // namespace advss

View File

@@ -240,7 +240,7 @@ bool MacroActionSwitchScene::Load(obs_data_t *obj)
GetWeakCanvasByName(obs_data_get_string(obj, "canvas"));
} else {
OBSCanvasAutoRelease main = obs_get_main_canvas();
_canvas = OBSGetWeakRef(main);
_canvas = obs_canvas_get_weak_canvas(main);
}
_index.Save(obj, "index");

View File

@@ -7,6 +7,8 @@
#include <obs-frontend-api.h>
#include <QMainWindow>
namespace advss {
const std::string MacroActionSource::id = "source";
@@ -37,6 +39,12 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.source.type.openFilterDialog"},
{MacroActionSource::Action::OPEN_PROPERTIES_DIALOG,
"AdvSceneSwitcher.action.source.type.openPropertiesDialog"},
{MacroActionSource::Action::CLOSE_INTERACTION_DIALOG,
"AdvSceneSwitcher.action.source.type.closeInteractionDialog"},
{MacroActionSource::Action::CLOSE_FILTER_DIALOG,
"AdvSceneSwitcher.action.source.type.closeFilterDialog"},
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
"AdvSceneSwitcher.action.source.type.closePropertiesDialog"},
};
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
@@ -108,9 +116,81 @@ static bool isInteractable(obs_source_t *source)
return (flags & OBS_SOURCE_INTERACTION) != 0;
}
static void closeSourceDialog(obs_source_t *source, bool accept,
const char *className)
{
if (!source) {
return;
}
auto mainWindow =
reinterpret_cast<QMainWindow *>(obs_frontend_get_main_window());
if (!mainWindow) {
return;
}
for (const auto widget : mainWindow->children()) {
auto dialog = qobject_cast<QDialog *>(widget);
if (!dialog) {
continue;
}
if (dialog->metaObject()->className() != QString(className)) {
continue;
}
const char *name = obs_source_get_name(source);
if (!name) {
continue;
}
if (!dialog->windowTitle().contains(QString(name))) {
continue;
}
if (accept) {
dialog->accept();
return;
}
// dialog->reject() will ask for confirmation
// Try to avoid this by finding and pressing the cancel button
for (const auto widget : dialog->children()) {
auto buttons = qobject_cast<QDialogButtonBox *>(widget);
if (!buttons) {
continue;
}
for (auto *button : buttons->buttons()) {
if (buttons->buttonRole(button) ==
QDialogButtonBox::RejectRole) {
button->click();
break;
}
}
}
}
}
template<typename F> void QueueUITaskLambda(F &&func)
{
using FnType = std::decay_t<F>;
auto *heapFunc = new FnType(std::forward<F>(func));
QueueUITask(
[](void *param) {
std::unique_ptr<FnType> fn(
static_cast<FnType *>(param));
(*fn)();
},
heapFunc);
}
bool MacroActionSource::PerformAction()
{
auto s = obs_weak_source_get_source(_source.GetSource());
OBSSource s = obs_weak_source_get_source(_source.GetSource());
obs_source_release(s);
switch (_action) {
case Action::ENABLE:
obs_source_set_enabled(s, true);
@@ -172,10 +252,31 @@ bool MacroActionSource::PerformAction()
case Action::OPEN_PROPERTIES_DIALOG:
obs_frontend_open_source_properties(s);
break;
case Action::CLOSE_INTERACTION_DIALOG:
if (!isInteractable(s)) {
// Nothing to do
break;
}
QueueUITaskLambda([&]() {
closeSourceDialog(s, true, "OBSBasicInteraction");
});
break;
case Action::CLOSE_FILTER_DIALOG:
QueueUITaskLambda([&]() {
closeSourceDialog(s, _acceptDialog, "OBSBasicFilters");
});
break;
case Action::CLOSE_PROPERTIES_DIALOG:
QueueUITaskLambda([&]() {
closeSourceDialog(s, _acceptDialog,
"OBSBasicProperties");
});
break;
default:
break;
}
obs_source_release(s);
return true;
}
@@ -207,6 +308,7 @@ bool MacroActionSource::Save(obs_data_t *obj) const
static_cast<int>(_deinterlaceMode));
obs_data_set_int(obj, "deinterlaceOrder",
static_cast<int>(_deinterlaceOrder));
obs_data_set_bool(obj, "acceptDialog", _acceptDialog);
return true;
}
@@ -231,6 +333,7 @@ bool MacroActionSource::Load(obs_data_t *obj)
obs_data_get_int(obj, "deinterlaceMode"));
_deinterlaceOrder = static_cast<obs_deinterlace_field_order>(
obs_data_get_int(obj, "deinterlaceOrder"));
_acceptDialog = obs_data_get_bool(obj, "acceptDialog");
return true;
}
@@ -307,7 +410,9 @@ MacroActionSourceEdit::MacroActionSourceEdit(
_warning(new QLabel(
obs_module_text("AdvSceneSwitcher.action.source.warning"))),
_refreshSettingSelection(new QPushButton(
obs_module_text("AdvSceneSwitcher.action.source.refresh")))
obs_module_text("AdvSceneSwitcher.action.source.refresh"))),
_acceptDialog(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.source.dialog.accept")))
{
populateActionSelection(_actions);
@@ -347,6 +452,8 @@ MacroActionSourceEdit::MacroActionSourceEdit(
SLOT(SelectionChanged(const SourceSetting &)));
QWidget::connect(_refreshSettingSelection, SIGNAL(clicked()), this,
SLOT(RefreshVariableSourceSelectionValue()));
QWidget::connect(_acceptDialog, SIGNAL(stateChanged(int)), this,
SLOT(AcceptDialogChanged(int)));
auto entryLayout = new QHBoxLayout;
entryLayout->setContentsMargins(0, 0, 0, 0);
@@ -374,6 +481,7 @@ MacroActionSourceEdit::MacroActionSourceEdit(
mainLayout->addLayout(_settingsLayout);
mainLayout->addWidget(_warning);
mainLayout->addWidget(_settingsString);
mainLayout->addWidget(_acceptDialog);
auto buttonLayout = new QHBoxLayout;
buttonLayout->setContentsMargins(0, 0, 0, 0);
buttonLayout->addWidget(_getSettings);
@@ -406,6 +514,7 @@ void MacroActionSourceEdit::UpdateEntryData()
static_cast<int>(_entryData->_settingsInputMethod)));
_tempVars->SetVariable(_entryData->_tempVar);
_manualSettingValue->setPlainText(_entryData->_manualSettingValue);
_acceptDialog->setChecked(_entryData->_acceptDialog);
SetWidgetVisibility();
}
@@ -531,6 +640,12 @@ void MacroActionSourceEdit::RefreshVariableSourceSelectionValue()
_sourceSettings->SetSource(_entryData->_source.GetSource());
}
void MacroActionSourceEdit::AcceptDialogChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_acceptDialog = state;
}
static QString GetIndividualListEntryName()
{
static const auto matchesInput =
@@ -613,6 +728,12 @@ void MacroActionSourceEdit::SetWidgetVisibility()
_entryData->_source.GetType() ==
SourceSelection::Type::VARIABLE);
_acceptDialog->setVisible(
_entryData->_action ==
MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
_entryData->_action ==
MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
adjustSize();
updateGeometry();
}

View File

@@ -5,9 +5,10 @@
#include "source-selection.hpp"
#include "source-setting.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
namespace advss {
@@ -33,6 +34,7 @@ public:
OBS_DEINTERLACE_FIELD_ORDER_TOP;
TempVariableRef _tempVar;
SourceSetting _setting;
bool _acceptDialog = false;
enum class Action {
ENABLE,
@@ -45,6 +47,9 @@ public:
OPEN_INTERACTION_DIALOG,
OPEN_FILTER_DIALOG,
OPEN_PROPERTIES_DIALOG,
CLOSE_INTERACTION_DIALOG,
CLOSE_FILTER_DIALOG,
CLOSE_PROPERTIES_DIALOG,
};
Action _action = Action::SETTINGS;
@@ -91,6 +96,7 @@ private slots:
void SelectionChanged(const SourceSetting &);
void ManualSettingsValueChanged();
void RefreshVariableSourceSelectionValue();
void AcceptDialogChanged(int);
signals:
void HeaderInfoChanged(const QString &);
@@ -112,6 +118,7 @@ private:
QComboBox *_deinterlaceOrder;
QLabel *_warning;
QPushButton *_refreshSettingSelection;
QCheckBox *_acceptDialog;
std::shared_ptr<MacroActionSource> _entryData;
bool _loading = true;

View File

@@ -58,6 +58,10 @@ bool MacroConditionReplayBuffer::ReplayBufferWasSaved()
bool MacroConditionReplayBuffer::CheckCondition()
{
char *lastSavePath = obs_frontend_get_last_replay();
SetTempVarValue("lastSavePath", lastSavePath ? lastSavePath : "");
bfree(lastSavePath);
switch (_state) {
case Condition::STOP:
return !obs_frontend_replay_buffer_active();
@@ -85,6 +89,16 @@ bool MacroConditionReplayBuffer::Load(obs_data_t *obj)
return true;
}
void MacroConditionReplayBuffer::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar(
"lastSavePath",
obs_module_text("AdvSceneSwitcher.tempVar.replay.lastSavePath"),
obs_module_text(
"AdvSceneSwitcher.tempVar.replay.lastSavePath.description"));
}
static inline void populateStateSelection(QComboBox *list)
{
for (const auto &entry : conditions) {

View File

@@ -6,7 +6,7 @@
namespace advss {
class MacroConditionReplayBuffer : public MacroCondition {
class MacroConditionReplayBuffer final : public MacroCondition {
public:
MacroConditionReplayBuffer(Macro *m) : MacroCondition(m) {}
bool CheckCondition();
@@ -27,6 +27,7 @@ public:
Condition _state = Condition::STOP;
private:
void SetupTempVars();
bool ReplayBufferWasSaved();
bool _saveTimeInitialized = false;
@@ -35,7 +36,7 @@ private:
static const std::string id;
};
class MacroConditionReplayBufferEdit : public QWidget {
class MacroConditionReplayBufferEdit final : public QWidget {
Q_OBJECT
public:
@@ -55,11 +56,10 @@ public:
private slots:
void StateChanged(int value);
protected:
QComboBox *_state;
std::shared_ptr<MacroConditionReplayBuffer> _entryData;
private:
QComboBox *_state;
std::shared_ptr<MacroConditionReplayBuffer> _entryData;
bool _loading = true;
};

View File

@@ -252,6 +252,7 @@ MacroConditionSlideshowEdit::MacroConditionSlideshowEdit(
setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.slideshow.updateInterval.tooltip"));
_index->setMinimum(1);
_index->setMaximum(9999);
populateConditionSelection(_conditions);
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,

View File

@@ -242,7 +242,7 @@ MacroConditionStreamEdit::MacroConditionStreamEdit(
_keyFrameInterval(new VariableSpinBox()),
_streamKey(new VariableLineEdit(this)),
_serviceName(new VariableLineEdit(this)),
_currentService(new AutoUpdateTooltipLabel(
_currentService(new AutoUpdateHelpIcon(
this,
[]() {
QString formatString = obs_module_text(
@@ -255,13 +255,6 @@ MacroConditionStreamEdit::MacroConditionStreamEdit(
_keyFrameInterval->setMinimum(0);
_keyFrameInterval->setMaximum(25);
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_currentService->setPixmap(pixmap);
_streamKey->setEchoMode(QLineEdit::PasswordEchoOnEdit);
populateConditionSelection(_conditions);

View File

@@ -15,12 +15,15 @@ if(NOT TARGET httplib)
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
find_package(ZLIB)

View File

@@ -3,6 +3,13 @@ project(advanced-scene-switcher-mqtt)
# --- Check paho.mqtt.cpp requirements ---
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
find_package(PahoMqttCpp)
if(NOT PahoMqttCpp_FOUND)
message(WARNING "PahoMqttCpp not found!\n"

View File

@@ -1,4 +1,5 @@
#include "mqtt-helpers.hpp"
#include "help-icon.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
@@ -22,6 +23,10 @@ MqttConnection::MqttConnection(const MqttConnection &other)
_uri(other._uri),
_username(other._username),
_password(other._password),
_trustStore(other._trustStore),
_keyStore(other._keyStore),
_privateKey(other._privateKey),
_verifyServerCert(other._verifyServerCert),
_connectOnStart(other._connectOnStart),
_reconnect(other._reconnect),
_reconnectDelay(other._reconnectDelay)
@@ -30,12 +35,20 @@ MqttConnection::MqttConnection(const MqttConnection &other)
MqttConnection::MqttConnection(const std::string &name, const std::string &uri,
const std::string &username,
const std::string &password, bool connectOnStart,
const std::string &password,
const std::string &trustStore,
const std::string &keyStore,
const std::string &privateKey,
bool verifyServerCert, bool connectOnStart,
bool reconnect, int reconnectDelay)
: Item(name),
_uri(uri),
_username(username),
_password(password),
_trustStore(trustStore),
_keyStore(keyStore),
_privateKey(privateKey),
_verifyServerCert(verifyServerCert),
_connectOnStart(connectOnStart),
_reconnect(reconnect),
_reconnectDelay(reconnectDelay)
@@ -99,26 +112,41 @@ void MqttConnection::ConnectThread()
};
do {
std::unique_lock<std::mutex> clientLock(_clientMtx);
_client = std::make_shared<mqtt::async_client>(
_uri, std::string("advss_") + _name);
#ifdef ENABLE_MQTT5_SUPPORT
auto connOpts = mqtt::connect_options_builder::v5()
#else
auto connOpts = mqtt::connect_options_builder()
#endif
.clean_start(false)
.clean_session(true)
.connect_timeout(5s)
.user_name(_username)
.password(_password)
.finalize();
_client->set_connection_lost_handler(logConnectionLost);
_client->set_message_callback(dispatchMessage);
_client->start_consuming();
try {
std::unique_lock<std::mutex> clientLock(_clientMtx);
_client = std::make_shared<mqtt::async_client>(
_uri, std::string("advss_") + _name);
mqtt::ssl_options sslOptions;
if (!_trustStore.empty()) {
sslOptions.set_trust_store(_trustStore);
}
if (!_keyStore.empty()) {
sslOptions.set_key_store(_keyStore);
}
if (!_privateKey.empty()) {
sslOptions.set_private_key(_privateKey);
}
sslOptions.set_enable_server_cert_auth(
_verifyServerCert);
#ifdef ENABLE_MQTT5_SUPPORT
auto connOpts = mqtt::connect_options_builder::v5()
#else
auto connOpts = mqtt::connect_options_builder()
#endif
.clean_start(false)
.clean_session(true)
.connect_timeout(5s)
.user_name(_username)
.password(_password)
.ssl(sslOptions)
.finalize();
_client->set_connection_lost_handler(logConnectionLost);
_client->set_message_callback(dispatchMessage);
_client->start_consuming();
vblog(LOG_INFO, "connecting to MQTT server \"%s\" ...",
_name.c_str());
auto tok = _client->connect(connOpts);
@@ -220,6 +248,10 @@ void MqttConnection::Load(obs_data_t *data)
_uri = obs_data_get_string(data, "uri");
_username = obs_data_get_string(data, "username");
_password = obs_data_get_string(data, "password");
_trustStore = obs_data_get_string(data, "trustStore");
_keyStore = obs_data_get_string(data, "keyStore");
_privateKey = obs_data_get_string(data, "privateKey");
_verifyServerCert = obs_data_get_bool(data, "verifyServerCert");
_connectOnStart = obs_data_get_bool(data, "connectOnStart");
_reconnect = obs_data_get_bool(data, "reconnect");
_reconnectDelay = obs_data_get_int(data, "reconnectDelay");
@@ -251,6 +283,10 @@ void MqttConnection::Save(obs_data_t *data) const
obs_data_set_string(data, "uri", _uri.c_str());
obs_data_set_string(data, "username", _username.c_str());
obs_data_set_string(data, "password", _password.c_str());
obs_data_set_string(data, "trustStore", _trustStore.c_str());
obs_data_set_string(data, "keyStore", _keyStore.c_str());
obs_data_set_string(data, "privateKey", _privateKey.c_str());
obs_data_set_bool(data, "verifyServerCert", _verifyServerCert);
obs_data_set_bool(data, "connectOnStart", _connectOnStart);
obs_data_set_bool(data, "reconnect", _reconnect);
obs_data_set_int(data, "reconnectDelay", _reconnectDelay);
@@ -308,6 +344,10 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
_username(new QLineEdit()),
_password(new QLineEdit()),
_showPassword(new QPushButton()),
_trustStore(new FileSelection()),
_keyStore(new FileSelection()),
_privateKey(new FileSelection()),
_verifyServerCert(new QCheckBox()),
_topics(new MqttTopicListWidget(this)),
_connectOnStart(new QCheckBox()),
_reconnect(new QCheckBox()),
@@ -324,6 +364,10 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
_uri->setText(QString::fromStdString(connection._uri));
_username->setText(QString::fromStdString(connection._username));
_password->setText(QString::fromStdString(connection._password));
_trustStore->SetPath(QString::fromStdString(connection._trustStore));
_keyStore->SetPath(QString::fromStdString(connection._keyStore));
_privateKey->SetPath(QString::fromStdString(connection._privateKey));
_verifyServerCert->setChecked(connection._verifyServerCert);
_topics->SetValues(connection._topics, connection._qos);
_reconnectDelay->setMaximum(9999);
_reconnectDelay->setSuffix("s");
@@ -364,6 +408,41 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
passLayout->addWidget(_showPassword);
_layout->addLayout(passLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.trustStore")),
row, 0);
auto trustStoreLayout = new QHBoxLayout();
trustStoreLayout->addWidget(_trustStore);
trustStoreLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.trustStore.help")));
_layout->addLayout(trustStoreLayout, row, 1);
++row;
_layout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.keyStore")),
row, 0);
auto keyStoreLayout = new QHBoxLayout();
keyStoreLayout->addWidget(_keyStore);
keyStoreLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.keyStore.help")));
_layout->addLayout(keyStoreLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.privateKey")),
row, 0);
auto privateKeyLayout = new QHBoxLayout();
privateKeyLayout->addWidget(_privateKey);
privateKeyLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.privateKey.help")));
_layout->addLayout(privateKeyLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.verifyServerCert")),
row, 0);
_layout->addWidget(_verifyServerCert, row, 1);
++row;
_layout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.mqttConnection.topics")));
++row;
@@ -412,6 +491,10 @@ bool MqttConnectionSettingsDialog::AskForSettings(QWidget *parent,
connection._uri = dialog._uri->text().toStdString();
connection._username = dialog._username->text().toStdString();
connection._password = dialog._password->text().toStdString();
connection._trustStore = dialog._trustStore->GetPath().toStdString();
connection._keyStore = dialog._keyStore->GetPath().toStdString();
connection._privateKey = dialog._privateKey->GetPath().toStdString();
connection._verifyServerCert = dialog._verifyServerCert->isChecked();
connection._topics = dialog._topics->GetTopics();
connection._qos = dialog._topics->GetQoS();
connection._connectOnStart = dialog._connectOnStart->isChecked();
@@ -455,6 +538,10 @@ void MqttConnectionSettingsDialog::TestConnection()
connection->_uri = _uri->text().toStdString();
connection->_username = _username->text().toStdString();
connection->_password = _password->text().toStdString();
connection->_trustStore = _trustStore->GetPath().toStdString();
connection->_keyStore = _keyStore->GetPath().toStdString();
connection->_privateKey = _privateKey->GetPath().toStdString();
connection->_verifyServerCert = _verifyServerCert->isChecked();
connection->_topics = _topics->GetTopics();
connection->_qos = _topics->GetQoS();
connection->_connectOnStart = false;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "message-dispatcher.hpp"
#include "file-selection.hpp"
#include "item-selection-helpers.hpp"
#include "topic-selection.hpp"
@@ -27,6 +28,9 @@ public:
MqttConnection(const MqttConnection &other);
MqttConnection(const std::string &name, const std::string &uri,
const std::string &username, const std::string &password,
const std::string &trustStore,
const std::string &keyStore,
const std::string &privateKey, bool verifyServerCert,
bool connectOnStart, bool reconnect, int reconnectDelay);
static std::shared_ptr<Item> Create()
{
@@ -51,8 +55,12 @@ private:
std::string _uri = "mqtt://localhost:1883";
std::string _username = "user";
std::string _password = "password";
std::string _trustStore = "";
std::string _keyStore = "";
std::string _privateKey = "";
bool _verifyServerCert = false;
std::vector<std::string> _topics = {"/#"};
std::vector<std::string> _topics = {"/example"};
std::vector<int> _qos = {1};
std::thread _thread;
@@ -91,6 +99,10 @@ private:
QLineEdit *_username;
QLineEdit *_password;
QPushButton *_showPassword;
FileSelection *_trustStore;
FileSelection *_keyStore;
FileSelection *_privateKey;
QCheckBox *_verifyServerCert;
MqttTopicListWidget *_topics;
QCheckBox *_connectOnStart;
QCheckBox *_reconnect;

View File

@@ -15,13 +15,15 @@ if(NOT TARGET httplib)
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n"
"Twitch support will be disabled!\n\n")
return()
endif()
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "Twitch support will be disabled!\n\n")
return()
endif()
find_package(ZLIB)

View File

@@ -32,28 +32,17 @@ static const int reconnectDelay = 15;
#undef DispatchMessage
EventSub::EventSub() : QObject(nullptr)
EventSub::EventSub()
: QObject(nullptr),
_client(std::make_unique<EventSubWSClient>())
{
_client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
_client.init_asio();
#ifndef _WIN32
_client.set_reuse_addr(true);
#endif
SetupClient(*_client);
_client.set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client.set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client.set_close_handler(bind(&EventSub::OnClose, this, _1));
_client.set_fail_handler(bind(&EventSub::OnFail, this, _1));
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client->set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client->set_close_handler(bind(&EventSub::OnClose, this, _1));
_client->set_fail_handler(bind(&EventSub::OnFail, this, _1));
#ifndef USE_TWITCH_CLI_MOCK
_client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
#endif
_url = defaultURL.data();
RegisterInstance();
}
@@ -82,28 +71,34 @@ void EventSub::UnregisterInstance()
void EventSub::ConnectThread()
{
while (!_disconnect) {
std::unique_lock<std::mutex> lock(_waitMtx);
_client.reset();
_connected = true;
websocketpp::lib::error_code ec;
EventSubWSClient::connection_ptr con =
_client.get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch EventSub failed: %s",
ec.message().c_str());
} else {
_client.connect(con);
_connection = connection_hdl(con);
_client.run();
}
_client->reset();
_connected = true;
websocketpp::lib::error_code ec;
EventSubWSClient::connection_ptr con =
_client->get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch EventSub failed: %s",
ec.message().c_str());
} else {
_client->connect(con);
_connection = connection_hdl(con);
_client->run();
}
_connected = false;
}
void EventSub::WaitAndReconnect()
{
auto thread = std::thread([this]() {
std::unique_lock<std::mutex> lock(_waitMtx);
blog(LOG_INFO,
"Twitch EventSub trying to reconnect to in %d seconds.",
reconnectDelay);
_cv.wait_for(lock, std::chrono::seconds(reconnectDelay));
}
_connected = false;
Connect();
});
thread.detach();
}
void EventSub::Connect()
@@ -137,8 +132,8 @@ void EventSub::Disconnect()
std::lock_guard<std::mutex> lock(_connectMtx);
_disconnect = true;
websocketpp::lib::error_code ec;
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
_client->close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
{
std::unique_lock<std::mutex> waitLock(_waitMtx);
_cv.notify_all();
@@ -146,8 +141,8 @@ void EventSub::Disconnect()
while (_connected) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
_client->close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
}
if (_thread.joinable()) {
@@ -238,6 +233,24 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
return subscription.id;
}
void EventSub::SetupClient(EventSubWSClient &client)
{
client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
client.init_asio();
#ifndef _WIN32
client.set_reuse_addr(true);
#endif
#ifndef USE_TWITCH_CLI_MOCK
client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
#endif
}
void EventSub::OnOpen(connection_hdl)
{
vblog(LOG_INFO, "Twitch EventSub connection opened");
@@ -303,13 +316,14 @@ bool EventSub::IsValidID(const std::string &id)
return !_sessionID.empty() && id == _sessionID;
}
void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
std::optional<const EventSub::ParsedMessage>
EventSub::ParseWebSocketMessage(const EventSubWSClient::message_ptr &message)
{
if (!message) {
return;
return {};
}
if (message->get_opcode() != websocketpp::frame::opcode::text) {
return;
return {};
}
std::string payload = message->get_payload();
@@ -317,7 +331,7 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
if (!json) {
blog(LOG_ERROR, "invalid JSON payload received for '%s'",
payload.c_str());
return;
return {};
}
OBSDataAutoRelease metadata = obs_data_get_obj(json, "metadata");
@@ -325,31 +339,46 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
obs_data_get_string(metadata, "message_timestamp");
if (_validateTimestamps && !isValidTimestamp(timestamp)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid timestamp %s",
"discarding Twitch EventSub with invalid timestamp %s",
timestamp.c_str());
return;
return {};
}
std::string id = obs_data_get_string(metadata, "message_id");
if (!IsValidMessageID(id)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid message_id");
"discarding Twitch EventSub with invalid message_id");
return {};
}
ParsedMessage parsedMessage{obs_data_get_string(metadata,
"message_type"),
obs_data_get_obj(json, "payload")};
return parsedMessage;
}
void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
{
const auto msg = ParseWebSocketMessage(message);
if (!msg) {
return;
}
std::string messageType = obs_data_get_string(metadata, "message_type");
OBSDataAutoRelease payloadJson = obs_data_get_obj(json, "payload");
if (messageType == "session_welcome") {
HandleWelcome(payloadJson);
} else if (messageType == "session_keepalive") {
const auto &type = msg->type;
const auto &data = msg->payload;
if (type == "session_welcome") {
HandleWelcome(data);
} else if (type == "session_keepalive") {
HandleKeepAlive();
} else if (messageType == "notification") {
HandleNotification(payloadJson);
} else if (messageType == "session_reconnect") {
HandleReconnect(payloadJson);
} else if (messageType == "revocation") {
HandleRevocation(payloadJson);
} else if (type == "notification") {
HandleNotification(data);
} else if (type == "session_reconnect") {
HandleServerMigration(data);
} else if (type == "revocation") {
HandleRevocation(data);
} else {
vblog(LOG_INFO, "ignoring message of unknown type '%s'",
messageType.c_str());
vblog(LOG_INFO,
"Twitch EventSub ignoring message of unknown type '%s'",
type.c_str());
}
}
@@ -377,9 +406,96 @@ void EventSub::HandleNotification(obs_data_t *data)
_dispatcher.DispatchMessage(event);
}
void EventSub::HandleReconnect(obs_data_t *data)
void EventSub::OnServerMigrationWelcome(
connection_hdl newHdl, std::unique_ptr<EventSubWSClient> &newClient)
{
std::lock_guard<std::mutex> lock(_connectMtx);
// Disable reconnect handling for old connection which will be closed
_client->set_close_handler([](connection_hdl) {});
_client->set_fail_handler([](connection_hdl) {});
auto connection = _client->get_con_from_hdl(_connection);
connection->set_close_handler([](connection_hdl) {
vblog(LOG_INFO, "previous Twitch EventSub connection closed");
});
connection->set_fail_handler([](connection_hdl) {});
websocketpp::lib::error_code ec;
_client->close(_connection, websocketpp::close::status::normal,
"Switching to new connection", ec);
_client.swap(newClient);
_sessionID = _migrationSessionID;
_connection = newHdl;
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client->set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client->set_close_handler(bind(&EventSub::OnClose, this, _1));
_client->set_fail_handler(bind(&EventSub::OnFail, this, _1));
auto newConnection = _client->get_con_from_hdl(_connection);
newConnection->set_open_handler(bind(&EventSub::OnOpen, this, _1));
newConnection->set_message_handler(
bind(&EventSub::OnMessage, this, _1, _2));
newConnection->set_close_handler(bind(&EventSub::OnClose, this, _1));
newConnection->set_fail_handler(bind(&EventSub::OnFail, this, _1));
_connected = true;
_migrating = false;
}
void EventSub::StartServerMigrationClient(const std::string &url)
{
auto client = std::make_unique<EventSubWSClient>();
SetupClient(*client);
client->set_open_handler([this](connection_hdl hdl) {
vblog(LOG_INFO, "Twitch EventSub migration client opened");
});
client->set_message_handler([this,
&client](connection_hdl hdl,
EventSubWSClient::message_ptr
message) {
const auto msg = ParseWebSocketMessage(message);
if (!msg) {
return;
}
const auto &type = msg->type;
const auto &data = msg->payload;
if (type == "session_welcome") {
vblog(LOG_INFO,
"Twitch EventSub migration successful - switching to new connection");
OBSDataAutoRelease session =
obs_data_get_obj(data, "session");
_migrationSessionID =
obs_data_get_string(session, "id");
OnServerMigrationWelcome(hdl, client);
} else {
OnMessage(hdl, message);
}
});
websocketpp::lib::error_code ec;
auto con = client->get_connection(url, ec);
if (ec) {
blog(LOG_ERROR,
"Twitch EventSub migration connection failed: %s",
ec.message().c_str());
_migrating = false;
return;
}
_migrationConnection = con;
client->connect(con);
client->run();
}
void EventSub::HandleServerMigration(obs_data_t *data)
{
blog(LOG_INFO, "Twitch EventSub session_reconnect received");
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
auto id = obs_data_get_string(session, "id");
if (!IsValidID(id)) {
@@ -388,8 +504,25 @@ void EventSub::HandleReconnect(obs_data_t *data)
return;
}
// TODO:
// Implement proper reconnect handing to avoid dropped events
const std::string newURL =
obs_data_get_string(session, "reconnect_url");
if (newURL.empty()) {
blog(LOG_WARNING, "missing reconnect_url in session_reconnect");
return;
}
if (_migrating.exchange(true)) {
vblog(LOG_INFO,
"ignoring Twitch EventSub session_reconnect - already in progress");
return;
}
vblog(LOG_INFO, "Twitch EventSub reconnect: connecting to %s",
newURL.c_str());
std::thread([this, newURL]() {
StartServerMigrationClient(newURL);
}).detach();
}
void EventSub::HandleRevocation(obs_data_t *data)
@@ -425,23 +558,41 @@ void EventSub::HandleRevocation(obs_data_t *data)
void EventSub::OnClose(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
EventSubWSClient::connection_ptr con = _client->get_con_from_hdl(hdl);
const auto msg = con->get_ec().message();
const auto reason = con->get_remote_close_reason();
const auto code = con->get_remote_close_code();
blog(LOG_INFO, "Twitch EventSub connection closed: %s / %s (%d)",
msg.c_str(), reason.c_str(), code);
ClearActiveSubscriptions();
if (_migrating) {
ClearActiveSubscriptions();
}
_connected = false;
if (_disconnect) {
return;
}
WaitAndReconnect();
}
void EventSub::OnFail(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
EventSubWSClient::connection_ptr con = _client->get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch EventSub connection failed: %s", msg.c_str());
ClearActiveSubscriptions();
if (!_migrating) {
ClearActiveSubscriptions();
}
_connected = false;
if (_disconnect) {
return;
}
WaitAndReconnect();
}
bool Subscription::operator<(const Subscription &other) const

View File

@@ -19,10 +19,10 @@
namespace advss {
#ifdef USE_TWITCH_CLI_MOCK
typedef websocketpp::client<websocketpp::config::asio_client> EventSubWSClient;
using EventSubWSClient = websocketpp::client<websocketpp::config::asio_client>;
#else
typedef websocketpp::client<websocketpp::config::asio_tls_client>
EventSubWSClient;
using EventSubWSClient =
websocketpp::client<websocketpp::config::asio_tls_client>;
#endif
struct Event;
@@ -61,12 +61,16 @@ public:
void EnableTimestampValidation(bool enable);
private:
static void SetupClient(EventSubWSClient &);
void OnOpen(connection_hdl hdl);
void OnMessage(connection_hdl hdl,
EventSubWSClient::message_ptr message);
void OnClose(connection_hdl hdl);
void OnFail(connection_hdl hdl);
void ConnectThread();
void WaitAndReconnect();
bool IsValidMessageID(const std::string &);
bool IsValidID(const std::string &);
@@ -74,20 +78,39 @@ private:
void HandleWelcome(obs_data_t *);
void HandleKeepAlive() const;
void HandleNotification(obs_data_t *);
void HandleReconnect(obs_data_t *);
void HandleServerMigration(obs_data_t *);
void HandleRevocation(obs_data_t *);
void RegisterInstance();
void UnregisterInstance();
EventSubWSClient _client;
void StartServerMigrationClient(const std::string &url);
void OnServerMigrationWelcome(connection_hdl,
std::unique_ptr<EventSubWSClient> &);
struct ParsedMessage {
std::string type;
OBSDataAutoRelease payload;
};
std::optional<const ParsedMessage>
ParseWebSocketMessage(const EventSubWSClient::message_ptr &);
std::unique_ptr<EventSubWSClient> _client;
connection_hdl _connection;
std::unique_ptr<EventSubWSClient> _migrationClient;
connection_hdl _migrationConnection;
std::atomic_bool _migrating{false};
std::string _migrationSessionID;
std::thread _thread;
std::mutex _waitMtx;
std::mutex _connectMtx;
std::condition_variable _cv;
std::atomic_bool _connected{false};
std::atomic_bool _disconnect{false};
std::string _url;
std::string _sessionID;
bool _validateTimestamps = true;

View File

@@ -40,7 +40,7 @@ std::string GetThemeTypeName()
return "Dark";
}
void QeueUITask(void (*task)(void *param), void *) {}
void QueueUITask(void (*task)(void *param), void *) {}
QWidget *GetSettingsWindow()
{