diff --git a/UI/window-importer.cpp b/frontend/importer/ImporterEntryPathItemDelegate.cpp similarity index 100% rename from UI/window-importer.cpp rename to frontend/importer/ImporterEntryPathItemDelegate.cpp diff --git a/UI/window-importer.hpp b/frontend/importer/ImporterEntryPathItemDelegate.hpp similarity index 100% rename from UI/window-importer.hpp rename to frontend/importer/ImporterEntryPathItemDelegate.hpp diff --git a/frontend/importer/ImporterModel.cpp b/frontend/importer/ImporterModel.cpp new file mode 100644 index 000000000..88d4ad2d9 --- /dev/null +++ b/frontend/importer/ImporterModel.cpp @@ -0,0 +1,570 @@ +/****************************************************************************** + Copyright (C) 2019-2020 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 "moc_window-importer.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importers/importers.hpp" + +extern bool SceneCollectionExists(const char *findName); + +enum ImporterColumn { + Selected, + Name, + Path, + Program, + + Count +}; + +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} + +QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + bool empty = index.model() + ->index(index.row(), ImporterColumn::Path) + .data(ImporterEntryRole::CheckEmpty) + .value(); + + 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->setContentsMargins(0, 0, 0, 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); + + QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); + + 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 output cells + // or the insertion point's input cell. + if (!empty) { + 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); + return container; +} + +void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void ImporterEntryPathItemDelegate::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(); + model->setData(index, list, ImporterEntryRole::NewPath); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void ImporterEntryPathItemDelegate::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 ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + + bool isSet = false; + QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } + + if (isSet) + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::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()); + + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/** + Model +**/ + +int ImporterModel::rowCount(const QModelIndex &) const +{ + return options.length() + 1; +} + +int ImporterModel::columnCount(const QModelIndex &) const +{ + return ImporterColumn::Count; +} + +QVariant ImporterModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= options.length()) { + if (role == ImporterEntryRole::CheckEmpty) + result = true; + else + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case ImporterColumn::Path: + result = options[index.row()].path; + break; + case ImporterColumn::Program: + result = options[index.row()].program; + break; + case ImporterColumn::Name: + result = options[index.row()].name; + } + } else if (role == Qt::EditRole) { + if (index.column() == ImporterColumn::Name) { + result = options[index.row()].name; + } + } else if (role == Qt::CheckStateRole) { + switch (index.column()) { + case ImporterColumn::Selected: + if (options[index.row()].program != "") + result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; + else + result = Qt::Unchecked; + } + } else if (role == ImporterEntryRole::CheckEmpty) { + result = options[index.row()].empty; + } + + return result; +} + +Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { + flags |= Qt::ItemIsUserCheckable; + } else if (index.column() == ImporterColumn::Path || + (index.column() == ImporterColumn::Name && index.row() != options.length())) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void ImporterModel::checkInputPath(int row) +{ + ImporterEntry &entry = options[row]; + + if (entry.path.isEmpty()) { + entry.program = ""; + entry.empty = true; + entry.selected = false; + entry.name = ""; + } else { + entry.empty = false; + + std::string program = DetectProgram(entry.path.toStdString()); + entry.program = QTStr(program.c_str()); + + if (program.empty()) { + entry.selected = false; + } else { + std::string name = GetSCName(entry.path.toStdString(), program); + entry.name = name.c_str(); + } + } + + emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); +} + +bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == ImporterEntryRole::NewPath) { + QStringList list = value.toStringList(); + + if (list.size() == 0) { + if (index.row() < options.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + options.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (list.size() > 0 && index.row() < options.length()) { + options[index.row()].path = list[0]; + checkInputPath(index.row()); + + list.removeAt(0); + } + + if (list.size() > 0) { + int row = index.row(); + int lastRow = row + list.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : list) { + ImporterEntry entry; + entry.path = path; + + options.insert(row, entry); + + row++; + } + + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + } + } + } else if (index.row() == options.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + ImporterEntry entry; + entry.path = path; + entry.selected = role != ImporterEntryRole::AutoPath; + entry.empty = false; + + beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); + options.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + } + } else if (index.column() == ImporterColumn::Selected) { + bool select = value.toBool(); + + options[index.row()].selected = select; + } else if (index.column() == ImporterColumn::Path) { + QString path = value.toString(); + options[index.row()].path = path; + + checkInputPath(index.row()); + } else if (index.column() == ImporterColumn::Name) { + QString name = value.toString(); + options[index.row()].name = name; + } + + emit dataChanged(index, index); + + return true; +} + +QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case ImporterColumn::Path: + result = QTStr("Importer.Path"); + break; + case ImporterColumn::Program: + result = QTStr("Importer.Program"); + break; + case ImporterColumn::Name: + result = QTStr("Name"); + } + } + + return result; +} + +/** + Window +**/ + +OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(optionsModel); + ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); + + connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); + + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); + ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, + &OBSImporter::importCollections); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); + + ImportersInit(); + + bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); + + if (!autoSearchPrompt) { + QMessageBox::StandardButton button = OBSMessageBox::question( + parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); + + if (button == QMessageBox::Yes) { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); + } else { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); + } + + config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); + } + + bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + + OBSImporterFiles f; + if (autoSearch) + f = ImportersFindFiles(); + + for (size_t i = 0; i < f.size(); i++) { + QString path = f[i].c_str(); + path.replace("\\", "/"); + addImportOption(path, true); + } + + f.clear(); + + ui->tableView->resizeColumnsToContents(); + + QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +void OBSImporter::addImportOption(QString path, bool automatic) +{ + QStringList list; + + list.append(path); + + QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); + + optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); +} + +void OBSImporter::dropEvent(QDropEvent *ev) +{ + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + if (fileInfo.isDir()) { + + QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); + + while (dirIter.hasNext()) { + addImportOption(dirIter.next(), false); + } + } else { + addImportOption(fileInfo.canonicalFilePath(), false); + } + } +} + +void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls()) + ev->accept(); +} + +void OBSImporter::browseImport() +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + for (int i = 0; i < paths.count(); i++) { + addImportOption(paths[i], false); + } + } +} + +bool GetUnusedName(std::string &name) +{ + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { + return false; + } + + std::string newName; + int inc = 2; + do { + newName = name; + newName += " "; + newName += std::to_string(inc++); + } while (basic->GetSceneCollectionByName(newName)); + + name = newName; + return true; +} + +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + +void OBSImporter::importCollections() +{ + setEnabled(false); + + const std::filesystem::path sceneCollectionLocation = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); + + for (int i = 0; i < optionsModel->rowCount() - 1; i++) { + int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); + + if (selected == Qt::Unchecked) + continue; + + std::string pathStr = optionsModel->index(i, ImporterColumn::Path) + .data(Qt::DisplayRole) + .value() + .toStdString(); + std::string nameStr = optionsModel->index(i, ImporterColumn::Name) + .data(Qt::DisplayRole) + .value() + .toStdString(); + + json11::Json res; + ImportSC(pathStr, nameStr, res); + + if (res != json11::Json()) { + json11::Json::object out = res.object_items(); + std::string name = res["name"].string_value(); + std::string file; + + if (GetUnusedName(name)) { + json11::Json::object newOut = out; + newOut["name"] = name; + out = newOut; + } + + std::string fileName; + if (!GetFileSafeName(name.c_str(), fileName)) { + blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); + } + + std::string collectionFile; + collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); + collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); + } + + std::string out_str = json11::Json(out).dump(); + + bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), + false); + + blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), + success ? "SUCCESS" : "FAILURE"); + } + } + + close(); +} + +void OBSImporter::dataChanged() +{ + ui->tableView->resizeColumnToContents(ImporterColumn::Name); +} diff --git a/frontend/importer/ImporterModel.hpp b/frontend/importer/ImporterModel.hpp new file mode 100644 index 000000000..8e77db645 --- /dev/null +++ b/frontend/importer/ImporterModel.hpp @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2019-2020 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 "obs-app.hpp" +#include "window-basic-main.hpp" +#include +#include +#include +#include "ui_OBSImporter.h" + +class ImporterModel; + +class OBSImporter : public QDialog { + Q_OBJECT + + QPointer optionsModel; + std::unique_ptr ui; + +public: + explicit OBSImporter(QWidget *parent = nullptr); + + void addImportOption(QString path, bool automatic); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + +public slots: + void browseImport(); + void importCollections(); + void dataChanged(); +}; + +class ImporterModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSImporter; + +public: + ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) 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); + +private: + struct ImporterEntry { + QString path; + QString program; + QString name; + + bool selected; + bool empty; + }; + + QList options; + + void checkInputPath(int row); +}; + +class ImporterEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + ImporterEntryPathItemDelegate(); + + 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: + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/frontend/importer/OBSImporter.cpp b/frontend/importer/OBSImporter.cpp new file mode 100644 index 000000000..88d4ad2d9 --- /dev/null +++ b/frontend/importer/OBSImporter.cpp @@ -0,0 +1,570 @@ +/****************************************************************************** + Copyright (C) 2019-2020 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 "moc_window-importer.cpp" + +#include "obs-app.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importers/importers.hpp" + +extern bool SceneCollectionExists(const char *findName); + +enum ImporterColumn { + Selected, + Name, + Path, + Program, + + Count +}; + +enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty }; + +/********************************************************** + Delegate - Presents cells in the grid. +**********************************************************/ + +ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {} + +QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */, + const QModelIndex &index) const +{ + bool empty = index.model() + ->index(index.row(), ImporterColumn::Path) + .data(ImporterEntryRole::CheckEmpty) + .value(); + + 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->setContentsMargins(0, 0, 0, 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); + + QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText); + + 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 output cells + // or the insertion point's input cell. + if (!empty) { + 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); + return container; +} + +void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QLineEdit *text = editor->findChild(); + text->setText(index.data().toString()); + editor->setProperty(PATH_LIST_PROP, QVariant()); +} + +void ImporterEntryPathItemDelegate::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(); + model->setData(index, list, ImporterEntryRole::NewPath); + } else { + QLineEdit *lineEdit = editor->findChild(); + model->setData(index, lineEdit->text()); + } +} + +void ImporterEntryPathItemDelegate::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 ImporterEntryPathItemDelegate::handleBrowse(QWidget *container) +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QLineEdit *text = container->findChild(); + + QString currentPath = text->text(); + + bool isSet = false; + QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath, + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + container->setProperty(PATH_LIST_PROP, paths); + isSet = true; + } + + if (isSet) + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::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()); + + emit commitData(container); +} + +void ImporterEntryPathItemDelegate::updateText() +{ + QLineEdit *lineEdit = dynamic_cast(sender()); + QWidget *editor = lineEdit->parentWidget(); + emit commitData(editor); +} + +/** + Model +**/ + +int ImporterModel::rowCount(const QModelIndex &) const +{ + return options.length() + 1; +} + +int ImporterModel::columnCount(const QModelIndex &) const +{ + return ImporterColumn::Count; +} + +QVariant ImporterModel::data(const QModelIndex &index, int role) const +{ + QVariant result = QVariant(); + + if (index.row() >= options.length()) { + if (role == ImporterEntryRole::CheckEmpty) + result = true; + else + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case ImporterColumn::Path: + result = options[index.row()].path; + break; + case ImporterColumn::Program: + result = options[index.row()].program; + break; + case ImporterColumn::Name: + result = options[index.row()].name; + } + } else if (role == Qt::EditRole) { + if (index.column() == ImporterColumn::Name) { + result = options[index.row()].name; + } + } else if (role == Qt::CheckStateRole) { + switch (index.column()) { + case ImporterColumn::Selected: + if (options[index.row()].program != "") + result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked; + else + result = Qt::Unchecked; + } + } else if (role == ImporterEntryRole::CheckEmpty) { + result = options[index.row()].empty; + } + + return result; +} + +Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (index.column() == ImporterColumn::Selected && index.row() != options.length()) { + flags |= Qt::ItemIsUserCheckable; + } else if (index.column() == ImporterColumn::Path || + (index.column() == ImporterColumn::Name && index.row() != options.length())) { + flags |= Qt::ItemIsEditable; + } + + return flags; +} + +void ImporterModel::checkInputPath(int row) +{ + ImporterEntry &entry = options[row]; + + if (entry.path.isEmpty()) { + entry.program = ""; + entry.empty = true; + entry.selected = false; + entry.name = ""; + } else { + entry.empty = false; + + std::string program = DetectProgram(entry.path.toStdString()); + entry.program = QTStr(program.c_str()); + + if (program.empty()) { + entry.selected = false; + } else { + std::string name = GetSCName(entry.path.toStdString(), program); + entry.name = name.c_str(); + } + } + + emit dataChanged(index(row, 0), index(row, ImporterColumn::Count)); +} + +bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == ImporterEntryRole::NewPath) { + QStringList list = value.toStringList(); + + if (list.size() == 0) { + if (index.row() < options.size()) { + beginRemoveRows(QModelIndex(), index.row(), index.row()); + options.removeAt(index.row()); + endRemoveRows(); + } + } else { + if (list.size() > 0 && index.row() < options.length()) { + options[index.row()].path = list[0]; + checkInputPath(index.row()); + + list.removeAt(0); + } + + if (list.size() > 0) { + int row = index.row(); + int lastRow = row + list.size() - 1; + beginInsertRows(QModelIndex(), row, lastRow); + + for (QString path : list) { + ImporterEntry entry; + entry.path = path; + + options.insert(row, entry); + + row++; + } + + endInsertRows(); + + for (row = index.row(); row <= lastRow; row++) { + checkInputPath(row); + } + } + } + } else if (index.row() == options.length()) { + QString path = value.toString(); + + if (!path.isEmpty()) { + ImporterEntry entry; + entry.path = path; + entry.selected = role != ImporterEntryRole::AutoPath; + entry.empty = false; + + beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1); + options.append(entry); + endInsertRows(); + + checkInputPath(index.row()); + } + } else if (index.column() == ImporterColumn::Selected) { + bool select = value.toBool(); + + options[index.row()].selected = select; + } else if (index.column() == ImporterColumn::Path) { + QString path = value.toString(); + options[index.row()].path = path; + + checkInputPath(index.row()); + } else if (index.column() == ImporterColumn::Name) { + QString name = value.toString(); + options[index.row()].name = name; + } + + emit dataChanged(index, index); + + return true; +} + +QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + QVariant result = QVariant(); + + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) { + switch (section) { + case ImporterColumn::Path: + result = QTStr("Importer.Path"); + break; + case ImporterColumn::Program: + result = QTStr("Importer.Program"); + break; + case ImporterColumn::Name: + result = QTStr("Name"); + } + } + + return result; +} + +/** + Window +**/ + +OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter) +{ + setAcceptDrops(true); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + ui->setupUi(this); + + ui->tableView->setModel(optionsModel); + ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate()); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch); + + connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged); + + ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import")); + ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add")); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, + &OBSImporter::importCollections); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close); + + ImportersInit(); + + bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt"); + + if (!autoSearchPrompt) { + QMessageBox::StandardButton button = OBSMessageBox::question( + parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText")); + + if (button == QMessageBox::Yes) { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true); + } else { + config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false); + } + + config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true); + } + + bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch"); + + OBSImporterFiles f; + if (autoSearch) + f = ImportersFindFiles(); + + for (size_t i = 0; i < f.size(); i++) { + QString path = f[i].c_str(); + path.replace("\\", "/"); + addImportOption(path, true); + } + + f.clear(); + + ui->tableView->resizeColumnsToContents(); + + QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2); + QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection, + Q_ARG(const QModelIndex &, index)); +} + +void OBSImporter::addImportOption(QString path, bool automatic) +{ + QStringList list; + + list.append(path); + + QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path); + + optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath); +} + +void OBSImporter::dropEvent(QDropEvent *ev) +{ + for (QUrl url : ev->mimeData()->urls()) { + QFileInfo fileInfo(url.toLocalFile()); + if (fileInfo.isDir()) { + + QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files); + + while (dirIter.hasNext()) { + addImportOption(dirIter.next(), false); + } + } else { + addImportOption(fileInfo.canonicalFilePath(), false); + } + } +} + +void OBSImporter::dragEnterEvent(QDragEnterEvent *ev) +{ + if (ev->mimeData()->hasUrls()) + ev->accept(); +} + +void OBSImporter::browseImport() +{ + QString Pattern = "(*.json *.bpres *.xml *.xconfig)"; + + QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "", + QTStr("Importer.Collection") + QString(" ") + Pattern); + + if (!paths.empty()) { + for (int i = 0; i < paths.count(); i++) { + addImportOption(paths[i], false); + } + } +} + +bool GetUnusedName(std::string &name) +{ + OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + if (!basic->GetSceneCollectionByName(name)) { + return false; + } + + std::string newName; + int inc = 2; + do { + newName = name; + newName += " "; + newName += std::to_string(inc++); + } while (basic->GetSceneCollectionByName(newName)); + + name = newName; + return true; +} + +constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/"; + +void OBSImporter::importCollections() +{ + setEnabled(false); + + const std::filesystem::path sceneCollectionLocation = + App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath); + + for (int i = 0; i < optionsModel->rowCount() - 1; i++) { + int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value(); + + if (selected == Qt::Unchecked) + continue; + + std::string pathStr = optionsModel->index(i, ImporterColumn::Path) + .data(Qt::DisplayRole) + .value() + .toStdString(); + std::string nameStr = optionsModel->index(i, ImporterColumn::Name) + .data(Qt::DisplayRole) + .value() + .toStdString(); + + json11::Json res; + ImportSC(pathStr, nameStr, res); + + if (res != json11::Json()) { + json11::Json::object out = res.object_items(); + std::string name = res["name"].string_value(); + std::string file; + + if (GetUnusedName(name)) { + json11::Json::object newOut = out; + newOut["name"] = name; + out = newOut; + } + + std::string fileName; + if (!GetFileSafeName(name.c_str(), fileName)) { + blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str()); + } + + std::string collectionFile; + collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size()); + collectionFile.append(sceneCollectionLocation.u8string()).append(fileName); + + if (!GetClosestUnusedFileName(collectionFile, "json")) { + blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str()); + } + + std::string out_str = json11::Json(out).dump(); + + bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(), + false); + + blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(), + success ? "SUCCESS" : "FAILURE"); + } + } + + close(); +} + +void OBSImporter::dataChanged() +{ + ui->tableView->resizeColumnToContents(ImporterColumn::Name); +} diff --git a/frontend/importer/OBSImporter.hpp b/frontend/importer/OBSImporter.hpp new file mode 100644 index 000000000..8e77db645 --- /dev/null +++ b/frontend/importer/OBSImporter.hpp @@ -0,0 +1,102 @@ +/****************************************************************************** + Copyright (C) 2019-2020 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 "obs-app.hpp" +#include "window-basic-main.hpp" +#include +#include +#include +#include "ui_OBSImporter.h" + +class ImporterModel; + +class OBSImporter : public QDialog { + Q_OBJECT + + QPointer optionsModel; + std::unique_ptr ui; + +public: + explicit OBSImporter(QWidget *parent = nullptr); + + void addImportOption(QString path, bool automatic); + +protected: + virtual void dropEvent(QDropEvent *ev) override; + virtual void dragEnterEvent(QDragEnterEvent *ev) override; + +public slots: + void browseImport(); + void importCollections(); + void dataChanged(); +}; + +class ImporterModel : public QAbstractTableModel { + Q_OBJECT + + friend class OBSImporter; + +public: + ImporterModel(QObject *parent = 0) : QAbstractTableModel(parent) {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) 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); + +private: + struct ImporterEntry { + QString path; + QString program; + QString name; + + bool selected; + bool empty; + }; + + QList options; + + void checkInputPath(int row); +}; + +class ImporterEntryPathItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + ImporterEntryPathItemDelegate(); + + 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: + const char *PATH_LIST_PROP = "pathList"; + + void handleBrowse(QWidget *container); + void handleClear(QWidget *container); + +private slots: + void updateText(); +}; diff --git a/UI/importers/classic.cpp b/frontend/importers/classic.cpp similarity index 100% rename from UI/importers/classic.cpp rename to frontend/importers/classic.cpp diff --git a/UI/importers/importers.cpp b/frontend/importers/importers.cpp similarity index 100% rename from UI/importers/importers.cpp rename to frontend/importers/importers.cpp diff --git a/UI/importers/importers.hpp b/frontend/importers/importers.hpp similarity index 100% rename from UI/importers/importers.hpp rename to frontend/importers/importers.hpp diff --git a/UI/importers/sl.cpp b/frontend/importers/sl.cpp similarity index 100% rename from UI/importers/sl.cpp rename to frontend/importers/sl.cpp diff --git a/UI/importers/studio.cpp b/frontend/importers/studio.cpp similarity index 100% rename from UI/importers/studio.cpp rename to frontend/importers/studio.cpp diff --git a/UI/importers/xsplit.cpp b/frontend/importers/xsplit.cpp similarity index 100% rename from UI/importers/xsplit.cpp rename to frontend/importers/xsplit.cpp