File indexing completed on 2024-05-19 04:07:50

0001 /*
0002     SPDX-FileCopyrightText: 2009, 2010 Stefan Majewsky <majewsky@gmx.net>
0003     SPDX-FileCopyrightText: 2010 Johannes Löhnert <loehnert.kde@gmx.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "piece.h"
0009 #include "piece_p.h"
0010 #include "scene.h"
0011 #include "settings.h"
0012 
0013 #include <QApplication>
0014 #include <QGraphicsSceneMouseEvent>
0015 #include <QPalette>
0016 #include <QPropertyAnimation>
0017 #include <QPainter> // IDW test.
0018 #include <QRandomGenerator>
0019 
0020 void Palapeli::Piece::commonInit(const Palapeli::PieceVisuals& pieceVisuals)
0021 {
0022     Palapeli::SelectionAwarePixmapItem* pieceItem = new Palapeli::SelectionAwarePixmapItem(pieceVisuals.pixmap(), this);
0023     connect(pieceItem, &Palapeli::SelectionAwarePixmapItem::selectedChanged, this, &Piece::pieceItemSelectedChanged);
0024     m_pieceItem = pieceItem;
0025     m_pieceItem->setOffset(pieceVisuals.offset());
0026     //initialize behavior
0027     m_pieceItem->setAcceptedMouseButtons(Qt::LeftButton);
0028     m_pieceItem->setCursor(Qt::OpenHandCursor);
0029     m_pieceItem->setFlag(QGraphicsItem::ItemIsSelectable);
0030     // replacing m_pieceItems pixmap (in rerenderBevel()) causes weird pixel errors
0031     // when using fast transformation. SmoothTransformation looks better anyway...
0032     m_pieceItem->setTransformationMode(Qt::SmoothTransformation);
0033     m_offset = m_pieceItem->offset().toPoint();
0034 }
0035 
0036 Palapeli::Piece::Piece(const QImage& pieceImage, const QPoint& offset)
0037     : m_pieceItem(nullptr)
0038     , m_inactiveShadowItem(nullptr)
0039     , m_activeShadowItem(nullptr)
0040     , m_highlightItem(nullptr)
0041     , m_animator(nullptr)
0042     , m_offset(offset)
0043 {
0044     //create bevel map if wanted
0045     if (Settings::pieceBevelsEnabled())
0046     {
0047         //NOTE: The bevel map calculation and application is divided because we
0048         //need different result visuals for different turning angles when piece
0049         //rotation gets implementation.
0050         const QSize size = pieceImage.size();
0051         const int radius = 0.04 * (size.width() + size.height());
0052         const Palapeli::BevelMap bevelMap = Palapeli::calculateBevelMap(pieceImage, radius);
0053         commonInit(Palapeli::PieceVisuals(Palapeli::applyBevelMap(pieceImage, bevelMap, /* angle= */ 0), offset));
0054     }
0055     else
0056         commonInit(Palapeli::PieceVisuals(pieceImage, offset));
0057 }
0058 
0059 Palapeli::Piece::Piece(const Palapeli::PieceVisuals& pieceVisuals, const Palapeli::PieceVisuals& shadowVisuals, const Palapeli::PieceVisuals& highlightVisuals)
0060     : m_pieceItem(nullptr)
0061     , m_inactiveShadowItem(nullptr)
0062     , m_activeShadowItem(nullptr)
0063     , m_highlightItem(nullptr)
0064     , m_animator(nullptr)
0065     , m_offset(QPoint(0, 0))    // Gets set in Piece::commonInit().
0066 {
0067     commonInit(pieceVisuals);
0068     if (!shadowVisuals.isNull())
0069         createShadowItems(shadowVisuals);
0070     if (!highlightVisuals.isNull()) {
0071         m_highlightItem = new QGraphicsPixmapItem
0072                     (highlightVisuals.pixmap(), this);
0073         m_highlightItem->setOffset(highlightVisuals.offset());
0074         m_highlightItem->setZValue(-1);
0075         m_highlightItem->setVisible(isSelected());
0076     }
0077 }
0078 
0079 //BEGIN visuals
0080 
0081 bool Palapeli::Piece::completeVisuals()
0082 {
0083     if (Settings::pieceShadowsEnabled() && !m_inactiveShadowItem)
0084     {
0085         createShadowItems(Palapeli::createShadow(pieceVisuals(), m_atomicSize));
0086         return true;
0087     }
0088     return false;
0089 }
0090 
0091 bool Palapeli::Piece::hasShadow() const
0092 {
0093     return (bool) m_inactiveShadowItem;
0094 }
0095 
0096 bool Palapeli::Piece::hasHighlight() const
0097 {
0098     return (bool) m_highlightItem;
0099 }
0100 
0101 void Palapeli::Piece::createShadowItems(const Palapeli::PieceVisuals& shadowVisuals)
0102 {
0103 /* IDW TODO - DELETE this.
0104 #ifdef Q_OS_MAC
0105     // On Apple OS X the QPalette::Highlight color is blue, but is
0106     // dimmed down, for highlighting black-on-white text presumably.
0107     const QColor activeShadowColor(Qt::cyan);
0108     // Note: Q_WS_MAC is deprecated and does not exist in Qt 5.
0109 #else
0110     const QColor activeShadowColor =
0111         QApplication::palette().color(QPalette::Highlight);
0112 #endif
0113 */
0114     const QColor activeShadowColor = Settings::viewHighlightColor();
0115     const Palapeli::PieceVisuals activeShadowVisuals =
0116         Palapeli::changeShadowColor(shadowVisuals, activeShadowColor);
0117     // Create inactive (unhighlighted) shadow item.
0118     m_inactiveShadowItem = new QGraphicsPixmapItem(shadowVisuals.pixmap(), this);
0119     m_inactiveShadowItem->setOffset(shadowVisuals.offset());
0120     m_inactiveShadowItem->setZValue(-2);
0121     // Create active shadow item (highlighted) and animator for its opacity.
0122     m_activeShadowItem = new QGraphicsPixmapItem(activeShadowVisuals.pixmap(), this);
0123     m_activeShadowItem->setOffset(activeShadowVisuals.offset());
0124     m_activeShadowItem->setZValue(-1);
0125     m_activeShadowItem->setOpacity(isSelected() ? 1.0 : 0.0);
0126     m_animator = new QPropertyAnimation(this, "activeShadowOpacity", this);
0127 }
0128 
0129 void Palapeli::Piece::createHighlight(const QSizeF& pieceAreaSize)
0130 {
0131     QRectF rect = sceneBareBoundingRect();
0132     // IDW TODO - Make the factor an adjustable setting (1.2-2.0).
0133     QSizeF area = 1.5 * pieceAreaSize;
0134     int w = area.width();
0135     int h = area.height();
0136     // IDW TODO - Paint pixmap just once (in Scene?) and shallow-copy it.
0137     QRadialGradient g(QPoint(w/2, h/2), qMin(w/2, h/2));
0138     g.setColorAt(0, Settings::viewHighlightColor());
0139     g.setColorAt(1,Qt::transparent);
0140 
0141     QPixmap p(w, h);
0142     p.fill(Qt::transparent);
0143     QPainter pa;
0144     pa.begin(&p);
0145     pa.setPen(Qt::NoPen);
0146     pa.setBrush(QBrush(g));
0147     pa.drawEllipse(0, 0, w, h);
0148     pa.end();
0149 
0150     m_highlightItem = new QGraphicsPixmapItem(p, this);
0151     m_highlightItem->setOffset(m_offset.x() - w/2 + rect.width()/2,
0152                    m_offset.y() - h/2 + rect.height()/2);
0153     m_highlightItem->setZValue(-1);
0154 }
0155 
0156 QRectF Palapeli::Piece::bareBoundingRect() const
0157 {
0158     return m_pieceItem->boundingRect();
0159 }
0160 
0161 QRectF Palapeli::Piece::sceneBareBoundingRect() const
0162 {
0163     return mapToScene(bareBoundingRect()).boundingRect();
0164 }
0165 
0166 Palapeli::PieceVisuals Palapeli::Piece::pieceVisuals() const
0167 {
0168     return Palapeli::PieceVisuals(m_pieceItem->pixmap(), m_pieceItem->offset().toPoint());
0169 }
0170 
0171 Palapeli::PieceVisuals Palapeli::Piece::shadowVisuals() const
0172 {
0173     if (!m_inactiveShadowItem)
0174         return Palapeli::PieceVisuals();
0175     return Palapeli::PieceVisuals(m_inactiveShadowItem->pixmap(), m_inactiveShadowItem->offset().toPoint());
0176 }
0177 
0178 Palapeli::PieceVisuals Palapeli::Piece::highlightVisuals() const
0179 {
0180     if (!m_highlightItem)
0181         return Palapeli::PieceVisuals();
0182     return Palapeli::PieceVisuals(m_highlightItem->pixmap(), m_highlightItem->offset().toPoint());
0183 }
0184 
0185 qreal Palapeli::Piece::activeShadowOpacity() const
0186 {
0187     return m_activeShadowItem ? m_activeShadowItem->opacity() : 0.0;
0188 }
0189 
0190 void Palapeli::Piece::setActiveShadowOpacity(qreal opacity)
0191 {
0192     if (m_activeShadowItem)
0193         m_activeShadowItem->setOpacity(opacity);
0194 }
0195 
0196 void Palapeli::Piece::pieceItemSelectedChanged(bool selected)
0197 {
0198     if (!m_activeShadowItem) {
0199         // No shadows: use a highlighter.
0200         if (!m_highlightItem) {
0201             createHighlight((qobject_cast<Palapeli::Scene*>
0202                     (scene()))->pieceAreaSize());
0203         }
0204         // IDW TODO - Use an animator to change the visibility?
0205         m_highlightItem->setVisible(selected);
0206         return;
0207     }
0208     //change visibility of active shadow
0209     // IDW TODO - On select, hide black shadow and brighten highlight.
0210     // m_inactiveShadowItem->setVisible(! selected);    // IDW test.
0211     const qreal targetOpacity = selected ? 1.0 : 0.0;
0212     const qreal opacityDiff = qAbs(targetOpacity - activeShadowOpacity());
0213     if (m_animator && opacityDiff != 0)
0214     {
0215         m_animator->setDuration(150 * opacityDiff);
0216         m_animator->setStartValue(activeShadowOpacity());
0217         m_animator->setEndValue(targetOpacity);
0218         m_animator->start();
0219     }
0220 }
0221 
0222 //END visuals
0223 //BEGIN internal datastructures
0224 
0225 void Palapeli::Piece::addRepresentedAtomicPieces(const QList<int>& representedAtomicPieces)
0226 {
0227     for (int id : representedAtomicPieces)
0228         if (!m_representedAtomicPieces.contains(id))
0229             m_representedAtomicPieces << id;
0230 }
0231 
0232 QList<int> Palapeli::Piece::representedAtomicPieces() const
0233 {
0234     return m_representedAtomicPieces;
0235 }
0236 
0237 void Palapeli::Piece::addLogicalNeighbors(const QList<Palapeli::Piece*>& logicalNeighbors)
0238 {
0239     for (Palapeli::Piece* piece : logicalNeighbors)
0240         // IDW TODO - if (!m_logicalNeighbors.contains(piece) && piece)
0241         //            If piece == 0, pieceID was not in m_loadedPieces.
0242         //            This would be an integrity error in .puzzle file.
0243         if (!m_logicalNeighbors.contains(piece))
0244             m_logicalNeighbors << piece;
0245 }
0246 
0247 QList<Palapeli::Piece*> Palapeli::Piece::logicalNeighbors() const
0248 {
0249     return m_logicalNeighbors;
0250 }
0251 
0252 void Palapeli::Piece::rewriteLogicalNeighbors(const QList<Palapeli::Piece*>& oldPieces, Palapeli::Piece* newPiece)
0253 {
0254     bool oldPiecesFound = false;
0255     for (Palapeli::Piece* oldPiece : oldPieces)
0256     {
0257         int index = m_logicalNeighbors.indexOf(oldPiece);
0258         if (index != -1)
0259         {
0260             oldPiecesFound = true;
0261             m_logicalNeighbors.removeAt(index);
0262         }
0263     }
0264     if (oldPiecesFound && newPiece) //newPiece == 0 allows to just drop the old pieces
0265         m_logicalNeighbors += newPiece;
0266 }
0267 
0268 void Palapeli::Piece::announceReplaced(Palapeli::Piece* replacement)
0269 {
0270     Q_EMIT replacedBy(replacement);
0271     deleteLater();
0272 }
0273 
0274 void Palapeli::Piece::addAtomicSize(const QSize& size)
0275 {
0276     m_atomicSize = m_atomicSize.expandedTo(size);
0277 }
0278 
0279 QSize Palapeli::Piece::atomicSize() const
0280 {
0281     return m_atomicSize;
0282 }
0283 
0284 //END internal datastructures
0285 
0286 void Palapeli::Piece::setPlace(const QPointF& topLeft, int x, int y,
0287                 const QSizeF& area, bool random)
0288 {
0289     const QRectF b = sceneBareBoundingRect();
0290     const QSizeF pieceSize = b.size();
0291     QPointF areaOffset;
0292     // QPoint pieceOffset = m_pieceItem->offset().toPoint();
0293     if (random) {
0294         int dx = area.width() - pieceSize.width();
0295         int dy = area.height() - pieceSize.height();
0296                 auto *generator = QRandomGenerator::global();
0297         areaOffset = QPointF(   // Place the piece randomly in the cell.
0298                         (dx > 0) ? (generator->bounded(dx)) : 0,    // Avoid division by 0.
0299                         (dy > 0) ? (generator->bounded(dy)) : 0);
0300     }
0301     else {
0302         areaOffset = QPointF(   // Center the piece in the cell.
0303             (area.width() - pieceSize.width())/2.0,
0304             (area.height() - pieceSize.height())/2.0);
0305     }
0306     const QPointF gridPos(x * area.width(), y * area.height());
0307     setPos(topLeft + gridPos + areaOffset - m_offset);  // Move it.
0308 }
0309 
0310 //BEGIN mouse interaction
0311 
0312 bool Palapeli::Piece::isSelected() const
0313 {
0314     return m_pieceItem->isSelected();
0315 }
0316 
0317 void Palapeli::Piece::setSelected(bool selected)
0318 {
0319     m_pieceItem->setSelected(selected);
0320 }
0321 
0322 void Palapeli::Piece::startClick()
0323 {
0324     m_pieceItem->setCursor(Qt::ClosedHandCursor);   // Button pressed.
0325 }
0326 
0327 void Palapeli::Piece::endClick()
0328 {
0329     m_pieceItem->setCursor(Qt::OpenHandCursor); // Button released.
0330 }
0331 
0332 Palapeli::Piece* Palapeli::Piece::fromSelectedItem(QGraphicsItem* item)
0333 {
0334     //We expect: item == piece->m_pieceItem && item->parentItem() == piece
0335     return qgraphicsitem_cast<Palapeli::Piece*>(item->parentItem());
0336 }
0337 
0338 void Palapeli::Piece::beginMove()
0339 {
0340     static int zValue = 0;
0341     setZValue(++zValue); //move piece to top
0342     m_pieceItem->setCursor(Qt::ClosedHandCursor);
0343 }
0344 
0345 void Palapeli::Piece::doMove()
0346 {
0347     Palapeli::Scene* scene = qobject_cast<Palapeli::Scene*>(this->scene());
0348     if (scene) {
0349         scene->validatePiecePosition(this);
0350         Q_EMIT moved(false);    // Still moving.
0351     }
0352 }
0353 
0354 void Palapeli::Piece::endMove()
0355 {
0356     m_pieceItem->setCursor(Qt::OpenHandCursor);
0357     Q_EMIT moved(true);     // Finishd moving.
0358 }
0359 
0360 //END mouse interaction
0361 
0362 
0363 //
0364 
0365 #include "moc_piece.cpp"
0366 #include "moc_piece_p.cpp"