KDEGames

kgamepopupitem.cpp
1 /*
2  SPDX-FileCopyrightText: 2007 Dmitry Suzdalev <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "kgamepopupitem.h"
8 
9 // KF
10 #include <KColorScheme>
11 #include <KStatefulBrush>
12 // Qt
13 #include <QGraphicsScene>
14 #include <QGraphicsTextItem>
15 #include <QGraphicsView>
16 #include <QIcon>
17 #include <QPainter>
18 #include <QTimeLine>
19 #include <QTimer>
20 
21 // margin on the sides of message box
22 static const int MARGIN = 15;
23 // offset of message from start of the scene
24 static const int SHOW_OFFSET = 15;
25 // space between pixmap and text
26 static const int SOME_SPACE = 10;
27 // width of the border in pixels
28 static const qreal BORDER_PEN_WIDTH = 1.0;
29 
30 class TextItemWithOpacity : public QGraphicsTextItem
31 {
32  Q_OBJECT
33 
34 public:
35  TextItemWithOpacity(QGraphicsItem *parent = nullptr)
37  , m_opacity(1.0)
38  {
39  }
40  void setOpacity(qreal opa)
41  {
42  m_opacity = opa;
43  }
44  void setTextColor(const KStatefulBrush &brush)
45  {
46  m_brush = brush;
47  }
48  void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
49 
50 Q_SIGNALS:
51  void mouseClicked();
52 
53 private:
55 
56 private:
57  qreal m_opacity;
58  KStatefulBrush m_brush;
59 };
60 
61 void TextItemWithOpacity::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
62 {
63  // hope that it is ok to call this function here - i.e. I hope it won't be too expensive :)
64  // we call it here (and not in setTextColor), because KstatefulBrush
65  // absolutely needs QWidget parameter :)
66  // NOTE from majewsky: For some weird reason, setDefaultTextColor does on some systems not check
67  // whether the given color is equal to the one already set. Just calling setDefaultTextColor without
68  // this check may result in an infinite loop of paintEvent -> setDefaultTextColor -> update -> paintEvent...
69  const QColor textColor = widget ? m_brush.brush(widget->palette()).color() : QColor(Qt::black);
70  if (textColor != defaultTextColor()) {
71  setDefaultTextColor(textColor);
72  }
73  // render contents
74  p->save();
75  p->setOpacity(m_opacity);
76  QGraphicsTextItem::paint(p, option, widget);
77  p->restore();
78 }
79 
80 void TextItemWithOpacity::mouseReleaseEvent(QGraphicsSceneMouseEvent *ev)
81 {
82  // NOTE: this item is QGraphicsTextItem which "eats" mouse events
83  // because of interaction with links. Because of that let's make a
84  // special signal to indicate mouse click
85  Q_EMIT mouseClicked();
87 }
88 
89 class KGamePopupItemPrivate
90 {
91 private:
92  KGamePopupItemPrivate(const KGamePopupItemPrivate &);
93  const KGamePopupItemPrivate &operator=(const KGamePopupItemPrivate &);
94 
95 public:
96  KGamePopupItemPrivate() = default;
97 
98 public:
99  /**
100  * Timeline for animations
101  */
102  QTimeLine m_timeLine;
103  /**
104  * Timer used to start hiding
105  */
106  QTimer m_timer;
107  /**
108  * Holds bounding rect of an item
109  */
110  QRect m_boundRect;
111  /**
112  * Holds bounding rect of an item (mapped to scene coordinates)
113  */
114  QRectF m_mappedBoundRect;
115  /**
116  * Offset of message from start of the scene (mapped to scene coordinates)
117  */
118  qreal m_mapped_SHOW_OFFSET;
119  /**
120  * Position where item will appear
121  */
122  KGamePopupItem::Position m_position = KGamePopupItem::BottomLeft;
123  /**
124  * Timeout to stay visible on screen
125  */
126  int m_timeout = 2000;
127  /**
128  * Item opacity
129  */
130  qreal m_opacity = 1.0;
131  /**
132  * Opacity used while animating appearing in center
133  */
134  qreal m_animOpacity = -1;
135  /**
136  * Pixmap to display at the left of the text
137  */
138  QPixmap m_iconPix;
139  /**
140  * Set to true when mouse hovers the message
141  */
142  bool m_hoveredByMouse = false;
143  /**
144  * Set to true if this popup item hides on mouse click.
145  */
146  bool m_hideOnClick = true;
147  /**
148  * Child of KGamePopupItem used to display text
149  */
150  TextItemWithOpacity *m_textChildItem = nullptr;
151  /**
152  * Part of the scene that is actually visible in QGraphicsView
153  * This is needed for item to work correctly when scene is larger than
154  * the View
155  */
156  QRectF m_visibleSceneRect;
157  /**
158  * Background brush color
159  */
160  KStatefulBrush m_brush;
161  /**
162  * popup angles sharpness
163  */
164  KGamePopupItem::Sharpness m_sharpness = KGamePopupItem::Square;
165  /**
166  * painter path to draw a frame
167  */
168  QPainterPath m_path;
169  /**
170  * Indicates if some link is hovered in text item
171  */
172  bool m_linkHovered = false;
173 };
174 
176  : QGraphicsItem(parent)
177  , d_ptr(new KGamePopupItemPrivate)
178 {
180 
181  hide();
182  d->m_textChildItem = new TextItemWithOpacity(this);
183  d->m_textChildItem->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
184  // above call said to enable ItemIsFocusable which we don't need.
185  // So disabling it
186  d->m_textChildItem->setFlag(QGraphicsItem::ItemIsFocusable, false);
187 
188  connect(d->m_textChildItem, &TextItemWithOpacity::linkActivated, this, &KGamePopupItem::linkActivated);
189  connect(d->m_textChildItem, &TextItemWithOpacity::linkHovered, this, &KGamePopupItem::onLinkHovered);
190  connect(d->m_textChildItem, &TextItemWithOpacity::mouseClicked, this, &KGamePopupItem::onTextItemClicked);
191 
192  setZValue(100); // is 100 high enough???
193  d->m_textChildItem->setZValue(100);
194 
195  QIcon infoIcon = QIcon::fromTheme(QStringLiteral("dialog-information"));
196  // default size is 32
197  setMessageIcon(infoIcon.pixmap(32, 32));
198 
199  d->m_timer.setSingleShot(true);
200 
201  setAcceptHoverEvents(true);
202  // ignore scene transformations
204 
205  // setup default colors
207  d->m_textChildItem->setTextColor(KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalText));
208 
209  connect(&d->m_timeLine, &QTimeLine::frameChanged, this, &KGamePopupItem::animationFrame);
210  connect(&d->m_timeLine, &QTimeLine::finished, this, &KGamePopupItem::hideMe);
211  connect(&d->m_timer, &QTimer::timeout, this, &KGamePopupItem::playHideAnimation);
212 }
213 
215 
217 {
219 
220  Q_UNUSED(option);
221 
222  p->save();
223 
224  QPen pen = p->pen();
225  pen.setWidthF(BORDER_PEN_WIDTH);
226  p->setPen(pen);
227 
228  if (d->m_animOpacity != -1) // playing Center animation
229  {
230  p->setOpacity(d->m_animOpacity);
231  } else {
232  p->setOpacity(d->m_opacity);
233  }
234  p->setBrush(widget ? d->m_brush.brush(widget->palette()) : QBrush());
235  p->drawPath(d->m_path);
236  p->drawPixmap(MARGIN, static_cast<int>(d->m_boundRect.height() / 2) - d->m_iconPix.height() / 2.0 / d->m_iconPix.devicePixelRatio(), d->m_iconPix);
237  p->restore();
238 }
239 
241 {
243 
244  if (d->m_timeLine.state() == QTimeLine::Running || d->m_timer.isActive()) {
245  if (mode == ReplacePrevious) {
246  forceHide(InstantHide);
247  } else {
248  return; // we're already showing a message
249  }
250  }
251 
252  // NOTE: we blindly take first visible view we found. I.e. we don't support
253  // multiple views. If no visible scene is found, we simply pick the first one.
254  QGraphicsView *sceneView = nullptr;
255  const auto views = scene()->views();
256  for (QGraphicsView *view : views) {
257  if (view->isVisible()) {
258  sceneView = view;
259  break;
260  }
261  }
262  if (!sceneView) {
263  sceneView = views.at(0);
264  }
265 
266  QPolygonF poly = sceneView->mapToScene(sceneView->viewport()->contentsRect());
267  d->m_visibleSceneRect = poly.boundingRect();
268 
269  d->m_textChildItem->setHtml(text);
270 
271  d->m_position = pos;
272 
273  // do as QGS docs say: notify the scene about rect change
275 
276  // recalculate bounding rect
277  const qreal iconDpr = d->m_iconPix.devicePixelRatio();
278  qreal w = d->m_textChildItem->boundingRect().width() + MARGIN * 2 + d->m_iconPix.width() / iconDpr + SOME_SPACE;
279  qreal h = d->m_textChildItem->boundingRect().height() + MARGIN * 2;
280  if (d->m_iconPix.height() / iconDpr > h) {
281  h = d->m_iconPix.height() / iconDpr + MARGIN * 2;
282  }
283  d->m_boundRect = QRect(0, 0, w, h);
284 
285  // adjust to take into account the width of the pen
286  // used to draw the border
287  const qreal borderRadius = BORDER_PEN_WIDTH / 2.0;
288  d->m_boundRect.adjust(-borderRadius, -borderRadius, borderRadius, borderRadius);
289 
290  d->m_mappedBoundRect = sceneView->mapToScene(d->m_boundRect).boundingRect();
291  d->m_mapped_SHOW_OFFSET = qAbs(sceneView->mapToScene(0, SHOW_OFFSET).y());
292 
293  QPainterPath roundRectPath;
294  roundRectPath.moveTo(w, d->m_sharpness);
295  roundRectPath.arcTo(w - (2 * d->m_sharpness), 0.0, (2 * d->m_sharpness), (d->m_sharpness), 0.0, 90.0);
296  roundRectPath.lineTo(d->m_sharpness, 0.0);
297  roundRectPath.arcTo(0.0, 0.0, (2 * d->m_sharpness), (2 * d->m_sharpness), 90.0, 90.0);
298  roundRectPath.lineTo(0.0, h - (d->m_sharpness));
299  roundRectPath.arcTo(0.0, h - (2 * d->m_sharpness), 2 * d->m_sharpness, 2 * d->m_sharpness, 180.0, 90.0);
300  roundRectPath.lineTo(w - (d->m_sharpness), h);
301  roundRectPath.arcTo(w - (2 * d->m_sharpness), h - (2 * d->m_sharpness), (2 * d->m_sharpness), (2 * d->m_sharpness), 270.0, 90.0);
302  roundRectPath.closeSubpath();
303 
304  d->m_path = roundRectPath;
305 
306  // adjust y-pos of text item so it appears centered
307  d->m_textChildItem->setPos(d->m_textChildItem->x(), d->m_boundRect.height() / 2 - d->m_textChildItem->boundingRect().height() / 2);
308 
309  // setup animation
310  setupTimeline();
311 
312  // move to the start position
313  animationFrame(d->m_timeLine.startFrame());
314  show();
315  d->m_timeLine.start();
316 
317  if (d->m_timeout != 0) {
318  // 300 msec to animate showing message + d->m_timeout to stay visible => then hide
319  d->m_timer.start(300 + d->m_timeout);
320  }
321 }
322 
323 void KGamePopupItem::setupTimeline()
324 {
326 
327  d->m_timeLine.setDirection(QTimeLine::Forward);
328  d->m_timeLine.setDuration(300);
329  if (d->m_position == TopLeft || d->m_position == TopRight) {
330  int start = static_cast<int>(d->m_visibleSceneRect.top() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
331  int end = static_cast<int>(d->m_visibleSceneRect.top() + d->m_mapped_SHOW_OFFSET);
332  d->m_timeLine.setFrameRange(start, end);
333  } else if (d->m_position == BottomLeft || d->m_position == BottomRight) {
334  int start = static_cast<int>(d->m_visibleSceneRect.bottom() + d->m_mapped_SHOW_OFFSET);
335  int end = static_cast<int>(d->m_visibleSceneRect.bottom() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
336  d->m_timeLine.setFrameRange(start, end);
337  } else if (d->m_position == Center) {
338  d->m_timeLine.setFrameRange(0, d->m_timeLine.duration());
339  setPos(d->m_visibleSceneRect.left() + d->m_visibleSceneRect.width() / 2 - d->m_mappedBoundRect.width() / 2,
340  d->m_visibleSceneRect.top() + d->m_visibleSceneRect.height() / 2 - d->m_mappedBoundRect.height() / 2);
341  }
342 }
343 
344 void KGamePopupItem::animationFrame(int frame)
345 {
347 
348  if (d->m_position == TopLeft || d->m_position == BottomLeft) {
349  setPos(d->m_visibleSceneRect.left() + d->m_mapped_SHOW_OFFSET, frame);
350  } else if (d->m_position == TopRight || d->m_position == BottomRight) {
351  setPos(d->m_visibleSceneRect.right() - d->m_mappedBoundRect.width() - d->m_mapped_SHOW_OFFSET, frame);
352  } else if (d->m_position == Center) {
353  d->m_animOpacity = frame * d->m_opacity / d->m_timeLine.duration();
354  d->m_textChildItem->setOpacity(d->m_animOpacity);
355  update();
356  }
357 }
358 
359 void KGamePopupItem::playHideAnimation()
360 {
362 
363  if (d->m_hoveredByMouse) {
364  return;
365  }
366  // let's hide
367  d->m_timeLine.setDirection(QTimeLine::Backward);
368  d->m_timeLine.start();
369 }
370 
372 {
374 
375  d->m_timeout = msec;
376 }
377 
379 {
381 
382  d->m_hideOnClick = hide;
383 }
384 
386 {
387  Q_D(const KGamePopupItem);
388 
389  return d->m_hideOnClick;
390 }
391 
393 {
395 
396  d->m_opacity = opacity;
397  d->m_textChildItem->setOpacity(opacity);
398 }
399 
401 {
402  Q_D(const KGamePopupItem);
403 
404  return d->m_boundRect;
405 }
406 
407 void KGamePopupItem::hideMe()
408 {
410 
411  d->m_animOpacity = -1;
412  // and restore child's opacity too
413  d->m_textChildItem->setOpacity(d->m_opacity);
414 
415  // if we just got moved out of visibility, let's do more - let's hide :)
416  if (d->m_timeLine.direction() == QTimeLine::Backward) {
417  hide();
418  Q_EMIT hidden();
419  }
420 }
421 
422 void KGamePopupItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
423 {
425 
426  d->m_hoveredByMouse = true;
427 }
428 
429 void KGamePopupItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
430 {
432 
433  d->m_hoveredByMouse = false;
434 
435  if (d->m_timeout != 0 && !d->m_timer.isActive() && d->m_timeLine.state() != QTimeLine::Running) {
436  playHideAnimation(); // let's hide
437  }
438 }
439 
441 {
443 
444  d->m_iconPix = pix;
445  d->m_textChildItem->setPos(MARGIN + pix.width() / pix.devicePixelRatio() + SOME_SPACE, MARGIN);
446  // bounding rect is updated in showMessage()
447 }
448 
450 {
451  Q_D(const KGamePopupItem);
452 
453  return d->m_timeout;
454 }
455 
457 {
459 
460  if (!isVisible()) {
461  return;
462  }
463 
464  if (howToHide == InstantHide) {
465  d->m_timeLine.stop();
466  d->m_timer.stop();
467  hide();
468  Q_EMIT hidden();
469  } else if (howToHide == AnimatedHide) {
470  // forcefully unset it even if it is set
471  // so we'll hide in any event
472  d->m_hoveredByMouse = false;
473  d->m_timer.stop();
474  playHideAnimation();
475  }
476 }
477 
479 {
480  Q_D(const KGamePopupItem);
481 
482  return d->m_opacity;
483 }
484 
486 {
488 
489  d->m_brush = KStatefulBrush(brush);
490 }
491 
493 {
495 
496  KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active));
497  d->m_textChildItem->setTextColor(brush);
498 }
499 
500 void KGamePopupItem::onLinkHovered(const QString &link)
501 {
503 
504  if (link.isEmpty()) {
505  d->m_textChildItem->setCursor(Qt::ArrowCursor);
506  } else {
507  d->m_textChildItem->setCursor(Qt::PointingHandCursor);
508  }
509 
510  d->m_linkHovered = !link.isEmpty();
511  Q_EMIT linkHovered(link);
512 }
513 
515 {
517 
518  d->m_sharpness = sharpness;
519 }
520 
522 {
523  Q_D(const KGamePopupItem);
524 
525  return d->m_sharpness;
526 }
527 
528 void KGamePopupItem::mousePressEvent(QGraphicsSceneMouseEvent *)
529 {
530  // it is needed to reimplement this function to receive future
531  // mouse release events
532 }
533 
534 void KGamePopupItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *)
535 {
537 
538  // NOTE: text child item is QGraphicsTextItem which "eats" mouse events
539  // because of interaction with links. Because of that TextItemWithOpacity has
540  // special signal to indicate mouse click which we catch in a onTextItemClicked()
541  // slot
542  if (d->m_hideOnClick) {
543  forceHide();
544  }
545 }
546 
547 void KGamePopupItem::onTextItemClicked()
548 {
550 
551  // if link is hovered we don't hide as click should go to the link
552  if (d->m_hideOnClick && !d->m_linkHovered) {
553  forceHide();
554  }
555 }
556 
557 #include "kgamepopupitem.moc" // For automocing TextItemWithOpacity
558 #include "moc_kgamepopupitem.cpp" // For automocing KGamePopupItem
Q_OBJECTQ_OBJECT
void setZValue(qreal z)
void arcTo(const QRectF &rectangle, qreal startAngle, qreal sweepLength)
void setPen(const QColor &color)
HideType
Used to specify how to hide in forceHide() - instantly or animatedly.
int messageTimeout() const
qreal devicePixelRatio() const const
void drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)
Q_EMITQ_EMIT
QRectF boundingRect() const const
void update(const QRectF &rect)
virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
void setOpacity(qreal opacity)
Sharpness sharpness() const
QRectF boundingRect() const override
Q_SCRIPTABLE Q_NOREPLY void start()
QIcon fromTheme(const QString &name)
void setHideOnMouseClick(bool hide)
Sets whether to hide this popup item on mouse click.
void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget) override
Paints item.
const QPen & pen() const const
void prepareGeometryChange()
void frameChanged(int frame)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void hidden()
Emitted when the popup finishes hiding.
void setWidthF(qreal width)
ReplaceMode
Possible values for message showing mode in respect to a previous message.
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
void setAcceptHoverEvents(bool enabled)
QList< QGraphicsView * > views() const const
LinksAccessibleByMouse
void setPos(const QPointF &pos)
QGraphicsScene * scene() const const
void timeout()
void drawPath(const QPainterPath &path)
ArrowCursor
void linkActivated(const QString &link)
Emitted when user clicks on a link in item.
~KGamePopupItem() override
Destructs a message item.
void forceHide(HideType type=AnimatedHide)
Requests the item to be hidden immediately.
void setFlag(QGraphicsItem::GraphicsItemFlag flag, bool enabled)
void setBrush(const QBrush &brush)
void setMessageIcon(const QPixmap &pix)
Sets custom pixmap to show instead of default icon on the left.
Position
The possible places in the scene where a message can be shown.
Q_SIGNALSQ_SIGNALS
void setOpacity(qreal opacity)
void linkHovered(const QString &link)
Emitted when user hovers a link in item.
virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
void closeSubpath()
void setBackgroundBrush(const QBrush &brush)
Sets brush used to paint item background By default system-default brush is used.
qreal y() const const
void setSharpness(Sharpness sharpness)
Sets the popup angles sharpness.
QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) const const
qreal messageOpacity() const
Sharpness
Possible values for the popup angles sharpness.
void moveTo(const QPointF &point)
void showMessage(const QString &text, Position pos, ReplaceMode mode=LeavePrevious)
Shows the message: item will appear at specified place of the scene using simple animation Item will ...
void restore()
KGamePopupItem(QGraphicsItem *parent=nullptr)
Constructs a message item.
QPointF pos() const const
void save()
bool isVisible() const const
void setTextColor(const QColor &color)
Sets default color for unformatted text By default system-default color is used.
qreal opacity() const const
QWidget * viewport() const const
void setMessageOpacity(qreal opacity)
Sets the message opacity from 0 (fully transparent) to 1 (fully opaque) For example 0....
void setMessageTimeout(int msec)
Sets the amount of time the item will stay visible on screen before it goes away.
const QList< QKeySequence > & end()
bool hidesOnMouseClick() const
QPointF mapToScene(const QPoint &point) const const
int width() const const
Q_D(Todo)
QRect contentsRect() const const
void finished()
void lineTo(const QPointF &endPoint)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Thu Dec 7 2023 03:50:25 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.