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 "nsIContentInlines.h"
16 #include "nsIDragService.h"
17 #include "nsIDragSession.h"
18 #include "nsITreeView.h"
19 #include "nsIScriptContext.h"
20 #include "nsPIDOMWindow.h"
21 #include "nsXULPopupManager.h"
22 #include "nsIPopupContainer.h"
23 #include "nsServiceManagerUtils.h"
24 #include "nsTreeColumns.h"
25 #include "nsContentUtils.h"
26 #include "mozilla/ErrorResult.h"
27 #include "mozilla/LookAndFeel.h"
28 #include "mozilla/PresShell.h"
29 #include "mozilla/ScopeExit.h"
30 #include "mozilla/StaticPrefs_browser.h"
31 #include "mozilla/dom/Element.h"
32 #include "mozilla/dom/Event.h" // for Event
33 #include "mozilla/dom/MouseEvent.h"
34 #include "mozilla/dom/TreeColumnBinding.h"
35 #include "mozilla/dom/XULTreeElementBinding.h"
36 #include "mozilla/TextEvents.h"
38 using namespace mozilla
;
39 using namespace mozilla::dom
;
41 nsXULTooltipListener
* nsXULTooltipListener::sInstance
= nullptr;
43 //////////////////////////////////////////////////////////////////////////
46 nsXULTooltipListener::nsXULTooltipListener()
47 : mTooltipShownOnce(false),
52 nsXULTooltipListener::~nsXULTooltipListener() {
53 MOZ_ASSERT(sInstance
== this);
59 NS_IMPL_ISUPPORTS(nsXULTooltipListener
, nsIDOMEventListener
)
61 void nsXULTooltipListener::MouseOut(Event
* aEvent
) {
62 // reset flag so that tooltip will display on the next MouseMove
63 mTooltipShownOnce
= false;
64 mPreviousMouseMoveTarget
= nullptr;
66 // if the timer is running and no tooltip is shown, we
67 // have to cancel the timer here so that it doesn't
68 // show the tooltip if we move the mouse out of the window
69 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
70 if (mTooltipTimer
&& !currentTooltip
) {
71 mTooltipTimer
->Cancel();
72 mTooltipTimer
= nullptr;
77 if (mNeedTitletip
) return;
80 // check to see if the mouse left the targetNode, and if so,
83 nsCOMPtr
<nsINode
> targetNode
=
84 nsINode::FromEventTargetOrNull(aEvent
->GetOriginalTarget());
85 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
86 nsCOMPtr
<nsINode
> tooltipNode
=
87 pm
->GetLastTriggerTooltipNode(currentTooltip
->GetComposedDoc());
89 // If the target node is the current tooltip target node, the mouse
90 // left the node the tooltip appeared on, so close the tooltip. However,
91 // don't do this if the mouse moved onto the tooltip in case the
92 // tooltip appears positioned near the mouse.
93 nsCOMPtr
<EventTarget
> relatedTarget
=
94 aEvent
->AsMouseEvent()->GetRelatedTarget();
95 auto* relatedContent
= nsIContent::FromEventTargetOrNull(relatedTarget
);
96 if (tooltipNode
== targetNode
&& relatedContent
!= currentTooltip
) {
98 // reset special tree tracking
101 mLastTreeCol
= nullptr;
108 void nsXULTooltipListener::MouseMove(Event
* aEvent
) {
109 if (!ShowTooltips()) {
113 // stash the coordinates of the event so that we can still get back to it from
114 // within the timer callback. On win32, we'll get a MouseMove event even when
115 // a popup goes away -- even when the mouse doesn't change position! To get
116 // around this, we make sure the mouse has really moved before proceeding.
117 MouseEvent
* mouseEvent
= aEvent
->AsMouseEvent();
121 auto newMouseScreenPoint
= mouseEvent
->ScreenPointLayoutDevicePix();
123 // filter out false win32 MouseMove event
124 if (mMouseScreenPoint
== newMouseScreenPoint
) {
128 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
129 auto* const mouseMoveTarget
=
130 nsIContent::FromEventTargetOrNull(aEvent
->GetOriginalTarget());
132 bool isSameTarget
= true;
133 nsCOMPtr
<nsIContent
> tempContent
= do_QueryReferent(mPreviousMouseMoveTarget
);
134 if (tempContent
&& tempContent
!= mouseMoveTarget
) {
135 isSameTarget
= false;
138 // filter out minor movements due to crappy optical mice and shaky hands
139 // to prevent tooltips from hiding prematurely. Do not filter out movements
140 // if we are changing targets, as they may register new tooltips.
141 if (currentTooltip
&& isSameTarget
&&
142 abs(mMouseScreenPoint
.x
- newMouseScreenPoint
.x
) <=
143 kTooltipMouseMoveTolerance
&&
144 abs(mMouseScreenPoint
.y
- newMouseScreenPoint
.y
) <=
145 kTooltipMouseMoveTolerance
) {
148 mMouseScreenPoint
= newMouseScreenPoint
;
149 mPreviousMouseMoveTarget
= do_GetWeakReference(mouseMoveTarget
);
151 auto* const sourceContent
=
152 nsIContent::FromEventTargetOrNull(aEvent
->GetCurrentTarget());
153 mSourceNode
= do_GetWeakReference(sourceContent
);
154 mIsSourceTree
= sourceContent
->IsXULElement(nsGkAtoms::treechildren
);
156 CheckTreeBodyMove(mouseEvent
);
159 // as the mouse moves, we want to make sure we reset the timer to show it,
160 // so that the delay is from when the mouse stops moving, not when it enters
164 // Hide the current tooltip if we change target nodes. If the new target
165 // has the same tooltip, we will open it again. We cannot compare
166 // the targets' tooltips because popupshowing events can set the tooltip.
169 mTooltipShownOnce
= false;
172 // If the mouse moves while the tooltip is up, hide it. If nothing is
173 // showing and the tooltip hasn't been displayed since the mouse entered
174 // the node, then start the timer to show the tooltip.
175 // If we have moved to a different target, we need to display the new tooltip,
176 // as the previous target's tooltip will have just been hidden.
177 if ((!currentTooltip
&& !mTooltipShownOnce
) || !isSameTarget
) {
178 // don't show tooltips attached to elements outside of a menu popup
179 // when hovering over an element inside it. The popupsinherittooltip
180 // attribute may be used to disable this behaviour, which is useful for
181 // large menu hierarchies such as bookmarks.
182 const bool allowTooltipCrossingPopup
=
183 !sourceContent
->GetParent() ||
184 (sourceContent
->IsElement() &&
185 sourceContent
->AsElement()->AttrValueIs(
186 kNameSpaceID_None
, nsGkAtoms::popupsinherittooltip
,
187 nsGkAtoms::_true
, eCaseMatters
));
188 if (!allowTooltipCrossingPopup
) {
189 for (auto* targetContent
= mouseMoveTarget
;
190 targetContent
&& targetContent
!= sourceContent
;
191 targetContent
= targetContent
->GetFlattenedTreeParent()) {
192 if (targetContent
->IsAnyOfXULElements(
193 nsGkAtoms::menupopup
, nsGkAtoms::panel
, nsGkAtoms::tooltip
)) {
194 mSourceNode
= nullptr;
200 mTargetNode
= do_GetWeakReference(mouseMoveTarget
);
202 nsresult rv
= NS_NewTimerWithFuncCallback(
203 getter_AddRefs(mTooltipTimer
), sTooltipCallback
, this,
204 StaticPrefs::ui_tooltip_delay_ms(), nsITimer::TYPE_ONE_SHOT
,
205 "sTooltipCallback", GetMainThreadSerialEventTarget());
207 mTargetNode
= nullptr;
208 mSourceNode
= nullptr;
217 // Hide the tooltip if it is currently showing.
218 if (currentTooltip
) {
220 // set a flag so that the tooltip is only displayed once until the mouse
222 mTooltipShownOnce
= true;
227 nsXULTooltipListener::HandleEvent(Event
* aEvent
) {
229 aEvent
->GetType(type
);
230 if (type
.EqualsLiteral("wheel") || type
.EqualsLiteral("mousedown") ||
231 type
.EqualsLiteral("mouseup") || type
.EqualsLiteral("dragstart")) {
236 if (type
.EqualsLiteral("keydown")) {
237 // Hide the tooltip if a non-modifier key is pressed.
238 WidgetKeyboardEvent
* keyEvent
= aEvent
->WidgetEventPtr()->AsKeyboardEvent();
239 if (KeyEventHidesTooltip(*keyEvent
)) {
245 if (type
.EqualsLiteral("popuphiding")) {
250 // Note that mousemove, mouseover and mouseout might be
251 // fired even during dragging due to widget's bug.
252 nsCOMPtr
<nsIDragService
> dragService
=
253 do_GetService("@mozilla.org/widget/dragservice;1");
254 NS_ENSURE_TRUE(dragService
, NS_OK
);
255 nsCOMPtr
<nsIDragSession
> dragSession
;
256 dragService
->GetCurrentSession(getter_AddRefs(dragSession
));
263 if (type
.EqualsLiteral("mousemove")) {
268 if (type
.EqualsLiteral("mouseout")) {
276 //////////////////////////////////////////////////////////////////////////
277 //// nsXULTooltipListener
279 bool nsXULTooltipListener::ShowTooltips() {
280 return StaticPrefs::browser_chrome_toolbar_tips();
283 bool nsXULTooltipListener::KeyEventHidesTooltip(
284 const WidgetKeyboardEvent
& aEvent
) {
285 switch (StaticPrefs::browser_chrome_toolbar_tips_hide_on_keydown()) {
291 return !aEvent
.IsModifierKeyEvent();
295 void nsXULTooltipListener::AddTooltipSupport(nsIContent
* aNode
) {
297 MOZ_ASSERT(this == sInstance
);
299 aNode
->AddSystemEventListener(u
"mouseout"_ns
, this, false, false);
300 aNode
->AddSystemEventListener(u
"mousemove"_ns
, this, false, false);
301 aNode
->AddSystemEventListener(u
"mousedown"_ns
, this, false, false);
302 aNode
->AddSystemEventListener(u
"mouseup"_ns
, this, false, false);
303 aNode
->AddSystemEventListener(u
"dragstart"_ns
, this, true, false);
306 void nsXULTooltipListener::RemoveTooltipSupport(nsIContent
* aNode
) {
308 MOZ_ASSERT(this == sInstance
);
310 // The last reference to us can go after some of these calls.
311 RefPtr
<nsXULTooltipListener
> instance
= this;
313 aNode
->RemoveSystemEventListener(u
"mouseout"_ns
, this, false);
314 aNode
->RemoveSystemEventListener(u
"mousemove"_ns
, this, false);
315 aNode
->RemoveSystemEventListener(u
"mousedown"_ns
, this, false);
316 aNode
->RemoveSystemEventListener(u
"mouseup"_ns
, this, false);
317 aNode
->RemoveSystemEventListener(u
"dragstart"_ns
, this, true);
320 void nsXULTooltipListener::CheckTreeBodyMove(MouseEvent
* aMouseEvent
) {
321 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
322 if (!sourceNode
) return;
324 // get the documentElement of the document the tree is in
325 Document
* doc
= sourceNode
->GetComposedDoc();
327 RefPtr
<XULTreeElement
> tree
= GetSourceTree();
328 Element
* root
= doc
? doc
->GetRootElement() : nullptr;
329 if (root
&& root
->GetPrimaryFrame() && tree
) {
330 CSSIntPoint pos
= aMouseEvent
->ScreenPoint(CallerType::System
);
332 // subtract off the documentElement's position
333 // XXX Isn't this just converting to client points?
334 CSSIntRect rect
= root
->GetPrimaryFrame()->GetScreenRect();
335 pos
-= rect
.TopLeft();
338 TreeCellInfo cellInfo
;
339 tree
->GetCellAt(pos
.x
, pos
.y
, cellInfo
, rv
);
341 int32_t row
= cellInfo
.mRow
;
342 RefPtr
<nsTreeColumn
> col
= cellInfo
.mCol
;
344 // determine if we are going to need a titletip
345 // XXX check the disabletitletips attribute on the tree content
346 mNeedTitletip
= false;
347 if (row
>= 0 && cellInfo
.mChildElt
.EqualsLiteral("text")) {
348 mNeedTitletip
= tree
->IsCellCropped(row
, col
, rv
);
351 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
352 if (currentTooltip
&& (row
!= mLastTreeRow
|| col
!= mLastTreeCol
)) {
361 nsresult
nsXULTooltipListener::ShowTooltip() {
362 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
364 // get the tooltip content designated for the target node
365 nsCOMPtr
<nsIContent
> tooltipNode
;
366 GetTooltipFor(sourceNode
, getter_AddRefs(tooltipNode
));
367 if (!tooltipNode
|| sourceNode
== tooltipNode
) {
368 return NS_ERROR_FAILURE
; // the target node doesn't need a tooltip
371 // set the node in the document that triggered the tooltip and show it
372 // Make sure the document still has focus.
373 auto* doc
= tooltipNode
->GetComposedDoc();
374 if (!doc
|| !nsContentUtils::IsChromeDoc(doc
) ||
375 doc
->IsTopLevelWindowInactive()) {
379 // Make sure the target node is still attached to some document.
380 // It might have been deleted.
381 if (!sourceNode
->IsInComposedDoc()) {
385 if (!mIsSourceTree
) {
387 mLastTreeCol
= nullptr;
390 mCurrentTooltip
= do_GetWeakReference(tooltipNode
);
392 mTargetNode
= nullptr;
394 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
395 if (!currentTooltip
) {
399 // Listen for popuphidden on the tooltip node so that we can be sure
400 // DestroyPopup is called even if someone else closes the tooltip.
401 currentTooltip
->AddSystemEventListener(u
"popuphiding"_ns
, this, false, false);
403 // Listen for mousedown, mouseup, keydown, and mouse events at document level.
404 if (Document
* doc
= sourceNode
->GetComposedDoc()) {
405 // Probably, we should listen to untrusted events for hiding tooltips on
406 // content since tooltips might disturb something of web applications. If we
407 // don't specify the aWantsUntrusted of AddSystemEventListener(), the event
408 // target sets it to TRUE if the target is in content.
409 doc
->AddSystemEventListener(u
"wheel"_ns
, this, true);
410 doc
->AddSystemEventListener(u
"mousedown"_ns
, this, true);
411 doc
->AddSystemEventListener(u
"mouseup"_ns
, this, true);
412 doc
->AddSystemEventListener(u
"keydown"_ns
, this, true);
414 mSourceNode
= nullptr;
419 static void SetTitletipLabel(XULTreeElement
* aTree
, Element
* aTooltip
,
420 int32_t aRow
, nsTreeColumn
* aCol
) {
421 nsCOMPtr
<nsITreeView
> view
= aTree
->GetView();
427 view
->GetCellText(aRow
, aCol
, label
);
428 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "Couldn't get the cell text!");
429 aTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, label
, true);
433 void nsXULTooltipListener::LaunchTooltip() {
434 RefPtr
<Element
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
435 if (!currentTooltip
) {
439 if (mIsSourceTree
&& mNeedTitletip
) {
440 RefPtr
<XULTreeElement
> tree
= GetSourceTree();
442 SetTitletipLabel(tree
, currentTooltip
, mLastTreeRow
, mLastTreeCol
);
443 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
444 // Because of mutation events, currentTooltip can be null.
447 currentTooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, u
"true"_ns
,
450 currentTooltip
->UnsetAttr(kNameSpaceID_None
, nsGkAtoms::titletip
, true);
453 if (!(currentTooltip
= do_QueryReferent(mCurrentTooltip
))) {
454 // Because of mutation events, currentTooltip can be null.
458 nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance();
463 auto cleanup
= MakeScopeExit([&] {
464 // Clear the current tooltip if the popup was not opened successfully.
465 if (!pm
->IsPopupOpen(currentTooltip
)) {
466 mCurrentTooltip
= nullptr;
470 RefPtr
<Element
> target
= do_QueryReferent(mTargetNode
);
475 pm
->ShowTooltipAtScreen(currentTooltip
, target
, mMouseScreenPoint
);
478 nsresult
nsXULTooltipListener::HideTooltip() {
479 if (nsCOMPtr
<Element
> currentTooltip
= do_QueryReferent(mCurrentTooltip
)) {
480 if (nsXULPopupManager
* pm
= nsXULPopupManager::GetInstance()) {
481 pm
->HidePopup(currentTooltip
, {});
489 static void GetImmediateChild(nsIContent
* aContent
, nsAtom
* aTag
,
490 nsIContent
** aResult
) {
492 for (nsCOMPtr
<nsIContent
> childContent
= aContent
->GetFirstChild();
493 childContent
; childContent
= childContent
->GetNextSibling()) {
494 if (childContent
->IsXULElement(aTag
)) {
495 childContent
.forget(aResult
);
501 nsresult
nsXULTooltipListener::FindTooltip(nsIContent
* aTarget
,
502 nsIContent
** aTooltip
) {
504 return NS_ERROR_NULL_POINTER
;
507 // before we go on, make sure that target node still has a window
508 Document
* document
= aTarget
->GetComposedDoc();
510 NS_WARNING("Unable to retrieve the tooltip node document.");
511 return NS_ERROR_FAILURE
;
513 nsPIDOMWindowOuter
* window
= document
->GetWindow();
518 if (window
->Closed()) {
522 // non-XUL elements should just use the default tooltip
523 if (!aTarget
->IsXULElement()) {
524 nsIPopupContainer
* popupContainer
=
525 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
526 NS_ENSURE_STATE(popupContainer
);
527 if (RefPtr
<Element
> tooltip
= popupContainer
->GetDefaultTooltip()) {
528 tooltip
.forget(aTooltip
);
531 return NS_ERROR_FAILURE
;
534 // On Windows, the OS shows the tooltip, so we don't want Gecko to do it
536 if (nsIFrame
* f
= aTarget
->GetPrimaryFrame()) {
537 if (f
->StyleDisplay()->GetWindowButtonType()) {
543 nsAutoString tooltipText
;
544 aTarget
->AsElement()->GetAttr(nsGkAtoms::tooltiptext
, tooltipText
);
546 if (!tooltipText
.IsEmpty()) {
547 // specifying tooltiptext means we will always use the default tooltip
548 nsIPopupContainer
* popupContainer
=
549 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
550 NS_ENSURE_STATE(popupContainer
);
551 if (RefPtr
<Element
> tooltip
= popupContainer
->GetDefaultTooltip()) {
552 tooltip
->SetAttr(kNameSpaceID_None
, nsGkAtoms::label
, tooltipText
, true);
553 tooltip
.forget(aTooltip
);
558 nsAutoString tooltipId
;
559 aTarget
->AsElement()->GetAttr(nsGkAtoms::tooltip
, tooltipId
);
561 // if tooltip == _child, look for first <tooltip> child
562 if (tooltipId
.EqualsLiteral("_child")) {
563 GetImmediateChild(aTarget
, nsGkAtoms::tooltip
, aTooltip
);
567 if (!tooltipId
.IsEmpty()) {
568 DocumentOrShadowRoot
* documentOrShadowRoot
=
569 aTarget
->GetUncomposedDocOrConnectedShadowRoot();
570 // tooltip must be an id, use getElementById to find it
571 if (documentOrShadowRoot
) {
572 nsCOMPtr
<nsIContent
> tooltipEl
=
573 documentOrShadowRoot
->GetElementById(tooltipId
);
576 mNeedTitletip
= false;
577 tooltipEl
.forget(aTooltip
);
583 // titletips should just use the default tooltip
584 if (mIsSourceTree
&& mNeedTitletip
) {
585 nsIPopupContainer
* popupContainer
=
586 nsIPopupContainer::GetPopupContainer(document
->GetPresShell());
587 NS_ENSURE_STATE(popupContainer
);
588 NS_IF_ADDREF(*aTooltip
= popupContainer
->GetDefaultTooltip());
594 nsresult
nsXULTooltipListener::GetTooltipFor(nsIContent
* aTarget
,
595 nsIContent
** aTooltip
) {
597 nsCOMPtr
<nsIContent
> tooltip
;
598 nsresult rv
= FindTooltip(aTarget
, getter_AddRefs(tooltip
));
599 if (NS_FAILED(rv
) || !tooltip
) {
603 // Submenus can't be used as tooltips, see bug 288763.
604 if (nsIContent
* parent
= tooltip
->GetParent()) {
605 if (auto* button
= XULButtonElement::FromNode(parent
)) {
606 if (button
->IsMenu()) {
607 NS_WARNING("Menu cannot be used as a tooltip");
608 return NS_ERROR_FAILURE
;
613 tooltip
.swap(*aTooltip
);
617 nsresult
nsXULTooltipListener::DestroyTooltip() {
618 nsCOMPtr
<nsIDOMEventListener
> kungFuDeathGrip(this);
619 nsCOMPtr
<nsIContent
> currentTooltip
= do_QueryReferent(mCurrentTooltip
);
620 if (currentTooltip
) {
621 // release tooltip before removing listener to prevent our destructor from
622 // being called recursively (bug 120863)
623 mCurrentTooltip
= nullptr;
625 // clear out the tooltip node on the document
626 if (nsCOMPtr
<Document
> doc
= currentTooltip
->GetComposedDoc()) {
627 // remove the mousedown and keydown listener from document
628 doc
->RemoveSystemEventListener(u
"wheel"_ns
, this, true);
629 doc
->RemoveSystemEventListener(u
"mousedown"_ns
, this, true);
630 doc
->RemoveSystemEventListener(u
"mouseup"_ns
, this, true);
631 doc
->RemoveSystemEventListener(u
"keydown"_ns
, this, true);
634 // remove the popuphidden listener from tooltip
635 currentTooltip
->RemoveSystemEventListener(u
"popuphiding"_ns
, this, false);
638 // kill any ongoing timers
640 mSourceNode
= nullptr;
641 mLastTreeCol
= nullptr;
646 void nsXULTooltipListener::KillTooltipTimer() {
648 mTooltipTimer
->Cancel();
649 mTooltipTimer
= nullptr;
650 mTargetNode
= nullptr;
654 void nsXULTooltipListener::sTooltipCallback(nsITimer
* aTimer
, void* aListener
) {
655 RefPtr
<nsXULTooltipListener
> instance
= sInstance
;
656 if (instance
) instance
->ShowTooltip();
659 XULTreeElement
* nsXULTooltipListener::GetSourceTree() {
660 nsCOMPtr
<nsIContent
> sourceNode
= do_QueryReferent(mSourceNode
);
661 if (mIsSourceTree
&& sourceNode
) {
662 RefPtr
<XULTreeElement
> xulEl
=
663 XULTreeElement::FromNodeOrNull(sourceNode
->GetParent());