1 | /* |
2 | * Copyright 2007 by Dan Meltzer <hydrogen@notyetimplemented.com> |
3 | * Copyright 2008 by Aaron Seigo <aseigo@kde.org> |
4 | * Copyright 2008 by Alexis Ménard <darktears31@gmail.com> |
5 | * |
6 | * This library is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Lesser General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2.1 of the License, or (at your option) any later version. |
10 | * |
11 | * This library is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Lesser General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Lesser General Public |
17 | * License along with this library; if not, write to the Free Software |
18 | * Foundation, Inc., 51 Franklin St, Fifth Floor, |
19 | * Boston, MA 02110-1301 USA |
20 | */ |
21 | |
22 | #include "tooltipmanager.h" |
23 | |
24 | //Qt |
25 | #include <QCoreApplication> |
26 | #include <QLabel> |
27 | #include <QTimer> |
28 | #include <QGridLayout> |
29 | #include <QGraphicsView> |
30 | #include <QGraphicsSceneHoverEvent> |
31 | |
32 | //KDE |
33 | #include <kwindowsystem.h> |
34 | |
35 | //X11 |
36 | #ifdef Q_WS_X11 |
37 | #include <QtGui/QX11Info> |
38 | #include <X11/Xlib.h> |
39 | #include <fixx11h.h> |
40 | #endif |
41 | |
42 | //Plasma |
43 | #include "plasma/applet.h" |
44 | #include "plasma/containment.h" |
45 | #include "plasma/corona.h" |
46 | #include "plasma/framesvg.h" |
47 | #include "plasma/popupapplet.h" |
48 | #include "plasma/theme.h" |
49 | #include "plasma/view.h" |
50 | #include "plasma/private/tooltip_p.h" |
51 | #include "plasma/private/dialogshadows_p.h" |
52 | |
53 | namespace Plasma |
54 | { |
55 | |
56 | class ToolTipManagerPrivate |
57 | { |
58 | public : |
59 | ToolTipManagerPrivate(ToolTipManager *manager) |
60 | : q(manager), |
61 | shadow(new DialogShadows(q, "widgets/tooltip" )), |
62 | currentWidget(0), |
63 | showTimer(new QTimer(manager)), |
64 | hideTimer(new QTimer(manager)), |
65 | tipWidget(0), |
66 | state(ToolTipManager::Activated), |
67 | isShown(false), |
68 | delayedHide(false), |
69 | clickable(false) |
70 | { |
71 | } |
72 | |
73 | ~ToolTipManagerPrivate() |
74 | { |
75 | if (!QCoreApplication::closingDown()) { |
76 | shadow->removeWindow(tipWidget); |
77 | delete tipWidget; |
78 | } |
79 | } |
80 | |
81 | void showToolTip(); |
82 | void resetShownState(); |
83 | |
84 | /** |
85 | * called when a widget inside the tooltip manager is deleted |
86 | */ |
87 | void onWidgetDestroyed(QObject * object); |
88 | void removeWidget(QGraphicsWidget *w, bool canSafelyAccess = true); |
89 | void clearTips(); |
90 | void doDelayedHide(); |
91 | void toolTipHovered(bool); |
92 | void createTipWidget(); |
93 | void hideTipWidget(); |
94 | |
95 | ToolTipManager *q; |
96 | DialogShadows *shadow; |
97 | QGraphicsWidget *currentWidget; |
98 | QTimer *showTimer; |
99 | QTimer *hideTimer; |
100 | QHash<QGraphicsWidget *, ToolTipContent> tooltips; |
101 | ToolTip *tipWidget; |
102 | ToolTipManager::State state; |
103 | bool isShown : 1; |
104 | bool delayedHide : 1; |
105 | bool clickable : 1; |
106 | }; |
107 | |
108 | //TOOLTIP IMPLEMENTATION |
109 | class ToolTipManagerSingleton |
110 | { |
111 | public: |
112 | ToolTipManagerSingleton() |
113 | { |
114 | } |
115 | ToolTipManager self; |
116 | }; |
117 | K_GLOBAL_STATIC(ToolTipManagerSingleton, privateInstance) |
118 | |
119 | ToolTipManager *ToolTipManager::self() |
120 | { |
121 | return &privateInstance->self; |
122 | } |
123 | |
124 | ToolTipManager::ToolTipManager(QObject *parent) |
125 | : QObject(parent), |
126 | d(new ToolTipManagerPrivate(this)), |
127 | m_corona(0) |
128 | { |
129 | d->showTimer->setSingleShot(true); |
130 | connect(d->showTimer, SIGNAL(timeout()), SLOT(showToolTip())); |
131 | |
132 | d->hideTimer->setSingleShot(true); |
133 | connect(d->hideTimer, SIGNAL(timeout()), SLOT(resetShownState())); |
134 | } |
135 | |
136 | ToolTipManager::~ToolTipManager() |
137 | { |
138 | delete d; |
139 | } |
140 | |
141 | void ToolTipManager::show(QGraphicsWidget *widget) |
142 | { |
143 | if (!d->tooltips.contains(widget)) { |
144 | return; |
145 | } |
146 | |
147 | d->delayedHide = false; |
148 | d->hideTimer->stop(); |
149 | d->showTimer->stop(); |
150 | const int defaultDelay = Theme::defaultTheme()->toolTipDelay(); |
151 | |
152 | if (defaultDelay < 0) { |
153 | return; |
154 | } |
155 | |
156 | ToolTipContent content = d->tooltips[widget]; |
157 | qreal delay = content.isInstantPopup() ? 0.0 : defaultDelay; |
158 | |
159 | d->currentWidget = widget; |
160 | |
161 | if (d->isShown) { |
162 | // small delay to prevent unnecessary showing when the mouse is moving quickly across items |
163 | // which can be too much for less powerful CPUs to keep up with |
164 | d->showTimer->start(200); |
165 | } else { |
166 | d->showTimer->start(qMax(qreal(200), delay)); |
167 | } |
168 | } |
169 | |
170 | bool ToolTipManager::isVisible(QGraphicsWidget *widget) const |
171 | { |
172 | return d->currentWidget == widget && d->tipWidget && d->tipWidget->isVisible(); |
173 | } |
174 | |
175 | void ToolTipManagerPrivate::doDelayedHide() |
176 | { |
177 | showTimer->stop(); // stop the timer to show the tooltip |
178 | delayedHide = true; |
179 | |
180 | if (isShown && clickable) { |
181 | // leave enough time for user to choose |
182 | hideTimer->start(1000); |
183 | } else { |
184 | hideTimer->start(250); |
185 | } |
186 | } |
187 | |
188 | void ToolTipManager::hide(QGraphicsWidget *widget) |
189 | { |
190 | if (d->currentWidget != widget) { |
191 | return; |
192 | } |
193 | |
194 | d->currentWidget = 0; |
195 | d->showTimer->stop(); // stop the timer to show the tooltip |
196 | d->delayedHide = false; |
197 | d->hideTipWidget(); |
198 | } |
199 | |
200 | void ToolTipManager::registerWidget(QGraphicsWidget *widget) |
201 | { |
202 | if (d->state == Deactivated || d->tooltips.contains(widget)) { |
203 | return; |
204 | } |
205 | |
206 | //the tooltip is not registered we add it in our map of tooltips |
207 | d->tooltips.insert(widget, ToolTipContent()); |
208 | widget->installEventFilter(this); |
209 | connect(widget, SIGNAL(destroyed(QObject*)), this, SLOT(onWidgetDestroyed(QObject*))); |
210 | } |
211 | |
212 | void ToolTipManager::unregisterWidget(QGraphicsWidget *widget) |
213 | { |
214 | if (!d->tooltips.contains(widget)) { |
215 | return; |
216 | } |
217 | |
218 | if (widget == d->currentWidget) { |
219 | d->currentWidget = 0; |
220 | d->showTimer->stop(); // stop the timer to show the tooltip |
221 | d->delayedHide = false; |
222 | d->hideTipWidget(); |
223 | } |
224 | |
225 | widget->removeEventFilter(this); |
226 | d->removeWidget(widget); |
227 | } |
228 | |
229 | void ToolTipManager::setContent(QGraphicsWidget *widget, const ToolTipContent &data) |
230 | { |
231 | if (d->state == Deactivated || !widget) { |
232 | return; |
233 | } |
234 | |
235 | registerWidget(widget); |
236 | d->tooltips.insert(widget, data); |
237 | |
238 | if (d->currentWidget == widget && d->tipWidget && d->tipWidget->isVisible()) { |
239 | if (data.isEmpty()) { |
240 | // after this call, d->tipWidget will be null |
241 | hide(widget); |
242 | } else { |
243 | d->delayedHide = data.autohide(); |
244 | d->clickable = data.isClickable(); |
245 | if (d->delayedHide) { |
246 | //kDebug() << "starting authoide"; |
247 | d->hideTimer->start(3000); |
248 | } else { |
249 | d->hideTimer->stop(); |
250 | } |
251 | } |
252 | |
253 | if (d->tipWidget) { |
254 | d->tipWidget->setContent(widget, data); |
255 | d->tipWidget->prepareShowing(); |
256 | |
257 | //look if the data prefers aother graphicswidget, otherwise use the one used as event catcher |
258 | QGraphicsWidget *referenceWidget = data.graphicsWidget() ? data.graphicsWidget() : widget; |
259 | Corona *corona = qobject_cast<Corona *>(referenceWidget->scene()); |
260 | if (!corona) { |
261 | // fallback to the corona we were given |
262 | corona = m_corona; |
263 | } |
264 | |
265 | if (corona) { |
266 | d->tipWidget->moveTo(corona->popupPosition(referenceWidget, d->tipWidget->size(), Qt::AlignCenter)); |
267 | } |
268 | } |
269 | } |
270 | } |
271 | |
272 | void ToolTipManager::clearContent(QGraphicsWidget *widget) |
273 | { |
274 | setContent(widget, ToolTipContent()); |
275 | } |
276 | |
277 | void ToolTipManager::setState(ToolTipManager::State state) |
278 | { |
279 | d->state = state; |
280 | |
281 | switch (state) { |
282 | case Activated: |
283 | break; |
284 | case Deactivated: |
285 | d->clearTips(); |
286 | //fallthrough |
287 | case Inhibited: |
288 | d->resetShownState(); |
289 | break; |
290 | } |
291 | } |
292 | |
293 | ToolTipManager::State ToolTipManager::state() const |
294 | { |
295 | return d->state; |
296 | } |
297 | |
298 | void ToolTipManagerPrivate::createTipWidget() |
299 | { |
300 | if (tipWidget) { |
301 | return; |
302 | } |
303 | |
304 | tipWidget = new ToolTip(0); |
305 | shadow->addWindow(tipWidget); |
306 | |
307 | QObject::connect(tipWidget, SIGNAL(activateWindowByWId(WId,Qt::MouseButtons,Qt::KeyboardModifiers,QPoint)), |
308 | q, SIGNAL(windowPreviewActivated(WId,Qt::MouseButtons,Qt::KeyboardModifiers,QPoint))); |
309 | QObject::connect(tipWidget, SIGNAL(linkActivated(QString,Qt::MouseButtons,Qt::KeyboardModifiers,QPoint)), |
310 | q, SIGNAL(linkActivated(QString,Qt::MouseButtons,Qt::KeyboardModifiers,QPoint))); |
311 | QObject::connect(tipWidget, SIGNAL(hovered(bool)), q, SLOT(toolTipHovered(bool))); |
312 | } |
313 | |
314 | void ToolTipManagerPrivate::hideTipWidget() |
315 | { |
316 | if (tipWidget) { |
317 | tipWidget->hide(); |
318 | shadow->removeWindow(tipWidget); |
319 | tipWidget->deleteLater(); |
320 | tipWidget = 0; |
321 | } |
322 | } |
323 | |
324 | void ToolTipManagerPrivate::onWidgetDestroyed(QObject *object) |
325 | { |
326 | if (!object) { |
327 | return; |
328 | } |
329 | |
330 | // we do a static_cast here since it really isn't a QGraphicsWidget by this |
331 | // point anymore since we are in the QObject dtor. we don't actually |
332 | // try and do anything with it, we just need the value of the pointer |
333 | // so this unsafe looking code is actually just fine. |
334 | // |
335 | // NOTE: DO NOT USE THE w VARIABLE FOR ANYTHING OTHER THAN COMPARING |
336 | // THE ADDRESS! ACTUALLY USING THE OBJECT WILL RESULT IN A CRASH!!! |
337 | QGraphicsWidget *w = static_cast<QGraphicsWidget*>(object); |
338 | removeWidget(w, false); |
339 | } |
340 | |
341 | void ToolTipManagerPrivate::removeWidget(QGraphicsWidget *w, bool canSafelyAccess) |
342 | { |
343 | if (currentWidget == w && currentWidget) { |
344 | currentWidget = 0; |
345 | showTimer->stop(); // stop the timer to show the tooltip |
346 | hideTipWidget(); |
347 | delayedHide = false; |
348 | } |
349 | |
350 | if (w && canSafelyAccess) { |
351 | QObject::disconnect(q, 0, w, 0); |
352 | } |
353 | |
354 | tooltips.remove(w); |
355 | } |
356 | |
357 | void ToolTipManagerPrivate::clearTips() |
358 | { |
359 | tooltips.clear(); |
360 | } |
361 | |
362 | void ToolTipManagerPrivate::resetShownState() |
363 | { |
364 | if (!tipWidget || !tipWidget->isVisible() || delayedHide) { |
365 | //One might have moused out and back in again |
366 | showTimer->stop(); |
367 | delayedHide = false; |
368 | isShown = false; |
369 | currentWidget = 0; |
370 | hideTipWidget(); |
371 | } |
372 | } |
373 | |
374 | void ToolTipManagerPrivate::showToolTip() |
375 | { |
376 | if (state != ToolTipManager::Activated || |
377 | !currentWidget || |
378 | QApplication::activePopupWidget() || |
379 | QApplication::activeModalWidget()) { |
380 | return; |
381 | } |
382 | |
383 | PopupApplet * = qobject_cast<PopupApplet*>(currentWidget); |
384 | if (popup && popup->isPopupShowing()) { |
385 | return; |
386 | } |
387 | |
388 | if (currentWidget->metaObject()->indexOfMethod("toolTipAboutToShow()" ) != -1) { |
389 | // toolTipAboutToShow may call into methods such as setContent which play |
390 | // with the current widget; so let's just pretend for a moment that we don't have |
391 | // a current widget |
392 | QGraphicsWidget *temp = currentWidget; |
393 | currentWidget = 0; |
394 | QMetaObject::invokeMethod(temp, "toolTipAboutToShow" ); |
395 | currentWidget = temp; |
396 | } |
397 | |
398 | QHash<QGraphicsWidget *, ToolTipContent>::const_iterator tooltip = tooltips.constFind(currentWidget); |
399 | |
400 | if (tooltip == tooltips.constEnd() || tooltip.value().isEmpty()) { |
401 | if (isShown) { |
402 | delayedHide = true; |
403 | hideTimer->start(250); |
404 | } |
405 | |
406 | return; |
407 | } |
408 | |
409 | createTipWidget(); |
410 | |
411 | Containment *c = dynamic_cast<Containment *>(currentWidget->topLevelItem()); |
412 | //kDebug() << "about to show" << (QObject*)c; |
413 | if (c) { |
414 | tipWidget->setDirection(Plasma::locationToDirection(c->location())); |
415 | } |
416 | |
417 | clickable = tooltip.value().isClickable(); |
418 | tipWidget->setContent(currentWidget, tooltip.value()); |
419 | tipWidget->prepareShowing(); |
420 | QGraphicsWidget *referenceWidget = tooltip.value().graphicsWidget() ? tooltip.value().graphicsWidget() : currentWidget; |
421 | Corona *corona = qobject_cast<Corona *>(referenceWidget->scene()); |
422 | if (!corona) { |
423 | // fallback to the corona we were given |
424 | corona = q->m_corona; |
425 | } |
426 | |
427 | if (corona) { |
428 | tipWidget->moveTo(corona->popupPosition(referenceWidget, tipWidget->size(), Qt::AlignCenter)); |
429 | } |
430 | tipWidget->show(); |
431 | isShown = true; //ToolTip is visible |
432 | |
433 | delayedHide = tooltip.value().autohide(); |
434 | if (delayedHide) { |
435 | //kDebug() << "starting authoide"; |
436 | hideTimer->start(3000); |
437 | } else { |
438 | hideTimer->stop(); |
439 | } |
440 | } |
441 | |
442 | void ToolTipManagerPrivate::toolTipHovered(bool hovered) |
443 | { |
444 | if (!clickable) { |
445 | return; |
446 | } |
447 | |
448 | if (hovered) { |
449 | hideTimer->stop(); |
450 | } else { |
451 | hideTimer->start(500); |
452 | } |
453 | } |
454 | |
455 | bool ToolTipManager::eventFilter(QObject *watched, QEvent *event) |
456 | { |
457 | QGraphicsWidget * widget = dynamic_cast<QGraphicsWidget *>(watched); |
458 | if (d->state != Activated || !widget) { |
459 | return QObject::eventFilter(watched, event); |
460 | } |
461 | |
462 | switch (event->type()) { |
463 | case QEvent::GraphicsSceneHoverMove: |
464 | // If the tooltip isn't visible, run through showing the tooltip again |
465 | // so that it only becomes visible after a stationary hover |
466 | if (Plasma::ToolTipManager::self()->isVisible(widget)) { |
467 | break; |
468 | } |
469 | |
470 | // Don't restart the show timer on a mouse move event if there hasn't |
471 | // been an enter event or the current widget has been cleared by a click |
472 | // or wheel event. |
473 | { |
474 | QGraphicsSceneHoverEvent *me = static_cast<QGraphicsSceneHoverEvent *>(event); |
475 | //FIXME: seems that wheel events generate hovermoves as well, with 0 delta |
476 | if (!d->currentWidget || (me->pos() == me->lastPos())) { |
477 | break; |
478 | } |
479 | } |
480 | |
481 | case QEvent::GraphicsSceneHoverEnter: |
482 | { |
483 | // Check that there is a tooltip to show |
484 | if (!d->tooltips.contains(widget)) { |
485 | break; |
486 | } |
487 | |
488 | show(widget); |
489 | break; |
490 | } |
491 | |
492 | case QEvent::GraphicsSceneHoverLeave: |
493 | if (d->currentWidget == widget) { |
494 | d->doDelayedHide(); |
495 | } |
496 | break; |
497 | |
498 | case QEvent::GraphicsSceneMousePress: |
499 | if (d->currentWidget == widget) { |
500 | hide(widget); |
501 | } |
502 | break; |
503 | |
504 | case QEvent::GraphicsSceneWheel: |
505 | default: |
506 | break; |
507 | } |
508 | |
509 | return QObject::eventFilter(watched, event); |
510 | } |
511 | |
512 | } // Plasma namespace |
513 | |
514 | #include "tooltipmanager.moc" |
515 | |
516 | |