1/***************************************************************************
2 * Copyright (C) 1999-2006 by Éric Bischoff <ebischoff@nerim.net> *
3 * Copyright (C) 2007-2008 by Albert Astals Cid <aacid@kde.org> *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 ***************************************************************************/
10
11/* Play ground widget */
12
13#include "playground.h"
14
15#include <klocale.h>
16#include <kstandarddirs.h>
17#include <kconfiggroup.h>
18#include <kdebug.h>
19
20#include <QCursor>
21#include <QDataStream>
22#include <QDomDocument>
23#include <QFile>
24#include <QGraphicsSvgItem>
25#include <QMouseEvent>
26#include <QPainter>
27#include <QPrinter>
28#include <QFileInfo>
29
30#include <kstandardaction.h>
31#include <kactioncollection.h>
32#include <kstandardshortcut.h>
33#include <kicon.h>
34#include <QAction>
35
36#include "action.h"
37#include "toplevel.h"
38#include "todraw.h"
39
40static const char *saveGameTextScaleTextMode = "KTuberlingSaveGameV2";
41static const char *saveGameTextTextMode = "KTuberlingSaveGameV3";
42static const char *saveGameText = "KTuberlingSaveGameV4";
43
44// Constructor
45PlayGround::PlayGround(TopLevel *parent)
46 : QGraphicsView(parent), m_dragItem(0), m_nextZValue(1), m_lockAspect(false)
47{
48 m_topLevel = parent;
49 setFrameStyle(QFrame::NoFrame);
50 setOptimizationFlag(QGraphicsView::DontSavePainterState, true); // all items here save the painter state
51 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
52 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
53}
54
55// Destructor
56PlayGround::~PlayGround()
57{
58 foreach (const SceneData &data, m_scenes)
59 {
60 delete data.scene;
61 delete data.undoStack;
62 }
63}
64
65// Reset the play ground
66void PlayGround::reset()
67{
68
69 foreach(QGraphicsItem *item, scene()->items())
70 {
71 ToDraw *currentObject = qgraphicsitem_cast<ToDraw *>(item);
72 delete currentObject;
73 }
74
75 undoStack()->clear();
76}
77
78// Save objects laid down on the editable area
79bool PlayGround::saveAs(const QString & name)
80{
81 QFile f(name);
82 if (!f.open( QIODevice::WriteOnly ) )
83 return false;
84
85 QFileInfo gameBoard(m_gameboardFile);
86 QDataStream out(&f);
87 out.setVersion(QDataStream::Qt_4_5);
88 out << QString::fromLatin1(saveGameText);
89 out << gameBoard.fileName();
90 foreach(QGraphicsItem *item, scene()->items())
91 {
92 ToDraw *currentObject = qgraphicsitem_cast<ToDraw *>(item);
93 if (currentObject != NULL) currentObject->save(out);
94 }
95
96 return (f.error() == QFile::NoError);
97}
98
99// Print gameboard's picture
100bool PlayGround::printPicture(QPrinter &printer)
101{
102 QPainter artist;
103 QPixmap picture(getPicture());
104
105 if (!artist.begin(&printer)) return false;
106 artist.drawPixmap(QPoint(32, 32), picture);
107 if (!artist.end()) return false;
108 return true;
109}
110
111// Get a pixmap containing the current picture
112QPixmap PlayGround::getPicture()
113{
114 QPixmap result(mapFromScene(backgroundRect()).boundingRect().size());
115 QPainter artist(&result);
116 scene()->render(&artist, QRectF(), backgroundRect(), Qt::IgnoreAspectRatio);
117 artist.end();
118 return result;
119}
120
121void PlayGround::connectRedoAction(QAction *action)
122{
123 connect(action, SIGNAL(triggered()), &m_undoGroup, SLOT(redo()));
124 connect(&m_undoGroup, SIGNAL(canRedoChanged(bool)), action, SLOT(setEnabled(bool)));
125}
126
127void PlayGround::connectUndoAction(QAction *action)
128{
129 connect(action, SIGNAL(triggered()), &m_undoGroup, SLOT(undo()));
130 connect(&m_undoGroup, SIGNAL(canUndoChanged(bool)), action, SLOT(setEnabled(bool)));
131}
132
133// Mouse pressed event
134void PlayGround::mousePressEvent(QMouseEvent *event)
135{
136 if (m_gameboardFile.isEmpty()) return;
137
138 if (event->button() != Qt::LeftButton) return;
139
140 if (m_dragItem) placeDraggedItem(event->pos());
141 else if (!m_pickedElement.isNull()) placeNewItem(event->pos());
142 else
143 {
144 // see if the user clicked on the warehouse of items
145 QPointF scenePos = mapToScene(event->pos());
146 QMap<QString, QString>::const_iterator it, itEnd;
147 it = m_objectsNameSound.constBegin();
148 itEnd = m_objectsNameSound.constEnd();
149 QString foundElem;
150 QRectF bounds;
151 for( ; foundElem.isNull() && it != itEnd; ++it)
152 {
153 bounds = m_SvgRenderer.boundsOnElement(it.key());
154 if (bounds.contains(scenePos)) foundElem = it.key();
155 }
156
157 if (!foundElem.isNull())
158 {
159 bounds = mapFromScene(bounds).boundingRect();
160 double objectScale = m_objectsNameRatio.value(foundElem);
161 int width = qRound(bounds.width() * objectScale);
162 int height = qRound(bounds.height() * objectScale);
163
164 m_topLevel->playSound(m_objectsNameSound.value(foundElem));
165 setCursor(QCursor(toPixmap(foundElem, width, height, &m_SvgRenderer)));
166 m_pickedElement = foundElem;
167 }
168 else
169 {
170 // see if the user clicked on an already existent item
171 QGraphicsItem *dragItem = scene()->itemAt(mapToScene(event->pos()));
172 m_dragItem = qgraphicsitem_cast<ToDraw*>(dragItem);
173 if (m_dragItem)
174 {
175 QRectF rect = m_dragItem->unclippedRect();
176 rect = m_dragItem->transform().mapRect(rect);
177 QPolygon poly = mapFromScene(rect);
178 QSize size = poly.boundingRect().size(); // the polygon should be a rect...
179 QString elem = m_dragItem->elementId();
180 setCursor(QCursor(toPixmap(elem, size.width(), size.height(), &m_SvgRenderer)));
181
182 scene()->removeItem(m_dragItem);
183 m_topLevel->playSound(m_objectsNameSound.value(elem));
184 }
185 }
186 }
187}
188
189bool PlayGround::insideBackground(const QSizeF &size, const QPointF &pos) const
190{
191 return backgroundRect().intersects(QRectF(pos, size));
192}
193
194QRectF PlayGround::backgroundRect() const
195{
196 return m_SvgRenderer.boundsOnElement(QLatin1String( "background" ));
197}
198
199void PlayGround::placeDraggedItem(const QPoint &pos)
200{
201 QPointF itemPos = mapToScene(pos);
202 const QSizeF &elementSize = m_dragItem->transform().mapRect(m_dragItem->unclippedRect()).size();
203 itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
204
205 if (insideBackground(elementSize, itemPos))
206 {
207 scene()->addItem(m_dragItem);
208 undoStack()->push(new ActionMove(m_dragItem, itemPos, m_nextZValue, scene()));
209 m_nextZValue++;
210 }
211 else
212 {
213 undoStack()->push(new ActionRemove(m_dragItem, scene()));
214 }
215
216 setCursor(QCursor());
217 m_dragItem = 0;
218}
219
220void PlayGround::placeNewItem(const QPoint &pos)
221{
222 double objectScale = m_objectsNameRatio.value(m_pickedElement);
223 QSizeF elementSize = m_SvgRenderer.boundsOnElement(m_pickedElement).size() * objectScale;
224 QPointF itemPos = mapToScene(pos);
225
226 itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
227
228 if (insideBackground(elementSize, itemPos))
229 {
230 ToDraw *item = new ToDraw;
231 item->setElementId(m_pickedElement);
232 item->setPos(itemPos);
233 item->setSharedRenderer(&m_SvgRenderer);
234 item->setZValue(m_nextZValue);
235 m_nextZValue++;
236 item->scale(objectScale, objectScale);
237
238 undoStack()->push(new ActionAdd(item, scene()));
239 }
240
241 setCursor(QCursor());
242 m_pickedElement.clear();
243}
244
245void PlayGround::recenterView()
246{
247 // Cannot use sceneRect() because sometimes items get placed
248 // with pos() outside rect (e.g. pizza theme)
249 fitInView(QRect(QPoint(0,0), m_SvgRenderer.defaultSize()),
250 m_lockAspect ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio);
251}
252
253QGraphicsScene *PlayGround::scene() const
254{
255 return m_scenes[m_gameboardFile].scene;
256}
257
258QUndoStack *PlayGround::undoStack() const
259{
260 return m_scenes[m_gameboardFile].undoStack;
261}
262
263void PlayGround::resizeEvent(QResizeEvent *)
264{
265 recenterView();
266}
267
268void PlayGround::lockAspectRatio(bool lock)
269{
270 if (m_lockAspect != lock)
271 {
272 m_lockAspect = lock;
273 recenterView();
274 }
275}
276
277bool PlayGround::isAspectRatioLocked() const
278{
279 return m_lockAspect;
280}
281
282// Register the various playgrounds
283void PlayGround::registerPlayGrounds()
284{
285 const QStringList list = KGlobal::dirs()->findAllResources("appdata", QLatin1String( "pics/*.theme" ));
286
287 foreach(const QString &theme, list)
288 {
289 QFile layoutFile(theme);
290 if (layoutFile.open(QIODevice::ReadOnly))
291 {
292 QDomDocument layoutDocument;
293 if (layoutDocument.setContent(&layoutFile))
294 {
295 QString desktop = layoutDocument.documentElement().attribute(QLatin1String( "desktop" ));
296 KConfig c( KStandardDirs::locate("appdata", QLatin1String( "pics/" ) ) + desktop);
297 KConfigGroup cg = c.group("KTuberlingTheme");
298 QString gameboard = layoutDocument.documentElement().attribute(QLatin1String( "gameboard" ));
299 QPixmap pixmap(200,100);
300 pixmap.fill(Qt::transparent);
301 playGroundPixmap(gameboard,pixmap);
302 m_topLevel->registerGameboard(cg.readEntry("Name"), theme, pixmap);
303 }
304 }
305 }
306}
307
308void PlayGround::playGroundPixmap(const QString &playgroundName, QPixmap &pixmap)
309{
310 m_SvgRenderer.load(KStandardDirs::locate("appdata", QLatin1String( "pics/" ) + playgroundName));
311 QPainter painter(&pixmap);
312 m_SvgRenderer.render(&painter,QLatin1String( "background" ));
313}
314
315// Load background and draggable objects masks
316bool PlayGround::loadPlayGround(const QString &gameboardFile)
317{
318 QDomNodeList playGroundsList,
319 editableAreasList, objectsList,
320 gameAreasList, maskAreasList, soundNamesList, labelsList;
321 QDomElement playGroundElement,
322 editableAreaElement, objectElement,
323 gameAreaElement, maskAreaElement, soundNameElement, labelElement;
324 QDomAttr gameboardAttribute, masksAttribute,
325 leftAttribute, topAttribute, rightAttribute, bottomAttribute,
326 refAttribute;
327
328 QFile layoutFile(gameboardFile);
329 if (!layoutFile.open(QIODevice::ReadOnly)) return false;
330
331 QDomDocument layoutDocument;
332 if (!layoutDocument.setContent(&layoutFile)) return false;
333
334 playGroundElement = layoutDocument.documentElement();
335
336 QString gameboardName = playGroundElement.attribute(QLatin1String( "gameboard" ));
337
338 QColor bgColor = QColor(playGroundElement.attribute(QLatin1String( "bgcolor" ), QLatin1String( "#fff" ) ) );
339 if (!bgColor.isValid())
340 bgColor = Qt::white;
341
342 if (!m_SvgRenderer.load(KStandardDirs::locate("appdata", QLatin1String( "pics/" ) + gameboardName)))
343 return false;
344
345 objectsList = playGroundElement.elementsByTagName(QLatin1String( "object" ));
346 if (objectsList.count() < 1)
347 return false;
348
349 m_objectsNameSound.clear();
350
351 // create scene data if needed
352 if(!m_scenes.contains(gameboardFile))
353 {
354 SceneData &data = m_scenes[gameboardFile];
355 data.scene = new QGraphicsScene();
356 data.undoStack = new QUndoStack();
357
358 QGraphicsSvgItem *background = new QGraphicsSvgItem();
359 background->setPos(QPoint(0,0));
360 background->setSharedRenderer(&m_SvgRenderer);
361 background->setZValue(0);
362 data.scene->addItem(background);
363
364 m_undoGroup.addStack(data.undoStack);
365 }
366
367 for (int decoration = 0; decoration < objectsList.count(); decoration++)
368 {
369 objectElement = (const QDomElement &) objectsList.item(decoration).toElement();
370
371 const QString &objectName = objectElement.attribute(QLatin1String( "name" ));
372 if (m_SvgRenderer.elementExists(objectName))
373 {
374 m_objectsNameSound.insert(objectName, objectElement.attribute(QLatin1String( "sound" )));
375 m_objectsNameRatio.insert(objectName, objectElement.attribute(QLatin1String( "scale" ), QLatin1String( "1" )).toDouble());
376 }
377 else
378 {
379 kWarning() << objectName << "does not exist. Check" << gameboardFile;
380 }
381 }
382
383 setBackgroundBrush(bgColor);
384 m_gameboardFile = gameboardFile;
385 setScene(scene());
386
387 recenterView();
388
389 m_undoGroup.setActiveStack(undoStack());
390
391 return true;
392}
393
394QString PlayGround::currentGameboard() const
395{
396 return m_gameboardFile;
397}
398
399// Load objects and lay them down on the editable area
400PlayGround::LoadError PlayGround::loadFrom(const QString &name)
401{
402 QFile f(name);
403 if (!f.open(QIODevice::ReadOnly))
404 return OtherError;
405
406 QDataStream in(&f);
407 in.setVersion(QDataStream::Qt_4_5);
408
409 bool scale = false;
410 bool reopenInTextMode = false;
411 QString magicText;
412 in >> magicText;
413 if ( QLatin1String( saveGameTextScaleTextMode ) == magicText) {
414 scale = true;
415 reopenInTextMode = true;
416 } else if (QLatin1String( saveGameTextTextMode ) == magicText) {
417 reopenInTextMode = true;
418 } else if ( QLatin1String( saveGameText ) != magicText) {
419 return OldFileVersionError;
420 }
421
422 if (reopenInTextMode) {
423 f.close();
424 if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
425 return OtherError;
426 in.setDevice(&f);
427 in.setVersion(QDataStream::Qt_4_5);
428 in >> magicText;
429 }
430
431 sceneRect();
432
433 if (in.atEnd())
434 return OtherError;
435
436 QString board;
437 in >> board;
438
439 qreal xFactor = 1.0;
440 qreal yFactor = 1.0;
441 m_topLevel->changeGameboard(board);
442
443 reset();
444
445 if (scale) {
446 QSize defaultSize = m_SvgRenderer.defaultSize();
447 QSize currentSize = size();
448 xFactor = (qreal)defaultSize.width() / (qreal)currentSize.width();
449 yFactor = (qreal)defaultSize.height() / (qreal)currentSize.height();
450 }
451
452 while ( !in.atEnd() )
453 {
454 ToDraw *obj = new ToDraw;
455 if (!obj->load(in))
456 {
457 delete obj;
458 return OtherError;
459 }
460 obj->setSharedRenderer(&m_SvgRenderer);
461 double objectScale = m_objectsNameRatio.value(obj->elementId());
462 obj->scale(objectScale, objectScale);
463 if (scale) { // Mimic old behavior
464 QPointF storedPos = obj->pos();
465 storedPos.setX(storedPos.x() * xFactor);
466 storedPos.setY(storedPos.y() * yFactor);
467 obj->setPos(storedPos);
468 }
469 undoStack()->push(new ActionAdd(obj, scene()));
470 }
471 if (f.error() == QFile::NoError) return NoError;
472 else return OtherError;
473}
474
475/* kate: replace-tabs on; indent-width 2; */
476