File indexing completed on 2023-09-24 08:14:02
0001 /* 0002 This file is part of the KDE games kwin4 program 0003 SPDX-FileCopyrightText: 2006 Martin Heni <kde@heni-online.de> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kwin4view.h" 0009 0010 // own 0011 #include "displaygame.h" 0012 #include "displayintro.h" 0013 #include "kfourinline_debug.h" 0014 #include "reflectiongraphicsscene.h" 0015 #include "score.h" 0016 #include "spritenotify.h" 0017 // KDEGames 0018 #define USE_UNSTABLE_LIBKDEGAMESPRIVATE_API 0019 #include <libkdegamesprivate/kgame/kplayer.h> 0020 // Qt 0021 #include <QColor> 0022 #include <QElapsedTimer> 0023 #include <QEvent> 0024 // Std 0025 #include <cmath> 0026 0027 // How many time measurements for average 0028 #define MEASUREMENT_LIST_SIZE 50 0029 // How many warnings until reflections are switched off 0030 #define WARNING_MAX_COUNT 5 0031 // How many milliseconds rounding error 0032 #define MEASUREMENT_ROUNDING_ERROR 5 0033 0034 // Constructor for the view 0035 KWin4View::KWin4View(int updateTime, const QSize &size, ReflectionGraphicsScene *scene, ThemeManager *theme, QWidget *parent) 0036 : Themeable(QStringLiteral("theview"), theme) 0037 , QGraphicsView(scene, parent) 0038 { 0039 // Store attributes 0040 mScene = scene; 0041 mTheme = theme; 0042 mDefaultUpdateTime = updateTime; 0043 mSlowDownFactor = 1.0; 0044 mSlowCnt = 0; 0045 mReflectPhase = 0; 0046 0047 // We do not need scrolling so switch it off 0048 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0049 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0050 setFrameStyle(QFrame::NoFrame); 0051 setCacheMode(QGraphicsView::CacheBackground); 0052 // setAlignment(Qt::AlignHCenter); 0053 0054 // setViewportUpdateMode(QGraphicsView::FullViewportUpdate); 0055 setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); 0056 setOptimizationFlags(QGraphicsView::DontSavePainterState | QGraphicsView::DontAdjustForAntialiasing); 0057 0058 viewport()->setMouseTracking(true); 0059 setMouseTracking(true); 0060 0061 // Choose a background color 0062 scene->setBackgroundBrush(QColor(0, 0, 128)); 0063 0064 mTimer = new QTimer(this); 0065 connect(mTimer, &QTimer::timeout, this, &KWin4View::updateAndAdvance); 0066 mTimer->start(mDefaultUpdateTime); 0067 0068 // Game status 0069 mIsRunning = false; 0070 0071 // Queue 0072 mThemeQueue.clear(); 0073 mThemeOffset.clear(); 0074 0075 // Set size and position of the view and the canvas: 0076 // they are resized once a level is loaded 0077 resize(size); 0078 scene->setSceneRect(0, 0, this->width(), this->height()); 0079 adjustSize(); 0080 0081 // Interact with user 0082 setInteractive(true); 0083 0084 // Scale theme 0085 mTheme->rescale(this->width(), QPoint(0, 0)); 0086 0087 // Start with the intro display 0088 mGameDisplay = nullptr; 0089 mIntroDisplay = nullptr; 0090 0091 // Reflections 0092 mReflectionSprite = new QGraphicsPixmapItem(); 0093 scene->addItem(mReflectionSprite); 0094 mReflectionSprite->setZValue(1000.0); 0095 mReflectionSprite->hide(); 0096 0097 // Debug 0098 mFrameSprite = new QGraphicsTextItem(); 0099 scene->addItem(mFrameSprite); 0100 mFrameSprite->setPos(QPointF(0.0, 0.0)); 0101 mFrameSprite->setZValue(1000.0); 0102 if (global_debug > 0) 0103 mFrameSprite->show(); 0104 else 0105 mFrameSprite->hide(); 0106 0107 // Skip the intro? 0108 if (!global_skip_intro) { 0109 mIntroDisplay = new DisplayIntro(scene, mTheme, this); 0110 connect(mIntroDisplay, &DisplayIntro::signalQuickStart, this, &KWin4View::signalQuickStart); 0111 mIntroDisplay->start(); 0112 } 0113 } 0114 0115 // Destruct the view object 0116 KWin4View::~KWin4View() 0117 { 0118 delete mIntroDisplay; 0119 delete mGameDisplay; 0120 if (global_debug > 0) 0121 qCDebug(KFOURINLINE_LOG) << "TRACKING" << hasMouseTracking() << "and" << viewport()->hasMouseTracking(); 0122 delete mFrameSprite; 0123 delete mReflectionSprite; 0124 } 0125 0126 // Main themeable function. Called for any theme change. 0127 void KWin4View::changeTheme() 0128 { 0129 if (global_debug > 0) 0130 qCDebug(KFOURINLINE_LOG) << "CHANGE THEME IN VIEW ... resetting slow counter"; 0131 mDrawTimes.clear(); 0132 mSlowDownFactor = 1.0; 0133 mSlowCnt = 0; 0134 mTimer->setInterval(int(mDefaultUpdateTime * mSlowDownFactor)); 0135 } 0136 0137 // Advance and update canvas/scene 0138 void KWin4View::updateAndAdvance() 0139 { 0140 // Time measurement (maybe remove static at some point) 0141 static bool first = true; 0142 static QElapsedTimer time; 0143 int elapsed = time.elapsed(); 0144 if (first) { 0145 elapsed = 0; 0146 first = false; 0147 } 0148 time.restart(); 0149 0150 // Time display 0151 mDrawTimes.append(elapsed); 0152 if (mDrawTimes.size() > MEASUREMENT_LIST_SIZE) 0153 mDrawTimes.removeFirst(); 0154 double avg = 0.0; 0155 for (int i = 0; i < mDrawTimes.size(); i++) 0156 avg += mDrawTimes[i]; 0157 avg /= mDrawTimes.size(); 0158 0159 // Set debug sprite 0160 if (global_debug > 0) { 0161 mFrameSprite->setPlainText(QStringLiteral("CurrentUpdate: %1 ms AverageUpdate%2 ms DefaultUpdate: %3*%4 ms") 0162 .arg(elapsed) 0163 .arg(int(avg)) 0164 .arg(mDefaultUpdateTime) 0165 .arg(mSlowDownFactor)); 0166 } 0167 0168 // Dynamic update of the graphics advance and update speed 0169 if (mDrawTimes.size() == MEASUREMENT_LIST_SIZE && avg > mDefaultUpdateTime * mSlowDownFactor + MEASUREMENT_ROUNDING_ERROR) { 0170 mSlowCnt++; 0171 qCDebug(KFOURINLINE_LOG) << "Warning " << mSlowCnt << " avg=" << avg; 0172 mDrawTimes.clear(); 0173 if (mSlowCnt > WARNING_MAX_COUNT) { 0174 mSlowDownFactor = double(MEASUREMENT_ROUNDING_ERROR + avg) / double(mDefaultUpdateTime); 0175 mSlowCnt = 0; 0176 mTimer->setInterval(int(mDefaultUpdateTime * mSlowDownFactor)); 0177 0178 qCDebug(KFOURINLINE_LOG) << "SLOW COMPUTER WARNING: Decreasing graphics update speed " << mDefaultUpdateTime * mSlowDownFactor 0179 << "ms. Maybe switch off reflections."; 0180 } 0181 } 0182 0183 // Scene advance 0184 scene()->advance(); 0185 // QGV takes care of updating dirty rects, no need to call update or the whole scene is dirtied and repainted 0186 // scene()->update(); 0187 0188 // ==================================================================================== 0189 // Reflections need to be done in the view otherwise the update's go wrong 0190 if (mReflectionRect.width() > 0 && mReflectionRect.height() > 0) { 0191 // Draw reflection in steps to save processing power 0192 if (mReflectPhase == 0) { 0193 mReflectImage = QImage(mReflectionRect.width(), mReflectionRect.height(), QImage::Format_ARGB32); 0194 mReflectImage.fill(Qt::transparent); 0195 QPainter imagePainter(&mReflectImage); 0196 // imagePainter.fillRect(image.rect(),QBrush(Qt::red)); 0197 0198 // Turn on all optimizations 0199 imagePainter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform, false); 0200 imagePainter.setClipping(true); 0201 imagePainter.setWorldTransform(QTransform(1.0, 0.0, 0.0, -1.0, 0.0, mReflectImage.height())); 0202 QRect source = QRect(mReflectionRect.x(), mReflectionRect.y() - mReflectImage.height(), mReflectImage.width(), mReflectImage.height()); 0203 0204 bool vis = mReflectionSprite->isVisible(); 0205 mReflectionSprite->hide(); 0206 dynamic_cast<ReflectionGraphicsScene *>(scene())->setBackground(false); 0207 scene()->render(&imagePainter, mReflectImage.rect(), source, Qt::IgnoreAspectRatio); 0208 dynamic_cast<ReflectionGraphicsScene *>(scene())->setBackground(true); 0209 if (vis) 0210 mReflectionSprite->show(); 0211 mReflectPhase = 1; 0212 } 0213 // Draw reflection in steps to save processing power 0214 else if (mReflectPhase == 1) { 0215 // Semi transparent 0216 QPainter imagePainter(&mReflectImage); 0217 imagePainter.setTransform(QTransform()); 0218 imagePainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); 0219 imagePainter.drawImage(0, 0, mGradientImage); 0220 mReflectPhase = 2; 0221 } 0222 // Draw reflection in steps to save processing power 0223 else if (mReflectPhase == 2) { 0224 // Set to sprite 0225 QPixmap pm = QPixmap::fromImage(mReflectImage); 0226 mReflectionSprite->setPixmap(pm); 0227 mReflectionSprite->update(); 0228 mReflectPhase = 0; 0229 } 0230 } 0231 // ==================================================================================== 0232 } 0233 0234 // Define the reflection 0235 void KWin4View::setReflection(int x, int y, int width, int height) 0236 { 0237 mReflectionRect = QRect(x, y, width, height); 0238 0239 QPoint p1, p2; 0240 p2.setY(height); 0241 mGradient = QLinearGradient(p1, p2); 0242 mGradient.setColorAt(0, QColor(0, 0, 0, 100)); 0243 mGradient.setColorAt(1, Qt::transparent); 0244 0245 qCDebug(KFOURINLINE_LOG) << "Set reflection " << x << " " << y << " " << width << " " << height; 0246 0247 mGradientImage = QImage(width, height, QImage::Format_ARGB32); 0248 mGradientImage.fill(Qt::transparent); 0249 QPainter p(&mGradientImage); 0250 p.fillRect(0, 0, width, height, mGradient); 0251 p.end(); 0252 0253 mReflectionSprite->setPos(x, y); 0254 if (width > 0 && height > 0) { 0255 mReflectionSprite->show(); 0256 } else { 0257 mReflectionSprite->hide(); 0258 } 0259 } 0260 0261 // QGV drawItems function (for debug time measurements) 0262 void KWin4View::drawItems(QPainter *painter, int numItems, QGraphicsItem *items[], const QStyleOptionGraphicsItem options[]) 0263 { 0264 QGraphicsView::drawItems(painter, numItems, items, options); 0265 } 0266 0267 // Stop intro display and init game display 0268 void KWin4View::initGame(Score *scoreData) 0269 { 0270 qCDebug(KFOURINLINE_LOG) << "KWin4View::initGame"; 0271 0272 // For better performance disable mouse tracking now 0273 viewport()->setMouseTracking(false); 0274 setMouseTracking(false); 0275 0276 delete mIntroDisplay; 0277 mIntroDisplay = nullptr; 0278 if (!mGameDisplay) { 0279 mGameDisplay = new DisplayGame(mScene, mTheme, this); 0280 } 0281 mGameDisplay->start(); 0282 0283 // Connect score and score sprite 0284 scoreData->setDisplay(mGameDisplay->score()); 0285 0286 mIsRunning = true; 0287 } 0288 0289 // End the game 0290 void KWin4View::endGame() 0291 { 0292 mIsRunning = false; 0293 mGameDisplay->displayEnd(); 0294 } 0295 0296 // Slot called by the framework when the view is resized. 0297 void KWin4View::resizeEvent(QResizeEvent *e) 0298 { 0299 if (global_debug > 2) 0300 qCDebug(KFOURINLINE_LOG) << "RESIZE EVENT" << e->size() << "oldSize=" << e->oldSize(); 0301 0302 // Test to prevent double resizing 0303 // if (QWidget::testAttribute(Qt::WA_PendingResizeEvent)) 0304 // { 0305 // return; 0306 // } 0307 0308 double diffW = double(e->oldSize().width() - e->size().width()); 0309 double diffH = double(e->oldSize().height() - e->size().height()); 0310 double delta = fabs(diffW) + fabs(diffH); 0311 0312 // Adapt the canvas size to the window size 0313 if (scene()) { 0314 scene()->setSceneRect(0, 0, e->size().width(), e->size().height()); 0315 } 0316 QSizeF size = QSizeF(e->size()); 0317 0318 // Rescale on minimum fitting aspect ratio either width or height limiting 0319 double width = 0.0; 0320 double aspect = size.width() / size.height(); 0321 QPoint offset; 0322 0323 // Scale width: 0324 // Ideal size would be: 'width'*'height' 0325 // Offset in width is (e->size().width()-width)/2, offset in height is zero 0326 if (aspect > mTheme->aspectRatio()) { 0327 width = e->size().height() * mTheme->aspectRatio(); 0328 offset = QPoint(int((e->size().width() - width) / 2.0), 0); 0329 } 0330 // Scale height: 0331 // 'height' = width/mTheme->aspectRatio() 0332 // Ideal size would be: 'width'*'height': 0333 // Offset in height is (e->size().height()-width/mTheme->aspectRatio())/2, offset in width is zero 0334 else { 0335 width = e->size().width(); // Scale height 0336 offset = QPoint(0, int((e->size().height() - width / mTheme->aspectRatio()) / 2.0)); 0337 } 0338 0339 // Pixel rescale 0340 double oldScale = mTheme->getScale(); 0341 0342 // resetTransform(); 0343 QTransform transform; 0344 if (width > oldScale) { 0345 transform.scale(double(width / oldScale), double(width / oldScale)); 0346 } 0347 setTransform(transform); 0348 0349 mThemeQueue.prepend(int(width)); 0350 mThemeOffset.prepend(offset); 0351 if (global_debug > 2) 0352 qCDebug(KFOURINLINE_LOG) << "Quequed resize, aspect=" << aspect << "theme aspect=" << mTheme->aspectRatio(); 0353 0354 long queueDelay = 0; 0355 if (delta < 15) 0356 queueDelay = 750; 0357 else if (delta < 35) 0358 queueDelay = 500; 0359 0360 QTimer::singleShot(queueDelay, this, &KWin4View::rescaleTheme); 0361 } 0362 0363 // Rescale the theme (update theme SVG graphics) from the theme list 0364 void KWin4View::rescaleTheme() 0365 { 0366 if (mThemeQueue.size() == 0) { 0367 if (global_debug > 2) 0368 qCDebug(KFOURINLINE_LOG) << "***************** Swallowing rescale event ***********************"; 0369 return; 0370 } 0371 0372 QElapsedTimer t; 0373 t.start(); 0374 0375 resetTransform(); 0376 0377 int width = mThemeQueue.first(); 0378 QPoint offset = mThemeOffset.first(); 0379 if (global_debug > 2) 0380 qCDebug(KFOURINLINE_LOG) << "Theme queue size=" << mThemeQueue.size() << "Rescale width to" << width << " offset " << offset; 0381 mThemeQueue.clear(); 0382 mThemeOffset.clear(); 0383 mTheme->rescale(width, offset); 0384 0385 if (global_debug > 2) 0386 qCDebug(KFOURINLINE_LOG) << "Time elapsed: " << t.elapsed() << "ms"; 0387 } 0388 0389 // This slot is called when a mouse key is pressed. As the mouse is used as 0390 // input for all players. It is called to generate a player move out of a mouse input, i.e. 0391 // it converts a QMouseEvent into a move for the game. 0392 void KWin4View::mouseInput(KGameIO *input, QDataStream &stream, QMouseEvent *mouse, bool *eatevent) 0393 { 0394 // Only react to mouse pressed not released 0395 if (mouse->type() != QEvent::MouseButtonPress) 0396 return; 0397 if (mouse->button() != Qt::LeftButton) 0398 return; 0399 if (!mIsRunning) 0400 return; 0401 0402 // Our player 0403 KPlayer *player = input->player(); 0404 if (!player->myTurn()) { 0405 // qCDebug(KFOURINLINE_LOG) <<" Kwin4View::TODO wrongPlayer"; 0406 // *eatevent=wrongPlayer(player,KGameIO::MouseIO); 0407 return; 0408 } 0409 0410 // Calculate movement position from mouse position 0411 int x = -1; 0412 if (mGameDisplay) 0413 x = mGameDisplay->mapMouseToMove(mouse->pos()); 0414 if (x < 0) 0415 return; 0416 0417 // Create a game move (pl id and move coordinate) 0418 qint32 move = x; 0419 qint32 pl = player->userId(); 0420 stream << pl << move; 0421 *eatevent = true; 0422 } 0423 0424 // This slot is called when a key event is received. It then produces a 0425 // valid move for the game. 0426 // This is analogous to the mouse event only it is called when a key is 0427 // pressed. 0428 void KWin4View::keyInput(KGameIO *input, QDataStream &stream, QKeyEvent *key, bool *eatevent) 0429 { 0430 // Ignore non running 0431 if (!mIsRunning) 0432 return; 0433 0434 // Ignore non key press 0435 if (key->type() != QEvent::KeyPress) 0436 return; 0437 0438 // Check key code 0439 int code = key->key(); 0440 if (code < Qt::Key_1 || code > Qt::Key_7) 0441 return; 0442 0443 // Our player 0444 KPlayer *player = input->player(); 0445 if (!player->myTurn()) { 0446 // qCDebug(KFOURINLINE_LOG) <<" Kwin4View::TODO wrongPlayer"; 0447 // *eatevent=wrongPlayer(player,KGameIO::KeyIO); 0448 return; 0449 } 0450 0451 // Create a valid game move (player id and movement position) 0452 qint32 move = code - Qt::Key_1; 0453 qint32 pl = player->userId(); 0454 stream << pl << move; 0455 *eatevent = true; 0456 } 0457 0458 // Displays a move on the game board. 0459 void KWin4View::displayMove(int x, int y, int color, int xarrow, int colorarrow, int no, bool animation) 0460 { 0461 mGameDisplay->displayArrow(xarrow, colorarrow); 0462 // animation only if no redo 0463 SpriteNotify *notify = mGameDisplay->displayPiece(x, y, color, no, animation); 0464 if (notify && animation) { 0465 QObject::disconnect(notify, &SpriteNotify::signalNotify, this, &KWin4View::moveDone); 0466 connect(notify, &SpriteNotify::signalNotify, this, &KWin4View::moveDone); 0467 } 0468 mGameDisplay->displayHint(0, 0, false); 0469 } 0470 0471 // Display a star of the given sprite number 0472 void KWin4View::displayStar(int x, int y, int no) 0473 { 0474 mGameDisplay->displayStar(x, y, no); 0475 } 0476 0477 // Display a hint on the board 0478 void KWin4View::displayHint(int x, int y) 0479 { 0480 mGameDisplay->displayHint(x, y, true); 0481 } 0482 0483 // Slot called when a sprite animation move is done. 0484 void KWin4View::moveDone(QGraphicsItem * /*item*/, int mode) 0485 { 0486 Q_EMIT signalMoveDone(mode); 0487 } 0488 0489 bool KWin4View::viewportEvent(QEvent *event) 0490 { 0491 if (mIntroDisplay) 0492 mIntroDisplay->viewEvent(event); 0493 return QGraphicsView::viewportEvent(event); 0494 } 0495 0496 #include "moc_kwin4view.cpp"