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