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