1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsXULTooltipListener.h"
9 #include "XULButtonElement.h"
10 #include "XULTreeElement.h"
11 #include "nsXULElement.h"
12 #include "mozilla/dom/Document.h"
13 #include "nsGkAtoms.h"
14 #include "nsMenuPopupFrame.h"
15 #include "nsIDragService.h"
16 #include "nsIDragSession.h"
17 #include "nsITreeView.h"
18 #include "nsIScriptContext.h"
19 #include "nsPIDOMWindow.h"
20 #include "nsXULPopupManager.h"
21 #include "nsIPopupContainer.h"
22 #include "nsServiceManagerUtils.h"
23 #include "nsTreeColumns.h"
24 #include "nsContentUtils.h"
25 #include "mozilla/ErrorResult.h"
26 #include "mozilla/LookAndFeel.h"
27 #include "mozilla/PresShell.h"
28 #include "mozilla/ScopeExit.h"
29 #include "mozilla/StaticPrefs_browser.h"
30 #include "mozilla/dom/Element.h"
31 #include "mozilla/dom/Event.h" // for Event
32 #include "mozilla/dom/MouseEvent.h"
33 #include "mozilla/dom/TreeColumnBinding.h"
34 #include "mozilla/dom/XULTreeElementBinding.h"
35 #include "mozilla/TextEvents.h"
37 using namespace mozilla
;
38 using namespace mozilla::dom
;
40 nsXULTooltipListener
* nsXULTooltipListener::sInstance
= nullptr;
42 //////////////////////////////////////////////////////////////////////////
45 nsXULTooltipListener::nsXULTooltipListener()
46 : mTooltipShownOnce(false),
51 nsXULTooltipListener::~nsXULTooltipListener() {
52 MOZ_ASSERT(sInstance
== this);
58 NS_IMPL_ISUPPORTS(nsXULTooltipListener
, nsIDOMEventListener
)
60 void nsXULTooltipListener::MouseOut(Event
* aEvent
) {
61 // reset flag so that tooltip will display on the next MouseMove
62 mTooltipShownOnce
= false;
63 mPreviousMouseMoveTarget
= nullptr;
65 // if the timer is running and no tooltip is shown, we
66 // have to cancel the timer here so that it doesn't
67 // show the tooltip if we move the mouse out of the window
68 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
69 if (mTooltipTimer
&& !currentTooltip
) {
70 mTooltipTimer
->Cancel();
71 mTooltipTimer
= nullptr;
76 if (mNeedTitletip
) return;
79 // check to see if the mouse left the targetNode, and if so,
82 // which node did the mouse leave?
83 EventTarget
* eventTarget
= aEvent
->GetComposedTarget();
84 nsCOMPtr
<nsINode
> targetNode
= nsINode::FromEventTargetOrNull(eventTarget
);
85 if (targetNode
&& targetNode
->IsContent() &&
86 !targetNode
->AsContent()->GetContainingShadow()) {
87 eventTarget
= aEvent
->GetTarget();
90 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
92 nsCOMPtr
<nsINode
> tooltipNode
=
93 pm
->GetLastTriggerTooltipNode(currentTooltip
->GetComposedDoc());
95 // If the target node is the current tooltip target node, the mouse
96 // left the node the tooltip appeared on, so close the tooltip. However,
97 // don't do this if the mouse moved onto the tooltip in case the
98 // tooltip appears positioned near the mouse.
99 nsCOMPtr
<EventTarget
> relatedTarget
=
100 aEvent
->AsMouseEvent()->GetRelatedTarget();
101 nsIContent
* relatedContent
=
102 nsIContent::FromEventTargetOrNull(relatedTarget
);
103 if (tooltipNode
== targetNode
&& relatedContent
!= currentTooltip
) {
105 // reset special tree tracking
108 mLastTreeCol
= nullptr;
115 void nsXULTooltipListener::MouseMove(Event
* aEvent
) {
116 if (!ShowTooltips()) {
120 // stash the coordinates of the event so that we can still get back to it from
121 // within the timer callback. On win32, we'll get a MouseMove event even when
122 // a popup goes away -- even when the mouse doesn't change position! To get
123 // around this, we make sure the mouse has really moved before proceeding.
124 MouseEvent
* mouseEvent
= aEvent
->AsMouseEvent();
128 auto newMouseScreenPoint
= mouseEvent
->ScreenPointLayoutDevicePix();
130 // filter out false win32 MouseMove event
131 if (mMouseScreenPoint
== newMouseScreenPoint
) {
135 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
136 nsCOMPtr
<EventTarget
> eventTarget
= aEvent
->GetComposedTarget();
137 nsIContent
* content
= nsIContent::FromEventTargetOrNull(eventTarget
);
139 bool isSameTarget
= true;
140 nsCOMPtr
<nsIContent
> tempContent
= do_QueryReferent(mPreviousMouseMoveTarget
);
141 if (tempContent
&& tempContent
!= content
) {
142 isSameTarget
= false;
145 // filter out minor movements due to crappy optical mice and shaky hands
146 // to prevent tooltips from hiding prematurely. Do not filter out movements
147 // if we are changing targets, as they may register new tooltips.
148 if (currentTooltip
&& isSameTarget
&&
149 abs(mMouseScreenPoint
.x
- newMouseScreenPoint
.x
) <=
150 kTooltipMouseMoveTolerance
&&
151 abs(mMouseScreenPoint
.y
- newMouseScreenPoint
.y
) <=
152 kTooltipMouseMoveTolerance
) {
155 mMouseScreenPoint
= newMouseScreenPoint
;
156 mPreviousMouseMoveTarget
= do_GetWeakReference(content
);
158 nsCOMPtr
<nsIContent
> sourceContent
=
159 do_QueryInterface(aEvent
->GetCurrentTarget());
160 mSourceNode
= do_GetWeakReference(sourceContent
);
161 mIsSourceTree
= sourceContent
->IsXULElement(nsGkAtoms::treechildren
);
162 if (mIsSourceTree
) CheckTreeBodyMove(mouseEvent
);
164 // as the mouse moves, we want to make sure we reset the timer to show it,
165 // so that the delay is from when the mouse stops moving, not when it enters
169 // Hide the current tooltip if we change target nodes. If the new target
170 // has the same tooltip, we will open it again. We cannot compare
171 // the targets' tooltips because popupshowing events can set the tooltip.
174 mTooltipShownOnce
= false;
177 // If the mouse moves while the tooltip is up, hide it. If nothing is
178 // showing and the tooltip hasn't been displayed since the mouse entered
179 // the node, then start the timer to show the tooltip.
180 // If we have moved to a different target, we need to display the new tooltip,
181 // as the previous target's tooltip will have just been hidden.
182 if ((!currentTooltip
&& !mTooltipShownOnce
) || !isSameTarget
) {
183 // don't show tooltips attached to elements outside of a menu popup
184 // when hovering over an element inside it. The popupsinherittooltip
185 // attribute may be used to disable this behaviour, which is useful for
186 // large menu hierarchies such as bookmarks.
187 if (!sourceContent
->IsElement() ||
188 !sourceContent
->AsElement()->AttrValueIs(
189 kNameSpaceID_None
, nsGkAtoms::popupsinherittooltip
,
190 nsGkAtoms::_true
, eCaseMatters
)) {
191 for (nsIContent
* targetContent
=
192 nsIContent::FromEventTargetOrNull(eventTarget
);
193 targetContent
&& targetContent
!= sourceContent
;
194 targetContent
= targetContent
->GetParent()) {
195 if (targetContent
->IsAnyOfXULElements(
196 nsGkAtoms::menupopup
, nsGkAtoms::panel
, nsGkAtoms::tooltip
)) {
197 mSourceNode
= nullptr;
203 mTargetNode
= do_GetWeakReference(eventTarget
);
205 nsresult rv
= NS_NewTimerWithFuncCallback(
206 getter_AddRefs(mTooltipTimer
), sTooltipCallback
, this,
207 LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay
, 500),
208 nsITimer::TYPE_ONE_SHOT
, "sTooltipCallback",
209 sourceContent
->OwnerDoc()->EventTargetFor(TaskCategory::Other
));
211 mTargetNode
= nullptr;
212 mSourceNode
= nullptr;
218 if (mIsSourceTree
) return;
219 // Hide the tooltip if it is currently showing.
220 if (currentTooltip
) {
222 // set a flag so that the tooltip is only displayed once until the mouse
224 mTooltipShownOnce
= true;
229 nsXULTooltipListener::HandleEvent(Event
* aEvent
) {
231 aEvent
->GetType(type
);
232 if (type
.EqualsLiteral("wheel") || type
.EqualsLiteral("mousedown") ||
233 type
.EqualsLiteral("mouseup") || type
.EqualsLiteral("dragstart")) {
238 if (type
.EqualsLiteral("keydown")) {
239 // Hide the tooltip if a non-modifier key is pressed.
240 WidgetKeyboardEvent
* keyEvent
= aEvent
->WidgetEventPtr()->AsKeyboardEvent();
241 if (KeyEventHidesTooltip(*keyEvent
)) {
247 if (type
.EqualsLiteral("popuphiding")) {
252 // Note that mousemove, mouseover and mouseout might be
253 // fired even during dragging due to widget's bug.
254 nsCOMPtr
<nsIDragService
> dragService
=
255 do_GetService("@mozilla.org/widget/dragservice;1");
256 NS_ENSURE_TRUE(dragService
, NS_OK
);
257 nsCOMPtr
<nsIDragSession
> dragSession
;
258 dragService
->GetCurrentSession(getter_AddRefs(dragSession
));
265 if (type
.EqualsLiteral("mousemove")) {
270 if (type
.EqualsLiteral("mouseout")) {
278 //////////////////////////////////////////////////////////////////////////
279 //// nsXULTooltipListener
281 bool nsXULTooltipListener::ShowTooltips() {
282 return StaticPrefs::browser_chrome_toolbar_tips();
285 bool nsXULTooltipListener::KeyEventHidesTooltip(
286 const WidgetKeyboardEvent
& aEvent
) {
287 switch (StaticPrefs::browser_chrome_toolbar_tips_hide_on_keydown()) {
293 return !aEvent
.IsModifierKeyEvent();
297 void nsXULTooltipListener::AddTooltipSupport(nsIContent
* aNode
) {
299 MOZ_ASSERT(this == sInstance
);
301 aNode
->AddSystemEventListener(u
"mouseout"_ns
, this, false, false);
302 aNode
->AddSystemEventListener(u
"mousemove"_ns
, this, false, false);
303 aNode
->AddSystemEventListener(u
"mousedown"_ns
, this, false, false);
304 aNode
->AddSystemEventListener(u
"mouseup"_ns
, this, false, false);
305 aNode
->AddSystemEventListener(u
"dragstart"_ns
, this, true, false);
308 void nsXULTooltipListener::RemoveTooltipSupport(nsIContent
* aNode
) {
310 MOZ_ASSERT(this == sInstance
);
312 // The last reference to us can go after some of these calls.
313 RefPtr
<nsXULTooltipListener
> instance
= this;
315 aNode
->RemoveSystemEventListener(u
"mouseout"_ns
, this, false);
316 aNode
->RemoveSystemEventListener(u
"mousemove"_ns
, this, false);
317 aNode
->RemoveSystemEventListener(u
"mousedown"_ns
, this, false);
318 aNode
->RemoveSystemEventListener(u
"mouseup"_ns
, this, false);
319 aNode
->RemoveSystemEventListener(u
"dragstart"_ns
, this, true);
322 void nsXULTooltipListener::CheckTreeBodyMove(MouseEvent
* aMouseEvent
) {
323 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
324 if (!sourceNode
) return;
326 // get the documentElement of the document the tree is in
327 Document
* doc
= sourceNode
->GetComposedDoc();
329 RefPtr
<XULTreeElement
> tree
= GetSourceTree();
330 Element
* root
= doc
? doc
->GetRootElement() : nullptr;
331 if (root
&& root
->GetPrimaryFrame() && tree
) {
332 CSSIntPoint pos
= aMouseEvent
->ScreenPoint(CallerType::System
);
334 // subtract off the documentElement's position
335 // XXX Isn't this just converting to client points?
336 CSSIntRect rect
= root
->GetPrimaryFrame()->GetScreenRect();
337 pos
-= rect
.TopLeft();
340 TreeCellInfo cellInfo
;
341 tree
->GetCellAt(pos
.x
, pos
.y
, cellInfo
, rv
);
343 int32_t row
= cellInfo
.mRow
;
344 RefPtr
<nsTreeColumn
> col
= cellInfo
.mCol
;
346 // determine if we are going to need a titletip
347 // XXX check the disabletitletips attribute on the tree content
348 mNeedTitletip
= false;
349 if (row
>= 0 && cellInfo
.mChildElt
.EqualsLiteral("text")) {
350 mNeedTitletip
= tree
->IsCellCropped(row
, col
, rv
);
353 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
354 if (currentTooltip
&& (row
!= mLastTreeRow
|| col
!= mLastTreeCol
)) {
363 nsresult
nsXULTooltipListener::ShowTooltip() {
364 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
366 // get the tooltip content designated for the target node
367 nsCOMPtr
<nsIContent
> tooltipNode
;
368 GetTooltipFor(sourceNode
, getter_AddRefs(tooltipNode
));
369 if (!tooltipNode
|| sourceNode
== tooltipNode
)
370 return NS_ERROR_FAILURE
; // the target node doesn't need a tooltip
372 // set the node in the document that triggered the tooltip and show it
373 if (tooltipNode
->GetComposedDoc() &&
374 nsContentUtils::IsChromeDoc(tooltipNode
->GetComposedDoc())) {
375 // Make sure the target node is still attached to some document.
376 // It might have been deleted.
377 if (sourceNode
->IsInComposedDoc()) {
378 if (!mIsSourceTree
) {
380 mLastTreeCol
= nullptr;
383 mCurrentTooltip
= do_GetWeakReference(tooltipNode
);
385 mTargetNode
= nullptr;
387 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
388 if (!currentTooltip
) return NS_OK
;
390 // listen for popuphidden on the tooltip node, so that we can
391 // be sure DestroyPopup is called even if someone else closes the tooltip
392 currentTooltip
->AddSystemEventListener(u
"popuphiding"_ns
, this, false,
395 // listen for mousedown, mouseup, keydown, and mouse events at
397 if (Document
* doc
= sourceNode
->GetComposedDoc()) {
398 // Probably, we should listen to untrusted events for hiding tooltips
399 // on content since tooltips might disturb something of web
400 // applications. If we don't specify the aWantsUntrusted of
401 // AddSystemEventListener(), the event target sets it to TRUE if the
402 // target is in content.
403 doc
->AddSystemEventListener(u
"wheel"_ns
, this, true);
404 doc
->AddSystemEventListener(u
"mousedown"_ns
, this, true);
405 doc
->AddSystemEventListener(u
"mouseup"_ns
, this, true);
406 doc
->AddSystemEventListener(u
"keydown"_ns
, this, true);
408 mSourceNode
= nullptr;
415 static void SetTitletipLabel(XULTreeElement
* aTree
, Element
* aTooltip
,
416 int32_t aRow
, nsTreeColumn
* aCol
) {
417 nsCOMPtr
<nsITreeView
> view
= aTree
->GetView();
423 view
->GetCellText(aRow
, aCol
, label
);
424 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "Couldn't get the cell text!");
425 aTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, label
, true);
429 void nsXULTooltipListener::LaunchTooltip() {
430 RefPtr
<Element
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
431 if (!currentTooltip
) {
435 if (mIsSourceTree
&& mNeedTitletip
) {
436 RefPtr
<XULTreeElement
> tree
= GetSourceTree();
438 SetTitletipLabel(tree
, currentTooltip
, mLastTreeRow
, mLastTreeCol
);
439 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
440 // Because of mutation events, currentTooltip can be null.
443 currentTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, u
"true"_ns
,
446 currentTooltip
->UnsetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, true);
449 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
450 // Because of mutation events, currentTooltip can be null.
454 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
459 auto cleanup
= MakeScopeExit([&] {
460 // Clear the current tooltip if the popup was not opened successfully.
461 if (!pm
->IsPopupOpen(currentTooltip
)) {
462 mCurrentTooltip
= nullptr;
466 RefPtr
<Element
> target
= do_QueryReferent(mTargetNode
);
471 pm
->ShowTooltipAtScreen(currentTooltip
, target
, mMouseScreenPoint
);
474 nsresult
nsXULTooltipListener::HideTooltip() {
475 if (nsCOMPtr
<Element
> currentTooltip
= do_QueryReferent(mCurrentTooltip
)) {
476 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
477 pm
->HidePopup(currentTooltip
, {});
485 static void GetImmediateChild(nsIContent
* aContent
, nsAtom
* aTag
,
486 nsIContent
** aResult
) {
488 for (nsCOMPtr
<nsIContent
> childContent
= aContent
->GetFirstChild();
489 childContent
; childContent
= childContent
->GetNextSibling()) {
490 if (childContent
->IsXULElement(aTag
)) {
491 childContent
.forget(aResult
);
497 nsresult
nsXULTooltipListener::FindTooltip(nsIContent
* aTarget
,
498 nsIContent
** aTooltip
) {
499 if (!aTarget
) return NS_ERROR_NULL_POINTER
;
501 // before we go on, make sure that target node still has a window
502 Document
* document
= aTarget
->GetComposedDoc();
504 NS_WARNING("Unable to retrieve the tooltip node document.");
505 return NS_ERROR_FAILURE
;
507 nsPIDOMWindowOuter
* window
= document
->GetWindow();
512 if (window
->Closed()) {
516 // non-XUL elements should just use the default tooltip
517 if (!aTarget
->IsXULElement()) {
518 nsIPopupContainer
* popupContainer
=
519 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
520 NS_ENSURE_STATE(popupContainer
);
521 if (RefPtr
<Element
> tooltip
= popupContainer
->GetDefaultTooltip()) {
522 tooltip
.forget(aTooltip
);
525 return NS_ERROR_FAILURE
;
528 // On Windows, the OS shows the tooltip, so we don't want Gecko to do it
530 if (nsIFrame
* f
= aTarget
->GetPrimaryFrame()) {
531 if (f
->StyleDisplay()->GetWindowButtonType()) {
537 nsAutoString tooltipText
;
538 aTarget
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::tooltiptext
,
541 if (!tooltipText
.IsEmpty()) {
542 // specifying tooltiptext means we will always use the default tooltip
543 nsIPopupContainer
* popupContainer
=
544 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
545 NS_ENSURE_STATE(popupContainer
);
546 if (RefPtr
<Element
> tooltip
= popupContainer
->GetDefaultTooltip()) {
547 tooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, tooltipText
, true);
548 tooltip
.forget(aTooltip
);
553 nsAutoString tooltipId
;
554 aTarget
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::tooltip
,
557 // if tooltip == _child, look for first <tooltip> child
558 if (tooltipId
.EqualsLiteral("_child")) {
559 GetImmediateChild(aTarget
, nsGkAtoms::tooltip
, aTooltip
);
563 if (!tooltipId
.IsEmpty()) {
564 DocumentOrShadowRoot
* documentOrShadowRoot
=
565 aTarget
->GetUncomposedDocOrConnectedShadowRoot();
566 // tooltip must be an id, use getElementById to find it
567 if (documentOrShadowRoot
) {
568 nsCOMPtr
<nsIContent
> tooltipEl
=
569 documentOrShadowRoot
->GetElementById(tooltipId
);
572 mNeedTitletip
= false;
573 tooltipEl
.forget(aTooltip
);
579 // titletips should just use the default tooltip
580 if (mIsSourceTree
&& mNeedTitletip
) {
581 nsIPopupContainer
* popupContainer
=
582 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
583 NS_ENSURE_STATE(popupContainer
);
584 NS_IF_ADDREF(*aTooltip
= popupContainer
->GetDefaultTooltip());
590 nsresult
nsXULTooltipListener::GetTooltipFor(nsIContent
* aTarget
,
591 nsIContent
** aTooltip
) {
593 nsCOMPtr
<nsIContent
> tooltip
;
594 nsresult rv
= FindTooltip(aTarget
, getter_AddRefs(tooltip
));
595 if (NS_FAILED(rv
) || !tooltip
) {
599 // Submenus can't be used as tooltips, see bug 288763.
600 if (nsIContent
* parent
= tooltip
->GetParent()) {
601 if (auto* button
= XULButtonElement::FromNode(parent
)) {
602 if (button
->IsMenu()) {
603 NS_WARNING("Menu cannot be used as a tooltip");
604 return NS_ERROR_FAILURE
;
609 tooltip
.swap(*aTooltip
);
613 nsresult
nsXULTooltipListener::DestroyTooltip() {
614 nsCOMPtr
<nsIDOMEventListener
> kungFuDeathGrip(this);
615 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
616 if (currentTooltip
) {
617 // release tooltip before removing listener to prevent our destructor from
618 // being called recursively (bug 120863)
619 mCurrentTooltip
= nullptr;
621 // clear out the tooltip node on the document
622 if (nsCOMPtr
<Document
> doc
= currentTooltip
->GetComposedDoc()) {
623 // remove the mousedown and keydown listener from document
624 doc
->RemoveSystemEventListener(u
"wheel"_ns
, this, true);
625 doc
->RemoveSystemEventListener(u
"mousedown"_ns
, this, true);
626 doc
->RemoveSystemEventListener(u
"mouseup"_ns
, this, true);
627 doc
->RemoveSystemEventListener(u
"keydown"_ns
, this, true);
630 // remove the popuphidden listener from tooltip
631 currentTooltip
->RemoveSystemEventListener(u
"popuphiding"_ns
, this, false);
634 // kill any ongoing timers
636 mSourceNode
= nullptr;
637 mLastTreeCol
= nullptr;
642 void nsXULTooltipListener::KillTooltipTimer() {
644 mTooltipTimer
->Cancel();
645 mTooltipTimer
= nullptr;
646 mTargetNode
= nullptr;
650 void nsXULTooltipListener::sTooltipCallback(nsITimer
* aTimer
, void* aListener
) {
651 RefPtr
<nsXULTooltipListener
> instance
= sInstance
;
652 if (instance
) instance
->ShowTooltip();
655 XULTreeElement
* nsXULTooltipListener::GetSourceTree() {
656 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
657 if (mIsSourceTree
&& sourceNode
) {
658 RefPtr
<XULTreeElement
> xulEl
=
659 XULTreeElement::FromNodeOrNull(sourceNode
->GetParent());