diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index ae881a3f6..ab10c577b 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 09205f7b2..e93094318 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 8a6fdf7bb..e7b9e5ab6 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 b9ab60a5a..6915ea83b 100644 --- a/UI/data/themes/Dark.qss +++ b/UI/data/themes/Dark.qss @@ -769,6 +769,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 000000000..01e87d795 --- /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 5ae927c40..7e069345f 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 125c5089e..93c935a54 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 000000000..93523b16e --- /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 000000000..afad76c71 --- /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 93b09b703..0f2a6d6a3 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 6ea33d713..677a71bd5 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" @@ -970,7 +971,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); @@ -1114,6 +1127,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 000000000..b320405cc --- /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 000000000..4e2144fee --- /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); +};