MissingFilesPathItemDelegate.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /******************************************************************************
  2. Copyright (C) 2019 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-missing-files.cpp"
  15. #include "window-basic-main.hpp"
  16. #include "obs-app.hpp"
  17. #include <QLineEdit>
  18. #include <QToolButton>
  19. #include <QFileDialog>
  20. #include <qt-wrappers.hpp>
  21. enum MissingFilesColumn {
  22. Source,
  23. OriginalPath,
  24. NewPath,
  25. State,
  26. Count
  27. };
  28. enum MissingFilesRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole };
  29. /**********************************************************
  30. Delegate - Presents cells in the grid.
  31. **********************************************************/
  32. MissingFilesPathItemDelegate::MissingFilesPathItemDelegate(bool isOutput, const QString &defaultPath)
  33. : QStyledItemDelegate(),
  34. isOutput(isOutput),
  35. defaultPath(defaultPath)
  36. {
  37. }
  38. QWidget *MissingFilesPathItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /* option */,
  39. const QModelIndex &) const
  40. {
  41. QSizePolicy buttonSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding,
  42. QSizePolicy::ControlType::PushButton);
  43. QWidget *container = new QWidget(parent);
  44. auto browseCallback = [this, container]() {
  45. const_cast<MissingFilesPathItemDelegate *>(this)->handleBrowse(container);
  46. };
  47. auto clearCallback = [this, container]() {
  48. const_cast<MissingFilesPathItemDelegate *>(this)->handleClear(container);
  49. };
  50. QHBoxLayout *layout = new QHBoxLayout();
  51. layout->setContentsMargins(0, 0, 0, 0);
  52. layout->setSpacing(0);
  53. QLineEdit *text = new QLineEdit();
  54. text->setObjectName(QStringLiteral("text"));
  55. text->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding,
  56. QSizePolicy::ControlType::LineEdit));
  57. layout->addWidget(text);
  58. QToolButton *browseButton = new QToolButton();
  59. browseButton->setText("...");
  60. browseButton->setSizePolicy(buttonSizePolicy);
  61. layout->addWidget(browseButton);
  62. container->connect(browseButton, &QToolButton::clicked, browseCallback);
  63. // The "clear" button is not shown in input cells
  64. if (isOutput) {
  65. QToolButton *clearButton = new QToolButton();
  66. clearButton->setText("X");
  67. clearButton->setSizePolicy(buttonSizePolicy);
  68. layout->addWidget(clearButton);
  69. container->connect(clearButton, &QToolButton::clicked, clearCallback);
  70. }
  71. container->setLayout(layout);
  72. container->setFocusProxy(text);
  73. return container;
  74. }
  75. void MissingFilesPathItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
  76. {
  77. QLineEdit *text = editor->findChild<QLineEdit *>();
  78. text->setText(index.data().toString());
  79. editor->setProperty(PATH_LIST_PROP, QVariant());
  80. }
  81. void MissingFilesPathItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
  82. const QModelIndex &index) const
  83. {
  84. // We use the PATH_LIST_PROP property to pass a list of
  85. // path strings from the editor widget into the model's
  86. // NewPathsToProcessRole. This is only used when paths
  87. // are selected through the "browse" or "delete" buttons
  88. // in the editor. If the user enters new text in the
  89. // text box, we simply pass that text on to the model
  90. // as normal text data in the default role.
  91. QVariant pathListProp = editor->property(PATH_LIST_PROP);
  92. if (pathListProp.isValid()) {
  93. QStringList list = editor->property(PATH_LIST_PROP).toStringList();
  94. if (isOutput) {
  95. model->setData(index, list);
  96. } else
  97. model->setData(index, list, MissingFilesRole::NewPathsToProcessRole);
  98. } else {
  99. QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
  100. model->setData(index, lineEdit->text(), 0);
  101. }
  102. }
  103. void MissingFilesPathItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
  104. const QModelIndex &index) const
  105. {
  106. QStyleOptionViewItem localOption = option;
  107. initStyleOption(&localOption, index);
  108. QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &localOption, painter);
  109. }
  110. void MissingFilesPathItemDelegate::handleBrowse(QWidget *container)
  111. {
  112. QLineEdit *text = container->findChild<QLineEdit *>();
  113. QString currentPath = text->text();
  114. if (currentPath.isEmpty() || currentPath.compare(QTStr("MissingFiles.Clear")) == 0)
  115. currentPath = defaultPath;
  116. bool isSet = false;
  117. if (isOutput) {
  118. QString newPath =
  119. QFileDialog::getOpenFileName(container, QTStr("MissingFiles.SelectFile"), currentPath, nullptr);
  120. #ifdef __APPLE__
  121. // TODO: Revisit when QTBUG-42661 is fixed
  122. container->window()->raise();
  123. #endif
  124. if (!newPath.isEmpty()) {
  125. container->setProperty(PATH_LIST_PROP, QStringList() << newPath);
  126. isSet = true;
  127. }
  128. }
  129. if (isSet)
  130. emit commitData(container);
  131. }
  132. void MissingFilesPathItemDelegate::handleClear(QWidget *container)
  133. {
  134. // An empty string list will indicate that the entry is being
  135. // blanked and should be deleted.
  136. container->setProperty(PATH_LIST_PROP, QStringList() << QTStr("MissingFiles.Clear"));
  137. container->findChild<QLineEdit *>()->clearFocus();
  138. ((QWidget *)container->parent())->setFocus();
  139. emit commitData(container);
  140. }
  141. /**
  142. Model
  143. **/
  144. MissingFilesModel::MissingFilesModel(QObject *parent) : QAbstractTableModel(parent)
  145. {
  146. QStyle *style = QApplication::style();
  147. warningIcon = style->standardIcon(QStyle::SP_MessageBoxWarning);
  148. }
  149. int MissingFilesModel::rowCount(const QModelIndex &) const
  150. {
  151. return files.length();
  152. }
  153. int MissingFilesModel::columnCount(const QModelIndex &) const
  154. {
  155. return MissingFilesColumn::Count;
  156. }
  157. int MissingFilesModel::found() const
  158. {
  159. int res = 0;
  160. for (int i = 0; i < files.length(); i++) {
  161. if (files[i].state != Missing && files[i].state != Cleared)
  162. res++;
  163. }
  164. return res;
  165. }
  166. QVariant MissingFilesModel::data(const QModelIndex &index, int role) const
  167. {
  168. QVariant result = QVariant();
  169. if (index.row() >= files.length()) {
  170. return QVariant();
  171. } else if (role == Qt::DisplayRole) {
  172. QFileInfo fi(files[index.row()].originalPath);
  173. switch (index.column()) {
  174. case MissingFilesColumn::Source:
  175. result = files[index.row()].source;
  176. break;
  177. case MissingFilesColumn::OriginalPath:
  178. result = fi.fileName();
  179. break;
  180. case MissingFilesColumn::NewPath:
  181. result = files[index.row()].newPath;
  182. break;
  183. case MissingFilesColumn::State:
  184. switch (files[index.row()].state) {
  185. case MissingFilesState::Missing:
  186. result = QTStr("MissingFiles.Missing");
  187. break;
  188. case MissingFilesState::Replaced:
  189. result = QTStr("MissingFiles.Replaced");
  190. break;
  191. case MissingFilesState::Found:
  192. result = QTStr("MissingFiles.Found");
  193. break;
  194. case MissingFilesState::Cleared:
  195. result = QTStr("MissingFiles.Cleared");
  196. break;
  197. }
  198. break;
  199. }
  200. } else if (role == Qt::DecorationRole && index.column() == MissingFilesColumn::Source) {
  201. OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
  202. OBSSourceAutoRelease source = obs_get_source_by_name(files[index.row()].source.toStdString().c_str());
  203. if (source) {
  204. result = main->GetSourceIcon(obs_source_get_id(source));
  205. }
  206. } else if (role == Qt::FontRole && index.column() == MissingFilesColumn::State) {
  207. QFont font = QFont();
  208. font.setBold(true);
  209. result = font;
  210. } else if (role == Qt::ToolTipRole && index.column() == MissingFilesColumn::State) {
  211. switch (files[index.row()].state) {
  212. case MissingFilesState::Missing:
  213. result = QTStr("MissingFiles.Missing");
  214. break;
  215. case MissingFilesState::Replaced:
  216. result = QTStr("MissingFiles.Replaced");
  217. break;
  218. case MissingFilesState::Found:
  219. result = QTStr("MissingFiles.Found");
  220. break;
  221. case MissingFilesState::Cleared:
  222. result = QTStr("MissingFiles.Cleared");
  223. break;
  224. default:
  225. break;
  226. }
  227. } else if (role == Qt::ToolTipRole) {
  228. switch (index.column()) {
  229. case MissingFilesColumn::OriginalPath:
  230. result = files[index.row()].originalPath;
  231. break;
  232. case MissingFilesColumn::NewPath:
  233. result = files[index.row()].newPath;
  234. break;
  235. default:
  236. break;
  237. }
  238. }
  239. return result;
  240. }
  241. Qt::ItemFlags MissingFilesModel::flags(const QModelIndex &index) const
  242. {
  243. Qt::ItemFlags flags = QAbstractTableModel::flags(index);
  244. if (index.column() == MissingFilesColumn::OriginalPath) {
  245. flags &= ~Qt::ItemIsEditable;
  246. } else if (index.column() == MissingFilesColumn::NewPath && index.row() != files.length()) {
  247. flags |= Qt::ItemIsEditable;
  248. }
  249. return flags;
  250. }
  251. void MissingFilesModel::fileCheckLoop(QList<MissingFileEntry> files, QString path, bool skipPrompt)
  252. {
  253. loop = false;
  254. QUrl url = QUrl().fromLocalFile(path);
  255. QString dir = url.toDisplayString(QUrl::RemoveScheme | QUrl::RemoveFilename | QUrl::PreferLocalFile);
  256. bool prompted = skipPrompt;
  257. for (int i = 0; i < files.length(); i++) {
  258. if (files[i].state != MissingFilesState::Missing)
  259. continue;
  260. QUrl origFile = QUrl().fromLocalFile(files[i].originalPath);
  261. QString filename = origFile.fileName();
  262. QString testFile = dir + filename;
  263. if (os_file_exists(testFile.toStdString().c_str())) {
  264. if (!prompted) {
  265. QMessageBox::StandardButton button =
  266. QMessageBox::question(nullptr, QTStr("MissingFiles.AutoSearch"),
  267. QTStr("MissingFiles.AutoSearchText"));
  268. if (button == QMessageBox::No)
  269. break;
  270. prompted = true;
  271. }
  272. QModelIndex in = index(i, MissingFilesColumn::NewPath);
  273. setData(in, testFile, 0);
  274. }
  275. }
  276. loop = true;
  277. }
  278. bool MissingFilesModel::setData(const QModelIndex &index, const QVariant &value, int role)
  279. {
  280. bool success = false;
  281. if (role == MissingFilesRole::NewPathsToProcessRole) {
  282. QStringList list = value.toStringList();
  283. int row = index.row() + 1;
  284. beginInsertRows(QModelIndex(), row, row);
  285. MissingFileEntry entry;
  286. entry.originalPath = list[0].replace("\\", "/");
  287. entry.source = list[1];
  288. files.insert(row, entry);
  289. row++;
  290. endInsertRows();
  291. success = true;
  292. } else {
  293. QString path = value.toString();
  294. if (index.column() == MissingFilesColumn::NewPath) {
  295. files[index.row()].newPath = value.toString();
  296. QString fileName = QUrl(path).fileName();
  297. QString origFileName = QUrl(files[index.row()].originalPath).fileName();
  298. if (path.isEmpty()) {
  299. files[index.row()].state = MissingFilesState::Missing;
  300. } else if (path.compare(QTStr("MissingFiles.Clear")) == 0) {
  301. files[index.row()].state = MissingFilesState::Cleared;
  302. } else if (fileName.compare(origFileName) == 0) {
  303. files[index.row()].state = MissingFilesState::Found;
  304. if (loop)
  305. fileCheckLoop(files, path, false);
  306. } else {
  307. files[index.row()].state = MissingFilesState::Replaced;
  308. if (loop)
  309. fileCheckLoop(files, path, false);
  310. }
  311. emit dataChanged(index, index);
  312. success = true;
  313. }
  314. }
  315. return success;
  316. }
  317. QVariant MissingFilesModel::headerData(int section, Qt::Orientation orientation, int role) const
  318. {
  319. QVariant result = QVariant();
  320. if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) {
  321. switch (section) {
  322. case MissingFilesColumn::State:
  323. result = QTStr("MissingFiles.State");
  324. break;
  325. case MissingFilesColumn::Source:
  326. result = QTStr("Basic.Main.Source");
  327. break;
  328. case MissingFilesColumn::OriginalPath:
  329. result = QTStr("MissingFiles.MissingFile");
  330. break;
  331. case MissingFilesColumn::NewPath:
  332. result = QTStr("MissingFiles.NewFile");
  333. break;
  334. }
  335. }
  336. return result;
  337. }
  338. OBSMissingFiles::OBSMissingFiles(obs_missing_files_t *files, QWidget *parent)
  339. : QDialog(parent),
  340. filesModel(new MissingFilesModel),
  341. ui(new Ui::OBSMissingFiles)
  342. {
  343. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  344. ui->setupUi(this);
  345. ui->tableView->setModel(filesModel);
  346. ui->tableView->setItemDelegateForColumn(MissingFilesColumn::OriginalPath,
  347. new MissingFilesPathItemDelegate(false, ""));
  348. ui->tableView->setItemDelegateForColumn(MissingFilesColumn::NewPath,
  349. new MissingFilesPathItemDelegate(true, ""));
  350. ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch);
  351. ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::Source,
  352. QHeaderView::ResizeMode::ResizeToContents);
  353. ui->tableView->horizontalHeader()->setMaximumSectionSize(width() / 3);
  354. ui->tableView->horizontalHeader()->setSectionResizeMode(MissingFilesColumn::State,
  355. QHeaderView::ResizeMode::ResizeToContents);
  356. ui->tableView->setEditTriggers(QAbstractItemView::EditTrigger::CurrentChanged);
  357. ui->warningIcon->setPixmap(filesModel->warningIcon.pixmap(QSize(32, 32)));
  358. for (size_t i = 0; i < obs_missing_files_count(files); i++) {
  359. obs_missing_file_t *f = obs_missing_files_get_file(files, (int)i);
  360. const char *oldPath = obs_missing_file_get_path(f);
  361. const char *name = obs_missing_file_get_source_name(f);
  362. addMissingFile(oldPath, name);
  363. }
  364. QString found = QTStr("MissingFiles.NumFound").arg("0", QString::number(obs_missing_files_count(files)));
  365. ui->found->setText(found);
  366. fileStore = files;
  367. connect(ui->doneButton, &QPushButton::clicked, this, &OBSMissingFiles::saveFiles);
  368. connect(ui->browseButton, &QPushButton::clicked, this, &OBSMissingFiles::browseFolders);
  369. connect(ui->cancelButton, &QPushButton::clicked, this, &OBSMissingFiles::close);
  370. connect(filesModel, &MissingFilesModel::dataChanged, this, &OBSMissingFiles::dataChanged);
  371. QModelIndex index = filesModel->createIndex(0, 1);
  372. QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex", Qt::QueuedConnection,
  373. Q_ARG(const QModelIndex &, index));
  374. }
  375. OBSMissingFiles::~OBSMissingFiles()
  376. {
  377. obs_missing_files_destroy(fileStore);
  378. }
  379. void OBSMissingFiles::addMissingFile(const char *originalPath, const char *sourceName)
  380. {
  381. QStringList list;
  382. list.append(originalPath);
  383. list.append(sourceName);
  384. QModelIndex insertIndex = filesModel->index(filesModel->rowCount() - 1, MissingFilesColumn::Source);
  385. filesModel->setData(insertIndex, list, MissingFilesRole::NewPathsToProcessRole);
  386. }
  387. void OBSMissingFiles::saveFiles()
  388. {
  389. for (int i = 0; i < filesModel->files.length(); i++) {
  390. MissingFilesState state = filesModel->files[i].state;
  391. if (state != MissingFilesState::Missing) {
  392. obs_missing_file_t *f = obs_missing_files_get_file(fileStore, i);
  393. QString path = filesModel->files[i].newPath;
  394. if (state == MissingFilesState::Cleared) {
  395. obs_missing_file_issue_callback(f, "");
  396. } else {
  397. char *p = bstrdup(path.toStdString().c_str());
  398. obs_missing_file_issue_callback(f, p);
  399. bfree(p);
  400. }
  401. }
  402. }
  403. QDialog::accept();
  404. }
  405. void OBSMissingFiles::browseFolders()
  406. {
  407. QString dir = QFileDialog::getExistingDirectory(this, QTStr("MissingFiles.SelectDir"), "",
  408. QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
  409. if (dir != "") {
  410. dir += "/";
  411. filesModel->fileCheckLoop(filesModel->files, dir, true);
  412. }
  413. }
  414. void OBSMissingFiles::dataChanged()
  415. {
  416. QString found =
  417. QTStr("MissingFiles.NumFound")
  418. .arg(QString::number(filesModel->found()), QString::number(obs_missing_files_count(fileStore)));
  419. ui->found->setText(found);
  420. ui->tableView->resizeColumnToContents(MissingFilesColumn::State);
  421. ui->tableView->resizeColumnToContents(MissingFilesColumn::Source);
  422. }
  423. QIcon OBSMissingFiles::GetWarningIcon()
  424. {
  425. return filesModel->warningIcon;
  426. }
  427. void OBSMissingFiles::SetWarningIcon(const QIcon &icon)
  428. {
  429. ui->warningIcon->setPixmap(icon.pixmap(QSize(32, 32)));
  430. filesModel->warningIcon = icon;
  431. }