File indexing completed on 2023-09-24 11:50:57

0001 /*
0002     SPDX-FileCopyrightText: 1999-2006 √Čric Bischoff <ebischoff@nerim.net>
0003     SPDX-FileCopyrightText: 2007-2008 Albert Astals Cid <aacid@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 /* Play ground widget */
0009 
0010 #include "playground.h"
0011 
0012 #include <KConfig>
0013 #include <KConfigGroup>
0014 #include "ktuberling_debug.h"
0015 
0016 #include <QAction>
0017 #include <QApplication>
0018 #include <QCursor>
0019 #include <QDataStream>
0020 #include <QDir>
0021 #include <QDomDocument>
0022 #include <QFile>
0023 #include <QFileInfo>
0024 #include <QGraphicsSvgItem>
0025 #include <QMouseEvent>
0026 #include <QPainter>
0027 #include <QPagedPaintDevice>
0028 
0029 #include "action.h"
0030 #include "filefactory.h"
0031 #include "todraw.h"
0032 
0033 static const char *saveGameTextScaleTextMode = "KTuberlingSaveGameV2";
0034 static const char *saveGameTextTextMode = "KTuberlingSaveGameV3";
0035 static const char *saveGameText = "KTuberlingSaveGameV4";
0036 
0037 // Constructor
0038 PlayGround::PlayGround(PlayGroundCallbacks *callbacks, QWidget *parent)
0039     : QGraphicsView(parent), m_callbacks(callbacks), m_newItem(nullptr), m_dragItem(nullptr), m_nextZValue(1), m_lockAspect(false), m_allowOnlyDrag(false)
0040 {
0041   setFrameStyle(QFrame::NoFrame);
0042   setOptimizationFlag(QGraphicsView::DontSavePainterState, true); // all items here save the painter state
0043   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0044   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0045   setMouseTracking(true);
0046 }
0047 
0048 // Destructor
0049 PlayGround::~PlayGround()
0050 {
0051   for (const SceneData &data : std::as_const(m_scenes))
0052   {
0053     delete data.scene;
0054     delete data.undoStack;
0055   }
0056 }
0057 
0058 // Reset the play ground
0059 void PlayGround::reset()
0060 {
0061 
0062   const QList<QGraphicsItem*> items = scene()->items();
0063   for(QGraphicsItem *item : items)
0064   {
0065     ToDraw *currentObject = qgraphicsitem_cast<ToDraw *>(item);
0066     delete currentObject;
0067   }
0068 
0069   undoStack()->clear();
0070 }
0071 
0072 // Save objects laid down on the editable area
0073 bool PlayGround::saveAs(const QString & name)
0074 {
0075   QFile f(name);
0076   if (!f.open( QIODevice::WriteOnly ) )
0077       return false;
0078 
0079   QFileInfo gameBoard(m_gameboardFile);
0080   QDataStream out(&f);
0081   out.setVersion(QDataStream::Qt_4_5);
0082   out << QString::fromLatin1(saveGameText);
0083   out << gameBoard.fileName();
0084   const QList<QGraphicsItem*> items = scene()->items();
0085   for(QGraphicsItem *item : items)
0086   {
0087     ToDraw *currentObject = qgraphicsitem_cast<ToDraw *>(item);
0088     if (currentObject != nullptr) currentObject->save(out);
0089   }
0090 
0091   return (f.error() == QFile::NoError);
0092 }
0093 
0094 // Print gameboard's picture
0095 bool PlayGround::printPicture(QPagedPaintDevice &printer)
0096 {
0097   QPainter artist;
0098   QPixmap picture(getPicture());
0099 
0100   if (!artist.begin(&printer)) return false;
0101   artist.drawPixmap(QPoint(32, 32), picture);
0102   if (!artist.end()) return false;
0103   return true;
0104 }
0105 
0106 // Get a pixmap containing the current picture
0107 QPixmap PlayGround::getPicture()
0108 {
0109   QPixmap result(mapFromScene(backgroundRect()).boundingRect().size());
0110   QPainter artist(&result);
0111   scene()->render(&artist, QRectF(), backgroundRect(), Qt::IgnoreAspectRatio);
0112   artist.end();
0113   return result;
0114 }
0115 
0116 void PlayGround::connectRedoAction(QAction *action)
0117 {
0118   connect(action, &QAction::triggered, &m_undoGroup, &QUndoGroup::redo);
0119   connect(&m_undoGroup, &QUndoGroup::canRedoChanged, action, &QAction::setEnabled);
0120 }
0121 
0122 void PlayGround::connectUndoAction(QAction *action)
0123 {
0124   connect(action, &QAction::triggered, &m_undoGroup, &QUndoGroup::undo);
0125   connect(&m_undoGroup, &QUndoGroup::canUndoChanged, action, &QAction::setEnabled);
0126 }
0127 
0128 // Mouse pressed event
0129 void PlayGround::mousePressEvent(QMouseEvent *event)
0130 {
0131   if (m_gameboardFile.isEmpty()) return;
0132 
0133   if (event->button() != Qt::LeftButton) return;
0134 
0135   m_mousePressPos = event->pos();
0136 
0137   if (m_dragItem) placeDraggedItem(event->pos());
0138   else if (m_newItem) placeNewItem(event->pos());
0139   else
0140   {
0141     // see if the user clicked on the warehouse of items
0142     QPointF scenePos = mapToScene(event->pos());
0143     QMap<QString, QString>::const_iterator it, itEnd;
0144     it = m_objectsNameSound.constBegin();
0145     itEnd = m_objectsNameSound.constEnd();
0146     QString foundElem;
0147     QRectF bounds;
0148     for( ; foundElem.isNull() && it != itEnd; ++it)
0149     {
0150       bounds = m_SvgRenderer.boundsOnElement(it.key());
0151       if (bounds.contains(scenePos)) foundElem = it.key();
0152     }
0153 
0154     if (!foundElem.isNull())
0155     {
0156       const double objectScale = m_objectsNameRatio.value(foundElem);
0157       const QSizeF elementSize = m_SvgRenderer.boundsOnElement(foundElem).size() * objectScale;
0158       QPointF itemPos = mapToScene(event->pos());
0159       itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
0160 
0161       m_callbacks->playSound(m_objectsNameSound.value(foundElem));
0162 
0163       m_newItem = new ToDraw;
0164       m_newItem->setBeingDragged(true);
0165       m_newItem->setPos(clipPos(itemPos, m_newItem));
0166       m_newItem->setSharedRenderer(&m_SvgRenderer);
0167       m_newItem->setElementId(foundElem);
0168       m_newItem->setZValue(m_nextZValue);
0169       m_nextZValue++;
0170       m_newItem->setTransform(QTransform::fromScale(objectScale, objectScale));
0171 
0172       scene()->addItem(m_newItem);
0173       setCursor(Qt::BlankCursor);
0174     }
0175     else
0176     {
0177       // see if the user clicked on an already existent item
0178       QGraphicsItem *dragItem = scene()->itemAt(mapToScene(event->pos()), QTransform());
0179       m_dragItem = qgraphicsitem_cast<ToDraw*>(dragItem);
0180       if (m_dragItem)
0181       {
0182         QString elem = m_dragItem->elementId();
0183 
0184         m_callbacks->playSound(m_objectsNameSound.value(elem));
0185         setCursor(Qt::BlankCursor);
0186         m_dragItem->setBeingDragged(true);
0187         m_itemDraggedPos = m_dragItem->pos();
0188 
0189         const QSizeF elementSize = m_dragItem->transform().mapRect(m_dragItem->unclippedRect()).size();
0190         QPointF itemPos = mapToScene(event->pos());
0191         itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
0192         m_dragItem->setPos(clipPos(itemPos, m_dragItem));
0193       }
0194     }
0195   }
0196 }
0197 
0198 void PlayGround::mouseMoveEvent(QMouseEvent *event)
0199 {
0200   ToDraw *movingItem = m_newItem ? m_newItem : m_dragItem;
0201   if (movingItem) {
0202     QPointF itemPos = mapToScene(event->pos());
0203     const QSizeF elementSize = movingItem->transform().mapRect(movingItem->unclippedRect()).size();
0204     itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
0205 
0206     movingItem->setPos(clipPos(itemPos, movingItem));
0207   }
0208 }
0209 
0210 void PlayGround::mouseReleaseEvent(QMouseEvent *event)
0211 {
0212   QPoint point = event->pos() - m_mousePressPos;
0213   if (m_allowOnlyDrag || point.manhattanLength() > qApp->startDragDistance()) {
0214       if (m_dragItem) placeDraggedItem(event->pos());
0215       else if (m_newItem) placeNewItem(event->pos());
0216   }
0217 }
0218 
0219 bool PlayGround::insideBackground(const QSizeF &size, const QPointF &pos) const
0220 {
0221   return backgroundRect().intersects(QRectF(pos, size));
0222 }
0223 
0224 QPointF PlayGround::clipPos(const QPointF &p, ToDraw *item) const
0225 {
0226   const qreal objectScale = m_objectsNameRatio.value(item->elementId());
0227 
0228   QPointF res = p;
0229   res.setX(qMax(qreal(0), res.x()));
0230   res.setY(qMax(qreal(0), res.y()));
0231   res.setX(qMin(m_SvgRenderer.defaultSize().width() - item->boundingRect().width() * objectScale, res.x()));
0232   res.setY(qMin(m_SvgRenderer.defaultSize().height()- item->boundingRect().height() * objectScale, res.y()));
0233   return res;
0234 }
0235 
0236 QRectF PlayGround::backgroundRect() const
0237 {
0238   return m_SvgRenderer.boundsOnElement(QStringLiteral( "background" ));
0239 }
0240 
0241 void PlayGround::placeDraggedItem(const QPoint &pos)
0242 {
0243   QPointF itemPos = mapToScene(pos);
0244   const QSizeF &elementSize = m_dragItem->transform().mapRect(m_dragItem->unclippedRect()).size();
0245   itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
0246 
0247   if (insideBackground(elementSize, itemPos))
0248   {
0249     m_dragItem->setBeingDragged(false);
0250     undoStack()->push(new ActionMove(m_dragItem, m_itemDraggedPos, m_nextZValue, scene()));
0251     m_nextZValue++;
0252   }
0253   else
0254   {
0255     undoStack()->push(new ActionRemove(m_dragItem, m_itemDraggedPos, scene()));
0256   }
0257 
0258   setCursor(QCursor());
0259   m_dragItem = nullptr;
0260 }
0261 
0262 void PlayGround::placeNewItem(const QPoint &pos)
0263 {
0264   const QSizeF elementSize = m_newItem->transform().mapRect(m_newItem->unclippedRect()).size();
0265   QPointF itemPos = mapToScene(pos);
0266   itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
0267   if (insideBackground(elementSize, itemPos))
0268   {
0269     m_newItem->setBeingDragged(false);
0270     undoStack()->push(new ActionAdd(m_newItem, scene()));
0271   } else {
0272     m_newItem->deleteLater();
0273   }
0274   m_newItem = nullptr;
0275   setCursor(QCursor());
0276 }
0277 
0278 void PlayGround::recenterView()
0279 {
0280   // Cannot use sceneRect() because sometimes items get placed
0281   // with pos() outside rect (e.g. pizza theme)
0282   fitInView(QRect(QPoint(0,0), m_SvgRenderer.defaultSize()),
0283       m_lockAspect ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio);
0284 }
0285 
0286 QGraphicsScene *PlayGround::scene() const
0287 {
0288   return m_scenes[m_gameboardFile].scene;
0289 }
0290 
0291 QUndoStack *PlayGround::undoStack() const
0292 {
0293   return m_scenes[m_gameboardFile].undoStack;
0294 }
0295 
0296 void PlayGround::resizeEvent(QResizeEvent *)
0297 {
0298   recenterView();
0299 }
0300 
0301 void PlayGround::lockAspectRatio(bool lock)
0302 {
0303   if (m_lockAspect != lock)
0304   {
0305     m_lockAspect = lock;
0306     recenterView();
0307   }
0308 }
0309 
0310 bool PlayGround::isAspectRatioLocked() const
0311 {
0312   return m_lockAspect;
0313 }
0314 
0315 // Register the various playgrounds
0316 void PlayGround::registerPlayGrounds()
0317 {
0318   QSet<QString> list;
0319   const QStringList dirs = FileFactory::locateAll(QStringLiteral("pics"));
0320   for (const QString &dir : dirs)
0321   {
0322     const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.theme"));
0323     for (const QString &file : fileNames)
0324     {
0325         list << dir + '/' + file;
0326     }
0327   }
0328 
0329   QMultiMap<QString, QPair<QString, QPixmap>> sortedByName;
0330 
0331   for(const QString &theme : std::as_const(list))
0332   {
0333     QFile layoutFile(theme);
0334     if (layoutFile.open(QIODevice::ReadOnly))
0335     {
0336       QDomDocument layoutDocument;
0337       if (layoutDocument.setContent(&layoutFile))
0338       {
0339         QString desktop = layoutDocument.documentElement().attribute(QStringLiteral( "desktop" ));
0340         KConfig c( FileFactory::locate( QLatin1String( "pics/" ) + desktop ) );
0341         KConfigGroup cg = c.group("KTuberlingTheme");
0342         QString gameboard = layoutDocument.documentElement().attribute(QStringLiteral( "gameboard" ));
0343         QPixmap pixmap(200,100);
0344         pixmap.fill(Qt::transparent);
0345         playGroundPixmap(gameboard,pixmap);
0346         sortedByName.insert(cg.readEntry("Name"), QPair<QString, QPixmap>(theme, pixmap));
0347       }
0348     }
0349   }
0350 
0351   for(auto it = sortedByName.begin(); it != sortedByName.end(); ++it) {
0352     m_callbacks->registerGameboard(it.key(), it.value().first, it.value().second);
0353   }
0354 
0355 }
0356 
0357 void PlayGround::playGroundPixmap(const QString &playgroundName, QPixmap &pixmap)
0358 {
0359   m_SvgRenderer.load(FileFactory::locate(QLatin1String( "pics/" ) + playgroundName ));
0360   QPainter painter(&pixmap);
0361   m_SvgRenderer.render(&painter,QStringLiteral( "background" ));
0362 }
0363 
0364 // Load background and draggable objects masks
0365 bool PlayGround::loadPlayGround(const QString &gameboardFile)
0366 {
0367   QFile layoutFile(gameboardFile);
0368   if (!layoutFile.open(QIODevice::ReadOnly)) return false;
0369   QDomDocument layoutDocument;
0370   if (!layoutDocument.setContent(&layoutFile)) return false;
0371 
0372   const QDomElement playGroundElement = layoutDocument.documentElement();
0373 
0374   QString gameboardName = playGroundElement.attribute(QStringLiteral( "gameboard" ));
0375 
0376   QColor bgColor = QColor(playGroundElement.attribute(QStringLiteral( "bgcolor" ), QStringLiteral( "#fff" ) ) );
0377   if (!bgColor.isValid())
0378     bgColor = Qt::white;
0379 
0380   if (!m_SvgRenderer.load(FileFactory::locate( QLatin1String( "pics/" ) + gameboardName )))
0381     return false;
0382 
0383   const QDomNodeList objectsList = playGroundElement.elementsByTagName(QStringLiteral( "object" ));
0384   if (objectsList.count() < 1)
0385     return false;
0386 
0387   m_objectsNameSound.clear();
0388 
0389   // create scene data if needed
0390   if(!m_scenes.contains(gameboardFile))
0391   {
0392     SceneData &data = m_scenes[gameboardFile];
0393     data.scene = new QGraphicsScene();
0394     data.undoStack = new QUndoStack();
0395 
0396     QGraphicsSvgItem *background = new QGraphicsSvgItem();
0397     background->setPos(QPoint(0,0));
0398     background->setSharedRenderer(&m_SvgRenderer);
0399     background->setZValue(0);
0400     data.scene->addItem(background);
0401 
0402     m_undoGroup.addStack(data.undoStack);
0403   }
0404 
0405   for (int decoration = 0; decoration < objectsList.count(); decoration++)
0406   {
0407     const QDomElement objectElement = objectsList.item(decoration).toElement();
0408 
0409     const QString &objectName = objectElement.attribute(QStringLiteral( "name" ));
0410     if (m_SvgRenderer.elementExists(objectName))
0411     {
0412       m_objectsNameSound.insert(objectName, objectElement.attribute(QStringLiteral( "sound" )));
0413       m_objectsNameRatio.insert(objectName, objectElement.attribute(QStringLiteral( "scale" ), QStringLiteral( "1" )).toDouble());
0414     }
0415     else
0416     {
0417       qCWarning(KTUBERLING_LOG) << objectName << "does not exist. Check" << gameboardFile;
0418     }
0419   }
0420 
0421   setBackgroundBrush(bgColor);
0422   m_gameboardFile = gameboardFile;
0423   setScene(scene());
0424 
0425   recenterView();
0426 
0427   m_undoGroup.setActiveStack(undoStack());
0428 
0429   return true;
0430 }
0431 
0432 void PlayGround::setAllowOnlyDrag(bool allowOnlyDrag)
0433 {
0434   m_allowOnlyDrag = allowOnlyDrag;
0435 }
0436 
0437 QString PlayGround::currentGameboard() const
0438 {
0439   return m_gameboardFile;
0440 }
0441 
0442 // Load objects and lay them down on the editable area
0443 PlayGround::LoadError PlayGround::loadFrom(const QString &name)
0444 {
0445   QFile f(name);
0446   if (!f.open(QIODevice::ReadOnly))
0447       return OtherError;
0448 
0449   QDataStream in(&f);
0450   in.setVersion(QDataStream::Qt_4_5);
0451 
0452   bool scale = false;
0453   bool reopenInTextMode = false;
0454   QString magicText;
0455   in >> magicText;
0456   if ( QLatin1String( saveGameTextScaleTextMode ) == magicText) {
0457       scale = true;
0458       reopenInTextMode = true;
0459   } else if (QLatin1String( saveGameTextTextMode ) == magicText) {
0460       reopenInTextMode = true;
0461   } else if ( QLatin1String( saveGameText ) != magicText) {
0462       return OldFileVersionError;
0463   }
0464 
0465   if (reopenInTextMode) {
0466       f.close();
0467       if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
0468         return OtherError;
0469       in.setDevice(&f);
0470       in.setVersion(QDataStream::Qt_4_5);
0471       in >> magicText;
0472   }
0473 
0474   sceneRect();
0475 
0476   if (in.atEnd())
0477     return OtherError;
0478 
0479   QString board;
0480   in >> board;
0481 
0482   qreal xFactor = 1.0;
0483   qreal yFactor = 1.0;
0484   m_callbacks->changeGameboard(board);
0485 
0486   reset();
0487 
0488   if (scale) {
0489     QSize defaultSize = m_SvgRenderer.defaultSize();
0490     QSize currentSize = size();
0491     xFactor = (qreal)defaultSize.width() / (qreal)currentSize.width();
0492     yFactor = (qreal)defaultSize.height() / (qreal)currentSize.height();
0493   }
0494 
0495   while ( !in.atEnd() )
0496   {
0497     ToDraw *obj = new ToDraw;
0498     if (!obj->load(in))
0499     {
0500       delete obj;
0501       return OtherError;
0502     }
0503     obj->setSharedRenderer(&m_SvgRenderer);
0504     double objectScale = m_objectsNameRatio.value(obj->elementId());
0505     obj->setTransform(QTransform::fromScale(objectScale, objectScale));
0506     if (scale) { // Mimic old behavior
0507       QPointF storedPos = obj->pos();
0508       storedPos.setX(storedPos.x() * xFactor);
0509       storedPos.setY(storedPos.y() * yFactor);
0510       obj->setPos(storedPos);
0511     }
0512     scene()->addItem(obj);
0513     undoStack()->push(new ActionAdd(obj, scene()));
0514   }
0515   if (f.error() == QFile::NoError) return NoError;
0516   else return OtherError;
0517 }
0518 
0519 #include "moc_playground.cpp"
0520 
0521 /* kate: replace-tabs on; indent-width 2; */