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 |
31 | static const int MARGIN = 15; |
32 | // offset of message from start of the scene |
33 | static const int SHOW_OFFSET = 5; |
34 | // space between pixmap and text |
35 | static const int SOME_SPACE = 10; |
36 | // width of the border in pixels |
37 | static const qreal BORDER_PEN_WIDTH = 1.0; |
38 | |
39 | class TextItemWithOpacity : public QGraphicsTextItem |
40 | { |
41 | Q_OBJECT |
42 | |
43 | public: |
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 | |
50 | Q_SIGNALS: |
51 | void mouseClicked(); |
52 | |
53 | private: |
54 | void mouseReleaseEvent(QGraphicsSceneMouseEvent*); |
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 = 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 | |
81 | void 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 | |
90 | class |
91 | { |
92 | private: |
93 | (const KGamePopupItemPrivate&); |
94 | const KGamePopupItemPrivate& (const KGamePopupItemPrivate&); |
95 | public: |
96 | () |
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 ; |
105 | /** |
106 | * Timer used to start hiding |
107 | */ |
108 | QTimer ; |
109 | /** |
110 | * Holds bounding rect of an item |
111 | */ |
112 | QRectF ; |
113 | /** |
114 | * Position where item will appear |
115 | */ |
116 | KGamePopupItem::Position ; |
117 | /** |
118 | * Timeout to stay visible on screen |
119 | */ |
120 | int ; |
121 | /** |
122 | * Item opacity |
123 | */ |
124 | qreal ; |
125 | /** |
126 | * Opacity used while animating appearing in center |
127 | */ |
128 | qreal ; |
129 | /** |
130 | * Pixmap to display at the left of the text |
131 | */ |
132 | QPixmap ; |
133 | /** |
134 | * Set to true when mouse hovers the message |
135 | */ |
136 | bool ; |
137 | /** |
138 | * Set to true if this popup item hides on mouse click. |
139 | */ |
140 | bool ; |
141 | /** |
142 | * Child of KGamePopupItem used to display text |
143 | */ |
144 | TextItemWithOpacity* ; |
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 ; |
151 | /** |
152 | * Background brush color |
153 | */ |
154 | KStatefulBrush ; |
155 | /** |
156 | * popup angles sharpness |
157 | */ |
158 | KGamePopupItem::Sharpness ; |
159 | /** |
160 | * painter path to draw a frame |
161 | */ |
162 | QPainterPath ; |
163 | /** |
164 | * Indicates if some link is hovered in text item |
165 | */ |
166 | bool ; |
167 | }; |
168 | |
169 | 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 | |
208 | void KGamePopupItem::( 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 | |
234 | void KGamePopupItem::( 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 | |
321 | void KGamePopupItem::() |
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 | |
348 | void KGamePopupItem::(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 | |
366 | void KGamePopupItem::() |
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 | |
377 | void KGamePopupItem::( int msec ) |
378 | { |
379 | d->m_timeout = msec; |
380 | } |
381 | |
382 | void KGamePopupItem::( bool hide ) |
383 | { |
384 | d->m_hideOnClick = hide; |
385 | } |
386 | |
387 | bool KGamePopupItem::() const |
388 | { |
389 | return d->m_hideOnClick; |
390 | } |
391 | |
392 | void KGamePopupItem::( qreal opacity ) |
393 | { |
394 | d->m_opacity = opacity; |
395 | d->m_textChildItem->setOpacity(opacity); |
396 | } |
397 | |
398 | QRectF KGamePopupItem::() const |
399 | { |
400 | return d->m_boundRect; |
401 | } |
402 | |
403 | KGamePopupItem::() |
404 | { |
405 | delete d; |
406 | } |
407 | |
408 | void KGamePopupItem::() |
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 | |
422 | void KGamePopupItem::( QGraphicsSceneHoverEvent* ) |
423 | { |
424 | d->m_hoveredByMouse = true; |
425 | } |
426 | |
427 | void KGamePopupItem::( 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 | |
437 | void KGamePopupItem::( 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 | |
444 | int KGamePopupItem::() const |
445 | { |
446 | return d->m_timeout; |
447 | } |
448 | |
449 | void KGamePopupItem::(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 | |
473 | qreal KGamePopupItem::() const |
474 | { |
475 | return d->m_opacity; |
476 | } |
477 | |
478 | void KGamePopupItem::( const QBrush& brush ) |
479 | { |
480 | d->m_brush = KStatefulBrush(brush); |
481 | } |
482 | |
483 | void KGamePopupItem::( const QColor& color ) |
484 | { |
485 | KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active)); |
486 | d->m_textChildItem->setTextColor(brush); |
487 | } |
488 | |
489 | void KGamePopupItem::(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 | |
504 | void KGamePopupItem::( Sharpness sharpness ) |
505 | { |
506 | d->m_sharpness = sharpness; |
507 | } |
508 | |
509 | KGamePopupItem::Sharpness KGamePopupItem::() const |
510 | { |
511 | return d->m_sharpness; |
512 | } |
513 | |
514 | void KGamePopupItem::( QGraphicsSceneMouseEvent* ) |
515 | { |
516 | // it is needed to reimplement this function to receive future |
517 | // mouse release events |
518 | } |
519 | |
520 | void KGamePopupItem::( 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 | |
532 | void KGamePopupItem::() |
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 | |