File indexing completed on 2024-04-28 04:05:03

0001 /*
0002     SPDX-FileCopyrightText: 2007 Dmitry Suzdalev <dimsuz@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kgamepopupitem.h"
0008 
0009 // KF
0010 #include <KColorScheme>
0011 #include <KStatefulBrush>
0012 // Qt
0013 #include <QGraphicsScene>
0014 #include <QGraphicsTextItem>
0015 #include <QGraphicsView>
0016 #include <QIcon>
0017 #include <QPainter>
0018 #include <QTimeLine>
0019 #include <QTimer>
0020 
0021 // margin on the sides of message box
0022 static const int MARGIN = 15;
0023 // offset of message from start of the scene
0024 static const int SHOW_OFFSET = 15;
0025 // space between pixmap and text
0026 static const int SOME_SPACE = 10;
0027 // width of the border in pixels
0028 static const qreal BORDER_PEN_WIDTH = 1.0;
0029 
0030 class TextItemWithOpacity : public QGraphicsTextItem
0031 {
0032     Q_OBJECT
0033 
0034 public:
0035     TextItemWithOpacity(QGraphicsItem *parent = nullptr)
0036         : QGraphicsTextItem(parent)
0037         , m_opacity(1.0)
0038     {
0039     }
0040     void setOpacity(qreal opa)
0041     {
0042         m_opacity = opa;
0043     }
0044     void setTextColor(const KStatefulBrush &brush)
0045     {
0046         m_brush = brush;
0047     }
0048     void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
0049 
0050 Q_SIGNALS:
0051     void mouseClicked();
0052 
0053 private:
0054     void mouseReleaseEvent(QGraphicsSceneMouseEvent *) override;
0055 
0056 private:
0057     qreal m_opacity;
0058     KStatefulBrush m_brush;
0059 };
0060 
0061 void TextItemWithOpacity::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
0062 {
0063     // hope that it is ok to call this function here - i.e. I hope it won't be too expensive :)
0064     // we call it here (and not in setTextColor), because KstatefulBrush
0065     // absolutely needs QWidget parameter :)
0066     // NOTE from majewsky: For some weird reason, setDefaultTextColor does on some systems not check
0067     // whether the given color is equal to the one already set. Just calling setDefaultTextColor without
0068     // this check may result in an infinite loop of paintEvent -> setDefaultTextColor -> update -> paintEvent...
0069     const QColor textColor = widget ? m_brush.brush(widget->palette()).color() : QColor(Qt::black);
0070     if (textColor != defaultTextColor()) {
0071         setDefaultTextColor(textColor);
0072     }
0073     // render contents
0074     p->save();
0075     p->setOpacity(m_opacity);
0076     QGraphicsTextItem::paint(p, option, widget);
0077     p->restore();
0078 }
0079 
0080 void TextItemWithOpacity::mouseReleaseEvent(QGraphicsSceneMouseEvent *ev)
0081 {
0082     // NOTE: this item is QGraphicsTextItem which "eats" mouse events
0083     // because of interaction with links. Because of that let's make a
0084     // special signal to indicate mouse click
0085     Q_EMIT mouseClicked();
0086     QGraphicsTextItem::mouseReleaseEvent(ev);
0087 }
0088 
0089 class KGamePopupItemPrivate
0090 {
0091 private:
0092     KGamePopupItemPrivate(const KGamePopupItemPrivate &);
0093     const KGamePopupItemPrivate &operator=(const KGamePopupItemPrivate &);
0094 
0095 public:
0096     KGamePopupItemPrivate() = default;
0097 
0098 public:
0099     /**
0100      * Timeline for animations
0101      */
0102     QTimeLine m_timeLine;
0103     /**
0104      * Timer used to start hiding
0105      */
0106     QTimer m_timer;
0107     /**
0108      * Holds bounding rect of an item
0109      */
0110     QRect m_boundRect;
0111     /**
0112      * Holds bounding rect of an item (mapped to scene coordinates)
0113      */
0114     QRectF m_mappedBoundRect;
0115     /**
0116      * Offset of message from start of the scene (mapped to scene coordinates)
0117      */
0118     qreal m_mapped_SHOW_OFFSET;
0119     /**
0120      * Position where item will appear
0121      */
0122     KGamePopupItem::Position m_position = KGamePopupItem::BottomLeft;
0123     /**
0124      * Timeout to stay visible on screen
0125      */
0126     int m_timeout = 2000;
0127     /**
0128      * Item opacity
0129      */
0130     qreal m_opacity = 1.0;
0131     /**
0132      * Opacity used while animating appearing in center
0133      */
0134     qreal m_animOpacity = -1;
0135     /**
0136      * Pixmap to display at the left of the text
0137      */
0138     QPixmap m_iconPix;
0139     /**
0140      * Set to true when mouse hovers the message
0141      */
0142     bool m_hoveredByMouse = false;
0143     /**
0144      * Set to true if this popup item hides on mouse click.
0145      */
0146     bool m_hideOnClick = true;
0147     /**
0148      * Child of KGamePopupItem used to display text
0149      */
0150     TextItemWithOpacity *m_textChildItem = nullptr;
0151     /**
0152      * Part of the scene that is actually visible in QGraphicsView
0153      * This is needed for item to work correctly when scene is larger than
0154      * the View
0155      */
0156     QRectF m_visibleSceneRect;
0157     /**
0158      * Background brush color
0159      */
0160     KStatefulBrush m_brush;
0161     /**
0162      * popup angles sharpness
0163      */
0164     KGamePopupItem::Sharpness m_sharpness = KGamePopupItem::Square;
0165     /**
0166      * painter path to draw a frame
0167      */
0168     QPainterPath m_path;
0169     /**
0170      * Indicates if some link is hovered in text item
0171      */
0172     bool m_linkHovered = false;
0173 };
0174 
0175 KGamePopupItem::KGamePopupItem(QGraphicsItem *parent)
0176     : QGraphicsItem(parent)
0177     , d_ptr(new KGamePopupItemPrivate)
0178 {
0179     Q_D(KGamePopupItem);
0180 
0181     hide();
0182     d->m_textChildItem = new TextItemWithOpacity(this);
0183     d->m_textChildItem->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
0184     // above call said to enable ItemIsFocusable which we don't need.
0185     // So disabling it
0186     d->m_textChildItem->setFlag(QGraphicsItem::ItemIsFocusable, false);
0187 
0188     connect(d->m_textChildItem, &TextItemWithOpacity::linkActivated, this, &KGamePopupItem::linkActivated);
0189     connect(d->m_textChildItem, &TextItemWithOpacity::linkHovered, this, &KGamePopupItem::onLinkHovered);
0190     connect(d->m_textChildItem, &TextItemWithOpacity::mouseClicked, this, &KGamePopupItem::onTextItemClicked);
0191 
0192     setZValue(100); // is 100 high enough???
0193     d->m_textChildItem->setZValue(100);
0194 
0195     QIcon infoIcon = QIcon::fromTheme(QStringLiteral("dialog-information"));
0196     // default size is 32
0197     setMessageIcon(infoIcon.pixmap(32, 32));
0198 
0199     d->m_timer.setSingleShot(true);
0200 
0201     setAcceptHoverEvents(true);
0202     // ignore scene transformations
0203     setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
0204 
0205     // setup default colors
0206     d->m_brush = KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalBackground);
0207     d->m_textChildItem->setTextColor(KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalText));
0208 
0209     connect(&d->m_timeLine, &QTimeLine::frameChanged, this, &KGamePopupItem::animationFrame);
0210     connect(&d->m_timeLine, &QTimeLine::finished, this, &KGamePopupItem::hideMe);
0211     connect(&d->m_timer, &QTimer::timeout, this, &KGamePopupItem::playHideAnimation);
0212 }
0213 
0214 KGamePopupItem::~KGamePopupItem() = default;
0215 
0216 void KGamePopupItem::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
0217 {
0218     Q_D(KGamePopupItem);
0219 
0220     Q_UNUSED(option);
0221 
0222     p->save();
0223 
0224     QPen pen = p->pen();
0225     pen.setWidthF(BORDER_PEN_WIDTH);
0226     p->setPen(pen);
0227 
0228     if (d->m_animOpacity != -1) // playing Center animation
0229     {
0230         p->setOpacity(d->m_animOpacity);
0231     } else {
0232         p->setOpacity(d->m_opacity);
0233     }
0234     p->setBrush(widget ? d->m_brush.brush(widget->palette()) : QBrush());
0235     p->drawPath(d->m_path);
0236     p->drawPixmap(MARGIN, static_cast<int>(d->m_boundRect.height() / 2) - d->m_iconPix.height() / 2.0 / d->m_iconPix.devicePixelRatio(), d->m_iconPix);
0237     p->restore();
0238 }
0239 
0240 void KGamePopupItem::showMessage(const QString &text, Position pos, ReplaceMode mode)
0241 {
0242     Q_D(KGamePopupItem);
0243 
0244     if (d->m_timeLine.state() == QTimeLine::Running || d->m_timer.isActive()) {
0245         if (mode == ReplacePrevious) {
0246             forceHide(InstantHide);
0247         } else {
0248             return; // we're already showing a message
0249         }
0250     }
0251 
0252     // NOTE: we blindly take first visible view we found. I.e. we don't support
0253     // multiple views. If no visible scene is found, we simply pick the first one.
0254     QGraphicsView *sceneView = nullptr;
0255     const auto views = scene()->views();
0256     for (QGraphicsView *view : views) {
0257         if (view->isVisible()) {
0258             sceneView = view;
0259             break;
0260         }
0261     }
0262     if (!sceneView) {
0263         sceneView = views.at(0);
0264     }
0265 
0266     QPolygonF poly = sceneView->mapToScene(sceneView->viewport()->contentsRect());
0267     d->m_visibleSceneRect = poly.boundingRect();
0268 
0269     d->m_textChildItem->setHtml(text);
0270 
0271     d->m_position = pos;
0272 
0273     // do as QGS docs say: notify the scene about rect change
0274     prepareGeometryChange();
0275 
0276     // recalculate bounding rect
0277     const qreal iconDpr = d->m_iconPix.devicePixelRatio();
0278     qreal w = d->m_textChildItem->boundingRect().width() + MARGIN * 2 + d->m_iconPix.width() / iconDpr + SOME_SPACE;
0279     qreal h = d->m_textChildItem->boundingRect().height() + MARGIN * 2;
0280     if (d->m_iconPix.height() / iconDpr > h) {
0281         h = d->m_iconPix.height() / iconDpr + MARGIN * 2;
0282     }
0283     d->m_boundRect = QRect(0, 0, w, h);
0284 
0285     // adjust to take into account the width of the pen
0286     // used to draw the border
0287     const qreal borderRadius = BORDER_PEN_WIDTH / 2.0;
0288     d->m_boundRect.adjust(-borderRadius, -borderRadius, borderRadius, borderRadius);
0289 
0290     d->m_mappedBoundRect = sceneView->mapToScene(d->m_boundRect).boundingRect();
0291     d->m_mapped_SHOW_OFFSET = qAbs(sceneView->mapToScene(0, SHOW_OFFSET).y());
0292 
0293     QPainterPath roundRectPath;
0294     roundRectPath.moveTo(w, d->m_sharpness);
0295     roundRectPath.arcTo(w - (2 * d->m_sharpness), 0.0, (2 * d->m_sharpness), (d->m_sharpness), 0.0, 90.0);
0296     roundRectPath.lineTo(d->m_sharpness, 0.0);
0297     roundRectPath.arcTo(0.0, 0.0, (2 * d->m_sharpness), (2 * d->m_sharpness), 90.0, 90.0);
0298     roundRectPath.lineTo(0.0, h - (d->m_sharpness));
0299     roundRectPath.arcTo(0.0, h - (2 * d->m_sharpness), 2 * d->m_sharpness, 2 * d->m_sharpness, 180.0, 90.0);
0300     roundRectPath.lineTo(w - (d->m_sharpness), h);
0301     roundRectPath.arcTo(w - (2 * d->m_sharpness), h - (2 * d->m_sharpness), (2 * d->m_sharpness), (2 * d->m_sharpness), 270.0, 90.0);
0302     roundRectPath.closeSubpath();
0303 
0304     d->m_path = roundRectPath;
0305 
0306     // adjust y-pos of text item so it appears centered
0307     d->m_textChildItem->setPos(d->m_textChildItem->x(), d->m_boundRect.height() / 2 - d->m_textChildItem->boundingRect().height() / 2);
0308 
0309     // setup animation
0310     setupTimeline();
0311 
0312     // move to the start position
0313     animationFrame(d->m_timeLine.startFrame());
0314     show();
0315     d->m_timeLine.start();
0316 
0317     if (d->m_timeout != 0) {
0318         // 300 msec to animate showing message + d->m_timeout to stay visible => then hide
0319         d->m_timer.start(300 + d->m_timeout);
0320     }
0321 }
0322 
0323 void KGamePopupItem::setupTimeline()
0324 {
0325     Q_D(KGamePopupItem);
0326 
0327     d->m_timeLine.setDirection(QTimeLine::Forward);
0328     d->m_timeLine.setDuration(300);
0329     if (d->m_position == TopLeft || d->m_position == TopRight) {
0330         int start = static_cast<int>(d->m_visibleSceneRect.top() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
0331         int end = static_cast<int>(d->m_visibleSceneRect.top() + d->m_mapped_SHOW_OFFSET);
0332         d->m_timeLine.setFrameRange(start, end);
0333     } else if (d->m_position == BottomLeft || d->m_position == BottomRight) {
0334         int start = static_cast<int>(d->m_visibleSceneRect.bottom() + d->m_mapped_SHOW_OFFSET);
0335         int end = static_cast<int>(d->m_visibleSceneRect.bottom() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
0336         d->m_timeLine.setFrameRange(start, end);
0337     } else if (d->m_position == Center) {
0338         d->m_timeLine.setFrameRange(0, d->m_timeLine.duration());
0339         setPos(d->m_visibleSceneRect.left() + d->m_visibleSceneRect.width() / 2 - d->m_mappedBoundRect.width() / 2,
0340                d->m_visibleSceneRect.top() + d->m_visibleSceneRect.height() / 2 - d->m_mappedBoundRect.height() / 2);
0341     }
0342 }
0343 
0344 void KGamePopupItem::animationFrame(int frame)
0345 {
0346     Q_D(KGamePopupItem);
0347 
0348     if (d->m_position == TopLeft || d->m_position == BottomLeft) {
0349         setPos(d->m_visibleSceneRect.left() + d->m_mapped_SHOW_OFFSET, frame);
0350     } else if (d->m_position == TopRight || d->m_position == BottomRight) {
0351         setPos(d->m_visibleSceneRect.right() - d->m_mappedBoundRect.width() - d->m_mapped_SHOW_OFFSET, frame);
0352     } else if (d->m_position == Center) {
0353         d->m_animOpacity = frame * d->m_opacity / d->m_timeLine.duration();
0354         d->m_textChildItem->setOpacity(d->m_animOpacity);
0355         update();
0356     }
0357 }
0358 
0359 void KGamePopupItem::playHideAnimation()
0360 {
0361     Q_D(KGamePopupItem);
0362 
0363     if (d->m_hoveredByMouse) {
0364         return;
0365     }
0366     // let's hide
0367     d->m_timeLine.setDirection(QTimeLine::Backward);
0368     d->m_timeLine.start();
0369 }
0370 
0371 void KGamePopupItem::setMessageTimeout(int msec)
0372 {
0373     Q_D(KGamePopupItem);
0374 
0375     d->m_timeout = msec;
0376 }
0377 
0378 void KGamePopupItem::setHideOnMouseClick(bool hide)
0379 {
0380     Q_D(KGamePopupItem);
0381 
0382     d->m_hideOnClick = hide;
0383 }
0384 
0385 bool KGamePopupItem::hidesOnMouseClick() const
0386 {
0387     Q_D(const KGamePopupItem);
0388 
0389     return d->m_hideOnClick;
0390 }
0391 
0392 void KGamePopupItem::setMessageOpacity(qreal opacity)
0393 {
0394     Q_D(KGamePopupItem);
0395 
0396     d->m_opacity = opacity;
0397     d->m_textChildItem->setOpacity(opacity);
0398 }
0399 
0400 QRectF KGamePopupItem::boundingRect() const
0401 {
0402     Q_D(const KGamePopupItem);
0403 
0404     return d->m_boundRect;
0405 }
0406 
0407 void KGamePopupItem::hideMe()
0408 {
0409     Q_D(KGamePopupItem);
0410 
0411     d->m_animOpacity = -1;
0412     // and restore child's opacity too
0413     d->m_textChildItem->setOpacity(d->m_opacity);
0414 
0415     // if we just got moved out of visibility, let's do more - let's hide :)
0416     if (d->m_timeLine.direction() == QTimeLine::Backward) {
0417         hide();
0418         Q_EMIT hidden();
0419     }
0420 }
0421 
0422 void KGamePopupItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
0423 {
0424     Q_D(KGamePopupItem);
0425 
0426     d->m_hoveredByMouse = true;
0427 }
0428 
0429 void KGamePopupItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
0430 {
0431     Q_D(KGamePopupItem);
0432 
0433     d->m_hoveredByMouse = false;
0434 
0435     if (d->m_timeout != 0 && !d->m_timer.isActive() && d->m_timeLine.state() != QTimeLine::Running) {
0436         playHideAnimation(); // let's hide
0437     }
0438 }
0439 
0440 void KGamePopupItem::setMessageIcon(const QPixmap &pix)
0441 {
0442     Q_D(KGamePopupItem);
0443 
0444     d->m_iconPix = pix;
0445     d->m_textChildItem->setPos(MARGIN + pix.width() / pix.devicePixelRatio() + SOME_SPACE, MARGIN);
0446     // bounding rect is updated in showMessage()
0447 }
0448 
0449 int KGamePopupItem::messageTimeout() const
0450 {
0451     Q_D(const KGamePopupItem);
0452 
0453     return d->m_timeout;
0454 }
0455 
0456 void KGamePopupItem::forceHide(HideType howToHide)
0457 {
0458     Q_D(KGamePopupItem);
0459 
0460     if (!isVisible()) {
0461         return;
0462     }
0463 
0464     if (howToHide == InstantHide) {
0465         d->m_timeLine.stop();
0466         d->m_timer.stop();
0467         hide();
0468         Q_EMIT hidden();
0469     } else if (howToHide == AnimatedHide) {
0470         // forcefully unset it even if it is set
0471         // so we'll hide in any event
0472         d->m_hoveredByMouse = false;
0473         d->m_timer.stop();
0474         playHideAnimation();
0475     }
0476 }
0477 
0478 qreal KGamePopupItem::messageOpacity() const
0479 {
0480     Q_D(const KGamePopupItem);
0481 
0482     return d->m_opacity;
0483 }
0484 
0485 void KGamePopupItem::setBackgroundBrush(const QBrush &brush)
0486 {
0487     Q_D(KGamePopupItem);
0488 
0489     d->m_brush = KStatefulBrush(brush);
0490 }
0491 
0492 void KGamePopupItem::setTextColor(const QColor &color)
0493 {
0494     Q_D(KGamePopupItem);
0495 
0496     KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active));
0497     d->m_textChildItem->setTextColor(brush);
0498 }
0499 
0500 void KGamePopupItem::onLinkHovered(const QString &link)
0501 {
0502     Q_D(KGamePopupItem);
0503 
0504     if (link.isEmpty()) {
0505         d->m_textChildItem->setCursor(Qt::ArrowCursor);
0506     } else {
0507         d->m_textChildItem->setCursor(Qt::PointingHandCursor);
0508     }
0509 
0510     d->m_linkHovered = !link.isEmpty();
0511     Q_EMIT linkHovered(link);
0512 }
0513 
0514 void KGamePopupItem::setSharpness(Sharpness sharpness)
0515 {
0516     Q_D(KGamePopupItem);
0517 
0518     d->m_sharpness = sharpness;
0519 }
0520 
0521 KGamePopupItem::Sharpness KGamePopupItem::sharpness() const
0522 {
0523     Q_D(const KGamePopupItem);
0524 
0525     return d->m_sharpness;
0526 }
0527 
0528 void KGamePopupItem::mousePressEvent(QGraphicsSceneMouseEvent *)
0529 {
0530     // it is needed to reimplement this function to receive future
0531     // mouse release events
0532 }
0533 
0534 void KGamePopupItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *)
0535 {
0536     Q_D(KGamePopupItem);
0537 
0538     // NOTE: text child item is QGraphicsTextItem which "eats" mouse events
0539     // because of interaction with links. Because of that TextItemWithOpacity has
0540     // special signal to indicate mouse click which we catch in a onTextItemClicked()
0541     // slot
0542     if (d->m_hideOnClick) {
0543         forceHide();
0544     }
0545 }
0546 
0547 void KGamePopupItem::onTextItemClicked()
0548 {
0549     Q_D(KGamePopupItem);
0550 
0551     // if link is hovered we don't hide as click should go to the link
0552     if (d->m_hideOnClick && !d->m_linkHovered) {
0553         forceHide();
0554     }
0555 }
0556 
0557 #include "kgamepopupitem.moc" // For automocing TextItemWithOpacity
0558 #include "moc_kgamepopupitem.cpp" // For automocing KGamePopupItem