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