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