23 #include "demomanager.h"
25 #include "configuration/doomseekerconfig.h"
26 #include "fileutils.h"
28 #include "gui/commongui.h"
29 #include "gui/createserver/wadspicker.h"
30 #include "gui/createserverdialog.h"
31 #include "gui/demometadatadialog.h"
32 #include "pathfinder/pathfinder.h"
33 #include "pathfinder/wadpathfinder.h"
34 #include "plugins/enginedefaults.h"
35 #include "plugins/engineplugin.h"
36 #include "plugins/pluginloader.h"
37 #include "serverapi/gamecreateparams.h"
38 #include "serverapi/gameexeretriever.h"
39 #include "serverapi/gamehost.h"
40 #include "serverapi/message.h"
41 #include "serverapi/server.h"
42 #include "templatedpathresolver.h"
43 #include "ui_demomanager.h"
46 #include <QFileDialog>
48 #include <QMessageBox>
49 #include <QModelIndex>
52 #include <QStandardItemModel>
53 #include <QStandardPaths>
60 class DemoModel :
public QStandardItemModel
77 ManagedNameRole = Qt::UserRole + 1,
78 FilePathRole = Qt::UserRole + 2,
81 DemoModel(QObject *parent) : QStandardItemModel(parent)
83 setHorizontalHeaderLabels({
"", tr(
"Created Time"), tr(
"Author"), tr(
"IWAD"), tr(
"WADs")});
86 void addGameDemo(
const GameDemo &demo)
88 auto gameinfo = this->gameInfo(demo.
game);
89 auto *itemGame =
new QStandardItem;
90 itemGame->setIcon(gameinfo.icon);
91 itemGame->setToolTip(gameinfo.name);
93 auto *itemCreatedTime =
new QStandardItem;
94 itemCreatedTime->setData(demo.
time, Qt::EditRole);
95 itemCreatedTime->setToolTip(demo.
time.toString());
97 auto *itemAuthor =
new QStandardItem(demo.
author);
98 itemAuthor->setToolTip(demo.
author);
100 auto *itemIwad =
new QStandardItem(demo.
iwad);
101 itemIwad->setToolTip(demo.
iwad);
107 ? (
"[" + wad.
name() +
"]")
110 auto *itemWads =
new QStandardItem(wads.join(
"; "));
111 itemWads->setToolTip(wads.join(
"\n"));
113 QStandardItem *itemData = itemGame;
114 itemData->setData(demo.
managedName(), ManagedNameRole);
115 itemData->setData(demo.
demopath, FilePathRole);
117 appendRow(QList<QStandardItem *> {itemGame, itemCreatedTime, itemAuthor, itemIwad, itemWads});
120 QString demoNameFromIndex(
const QModelIndex &index)
const
122 return index.isValid() ? demoNameFromRow(index.row()) : QString();
125 QString demoNameFromRow(
int row)
const
127 if (row >= this->rowCount())
129 auto *item = this->item(row, ColData);
130 return item->data(FilePathRole).toString();
135 this->removeRows(0, this->rowCount());
144 static GameInfo unknown(QString name)
146 return GameInfo {gUnknownEngineIcon(), name};
151 static const int GAMES_LIMIT = 10000;
153 mutable QMap<QString, GameInfo> games;
155 GameInfo gameInfo(
const QString &name)
const
157 if (this->games.contains(name))
158 return this->games[name];
159 if (this->games.size() > GAMES_LIMIT)
163 return GameInfo::unknown(name);
165 GameInfo gameinfo = gameInfoFromPlugin(name);
166 this->games[name] = gameinfo;
170 static GameInfo gameInfoFromPlugin(
const QString &name)
173 return plugin !=
nullptr
174 ? GameInfo {plugin->icon(), plugin->data()->name}
175 : GameInfo::unknown(name);
180 DClass<DemoManagerDlg> :
public Ui::DemoManagerDlg
184 DemoModel *demoModel;
185 QPointer<QSplitter> splitter;
187 QModelIndex currentIndex()
const
189 return this->demoTable->selectionModel()->currentIndex();
192 QString askForExportPath(QWidget *parent, QString proposedName)
const
195 gConfig.doomseeker.previousDemoExportDir,
196 QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first());
199 QFileDialog saveDialog(parent);
200 saveDialog.setAcceptMode(QFileDialog::AcceptSave);
201 saveDialog.selectFile(proposedName);
202 saveDialog.setDirectory(proposedDir);
203 if (saveDialog.exec() == QDialog::Accepted)
205 exportPath = saveDialog.selectedFiles().first();
206 gConfig.doomseeker.previousDemoExportDir = QFileInfo(exportPath).path();
220 d->splitter =
new QSplitter(Qt::Horizontal,
this);
221 d->splitter->addWidget(d->demoTable);
222 d->splitter->addWidget(d->previewArea);
223 d->splitter->setStretchFactor(0, 1);
224 d->horizontalLayout->addWidget(d->splitter);
225 d->splitter->restoreState(gConfig.doomseeker.demoManagerSplitterState);
227 auto *menu =
new QMenu(
this);
228 menu->addAction(DemoManagerDlg::tr(
"Export plain demo file"),
229 this, SLOT(exportSelectedPlain()));
230 menu->addAction(DemoManagerDlg::tr(
"Export with Doomseeker metadata ..."),
231 this, SLOT(exportSelectedDoomseeker()));
232 d->btnExport->setMenu(menu);
234 d->demoModel =
new DemoModel(
this);
235 d->demoTable->setModel(d->demoModel);
236 d->demoTable->setColumnWidth(DemoModel::ColGame, 24);
238 auto *header = d->demoTable->horizontalHeader();
239 header->setSectionResizeMode(DemoModel::ColCreatedTime, QHeaderView::ResizeToContents);
240 header->setSectionResizeMode(DemoModel::ColGame, QHeaderView::ResizeToContents);
243 d->demoTable->sortByColumn(DemoModel::ColCreatedTime, Qt::DescendingOrder);
245 updateUiSelectionState();
246 connect(d->demoTable->selectionModel(), &QItemSelectionModel::currentChanged,
247 this, &DemoManagerDlg::itemSelected);
250 DemoManagerDlg::~DemoManagerDlg()
252 gConfig.doomseeker.demoManagerSplitterState = d->splitter->saveState();
255 void DemoManagerDlg::adjustDemoList()
257 d->demoModel->removeAll();
258 QStringList demos = d->demoStore.listManagedDemos();
259 d->demoTable->setSortingEnabled(
false);
260 for (
const QString &demoName : demos)
262 GameDemo demo = d->demoStore.loadManagedGameDemo(demoName);
263 d->demoModel->addGameDemo(demo);
265 d->demoTable->setSortingEnabled(
true);
266 d->demoTable->resizeRowsToContents();
269 bool DemoManagerDlg::doRemoveDemo(
const QString &file)
271 if (!d->demoStore.removeManagedDemo(file))
272 QMessageBox::critical(
this, tr(
"Unable to remove"), tr(
"Could not remove the selected demo."));
275 updateUiSelectionState();
281 void DemoManagerDlg::deleteSelected()
283 if (QMessageBox::question(
this, tr(
"Remove demo?"),
284 tr(
"Are you sure you want to remove the selected demo?"),
285 QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes)
287 QModelIndex index = d->currentIndex();
290 const QString &demoName = d->demoModel->demoNameFromIndex(index);
291 if (doRemoveDemo(demoName))
293 d->demoModel->removeRow(index.row());
299 void DemoManagerDlg::exportSelectedDoomseeker()
301 QString demoName = d->demoModel->demoNameFromIndex(d->demoTable->currentIndex());
302 if (demoName.isEmpty())
304 GameDemo selectedDemo = d->demoStore.loadManagedGameDemo(demoName);
308 QString exportPath = d->askForExportPath(
this, selectedDemo.
exportedName());
309 if (exportPath.isEmpty())
312 QFile demoFile(selectedDemo.
demopath);
316 QFile destDemo(exportPath);
319 bool demoExists = destDemo.exists();
320 bool metaExists = destMeta.exists();
321 if (demoExists || metaExists)
323 QStringList existingPaths;
325 existingPaths << destDemo.fileName();
327 existingPaths << destMeta.fileName();
328 auto answer = QMessageBox::question(
this, tr(
"Demo export"),
329 tr(
"The exported files already exist at the specified "
330 "directory. Overwrite?\n\n%1").arg(existingPaths.join(
"\n")));
331 if (answer == QMessageBox::No)
342 if (!demoFile.copy(destDemo.fileName()))
348 if (!fail && metaFile.exists())
350 if (!metaFile.copy(destMeta.fileName()))
360 QMessageBox::critical(
this, tr(
"Unable to save"), tr(
"Could not write to the specified location."));
365 void DemoManagerDlg::exportSelectedPlain()
367 QString demoName = d->demoModel->demoNameFromIndex(d->demoTable->currentIndex());
368 if (demoName.isEmpty())
370 GameDemo selectedDemo = d->demoStore.loadManagedGameDemo(demoName);
374 QString exportPath = d->askForExportPath(
this, selectedDemo.
exportedName());
375 if (exportPath.isEmpty())
379 if (QFile::exists(exportPath))
380 QFile::remove(exportPath);
381 if (!QFile::copy(selectedDemo.
demopath, exportPath))
383 QMessageBox::critical(
this, tr(
"Unable to save"),
384 tr(
"Could not write to the specified location."));
388 void DemoManagerDlg::importDemo()
392 demoExtensions <<
"*.ini";
395 gConfig.doomseeker.previousDemoExportDir,
396 QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first());
398 QString selectedFile = QFileDialog::getOpenFileName(
this, tr(
"Import demo"),
400 tr(
"Demo files (%1)").arg(demoExtensions.join(
" ")));
401 if (selectedFile.isEmpty())
404 gConfig.doomseeker.previousDemoExportDir = QFileInfo(selectedFile).path();
410 importDialog.setWindowTitle(tr(
"Import game demo"));
411 if (importDialog.exec() == QDialog::Accepted)
413 GameDemo importedDemo = importDialog.gameDemo();
419 QFileInfo existingDemoFile(existingDemo.
demopath);
420 QFileInfo importedDemoFile(importedDemo.
demopath);
422 QByteArray existingMd5 = FileUtils::md5(existingDemo.
demopath);
423 QByteArray importedMd5 = FileUtils::md5(importedDemo.
demopath);
426 if (existingDemoFile.size() == importedDemoFile.size()
427 && existingMd5 == importedMd5)
429 question = tr(
"It looks like this demo is already imported. Overwrite anyway?");
433 question = tr(
"Another demo with this metadata already exists. Overwrite?");
435 QString comparison = tr(
438 .arg(labelDemoFile(existingDemoFile, existingMd5))
439 .arg(labelDemoFile(importedDemoFile, importedMd5));
441 auto answer = QMessageBox::question(
this, tr(
"Demo import"),
442 tr(
"%1\n\n%2").arg(question, comparison));
443 if (answer == QMessageBox::No)
449 if (!d->demoStore.importDemo(importedDemo))
451 QMessageBox::critical(
this, tr(
"Demo import"),
452 tr(
"Could not import the selected demo."));
463 QString DemoManagerDlg::labelDemoFile(QFileInfo &file,
const QByteArray &md5)
465 return tr(
"%1 %L2 B (MD5: %3)")
466 .arg(file.fileName())
468 .arg(QString::fromLatin1(md5.toHex().toLower()));
471 void DemoManagerDlg::itemDoubleClicked(
const QModelIndex &index)
473 QString demoName = d->demoModel->demoNameFromIndex(index);
474 if (!demoName.isEmpty())
478 void DemoManagerDlg::playSelected()
480 QString demoName = d->demoModel->demoNameFromIndex(d->demoTable->currentIndex());
481 if (!demoName.isEmpty())
485 void DemoManagerDlg::playDemo(
const QString &demoName)
487 GameDemo demo = d->demoStore.loadManagedGameDemo(demoName);
491 if (plugin ==
nullptr)
493 QMessageBox::critical(
this, tr(
"No plugin"),
494 tr(
"The \"%1\" plugin does not appear to be loaded.").arg(demo.
game));
508 QStringList missingWads;
509 QList<PickedGameFile> wadPaths;
511 QList<PWad> wads = demo.
wads;
514 for (
const PWad &wad : wads)
517 if (findResult.isValid())
520 missingWads << wad.
name();
527 if (!missingWads.isEmpty())
529 QMessageBox::critical(
this, tr(
"Files not found"),
530 tr(
"The following files could not be located: ") + missingWads.join(
", "));
534 const QString iwad = wadPaths[0].path;
535 const QList<PickedGameFile> pwads = wadPaths.mid(1);
538 csd.setAttribute(Qt::WA_DeleteOnClose,
false);
539 csd.makeDemoPlaybackSetupDialog(plugin, demo, iwad, pwads);
543 void DemoManagerDlg::itemSelected(
const QModelIndex &index)
545 updateUiSelectionState();
548 void DemoManagerDlg::updateUiSelectionState()
552 bool selected = d->demoTable->currentIndex().isValid();
553 d->btnPlay->setEnabled(selected);
554 d->btnDelete->setEnabled(selected);
555 d->btnExport->setEnabled(selected);
558 void DemoManagerDlg::updatePreview()
560 QString demoName = d->demoModel->demoNameFromIndex(d->demoTable->currentIndex());
561 if (demoName.isEmpty())
563 d->preview->setText(
"");
566 GameDemo selectedDemo = d->demoStore.loadManagedGameDemo(demoName);
568 static const QString PAR =
"<p style=\"margin: 0px 0px 0px 10px\">";
569 static const QString ENDPAR =
"</p>";
571 static const auto header = [](
const QString &text) {
return "<b>" + text +
":</b>"; };
575 QString gameName = plugin !=
nullptr ? plugin->data()->name : selectedDemo.
game;
577 QString text = header(tr(
"Game")) + PAR + gameName;
579 text += QString(
" (%1)").arg(selectedDemo.
gameVersion);
581 if (!selectedDemo.
author.isEmpty())
583 text += header(tr(
"Author")) + PAR + selectedDemo.
author + ENDPAR;
585 text += header(tr(
"WADs / mods")) + PAR;
586 text += selectedDemo.
iwad +
"<br />";
587 for (
const PWad &wad : selectedDemo.
wads)
590 text +=
"[" + wad.
name() +
"]";
596 d->preview->setText(text);
599 #include "demomanager.moc"