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"