#include "obs-module.h" #include "scripts.hpp" #include "../../properties-view.hpp" #include "../../qt-wrappers.hpp" #include "../../plain-text-edit.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_scripts.h" #if defined(Python_FOUND) && (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 = OBSPtr; 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 OBSPlainTextEdit *scriptLogWidget = nullptr; /* ----------------------------------------------------------------- */ ScriptLogWindow::ScriptLogWindow() : QDialog(nullptr) { OBSPlainTextEdit *edit = new OBSPlainTextEdit(); edit->setReadOnly(true); edit->setWordWrapMode(QTextOption::NoWrap); QHBoxLayout *buttonLayout = new QHBoxLayout(); QPushButton *clearButton = new QPushButton(tr("Clear")); connect(clearButton, &QPushButton::clicked, this, &ScriptLogWindow::ClearWindow); QPushButton *closeButton = new QPushButton(tr("Close")); connect(closeButton, &QPushButton::clicked, this, &QDialog::hide); buttonLayout->addStretch(); buttonLayout->addWidget(clearButton); buttonLayout->addWidget(closeButton); QVBoxLayout *layout = new QVBoxLayout(); layout->addWidget(edit); layout->addLayout(buttonLayout); setLayout(layout); scriptLogWidget = edit; setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 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::ClearWindow() { Clear(); scriptLogWidget->setPlainText(QString()); } void ScriptLogWindow::Clear() { lines.clear(); } /* ----------------------------------------------------------------- */ ScriptsTool::ScriptsTool() : QDialog(nullptr), ui(new Ui_ScriptsTool) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 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)); updatePythonVersionLabel(); #else delete ui->pythonSettingsTab; ui->pythonSettingsTab = nullptr; ui->tabWidget->setStyleSheet("QTabWidget::pane {border: 0;}"); #endif delete propertiesView; propertiesView = new QWidget(); propertiesView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->propertiesLayout->addWidget(propertiesView); config_t *global_config = obs_frontend_get_global_config(); int row = config_get_int(global_config, "scripts-tool", "prevScriptRow"); ui->scripts->setCurrentRow(row); } ScriptsTool::~ScriptsTool() { config_t *global_config = obs_frontend_get_global_config(); config_set_int(global_config, "scripts-tool", "prevScriptRow", ui->scripts->currentRow()); } void ScriptsTool::updatePythonVersionLabel() { QString label; if (obs_scripting_python_loaded()) { char version[8]; obs_scripting_python_version(version, sizeof(version)); label = QString(obs_module_text("PythonSettings.PythonVersion")) .arg(version); } else { label = obs_module_text("PythonSettings.PythonNotLoaded"); } ui->pythonVersionLabel->setText(label); } 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); OBSDataAutoRelease settings = obs_data_create(); obs_properties_t *prop = obs_script_get_properties(script); obs_properties_apply_settings(prop, settings); obs_properties_destroy(prop); break; } } } void ScriptsTool::RefreshLists() { ui->scripts->clear(); for (OBSScript &script : scriptData->scripts) { const char *script_file = obs_script_get_file(script); const char *script_path = obs_script_get_path(script); QListWidgetItem *item = new QListWidgetItem(script_file); item->setData(Qt::UserRole, QString(script_path)); ui->scripts->addItem(item); } } void ScriptsTool::SetScriptDefaults(const char *path) { for (OBSScript &script : scriptData->scripts) { const char *script_path = obs_script_get_path(script); if (strcmp(script_path, path) == 0) { OBSDataAutoRelease settings = obs_script_get_settings(script); obs_data_clear(settings); obs_script_update(script, nullptr); on_reloadScripts_clicked(); break; } } } 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; } QStringList files = OpenFiles(this, QT_UTF8(obs_module_text("AddScripts")), QT_UTF8(lastBrowsedDir.c_str()), filter); if (!files.count()) return; for (const QString &file : files) { lastBrowsedDir = QFileInfo(file).absolutePath().toUtf8().constData(); 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) { const char *script_file = obs_script_get_file(script); scriptData->scripts.emplace_back(script); QListWidgetItem *item = new QListWidgetItem(script_file); item->setData(Qt::UserRole, QString(file)); ui->scripts->addItem(item); OBSDataAutoRelease settings = obs_data_create(); obs_properties_t *prop = obs_script_get_properties(script); obs_properties_apply_settings(prop, settings); obs_properties_destroy(prop); ui->scripts->setCurrentItem(item); } } } void ScriptsTool::on_removeScripts_clicked() { QList items = ui->scripts->selectedItems(); for (QListWidgetItem *item : items) RemoveScript(item->data(Qt::UserRole) .toString() .toUtf8() .constData()); RefreshLists(); } void ScriptsTool::on_reloadScripts_clicked() { QList items = ui->scripts->selectedItems(); for (QListWidgetItem *item : items) ReloadScript(item->data(Qt::UserRole) .toString() .toUtf8() .constData()); on_scripts_currentRowChanged(ui->scripts->currentRow()); } void ScriptsTool::OpenScriptParentDirectory() { QList items = ui->scripts->selectedItems(); for (QListWidgetItem *item : items) { QDir dir(item->data(Qt::UserRole).toString()); dir.cdUp(); QDesktopServices::openUrl( QUrl::fromLocalFile(dir.absolutePath())); } } void ScriptsTool::on_scripts_customContextMenuRequested(const QPoint &pos) { QListWidgetItem *item = ui->scripts->itemAt(pos); QMenu popup(this); obs_frontend_push_ui_translation(obs_module_get_string); popup.addAction(tr("Add"), this, &ScriptsTool::on_addScripts_clicked); if (item) { popup.addSeparator(); popup.addAction(obs_module_text("Reload"), this, &ScriptsTool::on_reloadScripts_clicked); popup.addAction(obs_module_text("OpenFileLocation"), this, &ScriptsTool::OpenScriptParentDirectory); popup.addSeparator(); popup.addAction(tr("Remove"), this, &ScriptsTool::on_removeScripts_clicked); } obs_frontend_pop_ui_translation(); popup.exec(QCursor::pos()); } void ScriptsTool::on_editScript_clicked() { int row = ui->scripts->currentRow(); if (row == -1) return; QUrl url = QUrl::fromLocalFile( ui->scripts->item(row)->data(Qt::UserRole).toString()); QDesktopServices::openUrl(url); } void ScriptsTool::on_scriptLog_clicked() { scriptLogWindow->show(); scriptLogWindow->raise(); } void ScriptsTool::on_pythonPathBrowse_clicked() { QString curPath = ui->pythonPath->text(); QString newPath = SelectDirectory(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); bool loaded = obs_scripting_python_loaded(); if (loaded && !newPath.isEmpty() && curPath.compare(newPath) != 0) { char version[8]; obs_scripting_python_version(version, sizeof(version)); QString message = QString(obs_module_text( "PythonSettings.AlreadyLoaded.Message")) .arg(version); OBSMessageBox::information( this, obs_module_text("PythonSettings.AlreadyLoaded.Title"), message); return; } else if (loaded) { return; } if (!obs_scripting_load_python(path)) return; updatePythonVersionLabel(); 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)->data(Qt::UserRole).toString().toUtf8(); const char *path = array.constData(); obs_script_t *script = scriptData->FindScript(path); if (!script) { propertiesView = nullptr; return; } OBSDataAutoRelease settings = obs_script_get_settings(script); OBSPropertiesView *view = new OBSPropertiesView( settings.Get(), script, (PropertiesReloadCallback)obs_script_get_properties, nullptr, (PropertiesVisualUpdateCb)obs_script_update); view->SetDeferrable(false); propertiesView = view; ui->propertiesLayout->addWidget(propertiesView); ui->description->setText(obs_script_get_description(script)); } void ScriptsTool::on_defaults_clicked() { QListWidgetItem *item = ui->scripts->currentItem(); if (!item) return; SetScriptDefaults( item->data(Qt::UserRole).toString().toUtf8().constData()); } void ScriptsTool::on_description_linkActivated(const QString &link) { QUrl url(link, QUrl::StrictMode); if (url.isValid() && (url.scheme().compare("http") == 0 || url.scheme().compare("https") == 0)) { QString msg(obs_module_text("ScriptDescriptionLink.Text")); msg += "\n\n"; msg += QString(obs_module_text( "ScriptDescriptionLink.Text.Url")) .arg(link); const char *open = obs_module_text("ScriptDescriptionLink.OpenURL"); QMessageBox messageBox(this); messageBox.setWindowTitle(open); messageBox.setText(msg); obs_frontend_push_ui_translation(obs_module_get_string); QPushButton *yesButton = messageBox.addButton(open, QMessageBox::YesRole); QPushButton *noButton = messageBox.addButton(tr("Cancel"), QMessageBox::NoRole); obs_frontend_pop_ui_translation(); messageBox.setDefaultButton(yesButton); messageBox.setEscapeButton(noButton); messageBox.setIcon(QMessageBox::Question); messageBox.exec(); if (messageBox.clickedButton() == yesButton) QDesktopServices::openUrl(url); } } /* ----------------------------------------------------------------- */ 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; scriptData = nullptr; scriptsWindow = nullptr; scriptLogWindow = nullptr; } else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) { if (scriptLogWindow) { scriptLogWindow->hide(); scriptLogWindow->Clear(); } delete scriptData; scriptData = new ScriptData; } } static void load_script_data(obs_data_t *load_data, bool, void *) { OBSDataArrayAutoRelease 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++) { OBSDataAutoRelease obj = obs_data_array_item(array, i); const char *path = obs_data_get_string(obj, "path"); OBSDataAutoRelease settings = obs_data_get_obj(obj, "settings"); obs_script_t *script = obs_script_create(path, settings); if (script) { scriptData->scripts.emplace_back(script); } } if (scriptsWindow) scriptsWindow->RefreshLists(); } static void save_script_data(obs_data_t *save_data, bool saving, void *) { if (!saving) return; OBSDataArrayAutoRelease array = obs_data_array_create(); for (OBSScript &script : scriptData->scripts) { const char *script_path = obs_script_get_path(script); OBSDataAutoRelease settings = obs_script_save(script); OBSDataAutoRelease 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_set_array(save_data, "scripts-tool", array); } static void script_log(void *, obs_script_t *script, int log_level, const char *message) { QString qmsg; if (script) { qmsg = QStringLiteral("[%1] %2").arg( obs_script_get_file(script), message); } else { qmsg = QStringLiteral("[Unknown Script] %1").arg(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); #ifdef __APPLE__ if (python_path && *python_path) { std::string _python_path(python_path); std::size_t pos = _python_path.find("/Python.framework/Versions"); if (pos != std::string::npos) { std::string _temp = _python_path.substr(0, pos); config_set_string(config, "Python", "Path" ARCH_NAME, _temp.c_str()); config_save(config); python_path = _temp.c_str(); } } #endif 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); }