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 <kconfigwidgets_version.h>
12 #if KCONFIGWIDGETS_VERSION >= QT_VERSION_CHECK(5, 93, 0)
13 #include <KStatefulBrush> // was moved to own header in 5.93.0
14 #endif
15 // Qt
16 #include <QGraphicsScene>
17 #include <QGraphicsTextItem>
18 #include <QGraphicsView>
19 #include <QIcon>
20 #include <QPainter>
21 #include <QTimeLine>
22 #include <QTimer>
23 
24 // margin on the sides of message box
25 static const int MARGIN = 15;
26 // offset of message from start of the scene
27 static const int SHOW_OFFSET = 15;
28 // space between pixmap and text
29 static const int SOME_SPACE = 10;
30 // width of the border in pixels
31 static const qreal BORDER_PEN_WIDTH = 1.0;
32 
33 class TextItemWithOpacity : public QGraphicsTextItem
34 {
35  Q_OBJECT
36 
37 public:
38  TextItemWithOpacity(QGraphicsItem *parent = nullptr)
40  , m_opacity(1.0)
41  {
42  }
43  void setOpacity(qreal opa)
44  {
45  m_opacity = opa;
46  }
47  void setTextColor(const KStatefulBrush &brush)
48  {
49  m_brush = brush;
50  }
51  void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
52 
53 Q_SIGNALS:
54  void mouseClicked();
55 
56 private:
58 
59 private:
60  qreal m_opacity;
61  KStatefulBrush m_brush;
62 };
63 
64 void TextItemWithOpacity::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
65 {
66  // hope that it is ok to call this function here - i.e. I hope it won't be too expensive :)
67  // we call it here (and not in setTextColor), because KstatefulBrush
68  // absolutely needs QWidget parameter :)
69  // NOTE from majewsky: For some weird reason, setDefaultTextColor does on some systems not check
70  // whether the given color is equal to the one already set. Just calling setDefaultTextColor without
71  // this check may result in an infinite loop of paintEvent -> setDefaultTextColor -> update -> paintEvent...
72  const QColor textColor = widget ? m_brush.brush(widget->palette()).color() : QColor(Qt::black);
73  if (textColor != defaultTextColor()) {
74  setDefaultTextColor(textColor);
75  }
76  // render contents
77  p->save();
78  p->setOpacity(m_opacity);
79  QGraphicsTextItem::paint(p, option, widget);
80  p->restore();
81 }
82 
83 void TextItemWithOpacity::mouseReleaseEvent(QGraphicsSceneMouseEvent *ev)
84 {
85  // NOTE: this item is QGraphicsTextItem which "eats" mouse events
86  // because of interaction with links. Because of that let's make a
87  // special signal to indicate mouse click
88  Q_EMIT mouseClicked();
90 }
91 
92 class KGamePopupItemPrivate
93 {
94 private:
95  KGamePopupItemPrivate(const KGamePopupItemPrivate &);
96  const KGamePopupItemPrivate &operator=(const KGamePopupItemPrivate &);
97 
98 public:
99  KGamePopupItemPrivate()
100  : m_position(KGamePopupItem::BottomLeft)
101  , m_timeout(2000)
102  , m_opacity(1.0)
103  , m_animOpacity(-1)
104  , m_hoveredByMouse(false)
105  , m_hideOnClick(true)
106  , m_textChildItem(nullptr)
107  , m_sharpness(KGamePopupItem::Square)
108  , m_linkHovered(false)
109  {
110  }
111  /**
112  * Timeline for animations
113  */
114  QTimeLine m_timeLine;
115  /**
116  * Timer used to start hiding
117  */
118  QTimer m_timer;
119  /**
120  * Holds bounding rect of an item
121  */
122  QRect m_boundRect;
123  /**
124  * Holds bounding rect of an item (mapped to scene coordinates)
125  */
126  QRectF m_mappedBoundRect;
127  /**
128  * Offset of message from start of the scene (mapped to scene coordinates)
129  */
130  qreal m_mapped_SHOW_OFFSET;
131  /**
132  * Position where item will appear
133  */
134  KGamePopupItem::Position m_position;
135  /**
136  * Timeout to stay visible on screen
137  */
138  int m_timeout;
139  /**
140  * Item opacity
141  */
142  qreal m_opacity;
143  /**
144  * Opacity used while animating appearing in center
145  */
146  qreal m_animOpacity;
147  /**
148  * Pixmap to display at the left of the text
149  */
150  QPixmap m_iconPix;
151  /**
152  * Set to true when mouse hovers the message
153  */
154  bool m_hoveredByMouse;
155  /**
156  * Set to true if this popup item hides on mouse click.
157  */
158  bool m_hideOnClick;
159  /**
160  * Child of KGamePopupItem used to display text
161  */
162  TextItemWithOpacity *m_textChildItem;
163  /**
164  * Part of the scene that is actually visible in QGraphicsView
165  * This is needed for item to work correctly when scene is larger than
166  * the View
167  */
168  QRectF m_visibleSceneRect;
169  /**
170  * Background brush color
171  */
172  KStatefulBrush m_brush;
173  /**
174  * popup angles sharpness
175  */
176  KGamePopupItem::Sharpness m_sharpness;
177  /**
178  * painter path to draw a frame
179  */
180  QPainterPath m_path;
181  /**
182  * Indicates if some link is hovered in text item
183  */
184  bool m_linkHovered;
185 };
186 
188  : QGraphicsItem(parent)
189  , d(new KGamePopupItemPrivate)
190 {
191  hide();
192  d->m_textChildItem = new TextItemWithOpacity(this);
193  d->m_textChildItem->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
194  // above call said to enable ItemIsFocusable which we don't need.
195  // So disabling it
196  d->m_textChildItem->setFlag(QGraphicsItem::ItemIsFocusable, false);
197 
198  connect(d->m_textChildItem, &TextItemWithOpacity::linkActivated, this, &KGamePopupItem::linkActivated);
199  connect(d->m_textChildItem, &TextItemWithOpacity::linkHovered, this, &KGamePopupItem::onLinkHovered);
200  connect(d->m_textChildItem, &TextItemWithOpacity::mouseClicked, this, &KGamePopupItem::onTextItemClicked);
201 
202  setZValue(100); // is 100 high enough???
203  d->m_textChildItem->setZValue(100);
204 
205  QIcon infoIcon = QIcon::fromTheme(QStringLiteral("dialog-information"));
206  // default size is 32
207  setMessageIcon(infoIcon.pixmap(32, 32));
208 
209  d->m_timer.setSingleShot(true);
210 
211  setAcceptHoverEvents(true);
212  // ignore scene transformations
214 
215  // setup default colors
217  d->m_textChildItem->setTextColor(KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalText));
218 
219  connect(&d->m_timeLine, &QTimeLine::frameChanged, this, &KGamePopupItem::animationFrame);
220  connect(&d->m_timeLine, &QTimeLine::finished, this, &KGamePopupItem::hideMe);
221  connect(&d->m_timer, &QTimer::timeout, this, &KGamePopupItem::playHideAnimation);
222 }
223 
225 
227 {
228  Q_UNUSED(option);
229 
230  p->save();
231 
232  QPen pen = p->pen();
233  pen.setWidthF(BORDER_PEN_WIDTH);
234  p->setPen(pen);
235 
236  if (d->m_animOpacity != -1) // playing Center animation
237  {
238  p->setOpacity(d->m_animOpacity);
239  } else {
240  p->setOpacity(d->m_opacity);
241  }
242  p->setBrush(widget ? d->m_brush.brush(widget->palette()) : QBrush());
243  p->drawPath(d->m_path);
244  p->drawPixmap(MARGIN, static_cast<int>(d->m_boundRect.height() / 2) - d->m_iconPix.height() / 2.0 / d->m_iconPix.devicePixelRatio(), d->m_iconPix);
245  p->restore();
246 }
247 
249 {
250  if (d->m_timeLine.state() == QTimeLine::Running || d->m_timer.isActive()) {
251  if (mode == ReplacePrevious) {
252  forceHide(InstantHide);
253  } else {
254  return; // we're already showing a message
255  }
256  }
257 
258  // NOTE: we blindly take first visible view we found. I.e. we don't support
259  // multiple views. If no visible scene is found, we simply pick the first one.
260  QGraphicsView *sceneView = nullptr;
261  const auto views = scene()->views();
262  for (QGraphicsView *view : views) {
263  if (view->isVisible()) {
264  sceneView = view;
265  break;
266  }
267  }
268  if (!sceneView) {
269  sceneView = views.at(0);
270  }
271 
272  QPolygonF poly = sceneView->mapToScene(sceneView->viewport()->contentsRect());
273  d->m_visibleSceneRect = poly.boundingRect();
274 
275  d->m_textChildItem->setHtml(text);
276 
277  d->m_position = pos;
278 
279  // do as QGS docs say: notify the scene about rect change
281 
282  // recalculate bounding rect
283  const qreal iconDpr = d->m_iconPix.devicePixelRatio();
284  qreal w = d->m_textChildItem->boundingRect().width() + MARGIN * 2 + d->m_iconPix.width() / iconDpr + SOME_SPACE;
285  qreal h = d->m_textChildItem->boundingRect().height() + MARGIN * 2;
286  if (d->m_iconPix.height() / iconDpr > h) {
287  h = d->m_iconPix.height() / iconDpr + MARGIN * 2;
288  }
289  d->m_boundRect = QRect(0, 0, w, h);
290 
291  // adjust to take into account the width of the pen
292  // used to draw the border
293  const qreal borderRadius = BORDER_PEN_WIDTH / 2.0;
294  d->m_boundRect.adjust(-borderRadius, -borderRadius, borderRadius, borderRadius);
295 
296  d->m_mappedBoundRect = sceneView->mapToScene(d->m_boundRect).boundingRect();
297  d->m_mapped_SHOW_OFFSET = qAbs(sceneView->mapToScene(0, SHOW_OFFSET).y());
298 
299  QPainterPath roundRectPath;
300  roundRectPath.moveTo(w, d->m_sharpness);
301  roundRectPath.arcTo(w - (2 * d->m_sharpness), 0.0, (2 * d->m_sharpness), (d->m_sharpness), 0.0, 90.0);
302  roundRectPath.lineTo(d->m_sharpness, 0.0);
303  roundRectPath.arcTo(0.0, 0.0, (2 * d->m_sharpness), (2 * d->m_sharpness), 90.0, 90.0);
304  roundRectPath.lineTo(0.0, h - (d->m_sharpness));
305  roundRectPath.arcTo(0.0, h - (2 * d->m_sharpness), 2 * d->m_sharpness, 2 * d->m_sharpness, 180.0, 90.0);
306  roundRectPath.lineTo(w - (d->m_sharpness), h);
307  roundRectPath.arcTo(w - (2 * d->m_sharpness), h - (2 * d->m_sharpness), (2 * d->m_sharpness), (2 * d->m_sharpness), 270.0, 90.0);
308  roundRectPath.closeSubpath();
309 
310  d->m_path = roundRectPath;
311 
312  // adjust y-pos of text item so it appears centered
313  d->m_textChildItem->setPos(d->m_textChildItem->x(), d->m_boundRect.height() / 2 - d->m_textChildItem->boundingRect().height() / 2);
314 
315  // setup animation
316  setupTimeline();
317 
318  // move to the start position
319  animationFrame(d->m_timeLine.startFrame());
320  show();
321  d->m_timeLine.start();
322 
323  if (d->m_timeout != 0) {
324  // 300 msec to animate showing message + d->m_timeout to stay visible => then hide
325  d->m_timer.start(300 + d->m_timeout);
326  }
327 }
328 
329 void KGamePopupItem::setupTimeline()
330 {
331  d->m_timeLine.setDirection(QTimeLine::Forward);
332  d->m_timeLine.setDuration(300);
333  if (d->m_position == TopLeft || d->m_position == TopRight) {
334  int start = static_cast<int>(d->m_visibleSceneRect.top() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
335  int end = static_cast<int>(d->m_visibleSceneRect.top() + d->m_mapped_SHOW_OFFSET);
336  d->m_timeLine.setFrameRange(start, end);
337  } else if (d->m_position == BottomLeft || d->m_position == BottomRight) {
338  int start = static_cast<int>(d->m_visibleSceneRect.bottom() + d->m_mapped_SHOW_OFFSET);
339  int end = static_cast<int>(d->m_visibleSceneRect.bottom() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
340  d->m_timeLine.setFrameRange(start, end);
341  } else if (d->m_position == Center) {
342  d->m_timeLine.setFrameRange(0, d->m_timeLine.duration());
343  setPos(d->m_visibleSceneRect.left() + d->m_visibleSceneRect.width() / 2 - d->m_mappedBoundRect.width() / 2,
344  d->m_visibleSceneRect.top() + d->m_visibleSceneRect.height() / 2 - d->m_mappedBoundRect.height() / 2);
345  }
346 }
347 
348 void KGamePopupItem::animationFrame(int frame)
349 {
350  if (d->m_position == TopLeft || d->m_position == BottomLeft) {
351  setPos(d->m_visibleSceneRect.left() + d->m_mapped_SHOW_OFFSET, frame);
352  } else if (d->m_position == TopRight || d->m_position == BottomRight) {
353  setPos(d->m_visibleSceneRect.right() - d->m_mappedBoundRect.width() - d->m_mapped_SHOW_OFFSET, frame);
354  } else if (d->m_position == Center) {
355  d->m_animOpacity = frame * d->m_opacity / d->m_timeLine.duration();
356  d->m_textChildItem->setOpacity(d->m_animOpacity);
357  update();
358  }
359 }
360 
361 void KGamePopupItem::playHideAnimation()
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 {
373  d->m_timeout = msec;
374 }
375 
377 {
378  d->m_hideOnClick = hide;
379 }
380 
382 {
383  return d->m_hideOnClick;
384 }
385 
387 {
388  d->m_opacity = opacity;
389  d->m_textChildItem->setOpacity(opacity);
390 }
391 
393 {
394  return d->m_boundRect;
395 }
396 
397 void KGamePopupItem::hideMe()
398 {
399  d->m_animOpacity = -1;
400  // and restore child's opacity too
401  d->m_textChildItem->setOpacity(d->m_opacity);
402 
403  // if we just got moved out of visibility, let's do more - let's hide :)
404  if (d->m_timeLine.direction() == QTimeLine::Backward) {
405  hide();
406  Q_EMIT hidden();
407  }
408 }
409 
410 void KGamePopupItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
411 {
412  d->m_hoveredByMouse = true;
413 }
414 
415 void KGamePopupItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
416 {
417  d->m_hoveredByMouse = false;
418 
419  if (d->m_timeout != 0 && !d->m_timer.isActive() && d->m_timeLine.state() != QTimeLine::Running) {
420  playHideAnimation(); // let's hide
421  }
422 }
423 
425 {
426  d->m_iconPix = pix;
427  d->m_textChildItem->setPos(MARGIN + pix.width() / pix.devicePixelRatio() + SOME_SPACE, MARGIN);
428  // bounding rect is updated in showMessage()
429 }
430 
432 {
433  return d->m_timeout;
434 }
435 
437 {
438  if (!isVisible()) {
439  return;
440  }
441 
442  if (howToHide == InstantHide) {
443  d->m_timeLine.stop();
444  d->m_timer.stop();
445  hide();
446  Q_EMIT hidden();
447  } else if (howToHide == AnimatedHide) {
448  // forcefully unset it even if it is set
449  // so we'll hide in any event
450  d->m_hoveredByMouse = false;
451  d->m_timer.stop();
452  playHideAnimation();
453  }
454 }
455 
457 {
458  return d->m_opacity;
459 }
460 
462 {
463  d->m_brush = KStatefulBrush(brush);
464 }
465 
467 {
468  KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active));
469  d->m_textChildItem->setTextColor(brush);
470 }
471 
472 void KGamePopupItem::onLinkHovered(const QString &link)
473 {
474  if (link.isEmpty()) {
475  d->m_textChildItem->setCursor(Qt::ArrowCursor);
476  } else {
477  d->m_textChildItem->setCursor(Qt::PointingHandCursor);
478  }
479 
480  d->m_linkHovered = !link.isEmpty();
481  Q_EMIT linkHovered(link);
482 }
483 
485 {
486  d->m_sharpness = sharpness;
487 }
488 
490 {
491  return d->m_sharpness;
492 }
493 
494 void KGamePopupItem::mousePressEvent(QGraphicsSceneMouseEvent *)
495 {
496  // it is needed to reimplement this function to receive future
497  // mouse release events
498 }
499 
500 void KGamePopupItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *)
501 {
502  // NOTE: text child item is QGraphicsTextItem which "eats" mouse events
503  // because of interaction with links. Because of that TextItemWithOpacity has
504  // special signal to indicate mouse click which we catch in a onTextItemClicked()
505  // slot
506  if (d->m_hideOnClick) {
507  forceHide();
508  }
509 }
510 
511 void KGamePopupItem::onTextItemClicked()
512 {
513  // if link is hovered we don't hide as click should go to the link
514  if (d->m_hideOnClick && !d->m_linkHovered) {
515  forceHide();
516  }
517 }
518 
519 #include "kgamepopupitem.moc" // For automocing TextItemWithOpacity
520 #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
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 Sat Jan 28 2023 03:56:45 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.