File indexing completed on 2024-05-12 04:06:24
0001 /* 0002 SPDX-FileCopyrightText: 2010 Stefan Majewsky <majewsky@gmx.net> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "interactors.h" 0008 #include "piece.h" 0009 #include "scene.h" 0010 #include "view.h" 0011 0012 #include "palapeli_debug.h" // IDW test. 0013 0014 #include <QApplication> 0015 #include <QStyle> 0016 #include <QStyleOptionRubberBand> 0017 #include <QIcon> 0018 #include <KLocalizedString> 0019 0020 //BEGIN Palapeli::MovePieceInteractor 0021 0022 Palapeli::MovePieceInteractor::MovePieceInteractor(QGraphicsView* view) 0023 : Palapeli::Interactor(20, Palapeli::MouseInteractor, view) //priority: very high because this is the most important interaction 0024 { 0025 setMetadata(PieceInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Move pieces by dragging"), QIcon::fromTheme( QStringLiteral( "transform-move" ))); 0026 } 0027 0028 static QGraphicsItem* findSelectableItemAt(const QPointF& scenePos, QGraphicsScene* scene) 0029 { 0030 if (!scene) 0031 return nullptr; 0032 const QList<QGraphicsItem*> itemsUnderMouse = scene->items(scenePos); 0033 for (QGraphicsItem* itemUnderMouse : itemsUnderMouse) 0034 if (itemUnderMouse->flags() & QGraphicsItem::ItemIsSelectable) 0035 return itemUnderMouse; 0036 return nullptr; 0037 } 0038 0039 void Palapeli::MovePieceInteractor::determineSelectedItems(QGraphicsItem* clickedItem, Palapeli::Piece* clickedPiece) 0040 { 0041 m_currentPieces.clear(); 0042 const QList<QGraphicsItem*> selectedItems = clickedItem->scene()->selectedItems(); 0043 if (clickedItem->isSelected()) 0044 { 0045 //clicked item is already selected -> include all selected items/pieces in this move 0046 for (QGraphicsItem* selectedItem : selectedItems) 0047 { 0048 Palapeli::Piece* selectedPiece = Palapeli::Piece::fromSelectedItem(selectedItem); 0049 if (selectedPiece) 0050 m_currentPieces << selectedPiece; 0051 } 0052 //NOTE: clickedItem is in the list selectedItems, so it need not be handled separately. 0053 } 0054 else 0055 { 0056 //clicked item is not selected -> deselect everything else and select only this item 0057 for (QGraphicsItem* selectedItem : selectedItems) 0058 selectedItem->setSelected(false); 0059 clickedItem->setSelected(true); 0060 m_currentPieces << clickedPiece; 0061 Palapeli::View* v = qobject_cast<Palapeli::View*>(this->view()); 0062 v->handleNewPieceSelection(); 0063 } 0064 } 0065 0066 bool Palapeli::MovePieceInteractor::startInteraction(const Palapeli::MouseEvent& event) 0067 { 0068 //no item under mouse -> no interaction possible 0069 QGraphicsItem* selectableItemUnderMouse = findSelectableItemAt(event.scenePos, scene()); 0070 if (!selectableItemUnderMouse) 0071 return false; 0072 //we will only move pieces -> find the piece which we are moving 0073 Palapeli::Piece* piece = Palapeli::Piece::fromSelectedItem(selectableItemUnderMouse); 0074 if (!piece) 0075 return false; 0076 //start moving this piece 0077 determineSelectedItems(selectableItemUnderMouse, piece); 0078 m_baseViewPosition = event.pos; 0079 m_baseScenePosition = event.scenePos; 0080 m_currentOffset = QPointF(); 0081 m_basePositions.clear(); 0082 for (Palapeli::Piece* piece : std::as_const(m_currentPieces)) 0083 { 0084 m_basePositions << piece->pos(); 0085 connect(piece, &Piece::replacedBy, this, &MovePieceInteractor::pieceReplacedBy, Qt::DirectConnection); 0086 piece->beginMove(); 0087 } 0088 return true; 0089 } 0090 0091 void Palapeli::MovePieceInteractor::continueInteraction(const Palapeli::MouseEvent& event) 0092 { 0093 // Ignore tiny drags. They are probably meant to be a select. 0094 if ((event.pos - m_baseViewPosition).manhattanLength() < 0095 QApplication::startDragDistance()) { 0096 return; 0097 } 0098 m_currentOffset = event.scenePos - m_baseScenePosition; 0099 for (int i = 0; i < m_currentPieces.count(); ++i) 0100 { 0101 m_currentPieces[i]->setPos(m_basePositions[i] + m_currentOffset); 0102 m_currentPieces[i]->doMove(); 0103 } 0104 } 0105 0106 void Palapeli::MovePieceInteractor::pieceReplacedBy(Palapeli::Piece* replacement) 0107 { 0108 //This slot is triggered when a MergeGroup replaces one of the m_currentPieces by a new piece. 0109 //remove old piece from data structures 0110 int index = m_currentPieces.indexOf(qobject_cast<Palapeli::Piece*>(sender())); 0111 if (index == -1) //do nothing if sender is not a current piece 0112 return; 0113 m_currentPieces.removeAt(index); 0114 m_basePositions.removeAt(index); 0115 //add new piece (might not always be necessary, if the new piece replaces more than one of the selected pieces) 0116 if (!m_currentPieces.contains(replacement)) 0117 { 0118 m_currentPieces << replacement; 0119 m_basePositions << replacement->pos() - m_currentOffset; 0120 } 0121 } 0122 0123 void Palapeli::MovePieceInteractor::stopInteraction(const Palapeli::MouseEvent& event) 0124 { 0125 Q_UNUSED(event) 0126 for (Palapeli::Piece* piece : std::as_const(m_currentPieces)) 0127 { 0128 disconnect(piece, nullptr, this, nullptr); 0129 piece->endMove(); 0130 } 0131 m_currentPieces.clear(); 0132 } 0133 0134 //END Palapeli::MovePieceInteractor 0135 //BEGIN Palapeli::SelectPieceInteractor 0136 0137 Palapeli::SelectPieceInteractor::SelectPieceInteractor(QGraphicsView* view) 0138 : Palapeli::Interactor(19, Palapeli::MouseInteractor, view) //priority: a bit less than MovePieceInteractor 0139 { 0140 setMetadata(PieceInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Select pieces by clicking"), QIcon::fromTheme( QStringLiteral( "edit-select" ))); 0141 } 0142 0143 bool Palapeli::SelectPieceInteractor::startInteraction(const Palapeli::MouseEvent& event) 0144 { 0145 //no item under mouse -> no interaction possible 0146 QGraphicsItem* selectableItemUnderMouse = findSelectableItemAt(event.scenePos, scene()); 0147 if (!selectableItemUnderMouse) 0148 return false; 0149 //we will only move pieces -> find the piece which we are moving 0150 m_currentPiece = Palapeli::Piece::fromSelectedItem(selectableItemUnderMouse); 0151 if (!m_currentPiece) 0152 return false; 0153 //toggle selection state for piece under mouse 0154 bool previouslySelected = m_currentPiece->isSelected(); 0155 m_currentPiece->setSelected(! previouslySelected); 0156 m_currentPiece->startClick(); 0157 if (! previouslySelected) { 0158 Palapeli::View* v = qobject_cast<Palapeli::View*>(this->view()); 0159 v->handleNewPieceSelection(); 0160 } 0161 return true; 0162 } 0163 0164 void Palapeli::SelectPieceInteractor::stopInteraction(const Palapeli::MouseEvent& event) 0165 { 0166 Q_UNUSED(event) 0167 m_currentPiece->endClick(); 0168 } 0169 0170 //END Palapeli::SelectPieceInteractor 0171 //BEGIN Palapeli::TeleportPieceInteractor 0172 0173 Palapeli::TeleportPieceInteractor::TeleportPieceInteractor(QGraphicsView* view) 0174 : Palapeli::Interactor(25, Palapeli::MouseInteractor, view) 0175 { 0176 setMetadata(PieceInteraction, i18nc("Works instantly, without dragging", "Teleport pieces to or from a holder"), QIcon()); 0177 qCDebug(PALAPELI_LOG) << "CONSTRUCTED TeleportPieceInteractor"; 0178 } 0179 0180 bool Palapeli::TeleportPieceInteractor::startInteraction(const Palapeli::MouseEvent& event) 0181 { 0182 qCDebug(PALAPELI_LOG) << "ENTERED TeleportPieceInteractor::startInteraction"; 0183 Palapeli::View* view = qobject_cast<Palapeli::View*>(this->view()); 0184 if (!view) 0185 return false; 0186 QGraphicsItem* item = findSelectableItemAt(event.scenePos, scene()); 0187 Palapeli::Piece* piece = nullptr; 0188 if (item) { 0189 piece = Palapeli::Piece::fromSelectedItem(item); 0190 } 0191 view->teleportPieces(piece, event.scenePos); 0192 return true; 0193 } 0194 0195 //END Palapeli::TeleportPieceInteractor 0196 //BEGIN Palapeli::MoveViewportInteractor 0197 0198 Palapeli::MoveViewportInteractor::MoveViewportInteractor(QGraphicsView* view) 0199 : Palapeli::Interactor(1, Palapeli::MouseInteractor, view) //priority: very low because specific interaction points (e.g. pieces, scene boundaries) are much more important 0200 { 0201 setMetadata(ViewportInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Move viewport by dragging"), QIcon::fromTheme( QStringLiteral( "transform-move" ))); 0202 } 0203 0204 bool Palapeli::MoveViewportInteractor::startInteraction(const Palapeli::MouseEvent& event) 0205 { 0206 m_lastPos = event.pos; 0207 return true; 0208 } 0209 0210 void Palapeli::MoveViewportInteractor::continueInteraction(const Palapeli::MouseEvent& event) 0211 { 0212 Palapeli::View* view = qobject_cast<Palapeli::View*>(this->view()); 0213 if (view) 0214 view->moveViewportBy(event.pos - m_lastPos); 0215 m_lastPos = event.pos; 0216 } 0217 0218 //END Palapeli::MoveViewportInteractor 0219 //BEGIN Palapeli::ToggleCloseUpInteractor 0220 0221 Palapeli::ToggleCloseUpInteractor::ToggleCloseUpInteractor(QGraphicsView* view) 0222 : Palapeli::Interactor(2, Palapeli::MouseInteractor, view) 0223 { 0224 // IDW TODO - Check the priority against other priorities. 0225 // 0226 // IDW TODO - What about Palapeli::MouseEvent flags? 0227 setMetadata(ViewportInteraction, i18nc("As in a movie scene", "Switch to close-up or distant view"), QIcon::fromTheme(QStringLiteral("zoom-in"))); 0228 qCDebug(PALAPELI_LOG) << "CONSTRUCTED ToggleCloseUpInteractor"; 0229 } 0230 0231 bool Palapeli::ToggleCloseUpInteractor::startInteraction(const Palapeli::MouseEvent& event) 0232 { 0233 Q_UNUSED(event); 0234 qCDebug(PALAPELI_LOG) << "ENTERED ToggleCloseUpInteractor::startInteraction"; 0235 Palapeli::View* view = qobject_cast<Palapeli::View*>(this->view()); 0236 if (view) 0237 view->toggleCloseUp(); 0238 return true; 0239 } 0240 0241 //END Palapeli::ToggleCloseUpInteractor 0242 //BEGIN Palapeli::ZoomViewportInteractor 0243 0244 Palapeli::ZoomViewportInteractor::ZoomViewportInteractor(QGraphicsView* view) 0245 : Palapeli::Interactor(2, Palapeli::WheelInteractor, view) //priority: more important than ScrollViewport 0246 { 0247 setMetadata(ViewportInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Zoom viewport"), QIcon::fromTheme( QStringLiteral( "zoom-in" ))); 0248 } 0249 0250 void Palapeli::ZoomViewportInteractor::doInteraction(const Palapeli::WheelEvent& event) 0251 { 0252 Palapeli::View* view = qobject_cast<Palapeli::View*>(this->view()); 0253 if (view) 0254 view->zoomBy(event.delta); 0255 } 0256 0257 //END Palapeli::ZoomViewportInteractor 0258 //BEGIN Palapeli::ScrollViewportInteractor 0259 0260 Palapeli::ScrollViewportInteractor::ScrollViewportInteractor(Qt::Orientation orientation, QGraphicsView* view) 0261 : Palapeli::Interactor(1, Palapeli::WheelInteractor, view) //priority: less important than ZoomViewport 0262 , m_orientation(orientation) 0263 { 0264 QString description; 0265 if (orientation == Qt::Horizontal) 0266 description = i18nc("Description (used like a name) for a mouse interaction method", "Scroll viewport horizontally"); 0267 else 0268 description = i18nc("Description (used like a name) for a mouse interaction method", "Scroll viewport vertically"); 0269 setMetadata(ViewportInteraction, description, QIcon()); 0270 } 0271 0272 void Palapeli::ScrollViewportInteractor::doInteraction(const Palapeli::WheelEvent& event) 0273 { 0274 const QPoint widgetDelta = (m_orientation == Qt::Horizontal) ? QPoint(event.delta, 0) : QPoint(0, event.delta); 0275 Palapeli::View* view = qobject_cast<Palapeli::View*>(this->view()); 0276 if (view) 0277 view->moveViewportBy(view->mapToScene(widgetDelta)- view->mapToScene(QPoint())); 0278 } 0279 0280 //END Palapeli::ScrollViewportInteractor 0281 //BEGIN Palapeli::RubberBandItem 0282 0283 Palapeli::RubberBandItem::RubberBandItem(QGraphicsItem* parent) 0284 : QGraphicsItem(parent) 0285 { 0286 } 0287 0288 QRectF Palapeli::RubberBandItem::rect() const 0289 { 0290 return m_rect; 0291 } 0292 0293 void Palapeli::RubberBandItem::setRect(const QRectF& rect) 0294 { 0295 if (m_rect == rect || (m_rect.isEmpty() && rect.isEmpty())) 0296 return; 0297 prepareGeometryChange(); 0298 m_rect = rect; 0299 update(); 0300 //update list of selected items when rubberband is visible 0301 if (!rect.isEmpty()) 0302 { 0303 QPainterPath p; 0304 p.addRect(sceneBoundingRect()); 0305 scene()->setSelectionArea(p, Qt::ReplaceSelection, Qt::ContainsItemBoundingRect); 0306 } 0307 } 0308 0309 QRectF Palapeli::RubberBandItem::boundingRect() const 0310 { 0311 return m_rect; //The QStyle does not paint outside this rect! 0312 } 0313 0314 void Palapeli::RubberBandItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) 0315 { 0316 Q_UNUSED(painter) 0317 Q_UNUSED(option) 0318 if (m_rect.isEmpty()) 0319 return; 0320 //find the view which we are painting on (we could be painting directly, or on a viewport contained in the view) 0321 QGraphicsView* view = qobject_cast<QGraphicsView*>(widget); 0322 if (!view) 0323 view = qobject_cast<QGraphicsView*>(widget->parent()); 0324 Q_ASSERT(view); 0325 //render on view directly 0326 QPainter viewPainter(widget); 0327 QRect rect = view->mapFromScene(sceneBoundingRect()).boundingRect(); 0328 QStyleOptionRubberBand opt; 0329 opt.initFrom(widget); 0330 opt.rect = rect; 0331 opt.shape = QRubberBand::Rectangle; 0332 //painter clipping for masked rubberbands 0333 QStyleHintReturnMask mask; 0334 if (widget->style()->styleHint(QStyle::SH_RubberBand_Mask, &opt, widget, &mask)) 0335 painter->setClipRegion(mask.region, Qt::IntersectClip); 0336 //draw rubberband 0337 widget->style()->drawControl(QStyle::CE_RubberBand, &opt, &viewPainter, widget); 0338 } 0339 0340 //END Palapeli::RubberBandItem 0341 //BEGIN Palapeli::RubberBandInteractor 0342 0343 Palapeli::RubberBandInteractor::RubberBandInteractor(QGraphicsView* view) 0344 : Palapeli::Interactor(2, Palapeli::MouseInteractor, view) //priority: a bit more than MoveViewport, but still much less than interactions with specific interaction points (e.g. pieces, scene boundaries) 0345 , m_item(new Palapeli::RubberBandItem) 0346 { 0347 setMetadata(PieceInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Select multiple pieces at once"), QIcon::fromTheme( QStringLiteral( "select-rectangular" ))); 0348 if (scene()) 0349 scene()->addItem(m_item); 0350 m_item->hide(); //NOTE: This is not necessary for the painting, but we use m_item->isVisible() to determine whether we are rubberbanding at the moment. //FIXME: really? 0351 } 0352 0353 Palapeli::RubberBandInteractor::~RubberBandInteractor() 0354 { 0355 delete m_item; 0356 } 0357 0358 void Palapeli::RubberBandInteractor::sceneChangeEvent(QGraphicsScene* oldScene, QGraphicsScene* newScene) 0359 { 0360 if (oldScene) 0361 oldScene->removeItem(m_item); 0362 if (newScene) 0363 newScene->addItem(m_item); 0364 m_item->setVisible(false); 0365 } 0366 0367 bool Palapeli::RubberBandInteractor::startInteraction(const Palapeli::MouseEvent& event) 0368 { 0369 //check items under mouse 0370 if (findSelectableItemAt(event.scenePos, scene())) 0371 return false; 0372 //start rubberbanding 0373 m_basePosition = event.scenePos; 0374 m_item->show(); //NOTE: This is not necessary for the painting, but we use m_item->isVisible() to determine whether we are rubberbanding at the moment. 0375 m_item->setRect(QRectF(m_basePosition, QSizeF())); 0376 scene()->setSelectionArea(QPainterPath()); //deselect everything 0377 return true; 0378 } 0379 0380 void Palapeli::RubberBandInteractor::continueInteraction(const Palapeli::MouseEvent& event) 0381 { 0382 QSizeF size(event.scenePos.x() - m_basePosition.x(), event.scenePos.y() - m_basePosition.y()); 0383 QRectF rect(m_basePosition, size); 0384 m_item->setRect(rect.normalized()); 0385 } 0386 0387 void Palapeli::RubberBandInteractor::stopInteraction(const Palapeli::MouseEvent& event) 0388 { 0389 Q_UNUSED(event) 0390 m_item->hide(); //NOTE: This is not necessary for the painting, but we use m_item->isVisible() to determine whether we are rubberbanding at the moment. 0391 m_item->setRect(QRectF()); // Finalise the selection(s), if any. 0392 const QList<QGraphicsItem*> selectedItems = scene()->selectedItems(); 0393 for (QGraphicsItem* selectedItem : selectedItems) { 0394 Palapeli::Piece* selectedPiece = 0395 Palapeli::Piece::fromSelectedItem(selectedItem); 0396 if (selectedPiece) { 0397 Palapeli::View* v = 0398 qobject_cast<Palapeli::View*>(this->view()); 0399 v->handleNewPieceSelection(); 0400 break; 0401 } 0402 } 0403 } 0404 0405 //END Palapeli::RubberBandInteractor 0406 //BEGIN Palapeli::ToggleConstraintInteractor 0407 0408 Palapeli::ToggleConstraintInteractor::ToggleConstraintInteractor(QGraphicsView* view) 0409 : Palapeli::Interactor(30, Palapeli::MouseInteractor, view) //priority: higher than anything else (this should not depend on what the cursor is currently pointing) 0410 { 0411 setMetadata(TableInteraction, i18nc("Description (used like a name) for a mouse interaction method", "Toggle lock state of the puzzle table area"), QIcon()); 0412 } 0413 0414 bool Palapeli::ToggleConstraintInteractor::startInteraction(const Palapeli::MouseEvent& event) 0415 { 0416 Q_UNUSED(event) 0417 Palapeli::Scene* scene = qobject_cast<Palapeli::Scene*>(this->scene()); 0418 if (scene) 0419 scene->setConstrained(!scene->isConstrained()); 0420 return (bool) scene; 0421 } 0422 0423 //END Palapeli::ToggleConstraintInteractor 0424 0425 #include "moc_interactors.cpp"