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