diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 4ef45682..2a510504 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -801,6 +801,7 @@ AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken" AdvSceneSwitcher.condition.mqtt="MQTT" AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:" AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}" +AdvSceneSwitcher.condition.script="Script" # Macro Actions AdvSceneSwitcher.action.unknown="Unknown action" @@ -1250,6 +1251,7 @@ AdvSceneSwitcher.action.obsSetting.action.setOutputCanvasX="Set output resolutio AdvSceneSwitcher.action.obsSetting.action.setOutputCanvasY="Set output resolution Y value" AdvSceneSwitcher.action.obsSetting.getCurrentValue="Get current value" AdvSceneSwitcher.action.obsSettings.layout="{{actions}}{{fpsType}}{{fpsIntValue}}{{fpsStringValue}}{{canvasSizeValue}}{{getCurrentValue}}" +AdvSceneSwitcher.action.script="Script" # Hotkey AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher" @@ -1514,6 +1516,17 @@ AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noPermission="Can't sele AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel="Can't select points reward without entering a channel first!" AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error="Can't select points reward because Twitch responded with an error! Check OBS logs for more details." +AdvSceneSwitcher.script.type.inline="Inline" +AdvSceneSwitcher.script.type.file="File" +AdvSceneSwitcher.script.type.layout="Script type:{{scriptType}}" +AdvSceneSwitcher.script.language.python="Python" +AdvSceneSwitcher.script.language.lua="LUA" +AdvSceneSwitcher.script.language.select="--select language--" +AdvSceneSwitcher.script.language.layout="Script language:{{language}}" +AdvSceneSwitcher.script.file.open="Open" +AdvSceneSwitcher.script.file.open.failed="Could not open script file!" +AdvSceneSwitcher.script.file.layout="Script file:{{path}}{{open}}" + AdvSceneSwitcher.tempVar.select="--select value--" AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:" diff --git a/plugins/scripting/CMakeLists.txt b/plugins/scripting/CMakeLists.txt index 4f0d8ef5..c9b01fa4 100644 --- a/plugins/scripting/CMakeLists.txt +++ b/plugins/scripting/CMakeLists.txt @@ -9,16 +9,28 @@ target_sources( ${PROJECT_NAME} PRIVATE macro-action-script.cpp macro-action-script.hpp + macro-action-script-inline.cpp + macro-action-script-inline.hpp macro-condition-script.cpp macro-condition-script.hpp + macro-condition-script-inline.cpp + macro-condition-script-inline.hpp macro-script-handler.cpp macro-script-handler.hpp macro-segment-script.cpp - macro-segment-script.hpp) + macro-segment-script.hpp + macro-segment-script-inline.cpp + macro-segment-script-inline.hpp) target_sources( - ${PROJECT_NAME} PRIVATE utils/properties-view.cpp utils/properties-view.hpp - utils/properties-view.moc.hpp) + ${PROJECT_NAME} + PRIVATE utils/inline-script.cpp + utils/inline-script.hpp + utils/obs-script-helpers.cpp + utils/obs-script-helpers.hpp + utils/properties-view.cpp + utils/properties-view.hpp + utils/properties-view.moc.hpp) target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/utils") diff --git a/plugins/scripting/macro-action-script-inline.cpp b/plugins/scripting/macro-action-script-inline.cpp new file mode 100644 index 00000000..d6393f8b --- /dev/null +++ b/plugins/scripting/macro-action-script-inline.cpp @@ -0,0 +1,66 @@ +#include "macro-action-script-inline.hpp" + +namespace advss { + +const std::string MacroActionScriptInline::_id = "script"; + +bool MacroActionScriptInline::_registered = MacroActionFactory::Register( + MacroActionScriptInline::_id, + {MacroActionScriptInline::Create, MacroActionScriptInlineEdit::Create, + "AdvSceneSwitcher.action.script"}); + +bool MacroActionScriptInline::PerformAction() +{ + return _script.Run(); +} + +void MacroActionScriptInline::LogAction() const +{ + ablog(LOG_INFO, "performing inline script action"); +} + +bool MacroActionScriptInline::Save(obs_data_t *obj) const +{ + MacroAction::Save(obj); + _script.Save(obj); + return true; +} + +bool MacroActionScriptInline::Load(obs_data_t *obj) +{ + MacroAction::Load(obj); + _script.Load(obj); + return true; +} + +std::shared_ptr MacroActionScriptInline::Create(Macro *m) +{ + return std::make_shared(m); +} + +std::shared_ptr MacroActionScriptInline::Copy() const +{ + return std::make_shared(*this); +} + +void MacroActionScriptInline::ResolveVariablesToFixedValues() +{ + _script.ResolveVariablesToFixedValues(); +} + +MacroActionScriptInlineEdit::MacroActionScriptInlineEdit( + QWidget *parent, std::shared_ptr entryData) + : MacroSegmentScriptInlineEdit(parent, entryData) +{ +} + +QWidget * +MacroActionScriptInlineEdit::Create(QWidget *parent, + std::shared_ptr action) +{ + return new MacroActionScriptInlineEdit( + parent, + std::dynamic_pointer_cast(action)); +} + +} // namespace advss diff --git a/plugins/scripting/macro-action-script-inline.hpp b/plugins/scripting/macro-action-script-inline.hpp new file mode 100644 index 00000000..2875b29a --- /dev/null +++ b/plugins/scripting/macro-action-script-inline.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "macro-action-edit.hpp" +#include "macro-segment-script-inline.hpp" + +namespace advss { + +class MacroActionScriptInline : public MacroAction, + public MacroSegmentScriptInline { +public: + MacroActionScriptInline(Macro *m) : MacroAction(m) {} + + bool PerformAction(); + void LogAction() const; + + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + + std::string GetId() const { return _id; }; + + static std::shared_ptr Create(Macro *m); + std::shared_ptr Copy() const; + + void ResolveVariablesToFixedValues(); + +private: + static bool _registered; + static const std::string _id; +}; + +class MacroActionScriptInlineEdit : public MacroSegmentScriptInlineEdit { + Q_OBJECT + +public: + MacroActionScriptInlineEdit( + QWidget *, std::shared_ptr = nullptr); + static QWidget *Create(QWidget *, std::shared_ptr); +}; + +} // namespace advss diff --git a/plugins/scripting/macro-condition-script-inline.cpp b/plugins/scripting/macro-condition-script-inline.cpp new file mode 100644 index 00000000..eed17708 --- /dev/null +++ b/plugins/scripting/macro-condition-script-inline.cpp @@ -0,0 +1,51 @@ +#include "macro-condition-script-inline.hpp" + +namespace advss { + +const std::string MacroConditionScriptInline::_id = "script"; + +bool MacroConditionScriptInline::_registered = + MacroConditionFactory::Register(MacroConditionScriptInline::_id, + {MacroConditionScriptInline::Create, + MacroConditionScriptInlineEdit::Create, + "AdvSceneSwitcher.condition.script"}); + +bool MacroConditionScriptInline::CheckCondition() +{ + return _script.Run(); +} + +bool MacroConditionScriptInline::Save(obs_data_t *obj) const +{ + MacroCondition::Save(obj); + _script.Save(obj); + return true; +} + +bool MacroConditionScriptInline::Load(obs_data_t *obj) +{ + MacroCondition::Load(obj); + _script.Load(obj); + return true; +} + +std::shared_ptr MacroConditionScriptInline::Create(Macro *m) +{ + return std::make_shared(m); +} + +MacroConditionScriptInlineEdit::MacroConditionScriptInlineEdit( + QWidget *parent, std::shared_ptr entryData) + : MacroSegmentScriptInlineEdit(parent, entryData) +{ +} + +QWidget *MacroConditionScriptInlineEdit::Create( + QWidget *parent, std::shared_ptr condition) +{ + return new MacroConditionScriptInlineEdit( + parent, std::dynamic_pointer_cast( + condition)); +} + +} // namespace advss diff --git a/plugins/scripting/macro-condition-script-inline.hpp b/plugins/scripting/macro-condition-script-inline.hpp new file mode 100644 index 00000000..714a1677 --- /dev/null +++ b/plugins/scripting/macro-condition-script-inline.hpp @@ -0,0 +1,36 @@ +#pragma once +#include "macro-condition-edit.hpp" +#include "macro-segment-script-inline.hpp" + +namespace advss { + +class MacroConditionScriptInline : public MacroCondition, + public MacroSegmentScriptInline { +public: + MacroConditionScriptInline(Macro *m) : MacroCondition(m) {} + + bool CheckCondition(); + + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + + std::string GetId() const { return _id; }; + + static std::shared_ptr Create(Macro *m); + +private: + static bool _registered; + static const std::string _id; +}; + +class MacroConditionScriptInlineEdit : public MacroSegmentScriptInlineEdit { + Q_OBJECT + +public: + MacroConditionScriptInlineEdit( + QWidget *, + std::shared_ptr = nullptr); + static QWidget *Create(QWidget *, std::shared_ptr); +}; + +} // namespace advss diff --git a/plugins/scripting/macro-segment-script-inline.cpp b/plugins/scripting/macro-segment-script-inline.cpp new file mode 100644 index 00000000..69b8385b --- /dev/null +++ b/plugins/scripting/macro-segment-script-inline.cpp @@ -0,0 +1,198 @@ +#include "macro-segment-script-inline.hpp" +#include "layout-helpers.hpp" +#include "obs-module-helper.hpp" +#include "sync-helpers.hpp" +#include "ui-helpers.hpp" + +#include + +namespace advss { + +void MacroSegmentScriptInline::SetType(InlineScript::Type type) +{ + _script.SetType(type); +} + +void MacroSegmentScriptInline::SetLanguage(obs_script_lang language) +{ + _script.SetLanguage(language); +} + +void MacroSegmentScriptInline::SetScript(const std::string &text) +{ + _script.SetText(text); +} + +void MacroSegmentScriptInline::SetPath(const std::string &path) +{ + _script.SetPath(path); +} + +static void populateLanguageSelection(QComboBox *list) +{ + list->addItem( + obs_module_text("AdvSceneSwitcher.script.language.python"), + OBS_SCRIPT_LANG_PYTHON); + list->addItem(obs_module_text("AdvSceneSwitcher.script.language.lua"), + OBS_SCRIPT_LANG_LUA); + list->setPlaceholderText( + obs_module_text("AdvSceneSwitcher.script.language.select")); +} + +static void populateScriptTypeSelection(QComboBox *list) +{ + list->addItem(obs_module_text("AdvSceneSwitcher.script.type.inline"), + InlineScript::INLINE); + list->addItem(obs_module_text("AdvSceneSwitcher.script.type.file"), + InlineScript::FILE); +} + +MacroSegmentScriptInlineEdit::MacroSegmentScriptInlineEdit( + QWidget *parent, std::shared_ptr entryData) + : QWidget(parent), + _scriptType(new QComboBox(this)), + _language(new QComboBox(this)), + _script(new ScriptEditor(this)), + _path(new FileSelection(FileSelection::Type::WRITE, this)), + _openFile(new QPushButton( + obs_module_text("AdvSceneSwitcher.script.file.open"), this)), + _fileLayout(new QHBoxLayout()), + _entryData(entryData) +{ + SetupLayout(); + SetupWidgetConnections(); + PopulateWidgets(); + SetWidgetVisibility(); + _loading = false; +} + +void MacroSegmentScriptInlineEdit::ScriptTypeChanged(int idx) +{ + GUARD_LOADING_AND_LOCK(); + _entryData->SetType(static_cast( + _scriptType->itemData(idx).toInt())); + SetWidgetVisibility(); +} + +void MacroSegmentScriptInlineEdit::LanguageChanged(int idx) +{ + { + GUARD_LOADING_AND_LOCK(); + _entryData->SetLanguage(static_cast( + _language->itemData(idx).toInt())); + const QSignalBlocker b(_script); + _script->setPlainText(_entryData->GetScript()); + } + + if (_entryData->GetType() == InlineScript::Type::FILE) { + PathChanged(QString::fromStdString(_entryData->GetPath())); + } +} + +void MacroSegmentScriptInlineEdit::ScriptChanged() +{ + GUARD_LOADING_AND_LOCK(); + _entryData->SetScript(_script->toPlainText().toStdString()); + adjustSize(); + updateGeometry(); +} + +void MacroSegmentScriptInlineEdit::PathChanged(const QString &path) +{ + GUARD_LOADING_AND_LOCK(); + + if (path.isEmpty()) { + _entryData->SetPath(path.toStdString()); + return; + } + + // Script language will be detected by OBS based on file extension so + // adjust it if necessary + QString pathAdjusted = path; + if (_entryData->GetLanguage() == OBS_SCRIPT_LANG_PYTHON && + !path.endsWith(".py")) { + if (path.endsWith(".lua")) { + pathAdjusted.chop(4); + } + pathAdjusted += ".py"; + } + if (_entryData->GetLanguage() == OBS_SCRIPT_LANG_LUA && + !path.endsWith(".lua")) { + if (path.endsWith(".py")) { + pathAdjusted.chop(3); + } + pathAdjusted += ".lua"; + } + + const QSignalBlocker b(_path); + _path->SetPath(pathAdjusted); + _entryData->SetPath(pathAdjusted.toStdString()); +} + +void MacroSegmentScriptInlineEdit::PopulateWidgets() +{ + populateLanguageSelection(_language); + populateScriptTypeSelection(_scriptType); + + if (!_entryData) { + return; + } + + _scriptType->setCurrentIndex( + _scriptType->findData(_entryData->GetType())); + _language->setCurrentIndex( + _language->findData(_entryData->GetLanguage())); + _script->setPlainText(_entryData->GetScript()); + _path->SetPath(_entryData->GetPath()); +} + +void MacroSegmentScriptInlineEdit::SetupWidgetConnections() +{ + QWidget::connect(_scriptType, SIGNAL(currentIndexChanged(int)), this, + SLOT(ScriptTypeChanged(int))); + QWidget::connect(_language, SIGNAL(currentIndexChanged(int)), this, + SLOT(LanguageChanged(int))); + QWidget::connect(_script, SIGNAL(ScriptChanged()), this, + SLOT(ScriptChanged())); + QWidget::connect(_path, SIGNAL(PathChanged(const QString &)), this, + SLOT(PathChanged(const QString &))); + QWidget::connect(_openFile, &QPushButton::clicked, this, [this] { + QUrl fileUrl = QUrl::fromLocalFile( + QString::fromStdString(_entryData->GetPath())); + if (!QDesktopServices::openUrl(fileUrl)) { + DisplayMessage(obs_module_text( + "AdvSceneSwitcher.script.file.open.failed")); + } + }); +} + +void MacroSegmentScriptInlineEdit::SetupLayout() +{ + auto languageLayout = new QHBoxLayout(); + PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.language.layout"), + languageLayout, {{"{{language}}", _language}}); + auto typeLayout = new QHBoxLayout(); + PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.type.layout"), + typeLayout, {{"{{scriptType}}", _scriptType}}); + PlaceWidgets(obs_module_text("AdvSceneSwitcher.script.file.layout"), + _fileLayout, + {{"{{path}}", _path}, {"{{open}}", _openFile}}, false); + auto layout = new QVBoxLayout(); + layout->addLayout(typeLayout); + layout->addLayout(languageLayout); + layout->addLayout(_fileLayout); + layout->addWidget(_script); + setLayout(layout); +} + +void MacroSegmentScriptInlineEdit::SetWidgetVisibility() +{ + _script->setVisible(_entryData->GetType() == + InlineScript::Type::INLINE); + SetLayoutVisible(_fileLayout, + _entryData->GetType() == InlineScript::Type::FILE); + adjustSize(); + updateGeometry(); +} + +} // namespace advss diff --git a/plugins/scripting/macro-segment-script-inline.hpp b/plugins/scripting/macro-segment-script-inline.hpp new file mode 100644 index 00000000..88a14b3b --- /dev/null +++ b/plugins/scripting/macro-segment-script-inline.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "file-selection.hpp" +#include "inline-script.hpp" +#include "variable-text-edit.hpp" + +#include + +namespace advss { + +class MacroSegmentScriptInline { +public: + InlineScript::Type GetType() const { return _script.GetType(); } + void SetType(InlineScript::Type); + obs_script_lang GetLanguage() const { return _script.GetLanguage(); } + void SetLanguage(obs_script_lang); + StringVariable GetScript() const { return _script.GetText(); } + void SetScript(const std::string &); + std::string GetPath() const { return _script.GetPath(); } + void SetPath(const std::string &); + +protected: + InlineScript _script; +}; + +class MacroSegmentScriptInlineEdit : public QWidget { + Q_OBJECT + +public: + MacroSegmentScriptInlineEdit( + QWidget *, std::shared_ptr = nullptr); + virtual ~MacroSegmentScriptInlineEdit() = default; + +protected slots: + void ScriptTypeChanged(int); + void LanguageChanged(int); + void ScriptChanged(); + void PathChanged(const QString &); + +protected: + void PopulateWidgets(); + void SetupWidgetConnections(); + void SetupLayout(); + void SetWidgetVisibility(); + + QComboBox *_scriptType; + QComboBox *_language; + ScriptEditor *_script; + FileSelection *_path; + QPushButton *_openFile; + QHBoxLayout *_fileLayout; + + std::shared_ptr _entryData; + bool _loading = true; +}; + +} // namespace advss diff --git a/plugins/scripting/utils/inline-script.cpp b/plugins/scripting/utils/inline-script.cpp new file mode 100644 index 00000000..3c7ca19b --- /dev/null +++ b/plugins/scripting/utils/inline-script.cpp @@ -0,0 +1,344 @@ +#include "inline-script.hpp" +#include "log-helper.hpp" +#include "obs-module-helper.hpp" + +#include +#include + +#include +#include + +namespace advss { + +static constexpr std::string_view signalName = "advss_run_temp_script"; + +std::atomic_uint64_t InlineScript::_instanceIdCounter = 0; +const std::string_view InlineScript::_defaultPythonScript = + "import obspython as obs\n" + "\n" + "def run():\n" + " obs.script_log(obs.LOG_WARNING, \"Hello from Python!\")\n" + " return True\n"; +const std::string_view InlineScript::_defaultLUAScript = + "obs = obslua\n" + "\n" + "function run()\n" + " obs.script_log(obs.LOG_WARNING, \"Hello from LUA!\")\n" + " return true\n" + "end"; +; + +static bool setup() +{ + auto sh = obs_get_signal_handler(); + auto signalDecl = + std::string("void ") + signalName.data() + "(string id)"; + signal_handler_add(sh, signalDecl.c_str()); + + return true; +} +static bool setupDone = setup(); + +static void cleanupScriptFile(const std::string &path) +{ + const QFileInfo fileInfo(QString::fromStdString(path)); + if (!fileInfo.isFile()) { + return; + } + QFile file(fileInfo.absoluteFilePath()); + if (!file.remove()) { + vblog(LOG_INFO, "failed to clean up script file %s", + fileInfo.absoluteFilePath().toStdString().c_str()); + } +} + +static std::optional +getScriptTempFilePath(obs_script_lang language) +{ + static int counter = 0; + ++counter; + static const QString filenamePattern = + "scripting/advss-tmp-script%1.%2"; + const QString filename = filenamePattern.arg(counter).arg( + language == OBS_SCRIPT_LANG_PYTHON ? "py" : "lua"); + auto settingsFile = + obs_module_config_path(filename.toStdString().c_str()); + + if (!settingsFile) { + blog(LOG_WARNING, + "could not create temp script file! (obs_module_config_path)"); + return {}; + } + + std::string path = settingsFile; + bfree(settingsFile); + return path; +} + +static bool createScriptFile(const char *settingsFile, const char *content) +{ + const QFileInfo fileInfo(settingsFile); + const QString dirPath = fileInfo.absolutePath(); + const QDir dir(dirPath); + if (!dir.exists() && !dir.mkpath(dirPath)) { + blog(LOG_WARNING, "could not create script file! (mkpath)"); + return false; + } + + QFile file(settingsFile); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + + auto out = QTextStream(&file); + out << content; + + return true; +} + +InlineScript::InlineScript() : _instanceId(_instanceIdCounter++) +{ + Setup(); +} + +InlineScript::InlineScript(const InlineScript &other) + : _language(other._language), + _textPython(other._textPython), + _textLUA(other._textLUA), + _instanceId(_instanceIdCounter++) +{ + Setup(); +} + +void InlineScript::Save(obs_data_t *data) const +{ + OBSDataAutoRelease obj = obs_data_create(); + obs_data_set_int(obj, "type", _type); + obs_data_set_int(obj, "language", _language); + _textPython.Save(obj, "scriptPython"); + _textLUA.Save(obj, "scriptLUA"); + obs_data_set_string(obj, "file", _file.c_str()); + obs_data_set_obj(data, "script", obj); +} + +void InlineScript::Load(obs_data_t *data) +{ + OBSDataAutoRelease obj = obs_data_get_obj(data, "script"); + _type = static_cast(obs_data_get_int(obj, "type")); + _language = + static_cast(obs_data_get_int(obj, "language")); + _textPython.Load(obj, "scriptPython"); + _textLUA.Load(obj, "scriptLUA"); + _file = obs_data_get_string(obj, "file"); + Setup(); +} + +void InlineScript::SetType(Type type) +{ + _type = type; + Setup(); +} + +void InlineScript::SetLanguage(obs_script_lang language) +{ + _language = language; + Setup(); +} + +void InlineScript::SetText(const std::string &text) +{ + switch (_language) { + case OBS_SCRIPT_LANG_UNKNOWN: + break; + case OBS_SCRIPT_LANG_LUA: + _textLUA = text; + break; + case OBS_SCRIPT_LANG_PYTHON: + _textPython = text; + break; + default: + break; + } + Setup(); +} + +const StringVariable &InlineScript::GetText() const +{ + static const StringVariable defaultRet; + + switch (_language) { + case OBS_SCRIPT_LANG_UNKNOWN: + break; + case OBS_SCRIPT_LANG_LUA: + return _textLUA; + case OBS_SCRIPT_LANG_PYTHON: + return _textPython; + default: + break; + } + return defaultRet; +} + +void InlineScript::SetPath(const std::string &path) +{ + _file = path; + Setup(); +} + +bool InlineScript::Run() +{ + static auto sh = obs_get_signal_handler(); + + if (_type == INLINE && _lastResolvedText != std::string(GetText())) { + Setup(); + } + + if (_type == FILE && _lastPath != _file) { + Setup(); + } + + auto cd = calldata_create(); + calldata_set_string(cd, "id", GetID().c_str()); + signal_handler_signal(sh, signalName.data(), cd); + bool result = calldata_bool(cd, "result"); + calldata_destroy(cd); + return result; +} + +void InlineScript::ResolveVariablesToFixedValues() +{ + _textPython.ResolveVariables(); + _textLUA.ResolveVariables(); +} + +static std::string preprocessScriptText(const std::string &text, + obs_script_lang language, + const std::string &id) +{ + const std::string footerPython = + std::string("\n\n" + "## AUTO GENERATED ##\n" + "def script_load(settings):\n" + " def run_wrapper(data):\n" + " id = obs.calldata_string(data, \"id\")\n" + " if id == \"") + + id + + "\":\n" + " ret = run()\n" + " obs.calldata_set_bool(data, \"result\", ret)\n" + " sh = obs.obs_get_signal_handler()\n" + " obs.signal_handler_connect(sh, \"" + + signalName.data() + "\", run_wrapper)\n\n"; + + const std::string footerLUA = + std::string( + "\n\n" + "-- AUTO GENERATED --\n" + "function script_load(settings)\n" + " local run_wrapper = (function(data)\n" + " local id = obs.calldata_string(data, \"id\")\n" + " if id == \"") + + id + + "\" then\n" + " local ret = run()\n" + " obs.calldata_set_bool(data, \"result\", ret)\n" + " end\n" + " end)\n" + " local sh = obs.obs_get_signal_handler()\n" + " obs.signal_handler_connect(sh, \"" + + signalName.data() + + "\" , run_wrapper)\n" + "end\n"; + + std::string scriptText = + language == OBS_SCRIPT_LANG_PYTHON ? footerPython : footerLUA; + return text + scriptText; +} + +void InlineScript::SetupFile() +{ + const auto path = GetLUACompatiblePath(_file); + _fileId = path; + + if (path.empty()) { + return; + } + + if (!QFileInfo(QString::fromStdString(path)).exists()) { + const auto text = preprocessScriptText( + _language == OBS_SCRIPT_LANG_PYTHON + ? _defaultPythonScript.data() + : _defaultLUAScript.data(), + _language, GetID()); + (void)createScriptFile(_file.c_str(), text.c_str()); + } + + _script = std::unique_ptr( + CreateOBSScript(path.c_str(), nullptr), {}); + _lastPath = _file; +} + +void InlineScript::SetupInline() +{ + const StringVariable &text = + _language == OBS_SCRIPT_LANG_PYTHON ? _textPython : _textLUA; + const auto scriptText = preprocessScriptText(text, _language, GetID()); + + auto path_ = getScriptTempFilePath(_language); + if (!path_) { + return; + } + + auto path = GetLUACompatiblePath(*path_); + if (!createScriptFile(path.c_str(), scriptText.c_str())) { + return; + } + + _script = std::unique_ptr( + CreateOBSScript(path.c_str(), nullptr), {path}); + _lastResolvedText = text; +} + +void InlineScript::Setup() +{ + _script.reset(); + _lastResolvedText = ""; + _lastPath = ""; + + if (_type == FILE) { + SetupFile(); + } else { + SetupInline(); + } +} + +std::string InlineScript::GetID() const +{ + if (_type == FILE) { + return _fileId; + } + return std::to_string(_instanceId); +} + +void InlineScript::ScriptDeleter::operator()(obs_script_t *script) +{ + DestroyOBSScript(script); + if (!tempScriptPath.empty()) { + cleanupScriptFile(tempScriptPath); + } +} + +ScriptEditor::ScriptEditor(QWidget *parent) : VariableTextEdit(parent, 15, 5) +{ + installEventFilter(this); +} + +bool ScriptEditor::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::FocusOut) { + emit ScriptChanged(); + } + return QObject::eventFilter(obj, event); +} + +} // namespace advss diff --git a/plugins/scripting/utils/inline-script.hpp b/plugins/scripting/utils/inline-script.hpp new file mode 100644 index 00000000..a9b3fd27 --- /dev/null +++ b/plugins/scripting/utils/inline-script.hpp @@ -0,0 +1,76 @@ +#pragma once +#include "obs-script-helpers.hpp" +#include "variable-text-edit.hpp" + +#include +#include +#include +#include + +#include + +namespace advss { + +class InlineScript { +public: + InlineScript(); + InlineScript(const InlineScript &); + + enum Type { INLINE, FILE }; + + void Save(obs_data_t *) const; + void Load(obs_data_t *); + + void SetType(Type); + Type GetType() const { return _type; } + void SetLanguage(obs_script_lang); + obs_script_lang GetLanguage() const { return _language; } + void SetText(const std::string &); + const StringVariable &GetText() const; + void SetPath(const std::string &); + const std::string &GetPath() const { return _file; } + + bool Run(); + + void ResolveVariablesToFixedValues(); + +private: + void Setup(); + void SetupFile(); + void SetupInline(); + std::string GetID() const; + + Type _type = INLINE; + obs_script_lang _language = OBS_SCRIPT_LANG_PYTHON; + std::string _file; + StringVariable _textPython = _defaultPythonScript.data(); + StringVariable _textLUA = _defaultLUAScript.data(); + std::string _lastResolvedText; + std::string _lastPath; + std::string _fileId; + const uint64_t _instanceId; + + struct ScriptDeleter { + void operator()(obs_script_t *); + std::string tempScriptPath; + }; + std::unique_ptr _script; + + static std::atomic_uint64_t _instanceIdCounter; + static const std::string_view _defaultPythonScript; + static const std::string_view _defaultLUAScript; +}; + +class ScriptEditor : public VariableTextEdit { + Q_OBJECT +public: + ScriptEditor(QWidget *parent); + +signals: + void ScriptChanged(); + +private: + bool eventFilter(QObject *obj, QEvent *event) override; +}; + +} // namespace advss diff --git a/plugins/scripting/utils/obs-script-helpers.cpp b/plugins/scripting/utils/obs-script-helpers.cpp new file mode 100644 index 00000000..20cc608f --- /dev/null +++ b/plugins/scripting/utils/obs-script-helpers.cpp @@ -0,0 +1,72 @@ +#include "obs-script-helpers.hpp" +#include "log-helper.hpp" + +#include +#include + +namespace advss { + +static const char *libName = +#if defined(WIN32) + "obs-scripting.dll"; +#elif __APPLE__ + "obs-scripting.dylib"; +#else + "obs-scripting.so"; +#endif + +typedef obs_script_t *(*obs_script_create_t)(const char *, obs_data_t *); +typedef void (*obs_script_destroy_t)(obs_script_t *); + +obs_script_create_t obs_script_create = nullptr; +obs_script_destroy_t obs_script_destroy = nullptr; + +static bool setup() +{ + QLibrary scriptingLib(libName); + + obs_script_create = + (obs_script_create_t)scriptingLib.resolve("obs_script_create"); + if (!obs_script_create) { + blog(LOG_WARNING, + "could not resolve obs_script_create symbol!"); + } + + obs_script_destroy = (obs_script_destroy_t)scriptingLib.resolve( + "obs_script_destroy"); + if (!obs_script_destroy) { + blog(LOG_WARNING, + "could not resolve obs_script_destroy symbol!"); + } + + return true; +} +static bool setupDone = setup(); + +obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings) +{ + if (!obs_script_create) { + return nullptr; + } + return obs_script_create(path, settings); +} + +void DestroyOBSScript(obs_script_t *script) +{ + if (!obs_script_destroy) { + return; + } + obs_script_destroy(script); +} + +std::string GetLUACompatiblePath(const std::string &path) +{ + // Can't use settingsFile here as LUA will complain if Windows style + // paths (C:\some\path) are used. + // QFileInfo::absoluteFilePath will convert those paths so LUA won't + // complain. (C:/some/path) + const QFileInfo fileInfo(QString::fromStdString(path)); + return fileInfo.absoluteFilePath().toStdString(); +} + +} // namespace advss diff --git a/plugins/scripting/utils/obs-script-helpers.hpp b/plugins/scripting/utils/obs-script-helpers.hpp new file mode 100644 index 00000000..31101ed0 --- /dev/null +++ b/plugins/scripting/utils/obs-script-helpers.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +#include + +// Based on obs-scripting.h + +struct obs_script; +typedef struct obs_script obs_script_t; + +enum obs_script_lang { + OBS_SCRIPT_LANG_UNKNOWN, + OBS_SCRIPT_LANG_LUA, + OBS_SCRIPT_LANG_PYTHON +}; + +namespace advss { + +obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings); +void DestroyOBSScript(obs_script_t *script); +std::string GetLUACompatiblePath(const std::string &path); + +} // namespace advss