mainwindow.cpp
1 //------------------------------------------------------------------------------
2 // mainwindow.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 "application.h"
24 #include "apprunner.h"
25 #include "commandline.h"
26 #include "configuration/doomseekerconfig.h"
27 #include "configuration/queryspeed.h"
28 #include "connectionhandler.h"
29 #include "customservers.h"
30 #include "datapaths.h"
31 #include "doomseekerfilepaths.h"
32 #include "fileutils.h"
33 #include "gamedemo.h"
34 #include "gui/aboutdialog.h"
35 #include "gui/checkwadsdlg.h"
36 #include "gui/configuration/doomseekerconfigurationdialog.h"
37 #include "gui/configuration/irc/ircconfigurationdialog.h"
38 #include "gui/copytextdlg.h"
39 #include "gui/createserverdialog.h"
40 #include "gui/demomanager.h"
41 #include "gui/dockBuddiesList.h"
42 #include "gui/freedoomdialog.h"
43 #include "gui/helpers/playersdiagram.h"
44 #include "gui/helpers/taskbarbutton.h"
45 #include "gui/helpers/taskbarprogress.h"
46 #include "gui/ip2cupdatebox.h"
47 #include "gui/irc/ircdock.h"
48 #include "gui/irc/ircsounds.h"
49 #include "gui/logdock.h"
50 #include "gui/mainwindow.h"
51 #include "gui/models/serverlistmodel.h"
52 #include "gui/programargshelpdialog.h"
53 #include "gui/serverdetailsdock.h"
54 #include "gui/serverfilterdock.h"
55 #include "gui/serverlist.h"
56 #include "gui/wadseekerinterface.h"
57 #include "gui/wadseekershow.h"
58 #include "gui/widgets/serversstatuswidget.h"
59 #include "ip2c/ip2cloader.h"
60 #include "irc/configuration/chatnetworkscfg.h"
61 #include "irc/configuration/ircconfig.h"
62 #include "joincommandlinebuilder.h"
63 #include "log.h"
64 #include "main.h"
65 #include "pathfinder/pathfinder.h"
66 #include "pathfinder/wadpathfinder.h"
67 #include "plugins/engineplugin.h"
68 #include "plugins/pluginloader.h"
69 #include "refresher/refresher.h"
70 #include "serverapi/broadcast.h"
71 #include "serverapi/broadcastmanager.h"
72 #include "serverapi/gameclientrunner.h"
73 #include "serverapi/mastermanager.h"
74 #include "serverapi/message.h"
75 #include "serverapi/server.h"
76 #include "serverapi/serverlistcounttracker.h"
77 #include "strings.hpp"
78 #include "ui_mainwindow.h"
79 #include "updater/autoupdater.h"
80 #include "updater/updatechannel.h"
81 #include "updater/updateinstaller.h"
82 #include "updater/updatepackage.h"
83 #include "wadseeker/entities/checksum.h"
84 #include <cassert>
85 #include <QAction>
86 #include <QApplication>
87 #include <QDesktopServices>
88 #include <QDockWidget>
89 #include <QFileInfo>
90 #include <QHeaderView>
91 #include <QIcon>
92 #include <QLineEdit>
93 #include <QMessageBox>
94 #include <QMessageBox>
95 #include <QPointer>
96 #include <QProgressBar>
97 #include <QSizePolicy>
98 #include <QToolBar>
99 
100 const QString MainWindow::HELP_SITE_URL = "https://doomseeker.drdteam.org/help";
101 
109 class QueryMenuAction : public QAction
110 {
111 public:
112  QueryMenuAction(const EnginePlugin *plugin, ServersStatusWidget *statusWidget, QObject *parent = nullptr)
113  : QAction(parent)
114  {
115  this->pPlugin = plugin;
116 
117  if (plugin != nullptr)
118  {
119  connect(this, SIGNAL(toggled(bool)), plugin->data()->masterClient,
120  SLOT(setEnabled(bool)));
121  connect(this, SIGNAL(toggled(bool)), statusWidget, SLOT(setMasterEnabledStatus(bool)));
122  }
123  }
124 
125  const EnginePlugin *plugin() const
126  {
127  return pPlugin;
128  }
129 
130 private:
131  const EnginePlugin *pPlugin;
132 };
133 
134 DClass<MainWindow> : public Ui::MainWindowWnd
135 {
136 public:
137  PrivData() : bTotalRefreshInProcess(false), buddiesList(nullptr),
138  bWasMaximized(false), bWantToQuit(false), logDock(nullptr),
139  masterManager(nullptr), trayIcon(nullptr), trayIconMenu(nullptr)
140  {
141  }
142 
143  QApplication *application;
144  QTimer autoRefreshTimer;
145 
146  AutoUpdater *autoUpdater;
147  QWidget *autoUpdaterStatusBarWidget;
148  QPushButton *autoUpdaterAbortButton;
149  QLabel *autoUpdaterLabel;
150  QProgressBar *autoUpdaterFileProgressBar;
151  QProgressBar *autoUpdaterOverallProgressBar;
152 
157  bool bTotalRefreshInProcess;
158 
159  DockBuddiesList *buddiesList;
160  BroadcastManager *broadcastManager;
161 
165  bool bWasMaximized;
166 
172  bool bWantToQuit;
173 
174  IP2CLoader *ip2cLoader;
175  QProgressBar *ip2cUpdateProgressBar;
176  IRCDock *ircDock;
177  LogDock *logDock;
178  ServerDetailsDock *detailsDock;
179  QPointer<FreedoomDialog> freedoomDialog;
180  ServerFilterDock *serverFilterDock;
181  ServerList *serverList;
182 
183  MasterManager *masterManager;
184  QHash<const EnginePlugin *, QueryMenuAction *> queryMenuPorts;
185  QHash<const EnginePlugin *, ServersStatusWidget *> serversStatusesWidgets;
186  QAction *toolBarGetServers;
187  QSystemTrayIcon *trayIcon;
188  QMenu *trayIconMenu;
190  UpdateChannel *updateChannelOnUpdateStart;
191  int updaterInstallerErrorCode;
192 
193  ConnectionHandler *connectionHandler;
194  QDockWidget *mainDock;
195 
196  TaskbarProgress *taskbarProgress;
197  TaskbarButton *taskbarButton;
198 };
199 
200 DPointeredNoCopy(MainWindow)
201 
202 MainWindow::MainWindow(QApplication *application)
203 {
204  d->autoUpdater = nullptr;
205  d->mainDock = nullptr;
206  d->connectionHandler = nullptr;
207  d->updateChannelOnUpdateStart = new UpdateChannel();
208  d->updaterInstallerErrorCode = 0;
209 
210  d->application = application;
211 
212  this->setAttribute(Qt::WA_DeleteOnClose, true);
213  d->setupUi(this);
214 
215  d->taskbarButton = new TaskbarButton(this);
216  d->taskbarProgress = d->taskbarButton->progress();
217 
218  setupIcons();
219 
220  initAutoUpdaterWidgets();
221 
222  d->updatesConfirmationWidget->hide();
223  d->updatesDownloadedWidget->hide();
224 
225  // Hide menu options which aren't supported on target platform.
226  #ifndef WITH_AUTOUPDATES
227  d->menuActionCheckForUpdates->setVisible(false);
228  #endif
229 
230  initIP2CUpdater();
231 
232  // The buddies list must always be available so we can perform certain operations on it
233  d->buddiesList = new DockBuddiesList(this);
234  d->menuView->addAction(d->buddiesList->toggleViewAction());
235  d->buddiesList->toggleViewAction()->setText(MainWindow::tr("&Buddies"));
236  d->buddiesList->toggleViewAction()->setShortcut(MainWindow::tr("CTRL+B"));
237 
238  connect(d->buddiesList, SIGNAL(joinServer(ServerPtr)), this, SLOT(runGame(ServerPtr)));
239  d->buddiesList->hide();
240  this->addDockWidget(Qt::LeftDockWidgetArea, d->buddiesList);
241  initLogDock();
242  initIRCDock();
243  initServerFilterDock();
244  initMainDock();
245  splitDockWidget(d->mainDock, d->serverFilterDock, Qt::Horizontal);
246 
247  // Spawn Server Table Handler.
248  d->serverList = new ServerList(d->tableServers, this);
249  connectEntities();
250 
251  d->broadcastManager = new BroadcastManager(this);
252  d->serverList->connect(d->broadcastManager,
253  SIGNAL(newServerDetected(ServerPtr,int)), SLOT(registerServer(ServerPtr)));
254  d->serverList->connect(d->broadcastManager,
255  SIGNAL(serverLost(ServerPtr)), SLOT(removeServer(ServerPtr)));
256 
257  initServerDetailsDock();
258  tabifyDockWidget(d->ircDock, d->detailsDock);
259 
260  // Restore checked states.
261  d->menuActionRecordDemo->setChecked(gConfig.doomseeker.bRecordDemo);
262 
263  // Get the master
264  d->masterManager = new MasterManager();
265  d->masterManager->setBroadcastManager(d->broadcastManager);
266  d->buddiesList->scan(d->masterManager);
267  connect(d->masterManager, SIGNAL(masterMessage(MasterClient*,const QString&,const QString&,bool)),
268  this, SLOT(masterManagerMessages(MasterClient*,const QString&,const QString&,bool)));
269  connect(d->masterManager, SIGNAL(masterMessageImportant(MasterClient*,const Message&)),
270  this, SLOT(masterManagerMessagesImportant(MasterClient*,const Message&)));
271 
272  // Allow us to enable and disable ports.
273  fillQueryMenu(d->masterManager);
274 
275  // Init custom servers
276  QList<ServerPtr> customServers = d->masterManager->customServs()->readConfig();
277  for (ServerPtr server : customServers)
278  {
279  d->serverList->registerServer(server);
280  }
281 
282  setWindowIcon(Application::icon());
283 
284  // Auto refresh timer
285  initAutoRefreshTimer();
286  connect(&d->autoRefreshTimer, SIGNAL(timeout()), this, SLOT(autoRefreshTimer_timeout()));
287 
288  // Tray icon
289  initTrayIcon();
290 
291  setupToolBar();
292 
293  // Player diagram styles
294  PlayersDiagram::loadImages(gConfig.doomseeker.slotStyle);
295 
296  // IP2C
297  d->menuActionUpdateIP2C->setEnabled(false);
298  d->ip2cLoader = new IP2CLoader();
299  connectIP2CLoader();
300  d->ip2cLoader->load();
301 
302  restoreState(QByteArray::fromBase64(gConfig.doomseeker.mainWindowState.toUtf8()));
303  restoreGeometry(gConfig.doomseeker.mainWindowGeometry);
304 
305  // Start first refresh from a timer. We want the main window fully
306  // set up before refresh.
307  QTimer::singleShot(1, this, SLOT(postInitAppStartup()));
308 }
309 
310 MainWindow::~MainWindow()
311 {
312  // Window geometry settings
313  gConfig.doomseeker.mainWindowGeometry = saveGeometry();
314  gConfig.doomseeker.mainWindowState = saveState().toBase64();
315 
316  if (d->updateChannelOnUpdateStart != nullptr)
317  delete d->updateChannelOnUpdateStart;
318  if (d->autoUpdater != nullptr)
319  {
320  d->autoUpdater->disconnect();
321  delete d->autoUpdater;
322  }
323  if (d->connectionHandler)
324  delete d->connectionHandler;
325 
326  QList<QAction *> menuQueryActions = d->menuQuery->actions();
327  QList<QAction *>::iterator it;
328  for (QAction *action : menuQueryActions)
329  {
330  QString pluginName = action->text();
331 
332  if (!pluginName.isEmpty())
333  {
334  IniSection pluginConfig = gConfig.iniSectionForPlugin(pluginName);
335  pluginConfig["Query"] = action->isChecked();
336  }
337  }
338 
339  if (d->trayIcon != nullptr)
340  {
341  d->trayIcon->setVisible(false);
342  delete d->trayIcon;
343  d->trayIcon = nullptr;
344  }
345 
346  if (d->trayIconMenu != nullptr)
347  {
348  delete d->trayIconMenu;
349  d->trayIconMenu = nullptr;
350  }
351 
352  delete d->serverList;
353 
354  if (d->masterManager != nullptr)
355  delete d->masterManager;
356 
357  if (d->ip2cLoader != nullptr)
358  delete d->ip2cLoader;
359 }
360 
361 void MainWindow::abortAutoUpdater()
362 {
363  if (d->autoUpdater != nullptr)
364  d->autoUpdater->abort();
365 }
366 
367 void MainWindow::autoRefreshTimer_timeout()
368 {
369  if (gConfig.doomseeker.bQueryAutoRefreshDontIfActive && !isMinimized())
370  {
371  if (QApplication::activeWindow() != nullptr)
372  return;
373  }
374 
375  getServers();
376 }
377 
378 void MainWindow::blockRefreshButtons()
379 {
380  d->toolBarGetServers->setEnabled(false);
381 }
382 
383 DockBuddiesList *MainWindow::buddiesList()
384 {
385  return d->buddiesList;
386 }
387 
388 void MainWindow::changeEvent(QEvent *event)
389 {
390  if (event->type() == QEvent::ActivationChange && isActiveWindow() && !isMinimized() && !isHidden())
391  {
392  d->serverList->cleanUp();
393  event->accept();
394  }
395  QMainWindow::changeEvent(event);
396 }
397 
398 void MainWindow::checkForUpdates(bool bUserTriggered)
399 {
400  if (d->autoUpdater != nullptr)
401  {
402  if (d->autoUpdater->isRunning())
403  {
404  QMessageBox::warning(this, tr("Doomseeker - Auto Update"),
405  tr("Update is already in progress."));
406  return;
407  }
408  else
409  {
410  delete d->autoUpdater;
411  d->autoUpdater = nullptr;
412  }
413  }
414  gLog << tr("Removing old update packages from local temporary space.");
415  QStringList removeFilter(QString("%1*").arg(DataPaths::UPDATE_PACKAGE_FILENAME_PREFIX));
416  FileUtils::rmAllFiles(DoomseekerFilePaths::updatePackagesStorageDir(),
417  removeFilter);
418 
419  showAndLogStatusMessage(tr("Checking for updates..."));
420  d->autoUpdater = new AutoUpdater();
421  this->connect(d->autoUpdater, SIGNAL(statusMessage(QString)),
422  SLOT(showAndLogStatusMessage(QString)));
423  this->connect(d->autoUpdater, SIGNAL(finished()),
424  SLOT(onAutoUpdaterFinish()));
425  this->connect(d->autoUpdater, SIGNAL(downloadAndInstallConfirmationRequested()),
426  SLOT(onAutoUpdaterDownloadAndInstallConfirmationRequest()));
427  this->connect(d->autoUpdater, SIGNAL(overallProgress(int,int,const QString&)),
428  SLOT(onAutoUpdaterOverallProgress(int,int,const QString&)));
429  this->connect(d->autoUpdater, SIGNAL(packageDownloadProgress(qint64,qint64)),
430  SLOT(onAutoUpdaterFileProgress(qint64,qint64)));
431 
432  QMap<QString, QList<QString> > ignoredPackagesRevisions;
433  if (!bUserTriggered)
434  {
435  for (const QString &package : gConfig.autoUpdates.lastKnownUpdateRevisions.keys())
436  {
437  QString revision = gConfig.autoUpdates.lastKnownUpdateRevisions[package];
438  QList<QString> list;
439  list << revision;
440  ignoredPackagesRevisions.insert(package, list);
441  }
442  }
443  d->autoUpdater->setIgnoreRevisions(ignoredPackagesRevisions);
444 
445  UpdateChannel channel = UpdateChannel::fromName(gConfig.autoUpdates.updateChannelName);
446  d->autoUpdater->setChannel(channel);
447  *d->updateChannelOnUpdateStart = channel;
448 
449  bool bRequireConfirmation = true;
450  if (!bUserTriggered)
451  {
452  bRequireConfirmation = (gConfig.autoUpdates.updateMode
454  }
455  d->autoUpdater->setRequireDownloadAndInstallConfirmation(bRequireConfirmation);
456  d->autoUpdaterStatusBarWidget->show();
457  d->autoUpdater->start();
458 }
459 
460 
462 {
463  const bool bUserTriggered = true;
464  checkForUpdates(!bUserTriggered);
465 }
466 
468 {
469  const bool bUserTriggered = true;
470  checkForUpdates(bUserTriggered);
471 }
472 
473 void MainWindow::closeEvent(QCloseEvent *event)
474 {
475  // Check if tray icon is available and if we want to minimize to tray icon
476  // when 'X' button is pressed. Real quit requests are handled by
477  // quitProgram() method. This method sets d->bWantToQuit to true.
478  if (d->trayIcon != nullptr && gConfig.doomseeker.bCloseToTrayIcon && !d->bWantToQuit)
479  {
480  d->bWasMaximized = isMaximized();
481  event->ignore();
482  hide();
483  }
484  else
485  event->accept();
486 }
487 
488 void MainWindow::confirmUpdateInstallation()
489 {
490  assert(d->autoUpdater != nullptr && "MainWindow::confirmUpdateInstallation()");
491  d->updatesConfirmationWidget->hide();
492  d->autoUpdater->confirmDownloadAndInstall();
493 }
494 
495 void MainWindow::connectIP2CLoader()
496 {
497  this->connect(d->ip2cLoader, SIGNAL(finished()), SLOT(ip2cJobsFinished()));
498  this->connect(d->ip2cLoader, SIGNAL(downloadProgress(qint64,qint64)),
499  SLOT(ip2cDownloadProgress(qint64,qint64)));
500 }
501 
502 void MainWindow::discardUpdates()
503 {
504  assert(d->autoUpdater != nullptr && "MainWindow::confirmUpdateInstallation()");
505  d->updatesConfirmationWidget->hide();
506  // User rejected this update so let's add the packages
507  // to the ignore list so user won't be nagged again.
508  const QList<UpdatePackage> &pkgList = d->autoUpdater->newUpdatePackages();
509  for (const UpdatePackage &pkg : pkgList)
510  {
511  gConfig.autoUpdates.lastKnownUpdateRevisions.insert(pkg.name, pkg.revision);
512  }
513  d->autoUpdater->abort();
514 }
515 
517 {
518  // Connect refreshing thread.
519  connect(gRefresher, SIGNAL(block()), this, SLOT(blockRefreshButtons()));
520  connect(gRefresher, SIGNAL(finishedQueryingMaster(MasterClient*)), this, SLOT(finishedQueryingMaster(MasterClient*)));
521  connect(gRefresher, SIGNAL(sleepingModeEnter()), this, SLOT(refreshThreadEndsWork()));
522  connect(gRefresher, SIGNAL(sleepingModeEnter()), d->buddiesList, SLOT(scan()));
523  connect(gRefresher, SIGNAL(sleepingModeExit()), this, SLOT(refreshThreadBeginsWork()));
524 
525  // Controls
526  connect(d->menuActionAbout, SIGNAL(triggered()), this, SLOT(menuHelpAbout()));
527  connect(d->menuActionAboutQt, SIGNAL(triggered()), d->application, SLOT(aboutQt()));
528  connect(d->menuActionBuddies, SIGNAL(triggered()), this, SLOT(menuBuddies()));
529  connect(d->menuActionConfigure, SIGNAL(triggered()), this, SLOT(menuOptionsConfigure()));
530  connect(d->menuActionCreateServer, SIGNAL(triggered()), this, SLOT(menuCreateServer()));
531  connect(d->menuActionHelp, SIGNAL(triggered()), this, SLOT (menuHelpHelp()));
532  connect(d->menuActionIRCOptions, SIGNAL(triggered()), this, SLOT(menuIRCOptions()));
533  connect(d->menuActionLog, SIGNAL(triggered()), this, SLOT(menuLog()));
534  connect(d->menuActionManageDemos, SIGNAL(triggered()), this, SLOT(menuManageDemos()));
535  connect(d->menuActionRecordDemo, SIGNAL(triggered()), this, SLOT(menuRecordDemo()));
536  connect(d->menuActionUpdateIP2C, SIGNAL(triggered()), this, SLOT(menuUpdateIP2C()));
537  connect(d->menuActionQuit, SIGNAL(triggered()), this, SLOT(quitProgram()));
538  connect(d->menuActionViewIRC, SIGNAL(triggered()), this, SLOT(menuViewIRC()));
539  connect(d->menuActionWadseeker, SIGNAL(triggered()), this, SLOT(menuWadSeeker()));
540  connect(d->serverFilterDock, SIGNAL(filterUpdated(const ServerListFilterInfo&)),
541  this, SLOT(updateServerFilter(const ServerListFilterInfo&)));
542  connect(d->serverFilterDock, SIGNAL(nonEmptyServerGroupingAtTopToggled(bool)),
543  d->serverList, SLOT(setGroupServersWithPlayersAtTop(bool)));
544  connect(d->serverList, SIGNAL(serverFilterModified(ServerListFilterInfo)),
545  d->serverFilterDock, SLOT(setFilterInfo(ServerListFilterInfo)));
546  connect(d->serverList, SIGNAL(serverDoubleClicked(ServerPtr)), this, SLOT(runGame(ServerPtr)));
547  connect(d->serverList, SIGNAL(displayServerJoinCommandLine(const ServerPtr&)), this, SLOT(showServerJoinCommandLine(const ServerPtr&)));
548  connect(d->serverList, SIGNAL(findMissingWADs(const ServerPtr&)), this, SLOT(findMissingWADs(const ServerPtr&)));
549  connect(d->serverList, SIGNAL(serverInfoUpdated(ServerPtr)), this, SLOT(serverAddedToList(ServerPtr)));
550  connect(d->buddiesList, SIGNAL(scanCompleted()), d->serverList, SLOT(redraw()));
551 }
552 
554 {
555  // This is called only once from the constructor. No clears to
556  // d->queryMenuPorts are ever performed. Not even in the destructor.
557  for (unsigned i = 0; i < gPlugins->numPlugins(); ++i)
558  {
559  const EnginePlugin *plugin = gPlugins->info(i);
560  if (!plugin->data()->hasMasterClient() && !plugin->data()->hasBroadcast())
561  continue;
562 
563  if (plugin->data()->hasMasterClient())
564  {
565  MasterClient *pMasterClient = plugin->data()->masterClient;
566  pMasterClient->updateAddress();
567  masterManager->addMaster(pMasterClient);
568  }
569 
570  if (plugin->data()->hasBroadcast())
571  d->broadcastManager->registerPlugin(plugin);
572 
573  // Now is a good time to also populate the status bar widgets
574  auto statusWidget = new ServersStatusWidget(plugin, d->serverList);
575  d->serversStatusesWidgets.insert(plugin, statusWidget);
576 
577  this->connect(statusWidget, SIGNAL(clicked(const EnginePlugin*)),
578  SLOT(togglePluginQueryEnabled(const EnginePlugin*)));
579  this->connect(statusWidget, SIGNAL(counterUpdated()),
580  SLOT(updateRefreshProgress()));
581 
582  statusBar()->addPermanentWidget(statusWidget);
583 
584  QString name = gPlugins->info(i)->data()->name;
585  auto query = new QueryMenuAction(plugin, statusWidget, d->menuQuery);
586  d->queryMenuPorts.insert(plugin, query);
587 
588  d->menuQuery->addAction(query);
589 
590  query->setCheckable(true);
591  query->setIcon(plugin->icon());
592  query->setText(name);
593 
594  IniSection pluginConfig = gConfig.iniSectionForPlugin(name);
595 
596  if (!pluginConfig.retrieveSetting("Query").value().isNull())
597  {
598  bool enabled = pluginConfig["Query"];
599  setQueryPluginEnabled(plugin, enabled);
600  }
601  else
602  {
603  // if no setting is found for this engine
604  // set default to true:
605  setQueryPluginEnabled(plugin, true);
606  }
607  }
608 }
609 
610 void MainWindow::findMissingWADs(const ServerPtr &server)
611 {
612  // Display a message if all WADs are present.
613  QList<PWad> wads = server->wads();
614  PathFinder pathFinder = server->wadPathFinder();
615  QList<PWad> missingWads;
616  QList<PWad> incompatibleWads;
617 
618  auto checkWadsDlg = new CheckWadsDlg(&pathFinder);
619  checkWadsDlg->addWads(wads);
620  const CheckResult checkResults = checkWadsDlg->checkWads();
621 
622  for (const PWad &wad : checkResults.missingWads)
623  {
624  missingWads << PWad(wad.name(), true, wad.checksums());
625  }
626  incompatibleWads << checkResults.incompatibleWads;
627 
628  if (missingWads.isEmpty() && incompatibleWads.isEmpty())
629  {
630  QMessageBox::information(this, tr("All WADs found"), tr("All of the WADs used by this server are present."));
631  return;
632  }
633 
634  MissingWadsDialog dialog(missingWads, incompatibleWads, server->plugin(), this);
635  dialog.setAllowIgnore(false);
636  if (dialog.exec() == QDialog::Accepted && dialog.decision() == MissingWadsDialog::Install)
637  {
638  if (!gWadseekerShow->checkWadseekerValidity(this))
639  return;
640  WadseekerInterface *wadseeker = WadseekerInterface::createAutoNoGame();
641  wadseeker->setCustomSites(server->allWebSites());
642  wadseeker->setWads(dialog.filesToDownload());
643  wadseeker->setAttribute(Qt::WA_DeleteOnClose);
644  wadseeker->show();
645  }
646 }
647 
648 void MainWindow::finishConfiguration(DoomseekerConfigurationDialog &configDialog, bool lookupHostsChanged)
649 {
650  // In case the master server addresses changed, notify the master clients.
651  updateMasterAddresses();
652  gRefresher->setDelayBetweenResends(gConfig.doomseeker.querySpeed().delayBetweenSingleServerAttempts);
653 
654  // If appearance changed - update the widgets.
655  if (configDialog.wasAppearanceChanged())
656  {
657  updateDynamicAppearance();
658  initTrayIcon();
659  }
660 
661  // If changes require a restart, tell the user
662  if (configDialog.isRestartNeeded())
663  {
664  QString warningRestartNeeded = tr("Doomseeker needs to be restarted for some changes to be applied.");
665  d->importantMessagesWidget->addMessage(warningRestartNeeded);
666  }
667 
668  // Do the following only if setting changed from false to true.
669  if (lookupHostsChanged)
670  d->serverList->lookupHosts();
671 
672  // Refresh custom servers list:
673  if (configDialog.customServersChanged())
674  {
675  d->serverList->removeCustomServers();
676  QList<ServerPtr> servers = d->masterManager->customServs()->readConfig();
677  for (ServerPtr server : servers)
678  {
679  d->serverList->registerServer(server);
680  }
681  refreshCustomServers();
682  }
683 }
684 
685 void MainWindow::finishedQueryingMaster(MasterClient *master)
686 {
687  if (master == nullptr)
688  return;
689 
690  for (int i = 0; i < master->numServers(); i++)
691  d->serverList->registerServer((*master)[i]);
692 }
693 
694 void MainWindow::fixIconsDpi()
695 {
696  // http://blog.qt.io/blog/2013/04/25/retina-display-support-for-mac-os-ios-and-x11/
697  QIcon icon(":/icons/exclamation_16.png");
698  d->lblExclamation1->setPixmap(icon.pixmap(16));
699  d->lblExclamation2->setPixmap(icon.pixmap(16));
700 }
701 
702 void MainWindow::getServers()
703 {
704  // Check if this operation has any sense.
705  if (!isAnythingToRefresh())
706  {
707  QString message = tr("Doomseeker is unable to proceed with the refresh"
708  " operation because the following problem has occurred:\n\n");
709 
710  if (gPlugins->numPlugins() == 0)
711  message += tr("Plugins are missing from the \"engines/\" directory.");
712  else if (!isAnyMasterEnabled())
713  message += tr("No master servers are enabled in the \"Query\" menu.");
714  else
715  message += tr("Unknown error occurred.");
716 
717  gLog << message;
718  QMessageBox::warning(this, tr("Doomseeker - refresh problem"), message);
719  return;
720  }
721 
722  d->bTotalRefreshInProcess = true;
723  d->autoRefreshTimer.stop();
724  gLog << tr("Total refresh initialized!");
725 
726  // Remove all non special servers. Master servers will
727  // "re-provide" them if they are still alive. Whatever
728  // remains on the list should be refreshed now.
729  d->serverList->removeNonSpecialServers();
730  refreshServersOnList();
731 
732  if (!isAnyMasterEnabled() && !d->serverList->hasAtLeastOneServer())
733  {
734  gLog << tr("Warning: No master servers were enabled for this refresh. "
735  "Check your Query menu or \"engines/\" directory.");
736  }
737 
738  d->masterManager->clearServers();
739  for (int i = 0; i < d->masterManager->numMasters(); ++i)
740  {
741  MasterClient *pMaster = (*d->masterManager)[i];
742 
743  if (pMaster->isEnabled())
744  gRefresher->registerMaster(pMaster);
745  }
746 }
747 
748 bool MainWindow::hasCustomServers() const
749 {
750  CustomServers *customServers = d->masterManager->customServs();
751  return customServers->numServers() > 0;
752 }
753 
755 {
756  const unsigned MIN_DELAY = 30;
757  const unsigned MAX_DELAY = 3600;
758 
759  bool bEnabled = gConfig.doomseeker.bQueryAutoRefreshEnabled;
760 
761  if (!bEnabled)
762  d->autoRefreshTimer.stop();
763  else
764  {
765  // If delay value is out of bounds we should adjust
766  // config value as well.
767  unsigned &delay = gConfig.doomseeker.queryAutoRefreshEverySeconds;
768 
769  // Make sure delay is in given limit.
770  if (delay < MIN_DELAY)
771  delay = MIN_DELAY;
772  else if (delay > MAX_DELAY)
773  delay = MAX_DELAY;
774 
775  unsigned delayMs = delay * 1000;
776 
777  d->autoRefreshTimer.setSingleShot(false);
778  d->autoRefreshTimer.start(delayMs);
779  }
780 }
781 
782 void MainWindow::initAutoUpdaterWidgets()
783 {
784  static const int FILE_BAR_WIDTH = 50;
785  static const int OVERALL_BAR_WIDTH = 180;
786 
787  d->autoUpdaterStatusBarWidget = new QWidget(statusBar());
788  d->autoUpdaterStatusBarWidget->setLayout(new QHBoxLayout(d->autoUpdaterStatusBarWidget));
789  d->autoUpdaterStatusBarWidget->layout()->setContentsMargins(QMargins(0, 0, 0, 0));
790  statusBar()->addPermanentWidget(d->autoUpdaterStatusBarWidget);
791  d->autoUpdaterStatusBarWidget->hide();
792 
793  d->autoUpdaterLabel = new QLabel(d->autoUpdaterStatusBarWidget);
794  d->autoUpdaterLabel->setText(tr("Auto Updater:"));
795  d->autoUpdaterStatusBarWidget->layout()->addWidget(d->autoUpdaterLabel);
796 
797  d->autoUpdaterFileProgressBar = mkStdProgressBarForStatusBar();
798  d->autoUpdaterFileProgressBar->setFormat("%p%");
799  d->autoUpdaterFileProgressBar->setMaximumWidth(FILE_BAR_WIDTH);
800  d->autoUpdaterFileProgressBar->setMinimumWidth(FILE_BAR_WIDTH);
801  d->autoUpdaterStatusBarWidget->layout()->addWidget(d->autoUpdaterFileProgressBar);
802 
803  d->autoUpdaterOverallProgressBar = mkStdProgressBarForStatusBar();
804  d->autoUpdaterOverallProgressBar->setMaximumWidth(OVERALL_BAR_WIDTH);
805  d->autoUpdaterOverallProgressBar->setMinimumWidth(OVERALL_BAR_WIDTH);
806  d->autoUpdaterStatusBarWidget->layout()->addWidget(d->autoUpdaterOverallProgressBar);
807 
808  d->autoUpdaterAbortButton = new QPushButton(statusBar());
809  d->autoUpdaterAbortButton->setToolTip(tr("Abort update."));
810  d->autoUpdaterAbortButton->setIcon(QIcon(":/icons/x.png"));
811  this->connect(d->autoUpdaterAbortButton, SIGNAL(clicked()),
812  SLOT(abortAutoUpdater()));
813  d->autoUpdaterStatusBarWidget->layout()->addWidget(d->autoUpdaterAbortButton);
814 }
815 
816 void MainWindow::initIP2CUpdater()
817 {
818  static const int PROGRESSBAR_WIDTH = 220;
819 
820  d->ip2cUpdateProgressBar = mkStdProgressBarForStatusBar();
821  d->ip2cUpdateProgressBar->setFormat(tr("IP2C Update"));
822  d->ip2cUpdateProgressBar->hide();
823  d->ip2cUpdateProgressBar->setMaximumWidth(PROGRESSBAR_WIDTH);
824  d->ip2cUpdateProgressBar->setMinimumWidth(PROGRESSBAR_WIDTH);
825  statusBar()->addPermanentWidget(d->ip2cUpdateProgressBar);
826 }
827 
828 void MainWindow::initIRCDock()
829 {
830  d->ircDock = new IRCDock(this);
831  d->menuView->addAction(d->ircDock->toggleViewAction());
832  d->ircDock->toggleViewAction()->setText(tr("&IRC"));
833  d->ircDock->toggleViewAction()->setShortcut(tr("CTRL+I"));
834  d->ircDock->hide();
835  this->addDockWidget(Qt::BottomDockWidgetArea, d->ircDock);
836 
837  if (ChatNetworksCfg().isAnyNetworkOnAutoJoin())
838  {
839  this->d->ircDock->setVisible(true);
840  this->d->ircDock->performNetworkAutojoins();
841  }
842 }
843 
844 void MainWindow::initLogDock()
845 {
846  d->logDock = new LogDock(this);
847  d->menuView->addAction(d->logDock->toggleViewAction());
848  d->logDock->toggleViewAction()->setText(tr("&Log"));
849  d->logDock->toggleViewAction()->setShortcut(tr("CTRL+L"));
850  d->logDock->hide();
851  this->addDockWidget(Qt::BottomDockWidgetArea, d->logDock);
852 
853  connect(&gLog, SIGNAL(newEntry(const QString&)), d->logDock, SLOT(appendLogEntry(const QString&)));
854 
855  // Also add anything that already might be in the log to the box.
856  d->logDock->appendLogEntry(gLog.content());
857 }
858 
859 void MainWindow::initMainDock()
860 {
861  setDockNestingEnabled(true); // This line allows us to essentially treat a dock as a central widget.
862 
863  // Make a dock out of the central MainWindow widget and drop that widget
864  // from the MainWindow itself.
865  d->mainDock = new QDockWidget(tr("Servers"));
866  d->mainDock->setTitleBarWidget(new QWidget(this));
867  d->mainDock->setObjectName("ServerList");
868  d->mainDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
869  d->mainDock->setWidget(centralWidget());
870  setCentralWidget(nullptr);
871  addDockWidget(Qt::RightDockWidgetArea, d->mainDock);
872 }
873 
874 void MainWindow::initServerDetailsDock()
875 {
876  d->detailsDock = new ServerDetailsDock(this);
877  d->menuView->addAction(d->detailsDock->toggleViewAction());
878  d->detailsDock->toggleViewAction()->setText(tr("Server &details"));
879  d->detailsDock->toggleViewAction()->setShortcut(tr("CTRL+D"));
880  d->detailsDock->hide();
881  addDockWidget(Qt::BottomDockWidgetArea, d->detailsDock);
882 
883  d->detailsDock->connect(d->serverList, SIGNAL(serversSelected(QList<ServerPtr>&)), SLOT(displaySelection(QList<ServerPtr> &)));
884 }
885 
886 void MainWindow::initServerFilterDock()
887 {
888  d->serverFilterDock = new ServerFilterDock(this);
889 
890  d->menuView->addAction(d->serverFilterDock->toggleViewAction());
891  d->serverFilterDock->hide();
892  this->addDockWidget(Qt::RightDockWidgetArea, d->serverFilterDock);
893 }
894 
896 {
897  bool isEnabled = gConfig.doomseeker.bUseTrayIcon;
898  if (!isEnabled || !QSystemTrayIcon::isSystemTrayAvailable())
899  {
900  if (d->trayIcon != nullptr)
901  {
902  delete d->trayIcon;
903  d->trayIcon = nullptr;
904  }
905 
906  if (d->trayIconMenu != nullptr)
907  {
908  delete d->trayIconMenu;
909  d->trayIconMenu = nullptr;
910  }
911  }
912  else if (d->trayIcon == nullptr)
913  {
914  QAction *trayAction;
915  d->trayIconMenu = new QMenu(this);
916  trayAction = d->trayIconMenu->addAction("Exit");
917  connect(trayAction, SIGNAL(triggered()), this, SLOT(quitProgram()));
918 
919  // This should be automatically deleted when main window closes
920  d->trayIcon = new QSystemTrayIcon(this);
921  connect(d->trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayIcon_activated(QSystemTrayIcon::ActivationReason)));
922 
923  updateTrayIconTooltipAndLogTotalRefresh();
924 
925  d->trayIcon->setContextMenu(d->trayIconMenu);
926  d->trayIcon->setIcon(QIcon(":/icon.png"));
927  d->trayIcon->setVisible(true);
928  }
929 }
930 
931 void MainWindow::ip2cDownloadProgress(qint64 current, qint64 max)
932 {
933  d->ip2cUpdateProgressBar->setMaximum(max);
934  d->ip2cUpdateProgressBar->setValue(current);
935 }
936 
937 void MainWindow::ip2cJobsFinished()
938 {
939  d->menuActionUpdateIP2C->setEnabled(true);
940  d->serverList->updateCountryFlags();
941  d->ip2cUpdateProgressBar->hide();
942 
943  if (d->ip2cLoader != nullptr)
944  {
945  delete d->ip2cLoader;
946  d->ip2cLoader = nullptr;
947  }
948 }
949 
950 void MainWindow::ip2cStartUpdate()
951 {
952  if (d->ip2cLoader != nullptr)
953  {
954  // If update is currently in progress then prevent re-starting.
955  return;
956  }
957 
958  d->menuActionUpdateIP2C->setEnabled(false);
959  d->ip2cUpdateProgressBar->show();
960 
961  d->ip2cLoader = new IP2CLoader();
962  connectIP2CLoader();
963  d->ip2cLoader->update();
964 }
965 
967 {
968  return hasCustomServers() || isAnyMasterEnabled();
969 }
970 
971 bool MainWindow::isAnyMasterEnabled() const
972 {
973  for (int i = 0; i < d->masterManager->numMasters(); ++i)
974  {
975  MasterClient *pMaster = (*d->masterManager)[i];
976 
977  if (pMaster->isEnabled())
978  return true;
979  }
980 
981  return false;
982 }
983 
984 bool MainWindow::isEffectivelyActiveWindow() const
985 {
986  return this->isActiveWindow() || DoomseekerConfigurationDialog::isOpen();
987 }
988 
989 void MainWindow::masterManagerMessages(MasterClient *pSender, const QString &title, const QString &content, bool isError)
990 {
991  Q_UNUSED(title);
992  QString message = tr("Master server for %1: %2").arg(pSender->plugin()->data()->name).arg(content);
993 
994  if (isError)
995  {
996  message = tr("Error: %1").arg(message);
997  statusBar()->showMessage(message);
998  }
999 
1000  gLog << message;
1001 }
1002 
1003 void MainWindow::masterManagerMessagesImportant(MasterClient *pSender, const Message &objMessage)
1004 {
1005  QString strFullMessage = tr("%1: %2")
1006  .arg(pSender->plugin()->data()->name)
1007  .arg(objMessage.contents());
1008  d->importantMessagesWidget->addMessage(strFullMessage, objMessage.timestamp());
1009 }
1010 
1011 void MainWindow::menuBuddies()
1012 {
1013  d->buddiesList->setVisible(!d->buddiesList->isVisible());
1014 }
1015 
1016 void MainWindow::menuCreateServer()
1017 {
1018  // This object will auto-delete on close.
1019  auto dialog = new CreateServerDialog(GameCreateParams::Host, nullptr);
1020  dialog->setWindowIcon(this->windowIcon());
1021  dialog->show();
1022 }
1023 
1024 void MainWindow::menuHelpAbout()
1025 {
1026  AboutDialog dlg(this);
1027  d->autoRefreshTimer.stop();
1028  dlg.exec();
1030 }
1031 
1032 void MainWindow::menuHelpHelp()
1033 {
1034  if (HELP_SITE_URL.isEmpty() || !Strings::isUrlSafe(HELP_SITE_URL))
1035  {
1036  QMessageBox::critical(this, tr("Help error"), tr("No help found."), QMessageBox::Ok, QMessageBox::Ok);
1037  return;
1038  }
1039 
1040  bool bSuccess = QDesktopServices::openUrl(HELP_SITE_URL);
1041 
1042  if (!bSuccess)
1043  {
1044  QMessageBox::critical(this, tr("Help error"), tr("Failed to open URL:\n%1").arg(HELP_SITE_URL), QMessageBox::Ok, QMessageBox::Ok);
1045  return;
1046  }
1047 }
1048 
1049 void MainWindow::menuIRCOptions()
1050 {
1051  IRCConfigurationDialog dialog(this);
1052  dialog.initOptionsList();
1053  dialog.exec();
1054 
1055  if (d->ircDock != nullptr)
1056  {
1057  d->ircDock->applyAppearanceSettings();
1058 
1059  // This could probably be optimized to not re-read files from drive
1060  // if audio options didn't change but currently there are only two
1061  // files, so no harm should be done.
1062  d->ircDock->sounds().loadFromConfig();
1063  }
1064 }
1065 
1066 void MainWindow::menuLog()
1067 {
1068  d->logDock->setVisible(!d->logDock->isVisible());
1069 }
1070 
1071 void MainWindow::menuManageDemos()
1072 {
1073  DemoManagerDlg dm;
1074  dm.setWindowIcon(this->windowIcon());
1075  dm.exec();
1076 }
1077 
1078 void MainWindow::menuOptionsConfigure()
1079 {
1080  DoomseekerConfigurationDialog::openConfiguration(this);
1081 }
1082 
1083 void MainWindow::menuRecordDemo()
1084 {
1085  gConfig.doomseeker.bRecordDemo = d->menuActionRecordDemo->isChecked();
1086 }
1087 
1088 void MainWindow::menuUpdateIP2C()
1089 {
1090  IP2CUpdateBox updateBox(this);
1091 
1092  connect(&updateBox, SIGNAL(accepted()), this, SLOT(ip2cStartUpdate()));
1093 
1094  updateBox.exec();
1095 }
1096 
1097 void MainWindow::menuViewIRC()
1098 {
1099  d->ircDock->setVisible(!d->ircDock->isVisible());
1100 }
1101 
1102 void MainWindow::menuWadSeeker()
1103 {
1104  if (gWadseekerShow->checkWadseekerValidity(this))
1105  {
1106  WadseekerInterface *wadseeker = WadseekerInterface::create(nullptr);
1107  wadseeker->setAttribute(Qt::WA_DeleteOnClose);
1108  wadseeker->show();
1109  }
1110 }
1111 
1112 QProgressBar *MainWindow::mkStdProgressBarForStatusBar()
1113 {
1114  auto pBar = new QProgressBar(statusBar());
1115  pBar->setAlignment(Qt::AlignCenter);
1116  pBar->setTextVisible(true);
1117  pBar->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
1118  return pBar;
1119 }
1120 
1121 void MainWindow::notifyFirstRun()
1122 {
1123  // On first run prompt configuration box.
1124  QMessageBox::information(nullptr, tr("Welcome to Doomseeker"),
1125  tr("Before you start browsing for servers, please ensure that Doomseeker is properly configured."));
1126  d->menuActionConfigure->trigger();
1127 }
1128 
1129 void MainWindow::onAutoUpdaterDownloadAndInstallConfirmationRequest()
1130 {
1131  d->updatesConfirmationWidget->show();
1132 }
1133 
1134 void MainWindow::onAutoUpdaterFileProgress(qint64 bytesReceived, qint64 bytesTotal)
1135 {
1136  d->autoUpdaterFileProgressBar->setValue(bytesReceived);
1137  d->autoUpdaterFileProgressBar->setMaximum(bytesTotal);
1138 }
1139 
1140 void MainWindow::onAutoUpdaterFinish()
1141 {
1142  showAndLogStatusMessage(tr("Program update detection & download finished with status: [%1] %2")
1143  .arg((int)d->autoUpdater->errorCode()).arg(d->autoUpdater->errorString()));
1144  gConfig.autoUpdates.bPerformUpdateOnNextRun = false;
1145  if (d->autoUpdater->errorCode() == AutoUpdater::EC_Ok)
1146  {
1147  UpdateChannel channel = UpdateChannel::fromName(gConfig.autoUpdates.updateChannelName);
1148  if (channel == *d->updateChannelOnUpdateStart)
1149  {
1150  if (!d->autoUpdater->downloadedPackagesFilenames().isEmpty())
1151  {
1152  gLog << tr("Updates will be installed on next program start.");
1153  d->updatesDownloadedWidget->show();
1154  gConfig.autoUpdates.bPerformUpdateOnNextRun = true;
1155  }
1156  }
1157  else
1158  gLog << tr("Update channel was changed during update process. Discarding update.");
1159  }
1160  gConfig.saveToFile();
1161  d->autoUpdaterStatusBarWidget->hide();
1162  d->updatesConfirmationWidget->hide();
1163  d->autoUpdater->deleteLater();
1164  d->autoUpdater = nullptr;
1165 }
1166 
1167 void MainWindow::onAutoUpdaterOverallProgress(int current, int total,
1168  const QString &msg)
1169 {
1170  d->autoUpdaterOverallProgressBar->setValue(current);
1171  d->autoUpdaterOverallProgressBar->setMaximum(total);
1172  d->autoUpdaterOverallProgressBar->setFormat(msg);
1173 }
1174 
1175 void MainWindow::postInitAppStartup()
1176 {
1177  // Load server filter from config.
1178  d->serverFilterDock->setFilterInfo(gConfig.serverFilter.info);
1179 
1180  fixIconsDpi();
1181 
1182  // Check query on statup
1183  // Let's see if we have any plugins first. If not, display error.
1184  if (gPlugins->numPlugins() > 0)
1185  {
1186  bool bGettingServers = false;
1187  bool queryOnStartup = gConfig.doomseeker.bQueryOnStartup;
1188  if (queryOnStartup)
1189  {
1190  // If "Query on startup" option is enabled we should
1191  // attempt to refresh any masters that are enabled
1192  // in the Query menu.
1193 
1194  if (isAnyMasterEnabled())
1195  {
1196  bGettingServers = true;
1197  getServers();
1198  }
1199  else
1200  gLog << tr("Query on startup warning: No master servers are enabled in the Query menu.");
1201  }
1202 
1203  // If we already successfully called the getServers() method
1204  // there is no need to call refreshCustomServers().
1205  if (!bGettingServers && hasCustomServers())
1206  {
1207  // Custom servers should be refreshed no matter what.
1208  // They will not block the app in any way, there is no reason
1209  // not to refresh them.
1210  refreshCustomServers();
1211  }
1212  }
1213  else
1214  {
1215  // There are no plugins so something is really bad.
1216  // Display error message.
1217  QString error = tr("Doomseeker was unable to find any plugin libraries.\n"
1218  "Although the application will still work it will not be possible "
1219  "to fetch any server info or launch any game.\n\n"
1220  "Please check if there are any files in \"engines/\" directory.\n"
1221  "To fix this problem you may try downloading Doomseeker "
1222  "again from the site specified in the Help|About box and reinstalling "
1223  "Doomseeker.");
1224  QMessageBox::critical(nullptr, tr("Doomseeker critical error"), error);
1225  }
1226 }
1227 
1228 QueryMenuAction *MainWindow::queryMenuActionForPlugin(const EnginePlugin *plugin)
1229 {
1230  if (plugin == nullptr)
1231  return nullptr;
1232 
1233  if (d->queryMenuPorts.contains(plugin))
1234  return d->queryMenuPorts[plugin];
1235 
1236  return nullptr;
1237 }
1238 
1239 void MainWindow::quitProgram()
1240 {
1241  d->bWantToQuit = true;
1242  QApplication::closeAllWindows();
1243 }
1244 
1245 void MainWindow::refreshCustomServers()
1246 {
1247  for (const ServerPtr &server : d->serverList->servers())
1248  {
1249  if (server->isCustom())
1250  gRefresher->registerServer(server.data());
1251  }
1252 }
1253 
1254 void MainWindow::refreshServersOnList()
1255 {
1256  for (const ServerPtr &server : d->serverList->servers())
1257  {
1258  gRefresher->registerServer(server.data());
1259  }
1260 }
1261 
1262 void MainWindow::refreshThreadBeginsWork()
1263 {
1264  statusBar()->showMessage(tr("Querying..."));
1265  d->taskbarProgress->show();
1266 }
1267 
1268 void MainWindow::refreshThreadEndsWork()
1269 {
1270  d->toolBarGetServers->setEnabled(true);
1271 
1272  d->serverList->cleanUpRightNow();
1273  statusBar()->showMessage(tr("Done"));
1274  updateTrayIconTooltipAndLogTotalRefresh();
1275  d->taskbarProgress->hide();
1276 
1277  if (d->bTotalRefreshInProcess)
1279 
1280  d->bTotalRefreshInProcess = false;
1281  QList<ServerPtr> selectedServers = d->serverList->selectedServers();
1282  d->detailsDock->displaySelection(selectedServers);
1283 }
1284 
1285 void MainWindow::restartAndInstallUpdatesNow()
1286 {
1288  quitProgram();
1289 }
1290 
1291 void MainWindow::runGame(const ServerPtr &server)
1292 {
1293  if (d->connectionHandler)
1294  delete d->connectionHandler;
1295 
1296  d->connectionHandler = new ConnectionHandler(server, this);
1297  d->connectionHandler->run();
1298 }
1299 
1300 void MainWindow::setQueryPluginEnabled(const EnginePlugin *plugin, bool bEnabled)
1301 {
1302  assert(plugin != nullptr);
1303 
1304  QueryMenuAction *pAction = queryMenuActionForPlugin(plugin);
1305  if (pAction != nullptr)
1306  {
1307  pAction->setChecked(bEnabled);
1308  if (plugin->data()->hasMasterClient())
1309  plugin->data()->masterClient->setEnabled(bEnabled);
1310  if (plugin->data()->hasBroadcast())
1311  plugin->data()->broadcast->setEnabled(bEnabled);
1312  d->serversStatusesWidgets[plugin]->setMasterEnabledStatus(bEnabled);
1313  }
1314 }
1315 
1316 void MainWindow::serverAddedToList(const ServerPtr &pServer)
1317 {
1318  if (pServer->isKnown())
1319  {
1320  const QString &gameMode = pServer->gameMode().name();
1321  d->serverFilterDock->addGameModeToComboBox(gameMode);
1322  }
1323 }
1324 
1326 {
1327  assert(this->d->updaterInstallerErrorCode == 0 &&
1328  "MainWindow::setDisplayUpdaterProcessFailure()");
1329  this->d->updaterInstallerErrorCode = errorCode;
1330  QTimer::singleShot(0, this, SLOT(showUpdaterProcessErrorDialog()));
1331 }
1332 
1334 {
1335  assert(this->d->updaterInstallerErrorCode == 0 &&
1336  "MainWindow::setDisplayUpdateInstallerError()");
1337  this->d->updaterInstallerErrorCode = errorCode;
1338  QTimer::singleShot(0, this, SLOT(showUpdateInstallErrorDialog()));
1339 }
1340 
1341 void MainWindow::setupIcons()
1342 {
1343  QStyle &style = *QApplication::style();
1344 
1345  // File menu.
1346  d->menuActionQuit->setIcon(style.standardIcon(QStyle::SP_TitleBarCloseButton));
1347 
1348  // Help menu.
1349  d->menuActionHelp->setIcon(style.standardIcon(QStyle::SP_MessageBoxQuestion));
1350  d->menuActionAbout->setIcon(style.standardIcon(QStyle::SP_MessageBoxInformation));
1351 }
1352 
1353 void MainWindow::setupToolBar()
1354 {
1355  QToolBar *pToolBar = new QToolBar(tr("Main Toolbar"), this);
1356  pToolBar->setMovable(false);
1357  pToolBar->setObjectName("Toolbar");
1358 
1359  // Refresh buttons
1360  d->toolBarGetServers = new QAction(QIcon(":/icons/refresh.png"), tr("Get Servers"), pToolBar);
1361 
1362  // Setup menu
1363  // Refresh buttons
1364  pToolBar->addAction(d->toolBarGetServers);
1365 
1366  // File menu buttons.
1367  pToolBar->addSeparator();
1368  pToolBar->addAction(d->menuActionCreateServer);
1369  pToolBar->addAction(d->menuActionWadseeker);
1370 
1371  // Demo buttons
1372  pToolBar->addSeparator();
1373  pToolBar->addAction(d->menuActionManageDemos);
1374  pToolBar->addAction(d->menuActionRecordDemo);
1375 
1376  pToolBar->addSeparator();
1377 
1378  // Dockable windows buttons.
1379  pToolBar->addAction(d->buddiesList->toggleViewAction());
1380  pToolBar->addAction(d->logDock->toggleViewAction());
1381  pToolBar->addAction(d->ircDock->toggleViewAction());
1382  pToolBar->addAction(d->serverFilterDock->toggleViewAction());
1383  pToolBar->addAction(d->detailsDock->toggleViewAction());
1384 
1385  // Quick Search
1386  QLineEdit *qs = d->serverFilterDock->createQuickSearch();
1387  qs->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
1388  qs->setMinimumWidth(175);
1389  qs->setMaximumWidth(175);
1390 
1391  QWidget *searchSeparator = new QWidget();
1392  searchSeparator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
1393  pToolBar->addWidget(searchSeparator);
1394  pToolBar->addWidget(new QLabel(tr("Search:"), pToolBar));
1395  pToolBar->addWidget(qs);
1396 
1397  this->addToolBar(Qt::TopToolBarArea, pToolBar);
1398  setUnifiedTitleAndToolBarOnMac(true);
1399  connect(pToolBar, SIGNAL(actionTriggered(QAction*)), this, SLOT(toolBarAction(QAction*)));
1400 }
1401 
1402 void MainWindow::showAndLogStatusMessage(const QString &message)
1403 {
1404  gLog << message;
1405  statusBar()->showMessage(message);
1406 }
1407 
1408 void MainWindow::showEvent(QShowEvent *event)
1409 {
1410  Q_UNUSED(event);
1411  // http://stackoverflow.com/a/26910648/1089357
1412  d->taskbarButton->setWindow(windowHandle());
1413 }
1414 
1415 void MainWindow::showInstallFreedoomDialog()
1416 {
1417  if (!d->freedoomDialog.isNull())
1418  {
1419  d->freedoomDialog->activateWindow();
1420  return;
1421  }
1422  auto dialog = new FreedoomDialog(nullptr);
1423  dialog->setAttribute(Qt::WA_DeleteOnClose);
1424  dialog->show();
1425  d->freedoomDialog = dialog;
1426 }
1427 
1428 void MainWindow::showProgramArgsHelp()
1429 {
1430  auto dialog = new ProgramArgsHelpDialog(this);
1431  dialog->setAttribute(Qt::WA_DeleteOnClose);
1432  dialog->show();
1433 }
1434 
1435 void MainWindow::showServerJoinCommandLine(const ServerPtr &server)
1436 {
1437  CommandLineInfo cli;
1438  ConnectionHandler connectionHandler(server, this);
1439  GameDemo demo = gConfig.doomseeker.bRecordDemo ? GameDemo::Unmanaged : GameDemo::NoDemo;
1440  JoinCommandLineBuilder *builder = new JoinCommandLineBuilder(server, demo, this);
1441  this->connect(builder, SIGNAL(commandLineBuildFinished()), SLOT(showServerJoinCommandLineOnBuilderFinished()));
1442  builder->obtainJoinCommandLine();
1443 }
1444 
1445 void MainWindow::showServerJoinCommandLineOnBuilderFinished()
1446 {
1447  auto builder = static_cast<JoinCommandLineBuilder *>(sender());
1448  CommandLineInfo cli = builder->builtCommandLine();
1449  if (cli.isValid())
1450  {
1451  QString execPath = cli.executable.absoluteFilePath();
1452  QStringList args = cli.args;
1453 
1456 
1457  CopyTextDlg ctd(execPath + " " + args.join(" "), builder->server()->name(), this);
1458  ctd.exec();
1459  }
1460  else
1461  {
1462  if (!builder->error().isEmpty())
1463  {
1464  QMessageBox::critical(this, tr("Doomseeker - show join command line"),
1465  tr("The command line cannot be built:\n%1").arg(builder->error()));
1466  }
1467  }
1468  builder->deleteLater();
1469 }
1470 
1471 void MainWindow::showUpdaterProcessErrorDialog()
1472 {
1473  QString explanation;
1474  if (this->d->updaterInstallerErrorCode != UpdateInstaller::PEC_GeneralFailure)
1475  {
1476  QString errorCodeExplanation = UpdateInstaller::processErrorCodeToStr(
1477  (UpdateInstaller::ProcessErrorCode) this->d->updaterInstallerErrorCode);
1478  explanation = tr("Update installation problem:\n%1").arg(errorCodeExplanation);
1479  }
1480  else
1481  explanation = tr("Update installation failed.");
1482  QMessageBox::critical(this, tr("Doomseeker - Auto Update problem"),
1483  tr("%1\n\nRemaining updates have been discarded.").arg(explanation));
1484 }
1485 
1486 void MainWindow::showUpdateInstallErrorDialog()
1487 {
1488  QString error = UpdateInstaller::errorCodeToStr(
1489  (UpdateInstaller::ErrorCode)this->d->updaterInstallerErrorCode);
1490  QString msg = tr("Update install problem:\n%1\n\nRemaining updates have been discarded.").arg(error);
1491  QMessageBox::critical(this, tr("Doomseeker - Auto Update problem"), msg);
1492 }
1493 
1494 void MainWindow::stopAutoRefreshTimer()
1495 {
1496  d->autoRefreshTimer.stop();
1497 }
1498 
1500 {
1501  QueryMenuAction *pAction = queryMenuActionForPlugin(plugin);
1502  assert(pAction != nullptr);
1503 
1504  setQueryPluginEnabled(plugin, !pAction->isChecked());
1505 }
1506 
1507 void MainWindow::toolBarAction(QAction *pAction)
1508 {
1509  if (pAction == d->toolBarGetServers)
1510  getServers();
1511 }
1512 
1513 void MainWindow::trayIcon_activated(QSystemTrayIcon::ActivationReason reason)
1514 {
1515  if (reason == QSystemTrayIcon::Trigger)
1516  {
1517  if (isMinimized() || !isVisible())
1518  {
1519  d->bWasMaximized == true ? showMaximized() : showNormal();
1520  activateWindow();
1521  }
1522  else if (gConfig.doomseeker.bCloseToTrayIcon)
1523  close();
1524  else
1525  showMinimized();
1526  }
1527 }
1528 
1529 void MainWindow::updateDynamicAppearance()
1530 {
1531  d->tableServers->setShowGrid(gConfig.doomseeker.bDrawGridInServerTable);
1532  d->serverList->redraw();
1533  d->serverList->cleanUpForce();
1534 }
1535 
1536 // NOTE: Probably would be better if the master manager wasn't tied to the
1537 // MainWindow class?
1538 void MainWindow::updateMasterAddresses()
1539 {
1540  for (int i = 0; i < d->masterManager->numMasters(); i++)
1541  (*d->masterManager)[i]->updateAddress();
1542 }
1543 
1544 void MainWindow::updateServerFilter(const ServerListFilterInfo &filterInfo)
1545 {
1546  d->serverList->applyFilter(filterInfo);
1547  d->lblServerFilterApplied->setVisible(filterInfo.isFilteringAnything());
1548 }
1549 
1550 ServerListCount MainWindow::sumServerListCount() const
1551 {
1552  ServerListCount count;
1553  for (const ServersStatusWidget *status : d->serversStatusesWidgets.values())
1554  {
1555  count += status->count();
1556  }
1557  return count;
1558 }
1559 
1560 void MainWindow::updateRefreshProgress()
1561 {
1562  ServerListCount count = sumServerListCount();
1563  d->taskbarProgress->setMaximum(count.numServers);
1564  d->taskbarProgress->setValue(count.numServers - count.numRefreshing);
1565  updateTrayIconTooltip(count);
1566 }
1567 
1568 void MainWindow::updateTrayIconTooltip(const ServerListCount &count)
1569 {
1570  if (d->trayIcon != nullptr)
1571  {
1572  QString tip;
1573  tip += tr("Generic servers: %1\n").arg(count.numGenericServers);
1574  tip += tr("Custom servers: %1\n").arg(count.numCustomServers);
1575  tip += tr("LAN servers: %1\n").arg(count.numLanServers);
1576  tip += tr("Human players: %1").arg(count.numHumanPlayers);
1577  d->trayIcon->setToolTip(tip);
1578  }
1579 }
1580 
1581 void MainWindow::updateTrayIconTooltipAndLogTotalRefresh()
1582 {
1583  ServerListCount count = sumServerListCount();
1584  updateTrayIconTooltip(count);
1585 
1586  if (d->bTotalRefreshInProcess)
1587  {
1588  gLog << tr("Finished refreshing. Servers on the list: %1 "
1589  "(+%2 custom, +%3 LAN). Players: %4.")
1590  .arg(count.numGenericServers).arg(count.numCustomServers)
1591  .arg(count.numLanServers).arg(count.numHumanPlayers);
1592  }
1593 }