window-importer.cpp 15 KB

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