1 /*
2     This file is part of the KDE games kwin4 program
3     SPDX-FileCopyrightText: 2006 Martin Heni <kde@heni-online.de>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kwin4view.h"
9 
10 // own
11 #include "displayintro.h"
12 #include "displaygame.h"
13 #include "spritenotify.h"
14 #include "score.h"
15 #include "reflectiongraphicsscene.h"
16 #include "kfourinline_debug.h"
17 // KDEGames
18 #define USE_UNSTABLE_LIBKDEGAMESPRIVATE_API
19 #include <libkdegamesprivate/kgame/kplayer.h>
20 // Qt
21 #include <QColor>
22 #include <QEvent>
23 #include <QElapsedTimer>
24 // Std
25 #include <cmath>
26 
27 
28 // How many time measurements for average
29 #define MEASUREMENT_LIST_SIZE  50
30 // How many warnings until reflections are switched off
31 #define WARNING_MAX_COUNT      5
32 // How many milliseconds rounding error
33 #define MEASUREMENT_ROUNDING_ERROR 5
34 
35 
36 // Constructor for the view
KWin4View(int updateTime,const QSize & size,ReflectionGraphicsScene * scene,ThemeManager * theme,QWidget * parent)37 KWin4View::KWin4View(int updateTime,
38                      const QSize &size,
39                      ReflectionGraphicsScene* scene,
40                      ThemeManager* theme,
41                      QWidget* parent)
42           : Themeable(QStringLiteral("theview"), theme), QGraphicsView(scene, parent)
43 {
44   // Store attributes
45   mScene             = scene;
46   mTheme             = theme;
47   mDefaultUpdateTime = updateTime;
48   mSlowDownFactor    = 1.0;
49   mSlowCnt           = 0;
50   mReflectPhase      = 0;
51 
52   // We do not need scrolling so switch it off
53   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
54   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
55   setFrameStyle(QFrame::NoFrame);
56   setCacheMode(QGraphicsView::CacheBackground);
57   //setAlignment(Qt::AlignHCenter);
58 
59   //setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
60   setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
61   setOptimizationFlags(
62                        QGraphicsView::DontSavePainterState |
63                        QGraphicsView::DontAdjustForAntialiasing );
64 
65   viewport()->setMouseTracking(true);
66   setMouseTracking(true);
67 
68   // Choose a background color
69   scene->setBackgroundBrush(QColor(0,0,128));
70 
71 
72   mTimer = new QTimer(this);
73   connect(mTimer, &QTimer::timeout, this, &KWin4View::updateAndAdvance);
74   mTimer->start(mDefaultUpdateTime);
75 
76   // Game status
77   mIsRunning = false;
78 
79   // Queue
80   mThemeQueue.clear();
81   mThemeOffset.clear();
82 
83   // Set size and position of the view and the canvas:
84   // they are resized once a level is loaded
85   resize(size);
86   scene->setSceneRect(0, 0, this->width(), this->height());
87   adjustSize();
88 
89   // Interact with user
90   setInteractive(true);
91 
92   // Scale theme
93   mTheme->rescale(this->width(), QPoint(0,0));
94 
95   // Start with the intro display
96   mGameDisplay  = nullptr;
97   mIntroDisplay  = nullptr;
98 
99 
100   // Reflections
101   mReflectionSprite = new QGraphicsPixmapItem();
102   scene->addItem(mReflectionSprite);
103   mReflectionSprite->setZValue(1000.0);
104   mReflectionSprite->hide();
105 
106   // Debug
107   mFrameSprite = new QGraphicsTextItem();
108   scene->addItem(mFrameSprite);
109   mFrameSprite->setPos(QPointF(0.0, 0.0));
110   mFrameSprite->setZValue(1000.0);
111   if (global_debug > 0) mFrameSprite->show();
112   else mFrameSprite->hide();
113 
114 
115   // Skip the intro?
116   if (!global_skip_intro)
117   {
118     mIntroDisplay = new DisplayIntro(scene, mTheme, this);
119     connect(mIntroDisplay, &DisplayIntro::signalQuickStart, this, &KWin4View::signalQuickStart);
120     mIntroDisplay->start();
121   }
122 }
123 
124 
125 // Destruct the view object
~KWin4View()126 KWin4View::~KWin4View()
127 {
128   delete mIntroDisplay;
129   delete mGameDisplay;
130   if (global_debug>0) qCDebug(KFOURINLINE_LOG) << "TRACKING" << hasMouseTracking() << "and" << viewport()->hasMouseTracking();
131   delete mFrameSprite;
132   delete mReflectionSprite;
133 }
134 
135 // Main themeable function. Called for any theme change.
changeTheme()136 void KWin4View::changeTheme()
137 {
138   if (global_debug > 0) qCDebug(KFOURINLINE_LOG) << "CHANGE THEME IN VIEW ... resetting slow counter";
139   mDrawTimes.clear();
140   mSlowDownFactor = 1.0;
141   mSlowCnt = 0;
142   mTimer->setInterval(int(mDefaultUpdateTime*mSlowDownFactor));
143 }
144 
145 
146 // Advance and update canvas/scene
updateAndAdvance()147 void KWin4View::updateAndAdvance()
148 {
149   // Time measurement (maybe remove static at some point)
150   static bool first = true;
151   static QElapsedTimer time;
152   int elapsed = time.elapsed();
153   if (first) {elapsed = 0;first=false;}
154   time.restart();
155 
156   // Time display
157   mDrawTimes.append(elapsed);
158   if (mDrawTimes.size() > MEASUREMENT_LIST_SIZE) mDrawTimes.removeFirst();
159   double avg = 0.0;
160   for (int i=0; i<mDrawTimes.size(); i++) avg += mDrawTimes[i];
161   avg /= mDrawTimes.size();
162 
163   // Set debug sprite
164   if (global_debug > 0)
165   {
166      mFrameSprite->setPlainText(QStringLiteral("CurrentUpdate: %1 ms  AverageUpdate%2 ms  DefaultUpdate: %3*%4 ms").
167                   arg(elapsed).arg(int(avg)).arg(mDefaultUpdateTime).arg(mSlowDownFactor));
168   }
169 
170 
171   // Dynamic update of the graphics advance and update speed
172   if (mDrawTimes.size() == MEASUREMENT_LIST_SIZE &&
173       avg > mDefaultUpdateTime*mSlowDownFactor+MEASUREMENT_ROUNDING_ERROR)
174   {
175     mSlowCnt++;
176     qCDebug(KFOURINLINE_LOG) << "Warning " << mSlowCnt << " avg=" << avg;
177     mDrawTimes.clear();
178     if (mSlowCnt > WARNING_MAX_COUNT)
179     {
180       mSlowDownFactor = double(MEASUREMENT_ROUNDING_ERROR+avg)/double(mDefaultUpdateTime);
181       mSlowCnt = 0;
182       mTimer->setInterval(int(mDefaultUpdateTime*mSlowDownFactor));
183 
184       qCDebug(KFOURINLINE_LOG) << "SLOW COMPUTER WARNING: Decreasing graphics update speed "
185                << mDefaultUpdateTime*mSlowDownFactor<<"ms. Maybe switch off reflections.";
186     }
187   }
188 
189 
190   // Scene advance
191   scene()->advance();
192   // QGV takes care of updating dirty rects, no need to call update or the whole scene is dirtied and repainted
193   // scene()->update();
194 
195 
196   // ====================================================================================
197   // Reflections need to be done in the view otherwise the update's go wrong
198   if (mReflectionRect.width() >0 && mReflectionRect.height() > 0)
199   {
200     // Draw reflection in steps to save processing power
201     if (mReflectPhase == 0)
202     {
203       mReflectImage = QImage(mReflectionRect.width(), mReflectionRect.height(), QImage::Format_ARGB32);
204       mReflectImage.fill(Qt::transparent);
205       QPainter imagePainter(&mReflectImage);
206       // imagePainter.fillRect(image.rect(),QBrush(Qt::red));
207 
208       //Turn on all optimizations
209       imagePainter.setRenderHints(QPainter::Antialiasing |
210                                   QPainter::TextAntialiasing |
211                                   QPainter::SmoothPixmapTransform, false);
212       imagePainter.setClipping(true);
213       imagePainter.setWorldTransform(QTransform(1.0,0.0,0.0,-1.0,0.0,mReflectImage.height()));
214       QRect source = QRect(mReflectionRect.x(),mReflectionRect.y()-mReflectImage.height(),
215                            mReflectImage.width(), mReflectImage.height());
216 
217       bool vis = mReflectionSprite->isVisible();
218       mReflectionSprite->hide();
219       dynamic_cast<ReflectionGraphicsScene*>(scene())->setBackground(false);
220       scene()->render(&imagePainter, mReflectImage.rect(), source, Qt::IgnoreAspectRatio);
221       dynamic_cast<ReflectionGraphicsScene*>(scene())->setBackground(true);
222       if (vis) mReflectionSprite->show();
223       mReflectPhase = 1;
224     }
225     // Draw reflection in steps to save processing power
226     else if (mReflectPhase == 1)
227     {
228        // Semi transparent
229       QPainter imagePainter(&mReflectImage);
230       imagePainter.setTransform(QTransform());
231       imagePainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
232       imagePainter.drawImage(0,0,mGradientImage);
233       mReflectPhase = 2;
234     }
235     // Draw reflection in steps to save processing power
236     else if (mReflectPhase == 2)
237     {
238        // Set to sprite
239       QPixmap pm = QPixmap::fromImage(mReflectImage);
240       mReflectionSprite->setPixmap(pm);
241       mReflectionSprite->update();
242       mReflectPhase = 0;
243     }
244   }
245   // ====================================================================================
246 
247 }
248 
249 
250 // Define the reflection
setReflection(int x,int y,int width,int height)251 void KWin4View::setReflection(int x, int y, int width, int height)
252 {
253   mReflectionRect = QRect(x,y,width,height);
254 
255   QPoint p1, p2;
256   p2.setY(height);
257   mGradient = QLinearGradient(p1, p2);
258   mGradient.setColorAt(0, QColor(0, 0, 0, 100));
259   mGradient.setColorAt(1, Qt::transparent);
260 
261   qCDebug(KFOURINLINE_LOG) << "Set reflection "<< x << " " << y << " " << width << " " << height ;
262 
263   mGradientImage = QImage(width, height, QImage::Format_ARGB32);
264   mGradientImage.fill(Qt::transparent);
265   QPainter p( &mGradientImage );
266   p.fillRect(0,0,width, height, mGradient);
267   p.end();
268 
269   mReflectionSprite->setPos(x,y);
270   if (width >0 && height > 0)
271   {
272     mReflectionSprite->show();
273   }
274   else
275   {
276     mReflectionSprite->hide();
277   }
278 }
279 
280 
281 // QGV drawItems function (for debug time measurements)
drawItems(QPainter * painter,int numItems,QGraphicsItem * items[],const QStyleOptionGraphicsItem options[])282 void KWin4View::drawItems(QPainter* painter, int numItems, QGraphicsItem* items[], const QStyleOptionGraphicsItem options[])
283 {
284   QGraphicsView::drawItems(painter, numItems, items, options);
285 }
286 
287 
288 // Stop intro display and init game display
initGame(Score * scoreData)289 void KWin4View::initGame(Score* scoreData)
290 {
291   qCDebug(KFOURINLINE_LOG) << "KWin4View::initGame";
292 
293   // For better performance disable mouse tracking now
294   viewport()->setMouseTracking(false);
295   setMouseTracking(false);
296 
297   delete mIntroDisplay;
298   mIntroDisplay = nullptr;
299   if (!mGameDisplay)
300   {
301      mGameDisplay = new DisplayGame(mScene, mTheme, this);
302   }
303   mGameDisplay->start();
304 
305   // Connect score and score sprite
306   scoreData->setDisplay(mGameDisplay->score());
307 
308   mIsRunning = true;
309 }
310 
311 
312 // End the game
endGame()313 void  KWin4View::endGame()
314 {
315   mIsRunning = false;
316   mGameDisplay->displayEnd();
317 }
318 
319 
320 // Slot called by the framework when the view is resized.
resizeEvent(QResizeEvent * e)321 void KWin4View::resizeEvent (QResizeEvent* e)
322 {
323  if (global_debug > 2) qCDebug(KFOURINLINE_LOG) <<"RESIZE EVENT" << e->size() << "oldSize="<< e->oldSize();
324 
325   // Test to prevent double resizing
326   // if (QWidget::testAttribute(Qt::WA_PendingResizeEvent))
327   // {
328   //   return;
329   // }
330 
331   double diffW = double(e->oldSize().width()-e->size().width());
332   double diffH = double(e->oldSize().height()-e->size().height());
333   double delta = fabs(diffW) + fabs(diffH);
334 
335 
336 
337   // Adapt the canvas size to the window size
338   if (scene())
339   {
340     scene()->setSceneRect(0, 0, e->size().width(), e->size().height());
341   }
342   QSizeF size = QSizeF(e->size());
343 
344   // Rescale on minimum fitting aspect ratio either width or height limiting
345   double width = 0.0;
346   double aspect = size.width() / size.height();
347   QPoint offset;
348 
349   // Scale width:
350   // Ideal size would be: 'width'*'height'
351   // Offset in width is (e->size().width()-width)/2, offset in height is zero
352   if (aspect > mTheme->aspectRatio())
353   {
354      width = e->size().height()*mTheme->aspectRatio();
355      offset = QPoint(int((e->size().width()-width)/2.0), 0);
356   }
357   // Scale height:
358   // 'height' = width/mTheme->aspectRatio()
359   // Ideal size would be: 'width'*'height':
360   // Offset in height is (e->size().height()-width/mTheme->aspectRatio())/2, offset in width is zero
361   else
362   {
363      width = e->size().width();  // Scale height
364      offset = QPoint(0, int((e->size().height()-width/mTheme->aspectRatio())/2.0));
365   }
366 
367 
368   // Pixel rescale
369   double oldScale = mTheme->getScale();
370 
371   //resetTransform();
372   QTransform transform;
373   if (width > oldScale)
374   {
375     transform.scale(double(width/oldScale), double(width/oldScale));
376   }
377   setTransform(transform);
378 
379   mThemeQueue.prepend(int(width));
380   mThemeOffset.prepend(offset);
381   if (global_debug > 2) qCDebug(KFOURINLINE_LOG) << "Quequed resize, aspect=" << aspect << "theme aspect="<< mTheme->aspectRatio();
382 
383   long queueDelay = 0;
384   if (delta < 15) queueDelay = 750;
385   else if (delta < 35) queueDelay = 500;
386 
387   QTimer::singleShot(queueDelay, this, &KWin4View::rescaleTheme );
388 }
389 
390 
391 // Rescale the theme (update theme SVG graphics) from the theme list
rescaleTheme()392 void KWin4View::rescaleTheme()
393 {
394   if (mThemeQueue.size() == 0)
395   {
396     if (global_debug > 2) qCDebug(KFOURINLINE_LOG) << "***************** Swallowing rescale event ***********************";
397     return;
398   }
399 
400   QElapsedTimer t;
401   t.start();
402 
403   resetTransform();
404 
405   int width     = mThemeQueue.first();
406   QPoint offset = mThemeOffset.first();
407   if (global_debug > 2) qCDebug(KFOURINLINE_LOG) << "Theme queue size=" << mThemeQueue.size() << "Rescale width to" << width
408                                  << " offset " << offset;
409   mThemeQueue.clear();
410   mThemeOffset.clear();
411   mTheme->rescale(width, offset);
412 
413    if (global_debug > 2) qCDebug(KFOURINLINE_LOG) << "Time elapsed: "<< t.elapsed() << "ms";
414 }
415 
416 
417 
418 
419 // This slot is called when a mouse key is pressed. As the mouse is used as
420 // input for all players. It is called to generate a player move out of a mouse input, i.e.
421 // it converts a QMouseEvent into a move for the game.
mouseInput(KGameIO * input,QDataStream & stream,QMouseEvent * mouse,bool * eatevent)422 void KWin4View::mouseInput(KGameIO* input, QDataStream& stream, QMouseEvent* mouse, bool* eatevent)
423 {
424   // Only react to mouse pressed not released
425   if (mouse->type()   != QEvent::MouseButtonPress ) return;
426   if (mouse->button() != Qt::LeftButton) return ;
427   if (!mIsRunning) return;
428 
429   // Our player
430   KPlayer* player=input->player();
431   if (!player->myTurn())
432   {
433     // qCDebug(KFOURINLINE_LOG) <<" Kwin4View::TODO wrongPlayer";
434   //  *eatevent=wrongPlayer(player,KGameIO::MouseIO);
435     return;
436   }
437 
438   // Calculate movement position from mouse position
439   int x = -1;
440   if (mGameDisplay) x = mGameDisplay->mapMouseToMove(mouse->pos());
441   if (x<0) return;
442 
443   // Create a game move (pl id and move coordinate)
444   qint32 move = x;
445   qint32 pl   = player->userId();
446   stream << pl << move;
447   *eatevent=true;
448 }
449 
450 
451 // This slot is called when a key event is received. It then produces a
452 // valid move for the game.
453 // This is analogous to the mouse event only it is called when a key is
454 // pressed.
keyInput(KGameIO * input,QDataStream & stream,QKeyEvent * key,bool * eatevent)455 void KWin4View::keyInput(KGameIO* input, QDataStream& stream, QKeyEvent* key, bool* eatevent)
456 {
457   // Ignore non running
458   if (!mIsRunning) return;
459 
460   // Ignore non key press
461   if (key->type() != QEvent::KeyPress) return ;
462 
463   // Check key code
464   int code=key->key();
465   if (code< Qt::Key_1 || code> Qt::Key_7) return ;
466 
467 
468   // Our player
469   KPlayer *player=input->player();
470   if (!player->myTurn())
471   {
472     //qCDebug(KFOURINLINE_LOG) <<" Kwin4View::TODO wrongPlayer";
473    // *eatevent=wrongPlayer(player,KGameIO::KeyIO);
474     return;
475   }
476 
477   // Create a valid game move (player id and movement position)
478   qint32 move = code-Qt::Key_1;
479   qint32 pl   = player->userId();
480   stream << pl << move;
481   *eatevent=true;
482 }
483 
484 
485 
486 // Displays a move on the game board.
displayMove(int x,int y,int color,int xarrow,int colorarrow,int no,bool animation)487 void KWin4View::displayMove(int x, int y, int color, int xarrow, int colorarrow, int no, bool animation)
488 {
489   mGameDisplay->displayArrow(xarrow, colorarrow);
490   // animation only if no redo
491   SpriteNotify* notify = mGameDisplay->displayPiece(x, y, color, no, animation);
492   if (notify && animation)
493   {
494     QObject::disconnect(notify,&SpriteNotify::signalNotify,
495                         this,&KWin4View::moveDone);
496     connect(notify, &SpriteNotify::signalNotify, this, &KWin4View::moveDone);
497   }
498   mGameDisplay->displayHint(0,0,false);
499 }
500 
501 
502 // Display a star of the given sprite number
displayStar(int x,int y,int no)503 void KWin4View::displayStar(int x, int y, int no)
504 {
505   mGameDisplay->displayStar(x, y, no);
506 }
507 
508 // Display a hint on the board
displayHint(int x,int y)509 void KWin4View::displayHint(int x, int y)
510 {
511   mGameDisplay->displayHint(x, y, true);
512 }
513 
514 // Slot called when a sprite animation move is done.
moveDone(QGraphicsItem *,int mode)515 void KWin4View::moveDone(QGraphicsItem* /*item*/, int mode)
516 {
517   Q_EMIT signalMoveDone(mode);
518 }
519 
520 
viewportEvent(QEvent * event)521 bool KWin4View::viewportEvent ( QEvent * event )
522 {
523   if (mIntroDisplay) mIntroDisplay->viewEvent(event);
524   return QGraphicsView::viewportEvent(event);
525 }
526 
527 
528 
529