diff --git a/CMakeLists.txt b/CMakeLists.txt index b37fe52f..dde2a357 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,8 @@ target_sources( lib/macro/macro-action-variable-scene.hpp lib/macro/macro-action-variable-string.cpp lib/macro/macro-action-variable-string.hpp + lib/macro/macro-action-variable-time.cpp + lib/macro/macro-action-variable-time.hpp lib/macro/macro-action-variable.cpp lib/macro/macro-action-variable.hpp lib/macro/macro-action.cpp diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index eb8bc662..a712667e 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1382,6 +1382,15 @@ AdvSceneSwitcher.action.variable.type.mathExpression="Mathematical expression" AdvSceneSwitcher.action.variable.type.askForValue="Get user input" AdvSceneSwitcher.action.variable.type.environmentVariable="Set to environment variable value" AdvSceneSwitcher.action.variable.type.sceneItemCount="Set to scene item count of scene" +AdvSceneSwitcher.action.variable.type.currentScene="Set to current scene name" +AdvSceneSwitcher.action.variable.type.previousScene="Set to previous scene name" +AdvSceneSwitcher.action.variable.type.previewScene="Set to preview scene name" +AdvSceneSwitcher.action.variable.type.currentTransition="Set to current transition name" +AdvSceneSwitcher.action.variable.type.currentDateTime="Set to current date and time" +AdvSceneSwitcher.action.variable.type.dateTimeWithOffset="Set to date and time with offset" +AdvSceneSwitcher.action.variable.type.unixTimestamp="Set to Unix timestamp" +AdvSceneSwitcher.action.variable.type.timeSinceSceneChange="Set to time since last scene change" +AdvSceneSwitcher.action.variable.type.currentDateTime.help="Uses the Qt date format syntax.\nFor example:\n\n • yyyy-MM-dd -> 2026-10-19\n • dd.MM.yyyy hh:mm:ss -> 19.10.2026 14:05:09\n • dddd, MMMM d -> Monday, October 19\n\nText in single quotes is copied as is." AdvSceneSwitcher.action.variable.type.stringLength="Set to length of string" AdvSceneSwitcher.action.variable.type.extractJsonField="Extract JSON field with name" AdvSceneSwitcher.action.variable.type.queryJson="Query JSON" @@ -1389,6 +1398,9 @@ AdvSceneSwitcher.action.variable.type.queryJson.info="You can use \"JSONPath\" ( AdvSceneSwitcher.action.variable.type.queryJson.extractSingle="Extract value if single result" AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info="When enabled and the query returns exactly one result, the value is extracted from the result array and stored directly.\nIf the query matches zero or more than one element, the full JSON array is stored as usual." AdvSceneSwitcher.action.variable.type.accessJsonArray="Access JSON array at index" +AdvSceneSwitcher.action.variable.type.jsonArrayLength="Get length of JSON array" +AdvSceneSwitcher.action.variable.type.setJsonField="Set JSON field" +AdvSceneSwitcher.action.variable.type.formatNumber="Format number" AdvSceneSwitcher.action.variable.type.setToTempvar="Set to macro property" AdvSceneSwitcher.action.variable.type.setToTempvar.help="This action type will allow you to extract values out of segments of the current macro and assign those values to the selected variable.\nFor example, you can get the current scene name from a scene condition and assign this name to a variable." AdvSceneSwitcher.action.variable.type.sceneItemName="Set to scene item name at index" @@ -1425,6 +1437,7 @@ AdvSceneSwitcher.action.variable.layout.variable2="{{actions}}{{variables}}{{var AdvSceneSwitcher.action.variable.layout.changeCase="{{actions}}{{variables}}{{caseType}}" AdvSceneSwitcher.action.variable.layout.queryJson="{{actions}}{{variables}}{{jsonQuery}}{{jsonQueryHelp}}" AdvSceneSwitcher.action.variable.layout.accessJsonArray="{{actions}}{{variables}}{{jsonIndex}}" +AdvSceneSwitcher.action.variable.layout.jsonArrayLength="{{actions}}{{variables2}}and store it in{{variables}}" AdvSceneSwitcher.action.variable.layout.setToTempvar="{{actions}}{{variables}}{{tempVars}}{{tempVarsHelp}}" AdvSceneSwitcher.action.variable.layout.segmentValue="{{actions}}{{variables}}{{segmentIndex}}" AdvSceneSwitcher.action.variable.layout.mathExpression="{{actions}}{{variables}}{{mathExpression}}" @@ -1432,6 +1445,11 @@ AdvSceneSwitcher.action.variable.layout.numValue="{{actions}}{{variables}}{{numV AdvSceneSwitcher.action.variable.layout.environmentVariable="{{actions}}{{variables}}{{envVariableName}}" AdvSceneSwitcher.action.variable.layout.sceneItemName="{{actions}}{{variables}}{{scenes}}{{sceneItemIndex}}" AdvSceneSwitcher.action.variable.layout.sceneItemCount="{{actions}}{{variables}}{{scenes}}" +AdvSceneSwitcher.action.variable.layout.currentDateTime="{{actions}}{{variables}}using format{{dateFormat}}{{dateFormatHelp}}" +AdvSceneSwitcher.action.variable.layout.dateTimeWithOffset="{{actions}}{{variables}}shifted by{{dateOffset}}{{dateOffsetUnit}}using format{{dateFormat}}{{dateFormatHelp}}" +AdvSceneSwitcher.action.variable.layout.timeUnit="{{actions}}{{variables}}in{{timeUnit}}" +AdvSceneSwitcher.action.variable.layout.setJsonField="{{actions}}{{strValue}}of{{variables}}to{{jsonValue}}" +AdvSceneSwitcher.action.variable.layout.formatNumber="{{actions}}{{variables}}with{{decimalPlaces}}decimal places{{groupDigits}}use thousands separator" AdvSceneSwitcher.action.variable.layout.copy="{{actions}}{{variables2}}to{{variables}}" AdvSceneSwitcher.action.variable.layout.pad="{{actions}}of{{variables}}to length{{stringLength}}by adding{{paddingCharSelection}}to the{{direction}}" AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}of{{variables}}to length{{stringLength}}by removing characters from the{{direction}}" diff --git a/lib/macro/macro-action-variable-json.cpp b/lib/macro/macro-action-variable-json.cpp index 5900c33a..bf57b234 100644 --- a/lib/macro/macro-action-variable-json.cpp +++ b/lib/macro/macro-action-variable-json.cpp @@ -17,6 +17,12 @@ static bool registered = RegisterVariableActionGroup( {Action::ARRAY_JSON, "AdvSceneSwitcher.action.variable.type.accessJsonArray", "AdvSceneSwitcher.action.variable.layout.accessJsonArray"}, + {Action::JSON_ARRAY_LENGTH, + "AdvSceneSwitcher.action.variable.type.jsonArrayLength", + "AdvSceneSwitcher.action.variable.layout.jsonArrayLength"}, + {Action::SET_JSON_FIELD, + "AdvSceneSwitcher.action.variable.type.setJsonField", + "AdvSceneSwitcher.action.variable.layout.setJsonField", true}, }, []() { return std::make_unique(); }, VariableActionJsonEdit::Create}); @@ -33,6 +39,7 @@ void VariableActionJson::Save(obs_data_t *obj, _jsonIndex.Save(obj, "jsonIndex"); obs_data_set_bool(obj, "jsonQueryExtractSingle", _jsonQueryExtractSingle); + _jsonValue.Save(obj, "jsonValue"); } void VariableActionJson::Load(obs_data_t *obj, MacroActionVariable &) @@ -41,12 +48,16 @@ void VariableActionJson::Load(obs_data_t *obj, MacroActionVariable &) _jsonIndex.Load(obj, "jsonIndex"); _jsonQueryExtractSingle = obs_data_get_bool(obj, "jsonQueryExtractSingle"); + if (obs_data_has_user_value(obj, "jsonValue")) { + _jsonValue.Load(obj, "jsonValue"); + } } void VariableActionJson::ResolveVariables() { _jsonQuery.ResolveVariables(); _jsonIndex.ResolveVariables(); + _jsonValue.ResolveVariables(); } bool VariableActionJson::Perform(MacroActionVariable &action, @@ -89,6 +100,27 @@ bool VariableActionJson::Perform(MacroActionVariable &action, var.SetValue(*value); return true; } + case Action::JSON_ARRAY_LENGTH: { + auto input = action._variable2.lock(); + if (!input) { + return true; + } + auto length = GetJsonArrayLength(input->Value()); + if (!length.has_value()) { + return true; + } + var.SetValue(*length); + return true; + } + case Action::SET_JSON_FIELD: { + auto value = + SetJsonField(var.Value(), action._strValue, _jsonValue); + if (!value.has_value()) { + return true; + } + var.SetValue(*value); + return true; + } default: return true; } @@ -112,7 +144,8 @@ VariableActionJsonEdit::VariableActionJsonEdit( _jsonExtractSingleHelp(new HelpIcon( obs_module_text( "AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info"), - this)) + this)), + _jsonValue(new VariableTextEdit(this, 5, 1, 1)) { _jsonIndex->setMaximum(999); @@ -124,6 +157,8 @@ VariableActionJsonEdit::VariableActionJsonEdit( this, SLOT(JsonIndexChanged(const NumberVariable &))); QWidget::connect(_jsonExtractSingle, SIGNAL(stateChanged(int)), this, SLOT(JsonQueryExtractSingleChanged(int))); + QWidget::connect(_jsonValue, SIGNAL(textChanged()), this, + SLOT(JsonValueChanged())); auto layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); @@ -147,6 +182,7 @@ VariableActionJsonEdit::GetInlineWidgets() {"{{jsonQuery}}", _jsonQuery}, {"{{jsonQueryHelp}}", _jsonQueryHelp}, {"{{jsonIndex}}", _jsonIndex}, + {"{{jsonValue}}", _jsonValue}, }; } @@ -159,6 +195,7 @@ void VariableActionJsonEdit::UpdateEntryData() _jsonQuery->setText(_data->_jsonQuery); _jsonIndex->SetValue(_data->_jsonIndex); _jsonExtractSingle->setChecked(_data->_jsonQueryExtractSingle); + _jsonValue->setPlainText(_data->_jsonValue); _loading = false; } @@ -186,4 +223,11 @@ void VariableActionJsonEdit::JsonQueryExtractSingleChanged(int value) _data->_jsonQueryExtractSingle = value; } +void VariableActionJsonEdit::JsonValueChanged() +{ + GUARD_LOADING_AND_LOCK(); + _data->_jsonValue = _jsonValue->toPlainText().toStdString(); + UpdateSize(); +} + } // namespace advss diff --git a/lib/macro/macro-action-variable-json.hpp b/lib/macro/macro-action-variable-json.hpp index c4ee1d1b..344dedc3 100644 --- a/lib/macro/macro-action-variable-json.hpp +++ b/lib/macro/macro-action-variable-json.hpp @@ -18,6 +18,7 @@ public: StringVariable _jsonQuery = "$.some.nested.value"; IntVariable _jsonIndex = 0; bool _jsonQueryExtractSingle = true; + StringVariable _jsonValue = "value"; }; class VariableActionJsonEdit : public VariableActionGroupEdit { @@ -36,6 +37,7 @@ private slots: void JsonQueryChanged(); void JsonIndexChanged(const NumberVariable &); void JsonQueryExtractSingleChanged(int); + void JsonValueChanged(); private: VariableActionJson *_data; @@ -45,6 +47,7 @@ private: VariableSpinBox *_jsonIndex; QCheckBox *_jsonExtractSingle; HelpIcon *_jsonExtractSingleHelp; + VariableTextEdit *_jsonValue; }; } // namespace advss diff --git a/lib/macro/macro-action-variable-math.cpp b/lib/macro/macro-action-variable-math.cpp index 7e7e7705..141a2df4 100644 --- a/lib/macro/macro-action-variable-math.cpp +++ b/lib/macro/macro-action-variable-math.cpp @@ -3,6 +3,8 @@ #include "log-helper.hpp" #include "math-helpers.hpp" +#include + #include namespace advss { @@ -24,6 +26,9 @@ static bool registered = RegisterVariableActionGroup( "AdvSceneSwitcher.action.variable.layout.numValue"}, {Action::ROUND_TO_INT, "AdvSceneSwitcher.action.variable.type.roundToInt"}, + {Action::FORMAT_NUMBER, + "AdvSceneSwitcher.action.variable.type.formatNumber", + "AdvSceneSwitcher.action.variable.layout.formatNumber"}, {Action::RANDOM_NUMBER, "AdvSceneSwitcher.action.variable.type.randomNumber"}, {Action::RANDOM_LIST_VALUE, @@ -61,6 +66,8 @@ void VariableActionMath::Save(obs_data_t *obj, obs_data_set_bool(obj, "generateInteger", _generateInteger); _randomValues.Save(obj, "randomValues", "value"); obs_data_set_bool(obj, "allowRepeatValues", _allowRepeatValues); + _decimalPlaces.Save(obj, "decimalPlaces"); + obs_data_set_bool(obj, "groupDigits", _groupDigits); } void VariableActionMath::Load(obs_data_t *obj, MacroActionVariable &) @@ -80,11 +87,31 @@ void VariableActionMath::Load(obs_data_t *obj, MacroActionVariable &) _generateInteger = obs_data_get_bool(obj, "generateInteger"); _randomValues.Load(obj, "randomValues", "value"); _allowRepeatValues = obs_data_get_bool(obj, "allowRepeatValues"); + if (obs_data_has_user_value(obj, "decimalPlaces")) { + _decimalPlaces.Load(obj, "decimalPlaces"); + } + _groupDigits = obs_data_get_bool(obj, "groupDigits"); } void VariableActionMath::ResolveVariables() { _mathExpression.ResolveVariables(); + _decimalPlaces.ResolveVariables(); +} + +void VariableActionMath::FormatNumber(Variable &var) +{ + auto value = var.DoubleValue(); + if (!value.has_value()) { + return; + } + + // C locale to keep the output parsable by other variable actions + auto locale = QLocale::c(); + locale.setNumberOptions(_groupDigits ? QLocale::NumberOptions() + : QLocale::OmitGroupSeparator); + var.SetValue(locale.toString(*value, 'f', _decimalPlaces.GetValue()) + .toStdString()); } void VariableActionMath::HandleMathExpression(Variable &var) @@ -177,6 +204,9 @@ bool VariableActionMath::Perform(MacroActionVariable &, var.SetValue(std::to_string(int(std::round(*curValue)))); break; } + case Action::FORMAT_NUMBER: + FormatNumber(var); + break; case Action::RANDOM_NUMBER: GenerateRandomNumber(var); break; @@ -209,8 +239,12 @@ VariableActionMathEdit::VariableActionMathEdit( obs_module_text( "AdvSceneSwitcher.action.variable.type.allowRepeat"), this)), - _randomValueLayout(new QVBoxLayout()) + _randomValueLayout(new QVBoxLayout()), + _decimalPlaces(new VariableSpinBox(this)), + _groupDigits(new QCheckBox(this)) { + _decimalPlaces->setMinimum(0); + _decimalPlaces->setMaximum(15); _numValue->setMinimum(-9999999999); _numValue->setMaximum(9999999999); _randomNumberStart->setMinimum(-9999999999); @@ -242,6 +276,12 @@ VariableActionMathEdit::VariableActionMathEdit( SLOT(RandomValueListChanged(const StringList &))); QWidget::connect(_allowRepeatValues, SIGNAL(stateChanged(int)), this, SLOT(AllowRepeatValuesChanged(int))); + QWidget::connect( + _decimalPlaces, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(DecimalPlacesChanged(const NumberVariable &))); + QWidget::connect(_groupDigits, SIGNAL(stateChanged(int)), this, + SLOT(GroupDigitsChanged(int))); auto randomLayout = new QHBoxLayout(); PlaceWidgets( @@ -277,6 +317,8 @@ VariableActionMathEdit::GetInlineWidgets() return { {"{{numValue}}", _numValue}, {"{{mathExpression}}", _mathExpression}, + {"{{decimalPlaces}}", _decimalPlaces}, + {"{{groupDigits}}", _groupDigits}, }; } @@ -293,6 +335,8 @@ void VariableActionMathEdit::UpdateEntryData() _generateInteger->setChecked(_data->_generateInteger); _allowRepeatValues->setChecked(_data->_allowRepeatValues); _randomValues->SetStringList(_data->_randomValues); + _decimalPlaces->SetValue(_data->_decimalPlaces); + _groupDigits->setChecked(_data->_groupDigits); _loading = false; } @@ -363,4 +407,17 @@ void VariableActionMathEdit::AllowRepeatValuesChanged(int value) _data->_allowRepeatValues = value; } +void VariableActionMathEdit::DecimalPlacesChanged( + const NumberVariable &value) +{ + GUARD_LOADING_AND_LOCK(); + _data->_decimalPlaces = value; +} + +void VariableActionMathEdit::GroupDigitsChanged(int value) +{ + GUARD_LOADING_AND_LOCK(); + _data->_groupDigits = value; +} + } // namespace advss diff --git a/lib/macro/macro-action-variable-math.hpp b/lib/macro/macro-action-variable-math.hpp index 0df79cc2..5729d225 100644 --- a/lib/macro/macro-action-variable-math.hpp +++ b/lib/macro/macro-action-variable-math.hpp @@ -25,9 +25,12 @@ public: bool _generateInteger = true; StringList _randomValues = {"value1", "value2", "value3"}; bool _allowRepeatValues = true; + IntVariable _decimalPlaces = 2; + bool _groupDigits = false; private: void HandleMathExpression(Variable &); + void FormatNumber(Variable &); void GenerateRandomNumber(Variable &); void PickRandomValue(Variable &); @@ -54,6 +57,8 @@ private slots: void GenerateIntegerChanged(int); void RandomValueListChanged(const StringList &); void AllowRepeatValuesChanged(int); + void DecimalPlacesChanged(const NumberVariable &); + void GroupDigitsChanged(int); private: VariableActionMath *_data; @@ -68,6 +73,8 @@ private: StringListEdit *_randomValues; QCheckBox *_allowRepeatValues; QVBoxLayout *_randomValueLayout; + VariableSpinBox *_decimalPlaces; + QCheckBox *_groupDigits; }; } // namespace advss diff --git a/lib/macro/macro-action-variable-scene.cpp b/lib/macro/macro-action-variable-scene.cpp index f7a778cd..19f04ceb 100644 --- a/lib/macro/macro-action-variable-scene.cpp +++ b/lib/macro/macro-action-variable-scene.cpp @@ -1,6 +1,9 @@ #include "macro-action-variable-scene.hpp" +#include "scene-switch-helpers.hpp" #include "source-helpers.hpp" +#include + namespace advss { using Action = MacroActionVariable::Action; @@ -8,6 +11,14 @@ using Action = MacroActionVariable::Action; static bool registered = RegisterVariableActionGroup( {60, { + {Action::CURRENT_SCENE, + "AdvSceneSwitcher.action.variable.type.currentScene"}, + {Action::PREVIOUS_SCENE, + "AdvSceneSwitcher.action.variable.type.previousScene"}, + {Action::PREVIEW_SCENE, + "AdvSceneSwitcher.action.variable.type.previewScene"}, + {Action::CURRENT_TRANSITION, + "AdvSceneSwitcher.action.variable.type.currentTransition"}, {Action::SCENE_ITEM_NAME, "AdvSceneSwitcher.action.variable.type.sceneItemName", "AdvSceneSwitcher.action.variable.layout.sceneItemName"}, @@ -96,6 +107,25 @@ bool VariableActionScene::Perform(MacroActionVariable &, case Action::SCENE_ITEM_COUNT: var.SetValue(GetSceneItemCount(_scene.GetScene(false))); break; + case Action::CURRENT_SCENE: + var.SetValue(GetWeakSourceName(GetCurrentScene())); + break; + case Action::PREVIOUS_SCENE: + var.SetValue(GetWeakSourceName(GetPreviousScene())); + break; + case Action::PREVIEW_SCENE: { + // Only available in studio mode + OBSSourceAutoRelease scene = + obs_frontend_get_current_preview_scene(); + var.SetValue(scene ? obs_source_get_name(scene) : ""); + break; + } + case Action::CURRENT_TRANSITION: { + OBSSourceAutoRelease transition = + obs_frontend_get_current_transition(); + var.SetValue(transition ? obs_source_get_name(transition) : ""); + break; + } default: break; } diff --git a/lib/macro/macro-action-variable-time.cpp b/lib/macro/macro-action-variable-time.cpp new file mode 100644 index 00000000..3e82cd58 --- /dev/null +++ b/lib/macro/macro-action-variable-time.cpp @@ -0,0 +1,268 @@ +#include "macro-action-variable-time.hpp" +#include "scene-switch-helpers.hpp" + +#include + +namespace advss { + +using Action = MacroActionVariable::Action; + +static bool registered = RegisterVariableActionGroup( + {70, + { + {Action::CURRENT_DATE_TIME, + "AdvSceneSwitcher.action.variable.type.currentDateTime", + "AdvSceneSwitcher.action.variable.layout.currentDateTime", + true}, + {Action::DATE_TIME_WITH_OFFSET, + "AdvSceneSwitcher.action.variable.type.dateTimeWithOffset", + "AdvSceneSwitcher.action.variable.layout.dateTimeWithOffset", + true}, + {Action::UNIX_TIMESTAMP, + "AdvSceneSwitcher.action.variable.type.unixTimestamp", + "AdvSceneSwitcher.action.variable.layout.timeUnit"}, + {Action::TIME_SINCE_SCENE_CHANGE, + "AdvSceneSwitcher.action.variable.type.timeSinceSceneChange", + "AdvSceneSwitcher.action.variable.layout.timeUnit"}, + }, + []() { return std::make_unique(); }, + VariableActionTimeEdit::Create}); + +std::unique_ptr VariableActionTime::Copy() const +{ + return std::make_unique(*this); +} + +void VariableActionTime::Save(obs_data_t *obj, + const MacroActionVariable &) const +{ + _dateFormat.Save(obj, "dateFormat"); + obs_data_set_int(obj, "timeUnit", static_cast(_timeUnit)); + _dateOffset.Save(obj, "dateOffset"); + obs_data_set_int(obj, "dateOffsetUnit", + static_cast(_dateOffsetUnit)); +} + +void VariableActionTime::Load(obs_data_t *obj, MacroActionVariable &) +{ + if (obs_data_has_user_value(obj, "dateFormat")) { + _dateFormat.Load(obj, "dateFormat"); + } + _timeUnit = static_cast(obs_data_get_int(obj, "timeUnit")); + if (obs_data_has_user_value(obj, "dateOffset")) { + _dateOffset.Load(obj, "dateOffset"); + } + obs_data_set_default_int(obj, "dateOffsetUnit", + static_cast(OffsetUnit::DAYS)); + _dateOffsetUnit = static_cast( + obs_data_get_int(obj, "dateOffsetUnit")); +} + +void VariableActionTime::ResolveVariables() +{ + _dateFormat.ResolveVariables(); + _dateOffset.ResolveVariables(); +} + +void VariableActionTime::SetToTimeSinceSceneChange(Variable &var) const +{ + const auto lastChange = GetLastSceneChangeTime(); + // Default constructed until the first scene change was observed + if (lastChange == std::chrono::high_resolution_clock::time_point{}) { + return; + } + + const auto elapsed = + std::chrono::high_resolution_clock::now() - lastChange; + if (_timeUnit == TimeUnit::MILLISECONDS) { + var.SetValue(std::to_string( + std::chrono::duration_cast( + elapsed) + .count())); + return; + } + var.SetValue(std::to_string( + std::chrono::duration_cast(elapsed) + .count())); +} + +QDateTime VariableActionTime::GetDateTimeWithOffset() const +{ + const auto now = QDateTime::currentDateTime(); + const int offset = _dateOffset.GetValue(); + + switch (_dateOffsetUnit) { + case OffsetUnit::SECONDS: + return now.addSecs(offset); + case OffsetUnit::MINUTES: + return now.addSecs(static_cast(offset) * 60); + case OffsetUnit::HOURS: + return now.addSecs(static_cast(offset) * 60 * 60); + case OffsetUnit::DAYS: + return now.addDays(offset); + case OffsetUnit::WEEKS: + return now.addDays(static_cast(offset) * 7); + case OffsetUnit::MONTHS: + return now.addMonths(offset); + case OffsetUnit::YEARS: + return now.addYears(offset); + } + return now; +} + +bool VariableActionTime::Perform(MacroActionVariable &, + MacroActionVariable::Action type, + Variable &var) +{ + const auto format = QString::fromStdString(_dateFormat); + + switch (type) { + case Action::CURRENT_DATE_TIME: + var.SetValue(QDateTime::currentDateTime() + .toString(format) + .toStdString()); + break; + case Action::DATE_TIME_WITH_OFFSET: + var.SetValue( + GetDateTimeWithOffset().toString(format).toStdString()); + break; + case Action::UNIX_TIMESTAMP: { + const auto now = QDateTime::currentDateTime(); + const auto timestamp = _timeUnit == TimeUnit::MILLISECONDS + ? now.toMSecsSinceEpoch() + : now.toSecsSinceEpoch(); + var.SetValue(std::to_string(timestamp)); + break; + } + case Action::TIME_SINCE_SCENE_CHANGE: + SetToTimeSinceSceneChange(var); + break; + default: + break; + } + return true; +} + +static void populateTimeUnitSelection(QComboBox *list) +{ + list->addItem(obs_module_text("AdvSceneSwitcher.unit.seconds"), + static_cast(VariableActionTime::TimeUnit::SECONDS)); + list->addItem( + obs_module_text("AdvSceneSwitcher.unit.milliseconds"), + static_cast(VariableActionTime::TimeUnit::MILLISECONDS)); +} + +static void populateOffsetUnitSelection(QComboBox *list) +{ + using Unit = VariableActionTime::OffsetUnit; + const std::vector> units = { + {Unit::SECONDS, "AdvSceneSwitcher.unit.seconds"}, + {Unit::MINUTES, "AdvSceneSwitcher.unit.minutes"}, + {Unit::HOURS, "AdvSceneSwitcher.unit.hours"}, + {Unit::DAYS, "AdvSceneSwitcher.unit.days"}, + {Unit::WEEKS, "AdvSceneSwitcher.unit.weeks"}, + {Unit::MONTHS, "AdvSceneSwitcher.unit.months"}, + {Unit::YEARS, "AdvSceneSwitcher.unit.years"}, + }; + for (const auto &[unit, name] : units) { + list->addItem(obs_module_text(name), static_cast(unit)); + } +} + +VariableActionTimeEdit::VariableActionTimeEdit( + QWidget *parent, std::shared_ptr entryData) + : VariableActionGroupEdit(parent, entryData), + _data(entryData ? entryData->GetGroup() + : nullptr), + _dateFormat(new VariableLineEdit(this)), + _dateFormatHelp(new HelpIcon( + obs_module_text( + "AdvSceneSwitcher.action.variable.type.currentDateTime.help"), + this)), + _timeUnit(new QComboBox(this)), + _dateOffset(new VariableSpinBox(this)), + _dateOffsetUnit(new QComboBox(this)) +{ + _dateOffset->setMinimum(-99999); + _dateOffset->setMaximum(99999); + populateTimeUnitSelection(_timeUnit); + populateOffsetUnitSelection(_dateOffsetUnit); + + QWidget::connect(_dateFormat, SIGNAL(editingFinished()), this, + SLOT(DateFormatChanged())); + QWidget::connect(_timeUnit, SIGNAL(currentIndexChanged(int)), this, + SLOT(TimeUnitChanged(int))); + QWidget::connect( + _dateOffset, + SIGNAL(NumberVariableChanged(const NumberVariable &)), + this, SLOT(DateOffsetChanged(const NumberVariable &))); + QWidget::connect(_dateOffsetUnit, SIGNAL(currentIndexChanged(int)), + this, SLOT(DateOffsetUnitChanged(int))); +} + +VariableActionGroupEdit * +VariableActionTimeEdit::Create(QWidget *parent, + std::shared_ptr entryData) +{ + return new VariableActionTimeEdit(parent, entryData); +} + +std::unordered_map +VariableActionTimeEdit::GetInlineWidgets() +{ + return { + {"{{dateFormat}}", _dateFormat}, + {"{{dateFormatHelp}}", _dateFormatHelp}, + {"{{timeUnit}}", _timeUnit}, + {"{{dateOffset}}", _dateOffset}, + {"{{dateOffsetUnit}}", _dateOffsetUnit}, + }; +} + +void VariableActionTimeEdit::UpdateEntryData() +{ + if (!_data) { + return; + } + + _dateFormat->setText(_data->_dateFormat); + _timeUnit->setCurrentIndex( + _timeUnit->findData(static_cast(_data->_timeUnit))); + _dateOffset->SetValue(_data->_dateOffset); + _dateOffsetUnit->setCurrentIndex(_dateOffsetUnit->findData( + static_cast(_data->_dateOffsetUnit))); + _loading = false; +} + +void VariableActionTimeEdit::SetWidgetVisibility(MacroActionVariable::Action) +{ + hide(); +} + +void VariableActionTimeEdit::DateFormatChanged() +{ + GUARD_LOADING_AND_LOCK(); + _data->_dateFormat = _dateFormat->text().toStdString(); +} + +void VariableActionTimeEdit::TimeUnitChanged(int) +{ + GUARD_LOADING_AND_LOCK(); + _data->_timeUnit = static_cast( + _timeUnit->currentData().toInt()); +} + +void VariableActionTimeEdit::DateOffsetChanged(const NumberVariable &value) +{ + GUARD_LOADING_AND_LOCK(); + _data->_dateOffset = value; +} + +void VariableActionTimeEdit::DateOffsetUnitChanged(int) +{ + GUARD_LOADING_AND_LOCK(); + _data->_dateOffsetUnit = static_cast( + _dateOffsetUnit->currentData().toInt()); +} + +} // namespace advss diff --git a/lib/macro/macro-action-variable-time.hpp b/lib/macro/macro-action-variable-time.hpp new file mode 100644 index 00000000..87e36b7a --- /dev/null +++ b/lib/macro/macro-action-variable-time.hpp @@ -0,0 +1,69 @@ +#pragma once +#include "macro-action-variable-group.hpp" +#include "help-icon.hpp" +#include "variable-line-edit.hpp" +#include "variable-spinbox.hpp" + +#include + +namespace advss { + +class VariableActionTime : public VariableActionGroup { +public: + std::unique_ptr Copy() const override; + void Save(obs_data_t *, const MacroActionVariable &) const override; + void Load(obs_data_t *, MacroActionVariable &) override; + void ResolveVariables() override; + bool Perform(MacroActionVariable &, MacroActionVariable::Action, + Variable &) override; + + enum class TimeUnit { SECONDS, MILLISECONDS }; + enum class OffsetUnit { + SECONDS, + MINUTES, + HOURS, + DAYS, + WEEKS, + MONTHS, + YEARS, + }; + + StringVariable _dateFormat = "yyyy-MM-dd hh:mm:ss"; + TimeUnit _timeUnit = TimeUnit::SECONDS; + IntVariable _dateOffset = 1; + OffsetUnit _dateOffsetUnit = OffsetUnit::DAYS; + +private: + void SetToTimeSinceSceneChange(Variable &) const; + QDateTime GetDateTimeWithOffset() const; +}; + +class VariableActionTimeEdit : public VariableActionGroupEdit { + Q_OBJECT + +public: + VariableActionTimeEdit(QWidget *parent, + std::shared_ptr entryData); + static VariableActionGroupEdit * + Create(QWidget *parent, std::shared_ptr entryData); + std::unordered_map GetInlineWidgets() override; + void UpdateEntryData() override; + void SetWidgetVisibility(MacroActionVariable::Action) override; + +private slots: + void DateFormatChanged(); + void TimeUnitChanged(int); + void DateOffsetChanged(const NumberVariable &); + void DateOffsetUnitChanged(int); + +private: + VariableActionTime *_data; + + VariableLineEdit *_dateFormat; + HelpIcon *_dateFormatHelp; + QComboBox *_timeUnit; + VariableSpinBox *_dateOffset; + QComboBox *_dateOffsetUnit; +}; + +} // namespace advss diff --git a/lib/macro/macro-action-variable.hpp b/lib/macro/macro-action-variable.hpp index cccca0ad..f4345f0a 100644 --- a/lib/macro/macro-action-variable.hpp +++ b/lib/macro/macro-action-variable.hpp @@ -67,6 +67,17 @@ public: ARRAY_JSON, COPY_VAR, RANDOM_LIST_VALUE, + CURRENT_SCENE, + PREVIOUS_SCENE, + PREVIEW_SCENE, + CURRENT_TRANSITION, + CURRENT_DATE_TIME, + DATE_TIME_WITH_OFFSET, + UNIX_TIMESTAMP, + TIME_SINCE_SCENE_CHANGE, + JSON_ARRAY_LENGTH, + SET_JSON_FIELD, + FORMAT_NUMBER, }; Action _action = Action::SET_VALUE; diff --git a/lib/utils/json-helpers.cpp b/lib/utils/json-helpers.cpp index 03dbd45d..6dac6489 100644 --- a/lib/utils/json-helpers.cpp +++ b/lib/utils/json-helpers.cpp @@ -121,6 +121,44 @@ ExtractSingleJsonArrayElement(const std::string &jsonStr) return {}; } +std::optional GetJsonArrayLength(const std::string &jsonStr) +{ + try { + nlohmann::json json = nlohmann::json::parse(jsonStr); + if (!json.is_array()) { + return {}; + } + return static_cast(json.size()); + } catch (const nlohmann::json::exception &) { + return {}; + } +} + +std::optional SetJsonField(const std::string &jsonStr, + const std::string &id, + const std::string &value) +{ + try { + auto json = jsonStr.empty() ? nlohmann::json::object() + : nlohmann::json::parse(jsonStr); + if (!json.is_object()) { + return {}; + } + + // Store valid JSON values (numbers, objects, ...) as is and + // everything else as a plain string + auto parsedValue = nlohmann::json::parse(value, nullptr, false); + if (parsedValue.is_discarded()) { + json[id] = value; + } else { + json[id] = parsedValue; + } + return json.dump(); + } catch (const nlohmann::json::exception &) { + return {}; + } +} + static void collectStrings(const nlohmann::json &json, std::vector> &result, std::unordered_map &index) diff --git a/lib/utils/json-helpers.hpp b/lib/utils/json-helpers.hpp index c53251aa..c41fd45a 100644 --- a/lib/utils/json-helpers.hpp +++ b/lib/utils/json-helpers.hpp @@ -21,6 +21,10 @@ EXPORT std::optional AccessJsonArrayIndex(const std::string &json, const int index); EXPORT std::optional ExtractSingleJsonArrayElement(const std::string &json); +EXPORT std::optional GetJsonArrayLength(const std::string &json); +EXPORT std::optional SetJsonField(const std::string &json, + const std::string &id, + const std::string &value); EXPORT std::vector> CollectDistinctJsonStringValues(const std::string &json);