window-importer.cpp 16 KB

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