1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "nsXULTooltipListener.h"
8 #include "nsIDOMMouseEvent.h"
9 #include "nsIDOMXULDocument.h"
10 #include "nsIDOMXULElement.h"
11 #include "nsIDocument.h"
12 #include "nsGkAtoms.h"
13 #include "nsIPopupBoxObject.h"
14 #include "nsMenuPopupFrame.h"
15 #include "nsIServiceManager.h"
16 #include "nsIDragService.h"
17 #include "nsIDragSession.h"
19 #include "nsITreeView.h"
21 #include "nsIScriptContext.h"
22 #include "nsPIDOMWindow.h"
24 #include "nsXULPopupManager.h"
26 #include "nsIRootBox.h"
27 #include "nsIBoxObject.h"
28 #include "mozilla/Preferences.h"
29 #include "mozilla/LookAndFeel.h"
30 #include "mozilla/dom/Element.h"
31 #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
33 using namespace mozilla
;
34 using namespace mozilla::dom
;
36 nsXULTooltipListener
* nsXULTooltipListener::mInstance
= nullptr;
38 //////////////////////////////////////////////////////////////////////////
41 nsXULTooltipListener::nsXULTooltipListener()
44 , mTooltipShownOnce(false)
46 , mIsSourceTree(false)
47 , mNeedTitletip(false)
51 if (sTooltipListenerCount
++ == 0) {
52 // register the callback so we get notified of updates
53 Preferences::RegisterCallback(ToolbarTipsPrefChanged
,
54 "browser.chrome.toolbar_tips");
56 // Call the pref callback to initialize our state.
57 ToolbarTipsPrefChanged("browser.chrome.toolbar_tips", nullptr);
61 nsXULTooltipListener::~nsXULTooltipListener()
63 if (nsXULTooltipListener::mInstance
== this) {
68 if (--sTooltipListenerCount
== 0) {
69 // Unregister our pref observer
70 Preferences::UnregisterCallback(ToolbarTipsPrefChanged
,
71 "browser.chrome.toolbar_tips");
75 NS_IMPL_ISUPPORTS(nsXULTooltipListener
, nsIDOMEventListener
)
78 nsXULTooltipListener::MouseOut(nsIDOMEvent
* aEvent
)
80 // reset flag so that tooltip will display on the next MouseMove
81 mTooltipShownOnce
= false;
83 // if the timer is running and no tooltip is shown, we
84 // have to cancel the timer here so that it doesn't
85 // show the tooltip if we move the mouse out of the window
86 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
87 if (mTooltipTimer
&& !currentTooltip
) {
88 mTooltipTimer
->Cancel();
89 mTooltipTimer
= nullptr;
99 // check to see if the mouse left the targetNode, and if so,
101 if (currentTooltip
) {
102 // which node did the mouse leave?
103 nsCOMPtr
<nsIDOMNode
> targetNode
= do_QueryInterface(
104 aEvent
->InternalDOMEvent()->GetTarget());
106 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
108 nsCOMPtr
<nsIDOMNode
> tooltipNode
=
109 pm
->GetLastTriggerTooltipNode(currentTooltip
->GetCurrentDoc());
110 if (tooltipNode
== targetNode
) {
111 // if the target node is the current tooltip target node, the mouse
112 // left the node the tooltip appeared on, so close the tooltip.
114 // reset special tree tracking
117 mLastTreeCol
= nullptr;
126 nsXULTooltipListener::MouseMove(nsIDOMEvent
* aEvent
)
131 // stash the coordinates of the event so that we can still get back to it from within the
132 // timer callback. On win32, we'll get a MouseMove event even when a popup goes away --
133 // even when the mouse doesn't change position! To get around this, we make sure the
134 // mouse has really moved before proceeding.
135 nsCOMPtr
<nsIDOMMouseEvent
> mouseEvent(do_QueryInterface(aEvent
));
138 int32_t newMouseX
, newMouseY
;
139 mouseEvent
->GetScreenX(&newMouseX
);
140 mouseEvent
->GetScreenY(&newMouseY
);
142 // filter out false win32 MouseMove event
143 if (mMouseScreenX
== newMouseX
&& mMouseScreenY
== newMouseY
)
146 // filter out minor movements due to crappy optical mice and shaky hands
147 // to prevent tooltips from hiding prematurely.
148 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
150 if ((currentTooltip
) &&
151 (abs(mMouseScreenX
- newMouseX
) <= kTooltipMouseMoveTolerance
) &&
152 (abs(mMouseScreenY
- newMouseY
) <= kTooltipMouseMoveTolerance
))
154 mMouseScreenX
= newMouseX
;
155 mMouseScreenY
= newMouseY
;
157 nsCOMPtr
<nsIContent
> sourceContent
= do_QueryInterface(
158 aEvent
->InternalDOMEvent()->GetCurrentTarget());
159 mSourceNode
= do_GetWeakReference(sourceContent
);
161 mIsSourceTree
= sourceContent
->Tag() == nsGkAtoms::treechildren
;
163 CheckTreeBodyMove(mouseEvent
);
166 // as the mouse moves, we want to make sure we reset the timer to show it,
167 // so that the delay is from when the mouse stops moving, not when it enters
171 // If the mouse moves while the tooltip is up, hide it. If nothing is
172 // showing and the tooltip hasn't been displayed since the mouse entered
173 // the node, then start the timer to show the tooltip.
174 if (!currentTooltip
&& !mTooltipShownOnce
) {
175 nsCOMPtr
<EventTarget
> eventTarget
= aEvent
->InternalDOMEvent()->GetTarget();
177 // don't show tooltips attached to elements outside of a menu popup
178 // when hovering over an element inside it. The popupsinherittooltip
179 // attribute may be used to disable this behaviour, which is useful for
180 // large menu hierarchies such as bookmarks.
181 if (!sourceContent
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::popupsinherittooltip
,
182 nsGkAtoms::_true
, eCaseMatters
)) {
183 nsCOMPtr
<nsIContent
> targetContent
= do_QueryInterface(eventTarget
);
184 while (targetContent
&& targetContent
!= sourceContent
) {
185 nsIAtom
* tag
= targetContent
->Tag();
186 if (targetContent
->GetNameSpaceID() == kNameSpaceID_XUL
&&
187 (tag
== nsGkAtoms::menupopup
||
188 tag
== nsGkAtoms::panel
||
189 tag
== nsGkAtoms::tooltip
)) {
190 mSourceNode
= nullptr;
194 targetContent
= targetContent
->GetParent();
198 mTooltipTimer
= do_CreateInstance("@mozilla.org/timer;1");
200 mTargetNode
= do_GetWeakReference(eventTarget
);
203 mTooltipTimer
->InitWithFuncCallback(sTooltipCallback
, this,
204 LookAndFeel::GetInt(LookAndFeel::eIntID_TooltipDelay
, 500),
205 nsITimer::TYPE_ONE_SHOT
);
207 mTargetNode
= nullptr;
208 mSourceNode
= nullptr;
221 // set a flag so that the tooltip is only displayed once until the mouse
223 mTooltipShownOnce
= true;
227 nsXULTooltipListener::HandleEvent(nsIDOMEvent
* aEvent
)
230 aEvent
->GetType(type
);
231 if (type
.EqualsLiteral("DOMMouseScroll") ||
232 type
.EqualsLiteral("keydown") ||
233 type
.EqualsLiteral("mousedown") ||
234 type
.EqualsLiteral("mouseup") ||
235 type
.EqualsLiteral("dragstart")) {
240 if (type
.EqualsLiteral("popuphiding")) {
245 // Note that mousemove, mouseover and mouseout might be
246 // fired even during dragging due to widget's bug.
247 nsCOMPtr
<nsIDragService
> dragService
=
248 do_GetService("@mozilla.org/widget/dragservice;1");
249 NS_ENSURE_TRUE(dragService
, NS_OK
);
250 nsCOMPtr
<nsIDragSession
> dragSession
;
251 dragService
->GetCurrentSession(getter_AddRefs(dragSession
));
258 if (type
.EqualsLiteral("mousemove")) {
263 if (type
.EqualsLiteral("mouseout")) {
271 //////////////////////////////////////////////////////////////////////////
272 //// nsXULTooltipListener
276 nsXULTooltipListener::ToolbarTipsPrefChanged(const char *aPref
,
280 Preferences::GetBool("browser.chrome.toolbar_tips", sShowTooltips
);
283 //////////////////////////////////////////////////////////////////////////
284 //// nsXULTooltipListener
286 bool nsXULTooltipListener::sShowTooltips
= false;
287 uint32_t nsXULTooltipListener::sTooltipListenerCount
= 0;
290 nsXULTooltipListener::AddTooltipSupport(nsIContent
* aNode
)
293 return NS_ERROR_NULL_POINTER
;
295 aNode
->AddSystemEventListener(NS_LITERAL_STRING("mouseout"), this,
297 aNode
->AddSystemEventListener(NS_LITERAL_STRING("mousemove"), this,
299 aNode
->AddSystemEventListener(NS_LITERAL_STRING("mousedown"), this,
301 aNode
->AddSystemEventListener(NS_LITERAL_STRING("mouseup"), this,
303 aNode
->AddSystemEventListener(NS_LITERAL_STRING("dragstart"), this,
310 nsXULTooltipListener::RemoveTooltipSupport(nsIContent
* aNode
)
313 return NS_ERROR_NULL_POINTER
;
315 aNode
->RemoveSystemEventListener(NS_LITERAL_STRING("mouseout"), this, false);
316 aNode
->RemoveSystemEventListener(NS_LITERAL_STRING("mousemove"), this, false);
317 aNode
->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"), this, false);
318 aNode
->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"), this, false);
319 aNode
->RemoveSystemEventListener(NS_LITERAL_STRING("dragstart"), this, true);
326 nsXULTooltipListener::CheckTreeBodyMove(nsIDOMMouseEvent
* aMouseEvent
)
328 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
332 // get the boxObject of the documentElement of the document the tree is in
333 nsCOMPtr
<nsIBoxObject
> bx
;
334 nsIDocument
* doc
= sourceNode
->GetComposedDoc();
337 bx
= doc
->GetBoxObjectFor(doc
->GetRootElement(), ignored
);
340 nsCOMPtr
<nsITreeBoxObject
> obx
;
341 GetSourceTreeBoxObject(getter_AddRefs(obx
));
344 aMouseEvent
->GetScreenX(&x
);
345 aMouseEvent
->GetScreenY(&y
);
348 nsCOMPtr
<nsITreeColumn
> col
;
351 // subtract off the documentElement's boxObject
353 bx
->GetScreenX(&boxX
);
354 bx
->GetScreenY(&boxY
);
358 obx
->GetCellAt(x
, y
, &row
, getter_AddRefs(col
), obj
);
360 // determine if we are going to need a titletip
361 // XXX check the disabletitletips attribute on the tree content
362 mNeedTitletip
= false;
363 if (row
>= 0 && obj
.EqualsLiteral("text")) {
364 obx
->IsCellCropped(row
, col
, &mNeedTitletip
);
367 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
368 if (currentTooltip
&& (row
!= mLastTreeRow
|| col
!= mLastTreeCol
)) {
379 nsXULTooltipListener::ShowTooltip()
381 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
383 // get the tooltip content designated for the target node
384 nsCOMPtr
<nsIContent
> tooltipNode
;
385 GetTooltipFor(sourceNode
, getter_AddRefs(tooltipNode
));
386 if (!tooltipNode
|| sourceNode
== tooltipNode
)
387 return NS_ERROR_FAILURE
; // the target node doesn't need a tooltip
389 // set the node in the document that triggered the tooltip and show it
390 nsCOMPtr
<nsIDOMXULDocument
> xulDoc
=
391 do_QueryInterface(tooltipNode
->GetComposedDoc());
393 // Make sure the target node is still attached to some document.
394 // It might have been deleted.
395 if (sourceNode
->IsInComposedDoc()) {
397 if (!mIsSourceTree
) {
399 mLastTreeCol
= nullptr;
403 mCurrentTooltip
= do_GetWeakReference(tooltipNode
);
405 mTargetNode
= nullptr;
407 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
411 // listen for popuphidden on the tooltip node, so that we can
412 // be sure DestroyPopup is called even if someone else closes the tooltip
413 currentTooltip
->AddSystemEventListener(NS_LITERAL_STRING("popuphiding"),
416 // listen for mousedown, mouseup, keydown, and DOMMouseScroll events at document level
417 nsIDocument
* doc
= sourceNode
->GetComposedDoc();
419 // Probably, we should listen to untrusted events for hiding tooltips
420 // on content since tooltips might disturb something of web
421 // applications. If we don't specify the aWantsUntrusted of
422 // AddSystemEventListener(), the event target sets it to TRUE if the
423 // target is in content.
424 doc
->AddSystemEventListener(NS_LITERAL_STRING("DOMMouseScroll"),
426 doc
->AddSystemEventListener(NS_LITERAL_STRING("mousedown"),
428 doc
->AddSystemEventListener(NS_LITERAL_STRING("mouseup"),
430 doc
->AddSystemEventListener(NS_LITERAL_STRING("keydown"),
433 mSourceNode
= nullptr;
441 // XXX: "This stuff inside DEBUG_crap could be used to make tree tooltips work
445 GetTreeCellCoords(nsITreeBoxObject
* aTreeBox
, nsIContent
* aSourceNode
,
446 int32_t aRow
, nsITreeColumn
* aCol
, int32_t* aX
, int32_t* aY
)
449 aTreeBox
->GetCoordsForCellItem(aRow
, aCol
, EmptyCString(), aX
, aY
, &junk
, &junk
);
450 nsCOMPtr
<nsIDOMXULElement
> xulEl(do_QueryInterface(aSourceNode
));
451 nsCOMPtr
<nsIBoxObject
> bx
;
452 xulEl
->GetBoxObject(getter_AddRefs(bx
));
462 SetTitletipLabel(nsITreeBoxObject
* aTreeBox
, nsIContent
* aTooltip
,
463 int32_t aRow
, nsITreeColumn
* aCol
)
465 nsCOMPtr
<nsITreeView
> view
;
466 aTreeBox
->GetView(getter_AddRefs(view
));
472 view
->GetCellText(aRow
, aCol
, label
);
473 NS_WARN_IF_FALSE(NS_SUCCEEDED(rv
), "Couldn't get the cell text!");
474 aTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, label
, true);
480 nsXULTooltipListener::LaunchTooltip()
482 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
487 if (mIsSourceTree
&& mNeedTitletip
) {
488 nsCOMPtr
<nsITreeBoxObject
> obx
;
489 GetSourceTreeBoxObject(getter_AddRefs(obx
));
491 SetTitletipLabel(obx
, currentTooltip
, mLastTreeRow
, mLastTreeCol
);
492 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
493 // Because of mutation events, currentTooltip can be null.
496 currentTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, NS_LITERAL_STRING("true"), true);
498 currentTooltip
->UnsetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, true);
500 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
501 // Because of mutation events, currentTooltip can be null.
505 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
507 nsCOMPtr
<nsIContent
> target
= do_QueryReferent(mTargetNode
);
508 pm
->ShowTooltipAtScreen(currentTooltip
, target
, mMouseScreenX
, mMouseScreenY
);
510 // Clear the current tooltip if the popup was not opened successfully.
511 if (!pm
->IsPopupOpen(currentTooltip
))
512 mCurrentTooltip
= nullptr;
519 nsXULTooltipListener::HideTooltip()
522 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
523 if (currentTooltip
) {
524 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
526 pm
->HidePopup(currentTooltip
, false, false, false, false);
535 GetImmediateChild(nsIContent
* aContent
, nsIAtom
*aTag
, nsIContent
** aResult
)
538 uint32_t childCount
= aContent
->GetChildCount();
539 for (uint32_t i
= 0; i
< childCount
; i
++) {
540 nsIContent
*child
= aContent
->GetChildAt(i
);
542 if (child
->Tag() == aTag
) {
553 nsXULTooltipListener::FindTooltip(nsIContent
* aTarget
, nsIContent
** aTooltip
)
556 return NS_ERROR_NULL_POINTER
;
558 // before we go on, make sure that target node still has a window
559 nsIDocument
*document
= aTarget
->GetComposedDoc();
561 NS_WARNING("Unable to retrieve the tooltip node document.");
562 return NS_ERROR_FAILURE
;
564 nsPIDOMWindow
*window
= document
->GetWindow();
570 window
->GetClosed(&closed
);
576 nsAutoString tooltipText
;
577 aTarget
->GetAttr(kNameSpaceID_None
, nsGkAtoms::tooltiptext
, tooltipText
);
578 if (!tooltipText
.IsEmpty()) {
579 // specifying tooltiptext means we will always use the default tooltip
580 nsIRootBox
* rootBox
= nsIRootBox::GetRootBox(document
->GetShell());
581 NS_ENSURE_STATE(rootBox
);
582 *aTooltip
= rootBox
->GetDefaultTooltip();
584 NS_ADDREF(*aTooltip
);
585 (*aTooltip
)->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, tooltipText
, true);
590 nsAutoString tooltipId
;
591 aTarget
->GetAttr(kNameSpaceID_None
, nsGkAtoms::tooltip
, tooltipId
);
593 // if tooltip == _child, look for first <tooltip> child
594 if (tooltipId
.EqualsLiteral("_child")) {
595 GetImmediateChild(aTarget
, nsGkAtoms::tooltip
, aTooltip
);
599 if (!tooltipId
.IsEmpty() && aTarget
->IsInUncomposedDoc()) {
600 // tooltip must be an id, use getElementById to find it
601 //XXXsmaug If aTarget is in shadow dom, should we use
602 // ShadowRoot::GetElementById()?
603 nsCOMPtr
<nsIContent
> tooltipEl
= document
->GetElementById(tooltipId
);
607 mNeedTitletip
= false;
609 tooltipEl
.forget(aTooltip
);
615 // titletips should just use the default tooltip
616 if (mIsSourceTree
&& mNeedTitletip
) {
617 nsIRootBox
* rootBox
= nsIRootBox::GetRootBox(document
->GetShell());
618 NS_ENSURE_STATE(rootBox
);
619 NS_IF_ADDREF(*aTooltip
= rootBox
->GetDefaultTooltip());
628 nsXULTooltipListener::GetTooltipFor(nsIContent
* aTarget
, nsIContent
** aTooltip
)
631 nsCOMPtr
<nsIContent
> tooltip
;
632 nsresult rv
= FindTooltip(aTarget
, getter_AddRefs(tooltip
));
633 if (NS_FAILED(rv
) || !tooltip
) {
638 // Submenus can't be used as tooltips, see bug 288763.
639 nsIContent
* parent
= tooltip
->GetParent();
641 nsMenuFrame
* menu
= do_QueryFrame(parent
->GetPrimaryFrame());
643 NS_WARNING("Menu cannot be used as a tooltip");
644 return NS_ERROR_FAILURE
;
649 tooltip
.swap(*aTooltip
);
654 nsXULTooltipListener::DestroyTooltip()
656 nsCOMPtr
<nsIDOMEventListener
> kungFuDeathGrip(this);
657 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
658 if (currentTooltip
) {
659 // release tooltip before removing listener to prevent our destructor from
660 // being called recursively (bug 120863)
661 mCurrentTooltip
= nullptr;
663 // clear out the tooltip node on the document
664 nsCOMPtr
<nsIDocument
> doc
= currentTooltip
->GetComposedDoc();
666 // remove the mousedown and keydown listener from document
667 doc
->RemoveSystemEventListener(NS_LITERAL_STRING("DOMMouseScroll"), this,
669 doc
->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"), this,
671 doc
->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"), this, true);
672 doc
->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"), this, true);
675 // remove the popuphidden listener from tooltip
676 currentTooltip
->RemoveSystemEventListener(NS_LITERAL_STRING("popuphiding"), this, false);
679 // kill any ongoing timers
681 mSourceNode
= nullptr;
683 mLastTreeCol
= nullptr;
690 nsXULTooltipListener::KillTooltipTimer()
693 mTooltipTimer
->Cancel();
694 mTooltipTimer
= nullptr;
695 mTargetNode
= nullptr;
700 nsXULTooltipListener::sTooltipCallback(nsITimer
*aTimer
, void *aListener
)
702 nsRefPtr
<nsXULTooltipListener
> instance
= mInstance
;
704 instance
->ShowTooltip();
709 nsXULTooltipListener::GetSourceTreeBoxObject(nsITreeBoxObject
** aBoxObject
)
711 *aBoxObject
= nullptr;
713 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
714 if (mIsSourceTree
&& sourceNode
) {
715 nsCOMPtr
<nsIDOMXULElement
> xulEl(do_QueryInterface(sourceNode
->GetParent()));
717 nsCOMPtr
<nsIBoxObject
> bx
;
718 xulEl
->GetBoxObject(getter_AddRefs(bx
));
719 nsCOMPtr
<nsITreeBoxObject
> obx(do_QueryInterface(bx
));
722 NS_ADDREF(*aBoxObject
);
727 return NS_ERROR_FAILURE
;