gameplay.cpp 47.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/***************************************************************************
 *   Copyright 2009 Stefan Majewsky <majewsky@gmx.net>
 *   Copyright 2014 Ian Wadham <iandw.au@gmail.com>
 *
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
***************************************************************************/

#include "gameplay.h"
Laurent Montel's avatar
Laurent Montel committed
21
#include "palapeli_debug.h"
22
23
24

#include "../file-io/collection-view.h"
#include "../window/puzzletablewidget.h"
25
#include "../window/pieceholder.h"
26
27
28
29
#include "puzzlepreview.h"

#include "scene.h"
#include "view.h"
30
#include "piece.h"
31
#include "texturehelper.h"
32
#include "interactormanager.h"
33
34
35
36
37
38
39
#include "../file-io/puzzle.h"
#include "../file-io/components.h"
#include "../file-io/collection.h"
#include "../creator/puzzlecreator.h"

#include "../config/configdialog.h"
#include "settings.h"
Laurent Montel's avatar
Laurent Montel committed
40
#include <QStackedWidget>
41
#include <QPointer>
42
#include <QPropertyAnimation>
43
#include <QFutureWatcher>
Yuri Chornoivan's avatar
Yuri Chornoivan committed
44
#include <QtMath>
45
#include <QStandardPaths>
46
#include <QInputDialog>
47
#include <QFileDialog>
Laurent Montel's avatar
Laurent Montel committed
48
#include <QRandomGenerator>
Laurent Montel's avatar
Port++    
Laurent Montel committed
49
50
51
#include <KActionCollection>
#include <KLocalizedString>
#include <KMessageBox>
52
53
54

// Use this because comma in type is not possible in foreach macro.
typedef QPair<int, int> DoubleIntPair;
55
56
57

//TODO: move LoadingWidget into here (stack into m_centralWidget)

58
59
const int Palapeli::GamePlay::LargePuzzle = 300;

60
61
62
63
const QString HeaderSaveGroup    ("-PalapeliSavedPuzzle");
const QString HolderSaveGroup    ("Holders");
const QString LocationSaveGroup  ("XYCo-ordinates");
const QString FormerSaveGroup    ("SaveGame");
64
65
const QString AppearanceSaveGroup("Appearance");
const QString PreviewSaveGroup   ("PuzzlePreview");
66

67
68
69
70
71
72
Palapeli::GamePlay::GamePlay(MainWindow* mainWindow)
	: QObject(mainWindow)
	, m_centralWidget(new QStackedWidget)
	, m_collectionView(new Palapeli::CollectionView)
	, m_puzzleTable(new Palapeli::PuzzleTableWidget)
	, m_puzzlePreview(0)
73
	, m_mainWindow(mainWindow)
74
	, m_puzzle(0)
75
	, m_pieceAreaSize(QSizeF(32.0, 32.0))	// Allow 1024 pixels initially.
76
	, m_savegameTimer(new QTimer(this))
77
78
	, m_currentHolder(0)
	, m_previousHolder(0)
79
	, m_loadingPuzzle(false)
80
	, m_restoredGame(false)
81
82
	, m_originalPieceCount(0)
	, m_currentPieceCount(0)
83
	, m_sizeFactor(1.3)
84
	, m_playing(false)
85
86
	, m_canDeletePuzzle(false)	// No puzzle selected at startup.
	, m_canExportPuzzle(false)
87
{
88
	m_puzzleTableScene = m_puzzleTable->view()->scene();
89
	m_viewList << m_puzzleTable->view();
90
91
92
93
94
	m_savegameTimer->setInterval(500); //write savegame twice per second at most
	m_savegameTimer->setSingleShot(true);
	connect(m_savegameTimer, SIGNAL(timeout()), this, SLOT(updateSavedGame()));
	connect(this, SIGNAL(reportProgress(int,int)),
		m_puzzleTable, SLOT(reportProgress(int,int)));
95
96
	connect(this, SIGNAL(victoryAnimationFinished()),
		m_puzzleTable->view(), SLOT(startVictoryAnimation()));
97
	connect(m_puzzleTable->view(),
Yuri Chornoivan's avatar
Yuri Chornoivan committed
98
		SIGNAL(teleport(Piece*,QPointF,View*)),
99
		this,
Yuri Chornoivan's avatar
Yuri Chornoivan committed
100
		SLOT(teleport(Piece*,QPointF,View*)));
101
102
103
104
}

Palapeli::GamePlay::~GamePlay()
{
105
	deletePuzzleViews();
106
107
108
	delete m_puzzlePreview;
}

109
110
void Palapeli::GamePlay::deletePuzzleViews()
{
Laurent Montel's avatar
Laurent Montel committed
111
	qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::deletePuzzleViews() ...";
112
113
114
	while (! m_viewList.isEmpty()) {
		Palapeli::View* view = m_viewList.takeLast();
		Palapeli::Scene* scene = view->scene();
Laurent Montel's avatar
Laurent Montel committed
115
		qCDebug(PALAPELI_LOG) << "DISCONNECT SLOT(positionChanged(int))";
116
117
		disconnect(scene, SIGNAL(saveMove(int)),
			   this, SLOT(positionChanged(int)));
Laurent Montel's avatar
Laurent Montel committed
118
		qCDebug(PALAPELI_LOG) << "scene->clearPieces();";
119
		view->interactorManager()->resetActiveTriggers();
120
		scene->clearPieces();
Laurent Montel's avatar
Laurent Montel committed
121
		qCDebug(PALAPELI_LOG) << "if (scene != m_puzzleTableScene) {";
122
		if (scene != m_puzzleTableScene) {
Laurent Montel's avatar
Laurent Montel committed
123
			qCDebug(PALAPELI_LOG) << "DELETING holder" << view->windowTitle();
124
125
126
			delete view;
		}
	}
127
128
	m_currentHolder = nullptr;
	m_previousHolder = nullptr;
129
130
}

131
132
133
void Palapeli::GamePlay::init()
{
	// Set up the collection view.
134
	m_collectionView->setModel(Palapeli::Collection::instance(m_mainWindow));
135
136
137
138
139
140
141
142
143
144
	connect(m_collectionView, SIGNAL(playRequest(Palapeli::Puzzle*)), SLOT(playPuzzle(Palapeli::Puzzle*)));

	// Set up the puzzle table.
	m_puzzleTable->showStatusBar(Settings::showStatusBar());

	// Set up the central widget.
	m_centralWidget->addWidget(m_collectionView);
	m_centralWidget->addWidget(m_puzzleTable);
	m_centralWidget->setCurrentWidget(m_collectionView);
	m_mainWindow->setCentralWidget(m_centralWidget);
145
146
147
148
149
150
151
	// Get some current action states from the collection.
	m_canDeletePuzzle = m_mainWindow->actionCollection()->
				action("game_delete")->isEnabled();
	m_canExportPuzzle = m_mainWindow->actionCollection()->
				action("game_export")->isEnabled();
	// Enable collection actions and disable playing actions initially.
	setPalapeliMode(false);
152
153
}

154
155
void Palapeli::GamePlay::shutdown()
{
Laurent Montel's avatar
Laurent Montel committed
156
	qCDebug(PALAPELI_LOG) << "ENTERED Palapeli::GamePlay::shutdown()";
157
158
159
160
161
162
163
164
165
166
	// Make sure the last change is saved.
	if (m_savegameTimer->isActive()) {
		m_savegameTimer->stop();
		updateSavedGame();
	}
	// Delete piece-holders cleanly: no closeEvents in PieceHolder objects
	// and no messages about holders not being empty.
	deletePuzzleViews();
}

167
168
169
170
//BEGIN action handlers

void Palapeli::GamePlay::playPuzzle(Palapeli::Puzzle* puzzle)
{
171
	t.start();	// IDW test. START the clock.
172
173
174
175
	// we need to load the preview every time, although when the puzzle
	// is already loaded because the preview is destroyed in actionGoCollection()
	QTimer::singleShot(0, this, SLOT(loadPreview()));

Laurent Montel's avatar
Laurent Montel committed
176
	qCDebug(PALAPELI_LOG) << "START playPuzzle(): elapsed 0";
177
178
179
180
181
	// Get some current action states from the collection.
	m_canDeletePuzzle = m_mainWindow->actionCollection()->
				action("game_delete")->isEnabled();
	m_canExportPuzzle = m_mainWindow->actionCollection()->
				action("game_export")->isEnabled();
182
	m_centralWidget->setCurrentWidget(m_puzzleTable);
183
	m_puzzlePreview = new Palapeli::PuzzlePreview(m_mainWindow);
184
185

	if (m_loadingPuzzle || (!puzzle) || (m_puzzle == puzzle)) {
186
		if (m_puzzle == puzzle) {
Laurent Montel's avatar
Laurent Montel committed
187
			qCDebug(PALAPELI_LOG) << "RESUMING A PUZZLE.";
188
			// IDW TODO - Show piece-holders.
189
190
191
192
193
194
195
196
197
198
			// Check if puzzle has been completed.
			if (m_currentPieceCount == 1) {
				int result = KMessageBox::questionYesNo(
					m_mainWindow,
					i18n("You have finished the puzzle. Do you want to restart it now?"));
				if (result == KMessageBox::Yes) {
					restartPuzzle();
					return;
				}
			}
199
			// True if same puzzle selected and not still loading.
200
			setPalapeliMode(! m_loadingPuzzle);
201
		}
Laurent Montel's avatar
Laurent Montel committed
202
		qCDebug(PALAPELI_LOG) << "NO LOAD: (m_puzzle == puzzle)"
203
			 << (m_puzzle == puzzle);
Laurent Montel's avatar
Laurent Montel committed
204
		qCDebug(PALAPELI_LOG) << "m_loadingPuzzle" << m_loadingPuzzle
205
			 << (puzzle ? "puzzle != 0" : "puzzle == 0");
206
207
208
		return;		// Already loaded, loading or failed to start.
	}
	m_puzzle = puzzle;
Laurent Montel's avatar
Laurent Montel committed
209
	qCDebug(PALAPELI_LOG) << "RESTART the clock: elapsed" << t.restart(); // IDW test.
210
	loadPuzzle();
Laurent Montel's avatar
Laurent Montel committed
211
	qCDebug(PALAPELI_LOG) << "Returned from loadPuzzle(): elapsed" << t.elapsed();
212
213

	// IDW TODO - There is no way to stop loading a puzzle and start loading
214
	//            another. The only option is to Quit or abort Palapeli.
215
216
217
218
219
220
221
}

void Palapeli::GamePlay::loadPreview()
{
	// IDW TODO - This WAS delaying the showing of the LoadingWidget. Now
	//            it is preventing the balls from moving for a few seconds.

222
223
224
225
	// Get metadata from archive (tar), to be sure of getting image data.
	// The config/palapeli-collectionrc file lacks image metadata (because
	// Palapeli must load the collection-list quickly at startup time).
	const Palapeli::PuzzleComponent* as =
226
		m_puzzle->get(Palapeli::PuzzleComponent::ArchiveStorage);
227
	const Palapeli::PuzzleComponent* cmd = (as == nullptr) ? nullptr :
228
229
230
231
232
233
234
235
		as->cast(Palapeli::PuzzleComponent::Metadata);
	if (cmd) {
		// Load puzzle preview image from metadata.
		const Palapeli::PuzzleMetadata md =
			dynamic_cast<const Palapeli::MetadataComponent*>(cmd)->
			metadata;
		m_puzzlePreview->loadImageFrom(md);
		m_mainWindow->setCaption(md.name);	// Set main title.
236
		delete cmd;
237
238
239
240
241
	}

	m_puzzlePreview->setVisible(Settings::puzzlePreviewVisible());
	connect (m_puzzlePreview, SIGNAL(closing()),
		SLOT(actionTogglePreview()));	// Hide preview: do not delete.
242
243
244
	// sync with mainWindow
	m_mainWindow->actionCollection()->action("view_preview")->
		setChecked(Settings::puzzlePreviewVisible());
245
246
247
248
249
250
251
252
253
254
255
256
257
}

void Palapeli::GamePlay::playPuzzleFile(const QString& path)
{
	const QString id = Palapeli::Puzzle::fsIdentifier(path);
	playPuzzle(new Palapeli::Puzzle(new Palapeli::ArchiveStorageComponent,
						path, id));
}

void Palapeli::GamePlay::actionGoCollection()
{
	m_centralWidget->setCurrentWidget(m_collectionView);
	delete m_puzzlePreview;
258
	m_puzzlePreview = nullptr;
259
	m_mainWindow->setCaption(QString());
260
261
262
263
264
265
	// IDW TODO - Disable piece-holder actions.
	foreach (Palapeli::View* view, m_viewList) {
		if (view != m_puzzleTable->view()) {
			view->hide();
		}
	}
266
267
	// Disable playing actions and enable collection actions.
	setPalapeliMode(false);
268
269
270
271
}

void Palapeli::GamePlay::actionTogglePreview()
{
272
	// This action is OK during puzzle loading.
273
274
	if (m_puzzlePreview) {
		m_puzzlePreview->toggleVisible();
275
		m_mainWindow->actionCollection()->action("view_preview")->
276
			setChecked(Settings::puzzlePreviewVisible());
277
278
		// remember state
		updateSavedGame();
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
	}
}

void Palapeli::GamePlay::actionCreate()
{
	QPointer<Palapeli::PuzzleCreatorDialog> creatorDialog(new Palapeli::PuzzleCreatorDialog);
	if (creatorDialog->exec())
	{
		if (!creatorDialog)
			return;
		Palapeli::Puzzle* puzzle = creatorDialog->result();
		if (!puzzle) {
			delete creatorDialog;
			return;
		}
		Palapeli::Collection::instance()->importPuzzle(puzzle);
		playPuzzle(puzzle);
	}
	delete creatorDialog;
}

void Palapeli::GamePlay::actionDelete()
{
	QModelIndexList indexes = m_collectionView->selectedIndexes();
	//ask user for confirmation
	QStringList puzzleNames;
	foreach (const QModelIndex& index, indexes)
		puzzleNames << index.data(Qt::DisplayRole).toString();
	const int result = KMessageBox::warningContinueCancelList(m_mainWindow, i18n("The following puzzles will be deleted. This action cannot be undone."), puzzleNames);
	if (result != KMessageBox::Continue)
		return;
	//do deletion
	Palapeli::Collection* coll = Palapeli::Collection::instance();
312
313
314
315
316
317
318
319
320
321

	// We cannot simply use a foreach here, because after deleting the first
	// puzzle, the rest of the indexes should no longer be used (model was
	// modified).  Ask again for the list of selected indexes after each
	// step instead.
	while (indexes.size() > 0)
	{
		coll->deletePuzzle(indexes.at(0));
		indexes = m_collectionView->selectedIndexes();
	}
322
323
324
325
}

void Palapeli::GamePlay::actionImport()
{
326
327
328
329
330
	const QString filter = i18nc("Filter for a file dialog", "Palapeli puzzles (*.puzzle)");
	const QStringList paths = QFileDialog::getOpenFileNames(m_mainWindow,
															i18n("Import Palapeli puzzles"),
															QString(),
															filter);
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
	Palapeli::Collection* coll = Palapeli::Collection::instance();
	foreach (const QString& path, paths)
		coll->importPuzzle(path);
}

void Palapeli::GamePlay::actionExport()
{
	QModelIndexList indexes = m_collectionView->selectedIndexes();
	Palapeli::Collection* coll = Palapeli::Collection::instance();
	foreach (const QModelIndex& index, indexes)
	{
		Palapeli::Puzzle* puzzle = coll->puzzleFromIndex(index);
		if (!puzzle)
			continue;
		//get puzzle name (as an initial guess for the file name)
346
		puzzle->get(Palapeli::PuzzleComponent::Metadata);
347
348
349
350
		const Palapeli::MetadataComponent* cmp = puzzle->component<Palapeli::MetadataComponent>();
		if (!cmp)
			continue;
		//ask user for target file name
351
352
353
354
355
356
		const QString startLoc = QString::fromLatin1("%1.puzzle").arg(cmp->metadata.name);
		const QString filter = i18nc("Filter for a file dialog", "Palapeli puzzles (*.puzzle)");
		const QString location = QFileDialog::getSaveFileName(m_mainWindow,
															  i18n("Save Palapeli puzzles"),
															  startLoc,
															  filter);
357
358
359
360
361
362
363
		if (location.isEmpty())
			continue; //process aborted by user
		//do export
		coll->exportPuzzle(index, location);
	}
}

364
365
void Palapeli::GamePlay::createHolder()
{
Laurent Montel's avatar
Laurent Montel committed
366
	qCDebug(PALAPELI_LOG) << "GamePlay::createHolder() entered";
367
368
369
370
371
372
373
374
	bool OK;
	QString name = QInputDialog::getText(m_mainWindow,
		i18n("Create a piece holder"),
		i18n("Enter a short name (optional):"),
		QLineEdit::Normal, QString(""), &OK);
	if (! OK) {
		return;		// If CANCELLED, do not create a piece holder.
	}
375
	createHolder(name);
376
377
	// Merges/moves in new holders add to the progress bar and are saved.
	Palapeli::View* view = m_viewList.last();
378
	view->setCloseUp(true);	// New holders start in close-up scale.
379
380
381
	connect(view->scene(), SIGNAL(saveMove(int)),
		this, SLOT(positionChanged(int)));
	connect(view,
Yuri Chornoivan's avatar
Yuri Chornoivan committed
382
		SIGNAL(teleport(Piece*,QPointF,View*)),
383
		this,
Yuri Chornoivan's avatar
Yuri Chornoivan committed
384
		SLOT(teleport(Piece*,QPointF,View*)));
385
386
	connect(view, SIGNAL(newPieceSelectionSeen(View*)),
		this, SLOT(handleNewPieceSelection(View*)));
387
388
}

389
void Palapeli::GamePlay::createHolder(const QString& name, bool sel)
390
{
391
	Palapeli::PieceHolder* h =
392
		new Palapeli::PieceHolder(m_mainWindow, m_pieceAreaSize, name);
393
	m_viewList << h;
394
	h->initializeZooming();			// Min. view 2x2 to 6x6 pieces.
395
396
	connect(h, SIGNAL(selected(PieceHolder*)),
		this, SLOT(changeSelectedHolder(PieceHolder*)));
397
398
	connect (h, SIGNAL(closing(PieceHolder*)),
		SLOT(closeHolder(PieceHolder*)));
399
400
401
402
403
404
	if (sel) {
		changeSelectedHolder(h);
	}
	else {
		h->setSelected(false);
	}
405
406
	m_puzzleTable->view()->setFocus(Qt::OtherFocusReason);
	m_puzzleTable->activateWindow();	// Return focus to main window.
407
	positionChanged(0);			// Save holder - a little later.
408
409
410
411
}

void Palapeli::GamePlay::deleteHolder()
{
Laurent Montel's avatar
Laurent Montel committed
412
	qCDebug(PALAPELI_LOG) << "GamePlay::deleteHolder() entered";
413
	if (m_currentHolder) {
414
		closeHolder(m_currentHolder);
415
416
417
418
	}
	else {
		KMessageBox::information(m_mainWindow,
			i18n("You need to click on a piece holder to "
419
420
421
422
423
424
425
426
427
428
			     "select it before you can delete it, or "
			     "you can just click on its Close button."));
	}
}

void Palapeli::GamePlay::closeHolder(Palapeli::PieceHolder* h)
{
	if (h->scene()->pieces().isEmpty()) {
		int count = m_viewList.count();
		m_viewList.removeOne(h);
Laurent Montel's avatar
Laurent Montel committed
429
		qCDebug(PALAPELI_LOG) << "m_viewList WAS" << count << "NOW" << m_viewList.count();
430
431
		m_currentHolder = nullptr;
		m_previousHolder = nullptr;
432
433
434
435
436
437
438
		h->deleteLater();
		positionChanged(0);	// Save change - a little later.
	}
	else {
		KMessageBox::information(m_mainWindow,
			i18n("The selected piece holder must be empty "
			     "before you can delete it."));
439
	}
440
441
442
443
}

void Palapeli::GamePlay::selectAll()
{
Laurent Montel's avatar
Laurent Montel committed
444
	qCDebug(PALAPELI_LOG) << "GamePlay::selectAll() entered";
445
446
447
448
449
450
451
	if (m_currentHolder) {
		QList<Palapeli::Piece*> pieces =
					m_currentHolder->scene()->pieces();
		if (! pieces.isEmpty()) {
			foreach (Palapeli::Piece* piece, pieces) {
				piece->setSelected(true);
			}
452
			handleNewPieceSelection(m_currentHolder);
453
454
455
456
457
458
459
460
461
462
463
464
465
		}
		else {
			KMessageBox::information(m_mainWindow,
				i18n("The selected piece holder must contain "
				     "some pieces for 'Select all' to use."));
		}
	}
	else {
		KMessageBox::information(m_mainWindow,
			i18n("You need to click on a piece holder to "
			     "select it before you can select all the "
			     "pieces in it."));
	}
466
467
468
469
}

void Palapeli::GamePlay::rearrangePieces()
{
Laurent Montel's avatar
Laurent Montel committed
470
	qCDebug(PALAPELI_LOG) << "GamePlay::rearrangePieces() entered";
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
	QList<Palapeli::Piece*> selectedPieces;
	Palapeli::View* view = m_puzzleTable->view();
	selectedPieces = getSelectedPieces(view);
	if (selectedPieces.isEmpty()) {
		if (m_currentHolder) {
			view = m_currentHolder;
			selectedPieces = getSelectedPieces(view);
		}
	}
	if (selectedPieces.isEmpty()) {
		KMessageBox::information(m_mainWindow,
			i18n("To rearrange pieces, either the puzzle table "
			     "must have some selected pieces or there "
			     "must be a selected holder with some selected "
			     "pieces in it."));
		return;
	}
	QRectF bRect;
	foreach (Palapeli::Piece* piece, selectedPieces) {
		bRect |= piece->sceneBareBoundingRect();
	}
	Palapeli::Scene* scene = view->scene();
493
494
495
	// If in a piece-holder and ALL pieces are selected, start at (0, 0).
	scene->initializeGrid(((view == m_currentHolder) &&
			(selectedPieces.count() == scene->pieces().count())) ?
496
				QPointF(0.0, 0.0) : bRect.topLeft());
497
498
499
	foreach (Palapeli::Piece* piece, selectedPieces) {
		scene->addToGrid(piece);
	}
500
501
502
503
	if (view == m_currentHolder) {
		// Adjust the piece-holder's scene to frame the pieces.
		scene->setSceneRect(scene->extPiecesBoundingRect());
	}
504
	positionChanged(0);	// There is no attempt to merge pieces here.
505
506
507
508
509
}

void Palapeli::GamePlay::actionZoomIn()
{
	// IDW TODO - Make ZoomIn work for whichever view is active.
510
	m_puzzleTable->view()->zoomIn();
511
512
513
514
515
}

void Palapeli::GamePlay::actionZoomOut()
{
	// IDW TODO - Make ZoomOut work for whichever view is active.
516
	m_puzzleTable->view()->zoomOut();
517
518
}

519
520
521
522
523
524
void Palapeli::GamePlay::restartPuzzle()
{
	if (!m_puzzle) {
		return;	// If no puzzle was successfully loaded and started.
	}
	// Discard the *.save file.
525
526
527
528
529
530
	const QString puzzleLoc(
				QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
									   saveGamePath() + saveGameFileName(m_puzzle->identifier())));
	if (!puzzleLoc.isEmpty())
		QFile(puzzleLoc).remove();

531
532
533
534
	// Load the puzzle and re-shuffle the pieces.
	loadPuzzle();
}

535
536
void Palapeli::GamePlay::teleport(Palapeli::Piece* pieceUnderMouse,
				  const QPointF& scenePos, Palapeli::View* view)
537
{
Laurent Montel's avatar
Laurent Montel committed
538
	qCDebug(PALAPELI_LOG) << "GamePlay::teleport: pieceUnder" << (pieceUnderMouse != nullptr)
539
540
541
		 << "scPos" << scenePos
		 << "PuzzleTable?" << (view == m_puzzleTable->view())
		 << "CurrentHolder?" << (view == m_currentHolder);
542
543
	if (! m_currentHolder) {
		KMessageBox::information(m_mainWindow,
544
545
			i18n("You need to have a piece holder and click it to "
			     "select it before you can transfer pieces into or "
546
			     "out of it."));
547
548
		return;
	}
549
550
551
552
553
554
555
	bool puzzleTableClick = (view == m_puzzleTable->view());
	QList<Palapeli::Piece*> selectedPieces;
	if (puzzleTableClick) {
		if (pieceUnderMouse && (!pieceUnderMouse->isSelected())) {
			pieceUnderMouse->setSelected(true);
		}
		selectedPieces = getSelectedPieces(view);
556
                if (!selectedPieces.isEmpty()) {
557
			// Transfer from the puzzle table to a piece-holder.
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
			foreach (Palapeli::Piece* piece, selectedPieces) {
				if (piece->representedAtomicPieces().count()
					> 6) {
					int ans = 0;
					ans = KMessageBox::questionYesNo (
						m_mainWindow,
						i18n("You have selected to "
						"transfer a large piece "
						"containing more than six "
						"small pieces to a holder. Do "
						"you really wish to do that?"));
					if (ans == KMessageBox::No) {
						return;
					}
				}
			}
574
575
576
577
			transferPieces(selectedPieces, view, m_currentHolder);
		}
		else {
			selectedPieces = getSelectedPieces(m_currentHolder);
Laurent Montel's avatar
Laurent Montel committed
578
			qCDebug(PALAPELI_LOG) << "Transfer from holder" << selectedPieces.count() << m_currentHolder->name();
579
			// Transfer from a piece-holder to the puzzle table.
580
                        if (!selectedPieces.isEmpty()) {
581
582
583
584
585
586
587
588
589
590
591
				transferPieces(selectedPieces, m_currentHolder,
						view, scenePos);
			}
			else {
				KMessageBox::information(m_mainWindow,
					i18n("You need to select one or more "
					     "pieces to be transferred out of "
					     "the selected holder or select "
					     "pieces from the puzzle table "
					     "to be transferred into it."));
			}
592
593
		}
	}
594
595
596
597
	else {
		if (m_previousHolder) {
			selectedPieces = getSelectedPieces(m_previousHolder);
			// Transfer from one piece-holder to another.
598
                        if (!selectedPieces.isEmpty()) {
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
				transferPieces(selectedPieces, m_previousHolder,
						view, scenePos);
			}
			else {
				KMessageBox::information(m_mainWindow,
					i18n("You need to select one or more "
					     "pieces to be transferred from "
					     "the previous holder into the "
					     "newly selected holder."));
			}
		}
		else {
			KMessageBox::information(m_mainWindow,
				i18n("You need to have at least two holders, "
				     "one of them selected and with selected "
				     "pieces inside it, before you can "
615
				     "transfer pieces to a second holder."));
616
		}
617
	}
618
	positionChanged(0);		// Save the transfer - a little later.
619
620
}

621
622
623
624
625
626
627
628
629
630
631
632
void Palapeli::GamePlay::handleNewPieceSelection(Palapeli::View* view)
{
	// De-select pieces on puzzle table, to prevent teleport bounce-back.
	Palapeli::View* m_puzzleTableView = m_puzzleTable->view();
	if (view != m_puzzleTableView) {	// Pieces selected in a holder.
		foreach (Palapeli::Piece* piece,
				getSelectedPieces(m_puzzleTableView)) {
			piece->setSelected(false);
		}
	}
}

Laurent Montel's avatar
Laurent Montel committed
633
void Palapeli::GamePlay::transferPieces(const QList<Palapeli::Piece*> &pieces,
634
635
636
					Palapeli::View* source,
					Palapeli::View* dest,
					const QPointF& scenePos)
637
{
Laurent Montel's avatar
Laurent Montel committed
638
	qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::transferPieces(): pieces" << pieces.count() << "SourceIsTable" << (source == m_puzzleTable->view()) << "DestIsTable" << (dest == m_puzzleTable->view()) << "scenePos" << scenePos;
639
640
641
642
643
644
645
646
647
648
649
	source->scene()->dispatchPieces(pieces);
	if ((source != m_puzzleTable->view()) &&	// If empty holder.
		(source->scene()->pieces().isEmpty())) {
		source->scene()->initializeGrid(QPointF(0.0, 0.0));
	}

	bool destIsPuzzleTable = (dest == m_puzzleTable->view());
	if (destIsPuzzleTable) {
		m_puzzleTableScene->initializeGrid(scenePos);
	}
	Palapeli::Scene* scene = dest->scene();
650
651
652
653
654
655
	foreach (Palapeli::Piece* piece, scene->pieces()) {
		// Clear all previous selections in the destination scene.
		if (piece->isSelected()) {
			piece->setSelected(false);
		}
	}
656
	foreach (Palapeli::Piece* piece, pieces) {
657
		// Leave the new arrivals selected, connected and in a grid.
658
659
660
661
662
663
664
		scene->addPieceToList(piece);
		scene->addItem(piece);
		scene->addToGrid(piece);
		piece->setSelected(true);
		connect(piece, SIGNAL(moved(bool)),
			scene, SLOT(pieceMoved(bool)));
	}
665
	source->scene()->update();
666
	scene->setSceneRect(scene->extPiecesBoundingRect());
667
668
	if (! destIsPuzzleTable) {
		dest->centerOn(pieces.last()->sceneBareBoundingRect().center());
669
	}
670
671
}

672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
void Palapeli::GamePlay::setPalapeliMode(bool playing)
{
	// Palapeli has three modes: playing, loading and managing a collection.
	// When playing, collection actions are disabled and playing actions are
	// enabled: vice versa when managing the collection. When loading a
	// puzzle, both sets of actions are disabled, because they cannot work
	// concurrently with loading (enPlaying and enCollection both false).

	const char* playingActions[] = {"view_collection", "game_restart",
					"view_preview", "move_create_holder",
					"move_delete_holder", "move_select_all",
					"move_rearrange", "view_zoom_in",
					"view_zoom_out", "END" };
	const char* collectionActions[] = {"game_new", "game_delete",
					"game_import", "game_export", "END" };
	bool enPlaying    = (! m_loadingPuzzle) && playing;
	bool enCollection = (! m_loadingPuzzle) && (! playing);

	for (uint i = 0; (strcmp (playingActions[i], "END") != 0); i++) {
		m_mainWindow->actionCollection()->
			action(playingActions[i])->setEnabled(enPlaying);
	}
	for (uint i = 0; (strcmp (collectionActions[i], "END") != 0); i++) {
		m_mainWindow->actionCollection()->
			action(collectionActions[i])->setEnabled(enCollection);
	}
	// The collection view may enable or disable Delete and Export actions,
	// depending on what puzzle, if any, is currently selected.
	if (enCollection) {
		m_mainWindow->actionCollection()->
			action("game_delete")->setEnabled(m_canDeletePuzzle);
		m_mainWindow->actionCollection()->
			action("game_export")->setEnabled(m_canExportPuzzle);
	}
	m_playing = playing;
}

709
710
QList<Palapeli::Piece*> Palapeli::GamePlay::getSelectedPieces(Palapeli::View* v)
{
Laurent Montel's avatar
Laurent Montel committed
711
	qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::getSelectedPieces(): PuzzleTable" << (v == m_puzzleTable->view());
712
713
714
715
716
717
718
719
720
	const QList<QGraphicsItem*> sel = v->scene()->selectedItems();
	QList<Palapeli::Piece*> pieces;
	foreach (QGraphicsItem* item, sel) {
		Palapeli::Piece* p = Palapeli::Piece::fromSelectedItem(item);
		if (p) {
			pieces << p;
		}
	}
	return pieces;
721
722
}

723
724
void Palapeli::GamePlay::configure()
{
725
726
	if (Palapeli::ConfigDialog().exec() == QDialog::Accepted) {
		if (m_playing) {
Laurent Montel's avatar
Laurent Montel committed
727
			qCDebug(PALAPELI_LOG) << "SAVING SETTINGS FOR THIS PUZZLE";
728
729
730
			updateSavedGame();	// Save current puzzle Settings.
		}
	}
731
732
733
734
}

//END action handlers

735
736
void Palapeli::GamePlay::loadPuzzle()
{
Laurent Montel's avatar
Laurent Montel committed
737
	qCDebug(PALAPELI_LOG) << "START loadPuzzle()";
738
	m_restoredGame = false;
739
740
741
	// Disable all collection and playing actions during loading.
	m_loadingPuzzle = true;
	setPalapeliMode(false);
742
743
744
	// Stop autosaving and progress-reporting and start the loading-widget.
	m_savegameTimer->stop(); // Just in case it is running ...
	emit reportProgress(0, 0);
745
	// Is there a saved game?
746
747
748
749
750
751
752
753
754
755
756
	const QString puzzleLoc(
				QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
									   saveGamePath() + saveGameFileName(m_puzzle->identifier())));

	if (!puzzleLoc.isEmpty())
	{
		KConfig savedConfig(puzzleLoc, KConfig::SimpleConfig);
		if (savedConfig.hasGroup(AppearanceSaveGroup)) {
			// Get settings for background, shadows, etc. in this puzzle.
			restorePuzzleSettings(&savedConfig);
		}
757
	}
758
759
	// Return to the event queue to start the loading-widget graphics ASAP.
	QTimer::singleShot(0, this, SLOT(loadPuzzleFile()));
Laurent Montel's avatar
Laurent Montel committed
760
	qCDebug(PALAPELI_LOG) << "END loadPuzzle()";
761
762
763
764
}

void Palapeli::GamePlay::loadPuzzleFile()
{
765
	// Clear all scenes, and delete any piece holders that exist.
Laurent Montel's avatar
Laurent Montel committed
766
	qCDebug(PALAPELI_LOG) << "Start clearing all scenes: elapsed" << t.elapsed();
767
768
	deletePuzzleViews();
	m_viewList << m_puzzleTable->view();	// Re-list the puzzle-table.
Laurent Montel's avatar
Laurent Montel committed
769
	qCDebug(PALAPELI_LOG) << "Finish clearing all scenes: elapsed" << t.elapsed();
770

Laurent Montel's avatar
Laurent Montel committed
771
	qCDebug(PALAPELI_LOG) << "Start loadPuzzleFile(): elapsed" << t.restart();
772
773
	// Begin loading the puzzle.
	// It is loaded asynchronously and processed one piece at a time.
774
775
	m_loadedPieces.clear();
	if (m_puzzle) {
776
777
		m_puzzle->get(Palapeli::PuzzleComponent::Contents);
		QTimer::singleShot(0, this, &Palapeli::GamePlay::loadNextPiece);
778
	}
Laurent Montel's avatar
Laurent Montel committed
779
	qCDebug(PALAPELI_LOG) << "Finish loadPuzzleFile(): time" << t.restart();
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
}

void Palapeli::GamePlay::loadNextPiece()
{
	if (!m_puzzle)
		return;
	const Palapeli::ContentsComponent* component =
			m_puzzle->component<Palapeli::ContentsComponent>();
	if (!component)
		return;
	// Add pieces, but only one at a time.
	// PuzzleContents structure is defined in src/file-io/puzzlestructs.h.
	// We iterate over contents.pieces: key = pieceID, value = QImage.
	const Palapeli::PuzzleContents contents = component->contents;
	QMap<int, QImage>::const_iterator iterPieces = contents.pieces.begin();
	const QMap<int, QImage>::const_iterator iterPiecesEnd =
						contents.pieces.end();
	for (int pieceID = iterPieces.key(); iterPieces != iterPiecesEnd;
						pieceID = (++iterPieces).key())
	{
		if (m_loadedPieces.contains(pieceID))
			continue;	// Already loaded.

		// Create a Palapeli::Piece from its image, offsets and ID.
804
		// This also adds bevels, if required.
805
806
807
808
		Palapeli::Piece* piece = new Palapeli::Piece(
			iterPieces.value(), contents.pieceOffsets[pieceID]);
		piece->addRepresentedAtomicPieces(QList<int>() << pieceID);
		piece->addAtomicSize(iterPieces.value().size());
Laurent Montel's avatar
Laurent Montel committed
809
		// IDW test. qCDebug(PALAPELI_LOG) << "PIECE" << pieceID
810
811
		//                << "offset" << contents.pieceOffsets[pieceID]
		//                << "size" << iterPieces.value().size();
812
		m_loadedPieces[pieceID] = piece;
813
		piece->completeVisuals();	// Add a shadow, if required.
814
815
816

		// Continue with next piece or next stage, after event loop run.
		if (contents.pieces.size() > m_loadedPieces.size())
817
			QTimer::singleShot(0, this, &Palapeli::GamePlay::loadNextPiece);
818
		else
819
			QTimer::singleShot(0, this, &Palapeli::GamePlay::loadPiecePositions);
820
821
822
823
824
825
		return;
	}
}

void Palapeli::GamePlay::loadPiecePositions()
{
Laurent Montel's avatar
Laurent Montel committed
826
	qCDebug(PALAPELI_LOG) << "Finish loadNextPiece() calls: time" << t.restart();
827
828
	if (!m_puzzle)
		return;
Laurent Montel's avatar
Laurent Montel committed
829
	qCDebug(PALAPELI_LOG) << "loadPiecePositions():";
830
831
832
833
	m_originalPieceCount = m_loadedPieces.count();
	const Palapeli::PuzzleContents contents = m_puzzle->component<Palapeli::ContentsComponent>()->contents;
	//add piece relations
	foreach (const DoubleIntPair& relation, contents.relations) {
834
835
836
837
		Palapeli::Piece* firstPiece =
				m_loadedPieces.value(relation.first, 0);
		Palapeli::Piece* secondPiece =
				m_loadedPieces.value(relation.second, 0);
838
839
840
841
842
		firstPiece->addLogicalNeighbors(QList<Palapeli::Piece*>()
				<< secondPiece);
		secondPiece->addLogicalNeighbors(QList<Palapeli::Piece*>()
				<< firstPiece);
	}
843
844
845
	calculatePieceAreaSize();
	m_puzzleTableScene->setPieceAreaSize(m_pieceAreaSize);

846
	// Is there a saved game?
847
848
849
850
	const QString puzzleLoc(
				QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
									   saveGamePath() + saveGameFileName(m_puzzle->identifier())));
	// empty -> file not found -> no saved game
851
852
853
	bool oldFormat = false;
	m_restoredGame = false;
	int nHolders = 0;
854
855
856
857
858
859
860
861
862
863
864
	KConfig savedConfig(puzzleLoc, KConfig::SimpleConfig);	// here because needed inside 'if (m_restoredGame)'
	if (!puzzleLoc.isEmpty()) {
		if (savedConfig.hasGroup(HeaderSaveGroup)) {
			KConfigGroup headerGroup(&savedConfig, HeaderSaveGroup);
			nHolders = headerGroup.readEntry("N_Holders", 0);
			m_restoredGame = true;
		}
		else if (savedConfig.hasGroup(FormerSaveGroup)) {
			m_restoredGame = true;
			oldFormat = true;
		}
865
	}
866
	if (m_restoredGame)
867
	{
868
		// IDW TODO - Enable piece-holder actions.
869

870
		// Read piece positions from the LocationSaveGroup.
871
872
873
874
875
		// The current positions of atomic pieces are listed. If
		// neighbouring pieces are joined, their position values are
		// identical and searchConnections(m_pieces) handles that by
		// calling on a MergeGroup object to join the pieces.

Laurent Montel's avatar
Laurent Montel committed
876
		qCDebug(PALAPELI_LOG) << "RESTORING SAVED PUZZLE.";
877
878
		KConfigGroup holderGroup   (&savedConfig, HolderSaveGroup);
		KConfigGroup locationGroup (&savedConfig, oldFormat ?
879
880
881
			FormerSaveGroup : LocationSaveGroup);

		// Re-create the saved piece-holders, if any.
882
		m_currentHolder = nullptr;
883
		for (int groupID = 1; groupID <= nHolders; groupID++) {
884
			KConfigGroup holder (&savedConfig,
885
886
					QString("Holder_%1").arg(groupID));
			// Re-create a piece-holder and add it to m_viewList.
Laurent Montel's avatar
Laurent Montel committed
887
			qCDebug(PALAPELI_LOG) << "RE-CREATE HOLDER"
888
889
890
891
892
893
				 << QString("Holder_%1").arg(groupID) << "name"
				 << holder.readEntry("Name", QString(""));
			createHolder(holder.readEntry("Name", QString("")),
				     holder.readEntry("Selected", false));
			// Restore the piece-holder's size and position.
			QRect r = holder.readEntry("Geometry", QRect());
Laurent Montel's avatar
Laurent Montel committed
894
			qCDebug(PALAPELI_LOG) << "GEOMETRY" << r;
895
896
897
898
899
900
901
902
			Palapeli::View* v = m_viewList.at(groupID);
			v->resize(r.size());
			int x = (r.left() < 0) ? 0 : r.left();
			int y = (r.top() < 0)  ? 0 : r.top();
			v->move(x, y);
		}

		// Move pieces to saved positions, in holders or puzzle table.
Laurent Montel's avatar
Laurent Montel committed
903
904
		qCDebug(PALAPELI_LOG) << "START POSITIONING PIECES";
		qCDebug(PALAPELI_LOG) << "Old format" << oldFormat << HolderSaveGroup << (oldFormat ? FormerSaveGroup : LocationSaveGroup);
905
906
907
908
909
		QMap<int, Palapeli::Piece*>::const_iterator i =
						m_loadedPieces.constBegin();
		const QMap<int, Palapeli::Piece*>::const_iterator end =
						m_loadedPieces.constEnd();
		for (int pieceID = i.key(); i != end; pieceID = (++i).key())
910
		{
911
			Palapeli::Piece* piece = i.value();
912
913
914
915
			const QString ID = QString::number(pieceID);
			const int group = oldFormat ? 0 :
					holderGroup.readEntry(ID, 0);
			const QPointF p = locationGroup.readEntry(ID, QPointF());
Laurent Montel's avatar
Laurent Montel committed
916
			// qCDebug(PALAPELI_LOG) << "Piece ID" << ID << "group" << group << "pos" << p;
917
			Palapeli::View* view = m_viewList.at(group);
Laurent Montel's avatar
Laurent Montel committed
918
			// qCDebug(PALAPELI_LOG) << "View" << (view != 0) << "Scene" << (view->scene() != 0);
919
			view->scene()->addPieceToList(piece);
Laurent Montel's avatar
Laurent Montel committed
920
			// qCDebug(PALAPELI_LOG) << "PIECE HAS BEEN ADDED TO SCENE's LIST";
921
			piece->setPos(p);
Laurent Montel's avatar
Laurent Montel committed
922
			// qCDebug(PALAPELI_LOG) << "PIECE HAS BEEN POSITIONED";
923
924
925
			// IDW TODO - Selecting/unselecting did not trigger a
			//            save. Needed to bring back a "dirty" flag.
			// IDW TODO - Same for all other saveable actions?
926
		}
Laurent Montel's avatar
Laurent Montel committed
927
		qCDebug(PALAPELI_LOG) << "FINISHED POSITIONING PIECES";
928
		// Each scene re-merges pieces, as required, with no animation.
929
930
931
		foreach (Palapeli::View* view, m_viewList) {
			view->scene()->mergeLoadedPieces();
		}
932
933
934
	}
	else
	{
935
		// Place pieces at nice positions.
Laurent Montel's avatar
Laurent Montel committed
936
		qCDebug(PALAPELI_LOG) << "GENERATING A NEW PUZZLE BY SHUFFLING.";
937
938
		// Step 1: determine maximum piece size.
		QSizeF pieceAreaSize = m_pieceAreaSize;
939
		m_sizeFactor = 1.0 + 0.05 * Settings::pieceSpacing();
Laurent Montel's avatar
Laurent Montel committed
940
		qCDebug(PALAPELI_LOG) << "PIECE SPACING FACTOR" << m_sizeFactor;
941
		pieceAreaSize *= m_sizeFactor;	// Allow more space for pieces.
942
943
944

		// Step 2: place pieces in a grid in random order.
		QList<Palapeli::Piece*> piecePool(m_loadedPieces.values());
945
946
947
948
949
950
951
		int nPieces = piecePool.count();
		Palapeli::ConfigDialog::SolutionSpace space =
			(nPieces < 20) ?  Palapeli::ConfigDialog::None :
				(Palapeli::ConfigDialog::SolutionSpace)
				Settings::solutionArea();

		// Find the size of the area required for the solution.
952
953
954
955
		QRectF r;
		foreach (Palapeli::Piece* piece, piecePool) {
			r |= piece->sceneBareBoundingRect();
		}
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
		int xResv = 0;
		int yResv = 0;
		if (space != Palapeli::ConfigDialog::None) {
			xResv = r.width()/pieceAreaSize.width() + 1.0;
			yResv = r.height()/pieceAreaSize.height() + 1.0;
		}

		// To get "a" pieces around the solution, both horizontally and
		// vertically, we need to solve for "a" in:
		//     (a+xResv) * (a+yResv) = piecePool.count() + xResv*yResv
		// or  a^2 + (xResv+yResv)*a - piecePool.count() = 0
		// Let q = qSqrt(((xResv+yResv)^2 + 4.piecePool.count())), then
		//     a = (-xResv-yResv +- q)/2, the solution of the quadratic.
		//
		// The positive root is a = (-xResv - yResv + q)/2. If there is
		// no solution area, xResv == yResv == 0 and the above equation
		// degenerates to "a" = sqrt(number of pieces), as in earlier
		// versions of Palapeli.

		qreal q  = qSqrt((xResv + yResv)*(xResv + yResv) + 4*nPieces);
		int a    = qRound((-xResv-yResv+q)/2.0);
		int xMax = xResv + a;

		// Set solution space for None or TopLeft: modify as required.
		int x1 = 0;
		int y1 = 0;
		if (space == Palapeli::ConfigDialog::TopRight) {
			x1 = a;
		}
		else if (space == Palapeli::ConfigDialog::Center) {
			x1 = a/2;
			y1 = a/2;
		}
		else if (space == Palapeli::ConfigDialog::BottomLeft) {
			y1 = a;
			// If the rows are uneven, push the partial row right.
			if ((nPieces + xResv*yResv) % xMax) {
				yResv++;
			}
		}
		else if (space == Palapeli::ConfigDialog::BottomRight) {
			x1 = a;
			y1 = a;
		}
		int x2 = x1 + xResv;
		int y2 = y1 + yResv;
Laurent Montel's avatar
Laurent Montel committed
1002
1003
		qCDebug(PALAPELI_LOG) << "Reserve:" << xResv << yResv << "position" << space;
		qCDebug(PALAPELI_LOG) << "Pieces" << piecePool.count() << "rect" << r
1004
			 << "pieceAreaSize" << pieceAreaSize;
Laurent Montel's avatar
Laurent Montel committed
1005
1006
		qCDebug(PALAPELI_LOG) << "q" << q << "a" << a << "a/2" << a/2;
		qCDebug(PALAPELI_LOG) << "xMax" << xMax << "x1 y1" << x1 << y1
1007
1008
					   << "x2 y2" << x2 << y2;

Laurent Montel's avatar
Laurent Montel committed
1009
                auto *generator = QRandomGenerator::global();
1010
1011
1012
1013
1014
1015
1016
1017
		for (int y = 0; !piecePool.isEmpty(); ++y) {
			for (int x = 0; x < xMax && !piecePool.isEmpty(); ++x) {
				if ((x >= x1) && (x < x2) &&
				    (y >= y1) && (y < y2)) {
					continue;	// This space reserved.
				}
				// Select a random piece.
				Palapeli::Piece* piece = piecePool.takeAt(
Laurent Montel's avatar
Laurent Montel committed
1018
                                                generator->bounded(piecePool.count()));
1019
				// Place it randomly in grid-cell (x, y).
1020
1021
1022
1023
				const QPointF p0(0.0, 0.0);
				piece->setPlace(p0, x, y, pieceAreaSize, true);
				// Add piece to the puzzle table list (only).
				m_puzzleTableScene->addPieceToList(piece);
1024
1025
			}
		}
1026
1027
1028
1029
1030
1031
1032
1033
		// Save the generated puzzle.
		//
		// If the user goes back to the collection, without making any
		// moves, and looks at another puzzle, the generated puzzle
		// should not be shuffled again when he/she reloads: only when
		// he/she hits Restart Puzzle or chooses to resart a previously
		// solved puzzle.
		updateSavedGame();
1034
	}
1035
1036
1037
1038
1039
	// Add constraint_handles+spacer to puzzle table and setSceneRect().
	QRectF s = m_puzzleTableScene->piecesBoundingRect();
	qreal handleWidth = qMin(s.width(), s.height())/100.0;
	m_puzzleTableScene->addMargin(handleWidth, 0.5*handleWidth);
	// Add all the pieces to the puzzle table and piece-holder scenes.
1040
1041
	foreach (Palapeli::View* view, m_viewList) {
		Palapeli::Scene* scene = view->scene();
1042
		scene->addPieceItemsToScene();
1043
1044
1045
1046
		if (scene != m_puzzleTableScene) {
			// Expand the piece-holder sceneRects.
			scene->setSceneRect(scene->extPiecesBoundingRect());
		}
1047
	}
Laurent Montel's avatar
Laurent Montel committed
1048
	qCDebug(PALAPELI_LOG) << "Finish loadPiecePositions(): time" << t.restart();
1049
1050
1051
1052
1053
	finishLoading();
}

void Palapeli::GamePlay::finishLoading()
{
Laurent Montel's avatar
Laurent Montel committed
1054
	// qCDebug(PALAPELI_LOG) << "finishLoading(): Starting";
1055
1056
	m_puzzle->dropComponent(Palapeli::PuzzleComponent::Contents);
	// Start each scene and view.
Laurent Montel's avatar
Laurent Montel committed
1057
	qCDebug(PALAPELI_LOG) << "COUNTING CURRENT PIECES";
1058
	m_currentPieceCount = 0;
1059
1060
	foreach (Palapeli::View* view, m_viewList) {
		Palapeli::Scene* scene = view->scene();
1061
1062
		m_currentPieceCount = m_currentPieceCount +
					scene->pieces().size();
Laurent Montel's avatar
Laurent Montel committed
1063
		qCDebug(PALAPELI_LOG) << "Counted" << scene->pieces().size();
1064
		if (view != m_puzzleTable->view()) {
1065
1066
			// Saved-and-restored holders start in close-up scale.
			view->setCloseUp(true);
1067
1068
		}
		else {
Laurent Montel's avatar
Laurent Montel committed
1069
			qCDebug(PALAPELI_LOG) << "Puzzle table" << scene->pieces().size();
1070
		}
1071
	}
1072
	// Initialize external progress display, hide loading widget, show view.
1073
	emit reportProgress(m_originalPieceCount, m_currentPieceCount);
1074
1075
	// Adjust zoom-levels, center the view, show autosave message if needed.
	m_puzzleTable->view()->puzzleStarted();
1076
1077
1078
	if (!m_restoredGame && (m_originalPieceCount >= LargePuzzle)) {
		// New puzzle and a large one: create a default PieceHolder.
		createHolder(i18nc("For holding pieces", "Hand"));
1079
1080
1081
1082
1083
1084
1085
1086
1087
		KMessageBox::information(m_mainWindow,
			i18nc("Hints for solving large puzzles",
			"You have just created a large puzzle: Palapeli has "
			"several features to help you solve it within the "
			"limited space on the desktop. They are described in "
			"detail in the Palapeli Handbook (on the Help menu). "
			"Here are just a few quick tips.\n\n"
			"Before beginning, it may be best not to use bevels or "
			"shadowing with large puzzles (see the Settings "
1088
1089
1090
			"dialog), because they make loading slower and "
			"highlighting harder to see when the pieces in the "
			"view are very small.\n\n"
1091
1092
1093
1094
			"The first feature is the puzzle Preview (a picture of "
			"the completed puzzle) and a toolbar button to turn it "
			"on or off. If you hover over it with the mouse, it "
			"magnifies parts of the picture, so the window size "
1095
			"you choose for the Preview can be quite small.\n\n"
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
			"Next, there are close-up and distant views of the "
			"puzzle table, which you can switch quickly by using "
			"a mouse button (default Middle-Click). In close-up "
			"view, use the empty space in the scroll bars to "
			"search through the puzzle pieces a 'page' at a time. "
			"You can adjust the two views by zooming in or out "
			"and your changes will be remembered.\n\n"
			"Then there is a space on the puzzle table reserved "
			"for building up the solution.\n\n"
			"Last but not least, there are small windows called "
			"'holders'. They are for sorting pieces into groups "
			"such as edges, sky or white house on left. You can "
			"have as many holders as you like and can give "
			"them names. You should already have one named "
			"'Hand', for carrying pieces from wherever you find "
			"them to the solution area.\n\n"
			"You use a special mouse click to transfer pieces into "
			"or out of a holder (default Shift Left-Click). First "
			"make sure the holder you want to use is active: it "
			"should have a blue outline. If not, click on it. To "
			"transfer pieces into the holder, select them on the "
			"puzzle table then do the special click to 'teleport' "
			"them into the holder. Or you can just do the special "
			"click on one piece at a time.\n\n"
1120
			"To transfer pieces out of a holder, make "
1121
1122
1123
1124
			"sure no pieces are selected on the puzzle table, go "
			"into the holder window and select some pieces, using "
			"normal Palapeli mouse operations, then go back to the "
			"puzzle table and do the special click on an empty "
1125
1126
			"space where you want the pieces to arrive. Transfer "
			"no more than a few pieces at a time, to avoid "
1127
1128
1129
1130
1131
			"collisions of pieces on the puzzle table.\n\n"
			"By the way, holders can do almost all the things the "
			"puzzle table and its window can do, including joining "
			"pieces to build up a part of the solution."),
			i18nc("Caption for hints", "Solving Large Puzzles"),
Laurent Montel's avatar
Laurent Montel committed
1132
			QStringLiteral("largepuzzle-introduction"));
1133
	}
1134
1135
1136
	// Check if puzzle has been completed.
	if (m_currentPieceCount == 1) {
		int result = KMessageBox::questionYesNo(m_mainWindow,
1137
			i18n("You have finished the puzzle. Do you want to restart it now?"));
1138
1139
1140
1141
1142
1143
		if (result == KMessageBox::Yes) {
			restartPuzzle();
			return;
		}
	}
	// Connect moves and merges of pieces to autosaving and progress-report.
1144
1145
1146
1147
1148
	foreach (Palapeli::View* view, m_viewList) {
		connect(view->scene(), SIGNAL(saveMove(int)),
			this, SLOT(positionChanged(int)));
		if (view != m_puzzleTable->view()) {
			connect(view,
Yuri Chornoivan's avatar
Yuri Chornoivan committed
1149
				SIGNAL(teleport(Piece*,QPointF,View*)),
1150
				this,
Yuri Chornoivan's avatar
Yuri Chornoivan committed
1151
				SLOT(teleport(Piece*,QPointF,View*)));
1152
1153
			connect(view, SIGNAL(newPieceSelectionSeen(View*)),
				this, SLOT(handleNewPieceSelection(View*)));
1154
1155
		}
	}
1156
1157
1158
	// Enable playing actions.
	m_loadingPuzzle = false;
	setPalapeliMode(true);
Laurent Montel's avatar
Laurent Montel committed
1159
	qCDebug(PALAPELI_LOG) << "finishLoading(): time" << t.restart();
1160
1161
}

1162
1163
1164
1165
1166
1167
1168
void Palapeli::GamePlay::calculatePieceAreaSize()
{
	m_pieceAreaSize = QSizeF(0.0, 0.0);
	foreach (Palapeli::Piece* piece, m_loadedPieces) {
		m_pieceAreaSize = m_pieceAreaSize.expandedTo
				(piece->sceneBareBoundingRect().size());
	}
Laurent Montel's avatar
Laurent Montel committed
1169
	qCDebug(PALAPELI_LOG) << "m_pieceAreaSize =" << m_pieceAreaSize;
1170
1171
}

1172
1173
1174
1175
1176
1177
void Palapeli::GamePlay::playVictoryAnimation()
{
	m_puzzleTableScene->setConstrained(true);
	QPropertyAnimation* animation = new QPropertyAnimation
					(m_puzzleTableScene, "sceneRect", this);
	animation->setStartValue(m_puzzleTableScene->sceneRect());
1178
	animation->setEndValue(m_puzzleTableScene->extPiecesBoundingRect());
1179
	animation->setDuration(1000);
1180
1181
	connect(animation, SIGNAL(finished()),
		this, SLOT(playVictoryAnimation2()));
1182
1183
1184
1185
1186
	animation->start(QAbstractAnimation::DeleteWhenStopped);
}

void Palapeli::GamePlay::playVictoryAnimation2()
{
1187
	m_puzzleTableScene->setSceneRect(m_puzzleTableScene->extPiecesBoundingRect());
1188
	QTimer::singleShot(100, this, SIGNAL(victoryAnimationFinished()));