ImporterModel.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /******************************************************************************
  2. Copyright (C) 2019-2020 by Dillon Pentz <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "moc_window-importer.cpp"
  15. #include "obs-app.hpp"
  16. #include <QPushButton>
  17. #include <QLineEdit>
  18. #include <QToolButton>
  19. #include <QMimeData>
  20. #include <QStyledItemDelegate>
  21. #include <QDirIterator>
  22. #include <QDropEvent>
  23. #include <qt-wrappers.hpp>
  24. #include "importers/importers.hpp"
  25. extern bool SceneCollectionExists(const char *findName);
  26. enum ImporterColumn {
  27. Selected,
  28. Name,
  29. Path,
  30. Program,
  31. Count
  32. };
  33. enum ImporterEntryRole { EntryStateRole = Qt::UserRole, NewPath, AutoPath, CheckEmpty };
  34. /**********************************************************
  35. Delegate - Presents cells in the grid.
  36. **********************************************************/
  37. ImporterEntryPathItemDelegate::ImporterEntryPathItemDelegate() : QStyledItemDelegate() {}
  38. QWidget *ImporterEntryPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */,
  39. const QModelIndex &index) const
  40. {
  41. bool empty = index.model()
  42. ->index(index.row(), ImporterColumn::Path)
  43. .data(ImporterEntryRole::CheckEmpty)
  44. .value<bool>();
  45. QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding,
  46. QSizePolicy::ControlType::PushButton);
  47. QWidget *container = new QWidget(parent);
  48. auto browseCallback = [this, container]() {
  49. const_cast<ImporterEntryPathItemDelegate *>(this)->handleBrowse(container);
  50. };
  51. auto clearCallback = [this, container]() {
  52. const_cast<ImporterEntryPathItemDelegate *>(this)->handleClear(container);
  53. };
  54. QHBoxLayout *layout = new QHBoxLayout();
  55. layout->setContentsMargins(0, 0, 0, 0);
  56. layout->setSpacing(0);
  57. QLineEdit *text = new QLineEdit();
  58. text->setObjectName(QStringLiteral("text"));
  59. text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding,
  60. QSizePolicy::ControlType::LineEdit));
  61. layout->addWidget(text);
  62. QObject::connect(text, &QLineEdit::editingFinished, this, &ImporterEntryPathItemDelegate::updateText);
  63. QToolButton *browseButton = new QToolButton();
  64. browseButton->setText("...");
  65. browseButton->setSizePolicy(buttonSizePolicy);
  66. layout->addWidget(browseButton);
  67. container->connect(browseButton, &QToolButton::clicked, browseCallback);
  68. // The "clear" button is not shown in output cells
  69. // or the insertion point's input cell.
  70. if (!empty) {
  71. QToolButton *clearButton = new QToolButton();
  72. clearButton->setText("X");
  73. clearButton->setSizePolicy(buttonSizePolicy);
  74. layout->addWidget(clearButton);
  75. container->connect(clearButton, &QToolButton::clicked, clearCallback);
  76. }
  77. container->setLayout(layout);
  78. container->setFocusProxy(text);
  79. return container;
  80. }
  81. void ImporterEntryPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
  82. {
  83. QLineEdit *text = editor->findChild<QLineEdit *>();
  84. text->setText(index.data().toString());
  85. editor->setProperty(PATH_LIST_PROP, QVariant());
  86. }
  87. void ImporterEntryPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
  88. const QModelIndex &index) const
  89. {
  90. // We use the PATH_LIST_PROP property to pass a list of
  91. // path strings from the editor widget into the model's
  92. // NewPathsToProcessRole. This is only used when paths
  93. // are selected through the "browse" or "delete" buttons
  94. // in the editor. If the user enters new text in the
  95. // text box, we simply pass that text on to the model
  96. // as normal text data in the default role.
  97. QVariant pathListProp = editor->property(PATH_LIST_PROP);
  98. if (pathListProp.isValid()) {
  99. QStringList list = editor->property(PATH_LIST_PROP).toStringList();
  100. model->setData(index, list, ImporterEntryRole::NewPath);
  101. } else {
  102. QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
  103. model->setData(index, lineEdit->text());
  104. }
  105. }
  106. void ImporterEntryPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
  107. const QModelIndex &index) const
  108. {
  109. QStyleOptionViewItem localOption = option;
  110. initStyleOption(&localOption, index);
  111. QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter);
  112. }
  113. void ImporterEntryPathItemDelegate::handleBrowse(QWidget *container)
  114. {
  115. QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
  116. QLineEdit *text = container->findChild<QLineEdit *>();
  117. QString currentPath = text->text();
  118. bool isSet = false;
  119. QStringList paths = OpenFiles(container, QTStr("Importer.SelectCollection"), currentPath,
  120. QTStr("Importer.Collection") + QString(" ") + Pattern);
  121. if (!paths.empty()) {
  122. container->setProperty(PATH_LIST_PROP, paths);
  123. isSet = true;
  124. }
  125. if (isSet)
  126. emit commitData(container);
  127. }
  128. void ImporterEntryPathItemDelegate::handleClear(QWidget *container)
  129. {
  130. // An empty string list will indicate that the entry is being
  131. // blanked and should be deleted.
  132. container->setProperty(PATH_LIST_PROP, QStringList());
  133. emit commitData(container);
  134. }
  135. void ImporterEntryPathItemDelegate::updateText()
  136. {
  137. QLineEdit *lineEdit = dynamic_cast<QLineEdit *>(sender());
  138. QWidget *editor = lineEdit->parentWidget();
  139. emit commitData(editor);
  140. }
  141. /**
  142. Model
  143. **/
  144. int ImporterModel::rowCount(const QModelIndex &) const
  145. {
  146. return options.length() + 1;
  147. }
  148. int ImporterModel::columnCount(const QModelIndex &) const
  149. {
  150. return ImporterColumn::Count;
  151. }
  152. QVariant ImporterModel::data(const QModelIndex &index, int role) const
  153. {
  154. QVariant result = QVariant();
  155. if (index.row() >= options.length()) {
  156. if (role == ImporterEntryRole::CheckEmpty)
  157. result = true;
  158. else
  159. return QVariant();
  160. } else if (role == Qt::DisplayRole) {
  161. switch (index.column()) {
  162. case ImporterColumn::Path:
  163. result = options[index.row()].path;
  164. break;
  165. case ImporterColumn::Program:
  166. result = options[index.row()].program;
  167. break;
  168. case ImporterColumn::Name:
  169. result = options[index.row()].name;
  170. }
  171. } else if (role == Qt::EditRole) {
  172. if (index.column() == ImporterColumn::Name) {
  173. result = options[index.row()].name;
  174. }
  175. } else if (role == Qt::CheckStateRole) {
  176. switch (index.column()) {
  177. case ImporterColumn::Selected:
  178. if (options[index.row()].program != "")
  179. result = options[index.row()].selected ? Qt::Checked : Qt::Unchecked;
  180. else
  181. result = Qt::Unchecked;
  182. }
  183. } else if (role == ImporterEntryRole::CheckEmpty) {
  184. result = options[index.row()].empty;
  185. }
  186. return result;
  187. }
  188. Qt::ItemFlags ImporterModel::flags(const QModelIndex &index) const
  189. {
  190. Qt::ItemFlags flags = QAbstractTableModel::flags(index);
  191. if (index.column() == ImporterColumn::Selected && index.row() != options.length()) {
  192. flags |= Qt::ItemIsUserCheckable;
  193. } else if (index.column() == ImporterColumn::Path ||
  194. (index.column() == ImporterColumn::Name && index.row() != options.length())) {
  195. flags |= Qt::ItemIsEditable;
  196. }
  197. return flags;
  198. }
  199. void ImporterModel::checkInputPath(int row)
  200. {
  201. ImporterEntry &entry = options[row];
  202. if (entry.path.isEmpty()) {
  203. entry.program = "";
  204. entry.empty = true;
  205. entry.selected = false;
  206. entry.name = "";
  207. } else {
  208. entry.empty = false;
  209. std::string program = DetectProgram(entry.path.toStdString());
  210. entry.program = QTStr(program.c_str());
  211. if (program.empty()) {
  212. entry.selected = false;
  213. } else {
  214. std::string name = GetSCName(entry.path.toStdString(), program);
  215. entry.name = name.c_str();
  216. }
  217. }
  218. emit dataChanged(index(row, 0), index(row, ImporterColumn::Count));
  219. }
  220. bool ImporterModel::setData(const QModelIndex &index, const QVariant &value, int role)
  221. {
  222. if (role == ImporterEntryRole::NewPath) {
  223. QStringList list = value.toStringList();
  224. if (list.size() == 0) {
  225. if (index.row() < options.size()) {
  226. beginRemoveRows(QModelIndex(), index.row(), index.row());
  227. options.removeAt(index.row());
  228. endRemoveRows();
  229. }
  230. } else {
  231. if (list.size() > 0 && index.row() < options.length()) {
  232. options[index.row()].path = list[0];
  233. checkInputPath(index.row());
  234. list.removeAt(0);
  235. }
  236. if (list.size() > 0) {
  237. int row = index.row();
  238. int lastRow = row + list.size() - 1;
  239. beginInsertRows(QModelIndex(), row, lastRow);
  240. for (QString path : list) {
  241. ImporterEntry entry;
  242. entry.path = path;
  243. options.insert(row, entry);
  244. row++;
  245. }
  246. endInsertRows();
  247. for (row = index.row(); row <= lastRow; row++) {
  248. checkInputPath(row);
  249. }
  250. }
  251. }
  252. } else if (index.row() == options.length()) {
  253. QString path = value.toString();
  254. if (!path.isEmpty()) {
  255. ImporterEntry entry;
  256. entry.path = path;
  257. entry.selected = role != ImporterEntryRole::AutoPath;
  258. entry.empty = false;
  259. beginInsertRows(QModelIndex(), options.length() + 1, options.length() + 1);
  260. options.append(entry);
  261. endInsertRows();
  262. checkInputPath(index.row());
  263. }
  264. } else if (index.column() == ImporterColumn::Selected) {
  265. bool select = value.toBool();
  266. options[index.row()].selected = select;
  267. } else if (index.column() == ImporterColumn::Path) {
  268. QString path = value.toString();
  269. options[index.row()].path = path;
  270. checkInputPath(index.row());
  271. } else if (index.column() == ImporterColumn::Name) {
  272. QString name = value.toString();
  273. options[index.row()].name = name;
  274. }
  275. emit dataChanged(index, index);
  276. return true;
  277. }
  278. QVariant ImporterModel::headerData(int section, Qt::Orientation orientation, int role) const
  279. {
  280. QVariant result = QVariant();
  281. if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) {
  282. switch (section) {
  283. case ImporterColumn::Path:
  284. result = QTStr("Importer.Path");
  285. break;
  286. case ImporterColumn::Program:
  287. result = QTStr("Importer.Program");
  288. break;
  289. case ImporterColumn::Name:
  290. result = QTStr("Name");
  291. }
  292. }
  293. return result;
  294. }
  295. /**
  296. Window
  297. **/
  298. OBSImporter::OBSImporter(QWidget *parent) : QDialog(parent), optionsModel(new ImporterModel), ui(new Ui::OBSImporter)
  299. {
  300. setAcceptDrops(true);
  301. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  302. ui->setupUi(this);
  303. ui->tableView->setModel(optionsModel);
  304. ui->tableView->setItemDelegateForColumn(ImporterColumn::Path, new ImporterEntryPathItemDelegate());
  305. ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents);
  306. ui->tableView->horizontalHeader()->setSectionResizeMode(ImporterColumn::Path, QHeaderView::ResizeMode::Stretch);
  307. connect(optionsModel, &ImporterModel::dataChanged, this, &OBSImporter::dataChanged);
  308. ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged);
  309. ui->buttonBox->button(QDialogButtonBox::Ok)->setText(QTStr("Import"));
  310. ui->buttonBox->button(QDialogButtonBox::Open)->setText(QTStr("Add"));
  311. connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this,
  312. &OBSImporter::importCollections);
  313. connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, this, &OBSImporter::browseImport);
  314. connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &OBSImporter::close);
  315. ImportersInit();
  316. bool autoSearchPrompt = config_get_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt");
  317. if (!autoSearchPrompt) {
  318. QMessageBox::StandardButton button = OBSMessageBox::question(
  319. parent, QTStr("Importer.AutomaticCollectionPrompt"), QTStr("Importer.AutomaticCollectionText"));
  320. if (button == QMessageBox::Yes) {
  321. config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", true);
  322. } else {
  323. config_set_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch", false);
  324. }
  325. config_set_bool(App()->GetUserConfig(), "General", "AutoSearchPrompt", true);
  326. }
  327. bool autoSearch = config_get_bool(App()->GetUserConfig(), "General", "AutomaticCollectionSearch");
  328. OBSImporterFiles f;
  329. if (autoSearch)
  330. f = ImportersFindFiles();
  331. for (size_t i = 0; i < f.size(); i++) {
  332. QString path = f[i].c_str();
  333. path.replace("\\", "/");
  334. addImportOption(path, true);
  335. }
  336. f.clear();
  337. ui->tableView->resizeColumnsToContents();
  338. QModelIndex index = optionsModel->createIndex(optionsModel->rowCount() - 1, 2);
  339. QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection,
  340. Q_ARG(const QModelIndex &, index));
  341. }
  342. void OBSImporter::addImportOption(QString path, bool automatic)
  343. {
  344. QStringList list;
  345. list.append(path);
  346. QModelIndex insertIndex = optionsModel->index(optionsModel->rowCount() - 1, ImporterColumn::Path);
  347. optionsModel->setData(insertIndex, list, automatic ? ImporterEntryRole::AutoPath : ImporterEntryRole::NewPath);
  348. }
  349. void OBSImporter::dropEvent(QDropEvent *ev)
  350. {
  351. for (QUrl url : ev->mimeData()->urls()) {
  352. QFileInfo fileInfo(url.toLocalFile());
  353. if (fileInfo.isDir()) {
  354. QDirIterator dirIter(fileInfo.absoluteFilePath(), QDir::Files);
  355. while (dirIter.hasNext()) {
  356. addImportOption(dirIter.next(), false);
  357. }
  358. } else {
  359. addImportOption(fileInfo.canonicalFilePath(), false);
  360. }
  361. }
  362. }
  363. void OBSImporter::dragEnterEvent(QDragEnterEvent *ev)
  364. {
  365. if (ev->mimeData()->hasUrls())
  366. ev->accept();
  367. }
  368. void OBSImporter::browseImport()
  369. {
  370. QString Pattern = "(*.json *.bpres *.xml *.xconfig)";
  371. QStringList paths = OpenFiles(this, QTStr("Importer.SelectCollection"), "",
  372. QTStr("Importer.Collection") + QString(" ") + Pattern);
  373. if (!paths.empty()) {
  374. for (int i = 0; i < paths.count(); i++) {
  375. addImportOption(paths[i], false);
  376. }
  377. }
  378. }
  379. bool GetUnusedName(std::string &name)
  380. {
  381. OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
  382. if (!basic->GetSceneCollectionByName(name)) {
  383. return false;
  384. }
  385. std::string newName;
  386. int inc = 2;
  387. do {
  388. newName = name;
  389. newName += " ";
  390. newName += std::to_string(inc++);
  391. } while (basic->GetSceneCollectionByName(newName));
  392. name = newName;
  393. return true;
  394. }
  395. constexpr std::string_view OBSSceneCollectionPath = "obs-studio/basic/scenes/";
  396. void OBSImporter::importCollections()
  397. {
  398. setEnabled(false);
  399. const std::filesystem::path sceneCollectionLocation =
  400. App()->userScenesLocation / std::filesystem::u8path(OBSSceneCollectionPath);
  401. for (int i = 0; i < optionsModel->rowCount() - 1; i++) {
  402. int selected = optionsModel->index(i, ImporterColumn::Selected).data(Qt::CheckStateRole).value<int>();
  403. if (selected == Qt::Unchecked)
  404. continue;
  405. std::string pathStr = optionsModel->index(i, ImporterColumn::Path)
  406. .data(Qt::DisplayRole)
  407. .value<QString>()
  408. .toStdString();
  409. std::string nameStr = optionsModel->index(i, ImporterColumn::Name)
  410. .data(Qt::DisplayRole)
  411. .value<QString>()
  412. .toStdString();
  413. json11::Json res;
  414. ImportSC(pathStr, nameStr, res);
  415. if (res != json11::Json()) {
  416. json11::Json::object out = res.object_items();
  417. std::string name = res["name"].string_value();
  418. std::string file;
  419. if (GetUnusedName(name)) {
  420. json11::Json::object newOut = out;
  421. newOut["name"] = name;
  422. out = newOut;
  423. }
  424. std::string fileName;
  425. if (!GetFileSafeName(name.c_str(), fileName)) {
  426. blog(LOG_WARNING, "Failed to create safe file name for '%s'", fileName.c_str());
  427. }
  428. std::string collectionFile;
  429. collectionFile.reserve(sceneCollectionLocation.u8string().size() + fileName.size());
  430. collectionFile.append(sceneCollectionLocation.u8string()).append(fileName);
  431. if (!GetClosestUnusedFileName(collectionFile, "json")) {
  432. blog(LOG_WARNING, "Failed to get closest file name for %s", fileName.c_str());
  433. }
  434. std::string out_str = json11::Json(out).dump();
  435. bool success = os_quick_write_utf8_file(collectionFile.c_str(), out_str.c_str(), out_str.size(),
  436. false);
  437. blog(LOG_INFO, "Import Scene Collection: %s (%s) - %s", name.c_str(), fileName.c_str(),
  438. success ? "SUCCESS" : "FAILURE");
  439. }
  440. }
  441. close();
  442. }
  443. void OBSImporter::dataChanged()
  444. {
  445. ui->tableView->resizeColumnToContents(ImporterColumn::Name);
  446. }