diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index ae881a3f657bf1e55d73ce37004d3da511a2670d..ab10c577b1c9ae01ef65a1775eed58b9a9581b9c 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -223,6 +223,7 @@ set(obs_SOURCES window-log-reply.cpp window-projector.cpp window-remux.cpp + window-missing-files.cpp auth-base.cpp source-tree.cpp scene-tree.cpp @@ -285,6 +286,7 @@ set(obs_HEADERS window-log-reply.hpp window-projector.hpp window-remux.hpp + window-missing-files.hpp auth-base.hpp source-tree.hpp scene-tree.hpp @@ -364,6 +366,7 @@ set(obs_UI forms/OBSUpdate.ui forms/OBSRemux.ui forms/OBSImporter.ui + forms/OBSMissingFiles.ui forms/OBSAbout.ui) set(obs_QRC diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 09205f7b2f529a7f5532e12a32d57342c8491a9a..e9309431821f8aaa59471518161cd22e6f93f1ae 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -364,6 +364,24 @@ Remux.ExitUnfinishedTitle="Remuxing in progress" Remux.ExitUnfinished="Remuxing is not finished, stopping now may render the target file unusable.\nAre you sure you want to stop remuxing?" Remux.HelpText="Drop files in this window to remux, or select an empty \"OBS Recording\" cell to browse for a file." +# missing file dialog +MissingFiles="Missing Files" +MissingFiles.MissingFile="Missing File" +MissingFiles.NewFile="New File" +MissingFiles.HelpText="You have some missing files since you last used OBS." +MissingFiles.Clear="" +MissingFiles.NumFound="Found $1 of $2" +MissingFiles.Search="Search Directory..." +MissingFiles.SelectFile="Select file..." +MissingFiles.SelectDir="Select Folder to Search in" +MissingFiles.State="State" +MissingFiles.Missing="Missing" +MissingFiles.Replaced="Replaced" +MissingFiles.Cleared="Cleared" +MissingFiles.Found="Found" +MissingFiles.AutoSearch="Additional file matches found" +MissingFiles.AutoSearchText="OBS has found additional matches for missing files in that directory. Would you like to add them?" + # update dialog UpdateAvailable="New Update Available" UpdateAvailable.Text="Version %1.%2.%3 is now available. Click here to download" @@ -533,6 +551,7 @@ Basic.Main.AddSourceHelp.Text="You need to have at least 1 scene to add a source # basic mode main window Basic.Main.Scenes="Scenes" Basic.Main.Sources="Sources" +Basic.Main.Source="Source" Basic.Main.Controls="Controls" Basic.Main.Connecting="Connecting..." Basic.Main.StartRecording="Start Recording" diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index e68982e1cf9ef6e91c62cb2d70933d2a3c03fd2d..1a8d2450140d24a3e7a86d7cdaafed5846da80e5 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -1048,6 +1048,10 @@ QPushButton#extraPanelDelete:pressed { background-color: #161f41; } +OBSMissingFiles { + qproperty-warningIcon: url(./Dark/alert.svg); +} + /* Source Icons */ OBSBasic { diff --git a/UI/data/themes/Dark.qss b/UI/data/themes/Dark.qss index 97af215c333853940f6f4c40895341181426b3c1..c8612ead0e30624366bd8afd4a1408e66ac71647 100644 --- a/UI/data/themes/Dark.qss +++ b/UI/data/themes/Dark.qss @@ -773,6 +773,10 @@ QPushButton#extraPanelDelete:pressed { background-color: rgb(31,30,31); } +OBSMissingFiles { + qproperty-warningIcon: url(./Dark/alert.svg); +} + /* Source Icons */ OBSBasic { diff --git a/UI/data/themes/Dark/alert.svg b/UI/data/themes/Dark/alert.svg new file mode 100644 index 0000000000000000000000000000000000000000..01e87d795343a2972bbde60e0baffd730e1a411b --- /dev/null +++ b/UI/data/themes/Dark/alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 5ae927c40f7d67fac4987506e6ab73c95d4cb3bd..7e069345f52f19b2d63a7f951f19fd1c16adc844 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -1359,6 +1359,10 @@ QPushButton#extraPanelDelete:pressed { background-color: rgb(240, 98, 146); } +OBSMissingFiles { + qproperty-warningIcon: url(./Dark/alert.svg); +} + /* Source Icons */ OBSBasic { diff --git a/UI/data/themes/System.qss b/UI/data/themes/System.qss index 125c5089eda30b6fa7c755a49e0f9dccde00ba8a..93c935a5477c0a27447f8bbd907195176f387b6f 100644 --- a/UI/data/themes/System.qss +++ b/UI/data/themes/System.qss @@ -209,6 +209,10 @@ VisibilityCheckBox::indicator:unchecked { qproperty-icon: url(:res/images/revert.svg); } +OBSMissingFiles { + qproperty-warningIcon: url(:res/images/alert.svg); +} + /* Source Icons */ OBSBasic { diff --git a/UI/forms/OBSMissingFiles.ui b/UI/forms/OBSMissingFiles.ui new file mode 100644 index 0000000000000000000000000000000000000000..93523b16e5da76613e8b934685168a68173086f5 --- /dev/null +++ b/UI/forms/OBSMissingFiles.ui @@ -0,0 +1,119 @@ + + + OBSMissingFiles + + + + 0 + 0 + 666 + 310 + + + + MissingFiles + + + + + + + + + 20 + + + + + + + + + 0 + 0 + + + + MissingFiles.HelpText + + + + + + + + + QAbstractItemView::NoSelection + + + 23 + + + 23 + + + false + + + 23 + + + + + + + + + + + MissingFiles.NumFound + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + MissingFiles.Search + + + + + + + Apply + + + + + + + Cancel + + + + + + + + + + + + + + diff --git a/UI/forms/images/alert.svg b/UI/forms/images/alert.svg new file mode 100644 index 0000000000000000000000000000000000000000..afad76c71fd5b412502d39fdcb913127c9ea2c39 --- /dev/null +++ b/UI/forms/images/alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/UI/forms/obs.qrc b/UI/forms/obs.qrc index 01f594176000f6a6e3cc77ae6a5243053fe5c585..9ff1a1d7e9264dd800472216c96b8edb97e2dc5a 100644 --- a/UI/forms/obs.qrc +++ b/UI/forms/obs.qrc @@ -26,6 +26,7 @@ images/help_light.svg images/trash.svg images/revert.svg + images/alert.svg images/sources/brush.svg images/sources/camera.svg images/sources/gamepad.svg diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 89ae20194c38a7a12900d1dab3f6f6acd34adbb5..845d509046284ca83c38b40dd7f33604cef81109 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -51,6 +51,7 @@ #include "window-log-reply.hpp" #include "window-projector.hpp" #include "window-remux.hpp" +#include "window-missing-files.hpp" #include "qt-wrappers.hpp" #include "context-bar-controls.hpp" #include "obs-proxy-style.hpp" @@ -980,7 +981,19 @@ void OBSBasic::Load(const char *file) obs_data_array_push_back_array(sources, groups); } - obs_load_sources(sources, nullptr, nullptr); + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb = [](void *private_data, obs_source_t *source) { + obs_missing_files_t *f = (obs_missing_files_t *)private_data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); + + UNUSED_PARAMETER(source); + }; + + obs_load_sources(sources, cb, files); if (transitions) LoadTransitions(transitions); @@ -1124,6 +1137,14 @@ retryScene: LogScenes(); + if (obs_missing_files_count(files) > 0) { + OBSMissingFiles *miss = new OBSMissingFiles(files, this); + miss->show(); + miss->raise(); + } else { + obs_missing_files_destroy(files); + } + disableSaving--; if (api) { diff --git a/UI/window-missing-files.cpp b/UI/window-missing-files.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b320405cc97df81109e56df8896758c8f9c6f0e2 --- /dev/null +++ b/UI/window-missing-files.cpp @@ -0,0 +1,597 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "window-missing-files.hpp" +#include "window-basic-main.hpp" + +#include "obs-app.hpp" + +#include +#include +#include + +#include "qt-wrappers.hpp" + +enum MissingFilesColumn { + Source, + OriginalPath, + NewPath, + State, + + Count +}; + +enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +MissingFilesPathItemDelegate::MissingFilesPathItemDelegate( + bool isOutput, const QString &defaultPath) + : QStyledItemDelegate(), isOutput(isOutput), defaultPath(defaultPath) +{ +} + +QWidget *MissingFilesPathItemDelegate::createEditor( + QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, + QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::PushButton); + + QWidget *container = new QWidget(parent); + + auto browseCallback = [this, container]() { + const_cast(this)->handleBrowse( + container); + }; + + auto clearCallback = [this, container]() { + const_cast(this)->handleClear( + container); + }; + + QHBoxLayout *layout = new QHBoxLayout(); + layout->setMargin(0); + layout->setSpacing(0); + + QLineEdit *text = new QLineEdit(); + text->setObjectName(QStringLiteral("text")); + text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, + QSizePolicy::Policy::Expanding, + QSizePolicy::ControlType::LineEdit)); + layout->addWidget(text); + + QToolButton *browseButton = new QToolButton(); + browseButton->setText("..."); + browseButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(browseButton); + + container->connect(browseButton, &QToolButton::clicked, browseCallback); + + // The "clear" button is not shown in input cells + if (isOutput) { + QToolButton *clearButton = new QToolButton(); + clearButton->setText("X"); + clearButton->setSizePolicy(buttonSizePolicy); + layout->addWidget(clearButton); + + container->connect(clearButton, &QToolButton::clicked, + clearCallback); + } + + container->setLayout(layout); + container->setFocusProxy(text); + + UNUSED_PARAMETER(index); + + return container; +} + +void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, + const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void MissingFilesPathItemDelegate::setModelData(QWidget *editor, + QAbstractItemModel *model, + const QModelIndex &index) const +{ + // We use the PATH_LIST_PROP property to pass a list of + // path strings from the editor widget into the model's + // NewPathsToProcessRole. This is only used when paths + // are selected through the "browse" or "delete" buttons + // in the editor. If the user enters new text in the + // text box, we simply pass that text on to the model + // as normal text data in the default role. + QVariant pathListProp = editor->property(PATH_LIST_PROP); + if (pathListProp.isValid()) { + QStringList list = + editor->property(PATH_LIST_PROP).toStringList(); + if (isOutput) { + model->setData(index, list); + } else + model->setData(index, list, + MissingFilesRole::NewPathsToProcessRole); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text(), 0); + } +} + +void MissingFilesPathItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem localOption = option; + initStyleOption(&localOption, index); + + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, + &localOption, painter); +} + +void MissingFilesPathItemDelegate::handleBrowse(QWidget *container) +{ + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + if (currentPath.isEmpty() || + currentPath.compare(QTStr("MissingFiles.Clear")) == 0) + currentPath = defaultPath; + + bool isSet = false; + if (isOutput) { + QString newPath = QFileDialog::getOpenFileName( + container, QTStr("MissingFiles.SelectFile"), + currentPath, nullptr); + + if (!newPath.isEmpty()) { + container->setProperty(PATH_LIST_PROP, + QStringList() << newPath); + isSet = true; + } + } + + if (isSet) + emit commitData(container); +} + +void MissingFilesPathItemDelegate::handleClear(QWidget *container) +{ + // An empty string list will indicate that the entry is being + // blanked and should be deleted. + container->setProperty(PATH_LIST_PROP, + QStringList() << QTStr("MissingFiles.Clear")); + container->findChild()->clearFocus(); + ((QWidget *)container->parent())->setFocus(); + emit commitData(container); +} + +/** + Model +**/ + +MissingFilesModel::MissingFilesModel(QObject *parent) + : QAbstractTableModel(parent) +{ + QStyle *style = QApplication::style(); + + warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning); +} + +int MissingFilesModel::rowCount(const QModelIndex &) const +{ + return files.length(); +} + +int MissingFilesModel::columnCount(const QModelIndex &) const +{ + return MissingFilesColumn::Count; +} + +int MissingFilesModel::found() const +{ + int res = 0; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != Missing && files[i].state != Cleared) + res++; + } + + return res; +} + +QVariant MissingFilesModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= files.length()) { + return QVariant(); + } else if (role == Qt::DisplayRole) { + QFileInfo fi(files[index.row()].originalPath); + + switch (index.column()) { + case MissingFilesColumn::Source: + result = files[index.row()].source; + break; + case MissingFilesColumn::OriginalPath: + result = fi.fileName(); + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + case MissingFilesColumn::State: + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + } + break; + } + } else if (role == Qt::DecorationRole && + index.column() == MissingFilesColumn::Source) { + OBSBasic *main = + reinterpret_cast(App()->GetMainWindow()); + obs_source_t *source = obs_get_source_by_name( + files[index.row()].source.toStdString().c_str()); + + result = main->GetSourceIcon(obs_source_get_id(source)); + + obs_source_release(source); + } else if (role == Qt::FontRole && + index.column() == MissingFilesColumn::State) { + QFont font = QFont(); + font.setBold(true); + + result = font; + } else if (role == Qt::ToolTipRole && + index.column() == MissingFilesColumn::State) { + switch (files[index.row()].state) { + case MissingFilesState::Missing: + result = QTStr("MissingFiles.Missing"); + break; + + case MissingFilesState::Replaced: + result = QTStr("MissingFiles.Replaced"); + break; + + case MissingFilesState::Found: + result = QTStr("MissingFiles.Found"); + break; + + case MissingFilesState::Cleared: + result = QTStr("MissingFiles.Cleared"); + break; + + default: + break; + } + } else if (role == Qt::ToolTipRole) { + switch (index.column()) { + case MissingFilesColumn::OriginalPath: + result = files[index.row()].originalPath; + break; + case MissingFilesColumn::NewPath: + result = files[index.row()].newPath; + break; + default: + break; + } + } + + return result; +} + +Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == MissingFilesColumn::OriginalPath) { + flags &= ~Qt::ItemIsEditable; + } else if (index.column() == MissingFilesColumn::NewPath && + index.row() != files.length()) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void MissingFilesModel::fileCheckLoop(QList files, + QString path, bool skipPrompt) +{ + loop = false; + QUrl url = QUrl().fromLocalFile(path); + QString dir = + url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | + QUrl::PreferLocalFile); + + bool prompted = skipPrompt; + + for (int i = 0; i < files.length(); i++) { + if (files[i].state != MissingFilesState::Missing) + continue; + + QUrl origFile = QUrl().fromLocalFile(files[i].originalPath); + QString filename = origFile.fileName(); + QString testFile = dir + filename; + + if (os_file_exists(testFile.toStdString().c_str())) { + if (!prompted) { + QMessageBox::StandardButton button = + QMessageBox::question( + nullptr, + QTStr("MissingFiles.AutoSearch"), + QTStr("MissingFiles.AutoSearchText")); + + if (button == QMessageBox::No) + break; + + prompted = true; + } + QModelIndex in = index(i, MissingFilesColumn::NewPath); + setData(in, testFile, 0); + } + } + loop = true; +} + +bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, + int role) +{ + bool success = false; + + if (role == MissingFilesRole::NewPathsToProcessRole) { + QStringList list = value.toStringList(); + + int row = index.row() + 1; + beginInsertRows(QModelIndex(), row, row); + + MissingFileEntry entry; + entry.originalPath = list[0].replace("\\", "/"); + entry.source = list[1]; + + files.insert(row, entry); + row++; + + endInsertRows(); + + success = true; + } else { + QString path = value.toString(); + if (index.column() == MissingFilesColumn::NewPath) { + files[index.row()].newPath = value.toString(); + QString fileName = QUrl(path).fileName(); + QString origFileName = + QUrl(files[index.row()].originalPath).fileName(); + + if (path.isEmpty()) { + files[index.row()].state = + MissingFilesState::Missing; + } else if (path.compare(QTStr("MissingFiles.Clear")) == + 0) { + files[index.row()].state = + MissingFilesState::Cleared; + } else if (fileName.compare(origFileName) == 0) { + files[index.row()].state = + MissingFilesState::Found; + + if (loop) + fileCheckLoop(files, path, false); + } else { + files[index.row()].state = + MissingFilesState::Replaced; + + if (loop) + fileCheckLoop(files, path, false); + } + + emit dataChanged(index, index); + success = true; + } + } + + return success; +} + +QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, + int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && + orientation == Qt::Orientation::Horizontal) { + switch (section) { + case MissingFilesColumn::State: + result = QTStr("MissingFiles.State"); + break; + case MissingFilesColumn::Source: + result = QTStr("Basic.Main.Source"); + break; + case MissingFilesColumn::OriginalPath: + result = QTStr("MissingFiles.MissingFile"); + break; + case MissingFilesColumn::NewPath: + result = QTStr("MissingFiles.NewFile"); + break; + } + } + + return result; +} + +OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent) + : QDialog(parent), + filesModel(new MissingFilesModel), + ui(new Ui::OBSMissingFiles) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(filesModel); + ui->tableView->setItemDelegateForColumn( + MissingFilesColumn::OriginalPath, + new MissingFilesPathItemDelegate(false, "")); + ui->tableView->setItemDelegateForColumn( + MissingFilesColumn::NewPath, + new MissingFilesPathItemDelegate(true, "")); + ui->tableView->horizontalHeader()->setSectionResizeMode( + QHeaderView::ResizeMode::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode( + MissingFilesColumn::Source, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3); + ui->tableView->horizontalHeader()->setSectionResizeMode( + MissingFilesColumn::State, + QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->setEditTriggers( + QAbstractItemView::EditTrigger::CurrentChanged); + + ui->warningIcon->setPixmap( + filesModel->warningIcon.pixmap(QSize(32, 32))); + + for (size_t i = 0; i < obs_missing_files_count(files); i++) { + obs_missing_file_t *f = + obs_missing_files_get_file(files, (int)i); + + const char *oldPath = obs_missing_file_get_path(f); + const char *name = obs_missing_file_get_source_name(f); + + addMissingFile(oldPath, name); + } + + QString found = QTStr("MissingFiles.NumFound"); + found.replace("$1", "0"); + found.replace("$2", QString::number(obs_missing_files_count(files))); + + ui->found->setText(found); + + fileStore = files; + + connect(ui->doneButton, &QPushButton::pressed, this, + &OBSMissingFiles::saveFiles); + connect(ui->browseButton, &QPushButton::pressed, this, + &OBSMissingFiles::browseFolders); + connect(ui->cancelButton, &QPushButton::pressed, this, + &OBSMissingFiles::close); + connect(filesModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, + SLOT(dataChanged())); + + QModelIndex index = filesModel->createIndex(0, 1); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", + Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +OBSMissingFiles::~OBSMissingFiles() +{ + obs_missing_files_destroy(fileStore); +} + +void OBSMissingFiles::addMissingFile(const char *originalPath, + const char *sourceName) +{ + QStringList list; + + list.append(originalPath); + list.append(sourceName); + + QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, + MissingFilesColumn::Source); + + filesModel->setData(insertIndex, list, + MissingFilesRole::NewPathsToProcessRole); +} + +void OBSMissingFiles::saveFiles() +{ + for (int i = 0; i < filesModel->files.length(); i++) { + MissingFilesState state = filesModel->files[i].state; + if (state != MissingFilesState::Missing) { + obs_missing_file_t *f = + obs_missing_files_get_file(fileStore, i); + + QString path = filesModel->files[i].newPath; + + if (state == MissingFilesState::Cleared) { + obs_missing_file_issue_callback(f, ""); + } else { + char *p = bstrdup(path.toStdString().c_str()); + obs_missing_file_issue_callback(f, p); + bfree(p); + } + } + } + + QDialog::accept(); + destroy(); +} + +void OBSMissingFiles::browseFolders() +{ + QString dir = QFileDialog::getExistingDirectory( + this, QTStr("MissingFiles.SelectDir"), "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir != "") { + dir += "/"; + filesModel->fileCheckLoop(filesModel->files, dir, true); + } +} + +void OBSMissingFiles::dataChanged() +{ + QString found = QTStr("MissingFiles.NumFound"); + found.replace("$1", QString::number(filesModel->found())); + found.replace("$2", + QString::number(obs_missing_files_count(fileStore))); + + ui->found->setText(found); + + ui->tableView->resizeColumnToContents(MissingFilesColumn::State); + ui->tableView->resizeColumnToContents(MissingFilesColumn::Source); +} + +QIcon OBSMissingFiles::GetWarningIcon() +{ + return filesModel->warningIcon; +} + +void OBSMissingFiles::SetWarningIcon(const QIcon &icon) +{ + ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32))); + filesModel->warningIcon = icon; +} diff --git a/UI/window-missing-files.hpp b/UI/window-missing-files.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4e2144feefcd9d5601e5144df159a1e5ca9b3422 --- /dev/null +++ b/UI/window-missing-files.hpp @@ -0,0 +1,120 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include "obs-app.hpp" +#include "ui_OBSMissingFiles.h" + +class MissingFilesModel; + +enum MissingFilesState { Missing, Found, Replaced, Cleared }; +Q_DECLARE_METATYPE(MissingFilesState); + +class OBSMissingFiles : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon warningIcon READ GetWarningIcon WRITE SetWarningIcon + DESIGNABLE true) + + QPointer filesModel; + std::unique_ptr ui; + +public: + explicit OBSMissingFiles(obs_missing_files_t *files, + QWidget *parent = nullptr); + virtual ~OBSMissingFiles() override; + + void addMissingFile(const char *originalPath, const char *sourceName); + + QIcon GetWarningIcon(); + void SetWarningIcon(const QIcon &icon); + +private: + void saveFiles(); + void browseFolders(); + + obs_missing_files_t *fileStore; + +public slots: + void dataChanged(); +}; + +class MissingFilesModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSMissingFiles; + +public: + explicit MissingFilesModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int found() const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + bool loop = true; + + QIcon warningIcon; + +private: + struct MissingFileEntry { + MissingFilesState state = MissingFilesState::Missing; + + QString source; + + QString originalPath; + QString newPath; + }; + + QList files; + + void fileCheckLoop(QList files, QString path, + bool skipPrompt); +}; + +class MissingFilesPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath); + + virtual QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const override; + + virtual void setEditorData(QWidget *editor, + const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + virtual void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + bool isOutput; + QString defaultPath; + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); +}; diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index a0d390fcba3cc9478987f384e4dc42f521bfc5da..5eb3042f73d41877b64b18b2b7e69d8679f1b61f 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -402,6 +402,7 @@ set(libobs_libobs_SOURCES obs.c obs-properties.c obs-data.c + obs-missing-files.c obs-hotkey.c obs-hotkey-name-map.c obs-module.c @@ -437,6 +438,7 @@ set(libobs_libobs_HEADERS obs-ui.h obs-properties.h obs-data.h + obs-missing-files.h obs-interaction.h obs-hotkey.h obs-hotkeys.h diff --git a/libobs/obs-missing-files.c b/libobs/obs-missing-files.c new file mode 100644 index 0000000000000000000000000000000000000000..494ef301dbaef003f036daf7b7b81063bc229d56 --- /dev/null +++ b/libobs/obs-missing-files.c @@ -0,0 +1,151 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "util/threading.h" +#include "util/dstr.h" +#include "obs-missing-files.h" +#include "obs.h" + +struct obs_missing_file { + volatile long ref; + char *file_path; + obs_missing_file_cb callback; + int src_type; + void *src; + char *src_name; + void *data; +}; + +struct obs_missing_files { + DARRAY(struct obs_missing_file *) files; +}; + +obs_missing_files_t *obs_missing_files_create() +{ + struct obs_missing_files *files = + bzalloc(sizeof(struct obs_missing_files)); + + return files; +} + +void obs_missing_files_destroy(obs_missing_files_t *files) +{ + for (size_t i = 0; i < files->files.num; i++) { + obs_missing_file_release(files->files.array[i]); + } + + da_free(files->files); + bfree(files); +} + +void obs_missing_files_add_file(obs_missing_files_t *files, + obs_missing_file_t *file) +{ + da_insert(files->files, files->files.num, &file); +} + +size_t obs_missing_files_count(obs_missing_files_t *files) +{ + return files->files.num; +} + +obs_missing_file_t *obs_missing_files_get_file(obs_missing_files_t *files, + int idx) +{ + return files->files.array[idx]; +} + +void obs_missing_files_append(obs_missing_files_t *dst, + obs_missing_files_t *src) +{ + for (size_t i = 0; i < src->files.num; i++) { + obs_missing_file_t *file = src->files.array[i]; + obs_missing_files_add_file(dst, file); + os_atomic_inc_long(&file->ref); + } +} + +obs_missing_file_t *obs_missing_file_create(const char *path, + obs_missing_file_cb callback, + int src_type, void *src, void *data) +{ + struct obs_missing_file *file = + bzalloc(sizeof(struct obs_missing_file)); + + file->file_path = bstrdup(path); + file->callback = callback; + file->src_type = src_type; + file->src = src; + file->data = data; + file->ref = 1; + + switch (src_type) { + case OBS_MISSING_FILE_SOURCE: + file->src_name = bstrdup(obs_source_get_name(src)); + break; + case OBS_MISSING_FILE_SCRIPT: + break; + } + + return file; +} + +void obs_missing_file_release(obs_missing_file_t *file) +{ + if (!file) + return; + + if (os_atomic_dec_long(&file->ref) == 0) + obs_missing_file_destroy(file); +} + +void obs_missing_file_destroy(obs_missing_file_t *file) +{ + switch (file->src_type) { + case OBS_MISSING_FILE_SOURCE: + bfree(file->src_name); + break; + case OBS_MISSING_FILE_SCRIPT: + break; + } + bfree(file->file_path); + bfree(file); +} + +void obs_missing_file_issue_callback(obs_missing_file_t *file, + const char *new_path) +{ + switch (file->src_type) { + case OBS_MISSING_FILE_SOURCE: + obs_source_replace_missing_file(file->callback, + (obs_source_t *)file->src, + new_path, file->data); + break; + case OBS_MISSING_FILE_SCRIPT: + break; + } +} + +const char *obs_missing_file_get_path(obs_missing_file_t *file) +{ + return file->file_path; +} + +const char *obs_missing_file_get_source_name(obs_missing_file_t *file) +{ + return file->src_name; +} diff --git a/libobs/obs-missing-files.h b/libobs/obs-missing-files.h new file mode 100644 index 0000000000000000000000000000000000000000..6e626ff35b13f7aa5019ba677f6dd50529d955da --- /dev/null +++ b/libobs/obs-missing-files.h @@ -0,0 +1,60 @@ +/****************************************************************************** + Copyright (C) 2019 by Dillon Pentz + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "util/c99defs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*obs_missing_file_cb)(void *src, const char *new_path, + void *data); + +struct obs_missing_file; +struct obs_missing_files; +typedef struct obs_missing_file obs_missing_file_t; +typedef struct obs_missing_files obs_missing_files_t; + +enum obs_missing_file_src { OBS_MISSING_FILE_SOURCE, OBS_MISSING_FILE_SCRIPT }; + +EXPORT obs_missing_files_t *obs_missing_files_create(); +EXPORT obs_missing_file_t *obs_missing_file_create(const char *path, + obs_missing_file_cb callback, + int src_type, void *src, + void *data); + +EXPORT void obs_missing_files_add_file(obs_missing_files_t *files, + obs_missing_file_t *file); +EXPORT size_t obs_missing_files_count(obs_missing_files_t *files); +EXPORT obs_missing_file_t * +obs_missing_files_get_file(obs_missing_files_t *files, int idx); +EXPORT void obs_missing_files_destroy(obs_missing_files_t *files); +EXPORT void obs_missing_files_append(obs_missing_files_t *dst, + obs_missing_files_t *src); + +EXPORT void obs_missing_file_issue_callback(obs_missing_file_t *file, + const char *new_path); +EXPORT const char *obs_missing_file_get_path(obs_missing_file_t *file); +EXPORT const char *obs_missing_file_get_source_name(obs_missing_file_t *file); +EXPORT void obs_missing_file_release(obs_missing_file_t *file); +EXPORT void obs_missing_file_destroy(obs_missing_file_t *file); + +#ifdef __cplusplus +} +#endif diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 657e1d39b57205248aa7a16b756ca8376a1b7c7f..6da3170dc68baf7a52fec501b486db7699c91d8e 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -844,6 +844,28 @@ obs_properties_t *obs_get_source_properties(const char *id) return NULL; } +obs_missing_files_t *obs_source_get_missing_files(const obs_source_t *source) +{ + if (!obs_source_valid(source, "obs_source_get_missing_files")) + return obs_missing_files_create(); + + if (source->info.missing_files) { + return source->info.missing_files(source->context.data); + } + + return obs_missing_files_create(); +} + +void obs_source_replace_missing_file(obs_missing_file_cb cb, + obs_source_t *source, const char *new_path, + void *data) +{ + if (!obs_source_valid(source, "obs_source_replace_missing_file")) + return; + + cb(source->context.data, new_path, data); +} + bool obs_is_source_configurable(const char *id) { const struct obs_source_info *info = get_source_info(id); diff --git a/libobs/obs-source.h b/libobs/obs-source.h index ec4194fc23a1f9fdee65c69d7fdc40e8ac7f4150..2ae0b182f411f5d82df9ce72ef0bf88ecda02238 100644 --- a/libobs/obs-source.h +++ b/libobs/obs-source.h @@ -532,6 +532,9 @@ struct obs_source_info { /* version-related stuff */ uint32_t version; /* increment if needed to specify a new version */ const char *unversioned_id; /* set internally, don't set manually */ + + /** Missing files **/ + obs_missing_files_t *(*missing_files)(void *data); }; EXPORT void obs_register_source_s(const struct obs_source_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index 29db567d4db8877bb329a8a7fec2194d760684dc..dd2a69fde4d2857c5b5887d15245e9be3d5aa671 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -68,6 +68,7 @@ typedef struct obs_weak_output obs_weak_output_t; typedef struct obs_weak_encoder obs_weak_encoder_t; typedef struct obs_weak_service obs_weak_service_t; +#include "obs-missing-files.h" #include "obs-source.h" #include "obs-encoder.h" #include "obs-output.h" @@ -909,6 +910,13 @@ EXPORT obs_data_t *obs_get_source_defaults(const char *id); /** Returns the property list, if any. Free with obs_properties_destroy */ EXPORT obs_properties_t *obs_get_source_properties(const char *id); +EXPORT obs_missing_files_t * +obs_source_get_missing_files(const obs_source_t *source); + +EXPORT void obs_source_replace_missing_file(obs_missing_file_cb cb, + obs_source_t *source, + const char *new_path, void *data); + /** Returns whether the source has custom properties or not */ EXPORT bool obs_is_source_configurable(const char *id); diff --git a/plugins/image-source/image-source.c b/plugins/image-source/image-source.c index 03787d271b18536a26d4151e093d996e99485be0..9f4c00867de8c1284c67030000fd84d3801f1028 100644 --- a/plugins/image-source/image-source.c +++ b/plugins/image-source/image-source.c @@ -264,6 +264,37 @@ uint64_t image_source_get_memory_usage(void *data) return s->if2.mem_usage; } +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + struct image_source *s = src; + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_set_string(settings, "file", new_path); + obs_source_update(source, settings); + obs_data_release(settings); + + UNUSED_PARAMETER(data); +} + +static obs_missing_files_t *image_source_missingfiles(void *data) +{ + struct image_source *s = data; + obs_missing_files_t *files = obs_missing_files_create(); + + if (strcmp(s->file, "") != 0) { + if (!os_file_exists(s->file)) { + obs_missing_file_t *file = obs_missing_file_create( + s->file, missing_file_callback, + OBS_MISSING_FILE_SOURCE, s->source, NULL); + + obs_missing_files_add_file(files, file); + } + } + + return files; +} + static struct obs_source_info image_source_info = { .id = "image_source", .type = OBS_SOURCE_TYPE_INPUT, @@ -279,6 +310,7 @@ static struct obs_source_info image_source_info = { .get_height = image_source_getheight, .video_render = image_source_render, .video_tick = image_source_tick, + .missing_files = image_source_missingfiles, .get_properties = image_source_properties, .icon_type = OBS_ICON_TYPE_IMAGE, }; diff --git a/plugins/image-source/obs-slideshow.c b/plugins/image-source/obs-slideshow.c index 145141153412cfc0fd4df1b7e7c89241f3cfd419..98adae075d397f89c98b2080bc9c1c978f28e98e 100644 --- a/plugins/image-source/obs-slideshow.c +++ b/plugins/image-source/obs-slideshow.c @@ -958,6 +958,71 @@ static void ss_deactivate(void *data) ss->pause_on_deactivate = true; } +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + struct slideshow *s = src; + const char *orig_path = data; + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_array_t *files = obs_data_get_array(settings, S_FILES); + + size_t l = obs_data_array_count(files); + for (size_t i = 0; i < l; i++) { + obs_data_t *file = obs_data_array_item(files, i); + const char *path = obs_data_get_string(file, "value"); + + if (strcmp(path, orig_path) == 0) { + obs_data_set_string(file, "value", new_path); + + obs_data_release(file); + break; + } + + obs_data_release(file); + } + + obs_source_update(source, settings); + + obs_data_array_release(files); + obs_data_release(settings); +} + +static obs_missing_files_t *ss_missingfiles(void *data) +{ + struct slideshow *s = data; + obs_missing_files_t *missing_files = obs_missing_files_create(); + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_array_t *files = obs_data_get_array(settings, S_FILES); + + size_t l = obs_data_array_count(files); + for (size_t i = 0; i < l; i++) { + obs_data_t *item = obs_data_array_item(files, i); + const char *path = obs_data_get_string(item, "value"); + + if (strcmp(path, "") != 0) { + if (!os_file_exists(path)) { + obs_missing_file_t *file = + obs_missing_file_create( + path, missing_file_callback, + OBS_MISSING_FILE_SOURCE, source, + (void *)path); + + obs_missing_files_add_file(missing_files, file); + } + } + + obs_data_release(item); + } + + obs_data_array_release(files); + obs_data_release(settings); + + return missing_files; +} + struct obs_source_info slideshow_info = { .id = "slideshow", .type = OBS_SOURCE_TYPE_INPUT, @@ -977,6 +1042,7 @@ struct obs_source_info slideshow_info = { .get_height = ss_height, .get_defaults = ss_defaults, .get_properties = ss_properties, + .missing_files = ss_missingfiles, .icon_type = OBS_ICON_TYPE_SLIDESHOW, .media_play_pause = ss_play_pause, .media_restart = ss_restart, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-source.c b/plugins/obs-ffmpeg/obs-ffmpeg-source.c index 48a44986e790207b1860f2abf840bc0fdd0087f9..06672202f908b6a31e77dddc9ada473e90d7c5d6 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-source.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-source.c @@ -727,6 +727,37 @@ static enum obs_media_state ffmpeg_source_get_state(void *data) return s->state; } +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + struct ffmpeg_source *s = src; + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_set_string(settings, "local_file", new_path); + obs_source_update(source, settings); + obs_data_release(settings); + + UNUSED_PARAMETER(data); +} + +static obs_missing_files_t *ffmpeg_source_missingfiles(void *data) +{ + struct ffmpeg_source *s = data; + obs_missing_files_t *files = obs_missing_files_create(); + + if (s->is_local_file && strcmp(s->input, "") != 0) { + if (!os_file_exists(s->input)) { + obs_missing_file_t *file = obs_missing_file_create( + s->input, missing_file_callback, + OBS_MISSING_FILE_SOURCE, s->source, NULL); + + obs_missing_files_add_file(files, file); + } + } + + return files; +} + struct obs_source_info ffmpeg_source = { .id = "ffmpeg_source", .type = OBS_SOURCE_TYPE_INPUT, @@ -741,6 +772,7 @@ struct obs_source_info ffmpeg_source = { .activate = ffmpeg_source_activate, .deactivate = ffmpeg_source_deactivate, .video_tick = ffmpeg_source_tick, + .missing_files = ffmpeg_source_missingfiles, .update = ffmpeg_source_update, .icon_type = OBS_ICON_TYPE_MEDIA, .media_play_pause = ffmpeg_source_play_pause, diff --git a/plugins/obs-text/gdiplus/obs-text.cpp b/plugins/obs-text/gdiplus/obs-text.cpp index 29f2181467f0e29b3dba03bb879614f2f6ba5543..484fb43603d12656ad7d2c53a2bdb7cff13162bc 100644 --- a/plugins/obs-text/gdiplus/obs-text.cpp +++ b/plugins/obs-text/gdiplus/obs-text.cpp @@ -1093,6 +1093,19 @@ static void defaults(obs_data_t *settings, int ver) obs_data_release(font_obj); }; +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + TextSource *s = reinterpret_cast(src); + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_set_string(settings, S_FILE, new_path); + obs_source_update(source, settings); + obs_data_release(settings); + + UNUSED_PARAMETER(data); +} + bool obs_module_load(void) { obs_source_info si = {}; @@ -1126,6 +1139,32 @@ bool obs_module_load(void) si.video_render = [](void *data, gs_effect_t *) { reinterpret_cast(data)->Render(); }; + si.missing_files = [](void *data) { + TextSource *s = reinterpret_cast(data); + obs_missing_files_t *files = obs_missing_files_create(); + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + + bool read = obs_data_get_bool(settings, S_USE_FILE); + const char *path = obs_data_get_string(settings, S_FILE); + + if (read && strcmp(path, "") != 0) { + if (!os_file_exists(path)) { + obs_missing_file_t *file = + obs_missing_file_create( + path, missing_file_callback, + OBS_MISSING_FILE_SOURCE, + s->source, NULL); + + obs_missing_files_add_file(files, file); + } + } + + obs_data_release(settings); + + return files; + }; obs_source_info si_v2 = si; si_v2.version = 2; diff --git a/plugins/text-freetype2/text-freetype2.c b/plugins/text-freetype2/text-freetype2.c index 573d2029155b23d94d5b4913d0064885b0f9dec4..cf831ea996b29750005aa38d754f9413a4a8a536 100644 --- a/plugins/text-freetype2/text-freetype2.c +++ b/plugins/text-freetype2/text-freetype2.c @@ -70,6 +70,7 @@ static struct obs_source_info freetype2_source_info_v2 = { .video_render = ft2_source_render, .video_tick = ft2_video_tick, .get_properties = ft2_source_properties, + .missing_files = ft2_missing_files, .icon_type = OBS_ICON_TYPE_TEXT, }; @@ -555,3 +556,42 @@ static void *ft2_source_create_v2(obs_data_t *settings, obs_source_t *source) { return ft2_source_create(settings, source, 2); } + +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + struct ft2_source *s = src; + + obs_source_t *source = s->src; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_set_string(settings, "text_file", new_path); + obs_source_update(source, settings); + obs_data_release(settings); + + UNUSED_PARAMETER(data); +} + +static obs_missing_files_t *ft2_missing_files(void *data) +{ + struct ft2_source *s = data; + obs_missing_files_t *files = obs_missing_files_create(); + + obs_source_t *source = s->src; + obs_data_t *settings = obs_source_get_settings(source); + + bool read = obs_data_get_bool(settings, "from_file"); + const char *path = obs_data_get_string(settings, "text_file"); + + if (read && strcmp(path, "") != 0) { + if (!os_file_exists(path)) { + obs_missing_file_t *file = obs_missing_file_create( + path, missing_file_callback, + OBS_MISSING_FILE_SOURCE, s->src, NULL); + + obs_missing_files_add_file(files, file); + } + } + + obs_data_release(settings); + + return files; +} diff --git a/plugins/text-freetype2/text-freetype2.h b/plugins/text-freetype2/text-freetype2.h index bae0cbb91577d55c7ff106a916e32bcfa6bd33bd..ff7e5b4cae578af957f5238270f026227aae3692 100644 --- a/plugins/text-freetype2/text-freetype2.h +++ b/plugins/text-freetype2/text-freetype2.h @@ -88,6 +88,8 @@ static obs_properties_t *ft2_source_properties(void *unused); static const char *ft2_source_get_name(void *unused); +static obs_missing_files_t *ft2_missing_files(void *data); + uint32_t get_ft2_text_width(wchar_t *text, struct ft2_source *srcdata); time_t get_modified_timestamp(char *filename); diff --git a/plugins/vlc-video/vlc-video-source.c b/plugins/vlc-video/vlc-video-source.c index dce6d471d87b92bf4ad202b99bf088ccb4181ebe..c663601c37b162af76e67638f2c2841c2e237e34 100644 --- a/plugins/vlc-video/vlc-video-source.c +++ b/plugins/vlc-video/vlc-video-source.c @@ -1086,6 +1086,71 @@ static obs_properties_t *vlcs_properties(void *data) return ppts; } +static void missing_file_callback(void *src, const char *new_path, void *data) +{ + struct vlc_source *s = src; + const char *orig_path = data; + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_array_t *files = obs_data_get_array(settings, S_PLAYLIST); + + size_t l = obs_data_array_count(files); + for (size_t i = 0; i < l; i++) { + obs_data_t *file = obs_data_array_item(files, i); + const char *path = obs_data_get_string(file, "value"); + + if (strcmp(path, orig_path) == 0) { + obs_data_set_string(file, "value", new_path); + + obs_data_release(file); + break; + } + + obs_data_release(file); + } + + obs_source_update(source, settings); + + obs_data_array_release(files); + obs_data_release(settings); +} + +static obs_missing_files_t *vlcs_missingfiles(void *data) +{ + struct vlc_source *s = data; + obs_missing_files_t *missing_files = obs_missing_files_create(); + + obs_source_t *source = s->source; + obs_data_t *settings = obs_source_get_settings(source); + obs_data_array_t *files = obs_data_get_array(settings, S_PLAYLIST); + + size_t l = obs_data_array_count(files); + for (size_t i = 0; i < l; i++) { + obs_data_t *item = obs_data_array_item(files, i); + const char *path = obs_data_get_string(item, "value"); + + if (strcmp(path, "") != 0) { + if (!os_file_exists(path)) { + obs_missing_file_t *file = + obs_missing_file_create( + path, missing_file_callback, + OBS_MISSING_FILE_SOURCE, source, + (void *)path); + + obs_missing_files_add_file(missing_files, file); + } + } + + obs_data_release(item); + } + + obs_data_array_release(files); + obs_data_release(settings); + + return missing_files; +} + struct obs_source_info vlc_source_info = { .id = "vlc_source", .type = OBS_SOURCE_TYPE_INPUT, @@ -1100,6 +1165,7 @@ struct obs_source_info vlc_source_info = { .get_properties = vlcs_properties, .activate = vlcs_activate, .deactivate = vlcs_deactivate, + .missing_files = vlcs_missingfiles, .icon_type = OBS_ICON_TYPE_MEDIA, .media_play_pause = vlcs_play_pause, .media_restart = vlcs_restart,