File indexing completed on 2024-04-28 04:02:09

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