serverlist.cpp
1 //------------------------------------------------------------------------------
2 // serverlist.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 "serverlist.h"
24 
25 #include "configuration/doomseekerconfig.h"
26 #include "gui/commongui.h"
27 #include "gui/mainwindow.h"
28 #include "gui/models/serverlistcolumn.h"
29 #include "gui/models/serverlistmodel.h"
30 #include "gui/models/serverlistproxymodel.h"
31 #include "gui/remoteconsole.h"
32 #include "gui/widgets/serverlistcontextmenu.h"
33 #include "gui/widgets/serverlistview.h"
34 #include "refresher/refresher.h"
35 #include "serverapi/server.h"
36 #include "serverapi/tooltips/servertooltip.h"
37 #include "serverapi/tooltips/tooltiprenderhint.h"
38 #include "urlopener.h"
39 
40 #include <QCursor>
41 #include <QFont>
42 #include <QHeaderView>
43 #include <QMessageBox>
44 #include <QScreen>
45 #include <QToolTip>
46 
47 using namespace ServerListColumnId;
48 
49 ServerList::ServerList(ServerListView *serverTable, MainWindow *pMainWindow)
50  : mainWindow(pMainWindow), model(nullptr), needsCleaning(false),
51  proxyModel(nullptr), sortOrder(Qt::AscendingOrder),
52  sortIndex(-1), table(serverTable)
53 {
54  prepareServerTable();
55  initCleanerTimer();
56 }
57 
58 ServerList::~ServerList()
59 {
60  saveColumnsWidthsSettings();
61 }
62 
63 void ServerList::applyFilter(const ServerListFilterInfo &filterInfo)
64 {
65  gConfig.serverFilter.info = filterInfo;
66  proxyModel->setFilterInfo(filterInfo);
67  needsCleaning = true;
68 }
69 
70 bool ServerList::areColumnsWidthsSettingsChanged()
71 {
72  for (int i = 0; i < NUM_SERVERLIST_COLUMNS; ++i)
73  {
74  if (ServerListColumns::columns[i].width != table->columnWidth(i))
75  return true;
76  }
77 
78  return false;
79 }
80 
81 void ServerList::cleanUp()
82 {
83  if (needsCleaning)
84  cleanUpRightNow();
85 }
86 
87 void ServerList::cleanUpRightNow()
88 {
89  if (mainWindow->isEffectivelyActiveWindow())
90  cleanUpForce();
91 }
92 
93 void ServerList::cleanUpForce()
94 {
95  if (table == nullptr || table->model() == nullptr)
96  return;
97 
98  if (sortIndex >= 0)
99  {
100  auto pModel = static_cast<ServerListProxyModel *>(table->model());
101  pModel->invalidate();
102  pModel->sortServers(sortIndex, sortOrder);
103  }
104 
106  needsCleaning = false;
107 }
108 
109 void ServerList::clearAdditionalSorting()
110 {
111  proxyModel->clearAdditionalSorting();
112 }
113 
114 void ServerList::columnHeaderClicked(int index)
115 {
116  if (isSortingByColumn(index))
117  sortOrder = swappedCurrentSortOrder();
118  else
119  sortOrder = getColumnDefaultSortOrder(index);
120  sortIndex = index;
121 
122  cleanUpRightNow();
123 
124  QHeaderView *header = table->horizontalHeader();
125  header->setSortIndicator(sortIndex, sortOrder);
126 }
127 
128 void ServerList::connectTableModelProxySlots()
129 {
130  QHeaderView *header = table->horizontalHeader();
131  this->connect(header, SIGNAL(sectionClicked(int)), SLOT(columnHeaderClicked(int)));
132 
133  this->connect(table->selectionModel(),
134  SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
135  SLOT(itemSelected(QItemSelection)));
136  this->connect(table, SIGNAL(middleMouseClicked(QModelIndex,QPoint)),
137  SLOT(tableMiddleClicked()));
138  connect(table, &QWidget::customContextMenuRequested,
139  this, &ServerList::showContextMenu);
140  this->connect(table, SIGNAL(entered(QModelIndex)), SLOT(mouseEntered(QModelIndex)));
141  this->connect(table, SIGNAL(leftMouseDoubleClicked(QModelIndex,QPoint)),
142  SLOT(doubleClicked(QModelIndex)));
143 }
144 
145 void ServerList::contextMenuAboutToHide()
146 {
147  sender()->deleteLater();
148 }
149 
150 void ServerList::contextMenuTriggered(QAction *action)
151 {
152  auto contextMenu = static_cast<ServerListContextMenu *>(sender());
153  ServerPtr server = contextMenu->server();
154  // 1. This is a bit convoluted, but emitting the serverFilterModified
155  // signal leads to a call to applyFilter() in this class.
156  // 2. Since the menu modifies existing server filter, the worst that can
157  // happen is that we set the same filter again.
158  emit serverFilterModified(contextMenu->serverFilter());
159 
160  ServerListContextMenu::Result contextMenuResult = contextMenu->translateQMenuResult(action);
161  switch (contextMenuResult)
162  {
164  // Do nothing.
165  break;
166 
167  case ServerListContextMenu::FindMissingWADs:
168  emit findMissingWADs(server);
169  break;
170 
171  case ServerListContextMenu::Join:
172  emit serverDoubleClicked(server);
173  break;
174 
175  case ServerListContextMenu::OpenRemoteConsole:
176  new RemoteConsole(server);
177  break;
178 
179  case ServerListContextMenu::OpenURL:
180  // Calling QDesktopServices::openUrl() here directly resulted
181  // in a crash somewhere in Qt libraries. UrlOpener defers the
182  // call with a timer and this fixes the crash.
183  UrlOpener::instance()->open(server->webSite());
184  break;
185 
187  // Do nothing; ignore.
188  break;
189 
190  case ServerListContextMenu::Refresh:
191  refreshSelected();
192  break;
193 
194  case ServerListContextMenu::ShowJoinCommandLine:
195  emit displayServerJoinCommandLine(server);
196  break;
197 
198  case ServerListContextMenu::SortAdditionallyAscending:
199  sortAdditionally(contextMenu->modelIndex(), Qt::AscendingOrder);
200  break;
201 
202  case ServerListContextMenu::SortAdditionallyDescending:
203  sortAdditionally(contextMenu->modelIndex(), Qt::DescendingOrder);
204  break;
205 
206  case ServerListContextMenu::RemoveAdditionalSortingForColumn:
207  removeAdditionalSortingForColumn(contextMenu->modelIndex());
208  break;
209 
210  case ServerListContextMenu::ClearAdditionalSorting:
211  clearAdditionalSorting();
212  break;
213 
214  case ServerListContextMenu::TogglePinServers:
215  for (const ServerPtr &server : contextMenu->servers())
216  {
217  model->redraw(server.data());
218  }
219  break;
220 
221  default:
222  QMessageBox::warning(mainWindow, tr("Doomseeker - context menu warning"),
223  tr("Unhandled behavior in ServerList::contextMenuTriggered()"));
224  break;
225  }
226 }
227 
228 ServerListModel *ServerList::createModel()
229 {
230  auto serverListModel = new ServerListModel(this);
231  serverListModel->prepareHeaders();
232  return serverListModel;
233 }
234 
235 ServerListProxyModel *ServerList::createSortingProxy(ServerListModel *serverListModel)
236 {
237  auto proxy = new ServerListProxyModel(this);
238  this->connect(proxy, SIGNAL(additionalSortColumnsChanged()),
239  SLOT(updateHeaderTitles()));
240  this->connect(proxy, SIGNAL(additionalSortColumnsChanged()),
241  SLOT(saveAdditionalSortingConfig()));
242  proxy->setSourceModel(serverListModel);
243  proxy->setSortRole(ServerListModel::SLDT_SORT);
244  proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
245  proxy->setFilterKeyColumn(IDServerName);
246 
247  return proxy;
248 }
249 
250 void ServerList::doubleClicked(const QModelIndex &index)
251 {
252  emit serverDoubleClicked(serverFromIndex(index));
253 }
254 
255 Qt::SortOrder ServerList::getColumnDefaultSortOrder(int columnId)
256 {
257  // Right now we can assume that columnIndex == columnId.
258  return ServerListColumns::columns[columnId].defaultSortOrder;
259 }
260 
261 bool ServerList::hasAtLeastOneServer() const
262 {
263  return model->rowCount() > 0;
264 }
265 
266 void ServerList::initCleanerTimer()
267 {
268  cleanerTimer.setInterval(200);
269  cleanerTimer.start();
270  connect(&cleanerTimer, SIGNAL(timeout()), this, SLOT (cleanUp()));
271 }
272 
273 bool ServerList::isAnyColumnSortedAdditionally() const
274 {
275  return proxyModel->isAnyColumnSortedAdditionally();
276 }
277 
278 bool ServerList::isSortingAdditionallyByColumn(int column) const
279 {
280  return proxyModel->isSortingAdditionallyByColumn(column);
281 }
282 
283 bool ServerList::isSortingByColumn(int columnIndex)
284 {
285  return sortIndex == columnIndex;
286 }
287 
288 void ServerList::itemSelected(const QItemSelection &selection)
289 {
290  auto pModel = static_cast<QSortFilterProxyModel *>(table->model());
291  QModelIndexList indexList = selection.indexes();
292 
293  QList<ServerPtr> servers;
294  for (int i = 0; i < indexList.count(); ++i)
295  {
296  QModelIndex realIndex = pModel->mapToSource(indexList[i]);
297  ServerPtr server = model->serverFromList(realIndex);
298  servers.append(server);
299  }
300  emit serversSelected(servers);
301 }
302 
304 {
305  for (int i = 0; i < model->rowCount(); ++i)
306  {
307  ServerPtr server = model->serverFromList(i);
308  server->lookupHost();
309  }
310 }
311 
312 void ServerList::mouseEntered(const QModelIndex &index)
313 {
314  auto pModel = static_cast<QSortFilterProxyModel *>(table->model());
315  QModelIndex realIndex = pModel->mapToSource(index);
316  ServerPtr server = model->serverFromList(realIndex);
317  QString tooltip;
318 
319  TooltipRenderHint renderHint;
320  renderHint.setFont(QToolTip::font());
321 
322  QScreen *screen = CommonGUI::screenAt(QCursor::pos());
323  if (screen != nullptr)
324  {
325  renderHint.setBoundingRect(screen->geometry());
326  }
327 
328  // Functions inside cases perform checks on the server structure
329  // to see if any tooltip should be generated. Empty string is returned
330  // in case if it should be not.
331  switch (index.column())
332  {
333  case IDPort:
334  tooltip = ServerTooltip::createPortToolTip(server);
335  break;
336 
337  case IDAddress:
338  tooltip = server->hostName(true);
339  break;
340 
341  case IDPlayers:
342  tooltip = ServerTooltip::createPlayersToolTip(renderHint, server);
343  break;
344 
345  case IDServerName:
346  tooltip = ServerTooltip::createServerNameToolTip(server);
347  break;
348 
349  case IDIwad:
350  tooltip = ServerTooltip::createIwadToolTip(server);
351  break;
352 
353  case IDWads:
354  tooltip = ServerTooltip::createPwadsToolTip(server);
355  break;
356 
357  default:
358  tooltip = "";
359  break;
360  }
361 
362  QToolTip::showText(QCursor::pos(), tooltip, nullptr);
363 }
364 
365 void ServerList::prepareServerTable()
366 {
367  model = createModel();
368  proxyModel = createSortingProxy(model);
369 
370  columnHeaderClicked(IDPlayers);
371  table->setModel(proxyModel);
372  table->setContextMenuPolicy(Qt::CustomContextMenu);
373  table->setupTableProperties();
374 
375  if (gConfig.doomseeker.serverListSortIndex >= 0)
376  {
377  sortIndex = gConfig.doomseeker.serverListSortIndex;
378  sortOrder = static_cast<Qt::SortOrder>(gConfig.doomseeker.serverListSortDirection);
379  }
380 
381  connectTableModelProxySlots();
382  proxyModel->setAdditionalSortColumns(gConfig.doomseeker.additionalSortColumns());
383 }
384 
385 void ServerList::redraw()
386 {
387  model->redrawAll();
388 }
389 
390 void ServerList::refreshSelected()
391 {
392  for (const ServerPtr &server : selectedServers())
393  {
394  gRefresher->registerServer(server);
395  }
396 }
397 
398 void ServerList::registerServer(ServerPtr server)
399 {
400  ServerPtr serverOnList = model->findSameServer(server.data());
401  if (serverOnList != nullptr)
402  {
403  serverOnList->setCustom(server->isCustom() || serverOnList->isCustom());
404  model->redraw(serverOnList.data());
405  gRefresher->registerServer(serverOnList);
406  }
407  else
408  {
409  this->connect(server.data(), SIGNAL(updated(ServerPtr,int)),
410  SLOT(onServerUpdated(ServerPtr)));
411  this->connect(server.data(), SIGNAL(begunRefreshing(ServerPtr)),
412  SLOT(onServerBegunRefreshing(ServerPtr)));
413  model->addServer(server);
414  emit serverRegistered(server);
415  gRefresher->registerServer(server);
416  }
417 }
418 
419 void ServerList::removeServer(const ServerPtr &server)
420 {
421  server->disconnect(this);
422  model->removeServer(server);
423  emit serverDeregistered(server);
424 }
425 
426 void ServerList::removeCustomServers()
427 {
428  for (ServerPtr server : model->customServers())
429  {
430  removeServer(server);
431  }
432 }
433 
434 void ServerList::removeNonSpecialServers()
435 {
436  for (ServerPtr server : model->nonSpecialServers())
437  {
438  removeServer(server);
439  }
440 }
441 
442 void ServerList::removeAdditionalSortingForColumn(const QModelIndex &modelIndex)
443 {
444  proxyModel->removeAdditionalColumnSorting(modelIndex.column());
445 }
446 
447 void ServerList::saveAdditionalSortingConfig()
448 {
449  gConfig.doomseeker.setAdditionalSortColumns(proxyModel->additionalSortColumns());
450 }
451 
452 void ServerList::saveColumnsWidthsSettings()
453 {
454  gConfig.doomseeker.serverListColumnState = table->horizontalHeader()->saveState().toBase64();
455  gConfig.doomseeker.serverListSortIndex = sortIndex;
456  gConfig.doomseeker.serverListSortDirection = sortOrder;
457 }
458 
459 QList<ServerPtr> ServerList::selectedServers() const
460 {
461  QModelIndexList indexList = table->selectionModel()->selectedRows();
462 
463  QList<ServerPtr> servers;
464  for (int i = 0; i < indexList.count(); ++i)
465  {
466  QModelIndex realIndex = proxyModel->mapToSource(indexList[i]);
467  ServerPtr server = model->serverFromList(realIndex);
468  servers.append(server);
469  }
470  return servers;
471 }
472 
473 void ServerList::onServerBegunRefreshing(const ServerPtr &server)
474 {
475  model->setRefreshing(server);
476 }
477 
478 QList<ServerPtr> ServerList::servers() const
479 {
480  return model->servers();
481 }
482 
483 ServerPtr ServerList::serverFromIndex(const QModelIndex &index)
484 {
485  auto pModel = static_cast<QSortFilterProxyModel *>(table->model());
486  QModelIndex indexReal = pModel->mapToSource(index);
487  return model->serverFromList(indexReal);
488 }
489 
490 QList<ServerPtr> ServerList::serversForPlugin(const EnginePlugin *plugin) const
491 {
492  return model->serversForPlugin(plugin);
493 }
494 
495 void ServerList::onServerUpdated(const ServerPtr &server)
496 {
497  int rowIndex = model->findServerOnTheList(server.data());
498  if (rowIndex >= 0)
499  rowIndex = model->updateServer(rowIndex, server);
500  else
501  rowIndex = model->addServer(server);
502 
503  needsCleaning = true;
504  emit serverInfoUpdated(server);
505 }
506 
508 {
509  const bool FORCE = true;
510  updateCountryFlags(!FORCE);
511 }
512 
513 void ServerList::sortAdditionally(const QModelIndex &modelIndex, Qt::SortOrder order)
514 {
515  auto model = static_cast<ServerListProxyModel *>(table->model());
516  model->addAdditionalColumnSorting(modelIndex.column(), order);
517 }
518 
519 Qt::SortOrder ServerList::swappedCurrentSortOrder()
520 {
521  return sortOrder == Qt::AscendingOrder ? Qt::DescendingOrder : Qt::AscendingOrder;
522 }
523 
524 void ServerList::tableMiddleClicked()
525 {
526  refreshSelected();
527 }
528 
529 void ServerList::showContextMenu(const QPoint &position)
530 {
531  const QModelIndex index = table->indexAt(position);
532  ServerPtr server = serverFromIndex(index);
533 
534  if (server == nullptr)
535  return;
536 
537  ServerListContextMenu *contextMenu = new ServerListContextMenu(server,
538  proxyModel->filterInfo(), index, selectedServers(), this);
539  this->connect(contextMenu, SIGNAL(aboutToHide()), SLOT(contextMenuAboutToHide()));
540  this->connect(contextMenu, SIGNAL(triggered(QAction*)), SLOT(contextMenuTriggered(QAction*)));
541 
542  QPoint displayPoint = table->viewport()->mapToGlobal(position);
543  contextMenu->popup(displayPoint);
544 }
545 
546 void ServerList::updateCountryFlags()
547 {
548  const bool FORCE = true;
549  updateCountryFlags(FORCE);
550 }
551 
552 void ServerList::updateCountryFlags(bool force)
553 {
554  for (int i = 0; i < model->rowCount(); ++i)
555  model->updateFlag(i, force);
556 }
557 
558 void ServerList::updateHeaderTitles()
559 {
560  const QList<ColumnSort> &sortings = proxyModel->additionalSortColumns();
561  for (int i = 0; i < ServerListColumnId::NUM_SERVERLIST_COLUMNS; ++i)
562  {
563  // Clear header icons.
564  model->setHeaderData(i, Qt::Horizontal, QIcon(), Qt::DecorationRole);
565  }
566  QStringList labels = ServerListColumns::generateColumnHeaderLabels();
567  for (int i = 0; i < sortings.size(); ++i)
568  {
569  const ColumnSort &sort = sortings[i];
570  labels[sort.columnId()] = QString("[%1] %2").arg(i + 1).arg(labels[sort.columnId()]);
571  QIcon icon = sort.order() == Qt::AscendingOrder ?
572  QIcon(":/icons/ascending.png") :
573  QIcon(":/icons/descending.png");
574  model->setHeaderData(sort.columnId(), Qt::Horizontal, icon, Qt::DecorationRole);
575  }
576  model->setHorizontalHeaderLabels(labels);
577 }