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 "window.h"
8 
9 #include "board.h"
10 #include "clock.h"
11 #include "gzip.h"
12 #include "language_dialog.h"
13 #include "locale_dialog.h"
14 #include "new_game_dialog.h"
15 #include "scores_dialog.h"
16 
17 #include <QAction>
18 #include <QActionGroup>
19 #include <QApplication>
20 #include <QDialog>
21 #include <QDialogButtonBox>
22 #include <QDragEnterEvent>
23 #include <QDropEvent>
24 #include <QFile>
25 #include <QFileDialog>
26 #include <QLabel>
27 #include <QMenu>
28 #include <QMenuBar>
29 #include <QMessageBox>
30 #include <QMimeData>
31 #include <QSettings>
32 #include <QStackedWidget>
33 #include <QStandardPaths>
34 #include <QStyle>
35 #include <QTemporaryFile>
36 #include <QTextEdit>
37 #include <QTextStream>
38 #include <QVBoxLayout>
39 
40 //-----------------------------------------------------------------------------
41 
42 /**
43  * @brief The Window::State class controls which central widget is shown in the main window.
44  */
45 class Window::State
46 {
47 public:
48 	/**
49 	 * Constructs a state instance.
50 	 * @param window the window to control
51 	 */
State(Window * window)52 	explicit State(Window* window)
53 		: m_window(window)
54 	{
55 	}
56 
57 	/**
58 	 * Destroys a state instance.
59 	 */
~State()60 	virtual ~State() { }
61 
62 	/**
63 	 * Handle the state being entered.
64 	 */
enter()65 	virtual void enter() { }
66 
67 	/**
68 	 * Handle request to switch to the new game state.
69 	 */
newGame()70 	virtual void newGame()
71 	{
72 		setState("NewGame");
73 	}
74 
75 	/**
76 	 * Handle request to switch to the open game state.
77 	 */
openGame()78 	virtual void openGame()
79 	{
80 		setState("OpenGame");
81 	}
82 
83 	/**
84 	 * Handle request to switch to the optimizing state.
85 	 */
optimizingStarted()86 	virtual void optimizingStarted() { }
87 
88 	/**
89 	 * Handle request to switch back from the optimizing state.
90 	 */
optimizingFinished()91 	virtual void optimizingFinished() { }
92 
93 	/**
94 	 * Handle request to switch to the play state.
95 	 */
play()96 	virtual void play() { }
97 
98 	/**
99 	 * Handle request to switch to the automatically paused state.
100 	 */
autoPause()101 	virtual void autoPause() { }
102 
103 	/**
104 	 * Handle request to switch back from the automatically paused state.
105 	 */
autoResume()106 	virtual void autoResume() { }
107 
108 	/**
109 	 * Handle request to switch to the paused state.
110 	 */
pause()111 	virtual void pause() { }
112 
113 	/**
114 	 * Handle request to switch back from the paused state.
115 	 */
resume()116 	virtual void resume() { }
117 
118 	/**
119 	 * Handle request to switch to the finished state.
120 	 */
finish()121 	virtual void finish()
122 	{
123 		setState("Finish");
124 	}
125 
126 protected:
127 	/**
128 	 * Pause or resume the game.
129 	 * @param paused whether to pause or resume
130 	 */
setPaused(bool paused)131 	void setPaused(bool paused)
132 	{
133 		m_window->m_board->setPaused(paused);
134 		m_window->m_pause_action->setChecked(paused);
135 	}
136 
137 	/**
138 	 * Set which widget is shown in the main window.
139 	 * @param index
140 	 */
setContentsIndex(int index)141 	void setContentsIndex(int index)
142 	{
143 		m_window->m_contents->setCurrentIndex(index);
144 	}
145 
146 	/**
147 	 * Sets the current state of the main window.
148 	 * @param state which state to switch to
149 	 */
setState(const QString & state)150 	void setState(const QString& state)
151 	{
152 		m_window->m_previous_state = m_window->m_state;
153 		m_window->m_state = m_window->m_states.value(state);
154 		m_window->m_state->enter();
155 	}
156 
157 	/**
158 	 * Restores the previous state of the main window.
159 	 */
setPreviousState()160 	void setPreviousState()
161 	{
162 		m_window->m_state = m_window->m_previous_state;
163 		m_window->m_state->enter();
164 		m_window->m_previous_state = nullptr;
165 	}
166 
167 private:
168 	Window* m_window; /**< window to control */
169 };
170 
171 //-----------------------------------------------------------------------------
172 
173 /**
174  * The Window::NewGameState class is a temporary state that shows a load screen while generating a
175  * board before switching to the Play state. It will also show the automatic pause screen and the
176  * optimizing screen if necessary.
177  */
178 class Window::NewGameState : public Window::State
179 {
180 public:
NewGameState(Window * window)181 	explicit NewGameState(Window* window)
182 		: State(window)
183 	{
184 	}
185 
enter()186 	void enter() override
187 	{
188 		m_next_state = "Play";
189 		setPaused(true);
190 		setContentsIndex(4);
191 	}
192 
optimizingStarted()193 	void optimizingStarted() override
194 	{
195 		setState("Optimizing");
196 	}
197 
play()198 	void play() override
199 	{
200 		setState(m_next_state);
201 	}
202 
autoPause()203 	void autoPause() override
204 	{
205 		m_next_state = "AutoPause";
206 	}
207 
autoResume()208 	void autoResume() override
209 	{
210 		m_next_state = "Play";
211 	}
212 
213 private:
214 	QString m_next_state; /**< state to enter once the game has been started */
215 };
216 
217 //-----------------------------------------------------------------------------
218 
219 /**
220  * The Window::OpenGameState class is a temporary state that shows a load screen while opening a
221  * board before switching to the Play state. It will also show the automatic pause screen and the
222  * optimizing screen if necessary.
223  */
224 class Window::OpenGameState : public Window::State
225 {
226 public:
OpenGameState(Window * window)227 	explicit OpenGameState(Window* window)
228 		: State(window)
229 	{
230 	}
231 
enter()232 	void enter() override
233 	{
234 		m_next_state = "Play";
235 		setPaused(true);
236 		setContentsIndex(2);
237 	}
238 
optimizingStarted()239 	void optimizingStarted() override
240 	{
241 		setState("Optimizing");
242 	}
243 
play()244 	void play() override
245 	{
246 		setState(m_next_state);
247 	}
248 
autoPause()249 	void autoPause() override
250 	{
251 		m_next_state = "AutoPause";
252 	}
253 
autoResume()254 	void autoResume() override
255 	{
256 		m_next_state = "Play";
257 	}
258 
259 private:
260 	QString m_next_state; /**< state to enter once the game has been loaded */
261 };
262 
263 //-----------------------------------------------------------------------------
264 
265 /**
266  * The Window::OptimizingState class shows the optimizing screen while the word list is being
267  * optimized and then switches back to the previous state.
268  */
269 class Window::OptimizingState : public Window::State
270 {
271 public:
OptimizingState(Window * window)272 	explicit OptimizingState(Window* window)
273 		: State(window)
274 	{
275 	}
276 
enter()277 	void enter() override
278 	{
279 		setContentsIndex(5);
280 	}
281 
optimizingFinished()282 	void optimizingFinished() override
283 	{
284 		setPreviousState();
285 	}
286 };
287 
288 //-----------------------------------------------------------------------------
289 
290 /**
291  * The Window::PlayState class shows the play area. It will switch to the automatic pause or regular
292  * pause screen if needed.
293  */
294 class Window::PlayState : public Window::State
295 {
296 public:
PlayState(Window * window)297 	explicit PlayState(Window* window)
298 		: State(window)
299 	{
300 	}
301 
enter()302 	void enter() override
303 	{
304 		setPaused(false);
305 		setContentsIndex(0);
306 	}
307 
autoPause()308 	void autoPause() override
309 	{
310 		setState("AutoPause");
311 	}
312 
pause()313 	void pause() override
314 	{
315 		setState("Pause");
316 	}
317 };
318 
319 //-----------------------------------------------------------------------------
320 
321 /**
322  * The Window::AutoPauseState class will show the pause screen until it its reference count of times
323  * called is reduced to 0, at which point it will show the previous state. If any state is requested,
324  * it will switch to that instead.
325  *
326  * This state is used to temporarily pause the game while a menu is shown.
327  */
328 class Window::AutoPauseState : public Window::State
329 {
330 public:
AutoPauseState(Window * window)331 	explicit AutoPauseState(Window* window)
332 		: State(window)
333 		, m_count(0)
334 	{
335 	}
336 
enter()337 	void enter() override
338 	{
339 		setPaused(true);
340 		setContentsIndex(1);
341 		m_count++;
342 	}
343 
autoPause()344 	void autoPause() override
345 	{
346 		m_count++;
347 	}
348 
autoResume()349 	void autoResume() override
350 	{
351 		m_count--;
352 		if (m_count < 1) {
353 			m_count = 0;
354 			setState("Play");
355 		}
356 	}
357 
newGame()358 	void newGame() override
359 	{
360 		m_count = 0;
361 		setState("NewGame");
362 	}
363 
openGame()364 	void openGame() override
365 	{
366 		m_count = 0;
367 		setState("OpenGame");
368 	}
369 
pause()370 	void pause() override
371 	{
372 		m_count = 0;
373 		setState("Pause");
374 	}
375 
resume()376 	void resume() override
377 	{
378 		m_count = 0;
379 		setState("Play");
380 	}
381 
finish()382 	void finish() override
383 	{
384 		m_count = 0;
385 		setState("Finish");
386 	}
387 
388 private:
389 	int m_count; /**< reference count of how many times automatic pause has been entered */
390 };
391 
392 //-----------------------------------------------------------------------------
393 
394 /**
395  * The Window::PauseState class shows the pause screen. It will switch to the play state when
396  * hidden. The base class handles switching to the new or open states when requested.
397  */
398 class Window::PauseState : public Window::State
399 {
400 public:
PauseState(Window * window)401 	explicit PauseState(Window* window)
402 		: State(window)
403 	{
404 	}
405 
enter()406 	void enter() override
407 	{
408 		setPaused(true);
409 		setContentsIndex(1);
410 	}
411 
resume()412 	void resume() override
413 	{
414 		setState("Play");
415 	}
416 };
417 
418 //-----------------------------------------------------------------------------
419 
420 /**
421  * The Window::FinishState class shows the main play area and does not switch to any other state
422  * like pause or automatic pause, because the game is already over. The base class handles switching
423  * to the new or open states when requested.
424  */
425 class Window::FinishState : public Window::State
426 {
427 public:
FinishState(Window * window)428 	explicit FinishState(Window* window)
429 		: State(window)
430 	{
431 	}
432 
enter()433 	void enter() override
434 	{
435 		setPaused(false);
436 		setContentsIndex(0);
437 	}
438 };
439 
440 //-----------------------------------------------------------------------------
441 
Window(const QString & file)442 Window::Window(const QString& file)
443 	: m_pause_action(nullptr)
444 	, m_previous_state(nullptr)
445 {
446 	setAcceptDrops(true);
447 
448 	// Create states
449 	m_states.insert("NewGame", new NewGameState(this));
450 	m_states.insert("OpenGame", new OpenGameState(this));
451 	m_states.insert("Optimizing", new OptimizingState(this));
452 	m_states.insert("Play", new PlayState(this));
453 	m_states.insert("AutoPause", new AutoPauseState(this));
454 	m_states.insert("Pause", new PauseState(this));
455 	m_states.insert("Finish", new FinishState(this));
456 	m_state = m_states.value("NewGame");
457 
458 	// Create widgets
459 	m_contents = new QStackedWidget(this);
460 	setCentralWidget(m_contents);
461 
462 	m_board = new Board(this);
463 	m_contents->addWidget(m_board);
464 	connect(m_board, &Board::started, this, &Window::gameStarted);
465 	connect(m_board, &Board::finished, this, &Window::gameFinished);
466 	connect(m_board, &Board::optimizingStarted, this, &Window::optimizingStarted);
467 	connect(m_board, &Board::optimizingFinished, this, &Window::optimizingFinished);
468 
469 	// Create pause screen
470 	m_pause_screen = new QLabel(tr("<p><b><big>Paused</big></b><br>Click to resume playing.</p>"), this);
471 	m_pause_screen->setAlignment(Qt::AlignCenter);
472 	m_pause_screen->installEventFilter(this);
473 	m_contents->addWidget(m_pause_screen);
474 
475 	// Create open game screen
476 	QLabel* open_game_screen = new QLabel(tr("<p><b><big>Please wait</big></b><br>Loading game...</p>"), this);
477 	open_game_screen->setAlignment(Qt::AlignCenter);
478 	m_contents->addWidget(open_game_screen);
479 
480 	// Create start screen
481 	QLabel* start_screen = new QLabel(tr("Click to start a new game."), this);
482 	start_screen->setAlignment(Qt::AlignCenter);
483 	start_screen->installEventFilter(this);
484 	m_contents->addWidget(start_screen);
485 
486 	// Create new game screen
487 	QLabel* new_game_screen = new QLabel(tr("<p><b><big>Please wait</big></b><br>Generating a new board...</p>"), this);
488 	new_game_screen->setAlignment(Qt::AlignCenter);
489 	m_contents->addWidget(new_game_screen);
490 
491 	// Create optimizing screen
492 	QLabel* optimizing_screen = new QLabel(tr("<p><b><big>Please wait</big></b><br>Optimizing word list...</p>"), this);
493 	optimizing_screen->setAlignment(Qt::AlignCenter);
494 	m_contents->addWidget(optimizing_screen);
495 
496 	// Create game menu
497 	QMenu* menu = menuBar()->addMenu(tr("&Game"));
498 	menu->addAction(tr("New &Game..."), this, SLOT(newGame()), tr("Ctrl+Shift+N"));
499 	menu->addAction(tr("&New Roll"), this, SLOT(newRoll()), QKeySequence::New);
500 	menu->addAction(tr("&Choose..."), this, SLOT(chooseGame()), QKeySequence::Open);
501 	menu->addAction(tr("&Share..."), this, SLOT(shareGame()), QKeySequence::Save);
502 	menu->addSeparator();
503 	QAction* end_action = menu->addAction(tr("&End"), this, SLOT(endGame()));
504 	end_action->setEnabled(false);
505 	connect(m_board, &Board::pauseAvailable, end_action, &QAction::setEnabled);
506 	m_pause_action = menu->addAction(tr("&Pause"));
507 	m_pause_action->setCheckable(true);
508 	m_pause_action->setShortcut(tr("Ctrl+P"));
509 	m_pause_action->setEnabled(false);
510 	connect(m_pause_action, &QAction::triggered, this, &Window::setPaused);
511 	connect(m_board, &Board::pauseAvailable, m_pause_action, &QAction::setEnabled);
512 	menu->addSeparator();
513 	m_details_action = menu->addAction(tr("&Details"), this, SLOT(showDetails()));
514 	m_details_action->setEnabled(false);
515 	menu->addAction(tr("&High Scores"), this, SLOT(showScores()), tr("Ctrl+H"));
516 	menu->addSeparator();
517 	QAction* action = menu->addAction(tr("&Quit"), this, SLOT(close()), tr("Ctrl+Q"));
518 	action->setMenuRole(QAction::QuitRole);
519 	monitorVisibility(menu);
520 
521 	// Create settings menu
522 	menu = menuBar()->addMenu(tr("&Settings"));
523 	QMenu* submenu = menu->addMenu(tr("Show &Maximum Score"));
524 	QAction* score_actions[3];
525 	score_actions[0] = submenu->addAction(tr("&Never"));
526 	score_actions[1] = submenu->addAction(tr("&End Of Game"));
527 	score_actions[2]  = submenu->addAction(tr("&Always"));
528 	QActionGroup* group = new QActionGroup(this);
529 	for (int i = 0; i < 3; ++i) {
530 		score_actions[i]->setData(i);
531 		score_actions[i]->setCheckable(true);
532 		group->addAction(score_actions[i]);
533 	}
534 	connect(group, &QActionGroup::triggered, m_board, &Board::setShowMaximumScore);
535 	QAction* missed_action = menu->addAction(tr("Show Missed &Words"));
536 	missed_action->setCheckable(true);
537 	connect(missed_action, &QAction::toggled, m_board, &Board::setShowMissedWords);
538 	QAction* counts_action = menu->addAction(tr("Show Word &Counts"));
539 	counts_action->setCheckable(true);
540 	counts_action->setChecked(true);
541 	connect(counts_action, &QAction::toggled, m_board, &Board::setShowWordCounts);
542 	menu->addAction(tr("&Board Language..."), this, SLOT(showLanguage()));
543 	menu->addSeparator();
544 	menu->addAction(tr("Application &Language..."), this, SLOT(showLocale()));
545 	monitorVisibility(menu);
546 
547 	// Create help menu
548 	menu = menuBar()->addMenu(tr("&Help"));
549 	menu->addAction(tr("&Controls"), this, SLOT(showControls()));
550 	menu->addSeparator();
551 	action = menu->addAction(tr("&About"), this, SLOT(about()));
552 	action->setMenuRole(QAction::AboutRole);
553 	action = menu->addAction(tr("About &Qt"), qApp, SLOT(aboutQt()));
554 	action->setMenuRole(QAction::AboutQtRole);
555 	monitorVisibility(menu);
556 
557 	// Load settings
558 	QSettings settings;
559 	QAction* score_action = score_actions[qBound(0, settings.value("ShowMaximumScore", 1).toInt(), 2)];
560 	score_action->setChecked(true);
561 	m_board->setShowMaximumScore(score_action);
562 	missed_action->setChecked(settings.value("ShowMissed", true).toBool());
563 	counts_action->setChecked(settings.value("ShowWordCounts", true).toBool());
564 	restoreGeometry(settings.value("Geometry").toByteArray());
565 
566 	// Start game
567 	QString current = file;
568 	if (settings.contains("Current/Version")) {
569 		if (current.isEmpty() ||
570 				QMessageBox::question(this, tr("Question"), tr("End the current game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
571 			current = ":saved:";
572 		}
573 	}
574 
575 	m_state->finish();
576 	m_contents->setCurrentIndex(3);
577 	if (current.isEmpty()) {
578 		newGame();
579 	} else {
580 		startGame(current);
581 	}
582 }
583 
584 //-----------------------------------------------------------------------------
585 
eventFilter(QObject * watched,QEvent * event)586 bool Window::eventFilter(QObject* watched, QEvent* event)
587 {
588 	if (watched == m_pause_screen) {
589 		if (event->type() == QEvent::MouseButtonPress) {
590 			m_state->resume();
591 			return true;
592 		} else {
593 			return false;
594 		}
595 	} else if (watched == m_contents->widget(3)) {
596 		if (event->type() == QEvent::MouseButtonPress) {
597 			newGame();
598 			return true;
599 		} else {
600 			return false;
601 		}
602 	} else {
603 		return QMainWindow::eventFilter(watched, event);
604 	}
605 }
606 
607 //-----------------------------------------------------------------------------
608 
closeEvent(QCloseEvent * event)609 void Window::closeEvent(QCloseEvent* event)
610 {
611 	QSettings().setValue("Geometry", saveGeometry());
612 	QMainWindow::closeEvent(event);
613 }
614 
615 //-----------------------------------------------------------------------------
616 
dragEnterEvent(QDragEnterEvent * event)617 void Window::dragEnterEvent(QDragEnterEvent* event)
618 {
619 	if (!(event->possibleActions() & Qt::CopyAction)) {
620 		return;
621 	}
622 
623 	// Handle dragged file
624 	if (event->mimeData()->hasUrls()) {
625 		const QList<QUrl> urls = event->mimeData()->urls();
626 		if (urls.size() != 1) {
627 			return;
628 		}
629 		const QUrl url = urls.first();
630 		if (!url.isLocalFile()) {
631 			return;
632 		}
633 		const QString filename = url.toLocalFile();
634 		if (!filename.endsWith(".tanglet")) {
635 			return;
636 		}
637 	// Handle dragged game data
638 	} else if (!event->mimeData()->hasFormat("application/x-tanglet")) {
639 		return;
640 	}
641 
642 	event->setDropAction(Qt::CopyAction);
643 	event->acceptProposedAction();
644 }
645 
646 //-----------------------------------------------------------------------------
647 
dropEvent(QDropEvent * event)648 void Window::dropEvent(QDropEvent* event)
649 {
650 	if (!(event->possibleActions() & Qt::CopyAction)) {
651 		return;
652 	}
653 
654 	// Handle dropped file
655 	if (event->mimeData()->hasUrls()) {
656 		const QList<QUrl> urls = event->mimeData()->urls();
657 		if (urls.size() != 1) {
658 			return;
659 		}
660 		const QUrl url = urls.first();
661 		if (!url.isLocalFile()) {
662 			return;
663 		}
664 		const QString filename = url.toLocalFile();
665 		if (!filename.endsWith(".tanglet")) {
666 			return;
667 		}
668 		if (endGame()) {
669 			startGame(filename);
670 		}
671 	// Handle dropped game data
672 	} else if (event->mimeData()->hasFormat("application/x-tanglet")) {
673 		QTemporaryFile file;
674 		if (file.open()) {
675 			file.write(event->mimeData()->data("application/x-tanglet"));
676 		}
677 		if (endGame()) {
678 			startGame(file.fileName());
679 		}
680 	}
681 
682 	event->setDropAction(Qt::CopyAction);
683 	event->acceptProposedAction();
684 }
685 
686 //-----------------------------------------------------------------------------
687 
event(QEvent * event)688 bool Window::event(QEvent* event)
689 {
690 	if (m_pause_action && m_pause_action->isEnabled()) {
691 		switch (event->type()) {
692 		case QEvent::WindowDeactivate:
693 			if (!QApplication::activeWindow() && !QApplication::activePopupWidget() && !QApplication::activeModalWidget()) {
694 				m_state->pause();
695 			}
696 			break;
697 		case QEvent::WindowBlocked:
698 			m_state->autoPause();
699 			break;
700 		case QEvent::WindowUnblocked:
701 			m_state->autoResume();
702 			break;
703 		default:
704 			break;
705 		}
706 	}
707 	return QMainWindow::event(event);
708 }
709 
710 //-----------------------------------------------------------------------------
711 
about()712 void Window::about()
713 {
714 	QMessageBox::about(this, tr("About"),
715 		QString("<center><p><big><b>%1</b></big><br/>%2<br/><small>%3<br/>%4</small></p><p>%5</p><p>%6</p></center>")
716 		.arg(tr("Tanglet %1").arg(QCoreApplication::applicationVersion()),
717 			tr("A single player variant of <a href=\"http://en.wikipedia.org/wiki/Boggle\">Boggle</a>"),
718 			tr("Copyright &copy; 2009-%1 Graeme Gott").arg("2021"),
719 			tr("Released under the <a href=\"http://www.gnu.org/licenses/gpl.html\">GPL 3</a> license"),
720 			tr("English word list is based on <a href=\"http://wordlist.sourceforge.net/\">SCOWL</a> by Kevin Atkinson"),
721 			tr("Hebrew word list is based on <a href=\"http://hspell.ivrix.org.il/\">Hspell</a> by Nadav Har'El and Dan Kenigsberg"))
722 	);
723 }
724 
725 //-----------------------------------------------------------------------------
726 
newRoll()727 void Window::newRoll()
728 {
729 	if (endGame()) {
730 		startGame();
731 	}
732 }
733 
734 //-----------------------------------------------------------------------------
735 
newGame()736 void Window::newGame()
737 {
738 	if (endGame()) {
739 		NewGameDialog dialog(this);
740 		if (dialog.exec() == QDialog::Accepted) {
741 			startGame();
742 		}
743 	}
744 }
745 
746 
747 //-----------------------------------------------------------------------------
748 
chooseGame()749 void Window::chooseGame()
750 {
751 	if (!endGame()) {
752 		return;
753 	}
754 
755 	QString filename = QFileDialog::getOpenFileName(window(),
756 			tr("Import Game"),
757 			QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
758 			tr("Tanglet Games (*.tanglet)"));
759 	if (filename.isEmpty()) {
760 		return;
761 	}
762 
763 	startGame(filename);
764 }
765 
766 //-----------------------------------------------------------------------------
767 
shareGame()768 void Window::shareGame()
769 {
770 	QString filename = QFileDialog::getSaveFileName(window(),
771 			tr("Export Game"),
772 			QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
773 			tr("Tanglet Games (*.tanglet)"));
774 
775 	if (filename.isEmpty()) {
776 		return;
777 	}
778 
779 	if (!filename.endsWith(".tanglet")) {
780 		filename += ".tanglet";
781 	}
782 
783 	// Share game
784 	{
785 		QSettings settings;
786 		settings.beginGroup("Current");
787 
788 		QSettings game(filename, QSettings::IniFormat);
789 		game.beginGroup("Game");
790 
791 		game.setValue("Version", settings.value("Version"));
792 		game.setValue("Size", settings.value("Size"));
793 		game.setValue("Density", settings.value("Density"));
794 		game.setValue("Minimum", settings.value("Minimum"));
795 		game.setValue("TimerMode", settings.value("TimerMode"));
796 		game.setValue("Letters", settings.value("Letters"));
797 		game.setValue("Locale", settings.value("Locale"));
798 		game.setValue("Dictionary", settings.value("Dictionary"));
799 
800 		const QString dice = settings.value("Dice").toString();
801 		if (dice.startsWith("tanglet:")) {
802 			game.setValue("Dice", dice);
803 		} else {
804 			QFile file(dice);
805 			if (file.open(QFile::ReadOnly)) {
806 				game.setValue("Dice", file.readAll().toBase64());
807 			}
808 		}
809 
810 		const QString words = settings.value("Words").toString();
811 		if (words.startsWith("tanglet:")) {
812 			game.setValue("Words", words);
813 		} else {
814 			QFile file(words);
815 			if (file.open(QFile::ReadOnly)) {
816 				game.setValue("Words", file.readAll().toBase64());
817 			}
818 		}
819 	}
820 	gzip(filename);
821 }
822 
823 //-----------------------------------------------------------------------------
824 
endGame()825 bool Window::endGame()
826 {
827 	if (!m_board->isFinished()) {
828 		if (QMessageBox::question(this, tr("Question"), tr("End the current game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
829 			m_board->abort();
830 		} else {
831 			return false;
832 		}
833 	}
834 	return true;
835 }
836 
837 //-----------------------------------------------------------------------------
838 
autoPause()839 void Window::autoPause()
840 {
841 	m_state->autoPause();
842 }
843 
844 //-----------------------------------------------------------------------------
845 
autoResume()846 void Window::autoResume()
847 {
848 	m_state->autoResume();
849 }
850 
851 //-----------------------------------------------------------------------------
852 
setPaused(bool paused)853 void Window::setPaused(bool paused)
854 {
855 	if (paused) {
856 		m_state->pause();
857 	} else {
858 		m_state->resume();
859 	}
860 }
861 
862 //-----------------------------------------------------------------------------
863 
showDetails()864 void Window::showDetails()
865 {
866 	QSettings settings;
867 	int size = settings.value("Current/Size").toInt();
868 	int density = settings.value("Current/Density").toInt();
869 	int minimum = settings.value("Current/Minimum").toInt();
870 	int timer = settings.value("Current/TimerMode").toInt();
871 	QMessageBox::information(this, tr("Details"),
872 		QString("<p><b>%1</b> %2<br>"
873 			"<b>%3</b> %4<br>"
874 			"<b>%5</b> %6<br>"
875 			"<b>%7</b> %8<br>"
876 			"<b>%9</b> %10</p>")
877 		.arg(tr("Board Size:"), (size == 4) ? tr("Normal") : tr("Large"))
878 		.arg(tr("Word Density:"), NewGameDialog::densityString(density))
879 		.arg(tr("Word Length:"), tr("%1 or more letters").arg(minimum))
880 		.arg(tr("Game Type:"), Clock::timerToString(timer))
881 		.arg(tr("Description:"), Clock::timerDescription(timer)));
882 }
883 
884 //-----------------------------------------------------------------------------
885 
showScores()886 void Window::showScores()
887 {
888 	ScoresDialog scores(this);
889 	scores.exec();
890 }
891 
892 //-----------------------------------------------------------------------------
893 
showLanguage()894 void Window::showLanguage()
895 {
896 	LanguageDialog dialog(this);
897 	if (dialog.exec() == QDialog::Accepted) {
898 		newGame();
899 	}
900 }
901 
902 //-----------------------------------------------------------------------------
903 
showLocale()904 void Window::showLocale()
905 {
906 	LocaleDialog dialog(this);
907 	dialog.exec();
908 }
909 
910 //-----------------------------------------------------------------------------
911 
showControls()912 void Window::showControls()
913 {
914 	QMessageBox::information(this, tr("Controls"), tr(
915 		"<p><b><big>Mouse Play:</big></b><br>"
916 		"<b>Select a word:</b> Click on the letters of a word.<br>"
917 		"<b>Make a guess:</b> Click on the last selected letter.<br>"
918 		"<b>Erase letters:</b> Click on an earlier selected letter.<br>"
919 		"<b>Clear the word:</b> Click twice on the first selected letter.</p>"
920 		"<p><b><big>Keyboard Play:</big></b><br>"
921 		"<b>Select a word:</b> Type the letters of a word.<br>"
922 		"<b>Make a guess:</b> Press Enter.<br>"
923 		"<b>Clear the word:</b> Press Ctrl+Backspace.</p>"
924 	));
925 }
926 
927 //-----------------------------------------------------------------------------
928 
optimizingStarted()929 void Window::optimizingStarted()
930 {
931 	m_state->optimizingStarted();
932 }
933 
934 //-----------------------------------------------------------------------------
935 
optimizingFinished()936 void Window::optimizingFinished()
937 {
938 	m_state->optimizingFinished();
939 }
940 
941 //-----------------------------------------------------------------------------
942 
gameStarted()943 void Window::gameStarted()
944 {
945 	m_state->play();
946 	m_details_action->setEnabled(true);
947 }
948 
949 //-----------------------------------------------------------------------------
950 
gameFinished(int score,int max_score)951 void Window::gameFinished(int score, int max_score)
952 {
953 	m_state->finish();
954 	ScoresDialog scores(this);
955 	if (scores.addScore(score, max_score)) {
956 		scores.exec();
957 	}
958 
959 	QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
960 	dir.remove("current");
961 	dir.remove("current-dice");
962 	dir.remove("current-words");
963 }
964 
965 //-----------------------------------------------------------------------------
966 
monitorVisibility(QMenu * menu)967 void Window::monitorVisibility(QMenu* menu)
968 {
969 #ifndef Q_OS_MAC
970 	connect(menu, &QMenu::aboutToShow, this, &Window::autoPause);
971 	connect(menu, &QMenu::aboutToHide, this, &Window::autoResume);
972 #endif
973 }
974 
975 //-----------------------------------------------------------------------------
976 
startGame(const QString & filename)977 void Window::startGame(const QString& filename)
978 {
979 	QSettings settings;
980 
981 	if (filename.isEmpty()) {
982 		// Start a new game
983 		settings.remove("Current");
984 		settings.sync();
985 		m_state->newGame();
986 		settings.beginGroup("Board");
987 		m_board->generate(settings);
988 	} else if (filename == ":saved:") {
989 		// Continue previous game
990 		m_state->openGame();
991 		settings.beginGroup("Current");
992 		if (!m_board->generate(settings)) {
993 			QMessageBox::warning(this, tr("Error"), tr("Unable to start requested game."));
994 			m_state->finish();
995 			m_contents->setCurrentIndex(3);
996 		}
997 	} else {
998 		// Uncompress requested game
999 		QString current = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
1000 		QDir::home().mkpath(current);
1001 		current += "/current";
1002 
1003 		QByteArray data = gunzip(filename);
1004 		QFile file(current);
1005 		if (!file.open(QFile::WriteOnly)) {
1006 			QMessageBox::warning(this, tr("Error"), tr("Unable to start requested game."));
1007 			return;
1008 		}
1009 		file.write(data);
1010 		file.close();
1011 
1012 		QSettings game(current, QSettings::IniFormat);
1013 		game.beginGroup("Game");
1014 
1015 		// Extract dice
1016 		data = game.value("Dice").toByteArray();
1017 		if (data.startsWith("tanglet:")) {
1018 			game.setValue("Dice", data);
1019 		} else {
1020 			const QString dice = QString("%1-dice").arg(current);
1021 			game.setValue("Dice", dice);
1022 			file.setFileName(dice);
1023 			if (!file.open(QFile::WriteOnly)) {
1024 				QMessageBox::warning(this, tr("Error"), tr("Unable to start requested game."));
1025 				return;
1026 			}
1027 			file.write(QByteArray::fromBase64(data));
1028 			file.close();
1029 		}
1030 
1031 		// Extract words
1032 		data = game.value("Words").toByteArray();
1033 		if (data.startsWith("tanglet:")) {
1034 			game.setValue("Words", data);
1035 		} else {
1036 			const QString words = QString("%1-words").arg(current);
1037 			game.setValue("Words", words);
1038 			file.setFileName(words);
1039 			if (!file.open(QFile::WriteOnly)) {
1040 				QMessageBox::warning(this, tr("Error"), tr("Unable to start requested game."));
1041 				return;
1042 			}
1043 			file.write(QByteArray::fromBase64(data));
1044 			file.close();
1045 		}
1046 		data.clear();
1047 
1048 		// Start requested game
1049 		settings.remove("Current");
1050 		settings.sync();
1051 		m_state->openGame();
1052 		if (!m_board->generate(game)) {
1053 			QMessageBox::warning(this, tr("Error"), tr("Unable to start requested game."));
1054 			m_state->finish();
1055 			m_contents->setCurrentIndex(3);
1056 		}
1057 	}
1058 }
1059 
1060 //-----------------------------------------------------------------------------
1061