File indexing completed on 2024-05-19 04:07:49

0001 /*
0002     SPDX-FileCopyrightText: 2009 Stefan Majewsky <majewsky@gmx.net>
0003     SPDX-FileCopyrightText: 2014 Ian Wadham <iandw.au@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "gameplay.h"
0009 #include "palapeli_debug.h"
0010 
0011 #include "../file-io/collection-view.h"
0012 #include "../window/puzzletablewidget.h"
0013 #include "../window/pieceholder.h"
0014 #include "puzzlepreview.h"
0015 
0016 #include "scene.h"
0017 #include "view.h"
0018 #include "piece.h"
0019 #include "texturehelper.h"
0020 #include "interactormanager.h"
0021 #include "../file-io/puzzle.h"
0022 #include "../file-io/components.h"
0023 #include "../file-io/collection.h"
0024 #include "../creator/puzzlecreator.h"
0025 
0026 #include "../config/configdialog.h"
0027 #include "settings.h"
0028 #include <QStackedWidget>
0029 #include <QPointer>
0030 #include <QPropertyAnimation>
0031 #include <QFutureWatcher>
0032 #include <QtMath>
0033 #include <QStandardPaths>
0034 #include <QInputDialog>
0035 #include <QFileDialog>
0036 #include <QRandomGenerator>
0037 
0038 #include <KActionCollection>
0039 #include <KLocalizedString>
0040 #include <KMessageBox>
0041 
0042 // Use this because comma in type is not possible in foreach macro.
0043 typedef QPair<int, int> DoubleIntPair;
0044 
0045 //TODO: move LoadingWidget into here (stack into m_centralWidget)
0046 
0047 const int Palapeli::GamePlay::LargePuzzle = 300;
0048 
0049 const QString HeaderSaveGroup     = QStringLiteral("-PalapeliSavedPuzzle");
0050 const QString HolderSaveGroup     = QStringLiteral("Holders");
0051 const QString LocationSaveGroup   = QStringLiteral("XYCo-ordinates");
0052 const QString FormerSaveGroup     = QStringLiteral("SaveGame");
0053 const QString AppearanceSaveGroup = QStringLiteral("Appearance");
0054 const QString PreviewSaveGroup    = QStringLiteral("PuzzlePreview");
0055 
0056 Palapeli::GamePlay::GamePlay(MainWindow* mainWindow)
0057     : QObject(mainWindow)
0058     , m_centralWidget(new QStackedWidget)
0059     , m_collectionView(new Palapeli::CollectionView)
0060     , m_puzzleTable(new Palapeli::PuzzleTableWidget)
0061     , m_puzzlePreview(nullptr)
0062     , m_mainWindow(mainWindow)
0063     , m_puzzle(nullptr)
0064     , m_pieceAreaSize(QSizeF(32.0, 32.0))   // Allow 1024 pixels initially.
0065     , m_savegameTimer(new QTimer(this))
0066     , m_currentHolder(nullptr)
0067     , m_previousHolder(nullptr)
0068     , m_loadingPuzzle(false)
0069     , m_restoredGame(false)
0070     , m_originalPieceCount(0)
0071     , m_currentPieceCount(0)
0072     , m_sizeFactor(1.3)
0073     , m_playing(false)
0074     , m_canDeletePuzzle(false)  // No puzzle selected at startup.
0075     , m_canExportPuzzle(false)
0076 {
0077     m_puzzleTableScene = m_puzzleTable->view()->scene();
0078     m_viewList << m_puzzleTable->view();
0079     m_savegameTimer->setInterval(500); //write savegame twice per second at most
0080     m_savegameTimer->setSingleShot(true);
0081     connect(m_savegameTimer, &QTimer::timeout, this, &GamePlay::updateSavedGame);
0082     connect(this, &GamePlay::reportProgress,
0083         m_puzzleTable, &PuzzleTableWidget::reportProgress);
0084     connect(this, &GamePlay::victoryAnimationFinished,
0085         m_puzzleTable->view(), &View::startVictoryAnimation);
0086     connect(m_puzzleTable->view(), &View::teleport,
0087         this, &GamePlay::teleport);
0088 }
0089 
0090 Palapeli::GamePlay::~GamePlay()
0091 {
0092     deletePuzzleViews();
0093     delete m_puzzlePreview;
0094 }
0095 
0096 void Palapeli::GamePlay::deletePuzzleViews()
0097 {
0098     qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::deletePuzzleViews() ...";
0099     while (! m_viewList.isEmpty()) {
0100         Palapeli::View* view = m_viewList.takeLast();
0101         Palapeli::Scene* scene = view->scene();
0102         qCDebug(PALAPELI_LOG) << "DISCONNECT SLOT(positionChanged(int))";
0103         disconnect(scene, &Scene::saveMove,
0104                this, &GamePlay::positionChanged);
0105         qCDebug(PALAPELI_LOG) << "scene->clearPieces();";
0106         view->interactorManager()->resetActiveTriggers();
0107         scene->clearPieces();
0108         qCDebug(PALAPELI_LOG) << "if (scene != m_puzzleTableScene) {";
0109         if (scene != m_puzzleTableScene) {
0110             qCDebug(PALAPELI_LOG) << "DELETING holder" << view->windowTitle();
0111             delete view;
0112         }
0113     }
0114     m_currentHolder = nullptr;
0115     m_previousHolder = nullptr;
0116 }
0117 
0118 void Palapeli::GamePlay::init()
0119 {
0120     // Set up the collection view.
0121     m_collectionView->setModel(Palapeli::Collection::instance(m_mainWindow));
0122     connect(m_collectionView, &CollectionView::playRequest, this, &GamePlay::playPuzzle);
0123 
0124     // Set up the puzzle table.
0125     m_puzzleTable->showStatusBar(Settings::showStatusBar());
0126 
0127     // Set up the central widget.
0128     m_centralWidget->addWidget(m_collectionView);
0129     m_centralWidget->addWidget(m_puzzleTable);
0130     m_centralWidget->setCurrentWidget(m_collectionView);
0131     m_mainWindow->setCentralWidget(m_centralWidget);
0132     // Get some current action states from the collection.
0133     m_canDeletePuzzle = m_mainWindow->actionCollection()->
0134                 action(QStringLiteral("game_delete"))->isEnabled();
0135     m_canExportPuzzle = m_mainWindow->actionCollection()->
0136                 action(QStringLiteral("game_export"))->isEnabled();
0137     // Enable collection actions and disable playing actions initially.
0138     setPalapeliMode(false);
0139 }
0140 
0141 void Palapeli::GamePlay::shutdown()
0142 {
0143     qCDebug(PALAPELI_LOG) << "ENTERED Palapeli::GamePlay::shutdown()";
0144     // Make sure the last change is saved.
0145     if (m_savegameTimer->isActive()) {
0146         m_savegameTimer->stop();
0147         updateSavedGame();
0148     }
0149     // Delete piece-holders cleanly: no closeEvents in PieceHolder objects
0150     // and no messages about holders not being empty.
0151     deletePuzzleViews();
0152 }
0153 
0154 //BEGIN action handlers
0155 
0156 void Palapeli::GamePlay::playPuzzle(Palapeli::Puzzle* puzzle)
0157 {
0158     t.start();  // IDW test. START the clock.
0159     // we need to load the preview every time, although when the puzzle
0160     // is already loaded because the preview is destroyed in actionGoCollection()
0161     QTimer::singleShot(0, this, &GamePlay::loadPreview);
0162 
0163     qCDebug(PALAPELI_LOG) << "START playPuzzle(): elapsed 0";
0164     // Get some current action states from the collection.
0165     m_canDeletePuzzle = m_mainWindow->actionCollection()->
0166                 action(QStringLiteral("game_delete"))->isEnabled();
0167     m_canExportPuzzle = m_mainWindow->actionCollection()->
0168                 action(QStringLiteral("game_export"))->isEnabled();
0169     m_centralWidget->setCurrentWidget(m_puzzleTable);
0170     m_puzzlePreview = new Palapeli::PuzzlePreview(m_mainWindow);
0171 
0172     if (m_loadingPuzzle || (!puzzle) || (m_puzzle == puzzle)) {
0173         if (m_puzzle == puzzle) {
0174             qCDebug(PALAPELI_LOG) << "RESUMING A PUZZLE.";
0175             // IDW TODO - Show piece-holders.
0176             // Check if puzzle has been completed.
0177             if (m_currentPieceCount == 1) {
0178                 int result = KMessageBox::questionTwoActions(
0179                     m_mainWindow,
0180                     i18n("You have finished the puzzle. Do you want to restart it now?"), {},
0181                     KGuiItem(i18nc("@action:button", "Restart"), QStringLiteral("view-refresh")),
0182                     KStandardGuiItem::cont());
0183                 if (result == KMessageBox::PrimaryAction) {
0184                     restartPuzzle();
0185                     return;
0186                 }
0187             }
0188             // True if same puzzle selected and not still loading.
0189             setPalapeliMode(! m_loadingPuzzle);
0190         }
0191         qCDebug(PALAPELI_LOG) << "NO LOAD: (m_puzzle == puzzle)"
0192              << (m_puzzle == puzzle);
0193         qCDebug(PALAPELI_LOG) << "m_loadingPuzzle" << m_loadingPuzzle
0194              << (puzzle ? "puzzle != 0" : "puzzle == 0");
0195         return;     // Already loaded, loading or failed to start.
0196     }
0197     m_puzzle = puzzle;
0198     qCDebug(PALAPELI_LOG) << "RESTART the clock: elapsed" << t.restart(); // IDW test.
0199     loadPuzzle();
0200     qCDebug(PALAPELI_LOG) << "Returned from loadPuzzle(): elapsed" << t.elapsed();
0201 
0202     // IDW TODO - There is no way to stop loading a puzzle and start loading
0203     //            another. The only option is to Quit or abort Palapeli.
0204 }
0205 
0206 void Palapeli::GamePlay::loadPreview()
0207 {
0208     // IDW TODO - This WAS delaying the showing of the LoadingWidget. Now
0209     //            it is preventing the balls from moving for a few seconds.
0210 
0211     // Get metadata from archive (tar), to be sure of getting image data.
0212     // The config/palapeli-collectionrc file lacks image metadata (because
0213     // Palapeli must load the collection-list quickly at startup time).
0214     const Palapeli::PuzzleComponent* as =
0215         m_puzzle->get(Palapeli::PuzzleComponent::ArchiveStorage);
0216     const Palapeli::PuzzleComponent* cmd = (as == nullptr) ? nullptr :
0217         as->cast(Palapeli::PuzzleComponent::Metadata);
0218     if (cmd) {
0219         // Load puzzle preview image from metadata.
0220         const Palapeli::PuzzleMetadata md =
0221             dynamic_cast<const Palapeli::MetadataComponent*>(cmd)->
0222             metadata;
0223         m_puzzlePreview->loadImageFrom(md);
0224         m_mainWindow->setCaption(md.name);  // Set main title.
0225         delete cmd;
0226     }
0227 
0228     m_puzzlePreview->setVisible(Settings::puzzlePreviewVisible());
0229     connect (m_puzzlePreview, &PuzzlePreview::closing,
0230          this, &GamePlay::actionTogglePreview); // Hide preview: do not delete.
0231     // sync with mainWindow
0232     m_mainWindow->actionCollection()->action(QStringLiteral("view_preview"))->
0233         setChecked(Settings::puzzlePreviewVisible());
0234 }
0235 
0236 void Palapeli::GamePlay::playPuzzleFile(const QString& path)
0237 {
0238     const QString id = Palapeli::Puzzle::fsIdentifier(path);
0239     playPuzzle(new Palapeli::Puzzle(new Palapeli::ArchiveStorageComponent,
0240                         path, id));
0241 }
0242 
0243 void Palapeli::GamePlay::actionGoCollection()
0244 {
0245     m_centralWidget->setCurrentWidget(m_collectionView);
0246     delete m_puzzlePreview;
0247     m_puzzlePreview = nullptr;
0248     m_mainWindow->setCaption(QString());
0249     // IDW TODO - Disable piece-holder actions.
0250     for (Palapeli::View* view : std::as_const(m_viewList)) {
0251         if (view != m_puzzleTable->view()) {
0252             view->hide();
0253         }
0254     }
0255     // Disable playing actions and enable collection actions.
0256     setPalapeliMode(false);
0257 }
0258 
0259 void Palapeli::GamePlay::actionTogglePreview()
0260 {
0261     // This action is OK during puzzle loading.
0262     if (m_puzzlePreview) {
0263         m_puzzlePreview->toggleVisible();
0264         m_mainWindow->actionCollection()->action(QStringLiteral("view_preview"))->
0265             setChecked(Settings::puzzlePreviewVisible());
0266         // remember state
0267         updateSavedGame();
0268     }
0269 }
0270 
0271 void Palapeli::GamePlay::actionCreate()
0272 {
0273     QPointer<Palapeli::PuzzleCreatorDialog> creatorDialog(new Palapeli::PuzzleCreatorDialog);
0274     if (creatorDialog->exec())
0275     {
0276         if (!creatorDialog)
0277             return;
0278         Palapeli::Puzzle* puzzle = creatorDialog->result();
0279         if (!puzzle) {
0280             delete creatorDialog;
0281             return;
0282         }
0283         Palapeli::Collection::instance()->importPuzzle(puzzle);
0284         playPuzzle(puzzle);
0285     }
0286     delete creatorDialog;
0287 }
0288 
0289 void Palapeli::GamePlay::actionDelete()
0290 {
0291     QModelIndexList indexes = m_collectionView->selectedIndexes();
0292     //ask user for confirmation
0293     QStringList puzzleNames;
0294     for (const QModelIndex& index : std::as_const(indexes))
0295         puzzleNames << index.data(Qt::DisplayRole).toString();
0296     const int result = KMessageBox::warningContinueCancelList(m_mainWindow, i18n("The following puzzles will be deleted. This action cannot be undone."), puzzleNames);
0297     if (result != KMessageBox::Continue)
0298         return;
0299     //do deletion
0300     Palapeli::Collection* coll = Palapeli::Collection::instance();
0301 
0302     // We cannot simply use a foreach here, because after deleting the first
0303     // puzzle, the rest of the indexes should no longer be used (model was
0304     // modified).  Ask again for the list of selected indexes after each
0305     // step instead.
0306     while (indexes.size() > 0)
0307     {
0308         coll->deletePuzzle(indexes.at(0));
0309         indexes = m_collectionView->selectedIndexes();
0310     }
0311 }
0312 
0313 void Palapeli::GamePlay::actionImport()
0314 {
0315     const QString filter = i18nc("Filter for a file dialog", "Palapeli puzzles (*.puzzle)");
0316     const QStringList paths = QFileDialog::getOpenFileNames(m_mainWindow,
0317                                                             i18nc("@title:window", "Import Palapeli Puzzles"),
0318                                                             QString(),
0319                                                             filter);
0320     Palapeli::Collection* coll = Palapeli::Collection::instance();
0321     for (const QString& path : paths)
0322         coll->importPuzzle(path);
0323 }
0324 
0325 void Palapeli::GamePlay::actionExport()
0326 {
0327     const QModelIndexList indexes = m_collectionView->selectedIndexes();
0328     Palapeli::Collection* coll = Palapeli::Collection::instance();
0329     for (const QModelIndex& index : indexes)
0330     {
0331         Palapeli::Puzzle* puzzle = coll->puzzleFromIndex(index);
0332         if (!puzzle)
0333             continue;
0334         //get puzzle name (as an initial guess for the file name)
0335         puzzle->get(Palapeli::PuzzleComponent::Metadata);
0336         const Palapeli::MetadataComponent* cmp = puzzle->component<Palapeli::MetadataComponent>();
0337         if (!cmp)
0338             continue;
0339         //ask user for target file name
0340         const QString startLoc = QString::fromLatin1("%1.puzzle").arg(cmp->metadata.name);
0341         const QString filter = i18nc("Filter for a file dialog", "Palapeli puzzles (*.puzzle)");
0342         const QString location = QFileDialog::getSaveFileName(m_mainWindow,
0343                                                               i18nc("@title:window", "Save Palapeli Puzzles"),
0344                                                               startLoc,
0345                                                               filter);
0346         if (location.isEmpty())
0347             continue; //process aborted by user
0348         //do export
0349         coll->exportPuzzle(index, location);
0350     }
0351 }
0352 
0353 void Palapeli::GamePlay::createHolder()
0354 {
0355     qCDebug(PALAPELI_LOG) << "GamePlay::createHolder() entered";
0356     bool OK;
0357     QString name = QInputDialog::getText(m_mainWindow,
0358         i18n("Create a piece holder"),
0359         i18n("Enter a short name (optional):"),
0360         QLineEdit::Normal, QString(), &OK);
0361     if (! OK) {
0362         return;     // If CANCELLED, do not create a piece holder.
0363     }
0364     createHolder(name);
0365     // Merges/moves in new holders add to the progress bar and are saved.
0366     Palapeli::View* view = m_viewList.last();
0367     view->setCloseUp(true); // New holders start in close-up scale.
0368     connect(view->scene(), &Scene::saveMove,
0369         this, &GamePlay::positionChanged);
0370     connect(view, &View::teleport,
0371         this, &GamePlay::teleport);
0372     connect(view, &View::newPieceSelectionSeen,
0373         this, &GamePlay::handleNewPieceSelection);
0374 }
0375 
0376 void Palapeli::GamePlay::createHolder(const QString& name, bool sel)
0377 {
0378     Palapeli::PieceHolder* h =
0379         new Palapeli::PieceHolder(m_mainWindow, m_pieceAreaSize, name);
0380     m_viewList << h;
0381     h->initializeZooming();         // Min. view 2x2 to 6x6 pieces.
0382     connect(h, &PieceHolder::selected,
0383         this, &GamePlay::changeSelectedHolder);
0384     connect (h, &PieceHolder::closing,
0385         this, &GamePlay::closeHolder);
0386     if (sel) {
0387         changeSelectedHolder(h);
0388     }
0389     else {
0390         h->setSelected(false);
0391     }
0392     m_puzzleTable->view()->setFocus(Qt::OtherFocusReason);
0393     m_puzzleTable->activateWindow();    // Return focus to main window.
0394     positionChanged(0);         // Save holder - a little later.
0395 }
0396 
0397 void Palapeli::GamePlay::deleteHolder()
0398 {
0399     qCDebug(PALAPELI_LOG) << "GamePlay::deleteHolder() entered";
0400     if (m_currentHolder) {
0401         closeHolder(m_currentHolder);
0402     }
0403     else {
0404         KMessageBox::information(m_mainWindow,
0405             i18n("You need to click on a piece holder to "
0406                  "select it before you can delete it, or "
0407                  "you can just click on its Close button."));
0408     }
0409 }
0410 
0411 void Palapeli::GamePlay::closeHolder(Palapeli::PieceHolder* h)
0412 {
0413     if (h->scene()->pieces().isEmpty()) {
0414         int count = m_viewList.count();
0415         m_viewList.removeOne(h);
0416         qCDebug(PALAPELI_LOG) << "m_viewList WAS" << count << "NOW" << m_viewList.count();
0417         m_currentHolder = nullptr;
0418         m_previousHolder = nullptr;
0419         h->deleteLater();
0420         positionChanged(0); // Save change - a little later.
0421     }
0422     else {
0423         KMessageBox::information(m_mainWindow,
0424             i18n("The selected piece holder must be empty "
0425                  "before you can delete it."));
0426     }
0427 }
0428 
0429 void Palapeli::GamePlay::selectAll()
0430 {
0431     qCDebug(PALAPELI_LOG) << "GamePlay::selectAll() entered";
0432     if (m_currentHolder) {
0433         const QList<Palapeli::Piece*> pieces =
0434                     m_currentHolder->scene()->pieces();
0435         if (! pieces.isEmpty()) {
0436             for (Palapeli::Piece* piece : pieces) {
0437                 piece->setSelected(true);
0438             }
0439             handleNewPieceSelection(m_currentHolder);
0440         }
0441         else {
0442             KMessageBox::information(m_mainWindow,
0443                 i18n("The selected piece holder must contain "
0444                      "some pieces for 'Select all' to use."));
0445         }
0446     }
0447     else {
0448         KMessageBox::information(m_mainWindow,
0449             i18n("You need to click on a piece holder to "
0450                  "select it before you can select all the "
0451                  "pieces in it."));
0452     }
0453 }
0454 
0455 void Palapeli::GamePlay::rearrangePieces()
0456 {
0457     qCDebug(PALAPELI_LOG) << "GamePlay::rearrangePieces() entered";
0458     QList<Palapeli::Piece*> selectedPieces;
0459     Palapeli::View* view = m_puzzleTable->view();
0460     selectedPieces = getSelectedPieces(view);
0461     if (selectedPieces.isEmpty()) {
0462         if (m_currentHolder) {
0463             view = m_currentHolder;
0464             selectedPieces = getSelectedPieces(view);
0465         }
0466     }
0467     if (selectedPieces.isEmpty()) {
0468         KMessageBox::information(m_mainWindow,
0469             i18n("To rearrange pieces, either the puzzle table "
0470                  "must have some selected pieces or there "
0471                  "must be a selected holder with some selected "
0472                  "pieces in it."));
0473         return;
0474     }
0475     QRectF bRect;
0476     for (Palapeli::Piece* piece : std::as_const(selectedPieces)) {
0477         bRect |= piece->sceneBareBoundingRect();
0478     }
0479     Palapeli::Scene* scene = view->scene();
0480     // If in a piece-holder and ALL pieces are selected, start at (0, 0).
0481     scene->initializeGrid(((view == m_currentHolder) &&
0482             (selectedPieces.count() == scene->pieces().count())) ?
0483                 QPointF(0.0, 0.0) : bRect.topLeft());
0484     for (Palapeli::Piece* piece : std::as_const(selectedPieces)) {
0485         scene->addToGrid(piece);
0486     }
0487     if (view == m_currentHolder) {
0488         // Adjust the piece-holder's scene to frame the pieces.
0489         scene->setSceneRect(scene->extPiecesBoundingRect());
0490     }
0491     positionChanged(0); // There is no attempt to merge pieces here.
0492 }
0493 
0494 void Palapeli::GamePlay::actionZoomIn()
0495 {
0496     // IDW TODO - Make ZoomIn work for whichever view is active.
0497     m_puzzleTable->view()->zoomIn();
0498 }
0499 
0500 void Palapeli::GamePlay::actionZoomOut()
0501 {
0502     // IDW TODO - Make ZoomOut work for whichever view is active.
0503     m_puzzleTable->view()->zoomOut();
0504 }
0505 
0506 void Palapeli::GamePlay::restartPuzzle()
0507 {
0508     if (!m_puzzle) {
0509         return; // If no puzzle was successfully loaded and started.
0510     }
0511     // Discard the *.save file.
0512     const QString puzzleLoc(
0513                 QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
0514                                        saveGamePath() + saveGameFileName(m_puzzle->identifier())));
0515     if (!puzzleLoc.isEmpty())
0516         QFile(puzzleLoc).remove();
0517 
0518     // Load the puzzle and re-shuffle the pieces.
0519     loadPuzzle();
0520 }
0521 
0522 void Palapeli::GamePlay::teleport(Palapeli::Piece* pieceUnderMouse,
0523                   const QPointF& scenePos, Palapeli::View* view)
0524 {
0525     qCDebug(PALAPELI_LOG) << "GamePlay::teleport: pieceUnder" << (pieceUnderMouse != nullptr)
0526          << "scPos" << scenePos
0527          << "PuzzleTable?" << (view == m_puzzleTable->view())
0528          << "CurrentHolder?" << (view == m_currentHolder);
0529     if (! m_currentHolder) {
0530         KMessageBox::information(m_mainWindow,
0531             i18n("You need to have a piece holder and click it to "
0532                  "select it before you can transfer pieces into or "
0533                  "out of it."));
0534         return;
0535     }
0536     bool puzzleTableClick = (view == m_puzzleTable->view());
0537     QList<Palapeli::Piece*> selectedPieces;
0538     if (puzzleTableClick) {
0539         if (pieceUnderMouse && (!pieceUnderMouse->isSelected())) {
0540             pieceUnderMouse->setSelected(true);
0541         }
0542         selectedPieces = getSelectedPieces(view);
0543                 if (!selectedPieces.isEmpty()) {
0544             // Transfer from the puzzle table to a piece-holder.
0545             for (Palapeli::Piece* piece : std::as_const(selectedPieces)) {
0546                 if (piece->representedAtomicPieces().count()
0547                     > 6) {
0548                     int ans = 0;
0549                     ans = KMessageBox::questionTwoActions (
0550                         m_mainWindow,
0551                         i18n("You have selected to "
0552                         "transfer a large piece "
0553                         "containing more than six "
0554                         "small pieces to a holder. Do "
0555                         "you really wish to do that?"), {},
0556                         KGuiItem(i18nc("@action:button", "Transfer"), QStringLiteral("dialog-ok")),
0557                         KStandardGuiItem::cancel());
0558                     if (ans == KMessageBox::SecondaryAction) {
0559                         return;
0560                     }
0561                 }
0562             }
0563             transferPieces(selectedPieces, view, m_currentHolder);
0564         }
0565         else {
0566             selectedPieces = getSelectedPieces(m_currentHolder);
0567             qCDebug(PALAPELI_LOG) << "Transfer from holder" << selectedPieces.count() << m_currentHolder->name();
0568             // Transfer from a piece-holder to the puzzle table.
0569                         if (!selectedPieces.isEmpty()) {
0570                 transferPieces(selectedPieces, m_currentHolder,
0571                         view, scenePos);
0572             }
0573             else {
0574                 KMessageBox::information(m_mainWindow,
0575                     i18n("You need to select one or more "
0576                          "pieces to be transferred out of "
0577                          "the selected holder or select "
0578                          "pieces from the puzzle table "
0579                          "to be transferred into it."));
0580             }
0581         }
0582     }
0583     else {
0584         if (m_previousHolder) {
0585             selectedPieces = getSelectedPieces(m_previousHolder);
0586             // Transfer from one piece-holder to another.
0587                         if (!selectedPieces.isEmpty()) {
0588                 transferPieces(selectedPieces, m_previousHolder,
0589                         view, scenePos);
0590             }
0591             else {
0592                 KMessageBox::information(m_mainWindow,
0593                     i18n("You need to select one or more "
0594                          "pieces to be transferred from "
0595                          "the previous holder into the "
0596                          "newly selected holder."));
0597             }
0598         }
0599         else {
0600             KMessageBox::information(m_mainWindow,
0601                 i18n("You need to have at least two holders, "
0602                      "one of them selected and with selected "
0603                      "pieces inside it, before you can "
0604                      "transfer pieces to a second holder."));
0605         }
0606     }
0607     positionChanged(0);     // Save the transfer - a little later.
0608 }
0609 
0610 void Palapeli::GamePlay::handleNewPieceSelection(Palapeli::View* view)
0611 {
0612     // De-select pieces on puzzle table, to prevent teleport bounce-back.
0613     Palapeli::View* m_puzzleTableView = m_puzzleTable->view();
0614     if (view != m_puzzleTableView) {    // Pieces selected in a holder.
0615         const auto selectedPieces = getSelectedPieces(m_puzzleTableView);
0616         for (Palapeli::Piece* piece : selectedPieces) {
0617             piece->setSelected(false);
0618         }
0619     }
0620 }
0621 
0622 void Palapeli::GamePlay::transferPieces(const QList<Palapeli::Piece*> &pieces,
0623                     Palapeli::View* source,
0624                     Palapeli::View* dest,
0625                     const QPointF& scenePos)
0626 {
0627     qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::transferPieces(): pieces" << pieces.count() << "SourceIsTable" << (source == m_puzzleTable->view()) << "DestIsTable" << (dest == m_puzzleTable->view()) << "scenePos" << scenePos;
0628     source->scene()->dispatchPieces(pieces);
0629     if ((source != m_puzzleTable->view()) &&    // If empty holder.
0630         (source->scene()->pieces().isEmpty())) {
0631         source->scene()->initializeGrid(QPointF(0.0, 0.0));
0632     }
0633 
0634     bool destIsPuzzleTable = (dest == m_puzzleTable->view());
0635     if (destIsPuzzleTable) {
0636         m_puzzleTableScene->initializeGrid(scenePos);
0637     }
0638     Palapeli::Scene* scene = dest->scene();
0639     const auto sccenePieces = scene->pieces();
0640     for (Palapeli::Piece* piece : sccenePieces) {
0641         // Clear all previous selections in the destination scene.
0642         if (piece->isSelected()) {
0643             piece->setSelected(false);
0644         }
0645     }
0646     for (Palapeli::Piece* piece : pieces) {
0647         // Leave the new arrivals selected, connected and in a grid.
0648         scene->addPieceToList(piece);
0649         scene->addItem(piece);
0650         scene->addToGrid(piece);
0651         piece->setSelected(true);
0652         connect(piece, &Piece::moved,
0653             scene, &Scene::pieceMoved);
0654     }
0655     source->scene()->update();
0656     scene->setSceneRect(scene->extPiecesBoundingRect());
0657     if (! destIsPuzzleTable) {
0658         dest->centerOn(pieces.last()->sceneBareBoundingRect().center());
0659     }
0660 }
0661 
0662 void Palapeli::GamePlay::setPalapeliMode(bool playing)
0663 {
0664     // Palapeli has three modes: playing, loading and managing a collection.
0665     // When playing, collection actions are disabled and playing actions are
0666     // enabled: vice versa when managing the collection. When loading a
0667     // puzzle, both sets of actions are disabled, because they cannot work
0668     // concurrently with loading (enPlaying and enCollection both false).
0669 
0670     const QString playingActions[] = {
0671             QStringLiteral("view_collection"),
0672             QStringLiteral("game_restart"),
0673             QStringLiteral("view_preview"),
0674             QStringLiteral("move_create_holder"),
0675             QStringLiteral("move_delete_holder"),
0676             QStringLiteral("move_select_all"),
0677             QStringLiteral("move_rearrange"),
0678             QStringLiteral("view_zoom_in"),
0679             QStringLiteral("view_zoom_out"),
0680         };
0681     const QString collectionActions[] = {
0682             QStringLiteral("game_new"),
0683             QStringLiteral("game_delete"),
0684             QStringLiteral("game_import"),
0685             QStringLiteral("game_export"),
0686         };
0687     bool enPlaying    = (! m_loadingPuzzle) && playing;
0688     bool enCollection = (! m_loadingPuzzle) && (! playing);
0689 
0690     for (const auto &actionId : playingActions) {
0691         m_mainWindow->actionCollection()->
0692             action(actionId)->setEnabled(enPlaying);
0693     }
0694     for (const auto &actionId : collectionActions) {
0695         m_mainWindow->actionCollection()->
0696             action(actionId)->setEnabled(enCollection);
0697     }
0698     // The collection view may enable or disable Delete and Export actions,
0699     // depending on what puzzle, if any, is currently selected.
0700     if (enCollection) {
0701         m_mainWindow->actionCollection()->
0702             action(QStringLiteral("game_delete"))->setEnabled(m_canDeletePuzzle);
0703         m_mainWindow->actionCollection()->
0704             action(QStringLiteral("game_export"))->setEnabled(m_canExportPuzzle);
0705     }
0706     m_playing = playing;
0707 }
0708 
0709 QList<Palapeli::Piece*> Palapeli::GamePlay::getSelectedPieces(Palapeli::View* v)
0710 {
0711     qCDebug(PALAPELI_LOG) << "ENTERED GamePlay::getSelectedPieces(): PuzzleTable" << (v == m_puzzleTable->view());
0712     const QList<QGraphicsItem*> sel = v->scene()->selectedItems();
0713     QList<Palapeli::Piece*> pieces;
0714     for (QGraphicsItem* item : sel) {
0715         Palapeli::Piece* p = Palapeli::Piece::fromSelectedItem(item);
0716         if (p) {
0717             pieces << p;
0718         }
0719     }
0720     return pieces;
0721 }
0722 
0723 void Palapeli::GamePlay::configure()
0724 {
0725     if (Palapeli::ConfigDialog().exec() == QDialog::Accepted) {
0726         if (m_playing) {
0727             qCDebug(PALAPELI_LOG) << "SAVING SETTINGS FOR THIS PUZZLE";
0728             updateSavedGame();  // Save current puzzle Settings.
0729         }
0730     }
0731 }
0732 
0733 //END action handlers
0734 
0735 void Palapeli::GamePlay::loadPuzzle()
0736 {
0737     qCDebug(PALAPELI_LOG) << "START loadPuzzle()";
0738     m_restoredGame = false;
0739     // Disable all collection and playing actions during loading.
0740     m_loadingPuzzle = true;
0741     setPalapeliMode(false);
0742     // Stop autosaving and progress-reporting and start the loading-widget.
0743     m_savegameTimer->stop(); // Just in case it is running ...
0744     Q_EMIT reportProgress(0, 0);
0745     // Is there a saved game?
0746     const QString puzzleLoc(
0747                 QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
0748                                        saveGamePath() + saveGameFileName(m_puzzle->identifier())));
0749 
0750     if (!puzzleLoc.isEmpty())
0751     {
0752         KConfig savedConfig(puzzleLoc, KConfig::SimpleConfig);
0753         if (savedConfig.hasGroup(AppearanceSaveGroup)) {
0754             // Get settings for background, shadows, etc. in this puzzle.
0755             restorePuzzleSettings(&savedConfig);
0756         }
0757     }
0758     // Return to the event queue to start the loading-widget graphics ASAP.
0759     QTimer::singleShot(0, this, &GamePlay::loadPuzzleFile);
0760     qCDebug(PALAPELI_LOG) << "END loadPuzzle()";
0761 }
0762 
0763 void Palapeli::GamePlay::loadPuzzleFile()
0764 {
0765     // Clear all scenes, and delete any piece holders that exist.
0766     qCDebug(PALAPELI_LOG) << "Start clearing all scenes: elapsed" << t.elapsed();
0767     deletePuzzleViews();
0768     m_viewList << m_puzzleTable->view();    // Re-list the puzzle-table.
0769     qCDebug(PALAPELI_LOG) << "Finish clearing all scenes: elapsed" << t.elapsed();
0770 
0771     qCDebug(PALAPELI_LOG) << "Start loadPuzzleFile(): elapsed" << t.restart();
0772     // Begin loading the puzzle.
0773     // It is loaded asynchronously and processed one piece at a time.
0774     m_loadedPieces.clear();
0775     if (m_puzzle) {
0776         m_puzzle->get(Palapeli::PuzzleComponent::Contents);
0777         QTimer::singleShot(0, this, &Palapeli::GamePlay::loadNextPiece);
0778     }
0779     qCDebug(PALAPELI_LOG) << "Finish loadPuzzleFile(): time" << t.restart();
0780 }
0781 
0782 void Palapeli::GamePlay::loadNextPiece()
0783 {
0784     if (!m_puzzle)
0785         return;
0786     const Palapeli::ContentsComponent* component =
0787             m_puzzle->component<Palapeli::ContentsComponent>();
0788     if (!component)
0789         return;
0790     // Add pieces, but only one at a time.
0791     // PuzzleContents structure is defined in src/file-io/puzzlestructs.h.
0792     // We iterate over contents.pieces: key = pieceID, value = QImage.
0793     const Palapeli::PuzzleContents contents = component->contents;
0794     QMap<int, QImage>::const_iterator iterPieces = contents.pieces.begin();
0795     const QMap<int, QImage>::const_iterator iterPiecesEnd =
0796                         contents.pieces.end();
0797     for (int pieceID = iterPieces.key(); iterPieces != iterPiecesEnd;
0798                         pieceID = (++iterPieces).key())
0799     {
0800         if (m_loadedPieces.contains(pieceID))
0801             continue;   // Already loaded.
0802 
0803         // Create a Palapeli::Piece from its image, offsets and ID.
0804         // This also adds bevels, if required.
0805         Palapeli::Piece* piece = new Palapeli::Piece(
0806             iterPieces.value(), contents.pieceOffsets[pieceID]);
0807         piece->addRepresentedAtomicPieces(QList<int>() << pieceID);
0808         piece->addAtomicSize(iterPieces.value().size());
0809         // IDW test. qCDebug(PALAPELI_LOG) << "PIECE" << pieceID
0810         //                << "offset" << contents.pieceOffsets[pieceID]
0811         //                << "size" << iterPieces.value().size();
0812         m_loadedPieces[pieceID] = piece;
0813         piece->completeVisuals();   // Add a shadow, if required.
0814 
0815         // Continue with next piece or next stage, after event loop run.
0816         if (contents.pieces.size() > m_loadedPieces.size())
0817             QTimer::singleShot(0, this, &Palapeli::GamePlay::loadNextPiece);
0818         else
0819             QTimer::singleShot(0, this, &Palapeli::GamePlay::loadPiecePositions);
0820         return;
0821     }
0822 }
0823 
0824 void Palapeli::GamePlay::loadPiecePositions()
0825 {
0826     qCDebug(PALAPELI_LOG) << "Finish loadNextPiece() calls: time" << t.restart();
0827     if (!m_puzzle)
0828         return;
0829     qCDebug(PALAPELI_LOG) << "loadPiecePositions():";
0830     m_originalPieceCount = m_loadedPieces.count();
0831     const Palapeli::PuzzleContents contents = m_puzzle->component<Palapeli::ContentsComponent>()->contents;
0832     //add piece relations
0833     for (const DoubleIntPair& relation : contents.relations) {
0834         Palapeli::Piece* firstPiece =
0835                 m_loadedPieces.value(relation.first, nullptr);
0836         Palapeli::Piece* secondPiece =
0837                 m_loadedPieces.value(relation.second, nullptr);
0838         firstPiece->addLogicalNeighbors(QList<Palapeli::Piece*>()
0839                 << secondPiece);
0840         secondPiece->addLogicalNeighbors(QList<Palapeli::Piece*>()
0841                 << firstPiece);
0842     }
0843     calculatePieceAreaSize();
0844     m_puzzleTableScene->setPieceAreaSize(m_pieceAreaSize);
0845 
0846     // Is there a saved game?
0847     const QString puzzleLoc(
0848                 QStandardPaths::locate(QStandardPaths::AppLocalDataLocation,
0849                                        saveGamePath() + saveGameFileName(m_puzzle->identifier())));
0850     // empty -> file not found -> no saved game
0851     bool oldFormat = false;
0852     m_restoredGame = false;
0853     int nHolders = 0;
0854     KConfig savedConfig(puzzleLoc, KConfig::SimpleConfig);  // here because needed inside 'if (m_restoredGame)'
0855     if (!puzzleLoc.isEmpty()) {
0856         if (savedConfig.hasGroup(HeaderSaveGroup)) {
0857             KConfigGroup headerGroup(&savedConfig, HeaderSaveGroup);
0858             nHolders = headerGroup.readEntry("N_Holders", 0);
0859             m_restoredGame = true;
0860         }
0861         else if (savedConfig.hasGroup(FormerSaveGroup)) {
0862             m_restoredGame = true;
0863             oldFormat = true;
0864         }
0865     }
0866     if (m_restoredGame)
0867     {
0868         // IDW TODO - Enable piece-holder actions.
0869 
0870         // Read piece positions from the LocationSaveGroup.
0871         // The current positions of atomic pieces are listed. If
0872         // neighbouring pieces are joined, their position values are
0873         // identical and searchConnections(m_pieces) handles that by
0874         // calling on a MergeGroup object to join the pieces.
0875 
0876         qCDebug(PALAPELI_LOG) << "RESTORING SAVED PUZZLE.";
0877         KConfigGroup holderGroup   (&savedConfig, HolderSaveGroup);
0878         KConfigGroup locationGroup (&savedConfig, oldFormat ?
0879             FormerSaveGroup : LocationSaveGroup);
0880 
0881         // Re-create the saved piece-holders, if any.
0882         m_currentHolder = nullptr;
0883         for (int groupID = 1; groupID <= nHolders; groupID++) {
0884             KConfigGroup holder (&savedConfig,
0885                     QStringLiteral("Holder_%1").arg(groupID));
0886             // Re-create a piece-holder and add it to m_viewList.
0887             qCDebug(PALAPELI_LOG) << "RE-CREATE HOLDER"
0888                  << QStringLiteral("Holder_%1").arg(groupID) << "name"
0889                  << holder.readEntry("Name", QString());
0890             createHolder(holder.readEntry("Name", QString()),
0891                      holder.readEntry("Selected", false));
0892             // Restore the piece-holder's size and position.
0893             QRect r = holder.readEntry("Geometry", QRect());
0894             qCDebug(PALAPELI_LOG) << "GEOMETRY" << r;
0895             Palapeli::View* v = m_viewList.at(groupID);
0896             v->resize(r.size());
0897             int x = (r.left() < 0) ? 0 : r.left();
0898             int y = (r.top() < 0)  ? 0 : r.top();
0899             v->move(x, y);
0900         }
0901 
0902         // Move pieces to saved positions, in holders or puzzle table.
0903         qCDebug(PALAPELI_LOG) << "START POSITIONING PIECES";
0904         qCDebug(PALAPELI_LOG) << "Old format" << oldFormat << HolderSaveGroup << (oldFormat ? FormerSaveGroup : LocationSaveGroup);
0905         QMap<int, Palapeli::Piece*>::const_iterator i =
0906                         m_loadedPieces.constBegin();
0907         const QMap<int, Palapeli::Piece*>::const_iterator end =
0908                         m_loadedPieces.constEnd();
0909         for (int pieceID = i.key(); i != end; pieceID = (++i).key())
0910         {
0911             Palapeli::Piece* piece = i.value();
0912             const QString ID = QString::number(pieceID);
0913             const int group = oldFormat ? 0 :
0914                     holderGroup.readEntry(ID, 0);
0915             const QPointF p = locationGroup.readEntry(ID, QPointF());
0916             // qCDebug(PALAPELI_LOG) << "Piece ID" << ID << "group" << group << "pos" << p;
0917             Palapeli::View* view = m_viewList.at(group);
0918             // qCDebug(PALAPELI_LOG) << "View" << (view != 0) << "Scene" << (view->scene() != 0);
0919             view->scene()->addPieceToList(piece);
0920             // qCDebug(PALAPELI_LOG) << "PIECE HAS BEEN ADDED TO SCENE's LIST";
0921             piece->setPos(p);
0922             // qCDebug(PALAPELI_LOG) << "PIECE HAS BEEN POSITIONED";
0923             // IDW TODO - Selecting/unselecting did not trigger a
0924             //            save. Needed to bring back a "dirty" flag.
0925             // IDW TODO - Same for all other saveable actions?
0926         }
0927         qCDebug(PALAPELI_LOG) << "FINISHED POSITIONING PIECES";
0928         // Each scene re-merges pieces, as required, with no animation.
0929         for (Palapeli::View* view : std::as_const(m_viewList)) {
0930             view->scene()->mergeLoadedPieces();
0931         }
0932     }
0933     else
0934     {
0935         // Place pieces at nice positions.
0936         qCDebug(PALAPELI_LOG) << "GENERATING A NEW PUZZLE BY SHUFFLING.";
0937         // Step 1: determine maximum piece size.
0938         QSizeF pieceAreaSize = m_pieceAreaSize;
0939         m_sizeFactor = 1.0 + 0.05 * Settings::pieceSpacing();
0940         qCDebug(PALAPELI_LOG) << "PIECE SPACING FACTOR" << m_sizeFactor;
0941         pieceAreaSize *= m_sizeFactor;  // Allow more space for pieces.
0942 
0943         // Step 2: place pieces in a grid in random order.
0944         QList<Palapeli::Piece*> piecePool(m_loadedPieces.values());
0945         int nPieces = piecePool.count();
0946         Palapeli::ConfigDialog::SolutionSpace space =
0947             (nPieces < 20) ?  Palapeli::ConfigDialog::None :
0948                 (Palapeli::ConfigDialog::SolutionSpace)
0949                 Settings::solutionArea();
0950 
0951         // Find the size of the area required for the solution.
0952         QRectF r;
0953         for (Palapeli::Piece* piece : std::as_const(piecePool)) {
0954             r |= piece->sceneBareBoundingRect();
0955         }
0956         int xResv = 0;
0957         int yResv = 0;
0958         if (space != Palapeli::ConfigDialog::None) {
0959             xResv = r.width()/pieceAreaSize.width() + 1.0;
0960             yResv = r.height()/pieceAreaSize.height() + 1.0;
0961         }
0962 
0963         // To get "a" pieces around the solution, both horizontally and
0964         // vertically, we need to solve for "a" in:
0965         //     (a+xResv) * (a+yResv) = piecePool.count() + xResv*yResv
0966         // or  a^2 + (xResv+yResv)*a - piecePool.count() = 0
0967         // Let q = qSqrt(((xResv+yResv)^2 + 4.piecePool.count())), then
0968         //     a = (-xResv-yResv +- q)/2, the solution of the quadratic.
0969         //
0970         // The positive root is a = (-xResv - yResv + q)/2. If there is
0971         // no solution area, xResv == yResv == 0 and the above equation
0972         // degenerates to "a" = sqrt(number of pieces), as in earlier
0973         // versions of Palapeli.
0974 
0975         qreal q  = qSqrt((xResv + yResv)*(xResv + yResv) + 4*nPieces);
0976         int a    = qRound((-xResv-yResv+q)/2.0);
0977         int xMax = xResv + a;
0978 
0979         // Set solution space for None or TopLeft: modify as required.
0980         int x1 = 0;
0981         int y1 = 0;
0982         if (space == Palapeli::ConfigDialog::TopRight) {
0983             x1 = a;
0984         }
0985         else if (space == Palapeli::ConfigDialog::Center) {
0986             x1 = a/2;
0987             y1 = a/2;
0988         }
0989         else if (space == Palapeli::ConfigDialog::BottomLeft) {
0990             y1 = a;
0991             // If the rows are uneven, push the partial row right.
0992             if ((nPieces + xResv*yResv) % xMax) {
0993                 yResv++;
0994             }
0995         }
0996         else if (space == Palapeli::ConfigDialog::BottomRight) {
0997             x1 = a;
0998             y1 = a;
0999         }
1000         int x2 = x1 + xResv;
1001         int y2 = y1 + yResv;
1002         qCDebug(PALAPELI_LOG) << "Reserve:" << xResv << yResv << "position" << space;
1003         qCDebug(PALAPELI_LOG) << "Pieces" << piecePool.count() << "rect" << r
1004              << "pieceAreaSize" << pieceAreaSize;
1005         qCDebug(PALAPELI_LOG) << "q" << q << "a" << a << "a/2" << a/2;
1006         qCDebug(PALAPELI_LOG) << "xMax" << xMax << "x1 y1" << x1 << y1
1007                        << "x2 y2" << x2 << y2;
1008 
1009                 auto *generator = QRandomGenerator::global();
1010         for (int y = 0; !piecePool.isEmpty(); ++y) {
1011             for (int x = 0; x < xMax && !piecePool.isEmpty(); ++x) {
1012                 if ((x >= x1) && (x < x2) &&
1013                     (y >= y1) && (y < y2)) {
1014                     continue;   // This space reserved.
1015                 }
1016                 // Select a random piece.
1017                 Palapeli::Piece* piece = piecePool.takeAt(
1018                                                 generator->bounded(piecePool.count()));
1019                 // Place it randomly in grid-cell (x, y).
1020                 const QPointF p0(0.0, 0.0);
1021                 piece->setPlace(p0, x, y, pieceAreaSize, true);
1022                 // Add piece to the puzzle table list (only).
1023                 m_puzzleTableScene->addPieceToList(piece);
1024             }
1025         }
1026         // Save the generated puzzle.
1027         //
1028         // If the user goes back to the collection, without making any
1029         // moves, and looks at another puzzle, the generated puzzle
1030         // should not be shuffled again when he/she reloads: only when
1031         // he/she hits Restart Puzzle or chooses to resart a previously
1032         // solved puzzle.
1033         updateSavedGame();
1034     }
1035     // Add constraint_handles+spacer to puzzle table and setSceneRect().
1036     QRectF s = m_puzzleTableScene->piecesBoundingRect();
1037     qreal handleWidth = qMin(s.width(), s.height())/100.0;
1038     m_puzzleTableScene->addMargin(handleWidth, 0.5*handleWidth);
1039     // Add all the pieces to the puzzle table and piece-holder scenes.
1040     for (Palapeli::View* view : std::as_const(m_viewList)) {
1041         Palapeli::Scene* scene = view->scene();
1042         scene->addPieceItemsToScene();
1043         if (scene != m_puzzleTableScene) {
1044             // Expand the piece-holder sceneRects.
1045             scene->setSceneRect(scene->extPiecesBoundingRect());
1046         }
1047     }
1048     qCDebug(PALAPELI_LOG) << "Finish loadPiecePositions(): time" << t.restart();
1049     finishLoading();
1050 }
1051 
1052 void Palapeli::GamePlay::finishLoading()
1053 {
1054     // qCDebug(PALAPELI_LOG) << "finishLoading(): Starting";
1055     m_puzzle->dropComponent(Palapeli::PuzzleComponent::Contents);
1056     // Start each scene and view.
1057     qCDebug(PALAPELI_LOG) << "COUNTING CURRENT PIECES";
1058     m_currentPieceCount = 0;
1059     for (Palapeli::View* view : std::as_const(m_viewList)) {
1060         Palapeli::Scene* scene = view->scene();
1061         m_currentPieceCount = m_currentPieceCount +
1062                     scene->pieces().size();
1063         qCDebug(PALAPELI_LOG) << "Counted" << scene->pieces().size();
1064         if (view != m_puzzleTable->view()) {
1065             // Saved-and-restored holders start in close-up scale.
1066             view->setCloseUp(true);
1067         }
1068         else {
1069             qCDebug(PALAPELI_LOG) << "Puzzle table" << scene->pieces().size();
1070         }
1071     }
1072     // Initialize external progress display, hide loading widget, show view.
1073     Q_EMIT reportProgress(m_originalPieceCount, m_currentPieceCount);
1074     // Adjust zoom-levels, center the view, show autosave message if needed.
1075     m_puzzleTable->view()->puzzleStarted();
1076     if (!m_restoredGame && (m_originalPieceCount >= LargePuzzle)) {
1077         // New puzzle and a large one: create a default PieceHolder.
1078         createHolder(i18nc("For holding pieces", "Hand"));
1079         KMessageBox::information(m_mainWindow,
1080             i18nc("Hints for solving large puzzles",
1081             "You have just created a large puzzle: Palapeli has "
1082             "several features to help you solve it within the "
1083             "limited space on the desktop. They are described in "
1084             "detail in the Palapeli Handbook (on the Help menu). "
1085             "Here are just a few quick tips.\n\n"
1086             "Before beginning, it may be best not to use bevels or "
1087             "shadowing with large puzzles (see the Settings "
1088             "dialog), because they make loading slower and "
1089             "highlighting harder to see when the pieces in the "
1090             "view are very small.\n\n"
1091             "The first feature is the puzzle Preview (a picture of "
1092             "the completed puzzle) and a toolbar button to turn it "
1093             "on or off. If you hover over it with the mouse, it "
1094             "magnifies parts of the picture, so the window size "
1095             "you choose for the Preview can be quite small.\n\n"
1096             "Next, there are close-up and distant views of the "
1097             "puzzle table, which you can switch quickly by using "
1098             "a mouse button (default Middle-Click). In close-up "
1099             "view, use the empty space in the scroll bars to "
1100             "search through the puzzle pieces a 'page' at a time. "
1101             "You can adjust the two views by zooming in or out "
1102             "and your changes will be remembered.\n\n"
1103             "Then there is a space on the puzzle table reserved "
1104             "for building up the solution.\n\n"
1105             "Last but not least, there are small windows called "
1106             "'holders'. They are for sorting pieces into groups "
1107             "such as edges, sky or white house on left. You can "
1108             "have as many holders as you like and can give "
1109             "them names. You should already have one named "
1110             "'Hand', for carrying pieces from wherever you find "
1111             "them to the solution area.\n\n"
1112             "You use a special mouse click to transfer pieces into "
1113             "or out of a holder (default Shift Left-Click). First "
1114             "make sure the holder you want to use is active: it "
1115             "should have a blue outline. If not, click on it. To "
1116             "transfer pieces into the holder, select them on the "
1117             "puzzle table then do the special click to 'teleport' "
1118             "them into the holder. Or you can just do the special "
1119             "click on one piece at a time.\n\n"
1120             "To transfer pieces out of a holder, make "
1121             "sure no pieces are selected on the puzzle table, go "
1122             "into the holder window and select some pieces, using "
1123             "normal Palapeli mouse operations, then go back to the "
1124             "puzzle table and do the special click on an empty "
1125             "space where you want the pieces to arrive. Transfer "
1126             "no more than a few pieces at a time, to avoid "
1127             "collisions of pieces on the puzzle table.\n\n"
1128             "By the way, holders can do almost all the things the "
1129             "puzzle table and its window can do, including joining "
1130             "pieces to build up a part of the solution."),
1131             i18nc("Caption for hints", "Solving Large Puzzles"),
1132             QStringLiteral("largepuzzle-introduction"));
1133     }
1134     // Check if puzzle has been completed.
1135     if (m_currentPieceCount == 1) {
1136         int result = KMessageBox::questionTwoActions(m_mainWindow,
1137             i18n("You have finished the puzzle. Do you want to restart it now?"), {},
1138             KGuiItem(i18nc("@action:button", "Restart"), QStringLiteral("view-refresh")),
1139             KStandardGuiItem::cont());
1140         if (result == KMessageBox::PrimaryAction) {
1141             restartPuzzle();
1142             return;
1143         }
1144     }
1145     // Connect moves and merges of pieces to autosaving and progress-report.
1146     for (Palapeli::View* view : std::as_const(m_viewList)) {
1147         connect(view->scene(), &Scene::saveMove,
1148             this, &GamePlay::positionChanged);
1149         if (view != m_puzzleTable->view()) {
1150             connect(view, &View::teleport,
1151                 this, &GamePlay::teleport);
1152             connect(view, &View::newPieceSelectionSeen,
1153                 this, &GamePlay::handleNewPieceSelection);
1154         }
1155     }
1156     // Enable playing actions.
1157     m_loadingPuzzle = false;
1158     setPalapeliMode(true);
1159     qCDebug(PALAPELI_LOG) << "finishLoading(): time" << t.restart();
1160 }
1161 
1162 void Palapeli::GamePlay::calculatePieceAreaSize()
1163 {
1164     m_pieceAreaSize = QSizeF(0.0, 0.0);
1165     for (Palapeli::Piece* piece : std::as_const(m_loadedPieces)) {
1166         m_pieceAreaSize = m_pieceAreaSize.expandedTo
1167                 (piece->sceneBareBoundingRect().size());
1168     }
1169     qCDebug(PALAPELI_LOG) << "m_pieceAreaSize =" << m_pieceAreaSize;
1170 }
1171 
1172 void Palapeli::GamePlay::playVictoryAnimation()
1173 {
1174     m_puzzleTableScene->setConstrained(true);
1175     QPropertyAnimation* animation = new QPropertyAnimation
1176                     (m_puzzleTableScene, "sceneRect", this);
1177     animation->setStartValue(m_puzzleTableScene->sceneRect());
1178     animation->setEndValue(m_puzzleTableScene->extPiecesBoundingRect());
1179     animation->setDuration(1000);
1180     connect(animation, &QAbstractAnimation::finished,
1181         this, &GamePlay::playVictoryAnimation2);
1182     animation->start(QAbstractAnimation::DeleteWhenStopped);
1183 }
1184 
1185 void Palapeli::GamePlay::playVictoryAnimation2()
1186 {
1187     m_puzzleTableScene->setSceneRect(m_puzzleTableScene->extPiecesBoundingRect());
1188     QTimer::singleShot(100, this, &GamePlay::victoryAnimationFinished);
1189     // Give the View some time to play its part of the victory animation.
1190     QTimer::singleShot(1500, this, &GamePlay::playVictoryAnimation3);
1191 }
1192 
1193 void Palapeli::GamePlay::playVictoryAnimation3()
1194 {
1195     KMessageBox::information(m_mainWindow, i18n("Great! You have finished the puzzle."));
1196 }
1197 
1198 void Palapeli::GamePlay::positionChanged(int reduction)
1199 {
1200     if (reduction) {
1201         qCDebug(PALAPELI_LOG) << "Reduction:" << reduction << "from" << m_currentPieceCount;
1202         bool victory = (m_currentPieceCount > 1) &&
1203                    ((m_currentPieceCount - reduction) <= 1);
1204         m_currentPieceCount = m_currentPieceCount - reduction;
1205         Q_EMIT reportProgress(m_originalPieceCount, m_currentPieceCount);
1206         if (victory) {
1207             playVictoryAnimation();
1208         }
1209     }
1210     if (!m_savegameTimer->isActive())
1211         m_savegameTimer->start();
1212 }
1213 
1214 void Palapeli::GamePlay::updateSavedGame()
1215 {
1216     QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
1217             QLatin1Char('/') + saveGamePath();
1218     QDir d(path);
1219     if (!d.exists())
1220         d.mkpath(path);
1221 
1222     KConfig savedConfig(path + saveGameFileName(m_puzzle->identifier()), KConfig::SimpleConfig);
1223 
1224     savePuzzleSettings(&savedConfig);
1225 
1226     // Save the positions of pieces and attributes of piece-holders.
1227     KConfigGroup headerGroup   (&savedConfig, HeaderSaveGroup);
1228     KConfigGroup holderGroup   (&savedConfig, HolderSaveGroup);
1229     KConfigGroup locationGroup (&savedConfig, LocationSaveGroup);
1230 
1231     headerGroup.writeEntry("N_Holders", m_viewList.count() - 1);
1232 
1233     int groupID = 0;
1234     for (Palapeli::View* view : std::as_const(m_viewList)) {
1235         bool isHolder = (view != m_puzzleTable->view());
1236         if (isHolder) {
1237         KConfigGroup holderDetails(&savedConfig,
1238             QStringLiteral("Holder_%1").arg(groupID));
1239         Palapeli::PieceHolder* holder =
1240             qobject_cast<Palapeli::PieceHolder*>(view);
1241         bool selected = (view == m_currentHolder);
1242         holderDetails.writeEntry("Name", holder->name());
1243         holderDetails.writeEntry("Selected", selected);
1244         holderDetails.writeEntry("Geometry",
1245             QRect(view->frameGeometry().topLeft(), view->size()));
1246         }
1247         const QList<Palapeli::Piece*> pieces = view->scene()->pieces();
1248         for (Palapeli::Piece* piece : pieces) {
1249         const QPointF position = piece->pos();
1250         const auto atomicPieces = piece->representedAtomicPieces();
1251         for (int atomicPieceID : atomicPieces) {
1252             const QString ID = QString::number(atomicPieceID);
1253             locationGroup.writeEntry(ID, position);
1254             if (isHolder) {
1255             holderGroup.writeEntry(ID, groupID);
1256             }
1257             else {
1258             holderGroup.deleteEntry(ID);
1259             }
1260         }
1261         }
1262         groupID++;
1263     }
1264 }
1265 
1266 void Palapeli::GamePlay::savePuzzleSettings(KConfig* savedConfig)
1267 {
1268     // Save the Appearance settings of the pieces and puzzle background.
1269     KConfigGroup settingsGroup (savedConfig, AppearanceSaveGroup);
1270     settingsGroup.writeEntry("PieceBevelsEnabled",
1271                 Settings::pieceBevelsEnabled());
1272     settingsGroup.writeEntry("PieceShadowsEnabled",
1273                 Settings::pieceShadowsEnabled());
1274     settingsGroup.writeEntry("PieceSpacing", Settings::pieceSpacing());
1275     settingsGroup.writeEntry("ViewBackground", Settings::viewBackground());
1276     settingsGroup.writeEntry("ViewBackgroundColor",
1277                 Settings::viewBackgroundColor());
1278     settingsGroup.writeEntry("ViewHighlightColor",
1279                 Settings::viewHighlightColor());
1280     Palapeli::ConfigDialog::SolutionSpace solutionArea =
1281                 (Palapeli::ConfigDialog::SolutionSpace)
1282                 Settings::solutionArea();
1283     settingsGroup.writeEntry("SolutionArea", (int)solutionArea);
1284 
1285     // Save the Preview settings.
1286     KConfigGroup previewGroup (savedConfig, PreviewSaveGroup);
1287     previewGroup.writeEntry("PuzzlePreviewGeometry",
1288                 Settings::puzzlePreviewGeometry());
1289     previewGroup.writeEntry("PuzzlePreviewVisible",
1290                 Settings::puzzlePreviewVisible());
1291 }
1292 
1293 void Palapeli::GamePlay::restorePuzzleSettings(KConfig* savedConfig)
1294 {
1295     // Assume Palapeli::loadPuzzle() has tested if Appearance group exists.
1296     KConfigGroup settingsGroup(savedConfig, AppearanceSaveGroup);
1297     Settings::setPieceBevelsEnabled(settingsGroup.readEntry(
1298                 "PieceBevelsEnabled", false));
1299     Settings::setPieceShadowsEnabled(settingsGroup.readEntry(
1300                 "PieceShadowsEnabled", false));
1301     Settings::setPieceSpacing(settingsGroup.readEntry(
1302                 "PieceSpacing", 6));
1303     Settings::setViewBackground(settingsGroup.readEntry(
1304                 "ViewBackground", "background.svg"));
1305     Settings::setViewBackgroundColor(settingsGroup.readEntry(
1306                 "ViewBackgroundColor", QColor(0xfff7eb)));
1307     Settings::setViewHighlightColor(settingsGroup.readEntry(
1308                 "ViewHighlightColor", QColor(0x6effff)));
1309     Settings::setSolutionArea(settingsGroup.readEntry(
1310                 "SolutionArea", 2));
1311 
1312     // Ask TextureHelper to re-draw background (but only after KConfigDialog
1313     // has written the settings, which might happen after this slot call).
1314     QTimer::singleShot(0, Palapeli::TextureHelper::instance(),
1315                 &Palapeli::TextureHelper::readSettings);
1316 
1317     if (savedConfig->hasGroup(PreviewSaveGroup)) {
1318         KConfigGroup previewGroup(savedConfig, PreviewSaveGroup);
1319         Settings::setPuzzlePreviewGeometry(previewGroup.readEntry(
1320                 "PuzzlePreviewGeometry", QRect(-1,-1,320,240)));
1321         Settings::setPuzzlePreviewVisible(previewGroup.readEntry(
1322                 "PuzzlePreviewVisible", true));
1323     }
1324 }
1325 
1326 void Palapeli::GamePlay::changeSelectedHolder(Palapeli::PieceHolder* h)
1327 {
1328     if (m_currentHolder && (m_currentHolder != h)) {
1329         m_previousHolder = m_currentHolder;
1330         m_currentHolder->setSelected(false);
1331     }
1332     m_currentHolder = h;
1333 }
1334 
1335 #include "moc_gameplay.cpp"