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  this->connect(table, SIGNAL(rightMouseClicked(QModelIndex,QPoint)),
133  SLOT(tableRightClicked(QModelIndex,QPoint)));
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->setupTableProperties();
358 
359  if (gConfig.doomseeker.serverListSortIndex >= 0)
360  {
361  sortIndex = gConfig.doomseeker.serverListSortIndex;
362  sortOrder = static_cast<Qt::SortOrder>(gConfig.doomseeker.serverListSortDirection);
363  }
364 
365  connectTableModelProxySlots();
366  proxyModel->setAdditionalSortColumns(gConfig.doomseeker.additionalSortColumns());
367 }
368 
369 void ServerList::redraw()
370 {
371  model->redrawAll();
372 }
373 
374 void ServerList::refreshSelected()
375 {
376  for (const ServerPtr &server : selectedServers())
377  {
378  gRefresher->registerServer(server.data());
379  }
380 }
381 
382 void ServerList::registerServer(ServerPtr server)
383 {
384  ServerPtr serverOnList = model->findSameServer(server.data());
385  if (serverOnList != nullptr)
386  {
387  serverOnList->setCustom(server->isCustom() || serverOnList->isCustom());
388  model->redraw(serverOnList.data());
389  return;
390  }
391  this->connect(server.data(), SIGNAL(updated(ServerPtr,int)),
392  SLOT(onServerUpdated(ServerPtr)));
393  this->connect(server.data(), SIGNAL(begunRefreshing(ServerPtr)),
394  SLOT(onServerBegunRefreshing(ServerPtr)));
395  model->addServer(server);
396  emit serverRegistered(server);
397 }
398 
399 void ServerList::removeServer(const ServerPtr &server)
400 {
401  server->disconnect(this);
402  model->removeServer(server);
403  emit serverDeregistered(server);
404 }
405 
406 void ServerList::removeCustomServers()
407 {
408  for (ServerPtr server : model->customServers())
409  {
410  removeServer(server);
411  }
412 }
413 
414 void ServerList::removeNonSpecialServers()
415 {
416  for (ServerPtr server : model->nonSpecialServers())
417  {
418  removeServer(server);
419  }
420 }
421 
422 void ServerList::removeAdditionalSortingForColumn(const QModelIndex &modelIndex)
423 {
424  proxyModel->removeAdditionalColumnSorting(modelIndex.column());
425 }
426 
427 void ServerList::saveAdditionalSortingConfig()
428 {
429  gConfig.doomseeker.setAdditionalSortColumns(proxyModel->additionalSortColumns());
430 }
431 
432 void ServerList::saveColumnsWidthsSettings()
433 {
434  gConfig.doomseeker.serverListColumnState = table->horizontalHeader()->saveState().toBase64();
435  gConfig.doomseeker.serverListSortIndex = sortIndex;
436  gConfig.doomseeker.serverListSortDirection = sortOrder;
437 }
438 
439 QList<ServerPtr> ServerList::selectedServers() const
440 {
441  QModelIndexList indexList = table->selectionModel()->selectedRows();
442 
443  QList<ServerPtr> servers;
444  for (int i = 0; i < indexList.count(); ++i)
445  {
446  QModelIndex realIndex = proxyModel->mapToSource(indexList[i]);
447  ServerPtr server = model->serverFromList(realIndex);
448  servers.append(server);
449  }
450  return servers;
451 }
452 
453 void ServerList::onServerBegunRefreshing(const ServerPtr &server)
454 {
455  model->setRefreshing(server);
456 }
457 
458 QList<ServerPtr> ServerList::servers() const
459 {
460  return model->servers();
461 }
462 
463 ServerPtr ServerList::serverFromIndex(const QModelIndex &index)
464 {
465  auto pModel = static_cast<QSortFilterProxyModel *>(table->model());
466  QModelIndex indexReal = pModel->mapToSource(index);
467  return model->serverFromList(indexReal);
468 }
469 
470 QList<ServerPtr> ServerList::serversForPlugin(const EnginePlugin *plugin) const
471 {
472  return model->serversForPlugin(plugin);
473 }
474 
475 void ServerList::onServerUpdated(const ServerPtr &server)
476 {
477  int rowIndex = model->findServerOnTheList(server.data());
478  if (rowIndex >= 0)
479  rowIndex = model->updateServer(rowIndex, server);
480  else
481  rowIndex = model->addServer(server);
482 
483  needsCleaning = true;
484  emit serverInfoUpdated(server);
485 }
486 
488 {
489  const bool FORCE = true;
490  updateCountryFlags(!FORCE);
491 }
492 
493 void ServerList::setGroupServersWithPlayersAtTop(bool b)
494 {
495  proxyModel->setGroupServersWithPlayersAtTop(b);
496 }
497 
498 void ServerList::sortAdditionally(const QModelIndex &modelIndex, Qt::SortOrder order)
499 {
500  auto model = static_cast<ServerListProxyModel *>(table->model());
501  model->addAdditionalColumnSorting(modelIndex.column(), order);
502 }
503 
504 Qt::SortOrder ServerList::swappedCurrentSortOrder()
505 {
506  return sortOrder == Qt::AscendingOrder ? Qt::DescendingOrder : Qt::AscendingOrder;
507 }
508 
509 void ServerList::tableMiddleClicked()
510 {
511  refreshSelected();
512 }
513 
514 void ServerList::tableRightClicked(const QModelIndex &index, const QPoint &cursorPosition)
515 {
516  ServerPtr server = serverFromIndex(index);
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(cursorPosition);
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 }
559 
560 void ServerList::updateSearch(const QString &search)
561 {
562  QRegExp pattern(QString("*") + search + "*", Qt::CaseInsensitive, QRegExp::Wildcard);
563  proxyModel->setFilterRegExp(pattern);
564 }