File indexing completed on 2024-05-12 04:06:26

0001 /*
0002     SPDX-FileCopyrightText: 2009, 2010 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "view.h"
0008 #include "interactormanager.h"
0009 #include "scene.h"
0010 #include "piece.h"
0011 #include "texturehelper.h"
0012 
0013 #include <cmath>
0014 #include <QMouseEvent>
0015 #include <QPropertyAnimation>
0016 #include <QScrollBar>
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 
0020 #include <QScreen>
0021 
0022 #include <QTimer>
0023 #include "palapeli_debug.h" // IDW test.
0024 
0025 const int Palapeli::View::MinimumZoomLevel = 0;
0026 const int Palapeli::View::MaximumZoomLevel = 200;
0027 const int DefaultDelta = 120;
0028 
0029 Palapeli::View::View()
0030     : m_interactorManager(new Palapeli::InteractorManager(this))
0031     , m_scene(nullptr)
0032     , m_zoomLevel(MinimumZoomLevel)
0033     , m_closeUpLevel(MaximumZoomLevel)
0034     , m_distantLevel(MinimumZoomLevel)
0035     , m_isCloseUp(false)
0036     , m_dZoom(20.0)
0037     , m_minScale(0.01)
0038     , m_adjustPointer(false)
0039 {
0040     setFrameStyle(QFrame::NoFrame);
0041     setMouseTracking(true);
0042     setResizeAnchor(QGraphicsView::AnchorUnderMouse);
0043     setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
0044     setScene(new Palapeli::Scene(this));
0045     connect(m_scene, &Palapeli::Scene::sceneRectChanged, this, &View::logSceneChange);
0046     qCDebug(PALAPELI_LOG) << "Initial size of Palapeli::View" << size();
0047 }
0048 
0049 // IDW test.
0050 void Palapeli::View::logSceneChange(const QRectF &r)
0051 {
0052     Q_UNUSED(r);
0053     // qCDebug(PALAPELI_LOG) << "View::logSceneChange" << r << "View size" << this->size();
0054 }
0055 
0056 Palapeli::InteractorManager* Palapeli::View::interactorManager() const
0057 {
0058     return m_interactorManager;
0059 }
0060 
0061 Palapeli::Scene* Palapeli::View::scene() const
0062 {
0063     return m_scene;
0064 }
0065 
0066 void Palapeli::View::setScene(Palapeli::Scene* scene)
0067 {
0068     if (m_scene == scene)
0069         return;
0070     m_scene = scene;
0071     this->QGraphicsView::setScene(m_scene);
0072     m_interactorManager->updateScene();
0073     Palapeli::TextureHelper::instance()->addScene(m_scene);
0074     // Draw empty, hidden scene: needed to get first load resized correctly.
0075     scene->addMargin(20.0, 10.0);
0076     // Set zoom level to middle of range.
0077     zoomTo((MaximumZoomLevel+MinimumZoomLevel)/2);
0078 }
0079 
0080 QRectF Palapeli::View::viewportRect() const
0081 {
0082     return mapToScene(viewport()->rect()).boundingRect();
0083 }
0084 
0085 void Palapeli::View::setViewportRect(const QRectF& viewportRect)
0086 {
0087     //NOTE: Do never ever use this except for the victory animation, or stuff will break!!!
0088     fitInView(viewportRect, Qt::KeepAspectRatio);
0089 }
0090 
0091 void Palapeli::View::keyPressEvent(QKeyEvent* event)
0092 {
0093     m_interactorManager->handleEvent(event);
0094     QGraphicsView::keyPressEvent(event);
0095 }
0096 
0097 void Palapeli::View::keyReleaseEvent(QKeyEvent* event)
0098 {
0099     m_interactorManager->handleEvent(event);
0100     QGraphicsView::keyReleaseEvent(event);
0101 }
0102 
0103 void Palapeli::View::mouseMoveEvent(QMouseEvent* event)
0104 {
0105     m_interactorManager->handleEvent(event);
0106     event->accept();
0107     //send a stripped QMouseEvent to base class to update resizeAnchor() etc.
0108     QMouseEvent modifiedEvent(event->type(),
0109         event->pos(), event->globalPosition().toPoint(),
0110         Qt::NoButton, Qt::NoButton, event->modifiers()
0111     );
0112     QGraphicsView::mouseMoveEvent(&modifiedEvent);
0113 }
0114 
0115 void Palapeli::View::mousePressEvent(QMouseEvent* event)
0116 {
0117     m_interactorManager->handleEvent(event);
0118     event->accept();
0119 }
0120 
0121 void Palapeli::View::mouseReleaseEvent(QMouseEvent* event)
0122 {
0123     m_interactorManager->handleEvent(event);
0124     event->accept();
0125 }
0126 
0127 void Palapeli::View::wheelEvent(QWheelEvent* event)
0128 {
0129     m_interactorManager->handleEvent(event);
0130     //We do intentionally *not* propagate to QGV::wheelEvent.
0131 }
0132 
0133 void Palapeli::View::moveViewportBy(const QPointF& sceneDelta)
0134 {
0135     horizontalScrollBar()->setValue(horizontalScrollBar()->value() + (isRightToLeft() ? sceneDelta.x() : -sceneDelta.x()));
0136     verticalScrollBar()->setValue(verticalScrollBar()->value() - sceneDelta.y());
0137 }
0138 
0139 void Palapeli::View::teleportPieces(Piece* pieceUnder, const QPointF& scenePos)
0140 {
0141     qCDebug(PALAPELI_LOG) << "TELEPORT: pieceUnder" << (pieceUnder != nullptr)
0142          << "scenePos" << scenePos;
0143     Q_EMIT teleport(pieceUnder, scenePos, this);
0144 }
0145 
0146 void Palapeli::View::zoomBy(int delta)
0147 {
0148     // Scroll wheel and touchpad come here.
0149     // Delta is typically +-120 per click for a mouse-wheel, but can be <10
0150     // for an Apple MacBook touchpad (using two fingers to scroll).
0151 
0152     // IDW TODO - Accept deltas of <10, either by accumulating deltas or by
0153     //            implementing fractional zoom levels.
0154     qCDebug(PALAPELI_LOG) << "View::zoomBy: delta" << delta;
0155     m_adjustPointer = true;
0156     zoomTo(m_zoomLevel + delta / 10);
0157 }
0158 
0159 void Palapeli::View::zoomTo(int level)
0160 {
0161     // IDW TODO - BUG: If you zoom out as far as Palapeli will go, using the
0162     //            scroll-wheel, then go on scrolling, the view will zoom in
0163     //            and back out again momentarily.
0164 
0165     // Validate/normalize input.
0166     level = qBound(MinimumZoomLevel, level, MaximumZoomLevel);
0167     // Skip unimportant requests.
0168     if (level == m_zoomLevel) {
0169         return;
0170     }
0171     // Save the mouse position in both view and scene.
0172     m_mousePos = mapFromGlobal(QCursor::pos());
0173     m_scenePos = mapToScene(m_mousePos);
0174     // Create a new transform.
0175     const qreal scalingFactor = m_minScale * pow(2, level/m_dZoom);
0176     qCDebug(PALAPELI_LOG) << "View::zoomTo: level" << level
0177          << "scalingFactor" << scalingFactor
0178          << m_mousePos << m_scenePos;
0179     // Translation, shear, etc. are the same: only the scale is replaced.
0180     QTransform t = transform();
0181     t.setMatrix(scalingFactor, t.m12(), t.m13(),
0182             t.m21(), scalingFactor, t.m23(),
0183             t.m31(), t.m32(), t.m33());
0184     setTransform(t);
0185     // Save and report changes.
0186     m_zoomLevel = level;
0187     Q_EMIT zoomLevelChanged(m_zoomLevel);
0188     // In a mouse-centered zoom, lock the pointer onto the scene position.
0189     if (m_adjustPointer) {
0190         // Let the new view settle down before checking the mouse.
0191         QTimer::singleShot(0, this, &View::adjustPointer);
0192     }
0193 }
0194 
0195 void Palapeli::View::adjustPointer()
0196 {
0197     // If the view moved, keep the mouse at the same position in the scene.
0198     const QPoint mousePos = mapFromScene(m_scenePos);
0199     if (mousePos != m_mousePos) {
0200         qCDebug(PALAPELI_LOG) << "POINTER MOVED from" << m_mousePos
0201              << "to" << mousePos << "scenePos" << m_scenePos;
0202         QCursor::setPos(mapToGlobal(mousePos));
0203     }
0204 }
0205 
0206 void Palapeli::View::zoomSliderInput(int level)
0207 {
0208     if (level == m_zoomLevel) {
0209         return;     // Avoid echo from zoomLevelChanged() signal.
0210     }
0211     m_adjustPointer = false;
0212     zoomTo(level);
0213 }
0214 
0215 void Palapeli::View::zoomIn()
0216 {
0217     // ZoomWidget ZoomIn button comes here via zoomInRequest signal.
0218     // ZoomIn menu and shortcut come here via GamePlay::actionZoomIn.
0219     m_adjustPointer = false;
0220     zoomTo(m_zoomLevel + DefaultDelta / 10);
0221 }
0222 
0223 void Palapeli::View::zoomOut()
0224 {
0225     // ZoomWidget ZoomOut button comes here via zoomOutRequest signal.
0226     // ZoomOut menu and shortcut come here via GamePlay::actionZoomOut.
0227     m_adjustPointer = false;
0228     zoomTo(m_zoomLevel - DefaultDelta / 10);
0229 }
0230 
0231 // IDW TODO - Keyboard shortcuts for moving the view left, right, up or down by
0232 //            one "frame" or "page".  Map to Arrow keys, PageUp and PageDown.
0233 //            Use QAbstractScrollArea (inherited by QGraphicsView) to get the
0234 //            QScrollBar objects (horizontal and vertical).  QAbstractSlider,
0235 //            an ancestor of QScrollBar, contains position info, signals and
0236 //            triggers for scroll bar moves (i.e. triggerAction(action type)).
0237 
0238 // NOTE: We must have m_closeUpLevel >= (m_distantLevel + MinDiff) at all times.
0239 const int MinDiff = 10;     // Minimum separation of the two zoom levels.
0240 
0241 void Palapeli::View::toggleCloseUp()
0242 {
0243     m_isCloseUp = !m_isCloseUp; // Switch to the other view.
0244     m_adjustPointer = true;
0245     if (m_isCloseUp) {
0246         // Save distant level as we leave: in case it changed.
0247         m_distantLevel = (m_zoomLevel <= (m_closeUpLevel - MinDiff)) ?
0248                     m_zoomLevel : m_closeUpLevel - MinDiff;
0249         zoomTo(m_closeUpLevel);
0250     }
0251     else {
0252         // Save close-up level as we leave: in case it changed.
0253         m_closeUpLevel = (m_zoomLevel >= (m_distantLevel + MinDiff)) ?
0254                     m_zoomLevel : m_distantLevel + MinDiff;
0255         zoomTo(m_distantLevel);
0256     }
0257 }
0258 
0259 void Palapeli::View::setCloseUp(bool onOff)
0260 {
0261     m_isCloseUp = onOff;
0262     // Force zoomTo() to recalculate, even if m_zoomLevel == required value.
0263     m_zoomLevel = m_isCloseUp ? m_closeUpLevel - 1 : m_distantLevel + 1;
0264     if (m_isCloseUp) {
0265         zoomTo(m_closeUpLevel);
0266     }
0267     else {
0268         zoomTo(m_distantLevel);
0269     }
0270 }
0271 
0272 void Palapeli::View::handleNewPieceSelection()
0273 {
0274     Q_EMIT newPieceSelectionSeen(this);
0275 }
0276 
0277 qreal Palapeli::View::calculateCloseUpScale()
0278 {
0279     // Get the size of the monitor on which this view resides (in pixels).
0280     const QRect monitor = screen()->availableGeometry();
0281     const int pixelsPerPiece = qMin(monitor.width(), monitor.height())/12;
0282     QSizeF size = scene()->pieceAreaSize();
0283     qreal  scale  = pixelsPerPiece/qMin(size.rwidth(),size.rheight());
0284     return scale;
0285 }
0286 
0287 int Palapeli::View::calculateZoomRange(qreal distantScale, bool distantView)
0288 {
0289     qreal closeUpScale = calculateCloseUpScale();
0290     if (closeUpScale < distantScale) {
0291         closeUpScale = distantScale;    // View is already large enough.
0292     }
0293     qCDebug(PALAPELI_LOG) << "View::calculateZoomRange: distantScale" << distantScale
0294          << "distantView" << distantView
0295          << "closeUpScale" << closeUpScale;
0296     const qreal minScale = distantScale*0.75;
0297     const qreal maxScale = closeUpScale*2.0;
0298     const qreal range = log(maxScale/minScale)/log(2.0);
0299     const qreal dZoom = (MaximumZoomLevel - MinimumZoomLevel)/range;
0300     qCDebug(PALAPELI_LOG) << "minScale" << minScale << "maxScale" << maxScale
0301          << "range" << range << "dZoom" << dZoom;
0302     m_dZoom = dZoom;
0303     m_minScale = minScale;
0304 
0305     // Set the toggling levels. If close-up is too small, adjust it.
0306     m_distantLevel = qRound(dZoom*log(distantScale/minScale)/log(2.0));;
0307     m_closeUpLevel = qRound(MaximumZoomLevel - MinimumZoomLevel - m_dZoom);
0308     m_closeUpLevel = (m_closeUpLevel >= (m_distantLevel + MinDiff)) ?
0309                 m_closeUpLevel : m_distantLevel + MinDiff;
0310     m_isCloseUp = (! distantView);  // Start with the view zoomed in or out.
0311     const int level = (distantView ? m_distantLevel : m_closeUpLevel);
0312     qCDebug(PALAPELI_LOG) << "INITIAL LEVEL" << level
0313          << "toggles" << m_distantLevel << m_closeUpLevel;
0314     return level;
0315 }
0316 
0317 void Palapeli::View::puzzleStarted()
0318 {
0319     qCDebug(PALAPELI_LOG) << "ENTERED View::puzzleStarted()";
0320     // At this point the puzzle pieces have been shuffled or loaded from a
0321     // .save file and the puzzle table has been scaled to fit the view. Now
0322     // adjust zooming and slider to a range of distant and close-up views.
0323 
0324     // Choose the lesser of the horizontal and vertical scaling factors.
0325     const qreal distantScale = qMin(transform().m11(), transform().m22());
0326     qCDebug(PALAPELI_LOG) << "distantScale" << distantScale;
0327     // Calculate the zooming range and return the distant scale's level.
0328     int level = calculateZoomRange(distantScale, true);
0329 
0330     // Don't readjust the zoom. Just set the slider pointer.
0331     m_zoomLevel = level;        // Make zoomTo() ignore the back-signal.
0332     Q_EMIT zoomLevelChanged(level);
0333     centerOn(sceneRect().center()); // Center the view of the whole puzzle.
0334     Q_EMIT zoomAdjustable(true);    // Enable the ZoomWidget.
0335 
0336     // Explain autosaving.
0337     KMessageBox::information(window(), i18n("Your progress is saved automatically while you play."), i18nc("used as caption for a dialog that explains the autosave feature", "Automatic saving"), QStringLiteral("autosave-introduction"));
0338     qCDebug(PALAPELI_LOG) << "EXITING View::puzzleStarted()";
0339 }
0340 
0341 void Palapeli::View::startVictoryAnimation()
0342 {
0343     //move viewport to show the complete puzzle
0344     QPropertyAnimation* animation = new QPropertyAnimation(this, "viewportRect", this);
0345     animation->setEndValue(m_scene->extPiecesBoundingRect());
0346     animation->setDuration(1000);
0347     animation->start(QAbstractAnimation::DeleteWhenStopped);
0348     Q_EMIT zoomAdjustable(false);
0349 }
0350 
0351 #include "moc_view.cpp"