1/*******************************************************************
2 Copyright 2007 Dmitry Suzdalev <dimsuz@gmail.com>
3
4 This library is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 ********************************************************************/
18#include "kgamepopupitem.h"
19#include <QPainter>
20#include <QTimeLine>
21#include <QTimer>
22#include <QGraphicsScene>
23#include <QGraphicsView>
24#include <QGraphicsTextItem>
25
26#include <KColorScheme>
27#include <KIcon>
28#include <KDebug>
29
30// margin on the sides of message box
31static const int MARGIN = 15;
32// offset of message from start of the scene
33static const int SHOW_OFFSET = 5;
34// space between pixmap and text
35static const int SOME_SPACE = 10;
36// width of the border in pixels
37static const qreal BORDER_PEN_WIDTH = 1.0;
38
39class TextItemWithOpacity : public QGraphicsTextItem
40{
41 Q_OBJECT
42
43public:
44 TextItemWithOpacity( QGraphicsItem* parent = 0 )
45 :QGraphicsTextItem(parent), m_opacity(1.0) {}
46 void setOpacity(qreal opa) { m_opacity = opa; }
47 void setTextColor(KStatefulBrush brush) { m_brush = brush; }
48 virtual void paint( QPainter* p, const QStyleOptionGraphicsItem *option, QWidget* widget );
49
50Q_SIGNALS:
51 void mouseClicked();
52
53private:
54 void mouseReleaseEvent(QGraphicsSceneMouseEvent*);
55
56private:
57 qreal m_opacity;
58 KStatefulBrush m_brush;
59};
60
61void 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 = m_brush.brush(widget).color();
70 if (textColor != defaultTextColor())
71 {
72 setDefaultTextColor(textColor);
73 }
74 //render contents
75 p->save();
76 p->setOpacity(m_opacity);
77 QGraphicsTextItem::paint(p,option,widget);
78 p->restore();
79}
80
81void TextItemWithOpacity::mouseReleaseEvent(QGraphicsSceneMouseEvent* ev)
82{
83 // NOTE: this item is QGraphicsTextItem which "eats" mouse events
84 // because of interaction with links. Because of that let's make a
85 // special signal to indicate mouse click
86 emit mouseClicked();
87 QGraphicsTextItem::mouseReleaseEvent(ev);
88}
89
90class KGamePopupItemPrivate
91{
92private:
93 KGamePopupItemPrivate(const KGamePopupItemPrivate&);
94 const KGamePopupItemPrivate& operator=(const KGamePopupItemPrivate&);
95public:
96 KGamePopupItemPrivate()
97 : m_position( KGamePopupItem::BottomLeft ), m_timeout(2000),
98 m_opacity(1.0), m_animOpacity(-1), m_hoveredByMouse(false),
99 m_hideOnClick(true), m_textChildItem(0),
100 m_sharpness(KGamePopupItem::Square), m_linkHovered(false) {}
101 /**
102 * Timeline for animations
103 */
104 QTimeLine m_timeLine;
105 /**
106 * Timer used to start hiding
107 */
108 QTimer m_timer;
109 /**
110 * Holds bounding rect of an item
111 */
112 QRectF m_boundRect;
113 /**
114 * Position where item will appear
115 */
116 KGamePopupItem::Position m_position;
117 /**
118 * Timeout to stay visible on screen
119 */
120 int m_timeout;
121 /**
122 * Item opacity
123 */
124 qreal m_opacity;
125 /**
126 * Opacity used while animating appearing in center
127 */
128 qreal m_animOpacity;
129 /**
130 * Pixmap to display at the left of the text
131 */
132 QPixmap m_iconPix;
133 /**
134 * Set to true when mouse hovers the message
135 */
136 bool m_hoveredByMouse;
137 /**
138 * Set to true if this popup item hides on mouse click.
139 */
140 bool m_hideOnClick;
141 /**
142 * Child of KGamePopupItem used to display text
143 */
144 TextItemWithOpacity* m_textChildItem;
145 /**
146 * Part of the scene that is actually visible in QGraphicsView
147 * This is needed for item to work correctly when scene is larger than
148 * the View
149 */
150 QRectF m_visibleSceneRect;
151 /**
152 * Background brush color
153 */
154 KStatefulBrush m_brush;
155 /**
156 * popup angles sharpness
157 */
158 KGamePopupItem::Sharpness m_sharpness;
159 /**
160 * painter path to draw a frame
161 */
162 QPainterPath m_path;
163 /**
164 * Indicates if some link is hovered in text item
165 */
166 bool m_linkHovered;
167};
168
169KGamePopupItem::KGamePopupItem(QGraphicsItem * parent)
170 : QGraphicsItem(parent), d(new KGamePopupItemPrivate)
171{
172 hide();
173 d->m_textChildItem = new TextItemWithOpacity(this);
174 d->m_textChildItem->setTextInteractionFlags( Qt::LinksAccessibleByMouse );
175 // above call said to enable ItemIsFocusable which we don't need.
176 // So disabling it
177 d->m_textChildItem->setFlag( QGraphicsItem::ItemIsFocusable, false );
178
179 connect( d->m_textChildItem, SIGNAL(linkActivated(QString)),
180 SIGNAL(linkActivated(QString)));
181 connect( d->m_textChildItem, SIGNAL(linkHovered(QString)),
182 SLOT(onLinkHovered(QString)));
183 connect( d->m_textChildItem, SIGNAL(mouseClicked()),
184 SLOT(onTextItemClicked()) );
185
186 setZValue(100); // is 100 high enough???
187 d->m_textChildItem->setZValue(100);
188
189 KIcon infoIcon( QLatin1String( "dialog-information" ));
190 // default size is 32
191 setMessageIcon( infoIcon.pixmap(32, 32) );
192
193 d->m_timer.setSingleShot(true);
194
195 setAcceptsHoverEvents(true);
196 // ignore scene transformations
197 setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
198
199 // setup default colors
200 d->m_brush = KStatefulBrush( KColorScheme::Tooltip, KColorScheme::NormalBackground );
201 d->m_textChildItem->setTextColor( KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalText) );
202
203 connect( &d->m_timeLine, SIGNAL(frameChanged(int)), SLOT(animationFrame(int)) );
204 connect( &d->m_timeLine, SIGNAL(finished()), SLOT(hideMe()));
205 connect( &d->m_timer, SIGNAL(timeout()), SLOT(playHideAnimation()) );
206}
207
208void KGamePopupItem::paint( QPainter* p, const QStyleOptionGraphicsItem *option, QWidget* widget )
209{
210 Q_UNUSED(option);
211 Q_UNUSED(widget);
212
213 p->save();
214
215 QPen pen = p->pen();
216 pen.setWidthF( BORDER_PEN_WIDTH );
217 p->setPen(pen);
218
219 if( d->m_animOpacity != -1) // playing Center animation
220 {
221 p->setOpacity(d->m_animOpacity);
222 }
223 else
224 {
225 p->setOpacity(d->m_opacity);
226 }
227 p->setBrush(d->m_brush.brush(widget));
228 p->drawPath(d->m_path);
229 p->drawPixmap( MARGIN, static_cast<int>(d->m_boundRect.height()/2) - d->m_iconPix.height()/2,
230 d->m_iconPix );
231 p->restore();
232}
233
234void KGamePopupItem::showMessage( const QString& text, Position pos, ReplaceMode mode )
235{
236 if(d->m_timeLine.state() == QTimeLine::Running || d->m_timer.isActive())
237 {
238 if (mode == ReplacePrevious)
239 {
240 forceHide(InstantHide);
241 }
242 else
243 {
244 return;// we're already showing a message
245 }
246 }
247
248 // NOTE: we blindly take first visible view we found. I.e. we don't support
249 // multiple views. If no visible scene is found, we simply pick the first one.
250 QGraphicsView *sceneView = 0;
251 foreach (QGraphicsView *view, scene()->views()) {
252 if (view->isVisible()) {
253 sceneView = view;
254 break;
255 }
256 }
257 if (!sceneView)
258 {
259 sceneView = scene()->views().at(0);
260 }
261
262 QPolygonF poly = sceneView->mapToScene( sceneView->viewport()->contentsRect() );
263 d->m_visibleSceneRect = poly.boundingRect();
264
265 d->m_textChildItem->setHtml(text);
266
267 d->m_position = pos;
268
269 // do as QGS docs say: notify the scene about rect change
270 prepareGeometryChange();
271
272 // recalculate bounding rect
273 qreal w = d->m_textChildItem->boundingRect().width()+MARGIN*2+d->m_iconPix.width()+SOME_SPACE;
274 qreal h = d->m_textChildItem->boundingRect().height()+MARGIN*2;
275 if( d->m_iconPix.height() > h )
276 {
277 h = d->m_iconPix.height() + MARGIN*2;
278 }
279 d->m_boundRect = QRectF(0, 0, w, h);
280
281 // adjust to take into account the width of the pen
282 // used to draw the border
283 const qreal borderRadius = BORDER_PEN_WIDTH / 2.0;
284 d->m_boundRect.adjust( -borderRadius ,
285 -borderRadius ,
286 borderRadius ,
287 borderRadius );
288
289 QPainterPath roundRectPath;
290 roundRectPath.moveTo(w, d->m_sharpness);
291 roundRectPath.arcTo(w-(2*d->m_sharpness), 0.0,(2*d->m_sharpness), (d->m_sharpness), 0.0, 90.0);
292 roundRectPath.lineTo(d->m_sharpness, 0.0);
293 roundRectPath.arcTo(0.0, 0.0, (2*d->m_sharpness), (2*d->m_sharpness), 90.0, 90.0);
294 roundRectPath.lineTo(0.0, h-(d->m_sharpness));
295 roundRectPath.arcTo(0.0, h-(2*d->m_sharpness), 2*d->m_sharpness, 2*d->m_sharpness, 180.0, 90.0);
296 roundRectPath.lineTo(w-(d->m_sharpness), h);
297 roundRectPath.arcTo(w-(2*d->m_sharpness), h-(2*d->m_sharpness), (2*d->m_sharpness), (2*d->m_sharpness), 270.0, 90.0);
298 roundRectPath.closeSubpath();
299
300 d->m_path = roundRectPath;
301
302 // adjust y-pos of text item so it appears centered
303 d->m_textChildItem->setPos( d->m_textChildItem->x(),
304 d->m_boundRect.height()/2 - d->m_textChildItem->boundingRect().height()/2);
305
306 // setup animation
307 setupTimeline();
308
309 // move to the start position
310 animationFrame(d->m_timeLine.startFrame());
311 show();
312 d->m_timeLine.start();
313
314 if(d->m_timeout != 0)
315 {
316 // 300 msec to animate showing message + d->m_timeout to stay visible => then hide
317 d->m_timer.start( 300+d->m_timeout );
318 }
319}
320
321void KGamePopupItem::setupTimeline()
322{
323 d->m_timeLine.setDirection( QTimeLine::Forward );
324 d->m_timeLine.setDuration(300);
325 if( d->m_position == TopLeft || d->m_position == TopRight )
326 {
327 int start = static_cast<int>(d->m_visibleSceneRect.top() - d->m_boundRect.height() - SHOW_OFFSET);
328 int end = static_cast<int>(d->m_visibleSceneRect.top() + SHOW_OFFSET);
329 d->m_timeLine.setFrameRange( start, end );
330 }
331 else if( d->m_position == BottomLeft || d->m_position == BottomRight )
332 {
333 int start = static_cast<int>(d->m_visibleSceneRect.bottom()+SHOW_OFFSET);
334 int end = static_cast<int>(d->m_visibleSceneRect.bottom() - d->m_boundRect.height() - SHOW_OFFSET);
335 d->m_timeLine.setFrameRange( start, end );
336 }
337 else if( d->m_position == Center )
338 {
339 d->m_timeLine.setFrameRange(0, d->m_timeLine.duration());
340 setPos( d->m_visibleSceneRect.left() +
341 d->m_visibleSceneRect.width()/2 - d->m_boundRect.width()/2,
342 d->m_visibleSceneRect.top() +
343 d->m_visibleSceneRect.height()/2 - d->m_boundRect.height()/2);
344 }
345
346}
347
348void KGamePopupItem::animationFrame(int frame)
349{
350 if( d->m_position == TopLeft || d->m_position == BottomLeft )
351 {
352 setPos( d->m_visibleSceneRect.left()+SHOW_OFFSET, frame );
353 }
354 else if( d->m_position == TopRight || d->m_position == BottomRight )
355 {
356 setPos( d->m_visibleSceneRect.right()-d->m_boundRect.width()-SHOW_OFFSET, frame );
357 }
358 else if( d->m_position == Center )
359 {
360 d->m_animOpacity = frame*d->m_opacity/d->m_timeLine.duration();
361 d->m_textChildItem->setOpacity( d->m_animOpacity );
362 update();
363 }
364}
365
366void KGamePopupItem::playHideAnimation()
367{
368 if( d->m_hoveredByMouse )
369 {
370 return;
371 }
372 // let's hide
373 d->m_timeLine.setDirection( QTimeLine::Backward );
374 d->m_timeLine.start();
375}
376
377void KGamePopupItem::setMessageTimeout( int msec )
378{
379 d->m_timeout = msec;
380}
381
382void KGamePopupItem::setHideOnMouseClick( bool hide )
383{
384 d->m_hideOnClick = hide;
385}
386
387bool KGamePopupItem::hidesOnMouseClick() const
388{
389 return d->m_hideOnClick;
390}
391
392void KGamePopupItem::setMessageOpacity( qreal opacity )
393{
394 d->m_opacity = opacity;
395 d->m_textChildItem->setOpacity(opacity);
396}
397
398QRectF KGamePopupItem::boundingRect() const
399{
400 return d->m_boundRect;
401}
402
403KGamePopupItem::~KGamePopupItem()
404{
405 delete d;
406}
407
408void KGamePopupItem::hideMe()
409{
410 d->m_animOpacity = -1;
411 // and restore child's opacity too
412 d->m_textChildItem->setOpacity(d->m_opacity);
413
414 // if we just got moved out of visibility, let's do more - let's hide :)
415 if( d->m_timeLine.direction() == QTimeLine::Backward )
416 {
417 hide();
418 emit hidden();
419 }
420}
421
422void KGamePopupItem::hoverEnterEvent( QGraphicsSceneHoverEvent* )
423{
424 d->m_hoveredByMouse = true;
425}
426
427void KGamePopupItem::hoverLeaveEvent( QGraphicsSceneHoverEvent* )
428{
429 d->m_hoveredByMouse = false;
430
431 if( d->m_timeout != 0 && !d->m_timer.isActive() && d->m_timeLine.state() != QTimeLine::Running )
432 {
433 playHideAnimation(); // let's hide
434 }
435}
436
437void KGamePopupItem::setMessageIcon( const QPixmap& pix )
438{
439 d->m_iconPix = pix;
440 d->m_textChildItem->setPos( MARGIN+pix.width()+SOME_SPACE, MARGIN );
441 // bounding rect is updated in showMessage()
442}
443
444int KGamePopupItem::messageTimeout() const
445{
446 return d->m_timeout;
447}
448
449void KGamePopupItem::forceHide(HideType howToHide)
450{
451 if(!isVisible())
452 {
453 return;
454 }
455
456 if(howToHide == InstantHide)
457 {
458 d->m_timeLine.stop();
459 d->m_timer.stop();
460 hide();
461 emit hidden();
462 }
463 else if(howToHide == AnimatedHide)
464 {
465 // forcefully unset it even if it is set
466 // so we'll hide in any event
467 d->m_hoveredByMouse = false;
468 d->m_timer.stop();
469 playHideAnimation();
470 }
471}
472
473qreal KGamePopupItem::messageOpacity() const
474{
475 return d->m_opacity;
476}
477
478void KGamePopupItem::setBackgroundBrush( const QBrush& brush )
479{
480 d->m_brush = KStatefulBrush(brush);
481}
482
483void KGamePopupItem::setTextColor( const QColor& color )
484{
485 KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active));
486 d->m_textChildItem->setTextColor(brush);
487}
488
489void KGamePopupItem::onLinkHovered(const QString& link)
490{
491 if(link.isEmpty())
492 {
493 d->m_textChildItem->setCursor( Qt::ArrowCursor );
494 }
495 else
496 {
497 d->m_textChildItem->setCursor( Qt::PointingHandCursor );
498 }
499
500 d->m_linkHovered = !link.isEmpty();
501 emit linkHovered(link);
502}
503
504void KGamePopupItem::setSharpness( Sharpness sharpness )
505{
506 d->m_sharpness = sharpness;
507}
508
509KGamePopupItem::Sharpness KGamePopupItem::sharpness() const
510{
511 return d->m_sharpness;
512}
513
514void KGamePopupItem::mousePressEvent( QGraphicsSceneMouseEvent* )
515{
516 // it is needed to reimplement this function to receive future
517 // mouse release events
518}
519
520void KGamePopupItem::mouseReleaseEvent( QGraphicsSceneMouseEvent* )
521{
522 // NOTE: text child item is QGraphicsTextItem which "eats" mouse events
523 // because of interaction with links. Because of that TextItemWithOpacity has
524 // special signal to indicate mouse click which we catch in a onTextItemClicked()
525 // slot
526 if (d->m_hideOnClick)
527 {
528 forceHide();
529 }
530}
531
532void KGamePopupItem::onTextItemClicked()
533{
534 // if link is hovered we don't hide as click should go to the link
535 if (d->m_hideOnClick && !d->m_linkHovered)
536 {
537 forceHide();
538 }
539}
540
541#include "moc_kgamepopupitem.cpp" // For automocing KGamePopupItem
542#include "kgamepopupitem.moc" // For automocing TextItemWithOpacity
543