playertable.cpp
1 //------------------------------------------------------------------------------
2 // playertable.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) 2010 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "playertable.h"
24 
25 #include "application.h"
26 #include "capsqt.h"
27 #include "gui/dockBuddiesList.h"
28 #include "gui/mainwindow.h"
29 #include "serverapi/server.h"
31 #include "serverapi/tooltips/tooltiprenderhint.h"
32 
33 #include <QFont>
34 #include <QFontMetrics>
35 #include <QRect>
36 
37 #include <climits>
38 
39 // Games can come up with their own team names; don't trust them too much.
40 static const int MAX_TEAMNAME_SIZE = 100;
41 
42 DClass<PlayerTable>
43 {
44 public:
45  TooltipRenderHint renderHint;
46  ServerCPtr server;
47 
48  bool isTeamGame() const
49  {
50  return server->gameMode().isTeamGame();
51  }
52 
53  int maxShownPlayers() const
54  {
55  if (this->renderHint.boundingRect().isNull())
56  return -1;
57 
58  QFontMetrics fontMetrics(this->renderHint.font());
59 
60  // Permit to take only about 80% of the viewport.
61  int viewportHeight = this->renderHint.boundingRect().height();
62  viewportHeight = qMax(300, viewportHeight * 80 / 100);
63 
64  return viewportHeight / fontMetrics.height();
65  }
66 };
67 
68 DPointered(PlayerTable)
69 
70 PlayerTable::PlayerTable(const TooltipRenderHint &renderHint, const ServerCPtr &server)
71 {
72  d->renderHint = renderHint;
73  d->server = server;
74 }
75 
76 PlayerTable::~PlayerTable()
77 {
78 }
79 
80 QString PlayerTable::generateHTML()
81 {
82  static const QString css = "<style>"
83  ".player-table {"
84  " background-color: #FFFFFF;"
85  " color: #000000;"
86  "}\n"
87  ".header-row th {"
88  " border-top: 1px solid black;"
89  " border-bottom: 1px solid black;"
90  " padding: 2px 4px;"
91  "}\n"
92  ".section-header-row th {"
93  " padding-left: 20px;"
94  " padding-top: 4px;"
95  " text-align: left;"
96  "}\n"
97  "th {"
98  " text-align: left;"
99  " white-space: nowrap;"
100  "}\n"
101  "th, td {"
102  " border-right: 1px dashed gray;"
103  " padding: 0px 4px;"
104  "}\n"
105  ".number-cell {"
106  " text-align: right;"
107  "}\n"
108  "</style>";
109 
110  const bool isTeamGame = d->isTeamGame();
111  const PlayersList &players = d->server->players();
112 
113  PlayersByTeams playersByTeams;
114  PlayersList bots, spectators;
115 
116  players.inGamePlayersByTeams(playersByTeams);
117  players.botsWithoutTeam(bots);
118  players.spectators(spectators);
119 
120  const int maxShownPlayers = d->maxShownPlayers();
121  const int numSections = qMax(1, playersByTeams.size()
122  + qMin(spectators.size(), 1));
123  int totalPlayers = bots.count() + spectators.count();
124  for (const PlayersList &players : playersByTeams.values())
125  {
126  totalPlayers += players.size();
127  }
128  const int maxShownPlayersBySection = (maxShownPlayers > 0 && totalPlayers > maxShownPlayers)
129  ? (maxShownPlayers / numSections)
130  : -1;
131 
132  QString table = R"(<table class="player-table" cellspacing="0" width="100%">)";
133  table += tableHeader();
134  bool separator = false;
135  for (int i : playersByTeams.keys())
136  {
137  const PlayersList &playersList = playersByTeams[i];
138  if (isTeamGame)
139  {
140  table += teamHeader(d->server->teamName(i));
141  }
142  table += createPlayerRows(playersList, maxShownPlayersBySection);
143  }
144  if (bots.count() > 0)
145  {
146  table += sectionHeader(tr("Bots"));
147  table += createPlayerRows(bots, maxShownPlayersBySection);
148  }
149  if (spectators.count() > 0)
150  {
151  table += sectionHeader(tr("Spectators"));
152  table += createPlayerRows(spectators, maxShownPlayersBySection);
153  }
154  table += "</table>";
155  return css + table;
156 }
157 
158 QString PlayerTable::createPlayerRows(const PlayersList &playerList, int maxShown) const
159 {
160  DockBuddiesList *buddiesList = (gApp != nullptr && gApp->mainWindow() != nullptr)
161  ? gApp->mainWindow()->buddiesList()
162  : nullptr;
163 
164  QList<Player> sorted;
165  const QList<Player> *players = &playerList.players();
166  if (maxShown > 0 && playerList.size() > maxShown)
167  {
168  // If we can't display everyone, sort them so that buddies are first
169  // and bots are last.
170  sorted = playerList.players();
171  std::sort(sorted.begin(), sorted.end(),
172  [buddiesList](const Player &p1, const Player &p2)
173  {
174  if (buddiesList != nullptr)
175  {
176  if (buddiesList->isBuddy(p1) && !buddiesList->isBuddy(p2))
177  return true;
178  }
179  if (!p1.isBot() && p2.isBot())
180  return true;
181  return false;
182  });
183  players = &sorted;
184  }
185 
186  QString rows;
187  int totalShown = 0;
188  for (const Player &player : *players)
189  {
190  rows += createPlayerRow(player);
191  if (maxShown > 0 && ++totalShown >= maxShown)
192  break;
193  }
194  if (maxShown > 0)
195  {
196  int remaining = static_cast<int>(playerList.size()) - maxShown;
197  if (remaining > 0)
198  {
199  rows += createMoreRow(remaining);
200  }
201  }
202  return rows;
203 }
204 
205 QString PlayerTable::createPlayerRow(const Player &player) const
206 {
207  QString status = "";
208  if (player.isBot())
209  {
210  status = tr("BOT");
211  }
212  else if (player.isSpectating())
213  {
214  status = tr("SPECTATOR");
215  }
216 
217  QString ping;
218  if (player.ping() != USHRT_MAX)
219  {
220  ping = QString::number(player.ping());
221  }
222 
223  return QString(
224  "<tr>"
225  R"(<td>%1</td>)"
226  R"(<td class="number-cell" width="60">%2</td>)"
227  R"(<td class="number-cell" width="60">%3</td>)"
228  R"(<td width="100">%4</td>)"
229  "</tr>")
230  .arg(player.nameFormatted())
231  .arg(player.score())
232  .arg(ping)
233  .arg(status);
234 }
235 
236 QString PlayerTable::createMoreRow(int count) const
237 {
238  QString text = tr("(and %n more ...)", nullptr, count);
239  return QString(
240  "<tr>"
241  "<td>%1</td>"
242  R"(<td width="60">&nbsp;</td>)"
243  R"(<td width="60">&nbsp;</td>)"
244  R"(<td width="100">&nbsp;</td>)"
245  "</tr>")
246  .arg(text);
247 }
248 
249 QString PlayerTable::tableHeader() const
250 {
251  static const bool canCssBorders = CapsQt::canCssCellBorders();
252 
253  const QString PLAYER = tr("Player");
254  const QString SCORE = tr("Score");
255  const QString PING = tr("Ping");
256  const QString STATUS = tr("Status");
257 
258  QString header;
259  if (!canCssBorders)
260  {
261  // If borders can't be drawn, add a horizontal line separator
262  // to imitate them.
263  header += R"(<tr><th colspan="4"><hr width="100%"></th></tr>)";
264  }
265  header += QString(
266  R"(<tr class="header-row">)"
267  R"(<th>%1</th>)"
268  R"(<th class="number-cell" width="60">&nbsp;%2</th>)"
269  R"(<th class="number-cell" width="60">&nbsp;%3</th>)"
270  R"(<th width="100">%4</th>)"
271  "</tr>")
272  .arg(PLAYER)
273  .arg(SCORE)
274  .arg(PING)
275  .arg(STATUS);
276  if (!canCssBorders)
277  {
278  header += R"(<tr><th colspan="4"><hr width="100%"></th></tr>)";
279  }
280  return header;
281 }
282 
283 QString PlayerTable::teamHeader(const QString &teamName)
284 {
285  // Don't trust the team name and sanitize it because
286  // it can be procured by the game.
287  return sectionHeader(tr("Team %1").arg(
288  teamName.toHtmlEscaped().left(MAX_TEAMNAME_SIZE)));
289 }
290 
291 QString PlayerTable::sectionHeader(const QString &title)
292 {
293  return QString(
294  R"(<tr class="section-header-row">)"
295  "<th>%1</th>"
296  "<th>&nbsp;</th>"
297  "<th>&nbsp;</th>"
298  "<th>&nbsp;</th>"
299  "</tr>")
300  .arg(title);
301 }