1 /*
2     This file is part of the game 'KJumpingCube'
3 
4     SPDX-FileCopyrightText: 1998-2000 Matthias Kiefer <matthias.kiefer@gmx.de>
5     SPDX-FileCopyrightText: 2012-2013 Ian Wadhan <iandw.au@gmail.com>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "kcubeboxwidget.h"
11 
12 #include <KgTheme>
13 
14 #include <QTimer>
15 #include <QLabel>
16 #include <QPainter>
17 
18 #include <assert.h>
19 #include <QStandardPaths>
20 
21 #include "prefs.h"
22 
23 #include "kjumpingcube_debug.h"
24 
KCubeBoxWidget(const int d,QWidget * parent)25 KCubeBoxWidget::KCubeBoxWidget (const int d, QWidget *parent)
26         : QWidget (parent),
27 	  m_side          (d),
28 	  m_popup         (new QLabel (this))
29 {
30    qCDebug(KJUMPINGCUBE_LOG) << "CONSTRUCT KCubeBoxWidget: side" << m_side;
31    cubes.clear();
32    init();
33 }
34 
~KCubeBoxWidget()35 KCubeBoxWidget::~KCubeBoxWidget()
36 {
37 }
38 
loadSettings()39 bool KCubeBoxWidget::loadSettings()
40 {
41   qCDebug(KJUMPINGCUBE_LOG) << "LOAD VIEW SETTINGS";
42   bool reColorCubes = ((color1 != Prefs::color1()) ||
43                        (color2 != Prefs::color2()) ||
44                        (color0 != Prefs::color0()));
45 
46   color1 = Prefs::color1();
47   color2 = Prefs::color2();
48   color0 = Prefs::color0();
49 
50   if (Prefs::animationNone()) {
51      cascadeAnimation = None;
52   }
53   else if (Prefs::animationDelay() || (Prefs::animationSpeed() <= 1)) {
54      cascadeAnimation = Darken;
55   }
56   else if (Prefs::animationBlink()) {
57      cascadeAnimation = RapidBlink;
58   }
59   else if (Prefs::animationSpread()) {
60      cascadeAnimation = Scatter;
61   }
62 
63   animationTime = Prefs::animationSpeed() * 150;
64 
65   // NOTE: When the box-size (Prefs::cubeDim()) changes, Game::newGame() calls
66   //       KCubeBoxWidget::loadSettings() first, then KCubeBoxWidget::setDim().
67 
68   if (reColorCubes) {
69      makeStatusPixmaps (sWidth);		// Make new status pixmaps.
70      makeSVGCubes (cubeSize);
71      setColors ();
72   }
73   return reColorCubes;
74 }
75 
reset()76 void KCubeBoxWidget::reset()	// Called if a player wins or requests New game.
77 {
78    for (KCubeWidget * cube : std::as_const(cubes)) {
79       cube->reset();
80    }
81 
82    KCubeWidget::enableClicks(true);
83    currentAnimation = None;
84 }
85 
displayCube(int index,Player owner,int value)86 void KCubeBoxWidget::displayCube (int index, Player owner, int value)
87 {
88    cubes.at(index)->setOwner (owner);
89    cubes.at(index)->setValue (value);
90 }
91 
highlightCube(int index,bool highlight)92 void KCubeBoxWidget::highlightCube (int index, bool highlight)
93 {
94    if (highlight) {
95       cubes.at(index)->setDark();
96    }
97    else {
98       cubes.at(index)->setNeutral();
99    }
100 }
101 
timedCubeHighlight(int index)102 void KCubeBoxWidget::timedCubeHighlight (int index)
103 {
104    if (m_highlighted > 0) {
105       highlightDone();
106    }
107    cubes.at(index)->setDark();
108    m_highlighted = index;
109    m_highlightTimer->start();
110 }
111 
highlightDone()112 void KCubeBoxWidget::highlightDone()
113 {
114    cubes.at(m_highlighted)->setNeutral();
115    m_highlightTimer->stop();
116    m_highlighted = -1;
117 }
118 
setColors()119 void KCubeBoxWidget::setColors ()
120 {
121    for (KCubeWidget * cube : std::as_const(cubes)) {
122       cube->updateColors();
123    }
124 }
125 
setDim(int d)126 void KCubeBoxWidget::setDim(int d)
127 {
128    if (d != m_side) {
129       m_side  = d;
130       initCubes();
131       reCalculateGraphics (width(), height());
132       reset();
133    }
134 }
135 
136 /* ***************************************************************** **
137 **                               slots                               **
138 ** ***************************************************************** */
139 
setWaitCursor()140 void KCubeBoxWidget::setWaitCursor()
141 {
142    setCursor (Qt::BusyCursor);
143 }
144 
setNormalCursor()145 void KCubeBoxWidget::setNormalCursor()
146 {
147    setCursor (Qt::PointingHandCursor);
148 }
149 
checkClick(int x,int y)150 bool KCubeBoxWidget::checkClick (int x, int y)
151 {
152    /* IDW TODO - Remove this from the view OR rewrite it as a MouseEvent().
153     *
154    // IDW TODO - Write a new mouse-click event for KCubeBoxWidget? Remove the
155    //            one that KCubeWidget has?
156    */
157    qCDebug(KJUMPINGCUBE_LOG) << "Emit mouseClick (" << x << y << ")";
158    Q_EMIT mouseClick (x, y);
159    return false;
160 }
161 
162 /* ***************************************************************** **
163 **                   initializing functions                          **
164 ** ***************************************************************** */
init()165 void KCubeBoxWidget::init()
166 {
167    currentAnimation = None;
168    animationSteps = 12;
169    animationCount = 0;
170 
171    setMinimumSize (200, 200);
172    color1 = Prefs::color1();			// Set preferred colors.
173    color2 = Prefs::color2();
174    color0 = Prefs::color0();
175 
176    KgTheme theme((QByteArray()));
177    theme.readFromDesktopFile(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("pics/default.desktop")));
178    svg.load (theme.graphicsPath());
179 
180    initCubes();
181 
182    animationTime = Prefs::animationSpeed() * 150;
183    animationTimer = new QTimer(this);
184 
185    m_highlightTimer = new QTimer(this);
186    m_highlightTimer->setInterval (1500);
187    m_highlighted = -1;
188 
189    connect(animationTimer, &QTimer::timeout, this, &KCubeBoxWidget::nextAnimationStep);
190    connect(m_highlightTimer, &QTimer::timeout, this, &KCubeBoxWidget::highlightDone);
191    setNormalCursor();
192    setPopup();
193 }
194 
initCubes()195 void KCubeBoxWidget::initCubes()
196 {
197    qDeleteAll (cubes);
198    cubes.clear();
199 
200    int nCubes = m_side * m_side;
201    for (int n = 0; n < nCubes; n++) {
202       KCubeWidget * cube = new KCubeWidget (this);
203       cubes.append (cube);
204       cube->setCoordinates (n / m_side, n % m_side, m_side - 1);
205       cube->setPixmaps (&elements);
206       connect(cube, &KCubeWidget::clicked, this, &KCubeBoxWidget::checkClick);
207       cube->show();
208    }
209 }
210 
makeStatusPixmaps(const int width)211 void KCubeBoxWidget::makeStatusPixmaps (const int width)
212 {
213    qreal d, p;
214    QImage status (width, width, QImage::Format_ARGB32_Premultiplied);
215    QPainter s (&status);
216    sWidth = width;
217 
218    d = width/4.0;
219    p = width/2.0;
220    status.fill (0);
221    svg.render (&s, QStringLiteral("player_1"));
222    colorImage (status, color1, width);
223    svg.render (&s, QStringLiteral("lighting"));
224    svg.render (&s, QStringLiteral("pip"), QRectF (p - d/2.0, p - d/2.0, d, d));
225    status1 = QPixmap::fromImage (status);
226 
227    d = width/5.0;
228    p = width/3.0;
229    status.fill (0);
230    svg.render (&s, QStringLiteral("player_2"));
231    colorImage (status, color2, width);
232    svg.render (&s, QStringLiteral("lighting"));
233    svg.render (&s, QStringLiteral("pip"), QRectF (p - d/2.0, p - d/2.0, d, d));
234    svg.render (&s, QStringLiteral("pip"), QRectF (p + p - d/2.0, p + p - d/2.0, d, d));
235    s.end();
236    status2 = QPixmap::fromImage (status);
237 }
238 
makeSVGBackground(const int w,const int h)239 void KCubeBoxWidget::makeSVGBackground (const int w, const int h)
240 {
241    QImage img (w, h, QImage::Format_ARGB32_Premultiplied);
242    QPainter p (&img);
243    img.fill (0);
244    svg.render (&p, QStringLiteral("background"));
245    p.end();
246    background = QPixmap::fromImage (img);
247 }
248 
makeSVGCubes(const int width)249 void KCubeBoxWidget::makeSVGCubes (const int width)
250 {
251    QImage img (width, width, QImage::Format_ARGB32_Premultiplied);
252    QPainter q;                 // Paints whole faces of the dice.
253 
254    QImage pip (width/7, width/7, QImage::Format_ARGB32_Premultiplied);
255    QPainter r;                 // Paints the pips on the faces of the dice.
256 
257    QRectF rect (0, 0, width, width);
258    qreal  pc = 20.0;		// % radius on corners.
259    elements.clear();
260    for (int i = FirstElement; i <= LastElement; i++) {
261      q.begin(&img);
262      q.setPen (Qt::NoPen);
263      if (i == Pip) {
264        pip.fill (0);
265      }
266      else {
267        img.fill (0);
268      }
269 
270      // NOTE: "neutral", "player_1" and "player_2" from file "default.svg" cause
271      // odd effects at the corners. You get a cleaner look if they are omitted.
272 
273      switch (i) {
274      case Neutral:
275        // svg.render (&q, "neutral");
276        q.setBrush (color0);
277        q.drawRoundedRect (rect, pc, pc, Qt::RelativeSize);
278        svg.render (&q, QStringLiteral("lighting"));
279        break;
280      case Player1:
281        // svg.render (&q, "player_1");
282        q.setBrush (color1);
283        q.drawRoundedRect (rect, pc, pc, Qt::RelativeSize);
284        svg.render (&q, QStringLiteral("lighting"));
285        break;
286      case Player2:
287        // svg.render (&q, "player_2");
288        q.setBrush (color2);
289        q.drawRoundedRect (rect, pc, pc, Qt::RelativeSize);
290        svg.render (&q, QStringLiteral("lighting"));
291        break;
292      case Pip:
293        r.begin(&pip);
294        svg.render (&r, QStringLiteral("pip"));
295        r.end();
296        break;
297      case BlinkLight:
298        svg.render (&q, QStringLiteral("blink_light"));
299        break;
300      case BlinkDark:
301        svg.render (&q, QStringLiteral("blink_dark"));
302        break;
303      default:
304        break;
305      }
306      q.end();
307      elements.append
308        ((i == Pip) ? QPixmap::fromImage (pip) : QPixmap::fromImage (img));
309    }
310 }
311 
colorImage(QImage & img,const QColor & c,const int w)312 void KCubeBoxWidget::colorImage (QImage & img, const QColor & c, const int w)
313 {
314    QRgb rgba = c.rgba();
315    for (int i = 0; i < w; i++) {
316       for (int j = 0; j < w; j++) {
317          if (img.pixel (i, j) != 0) {
318 	    img.setPixel (i, j, rgba);
319          }
320       }
321    }
322 }
323 
paintEvent(QPaintEvent *)324 void KCubeBoxWidget::paintEvent (QPaintEvent * /* event unused */)
325 {
326    QPainter p (this);
327    p.drawPixmap (0, 0, background);
328 }
329 
resizeEvent(QResizeEvent * event)330 void KCubeBoxWidget::resizeEvent (QResizeEvent * event)
331 {
332    reCalculateGraphics (event->size().width(), event->size().height());
333 }
334 
reCalculateGraphics(const int w,const int h)335 void KCubeBoxWidget::reCalculateGraphics (const int w, const int h)
336 {
337    int boxSize = qMin(w, h);
338    int frameWidth = boxSize / 30;
339    // qCDebug(KJUMPINGCUBE_LOG) << "boxSize" << boxSize << "frameWidth" << frameWidth;
340    boxSize = boxSize - (2 * frameWidth);
341    cubeSize = (boxSize / m_side);
342    boxSize = (cubeSize * m_side);
343    topLeft.setX ((w - boxSize)/2);
344    topLeft.setY ((h - boxSize)/2);
345 
346    // qCDebug(KJUMPINGCUBE_LOG) << "Dimension:" << m_side << "cubeSize:" << cubeSize << "topLeft:" << topLeft;
347    makeSVGBackground (w, h);
348    makeSVGCubes (cubeSize);
349    for (int x = 0; x < m_side; x++) {
350       for (int y = 0; y < m_side; y++) {
351 	 int index = x * m_side + y;
352          cubes.at (index)->move (
353                             topLeft.x() + (x * cubeSize),
354                             topLeft.y() + (y * cubeSize));
355          cubes.at (index)->resize (cubeSize, cubeSize);
356       }
357    }
358    setPopup();
359 }
360 
sizeHint() const361 QSize  KCubeBoxWidget::sizeHint() const
362 {
363    return QSize(400,400);
364 }
365 
366 /* ***************************************************************** **
367 **                   other private functions                         **
368 ** ***************************************************************** */
369 
startAnimation(bool cascading,int index)370 void KCubeBoxWidget::startAnimation (bool cascading, int index)
371 {
372    int interval = 0;
373    m_index = index;
374    currentAnimation = cascading ? cascadeAnimation : ComputerMove;
375    switch (currentAnimation) {
376    case None:
377       animationCount = 0;
378       return;				// Should never happen.
379       break;
380    case ComputerMove:
381       interval = 150 + (Prefs::animationSpeed() - 1) * 50;	// 150-600 msec.
382       animationCount = 4;
383       cubes.at (index)->setLight();
384       break;
385    case Darken:
386       interval = animationTime;
387       animationCount = 1;
388       cubes.at (index)->setDark();
389       break;
390    case RapidBlink:
391       interval = 60 + Prefs::animationSpeed() * 30;		// 120-360 msec.
392       animationCount = 4;
393       cubes.at (index)->setLight();
394       break;
395    case Scatter:
396       interval = (animationTime + animationSteps/2) / animationSteps;
397       animationCount = animationSteps;
398       break;
399    }
400    animationTimer->setInterval (interval);
401    animationTimer->start();
402 }
403 
nextAnimationStep()404 void KCubeBoxWidget::nextAnimationStep()
405 {
406    animationCount--;
407    if (animationCount < 1) {
408       animationTimer->stop();		// Finish normally.
409       cubes.at (m_index)->setNeutral();
410       currentAnimation = None;
411       Q_EMIT animationDone (m_index);
412       return;
413    }
414    switch (currentAnimation) {
415    case None:
416       return;				// Should not happen.
417       break;
418    case ComputerMove:
419    case RapidBlink:
420       if (animationCount%2 == 1) {	// Set light or dark phase.
421          cubes.at (m_index)->setDark();
422       }
423       else {
424          cubes.at (m_index)->setLight();
425       }
426       break;
427    case Darken:
428       break;				// Should never happen (1 tick).
429    case Scatter:
430       int step = animationSteps - animationCount;
431       if (step <= 2) {			// Set the animation phase.
432          cubes.at (m_index)->shrink(1.0 - step * 0.3);
433       }
434       else if (step < 7) {
435          cubes.at (m_index)->expand((step - 2) * 0.2);
436       }
437       else if (step == 7) {
438          cubes.at (m_index)->expand(1.2);
439          scatterDots (0);
440       }
441       else {
442          scatterDots (step - 7);
443       }
444       break;
445    }
446 }
447 
scatterDots(int step)448 void KCubeBoxWidget::scatterDots (int step)
449 {
450    Player player = cubes.at(m_index)->owner();
451    int d = m_side - 1;
452    int x = m_index / m_side;
453    int y = m_index % m_side;
454    if (x > 0) cubes.at (m_index - m_side)->migrateDot (+1,  0, step, player);
455    if (x < d) cubes.at (m_index + m_side)->migrateDot (-1,  0, step, player);
456    if (y > 0) cubes.at (m_index - 1)     ->migrateDot ( 0, +1, step, player);
457    if (y < d) cubes.at (m_index + 1)     ->migrateDot ( 0, -1, step, player);
458 }
459 
killAnimation()460 int KCubeBoxWidget::killAnimation()
461 {
462    if (animationTimer->isActive()) {
463       animationTimer->stop();   	// Stop current animation immediately.
464    }
465    return m_index;
466 }
467 
playerPixmap(const int p)468 const QPixmap & KCubeBoxWidget::playerPixmap (const int p)
469 {
470    return ((p == 1) ? status1 : status2);
471 }
472 
setPopup()473 void KCubeBoxWidget::setPopup()
474 {
475    QFont f;
476    f.setPixelSize ((int) (height() * 0.04 + 0.5));
477    f.setWeight (QFont::Bold);
478    f.setStretch (QFont::Expanded);
479    m_popup->setStyleSheet(QStringLiteral("QLabel { color : rgba(255, 255, 255, 75%); }"));
480    m_popup->setFont (f);
481    m_popup->resize (width(), (int) (height() * 0.08 + 0.5));
482    m_popup->setAlignment (Qt::AlignCenter);
483 }
484 
showPopup(const QString & message)485 void KCubeBoxWidget::showPopup (const QString & message)
486 {
487    m_popup->setText (message);
488    m_popup->move ((this->width()  - m_popup->width()) / 2,
489                   (this->height() - m_popup->height()) / 2 +
490                   (cubes.at (0)->height() / 5));
491    m_popup->raise();
492    m_popup->show();
493    update();
494 }
495 
hidePopup()496 void KCubeBoxWidget::hidePopup()
497 {
498    m_popup->hide();
499    update();
500 }
501 
502 
503