From 0b01e4c7a153e49769a811139c72474b01770ae5 Mon Sep 17 00:00:00 2001 From: jp9000 Date: Mon, 25 Dec 2017 14:11:19 -0800 Subject: [PATCH] frontend-tools: Add scripting tool Adds a scripting tool that gives the users the ability to load scripts and change their settings. --- .../frontend-tools/CMakeLists.txt | 29 + .../frontend-tools/data/locale/en-US.ini | 19 + .../frontend-tools/data/scripts/countdown.lua | 180 ++++++ .../frontend-tools/forms/scripts.ui | 266 +++++++++ .../frontend-tools/frontend-tools-config.h.in | 19 + .../frontend-tools/frontend-tools.c | 11 + .../frontend-tools/scripts.cpp | 519 ++++++++++++++++++ .../frontend-tools/scripts.hpp | 49 ++ 8 files changed, 1092 insertions(+) create mode 100644 UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua create mode 100644 UI/frontend-plugins/frontend-tools/forms/scripts.ui create mode 100644 UI/frontend-plugins/frontend-tools/scripts.cpp create mode 100644 UI/frontend-plugins/frontend-tools/scripts.hpp diff --git a/UI/frontend-plugins/frontend-tools/CMakeLists.txt b/UI/frontend-plugins/frontend-tools/CMakeLists.txt index 6c8441216..44bb4e219 100644 --- a/UI/frontend-plugins/frontend-tools/CMakeLists.txt +++ b/UI/frontend-plugins/frontend-tools/CMakeLists.txt @@ -11,6 +11,8 @@ if(UNIX AND NOT APPLE) include_directories(${X11_INCLUDE_DIR}) endif() +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/obs-scripting") + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/frontend-tools-config.h.in" "${CMAKE_BINARY_DIR}/config/frontend-tools-config.h") @@ -21,12 +23,19 @@ set(frontend-tools_HEADERS auto-scene-switcher.hpp output-timer.hpp tool-helpers.hpp + ../../properties-view.hpp + ../../properties-view.moc.hpp + ../../vertical-scroll-area.hpp + ../../double-slider.hpp ) set(frontend-tools_SOURCES ${frontend-tools_SOURCES} auto-scene-switcher.cpp frontend-tools.c output-timer.cpp + ../../properties-view.cpp + ../../vertical-scroll-area.cpp + ../../double-slider.cpp ) set(frontend-tools_UI ${frontend-tools_UI} @@ -34,6 +43,25 @@ set(frontend-tools_UI forms/output-timer.ui ) +if(ENABLE_SCRIPTING) + set(frontend-tools_HEADERS + ${frontend-tools_HEADERS} + scripts.hpp + ) + set(frontend-tools_SOURCES + ${frontend-tools_SOURCES} + scripts.cpp + ) + set(frontend-tools_UI + ${frontend-tools_UI} + forms/scripts.ui + ) + set(EXTRA_LIBS + ${EXTRA_LIBS} + obs-scripting + ) +endif() + if(WIN32) set(frontend-tools_PLATFORM_SOURCES auto-scene-switcher-win.cpp) @@ -79,6 +107,7 @@ add_library(frontend-tools MODULE ) target_link_libraries(frontend-tools ${frontend-tools_PLATFORM_LIBS} + ${EXTRA_LIBS} obs-frontend-api Qt5::Widgets libobs) diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini b/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini index 092a0972b..5dfbfdeae 100644 --- a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini +++ b/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini @@ -24,3 +24,22 @@ OutputTimer.Stream.StoppingIn="Streaming stopping in:" OutputTimer.Record.StoppingIn="Recording stopping in:" OutputTimer.Stream.EnableEverytime="Enable streaming timer every time" OutputTimer.Record.EnableEverytime="Enable recording timer every time" + +Scripts="Scripts" +LoadedScripts="Loaded Scripts" +AddScripts="Add Scripts" +RemoveScripts="Remove Scripts" +ReloadScripts="Reload Scripts" +LoadedScripts="Loaded Scripts" +LuaSettings="Lua Settings" +LuaSettings.LuaDepPaths="Lua Dependency Paths" +LuaSettings.AddLuaDepPath="Add Lua Dependency Path" +LuaSettings.RemoveLuaDepPath="Remove Lua Dependency Path" +PythonSettings="Python Settings" +PythonSettings.PythonInstallPath32bit="Python Install Path (32bit)" +PythonSettings.PythonInstallPath64bit="Python Install Path (64bit)" +PythonSettings.BrowsePythonPath="Browse Python Path" +ScriptLogWindow="Script Log" + +FileFilter.ScriptFiles="Script Files" +FileFilter.AllFiles="All Files" diff --git a/UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua b/UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua new file mode 100644 index 000000000..862b53f25 --- /dev/null +++ b/UI/frontend-plugins/frontend-tools/data/scripts/countdown.lua @@ -0,0 +1,180 @@ +obs = obslua +source_name = "" +total_seconds = 0 + +cur_seconds = 0 +last_text = "" +stop_text = "" +activated = false + +hotkey_id = obs.OBS_INVALID_HOTKEY_ID + +-- Function to set the time text +function set_time_text() + local seconds = math.floor(cur_seconds % 60) + local total_minutes = math.floor(cur_seconds / 60) + local minutes = math.floor(total_minutes % 60) + local hours = math.floor(total_minutes / 60) + local text = string.format("%02d:%02d:%02d", hours, minutes, seconds) + + if cur_seconds < 1 then + text = stop_text + end + + if text ~= last_text then + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + obs.obs_source_release(source) + end + end + + last_text = text +end + +function timer_callback() + cur_seconds = cur_seconds - 1 + if cur_seconds < 0 then + obs.remove_current_callback() + cur_seconds = 0 + end + + set_time_text() +end + +function activate(activating) + if activated == activating then + return + end + + activated = activating + + if activating then + cur_seconds = total_seconds + set_time_text() + obs.timer_add(timer_callback, 1000) + else + obs.timer_remove(timer_callback) + end +end + +-- Called when a source is activated/deactivated +function activate_signal(cd, activating) + local source = obs.calldata_source(cd, "source") + if source ~= nil then + local name = obs.obs_source_get_name(source) + if (name == source_name) then + activate(activating) + end + end +end + +function source_activated(cd) + activate_signal(cd, true) +end + +function source_deactivated(cd) + activate_signal(cd, false) +end + +function reset(pressed) + if not pressed then + return + end + + activate(false) + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + local active = obs.obs_source_active(source) + obs.obs_source_release(source) + activate(active) + end +end + +function reset_button_clicked(props, p) + reset(true) + return false +end + +---------------------------------------------------------- + +-- A function named script_properties defines the properties that the user +-- can change for the entire script module itself +function script_properties() + local props = obs.obs_properties_create() + obs.obs_properties_add_int(props, "duration", "Duration (minutes)", 1, 100000, 1) + + local p = obs.obs_properties_add_list(props, "source", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) + local sources = obs.obs_enum_sources() + if sources ~= nil then + for _, source in ipairs(sources) do + source_id = obs.obs_source_get_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + local name = obs.obs_source_get_name(source) + obs.obs_property_list_add_string(p, name, name) + end + end + end + obs.source_list_release(sources) + + obs.obs_properties_add_text(props, "stop_text", "Final Text", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(props, "reset_button", "Reset Timer", reset_button_clicked) + + return props +end + +-- A function named script_description returns the description shown to +-- the user +function script_description() + return "Sets a text source to act as a countdown timer when the source is active.\n\nMade by Jim" +end + +-- A function named script_update will be called when settings are changed +function script_update(settings) + activate(false) + + total_seconds = obs.obs_data_get_int(settings, "duration") * 60 + source_name = obs.obs_data_get_string(settings, "source") + stop_text = obs.obs_data_get_string(settings, "stop_text") + + reset(true) +end + +-- A function named script_defaults will be called to set the default settings +function script_defaults(settings) + obs.obs_data_set_default_int(settings, "duration", 5) + obs.obs_data_set_default_string(settings, "stop_text", "Starting soon (tm)") +end + +-- A function named script_save will be called when the script is saved +-- +-- NOTE: This function is usually used for saving extra data (such as in this +-- case, a hotkey's save data). Settings set via the properties are saved +-- automatically. +function script_save(settings) + local hotkey_save_array = obs.obs_hotkey_save(hotkey_id) + obs.obs_data_set_array(settings, "reset_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) +end + +-- a function named script_load will be called on startup +function script_load(settings) + -- Connect hotkey and activation/deactivation signal callbacks + -- + -- NOTE: These particular script callbacks do not necessarily have to + -- be disconnected, as callbacks will automatically destroy themselves + -- if the script is unloaded. So there's no real need to manually + -- disconnect callbacks that are intended to last until the script is + -- unloaded. + local sh = obs.obs_get_signal_handler() + obs.signal_handler_connect(sh, "source_activate", source_activated) + obs.signal_handler_connect(sh, "source_deactivate", source_deactivated) + + hotkey_id = obs.obs_hotkey_register_frontend("reset_timer_thingy", "Reset Timer", reset) + local hotkey_save_array = obs.obs_data_get_array(settings, "reset_hotkey") + obs.obs_hotkey_load(hotkey_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) +end diff --git a/UI/frontend-plugins/frontend-tools/forms/scripts.ui b/UI/frontend-plugins/frontend-tools/forms/scripts.ui new file mode 100644 index 000000000..cd36cd7cb --- /dev/null +++ b/UI/frontend-plugins/frontend-tools/forms/scripts.ui @@ -0,0 +1,266 @@ + + + ScriptsTool + + + + 0 + 0 + 775 + 492 + + + + Scripts + + + + + + 0 + + + + Scripts + + + + + + + + LoadedScripts + + + scripts + + + + + + + true + + + + + + + + + + 22 + 22 + + + + AddScripts + + + AddScripts + + + + + + true + + + addIconSmall + + + + + + + + 22 + 22 + + + + RemoveScripts + + + RemoveScripts + + + + + + true + + + removeIconSmall + + + + + + + + 22 + 22 + + + + ReloadScripts + + + ReloadScripts + + + + + + true + + + refreshIconSmall + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ScriptLogWindow + + + + + + + + + + + + + + 0 + 0 + + + + Description + + + + + + + + + + true + + + 12 + + + + + + + + + + PythonSettings + + + + + + + + + pythonPath + + + + + + + + + true + + + + + + + PythonSettings.BrowsePythonPath + + + Browse + + + + + + + + + Qt::Vertical + + + + 510 + 306 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + tabWidget + close + pythonPath + pythonPathBrowse + + + + diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in b/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in index 3389a4260..1b7210597 100644 --- a/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in +++ b/UI/frontend-plugins/frontend-tools/frontend-tools-config.h.in @@ -1,3 +1,22 @@ #pragma once +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef ON +#define ON 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + +#ifndef OFF +#define OFF 0 +#endif + #define BUILD_CAPTIONS @BUILD_CAPTIONS@ +#define ENABLE_SCRIPTING @ENABLE_SCRIPTING@ +#define COMPILE_LUA @COMPILE_LUA@ +#define COMPILE_PYTHON @COMPILE_PYTHON@ diff --git a/UI/frontend-plugins/frontend-tools/frontend-tools.c b/UI/frontend-plugins/frontend-tools/frontend-tools.c index 81ee4b82c..78e199d22 100644 --- a/UI/frontend-plugins/frontend-tools/frontend-tools.c +++ b/UI/frontend-plugins/frontend-tools/frontend-tools.c @@ -15,6 +15,11 @@ void FreeCaptions(); void InitOutputTimer(); void FreeOutputTimer(); +#if ENABLE_SCRIPTING +void InitScripts(); +void FreeScripts(); +#endif + bool obs_module_load(void) { #if defined(_WIN32) && BUILD_CAPTIONS @@ -22,6 +27,9 @@ bool obs_module_load(void) #endif InitSceneSwitcher(); InitOutputTimer(); +#if ENABLE_SCRIPTING + InitScripts(); +#endif return true; } @@ -32,4 +40,7 @@ void obs_module_unload(void) #endif FreeSceneSwitcher(); FreeOutputTimer(); +#if ENABLE_SCRIPTING + FreeScripts(); +#endif } diff --git a/UI/frontend-plugins/frontend-tools/scripts.cpp b/UI/frontend-plugins/frontend-tools/scripts.cpp new file mode 100644 index 000000000..b69629b4b --- /dev/null +++ b/UI/frontend-plugins/frontend-tools/scripts.cpp @@ -0,0 +1,519 @@ +#include "scripts.hpp" +#include "frontend-tools-config.h" +#include "../../properties-view.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "ui_scripts.h" + +#if COMPILE_PYTHON && (defined(_WIN32) || defined(__APPLE__)) +#define PYTHON_UI 1 +#else +#define PYTHON_UI 0 +#endif + +#if ARCH_BITS == 64 +#define ARCH_NAME "64bit" +#else +#define ARCH_NAME "32bit" +#endif + +#define PYTHONPATH_LABEL_TEXT "PythonSettings.PythonInstallPath" ARCH_NAME + +/* ----------------------------------------------------------------- */ + +using OBSScript = OBSObj; + +struct ScriptData { + std::vector scripts; + + inline obs_script_t *FindScript(const char *path) + { + for (OBSScript &script : scripts) { + const char *script_path = obs_script_get_path(script); + if (strcmp(script_path, path) == 0) { + return script; + } + } + + return nullptr; + } + + bool ScriptOpened(const char *path) + { + for (OBSScript &script : scripts) { + const char *script_path = obs_script_get_path(script); + if (strcmp(script_path, path) == 0) { + return true; + } + } + + return false; + } +}; + +static ScriptData *scriptData = nullptr; +static ScriptsTool *scriptsWindow = nullptr; +static ScriptLogWindow *scriptLogWindow = nullptr; +static QPlainTextEdit *scriptLogWidget = nullptr; + +/* ----------------------------------------------------------------- */ + +ScriptLogWindow::ScriptLogWindow() : QWidget(nullptr) +{ + const QFont fixedFont = + QFontDatabase::systemFont(QFontDatabase::FixedFont); + + QPlainTextEdit *edit = new QPlainTextEdit(); + edit->setReadOnly(true); + edit->setFont(fixedFont); + edit->setWordWrapMode(QTextOption::NoWrap); + + QDialogButtonBox *buttonBox = new QDialogButtonBox( + QDialogButtonBox::Close); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QWidget::hide); + + QVBoxLayout *layout = new QVBoxLayout(); + layout->addWidget(edit); + layout->addWidget(buttonBox); + + setLayout(layout); + scriptLogWidget = edit; + + resize(600, 400); + + config_t *global_config = obs_frontend_get_global_config(); + const char *geom = config_get_string(global_config, + "ScriptLogWindow", "geometry"); + if (geom != nullptr) { + QByteArray ba = QByteArray::fromBase64(QByteArray(geom)); + restoreGeometry(ba); + } + + setWindowTitle(obs_module_text("ScriptLogWindow")); + + connect(edit->verticalScrollBar(), &QAbstractSlider::sliderMoved, + this, &ScriptLogWindow::ScrollChanged); +} + +ScriptLogWindow::~ScriptLogWindow() +{ + config_t *global_config = obs_frontend_get_global_config(); + config_set_string(global_config, + "ScriptLogWindow", "geometry", + saveGeometry().toBase64().constData()); +} + +void ScriptLogWindow::ScrollChanged(int val) +{ + QScrollBar *scroll = scriptLogWidget->verticalScrollBar(); + bottomScrolled = (val == scroll->maximum()); +} + +void ScriptLogWindow::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + if (bottomScrolled) { + QScrollBar *scroll = scriptLogWidget->verticalScrollBar(); + scroll->setValue(scroll->maximum()); + } +} + +void ScriptLogWindow::AddLogMsg(int log_level, QString msg) +{ + QScrollBar *scroll = scriptLogWidget->verticalScrollBar(); + bottomScrolled = scroll->value() == scroll->maximum(); + + lines += QStringLiteral("\n"); + lines += msg; + scriptLogWidget->setPlainText(lines); + + if (bottomScrolled) + scroll->setValue(scroll->maximum()); + + if (log_level <= LOG_WARNING) { + show(); + raise(); + } +} + +void ScriptLogWindow::Clear() +{ + lines.clear(); +} + +/* ----------------------------------------------------------------- */ + +ScriptsTool::ScriptsTool() + : QWidget (nullptr), + ui (new Ui_ScriptsTool) +{ + ui->setupUi(this); + RefreshLists(); + +#if PYTHON_UI + config_t *config = obs_frontend_get_global_config(); + const char *path = config_get_string(config, "Python", + "Path" ARCH_NAME); + ui->pythonPath->setText(path); + ui->pythonPathLabel->setText(obs_module_text(PYTHONPATH_LABEL_TEXT)); +#else + delete ui->pythonSettingsTab; + ui->pythonSettingsTab = nullptr; +#endif + + delete propertiesView; + propertiesView = new QWidget(); + propertiesView->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + ui->propertiesLayout->addWidget(propertiesView); +} + +ScriptsTool::~ScriptsTool() +{ + delete ui; +} + +void ScriptsTool::RemoveScript(const char *path) +{ + for (size_t i = 0; i < scriptData->scripts.size(); i++) { + OBSScript &script = scriptData->scripts[i]; + + const char *script_path = obs_script_get_path(script); + if (strcmp(script_path, path) == 0) { + scriptData->scripts.erase( + scriptData->scripts.begin() + i); + break; + } + } +} + +void ScriptsTool::ReloadScript(const char *path) +{ + for (OBSScript &script : scriptData->scripts) { + const char *script_path = obs_script_get_path(script); + if (strcmp(script_path, path) == 0) { + obs_script_reload(script); + break; + } + } +} + +void ScriptsTool::RefreshLists() +{ + ui->scripts->clear(); + + for (OBSScript &script : scriptData->scripts) { + const char *script_path = obs_script_get_path(script); + ui->scripts->addItem(script_path); + } +} + +void ScriptsTool::on_close_clicked() +{ + close(); +} + +void ScriptsTool::on_addScripts_clicked() +{ + const char **formats = obs_scripting_supported_formats(); + const char **cur_format = formats; + QString extensions; + QString filter; + + while (*cur_format) { + if (!extensions.isEmpty()) + extensions += QStringLiteral(" "); + + extensions += QStringLiteral("*."); + extensions += *cur_format; + + cur_format++; + } + + if (!extensions.isEmpty()) { + filter += obs_module_text("FileFilter.ScriptFiles"); + filter += QStringLiteral(" ("); + filter += extensions; + filter += QStringLiteral(")"); + } + + if (filter.isEmpty()) + return; + + static std::string lastBrowsedDir; + + if (lastBrowsedDir.empty()) { + BPtr baseScriptPath = obs_module_file("scripts"); + lastBrowsedDir = baseScriptPath; + } + + QFileDialog dlg(this, obs_module_text("AddScripts")); + dlg.setFileMode(QFileDialog::ExistingFiles); + dlg.setDirectory(QDir(lastBrowsedDir.c_str())); + dlg.setNameFilter(filter); + dlg.exec(); + + QStringList files = dlg.selectedFiles(); + if (!files.count()) + return; + + lastBrowsedDir = dlg.directory().path().toUtf8().constData(); + + for (const QString &file : files) { + QByteArray pathBytes = file.toUtf8(); + const char *path = pathBytes.constData(); + + if (scriptData->ScriptOpened(path)) { + continue; + } + + obs_script_t *script = obs_script_create(path, NULL); + if (script) { + scriptData->scripts.emplace_back(script); + ui->scripts->addItem(file); + } + } +} + +void ScriptsTool::on_removeScripts_clicked() +{ + QList items = ui->scripts->selectedItems(); + + for (QListWidgetItem *item : items) + RemoveScript(item->text().toUtf8().constData()); + RefreshLists(); +} + +void ScriptsTool::on_reloadScripts_clicked() +{ + QList items = ui->scripts->selectedItems(); + for (QListWidgetItem *item : items) + ReloadScript(item->text().toUtf8().constData()); + + on_scripts_currentRowChanged(ui->scripts->currentRow()); +} + +void ScriptsTool::on_scriptLog_clicked() +{ + scriptLogWindow->show(); + scriptLogWindow->raise(); +} + +void ScriptsTool::on_pythonPathBrowse_clicked() +{ + QString curPath = ui->pythonPath->text(); + QString newPath = QFileDialog::getExistingDirectory( + this, + ui->pythonPathLabel->text(), + curPath); + + if (newPath.isEmpty()) + return; + + QByteArray array = newPath.toUtf8(); + const char *path = array.constData(); + + config_t *config = obs_frontend_get_global_config(); + config_set_string(config, "Python", "Path" ARCH_NAME, path); + + ui->pythonPath->setText(newPath); + + if (obs_scripting_python_loaded()) + return; + if (!obs_scripting_load_python(path)) + return; + + for (OBSScript &script : scriptData->scripts) { + enum obs_script_lang lang = obs_script_get_lang(script); + if (lang == OBS_SCRIPT_LANG_PYTHON) { + obs_script_reload(script); + } + } + + on_scripts_currentRowChanged(ui->scripts->currentRow()); +} + +void ScriptsTool::on_scripts_currentRowChanged(int row) +{ + ui->propertiesLayout->removeWidget(propertiesView); + delete propertiesView; + + if (row == -1) { + propertiesView = new QWidget(); + propertiesView->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + ui->propertiesLayout->addWidget(propertiesView); + ui->description->setText(QString()); + return; + } + + QByteArray array = ui->scripts->item(row)->text().toUtf8(); + const char *path = array.constData(); + + obs_script_t *script = scriptData->FindScript(path); + if (!script) { + propertiesView = nullptr; + return; + } + + OBSData settings = obs_script_get_settings(script); + obs_data_release(settings); + + propertiesView = new OBSPropertiesView(settings, script, + (PropertiesReloadCallback)obs_script_get_properties, + (PropertiesUpdateCallback)obs_script_update); + ui->propertiesLayout->addWidget(propertiesView); + ui->description->setText(obs_script_get_description(script)); +} + +/* ----------------------------------------------------------------- */ + +extern "C" void FreeScripts() +{ + obs_scripting_unload(); +} + +static void obs_event(enum obs_frontend_event event, void *) +{ + if (event == OBS_FRONTEND_EVENT_EXIT) { + delete scriptData; + delete scriptsWindow; + delete scriptLogWindow; + + } else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) { + scriptLogWindow->hide(); + scriptLogWindow->Clear(); + + delete scriptData; + scriptData = new ScriptData; + } +} + +static void load_script_data(obs_data_t *load_data, bool, void *) +{ + obs_data_array_t *array = obs_data_get_array(load_data, + "scripts-tool"); + + delete scriptData; + scriptData = new ScriptData; + + size_t size = obs_data_array_count(array); + for (size_t i = 0; i < size; i++) { + obs_data_t *obj = obs_data_array_item(array, i); + const char *path = obs_data_get_string(obj, "path"); + obs_data_t *settings = obs_data_get_obj(obj, "settings"); + + obs_script_t *script = obs_script_create(path, settings); + if (script) { + scriptData->scripts.emplace_back(script); + } + + obs_data_release(settings); + obs_data_release(obj); + } + + if (scriptsWindow) + scriptsWindow->RefreshLists(); + + obs_data_array_release(array); +} + +static void save_script_data(obs_data_t *save_data, bool saving, void *) +{ + if (!saving) + return; + + obs_data_array_t *array = obs_data_array_create(); + + for (OBSScript &script : scriptData->scripts) { + const char *script_path = obs_script_get_path(script); + obs_data_t *settings = obs_script_save(script); + + obs_data_t *obj = obs_data_create(); + obs_data_set_string(obj, "path", script_path); + obs_data_set_obj(obj, "settings", settings); + obs_data_array_push_back(array, obj); + obs_data_release(obj); + + obs_data_release(settings); + } + + obs_data_set_array(save_data, "scripts-tool", array); + obs_data_array_release(array); +} + +static void script_log(void *, obs_script_t *script, int log_level, + const char *message) +{ + QString qmsg; + qmsg = QStringLiteral("[%1] %2").arg( + obs_script_get_file(script), + message); + + QMetaObject::invokeMethod(scriptLogWindow, "AddLogMsg", + Q_ARG(int, log_level), + Q_ARG(QString, qmsg)); +} + +extern "C" void InitScripts() +{ + scriptLogWindow = new ScriptLogWindow(); + + obs_scripting_load(); + obs_scripting_set_log_callback(script_log, nullptr); + + QAction *action = (QAction*)obs_frontend_add_tools_menu_qaction( + obs_module_text("Scripts")); + +#if PYTHON_UI + config_t *config = obs_frontend_get_global_config(); + const char *python_path = config_get_string(config, "Python", + "Path" ARCH_NAME); + + if (!obs_scripting_python_loaded() && python_path && *python_path) + obs_scripting_load_python(python_path); +#endif + + scriptData = new ScriptData; + + auto cb = [] () + { + obs_frontend_push_ui_translation(obs_module_get_string); + + if (!scriptsWindow) { + scriptsWindow = new ScriptsTool(); + scriptsWindow->show(); + } else { + scriptsWindow->show(); + scriptsWindow->raise(); + } + + obs_frontend_pop_ui_translation(); + }; + + obs_frontend_add_save_callback(save_script_data, nullptr); + obs_frontend_add_preload_callback(load_script_data, nullptr); + obs_frontend_add_event_callback(obs_event, nullptr); + + action->connect(action, &QAction::triggered, cb); +} diff --git a/UI/frontend-plugins/frontend-tools/scripts.hpp b/UI/frontend-plugins/frontend-tools/scripts.hpp new file mode 100644 index 000000000..af1194a4c --- /dev/null +++ b/UI/frontend-plugins/frontend-tools/scripts.hpp @@ -0,0 +1,49 @@ +#include +#include + +class Ui_ScriptsTool; + +class ScriptLogWindow : public QWidget { + Q_OBJECT + + QString lines; + bool bottomScrolled = true; + + void resizeEvent(QResizeEvent *event) override; + +public: + ScriptLogWindow(); + ~ScriptLogWindow(); + +public slots: + void AddLogMsg(int log_level, QString msg); + void Clear(); + void ScrollChanged(int val); +}; + +class ScriptsTool : public QWidget { + Q_OBJECT + + Ui_ScriptsTool *ui; + QWidget *propertiesView = nullptr; + +public: + ScriptsTool(); + ~ScriptsTool(); + + void RemoveScript(const char *path); + void ReloadScript(const char *path); + void RefreshLists(); + +public slots: + void on_close_clicked(); + + void on_addScripts_clicked(); + void on_removeScripts_clicked(); + void on_reloadScripts_clicked(); + void on_scriptLog_clicked(); + + void on_scripts_currentRowChanged(int row); + + void on_pythonPathBrowse_clicked(); +}; -- GitLab