1 /*
2 SPDX-FileCopyrightText: 2009-2021 Graeme Gott <graeme@gottcode.org>
3
4 SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7 #include "scores_dialog.h"
8
9 #include "board.h"
10 #include "clock.h"
11
12 #include <QDialogButtonBox>
13 #include <QGridLayout>
14 #include <QKeyEvent>
15 #include <QLabel>
16 #include <QLineEdit>
17 #include <QLocale>
18 #include <QPushButton>
19 #include <QSettings>
20 #include <QStyle>
21 #include <QTabWidget>
22 #include <QVBoxLayout>
23
24 #if defined(Q_OS_UNIX)
25 #include <pwd.h>
26 #include <unistd.h>
27 #elif defined(Q_OS_WIN)
28 #include <lmcons.h>
29 #include <windows.h>
30 #endif
31
32 //-----------------------------------------------------------------------------
33
34 QVector<int> ScoresDialog::m_max(Clock::TotalTimers, -1);
35 QVector<int> ScoresDialog::m_min(Clock::TotalTimers, -1);
36
37 //-----------------------------------------------------------------------------
38
Page(int timer,QSettings & settings,QWidget * parent)39 ScoresDialog::Page::Page(int timer, QSettings& settings, QWidget* parent)
40 : QWidget(parent)
41 , m_timer(timer)
42 , m_row(-1)
43 {
44 // Create score widgets
45 m_scores_layout = new QGridLayout(this);
46 m_scores_layout->setHorizontalSpacing(18);
47 m_scores_layout->setVerticalSpacing(6);
48 m_scores_layout->setColumnStretch(NameColumn, 1);
49 m_scores_layout->addWidget(new QLabel("<b>" + tr("Rank") + "</b>", this), 1, RankColumn, Qt::AlignCenter);
50 m_scores_layout->addWidget(new QLabel("<b>" + tr("Name") + "</b>", this), 1, NameColumn, Qt::AlignCenter);
51 m_scores_layout->addWidget(new QLabel("<b>" + tr("Score") + "</b>", this), 1, ScoreColumn, Qt::AlignCenter);
52 m_scores_layout->addWidget(new QLabel("<b>" + tr("Maximum") + "</b>", this), 1, MaxScoreColumn, Qt::AlignCenter);
53 m_scores_layout->addWidget(new QLabel("<b>" + tr("Date") + "</b>", this), 1, DateColumn, Qt::AlignCenter);
54 m_scores_layout->addWidget(new QLabel("<b>" + tr("Size") + "</b>", this), 1, SizeColumn, Qt::AlignCenter);
55
56 QFrame* divider = new QFrame(this);
57 divider->setFrameStyle(QFrame::HLine | QFrame::Sunken);
58 m_scores_layout->addWidget(divider, 2, 0, 1, TotalColumns);
59
60 QVector<Qt::Alignment> alignments(TotalColumns, Qt::AlignTrailing);
61 alignments[NameColumn] = Qt::AlignLeading;
62 alignments[SizeColumn] = Qt::AlignHCenter;
63 for (int r = 0; r < 10; ++r) {
64 m_score_labels[r][0] = new QLabel(tr("#%1").arg(r + 1), this);
65 m_scores_layout->addWidget(m_score_labels[r][0], r + 3, 0, alignments[RankColumn] | Qt::AlignVCenter);
66 for (int c = RankColumn + 1; c < TotalColumns; ++c) {
67 m_score_labels[r][c] = new QLabel("-", this);
68 m_scores_layout->addWidget(m_score_labels[r][c], r + 3, c, alignments[c] | Qt::AlignVCenter);
69 }
70 }
71
72 // Populate scores widgets with values
73 load(settings);
74
75 // Hide maximum scores column if showing maximum scores is set to "Never"
76 if (settings.value("ShowMaximumScore").toInt() == 0) {
77 m_scores_layout->itemAtPosition(0, MaxScoreColumn)->widget()->hide();
78 for (int r = 0; r < 10; ++r) {
79 m_score_labels[r][MaxScoreColumn]->hide();
80 }
81 }
82 }
83
84 //-----------------------------------------------------------------------------
85
name() const86 QString ScoresDialog::Page::name() const
87 {
88 return Clock::timerToString(m_timer);
89 }
90
91 //-----------------------------------------------------------------------------
92
addScore(const QString & name,int score,int max_score,const QDateTime & date,int size)93 bool ScoresDialog::Page::addScore(const QString& name, int score, int max_score, const QDateTime& date, int size)
94 {
95 m_row = -1;
96 if (score == 0) {
97 return false;
98 }
99
100 m_row = 0;
101 for (const Score& s : qAsConst(m_scores)) {
102 if (score >= s.score && date >= s.date) {
103 break;
104 }
105 ++m_row;
106 }
107 if (m_row == 10) {
108 m_row = -1;
109 return false;
110 }
111
112 Score s = { name, score, max_score, date, size };
113 m_scores.insert(m_row, s);
114 if (m_scores.size() == 11) {
115 m_scores.removeLast();
116 }
117
118 m_max[m_timer] = m_scores.first().score;
119 m_min[m_timer] = (m_scores.size() == 10) ? m_scores.last().score : 1;
120
121 return true;
122 }
123
124 //-----------------------------------------------------------------------------
125
editStart(QLineEdit * playername)126 void ScoresDialog::Page::editStart(QLineEdit* playername)
127 {
128 Q_ASSERT(m_row != -1);
129
130 // Inform player of success
131 QLabel* label = new QLabel(this);
132 label->setAlignment(Qt::AlignCenter);
133 if (m_row == 0) {
134 label->setText(QString("<big></big> %1<br>%2").arg(tr("Congratulations!"), tr("You beat your top score!")));
135 } else {
136 label->setText(QString("<big></big> %1<br>%2").arg(tr("Well done!"), tr("You have a new high score!")));
137 }
138 m_scores_layout->addWidget(label, 0, 0, 1, TotalColumns);
139
140 // Add score to display
141 updateItems();
142
143 // Show lineedit
144 m_scores_layout->addWidget(playername, m_row + 3, 1);
145 m_score_labels[m_row][1]->hide();
146 playername->setText(m_scores[m_row].name);
147 playername->show();
148 playername->setFocus();
149 }
150
151 //-----------------------------------------------------------------------------
152
editFinish(QLineEdit * playername)153 void ScoresDialog::Page::editFinish(QLineEdit* playername)
154 {
155 Q_ASSERT(m_row != -1);
156
157 // Set player name
158 m_scores[m_row].name = playername->text();
159 m_score_labels[m_row][1]->setText("<b>" + m_scores[m_row].name + "</b>");
160
161 // Hide lineedit
162 playername->hide();
163 m_scores_layout->removeWidget(playername);
164 m_score_labels[m_row][1]->show();
165
166 // Save scores
167 QSettings settings;
168 settings.setValue("Scores/DefaultName", playername->text());
169 settings.beginWriteArray(Clock::timerScoresGroup(m_timer));
170 for (int r = 0, size = m_scores.size(); r < size; ++r) {
171 const Score& score = m_scores[r];
172 settings.setArrayIndex(r);
173 settings.setValue("Name", score.name);
174 settings.setValue("Score", score.score);
175 settings.setValue("Maximum", score.max_score);
176 settings.setValue("Size", score.size);
177 settings.setValue("Date", score.date.toString(Qt::ISODate));
178 }
179 settings.endArray();
180 }
181
182 //-----------------------------------------------------------------------------
183
load(QSettings & settings)184 void ScoresDialog::Page::load(QSettings& settings)
185 {
186 const int size = std::min(settings.beginReadArray(Clock::timerScoresGroup(m_timer)), 10);
187 for (int r = 0; r < size; ++r) {
188 settings.setArrayIndex(r);
189 const QString name = settings.value("Name").toString();
190 const int score = settings.value("Score").toInt();
191 const int max_score = settings.value("Maximum", -1).toInt();
192 const int size = settings.value("Size", -1).toInt();
193 const QDateTime date = settings.value("Date").toDateTime();
194 addScore(name, score, max_score, date, size);
195 }
196 settings.endArray();
197
198 m_row = -1;
199 updateItems();
200 }
201
202 //-----------------------------------------------------------------------------
203
updateItems()204 void ScoresDialog::Page::updateItems()
205 {
206 const int size = m_scores.size();
207
208 // Add scores
209 for (int r = 0; r < size; ++r) {
210 const Score& score = m_scores[r];
211 m_score_labels[r][NameColumn]->setText(score.name);
212 m_score_labels[r][ScoreColumn]->setNum(score.score);
213 if (score.max_score > -1) {
214 m_score_labels[r][MaxScoreColumn]->setNum(score.max_score);
215 } else {
216 m_score_labels[r][MaxScoreColumn]->setText(tr("N/A"));
217 }
218 m_score_labels[r][DateColumn]->setText(QLocale().toString(score.date, QLocale::ShortFormat));
219 if (score.size > -1) {
220 m_score_labels[r][SizeColumn]->setText(Board::sizeToString(score.size));
221 } else {
222 m_score_labels[r][SizeColumn]->setText(tr("N/A"));
223 }
224 }
225
226 // Fill remainder of scores with dashes
227 for (int r = size; r < 10; ++r) {
228 for (int c = RankColumn + 1; c < TotalColumns; ++c) {
229 m_score_labels[r][c]->setText("-");
230 }
231 }
232
233 // Use bold text for new score
234 if (m_row != -1) {
235 for (int c = 0; c < TotalColumns; ++c) {
236 m_score_labels[m_row][c]->setText("<b>" + m_score_labels[m_row][c]->text() + "</b>");
237 }
238 }
239 }
240
241 //-----------------------------------------------------------------------------
242
ScoresDialog(QWidget * parent)243 ScoresDialog::ScoresDialog(QWidget* parent)
244 : QDialog(parent)
245 , m_active_page(nullptr)
246 {
247 setWindowTitle(tr("High Scores"));
248
249 QSettings settings;
250
251 // Load default name
252 m_default_name = settings.value("Scores/DefaultName").toString();
253 if (m_default_name.isEmpty()) {
254 #if defined(Q_OS_UNIX)
255 passwd* pws = getpwuid(geteuid());
256 if (pws) {
257 m_default_name = QString::fromLocal8Bit(pws->pw_gecos).section(',', 0, 0);
258 if (m_default_name.isEmpty()) {
259 m_default_name = QString::fromLocal8Bit(pws->pw_name);
260 }
261 }
262 #elif defined(Q_OS_WIN)
263 TCHAR buffer[UNLEN + 1];
264 DWORD count = UNLEN + 1;
265 if (GetUserName(buffer, &count)) {
266 m_default_name = QString::fromWCharArray(buffer, count);
267 }
268 #endif
269 }
270
271 m_username = new QLineEdit(this);
272 m_username->hide();
273 connect(m_username, &QLineEdit::editingFinished, this, &ScoresDialog::editingFinished);
274
275 m_tabs = new QTabWidget(this);
276 m_tabs->setTabPosition(QTabWidget::West);
277
278 for (int timer = 0; timer < Clock::TotalTimers; ++timer) {
279 Page* page = new Page(timer, settings, this);
280 m_pages.append(page);
281 if (!page->isEmpty()) {
282 addTab(page);
283 }
284 }
285 if (m_tabs->count() == 0) {
286 addTab(m_pages[Clock::Tanglet]);
287 }
288
289 // Show scores for current timer mode
290 const int index = m_tabs->indexOf(m_pages[settings.value("Board/TimerMode").toInt()]);
291 if (index != -1) {
292 m_tabs->setCurrentIndex(index);
293 }
294
295 // Lay out dialog
296 m_buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, this);
297 m_buttons->setCenterButtons(style()->styleHint(QStyle::SH_MessageBox_CenterButtons));
298 m_buttons->button(QDialogButtonBox::Close)->setDefault(true);
299 m_buttons->button(QDialogButtonBox::Close)->setFocus();
300 connect(m_buttons, &QDialogButtonBox::rejected, this, &ScoresDialog::reject);
301
302 QVBoxLayout* layout = new QVBoxLayout(this);
303 layout->addWidget(m_tabs);
304 layout->addWidget(m_buttons);
305 layout->setSizeConstraint(QLayout::SetFixedSize);
306 }
307
308 //-----------------------------------------------------------------------------
309
addScore(int score,int max_score)310 bool ScoresDialog::addScore(int score, int max_score)
311 {
312 QSettings settings;
313
314 // Fetch scores page
315 const int timer = settings.value("Current/TimerMode", Clock::Tanglet).toInt();
316 m_active_page = m_pages[timer];
317
318 // Add score
319 if (!m_active_page->addScore(m_default_name,
320 score,
321 max_score,
322 QDateTime::currentDateTime(),
323 settings.value("Current/Size", 4).toInt())) {
324 return false;
325 }
326
327 // Show tab
328 if (m_tabs->indexOf(m_active_page) == -1) {
329 addTab(m_active_page);
330 }
331 m_tabs->setCurrentWidget(m_active_page);
332
333 Page* tanglet_page = m_pages[Clock::Tanglet];
334 const int index = m_tabs->indexOf(tanglet_page);
335 if ((index != -1) && (m_active_page != tanglet_page) && (tanglet_page->isEmpty())) {
336 m_tabs->removeTab(index);
337 }
338
339 // Show lineedit
340 m_active_page->editStart(m_username);
341
342 m_buttons->button(QDialogButtonBox::Close)->setDefault(false);
343
344 return true;
345 }
346
347 //-----------------------------------------------------------------------------
348
isHighScore(int score,int timer)349 int ScoresDialog::isHighScore(int score, int timer)
350 {
351 if (m_max[timer] == -1) {
352 m_max[timer] = 1;
353 ScoresDialog();
354 }
355
356 if (score >= m_max[timer]) {
357 return 2;
358 } else if (score >= m_min[timer]) {
359 return 1;
360 } else {
361 return 0;
362 }
363 }
364
365 //-----------------------------------------------------------------------------
366
migrate()367 void ScoresDialog::migrate()
368 {
369 QSettings settings;
370 if (!settings.contains("Scores/Values")) {
371 return;
372 }
373
374 const QStringList data = settings.value("Scores/Values").toStringList();
375 settings.remove("Scores/Values");
376
377 QVector<int> indexes(Clock::TotalTimers, 0);
378
379 for (const QString& s : data) {
380 const QStringList values = s.split(':');
381 if (values.size() < 3 || values.size() > 6) {
382 continue;
383 }
384
385 const QString name = values[0];
386 const int score = values[1].toInt();
387 const int max_score = values.value(4, "-1").toInt();
388 const QDateTime date = QDateTime::fromString(values[2], "yyyy.MM.dd-hh.mm.ss");
389 const int timer = values.value(3, QString::number(Clock::Tanglet)).toInt();
390 const int size = values.value(5, "-1").toInt();
391
392 int& index = indexes[timer];
393 settings.beginWriteArray(Clock::timerScoresGroup(timer));
394 settings.setArrayIndex(index);
395 settings.setValue("Name", name);
396 settings.setValue("Score", score);
397 settings.setValue("Maximum", max_score);
398 settings.setValue("Size", size);
399 settings.setValue("Date", date.toString(Qt::ISODate));
400 settings.endArray();
401 ++index;
402 }
403 }
404
405 //-----------------------------------------------------------------------------
406
hideEvent(QHideEvent * event)407 void ScoresDialog::hideEvent(QHideEvent* event)
408 {
409 editingFinished();
410 QDialog::hideEvent(event);
411 }
412
413 //-----------------------------------------------------------------------------
414
keyPressEvent(QKeyEvent * event)415 void ScoresDialog::keyPressEvent(QKeyEvent* event)
416 {
417 if ((event->key() == Qt::Key_Enter) || (event->key() == Qt::Key_Return)) {
418 event->ignore();
419 return;
420 }
421 QDialog::keyPressEvent(event);
422 }
423
424 //-----------------------------------------------------------------------------
425
editingFinished()426 void ScoresDialog::editingFinished()
427 {
428 if (m_active_page) {
429 m_active_page->editFinish(m_username);
430 m_active_page = nullptr;
431
432 m_buttons->button(QDialogButtonBox::Close)->setDefault(true);
433 m_buttons->button(QDialogButtonBox::Close)->setFocus();
434 }
435 }
436
437 //-----------------------------------------------------------------------------
438
addTab(Page * page)439 void ScoresDialog::addTab(Page* page)
440 {
441 const QString name = page->name();
442 int index = 0;
443 for (index = 0; index < m_tabs->count(); ++index) {
444 if (m_tabs->tabText(index).localeAwareCompare(name) >= 0) {
445 break;
446 }
447 }
448 m_tabs->insertTab(index, page, name);
449 }
450
451 //-----------------------------------------------------------------------------
452