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"