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"