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