wadseekerinterface.cpp
1 //------------------------------------------------------------------------------
2 // wadseekerinterface.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2009 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "gui/wadseekerinterface.h"
24 #include "ui_wadseekerinterface.h"
25 
26 #include "application.h"
27 #include "configuration/doomseekerconfig.h"
28 #include "gui/commongui.h"
29 #include "gui/helpers/taskbarbutton.h"
30 #include "gui/helpers/taskbarprogress.h"
31 #include "mainwindow.h"
32 #include "serverapi/server.h"
34 #include "strings.hpp"
35 #include "templatedpathresolver.h"
36 #include "wadseeker/entities/checksum.h"
37 #include "wadseeker/entities/modfile.h"
38 #include "wadseeker/entities/modset.h"
39 
40 #include <QFontDatabase>
41 #include <QMessageBox>
42 #include <QPushButton>
43 
44 #include <algorithm>
45 
46 namespace
47 {
48 struct LogLevel
49 {
50  QString tag;
51  QString style;
52 };
53 
54 class LogLevelMap
55 {
56 public:
57  LogLevelMap() {}
58 
59  LogLevel operator[](const WadseekerLib::MessageType type) const
60  {
61  return this->map.value(type, this->unknown);
62  }
63 
64  void insert(WadseekerLib::MessageType type, LogLevel level)
65  {
66  this->map.insert(type, level);
67  }
68 
69  void refit()
70  {
71  for (LogLevel &lvl : this->map)
72  lvl.tag = lvl.tag.trimmed();
73  int longest = 0;
74  for (const LogLevel &lvl : this->map)
75  longest = std::max<int>(lvl.tag.length(), longest);
76  for (LogLevel &lvl : this->map)
77  {
78  while (lvl.tag.length() < longest)
79  lvl.tag.prepend(' ');
80  }
81  this->unknown = LogLevel{ QString(longest, '?'), QString() };
82  }
83 
84 private:
85  QMap<WadseekerLib::MessageType, LogLevel> map;
86  LogLevel unknown;
87 };
88 };
89 
90 const int WadseekerInterface::UPDATE_INTERVAL_MS = 500;
91 WadseekerInterface *WadseekerInterface::currentInstance = nullptr;
92 
93 DClass<WadseekerInterface> : public Ui::WadseekerInterface
94 {
95 public:
96  QPushButton *btnAbort;
97  QPushButton *btnClose;
98 
99  QString name;
100  LogLevelMap loglevels;
101 
102  bool bCompletedSuccessfully;
103  bool preventGame;
104  TaskbarButton *taskbarButton;
105  TaskbarProgress *taskbarProgress;
106 };
107 
108 DPointered(WadseekerInterface)
109 
110 WadseekerInterface::WadseekerInterface(QWidget *parent)
111  : QDialog(parent)
112 {
113  construct();
114  bAutomatic = false;
115 }
116 
117 WadseekerInterface::WadseekerInterface(ServerPtr server, QWidget *parent)
118  : QDialog(parent)
119 {
120  construct();
121  setupAutomatic();
122  d->lblTop->show();
123  d->lblTop->setText(tr("Downloading WADs for server \"%1\"").arg(server->name()));
124  setCustomSites(server->allWebSites());
125 }
126 
127 WadseekerInterface::~WadseekerInterface()
128 {
129  currentInstance = nullptr;
130 }
131 
132 void WadseekerInterface::abortService(const QString &service)
133 {
134  appendLog(tr("Aborting service: %1").arg(service), WadseekerLib::Notice, d->name);
135  wadseeker.skipService(service);
136 }
137 
138 void WadseekerInterface::abortSite(const QUrl &url)
139 {
140  appendLog(tr("Aborting site: %1").arg(url.toString()), WadseekerLib::Notice, d->name);
141  wadseeker.skipSiteSeek(url);
142 }
143 
144 void WadseekerInterface::accept()
145 {
146  if (isAutomatic())
147  {
148  if (d->bCompletedSuccessfully)
149  done(QDialog::Accepted);
150  }
151  else
152  {
153  if (d->leWadName->text().isEmpty())
154  return;
155  else
156  {
157  seekedWads.clear();
158  QStringList pwadNames = d->leWadName->text().split(',', Qt::SkipEmptyParts);
159  for (QString pwadName : pwadNames)
160  {
161  seekedWads << pwadName.trimmed();
162  }
163  }
164  startSeeking(seekedWads);
165  }
166 }
167 
168 void WadseekerInterface::appendLog(const QString &message, WadseekerLib::MessageType type, const QString &source)
169 {
170  static const int MAX_SOURCE_LEN = QString("Doomseeker").length();
171  static const QString lineStart = "<span style=\"%1; white-space: pre-wrap;\">";
172  static const QString lineEnd = "</span><br>\n";
173  const LogLevel level = d->loglevels[type];
174 
175  QString log;
176  for (const QString &line : message.split("\n"))
177  {
178  log += lineStart.arg(level.style)
179  + QString("%1|%2| %3").arg(source.left(MAX_SOURCE_LEN), -MAX_SOURCE_LEN).arg(level.tag, line)
180  + lineEnd;
181  }
182 
183  d->teWadseekerOutput->moveCursor(QTextCursor::End);
184  d->teWadseekerOutput->insertHtml(log);
185 }
186 
187 void WadseekerInterface::allDone(bool bSuccess)
188 {
189  setStateWaiting();
190  d->bCompletedSuccessfully = bSuccess;
191  QApplication::alert(this);
192  if (bSuccess)
193  {
194  appendLog(tr("All done. Success."), WadseekerLib::NoticeImportant, d->name);
195 
196  if (isAutomatic() && !d->preventGame)
197  {
198  if (isActiveWindow())
199  done(QDialog::Accepted);
200  else
201  d->btnStartGame->show();
202  }
203  }
204  else
205  {
206  QList<PWad> failures = unsuccessfulWads();
207 
208  for (const PWad &failure : failures)
209  {
210  d->twWads->setFileFailed(failure.name());
211  }
212 
213  appendLog(tr("All done. Fail."), WadseekerLib::CriticalError, d->name);
214  }
215 }
216 
217 void WadseekerInterface::connectWadseekerObject()
218 {
219  // Connect Wadseeker to the dialog box.
220  connect(&wadseeker, &Wadseeker::allDone,
221  this, &WadseekerInterface::allDone);
222  connect(&wadseeker, &Wadseeker::message,
223  this, [this](const QString &msg, WadseekerLib::MessageType type)
224  { this->appendLog(msg, type, tr("Wadseeker")); });
225  connect(&wadseeker, &Wadseeker::seekStarted,
226  this, &WadseekerInterface::seekStarted);
227  connect(&wadseeker, &Wadseeker::fileInstalled,
228  this, &WadseekerInterface::fileDownloadSuccessful);
229  connect(&wadseeker, &Wadseeker::siteFinished,
230  this, &WadseekerInterface::siteFinished);
231  connect(&wadseeker, &Wadseeker::siteProgress,
232  this, &WadseekerInterface::siteProgress);
233  connect(&wadseeker, &Wadseeker::siteRedirect,
234  this, &WadseekerInterface::siteRedirect);
235  connect(&wadseeker, &Wadseeker::siteStarted,
236  this, &WadseekerInterface::siteStarted);
237  connect(&wadseeker, &Wadseeker::serviceStarted,
238  this, &WadseekerInterface::serviceStarted);
239  connect(&wadseeker, &Wadseeker::serviceFinished,
240  this, &WadseekerInterface::serviceFinished);
241 
242  // Connect Wadseeker to the WADs table widget.
243  connect(&wadseeker, &Wadseeker::fileDownloadFinished,
244  d->twWads, &WadseekerWadsTable::setFileDownloadFinished);
245  connect(&wadseeker, &Wadseeker::fileDownloadProgress,
246  d->twWads, &WadseekerWadsTable::setFileProgress);
247  connect(&wadseeker, &Wadseeker::fileDownloadStarted,
248  d->twWads, &WadseekerWadsTable::setFileUrl);
249 }
250 
251 void WadseekerInterface::construct()
252 {
253  d->setupUi(this);
254  CommonGUI::setupDialog(*this);
255  d->name = "Doomseeker";
256 
257  d->teWadseekerOutput->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
258  d->preventGame = false;
259  d->bCompletedSuccessfully = false;
260  d->btnAbort = d->buttonBox->button(QDialogButtonBox::Abort);
261  d->btnClose = d->buttonBox->button(QDialogButtonBox::Close);
262 
263  d->taskbarButton = new TaskbarButton(this);
264 
265  d->taskbarProgress = d->taskbarButton->progress();
266  d->taskbarProgress->setMaximum(d->pbOverallProgress->maximum());
267 
268  initMessageColors();
269 
270  setStateWaiting();
271 
272  this->setWindowIcon(QIcon(":/icon.png"));
273  d->btnStartGame->hide();
274  connect(&this->updateTimer, &QTimer::timeout,
275  this, &WadseekerInterface::registerUpdateRequest);
276 
277  connectWadseekerObject();
278 
279  // Connect tables.
280  connect(d->twWads, &QWidget::customContextMenuRequested,
281  this, &WadseekerInterface::showWadsTableContextMenu);
282 
283  bAutomatic = false;
284  bFirstShown = false;
285 
286  QStringList urlList = gConfig.wadseeker.searchURLs;
287  if (gConfig.wadseeker.bAlwaysUseDefaultSites)
288  {
289  for (int i = 0; !Wadseeker::defaultSites[i].isEmpty(); ++i)
290  urlList << Wadseeker::defaultSites[i];
291  }
292 
293  wadseeker.setPrimarySites(urlList);
294 
295  updateTimer.setSingleShot(false);
296  updateTimer.start(UPDATE_INTERVAL_MS);
297 }
298 
299 WadseekerInterface *WadseekerInterface::create(QWidget *parent)
300 {
301  if (!isInstantiated())
302  {
303  currentInstance = new WadseekerInterface(parent);
304  return currentInstance;
305  }
306  return nullptr;
307 }
308 
309 WadseekerInterface *WadseekerInterface::create(ServerPtr server, QWidget *parent)
310 {
311  if (!isInstantiated())
312  {
313  currentInstance = new WadseekerInterface(server, parent);
314  return currentInstance;
315  }
316  return nullptr;
317 }
318 
319 WadseekerInterface *WadseekerInterface::createAutoNoGame(QWidget *parent)
320 {
321  WadseekerInterface *interface = create(parent);
322  if (interface != nullptr)
323  {
324  interface->setupAutomatic();
325  interface->d->preventGame = true;
326  }
327  return interface;
328 }
329 
330 void WadseekerInterface::fileDownloadSuccessful(const ModFile &filename)
331 {
332  successfulWads << filename;
333  d->twWads->setFileSuccessful(filename.fileName());
334 }
335 
336 void WadseekerInterface::initMessageColors()
337 {
338  this->colorHtmlMessageNotice = gConfig.wadseeker.colorMessageNotice;
339  this->colorHtmlMessageError = gConfig.wadseeker.colorMessageError;
340  this->colorHtmlMessageFatalError = gConfig.wadseeker.colorMessageCriticalError;
341  this->colorHtmlMessageNavigation = gConfig.wadseeker.colorMessageNavigation;
342 
343  d->loglevels = LogLevelMap();
344  // The tags are translatable, but they should be kept short
345  // regardless of the translation. 5 characters max is best.
346  // Doomseeker refits all the tags to the same length, padding
347  // them with spaces from the left.
348  d->loglevels.insert(WadseekerLib::CriticalError, {
349  tr("CRIT"),
350  QString("color: %1; font-weight: bold;").arg(colorHtmlMessageFatalError),
351  });
352  d->loglevels.insert(WadseekerLib::Error, {
353  tr("ERROR"),
354  QString("color: %1;").arg(colorHtmlMessageError),
355  });
356  d->loglevels.insert(WadseekerLib::Notice, {
357  tr("INFO"),
358  QString("color: %1;").arg(colorHtmlMessageNotice),
359  });
360  d->loglevels.insert(WadseekerLib::NoticeImportant, {
361  tr("NOTI"),
362  QString("color: %1; font-weight: bold;").arg(colorHtmlMessageNotice),
363  });
364  d->loglevels.insert(WadseekerLib::Navigation, {
365  tr("NAVI"),
366  QString("color: %1;").arg(colorHtmlMessageNavigation),
367  });
368  d->loglevels.refit();
369 }
370 
371 bool WadseekerInterface::isInstantiated()
372 {
373  return currentInstance != nullptr;
374 }
375 
376 void WadseekerInterface::registerUpdateRequest()
377 {
378  updateProgressBar();
379  updateTitle();
380 }
381 
382 void WadseekerInterface::reject()
383 {
384  switch (state)
385  {
386  case Downloading:
387  wadseeker.abort();
388  break;
389 
390  case Waiting:
391  this->done(Rejected);
392  break;
393  }
394 }
395 
396 void WadseekerInterface::resetTitleToDefault()
397 {
398  setWindowTitle(tr("Wadseeker"));
399 }
400 
401 void WadseekerInterface::seekStarted(const ModSet &filenames)
402 {
403  QList<PWad> wads;
404  QStringList names;
405  for (ModFile modFile : filenames.modFiles())
406  {
407  wads << modFile;
408  names << modFile.fileName();
409  }
410  d->teWadseekerOutput->clear();
411  d->pbOverallProgress->setValue(0);
412  d->taskbarProgress->setValue(0);
413  appendLog("Seek started on filenames: " + names.join(", "), WadseekerLib::NoticeImportant, d->name);
414 
415  seekedWads = wads;
416  successfulWads.clear();
417  d->twSites->setRowCount(0);
418  d->twWads->setRowCount(0);
419  setStateDownloading();
420 
421  for (const PWad &wad : seekedWads)
422  {
423  d->twWads->addFile(wad.name());
424  }
425 }
426 
427 void WadseekerInterface::setStateDownloading()
428 {
429  d->btnAbort->show();
430  d->btnClose->hide();
431  d->btnDownload->setEnabled(false);
432  d->taskbarProgress->show();
433  state = Downloading;
434 }
435 
436 void WadseekerInterface::setStateWaiting()
437 {
438  d->btnAbort->hide();
439  d->btnClose->show();
440  d->btnDownload->setEnabled(true);
441  d->taskbarProgress->hide();
442  state = Waiting;
443 }
444 
445 void WadseekerInterface::setupAutomatic()
446 {
447  bAutomatic = true;
448  d->lblTop->hide();
449  d->btnDownload->hide();
450  d->leWadName->hide();
451 }
452 
453 void WadseekerInterface::setWads(const QList<PWad> &wads)
454 {
455  seekedWads = wads;
456  if (!isAutomatic())
457  {
458  QStringList names;
459  for (PWad wad : wads)
460  {
461  names << wad.name();
462  }
463  d->leWadName->setText(names.join(", "));
464  }
465 }
466 
467 void WadseekerInterface::setupIdgames()
468 {
469  wadseeker.setIdgamesEnabled(gConfig.wadseeker.bSearchInIdgames);
470  wadseeker.setIdgamesUrl(gConfig.wadseeker.idgamesURL);
471 }
472 
473 void WadseekerInterface::showEvent(QShowEvent *event)
474 {
475  Q_UNUSED(event);
476  if (!bFirstShown)
477  {
478  d->taskbarButton->setWindow(windowHandle());
479  bFirstShown = true;
480 
481  if (isAutomatic())
482  startSeeking(seekedWads);
483  }
484 }
485 
486 void WadseekerInterface::serviceStarted(const QString &service)
487 {
488  d->twSites->addService(service);
489 }
490 
491 void WadseekerInterface::serviceFinished(const QString &service)
492 {
493  d->twSites->removeService(service);
494 }
495 
496 void WadseekerInterface::siteFinished(const QUrl &site)
497 {
498  d->twSites->removeUrl(site);
499 }
500 
501 void WadseekerInterface::siteProgress(const QUrl &site, qint64 bytes, qint64 total)
502 {
503  d->twSites->setUrlProgress(site, bytes, total);
504 }
505 
506 void WadseekerInterface::siteRedirect(const QUrl &oldUrl, const QUrl &newUrl)
507 {
508  d->twSites->removeUrl(oldUrl);
509  d->twSites->addUrl(newUrl);
510 }
511 
512 void WadseekerInterface::siteStarted(const QUrl &site)
513 {
514  d->twSites->addUrl(site);
515 }
516 
517 void WadseekerInterface::startSeeking(const QList<PWad> &seekedFilesList)
518 {
519  if (seekedFilesList.isEmpty())
520  return;
521  d->bCompletedSuccessfully = false;
522 
523  ModSet listWads;
524  for (PWad seekedFile : seekedFilesList)
525  {
526  listWads.addModFile(seekedFile);
527  }
528 
529  setupIdgames();
530 
531  wadseeker.setTargetDirectory(gDoomseekerTemplatedPathResolver().resolve(gConfig.wadseeker.targetDirectory));
532  wadseeker.setCustomSites(customSites);
533  wadseeker.setMaximumConcurrentSeeks(gConfig.wadseeker.maxConcurrentSiteDownloads);
534  wadseeker.setMaximumConcurrentDownloads(gConfig.wadseeker.maxConcurrentWadDownloads);
535  wadseeker.startSeek(listWads);
536 }
537 
538 void WadseekerInterface::updateProgressBar()
539 {
540  double totalPercentage = d->twWads->totalDonePercentage();
541  auto progressBarValue = (unsigned)(totalPercentage * 100.0);
542 
543  d->pbOverallProgress->setValue(progressBarValue);
544  d->taskbarProgress->setValue(progressBarValue);
545 }
546 
547 void WadseekerInterface::updateTitle()
548 {
549  switch (state)
550  {
551  case Downloading:
552  {
553  double totalPercentage = d->twWads->totalDonePercentage();
554  if (totalPercentage < 0.0)
555  totalPercentage = 0.0;
556 
557  setWindowTitle(tr("[%1%] Wadseeker").arg(totalPercentage, 6, 'f', 2));
558  break;
559  }
560 
561  default:
562  case Waiting:
563  resetTitleToDefault();
564  break;
565  }
566 }
567 
568 QList<PWad> WadseekerInterface::unsuccessfulWads() const
569 {
570  QList<PWad> allWads = seekedWads;
571  for (PWad successfulWad : successfulWads)
572  {
573  for (int i = 0; i < allWads.size(); ++i)
574  {
575  if (allWads[i].name() == successfulWad.name())
576  {
577  allWads.removeAt(i);
578  break;
579  }
580  }
581  }
582  return allWads;
583 }
584 
585 void WadseekerInterface::showWadsTableContextMenu(const QPoint &position)
586 {
587  const QModelIndex index = d->twWads->indexAt(position);
588  WadseekerWadsTable::ContextMenu *menu = d->twWads->contextMenu(index, position);
589 
590  // Disable actions depending on Wadseeker's state.
591  QString fileName = d->twWads->fileNameAtRow(index.row());
592  if (!wadseeker.isDownloadingFile(fileName))
593  menu->actionSkipCurrentSite->setEnabled(false);
594 
595  QAction *pResult = menu->exec();
596 
597  if (pResult == menu->actionSkipCurrentSite)
598  {
599  QString wadName = d->twWads->fileNameAtRow(index.row());
600  d->twWads->setFileUrl(fileName, QUrl());
601 
602  wadseeker.skipFileCurrentUrl(wadName);
603  }
604  else if (pResult != nullptr)
605  QMessageBox::warning(this, tr("Context menu error"), tr("Unknown action selected."));
606 
607  delete menu;
608 }