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 <QMessageBox>
41 #include <QPushButton>
42 
43 const int WadseekerInterface::UPDATE_INTERVAL_MS = 500;
44 WadseekerInterface *WadseekerInterface::currentInstance = nullptr;
45 
46 DClass<WadseekerInterface> : public Ui::WadseekerInterface
47 {
48 public:
49  QPushButton *btnAbort;
50  QPushButton *btnClose;
51 
52  bool bCompletedSuccessfully;
53  bool preventGame;
54  TaskbarButton *taskbarButton;
55  TaskbarProgress *taskbarProgress;
56 };
57 
58 DPointered(WadseekerInterface)
59 
60 WadseekerInterface::WadseekerInterface(QWidget *parent)
61  : QDialog(parent)
62 {
63  construct();
64  bAutomatic = false;
65 }
66 
67 WadseekerInterface::WadseekerInterface(ServerPtr server, QWidget *parent)
68  : QDialog(parent)
69 {
70  construct();
71  setupAutomatic();
72  d->lblTop->show();
73  d->lblTop->setText(tr("Downloading WADs for server \"%1\"").arg(server->name()));
74  setCustomSites(server->allWebSites());
75 }
76 
77 WadseekerInterface::~WadseekerInterface()
78 {
79  currentInstance = nullptr;
80 }
81 
82 void WadseekerInterface::abortService(const QString &service)
83 {
84  message(tr("Aborting service: %1").arg(service), WadseekerLib::Notice);
85  wadseeker.skipService(service);
86 }
87 
88 void WadseekerInterface::abortSite(const QUrl &url)
89 {
90  message(tr("Aborting site: %1").arg(url.toString()), WadseekerLib::Notice);
91  wadseeker.skipSiteSeek(url);
92 }
93 
94 void WadseekerInterface::accept()
95 {
96  if (isAutomatic())
97  {
98  if (d->bCompletedSuccessfully)
99  done(QDialog::Accepted);
100  }
101  else
102  {
103  if (d->leWadName->text().isEmpty())
104  return;
105  else
106  {
107  seekedWads.clear();
108  QStringList pwadNames = d->leWadName->text().split(',', Qt::SkipEmptyParts);
109  for (QString pwadName : pwadNames)
110  {
111  seekedWads << pwadName.trimmed();
112  }
113  }
114  startSeeking(seekedWads);
115  }
116 }
117 
118 void WadseekerInterface::allDone(bool bSuccess)
119 {
120  setStateWaiting();
121  d->bCompletedSuccessfully = bSuccess;
122  QApplication::alert(this);
123  if (bSuccess)
124  {
125  displayMessage(tr("All done. Success."), WadseekerLib::NoticeImportant, false);
126 
127  if (isAutomatic() && !d->preventGame)
128  {
129  if (isActiveWindow())
130  done(QDialog::Accepted);
131  else
132  d->btnStartGame->show();
133  }
134  }
135  else
136  {
137  QList<PWad> failures = unsuccessfulWads();
138 
139  for (const PWad &failure : failures)
140  {
141  d->twWads->setFileFailed(failure.name());
142  }
143 
144  displayMessage(tr("All done. Fail."), WadseekerLib::CriticalError, false);
145  }
146 }
147 
148 void WadseekerInterface::connectWadseekerObject()
149 {
150  // Connect Wadseeker to the dialog box.
151  this->connect(&wadseeker, SIGNAL(allDone(bool)),
152  SLOT(allDone(bool)));
153  this->connect(&wadseeker, SIGNAL(message(const QString&,WadseekerLib::MessageType)),
154  SLOT(message(const QString&,WadseekerLib::MessageType)));
155  this->connect(&wadseeker, SIGNAL(seekStarted(const ModSet&)),
156  SLOT(seekStarted(const ModSet&)));
157  this->connect(&wadseeker, SIGNAL(fileInstalled(const ModFile&)),
158  SLOT(fileDownloadSuccessful(const ModFile&)));
159  this->connect(&wadseeker, SIGNAL(siteFinished(const QUrl&)),
160  SLOT(siteFinished(const QUrl&)));
161  this->connect(&wadseeker, SIGNAL(siteProgress(const QUrl&,qint64,qint64)),
162  SLOT(siteProgress(const QUrl&,qint64,qint64)));
163  this->connect(&wadseeker, SIGNAL(siteRedirect(const QUrl&,const QUrl&)),
164  SLOT(siteRedirect(const QUrl&,const QUrl&)));
165  this->connect(&wadseeker, SIGNAL(siteStarted(const QUrl&)),
166  SLOT(siteStarted(const QUrl&)));
167  this->connect(&wadseeker, SIGNAL(serviceStarted(QString)),
168  SLOT(serviceStarted(QString)));
169  this->connect(&wadseeker, SIGNAL(serviceFinished(QString)),
170  SLOT(serviceFinished(QString)));
171 
172  // Connect Wadseeker to the WADs table widget.
173  d->twWads->connect(&wadseeker, SIGNAL(fileDownloadFinished(const ModFile&)),
174  SLOT(setFileDownloadFinished(const ModFile&)));
175  d->twWads->connect(&wadseeker, SIGNAL(fileDownloadProgress(const ModFile&,qint64,qint64)),
176  SLOT(setFileProgress(const ModFile&,qint64,qint64)));
177  d->twWads->connect(&wadseeker, SIGNAL(fileDownloadStarted(const ModFile&,const QUrl&)),
178  SLOT(setFileUrl(const ModFile&,const QUrl&)));
179 }
180 
181 void WadseekerInterface::construct()
182 {
183  d->setupUi(this);
184  CommonGUI::setupDialog(*this);
185  d->preventGame = false;
186  d->bCompletedSuccessfully = false;
187  d->btnAbort = d->buttonBox->button(QDialogButtonBox::Abort);
188  d->btnClose = d->buttonBox->button(QDialogButtonBox::Close);
189 
190  d->taskbarButton = new TaskbarButton(this);
191 
192  d->taskbarProgress = d->taskbarButton->progress();
193  d->taskbarProgress->setMaximum(d->pbOverallProgress->maximum());
194 
195  setStateWaiting();
196 
197  initMessageColors();
198 
199  this->setWindowIcon(QIcon(":/icon.png"));
200  d->btnStartGame->hide();
201  this->connect(&updateTimer, SIGNAL(timeout()), SLOT(registerUpdateRequest()));
202 
203  connectWadseekerObject();
204 
205  // Connect tables.
206  connect(d->twWads, &QWidget::customContextMenuRequested,
207  this, &WadseekerInterface::showWadsTableContextMenu);
208 
209  bAutomatic = false;
210  bFirstShown = false;
211 
212  QStringList urlList = gConfig.wadseeker.searchURLs;
213  if (gConfig.wadseeker.bAlwaysUseDefaultSites)
214  {
215  for (int i = 0; !Wadseeker::defaultSites[i].isEmpty(); ++i)
216  urlList << Wadseeker::defaultSites[i];
217  }
218 
219  wadseeker.setPrimarySites(urlList);
220 
221  updateTimer.setSingleShot(false);
222  updateTimer.start(UPDATE_INTERVAL_MS);
223 }
224 
225 WadseekerInterface *WadseekerInterface::create(QWidget *parent)
226 {
227  if (!isInstantiated())
228  {
229  currentInstance = new WadseekerInterface(parent);
230  return currentInstance;
231  }
232  return nullptr;
233 }
234 
235 WadseekerInterface *WadseekerInterface::create(ServerPtr server, QWidget *parent)
236 {
237  if (!isInstantiated())
238  {
239  currentInstance = new WadseekerInterface(server, parent);
240  return currentInstance;
241  }
242  return nullptr;
243 }
244 
245 WadseekerInterface *WadseekerInterface::createAutoNoGame(QWidget *parent)
246 {
247  WadseekerInterface *interface = create(parent);
248  if (interface != nullptr)
249  {
250  interface->setupAutomatic();
251  interface->d->preventGame = true;
252  }
253  return interface;
254 }
255 
256 void WadseekerInterface::displayMessage(const QString &message, WadseekerLib::MessageType type, bool bPrependErrorsWithMessageType)
257 {
258  QString strProcessedMessage;
259 
260  bool bPrependWithNewline = false;
261  QString wrapHtmlLeft = "<div style=\"%1\">";
262  QString wrapHtmlRight = "</div>";
263  QString htmlStyle;
264 
265  switch (type)
266  {
267  case WadseekerLib::CriticalError:
268  htmlStyle = QString("color: %1; font-weight: bold;").arg(colorHtmlMessageFatalError);
269  bPrependWithNewline = true;
270 
271  if (bPrependErrorsWithMessageType)
272  strProcessedMessage = tr("CRITICAL ERROR: %1").arg(message);
273  else
274  strProcessedMessage = message;
275 
276  setStateWaiting();
277  break;
278 
279  case WadseekerLib::Error:
280  htmlStyle = QString("color: %1;").arg(colorHtmlMessageError);
281 
282  if (bPrependErrorsWithMessageType)
283  strProcessedMessage = tr("Error: %1").arg(message);
284  else
285  strProcessedMessage = message;
286  break;
287 
288  case WadseekerLib::Notice:
289  htmlStyle = QString("color: %1;").arg(colorHtmlMessageNotice);
290 
291  strProcessedMessage = message;
292  break;
293 
294  case WadseekerLib::NoticeImportant:
295  htmlStyle = QString("color: %1; font-weight: bold;").arg(colorHtmlMessageNotice);
296  bPrependWithNewline = true;
297 
298  strProcessedMessage = message;
299  break;
300  }
301 
302  if (bPrependWithNewline && !d->teWadseekerOutput->toPlainText().isEmpty())
303  strProcessedMessage = "<br>" + strProcessedMessage;
304 
305  wrapHtmlLeft = wrapHtmlLeft.arg(htmlStyle);
306 
307  strProcessedMessage = wrapHtmlLeft + strProcessedMessage + wrapHtmlRight;
308 
309  d->teWadseekerOutput->append(strProcessedMessage);
310 }
311 
312 void WadseekerInterface::fileDownloadSuccessful(const ModFile &filename)
313 {
314  successfulWads << filename;
315  d->twWads->setFileSuccessful(filename.fileName());
316 }
317 
318 void WadseekerInterface::initMessageColors()
319 {
320  colorHtmlMessageNotice = gConfig.wadseeker.colorMessageNotice;
321  colorHtmlMessageError = gConfig.wadseeker.colorMessageError;
322  colorHtmlMessageFatalError = gConfig.wadseeker.colorMessageCriticalError;
323 }
324 
325 bool WadseekerInterface::isInstantiated()
326 {
327  return currentInstance != nullptr;
328 }
329 
330 void WadseekerInterface::message(const QString &message, WadseekerLib::MessageType type)
331 {
332  displayMessage(message, type, true);
333 }
334 
335 void WadseekerInterface::registerUpdateRequest()
336 {
337  updateProgressBar();
338  updateTitle();
339 }
340 
341 void WadseekerInterface::reject()
342 {
343  switch (state)
344  {
345  case Downloading:
346  wadseeker.abort();
347  break;
348 
349  case Waiting:
350  this->done(Rejected);
351  break;
352  }
353 }
354 
355 void WadseekerInterface::resetTitleToDefault()
356 {
357  setWindowTitle(tr("Wadseeker"));
358 }
359 
360 void WadseekerInterface::seekStarted(const ModSet &filenames)
361 {
362  QList<PWad> wads;
363  QStringList names;
364  for (ModFile modFile : filenames.modFiles())
365  {
366  wads << modFile;
367  names << modFile.fileName();
368  }
369  d->teWadseekerOutput->clear();
370  d->pbOverallProgress->setValue(0);
371  d->taskbarProgress->setValue(0);
372  displayMessage("Seek started on filenames: " + names.join(", "), WadseekerLib::Notice, false);
373 
374  seekedWads = wads;
375  successfulWads.clear();
376  d->twSites->setRowCount(0);
377  d->twWads->setRowCount(0);
378  setStateDownloading();
379 
380  for (const PWad &wad : seekedWads)
381  {
382  d->twWads->addFile(wad.name());
383  }
384 }
385 
386 void WadseekerInterface::setStateDownloading()
387 {
388  d->btnAbort->show();
389  d->btnClose->hide();
390  d->btnDownload->setEnabled(false);
391  d->taskbarProgress->show();
392  state = Downloading;
393 }
394 
395 void WadseekerInterface::setStateWaiting()
396 {
397  d->btnAbort->hide();
398  d->btnClose->show();
399  d->btnDownload->setEnabled(true);
400  d->taskbarProgress->hide();
401  state = Waiting;
402 }
403 
404 void WadseekerInterface::setupAutomatic()
405 {
406  bAutomatic = true;
407  d->lblTop->hide();
408  d->btnDownload->hide();
409  d->leWadName->hide();
410 }
411 
412 void WadseekerInterface::setWads(const QList<PWad> &wads)
413 {
414  seekedWads = wads;
415  if (!isAutomatic())
416  {
417  QStringList names;
418  for (PWad wad : wads)
419  {
420  names << wad.name();
421  }
422  d->leWadName->setText(names.join(", "));
423  }
424 }
425 
426 void WadseekerInterface::setupIdgames()
427 {
428  wadseeker.setIdgamesEnabled(gConfig.wadseeker.bSearchInIdgames);
429  wadseeker.setIdgamesUrl(gConfig.wadseeker.idgamesURL);
430 }
431 
432 void WadseekerInterface::showEvent(QShowEvent *event)
433 {
434  Q_UNUSED(event);
435  if (!bFirstShown)
436  {
437  d->taskbarButton->setWindow(windowHandle());
438  bFirstShown = true;
439 
440  if (isAutomatic())
441  startSeeking(seekedWads);
442  }
443 }
444 
445 void WadseekerInterface::serviceStarted(const QString &service)
446 {
447  d->twSites->addService(service);
448 }
449 
450 void WadseekerInterface::serviceFinished(const QString &service)
451 {
452  d->twSites->removeService(service);
453 }
454 
455 void WadseekerInterface::siteFinished(const QUrl &site)
456 {
457  d->twSites->removeUrl(site);
458  displayMessage("Site finished: " + site.toString(), WadseekerLib::Notice, false);
459 }
460 
461 void WadseekerInterface::siteProgress(const QUrl &site, qint64 bytes, qint64 total)
462 {
463  d->twSites->setUrlProgress(site, bytes, total);
464 }
465 
466 void WadseekerInterface::siteRedirect(const QUrl &oldUrl, const QUrl &newUrl)
467 {
468  d->twSites->removeUrl(oldUrl);
469  d->twSites->addUrl(newUrl);
470  displayMessage("Site redirect: " + oldUrl.toString() + " -> " + newUrl.toString(), WadseekerLib::Notice, false);
471 }
472 
473 void WadseekerInterface::siteStarted(const QUrl &site)
474 {
475  d->twSites->addUrl(site);
476  displayMessage("Site started: " + site.toString(), WadseekerLib::Notice, false);
477 }
478 
479 void WadseekerInterface::startSeeking(const QList<PWad> &seekedFilesList)
480 {
481  if (seekedFilesList.isEmpty())
482  return;
483  d->bCompletedSuccessfully = false;
484 
485  ModSet listWads;
486  for (PWad seekedFile : seekedFilesList)
487  {
488  listWads.addModFile(seekedFile);
489  }
490 
491  setupIdgames();
492 
493  wadseeker.setTargetDirectory(gDoomseekerTemplatedPathResolver().resolve(gConfig.wadseeker.targetDirectory));
494  wadseeker.setCustomSites(customSites);
495  wadseeker.setMaximumConcurrentSeeks(gConfig.wadseeker.maxConcurrentSiteDownloads);
496  wadseeker.setMaximumConcurrentDownloads(gConfig.wadseeker.maxConcurrentWadDownloads);
497  wadseeker.startSeek(listWads);
498 }
499 
500 void WadseekerInterface::updateProgressBar()
501 {
502  double totalPercentage = d->twWads->totalDonePercentage();
503  auto progressBarValue = (unsigned)(totalPercentage * 100.0);
504 
505  d->pbOverallProgress->setValue(progressBarValue);
506  d->taskbarProgress->setValue(progressBarValue);
507 }
508 
509 void WadseekerInterface::updateTitle()
510 {
511  switch (state)
512  {
513  case Downloading:
514  {
515  double totalPercentage = d->twWads->totalDonePercentage();
516  if (totalPercentage < 0.0)
517  totalPercentage = 0.0;
518 
519  setWindowTitle(tr("[%1%] Wadseeker").arg(totalPercentage, 6, 'f', 2));
520  break;
521  }
522 
523  default:
524  case Waiting:
525  resetTitleToDefault();
526  break;
527  }
528 }
529 
530 QList<PWad> WadseekerInterface::unsuccessfulWads() const
531 {
532  QList<PWad> allWads = seekedWads;
533  for (PWad successfulWad : successfulWads)
534  {
535  for (int i = 0; i < allWads.size(); ++i)
536  {
537  if (allWads[i].name() == successfulWad.name())
538  {
539  allWads.removeAt(i);
540  break;
541  }
542  }
543  }
544  return allWads;
545 }
546 
547 void WadseekerInterface::showWadsTableContextMenu(const QPoint &position)
548 {
549  const QModelIndex index = d->twWads->indexAt(position);
550  WadseekerWadsTable::ContextMenu *menu = d->twWads->contextMenu(index, position);
551 
552  // Disable actions depending on Wadseeker's state.
553  QString fileName = d->twWads->fileNameAtRow(index.row());
554  if (!wadseeker.isDownloadingFile(fileName))
555  menu->actionSkipCurrentSite->setEnabled(false);
556 
557  QAction *pResult = menu->exec();
558 
559  if (pResult == menu->actionSkipCurrentSite)
560  {
561  QString wadName = d->twWads->fileNameAtRow(index.row());
562  d->twWads->setFileUrl(fileName, QUrl());
563 
564  wadseeker.skipFileCurrentUrl(wadName);
565  }
566  else if (pResult != nullptr)
567  QMessageBox::warning(this, tr("Context menu error"), tr("Unknown action selected."));
568 
569  delete menu;
570 }