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"