From b7bf68615d098cea2ad3fcf78cd9d55a8dc3bef6 Mon Sep 17 00:00:00 2001 From: WarmUpTill <19472752+WarmUpTill@users.noreply.github.com> Date: Thu, 5 Nov 2026 09:38:55 +0100 Subject: [PATCH] Use stable names for macro pause hotkeys Otherwise external tools have a difficult time interacting with this particular set of hotkeys, as the id might change when changing scene collections. --- lib/macro/macro.cpp | 28 +++++++---- lib/macro/macro.hpp | 1 + tests/test-macro.cpp | 111 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/lib/macro/macro.cpp b/lib/macro/macro.cpp index 23bc5032..d642a5ea 100644 --- a/lib/macro/macro.cpp +++ b/lib/macro/macro.cpp @@ -10,6 +10,7 @@ #include "sync-helpers.hpp" #include +#include #include #include @@ -917,6 +918,9 @@ bool Macro::Save(obs_data_t *obj, bool saveForCopy) const "macroElseActionSplitterPosition"); obs_data_set_bool(obj, "registerHotkeys", _registerHotkeys); + if (!saveForCopy) { + obs_data_set_string(obj, "hotkeyId", _hotkeyId.c_str()); + } OBSDataArrayAutoRelease pauseHotkey = obs_hotkey_save(_pauseHotkey); obs_data_set_array(obj, "pauseHotkey", pauseHotkey); OBSDataArrayAutoRelease unpauseHotkey = obs_hotkey_save(_unpauseHotkey); @@ -1013,6 +1017,7 @@ bool Macro::Load(obs_data_t *obj) obs_data_set_default_bool(obj, "registerHotkeys", true); _registerHotkeys = !_isNested && obs_data_get_bool(obj, "registerHotkeys"); + _hotkeyId = obs_data_get_string(obj, "hotkeyId"); _pauseHotkeyData = obs_data_get_array(obj, "pauseHotkey"); _unpauseHotkeyData = obs_data_get_array(obj, "unpauseHotkey"); _togglePauseHotkeyData = obs_data_get_array(obj, "togglePauseHotkey"); @@ -1221,15 +1226,10 @@ static void togglePauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, } } -static int macroHotkeyID = 0; - -static obs_hotkey_id registerHotkeyHelper(const std::string prefix, +static obs_hotkey_id registerHotkeyHelper(const std::string &hotkeyName, const char *formatModuleText, Macro *macro, obs_hotkey_func func) { - macroHotkeyID++; - - std::string hotkeyName = prefix + std::to_string(macroHotkeyID); QString format{obs_module_text(formatModuleText)}; QString hotkeyDesc = format.arg(QString::fromStdString(macro->Name())); return obs_hotkey_register_frontend(hotkeyName.c_str(), @@ -1249,14 +1249,20 @@ void Macro::SetupHotkeys() ClearHotkeys(); } + if (_hotkeyId.empty()) { + char *uuid = os_generate_uuid(); + _hotkeyId = uuid; + bfree(uuid); + } + _pauseHotkey = registerHotkeyHelper( - "macro_pause_hotkey_", "AdvSceneSwitcher.hotkey.macro.pause", - this, pauseCB); + "macro_pause_hotkey_" + _hotkeyId, + "AdvSceneSwitcher.hotkey.macro.pause", this, pauseCB); _unpauseHotkey = registerHotkeyHelper( - "macro_unpause_hotkey_", + "macro_unpause_hotkey_" + _hotkeyId, "AdvSceneSwitcher.hotkey.macro.unpause", this, unpauseCB); _togglePauseHotkey = registerHotkeyHelper( - "macro_toggle_pause_hotkey_", + "macro_toggle_pause_hotkey_" + _hotkeyId, "AdvSceneSwitcher.hotkey.macro.togglePause", this, togglePauseCB); } @@ -1330,6 +1336,8 @@ std::vector> LoadImportedMacros( for (size_t i = 0; i < count; i++) { tempMacros.clear(); OBSDataAutoRelease obj = obs_data_array_item(array, i); + // The exported original might still exist in this collection + obs_data_erase(obj, "hotkeyId"); auto macro = std::make_shared(); tempMacros.emplace_back(macro); macro->Load(obj); diff --git a/lib/macro/macro.hpp b/lib/macro/macro.hpp index cd15f6dd..4b94224d 100644 --- a/lib/macro/macro.hpp +++ b/lib/macro/macro.hpp @@ -218,6 +218,7 @@ private: bool _paused = false; int _runCount = 0; bool _registerHotkeys = true; + std::string _hotkeyId; obs_hotkey_id _pauseHotkey = OBS_INVALID_HOTKEY_ID; obs_hotkey_id _unpauseHotkey = OBS_INVALID_HOTKEY_ID; obs_hotkey_id _togglePauseHotkey = OBS_INVALID_HOTKEY_ID; diff --git a/tests/test-macro.cpp b/tests/test-macro.cpp index 7bdecf7d..23867320 100644 --- a/tests/test-macro.cpp +++ b/tests/test-macro.cpp @@ -1,8 +1,11 @@ #include "catch.hpp" +#include +#include #include #include +#include namespace { @@ -414,3 +417,111 @@ TEST_CASE("GetActionTriggerMode returns the set mode", "[macro]") REQUIRE(m.GetActionTriggerMode() == advss::Macro::ActionTriggerMode::ANY_CONDITION_TRIGGERED); } + +// --------------------------------------------------------------------------- +// Hotkey ID persistence +// --------------------------------------------------------------------------- + +static std::string savedHotkeyId(const advss::Macro ¯o, + bool saveForCopy = false) +{ + OBSDataAutoRelease data = obs_data_create(); + macro.Save(data, saveForCopy); + return obs_data_get_string(data, "hotkeyId"); +} + +TEST_CASE("New macros with hotkeys enabled get a hotkey ID", "[macro]") +{ + advss::GlobalMacroSettings settings; + advss::Macro first("first", settings); + advss::Macro second("second", settings); + + REQUIRE_FALSE(savedHotkeyId(first).empty()); + REQUIRE(savedHotkeyId(first) != savedHotkeyId(second)); +} + +TEST_CASE("New macros with hotkeys disabled have no hotkey ID", "[macro]") +{ + advss::GlobalMacroSettings settings; + settings._newMacroRegisterHotkeys = false; + advss::Macro macro("macro", settings); + + REQUIRE(savedHotkeyId(macro).empty()); +} + +TEST_CASE("Hotkey ID is kept across save and load", "[macro]") +{ + advss::GlobalMacroSettings settings; + advss::Macro original("macro", settings); + + OBSDataAutoRelease data = obs_data_create(); + original.Save(data); + + advss::Macro loaded; + loaded.Load(data); + loaded.PostLoad(); + + REQUIRE(savedHotkeyId(loaded) == savedHotkeyId(original)); +} + +TEST_CASE("Hotkey ID is kept when hotkeys are toggled", "[macro]") +{ + advss::GlobalMacroSettings settings; + advss::Macro macro("macro", settings); + const auto id = savedHotkeyId(macro); + + macro.EnablePauseHotkeys(false); + macro.EnablePauseHotkeys(true); + + REQUIRE(savedHotkeyId(macro) == id); +} + +TEST_CASE("Macros saved without a hotkey ID get one on load", "[macro]") +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", "macro"); + + advss::Macro macro; + macro.Load(data); + macro.PostLoad(); + + REQUIRE_FALSE(savedHotkeyId(macro).empty()); +} + +TEST_CASE("Copied macros get a new hotkey ID", "[macro]") +{ + advss::GlobalMacroSettings settings; + advss::Macro original("macro", settings); + REQUIRE(savedHotkeyId(original, true).empty()); + + OBSDataAutoRelease data = obs_data_create(); + original.Save(data, true); + + advss::Macro copy("copy", settings); + copy.Load(data); + copy.PostLoad(); + + REQUIRE_FALSE(savedHotkeyId(copy).empty()); + REQUIRE(savedHotkeyId(copy) != savedHotkeyId(original)); +} + +TEST_CASE("Imported macros get a new hotkey ID", "[macro]") +{ + advss::GlobalMacroSettings settings; + advss::Macro original("macro", settings); + + OBSDataAutoRelease data = obs_data_create(); + original.Save(data); + OBSDataArrayAutoRelease array = obs_data_array_create(); + obs_data_array_push_back(array, data); + + const auto imported = advss::LoadImportedMacros( + array, + [](const std::shared_ptr &) { return true; }); + + REQUIRE(imported.size() == 1); + REQUIRE_FALSE(savedHotkeyId(*imported[0]).empty()); + REQUIRE(savedHotkeyId(*imported[0]) != savedHotkeyId(original)); + + advss::GetTopLevelMacros().clear(); +}