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