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();
+};