summaryrefslogtreecommitdiffstats
path: root/layout/xul
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /layout/xul
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'layout/xul')
-rw-r--r--layout/xul/MiddleCroppingLabelFrame.cpp38
-rw-r--r--layout/xul/MiddleCroppingLabelFrame.h29
-rw-r--r--layout/xul/SimpleXULLeafFrame.cpp37
-rw-r--r--layout/xul/SimpleXULLeafFrame.h46
-rw-r--r--layout/xul/crashtests/131008-1.xhtml11
-rw-r--r--layout/xul/crashtests/137216-1.xhtml4
-rw-r--r--layout/xul/crashtests/1379332-2.xhtml9
-rw-r--r--layout/xul/crashtests/140218-1.xml4
-rw-r--r--layout/xul/crashtests/151826-1.xhtml27
-rw-r--r--layout/xul/crashtests/168724-1.xhtml18
-rw-r--r--layout/xul/crashtests/289410-1.xhtml14
-rw-r--r--layout/xul/crashtests/291702-1.xhtml11
-rw-r--r--layout/xul/crashtests/291702-2.xhtml11
-rw-r--r--layout/xul/crashtests/291702-3.xhtml137
-rw-r--r--layout/xul/crashtests/294371-1.xhtml53
-rw-r--r--layout/xul/crashtests/322786-1.xhtml6
-rw-r--r--layout/xul/crashtests/325377.xhtml16
-rw-r--r--layout/xul/crashtests/326879-1.xhtml31
-rw-r--r--layout/xul/crashtests/329327-1.xhtml2
-rw-r--r--layout/xul/crashtests/329407-1.xml14
-rw-r--r--layout/xul/crashtests/336962-1.xhtml18
-rw-r--r--layout/xul/crashtests/344228-1.xhtml27
-rw-r--r--layout/xul/crashtests/365151.xhtml39
-rw-r--r--layout/xul/crashtests/366112-1.xhtml9
-rw-r--r--layout/xul/crashtests/366203-1.xhtml40
-rw-r--r--layout/xul/crashtests/367185-1.xhtml11
-rw-r--r--layout/xul/crashtests/369942-1.xhtml36
-rw-r--r--layout/xul/crashtests/376137-1.html18
-rw-r--r--layout/xul/crashtests/376137-2.html11
-rw-r--r--layout/xul/crashtests/378961.html9
-rw-r--r--layout/xul/crashtests/381862.html23
-rw-r--r--layout/xul/crashtests/382746-1.xhtml15
-rw-r--r--layout/xul/crashtests/382899-1.xhtml9
-rw-r--r--layout/xul/crashtests/384037-1.xhtml9
-rw-r--r--layout/xul/crashtests/384105-1-inner.xhtml21
-rw-r--r--layout/xul/crashtests/384105-1.html9
-rw-r--r--layout/xul/crashtests/384373-1.xhtml10
-rw-r--r--layout/xul/crashtests/384373-2.xhtml4
-rw-r--r--layout/xul/crashtests/384373.html23
-rw-r--r--layout/xul/crashtests/384871-1-inner.xhtml9
-rw-r--r--layout/xul/crashtests/384871-1.html9
-rw-r--r--layout/xul/crashtests/386642.xhtml31
-rw-r--r--layout/xul/crashtests/387080-1.xhtml6
-rw-r--r--layout/xul/crashtests/391974-1-inner.xhtml19
-rw-r--r--layout/xul/crashtests/391974-1.html9
-rw-r--r--layout/xul/crashtests/402912-1.xhtml5
-rw-r--r--layout/xul/crashtests/404192.xhtml12
-rw-r--r--layout/xul/crashtests/408904-1.xhtml1
-rw-r--r--layout/xul/crashtests/412479-1.xhtml4
-rw-r--r--layout/xul/crashtests/417509.xhtml7
-rw-r--r--layout/xul/crashtests/430356-1.xhtml5
-rw-r--r--layout/xul/crashtests/464407-1.xhtml9
-rw-r--r--layout/xul/crashtests/470063-1.html15
-rw-r--r--layout/xul/crashtests/470272.html21
-rw-r--r--layout/xul/crashtests/538308-1.xhtml32
-rw-r--r--layout/xul/crashtests/557174-1.xml1
-rw-r--r--layout/xul/crashtests/564705-1.xhtml6
-rw-r--r--layout/xul/crashtests/583957-1.html20
-rw-r--r--layout/xul/crashtests/617089.html9
-rw-r--r--layout/xul/crashtests/716503.html11
-rw-r--r--layout/xul/crashtests/crashtests.list52
-rw-r--r--layout/xul/crashtests/menulist-focused.xhtml5
-rw-r--r--layout/xul/moz.build50
-rw-r--r--layout/xul/nsIPopupContainer.h29
-rw-r--r--layout/xul/nsIScrollbarMediator.h101
-rw-r--r--layout/xul/nsMenuPopupFrame.cpp2433
-rw-r--r--layout/xul/nsMenuPopupFrame.h643
-rw-r--r--layout/xul/nsRepeatService.cpp93
-rw-r--r--layout/xul/nsRepeatService.h73
-rw-r--r--layout/xul/nsScrollbarButtonFrame.cpp272
-rw-r--r--layout/xul/nsScrollbarButtonFrame.h85
-rw-r--r--layout/xul/nsScrollbarFrame.cpp595
-rw-r--r--layout/xul/nsScrollbarFrame.h150
-rw-r--r--layout/xul/nsSliderFrame.cpp1578
-rw-r--r--layout/xul/nsSliderFrame.h235
-rw-r--r--layout/xul/nsSplitterFrame.cpp964
-rw-r--r--layout/xul/nsSplitterFrame.h84
-rw-r--r--layout/xul/nsXULPopupManager.cpp2887
-rw-r--r--layout/xul/nsXULPopupManager.h898
-rw-r--r--layout/xul/nsXULTooltipListener.cpp664
-rw-r--r--layout/xul/nsXULTooltipListener.h99
-rw-r--r--layout/xul/reftest/checkbox-dynamic-change-ref.xhtml6
-rw-r--r--layout/xul/reftest/checkbox-dynamic-change.xhtml17
-rw-r--r--layout/xul/reftest/image-scaling-min-height-1-ref.xhtml14
-rw-r--r--layout/xul/reftest/image-scaling-min-height-1.xhtml14
-rw-r--r--layout/xul/reftest/image-size-ref.xhtml102
-rw-r--r--layout/xul/reftest/image-size.xhtml104
-rw-r--r--layout/xul/reftest/image4x3.pngbin0 -> 176 bytes
-rw-r--r--layout/xul/reftest/popup-explicit-size-ref.xhtml6
-rw-r--r--layout/xul/reftest/popup-explicit-size.xhtml7
-rw-r--r--layout/xul/reftest/radio-dynamic-change-ref.xhtml6
-rw-r--r--layout/xul/reftest/radio-dynamic-change.xhtml17
-rw-r--r--layout/xul/reftest/reftest.list14
-rw-r--r--layout/xul/reftest/scrollbar-marks-overlay-ref.html64
-rw-r--r--layout/xul/reftest/scrollbar-marks-overlay.html18
-rw-r--r--layout/xul/reftest/scrollbar-marks-ref.html13
-rw-r--r--layout/xul/reftest/scrollbar-marks.html18
-rw-r--r--layout/xul/reftest/scrollbar-marks2.html19
-rw-r--r--layout/xul/reftest/textbox-text-transform-ref.xhtml6
-rw-r--r--layout/xul/reftest/textbox-text-transform.xhtml6
-rw-r--r--layout/xul/test/browser.ini11
-rw-r--r--layout/xul/test/browser_bug1163304.js83
-rw-r--r--layout/xul/test/browser_bug1754298.js35
-rw-r--r--layout/xul/test/browser_bug685470.js38
-rw-r--r--layout/xul/test/browser_bug703210.js56
-rw-r--r--layout/xul/test/browser_bug706743.js158
-rw-r--r--layout/xul/test/chrome.ini38
-rw-r--r--layout/xul/test/file_bug386386.sjs14
-rw-r--r--layout/xul/test/mochitest.ini17
-rw-r--r--layout/xul/test/test_bug1197913.xhtml63
-rw-r--r--layout/xul/test/test_bug159346.xhtml143
-rw-r--r--layout/xul/test/test_bug381167.xhtml52
-rw-r--r--layout/xul/test/test_bug386386.html34
-rw-r--r--layout/xul/test/test_bug394800.xhtml39
-rw-r--r--layout/xul/test/test_bug398982-1.xhtml31
-rw-r--r--layout/xul/test/test_bug398982-2.xhtml33
-rw-r--r--layout/xul/test/test_bug467442.xhtml53
-rw-r--r--layout/xul/test/test_bug477754.xhtml51
-rw-r--r--layout/xul/test/test_bug511075.html121
-rw-r--r--layout/xul/test/test_bug563416.html53
-rw-r--r--layout/xul/test/test_bug703150.xhtml74
-rw-r--r--layout/xul/test/test_bug987230.xhtml109
-rw-r--r--layout/xul/test/test_drag_thumb_in_link.html76
-rw-r--r--layout/xul/test/test_menuitem_ctrl_click.xhtml80
-rw-r--r--layout/xul/test/test_popupReflowPos.xhtml77
-rw-r--r--layout/xul/test/test_popupSizeTo.xhtml55
-rw-r--r--layout/xul/test/test_popupZoom.xhtml53
-rw-r--r--layout/xul/test/test_resizer_ctrl_click.xhtml51
-rw-r--r--layout/xul/test/test_resizer_incontent.xhtml42
-rw-r--r--layout/xul/test/test_splitter.xhtml117
-rw-r--r--layout/xul/test/test_splitter_sibling.xhtml88
-rw-r--r--layout/xul/test/test_submenuClose.xhtml91
-rw-r--r--layout/xul/test/test_toolbarbutton_ctrl_click.xhtml51
-rw-r--r--layout/xul/test/test_windowminmaxsize.xhtml193
-rw-r--r--layout/xul/test/titledpanelwindow.xhtml5
-rw-r--r--layout/xul/test/windowminmaxsize1.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize10.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize2.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize3.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize4.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize5.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize6.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize7.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize8.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize9.xhtml4
-rw-r--r--layout/xul/tree/crashtests/307298-1.xhtml21
-rw-r--r--layout/xul/tree/crashtests/309732-1.xhtml30
-rw-r--r--layout/xul/tree/crashtests/309732-2.xhtml31
-rw-r--r--layout/xul/tree/crashtests/366583-1.xhtml43
-rw-r--r--layout/xul/tree/crashtests/380217-1.xhtml31
-rw-r--r--layout/xul/tree/crashtests/382444-1-inner.html15
-rw-r--r--layout/xul/tree/crashtests/382444-1.html9
-rw-r--r--layout/xul/tree/crashtests/391178-1.xhtml41
-rw-r--r--layout/xul/tree/crashtests/391178-2.xhtml20
-rw-r--r--layout/xul/tree/crashtests/393665-1.xhtml3
-rw-r--r--layout/xul/tree/crashtests/399227-1.xhtml44
-rw-r--r--layout/xul/tree/crashtests/399692-1.xhtml10
-rw-r--r--layout/xul/tree/crashtests/399715-1.xhtml9
-rw-r--r--layout/xul/tree/crashtests/409807-1.xhtml25
-rw-r--r--layout/xul/tree/crashtests/414170-1.xhtml20
-rw-r--r--layout/xul/tree/crashtests/479931-1.xhtml19
-rw-r--r--layout/xul/tree/crashtests/585815-iframe.xhtml72
-rw-r--r--layout/xul/tree/crashtests/585815.html18
-rw-r--r--layout/xul/tree/crashtests/601427.html30
-rw-r--r--layout/xul/tree/crashtests/730441-3.xhtml38
-rw-r--r--layout/xul/tree/crashtests/crashtests.list18
-rw-r--r--layout/xul/tree/moz.build44
-rw-r--r--layout/xul/tree/nsITreeSelection.idl119
-rw-r--r--layout/xul/tree/nsITreeView.idl173
-rw-r--r--layout/xul/tree/nsTreeBodyFrame.cpp4363
-rw-r--r--layout/xul/tree/nsTreeBodyFrame.h607
-rw-r--r--layout/xul/tree/nsTreeColumns.cpp462
-rw-r--r--layout/xul/tree/nsTreeColumns.h214
-rw-r--r--layout/xul/tree/nsTreeContentView.cpp1269
-rw-r--r--layout/xul/tree/nsTreeContentView.h164
-rw-r--r--layout/xul/tree/nsTreeImageListener.cpp115
-rw-r--r--layout/xul/tree/nsTreeImageListener.h67
-rw-r--r--layout/xul/tree/nsTreeSelection.cpp724
-rw-r--r--layout/xul/tree/nsTreeSelection.h56
-rw-r--r--layout/xul/tree/nsTreeStyleCache.cpp103
-rw-r--r--layout/xul/tree/nsTreeStyleCache.h82
-rw-r--r--layout/xul/tree/nsTreeUtils.cpp135
-rw-r--r--layout/xul/tree/nsTreeUtils.h43
183 files changed, 25153 insertions, 0 deletions
diff --git a/layout/xul/MiddleCroppingLabelFrame.cpp b/layout/xul/MiddleCroppingLabelFrame.cpp
new file mode 100644
index 0000000000..73cb15be74
--- /dev/null
+++ b/layout/xul/MiddleCroppingLabelFrame.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MiddleCroppingLabelFrame.h"
+#include "MiddleCroppingBlockFrame.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/PresShell.h"
+
+nsIFrame* NS_NewMiddleCroppingLabelFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle) {
+ return new (aPresShell)
+ mozilla::MiddleCroppingLabelFrame(aStyle, aPresShell->GetPresContext());
+}
+
+namespace mozilla {
+
+void MiddleCroppingLabelFrame::GetUncroppedValue(nsAString& aValue) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::value, aValue);
+}
+
+nsresult MiddleCroppingLabelFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::value) {
+ UpdateDisplayedValueToUncroppedValue(true);
+ }
+ return NS_OK;
+}
+
+NS_QUERYFRAME_HEAD(MiddleCroppingLabelFrame)
+ NS_QUERYFRAME_ENTRY(MiddleCroppingLabelFrame)
+NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame)
+NS_IMPL_FRAMEARENA_HELPERS(MiddleCroppingLabelFrame)
+
+} // namespace mozilla
diff --git a/layout/xul/MiddleCroppingLabelFrame.h b/layout/xul/MiddleCroppingLabelFrame.h
new file mode 100644
index 0000000000..2d0bdf8686
--- /dev/null
+++ b/layout/xul/MiddleCroppingLabelFrame.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MiddleCroppingLabelFrame_h
+#define mozilla_MiddleCroppingLabelFrame_h
+
+#include "MiddleCroppingBlockFrame.h"
+namespace mozilla {
+
+// A frame for a <xul:label> or <xul:description> with crop="center"
+class MiddleCroppingLabelFrame final : public MiddleCroppingBlockFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(MiddleCroppingLabelFrame)
+
+ MiddleCroppingLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {}
+
+ void GetUncroppedValue(nsAString& aValue) override;
+ nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/layout/xul/SimpleXULLeafFrame.cpp b/layout/xul/SimpleXULLeafFrame.cpp
new file mode 100644
index 0000000000..96b3d81762
--- /dev/null
+++ b/layout/xul/SimpleXULLeafFrame.cpp
@@ -0,0 +1,37 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SimpleXULLeafFrame.h"
+#include "mozilla/PresShell.h"
+
+nsIFrame* NS_NewSimpleXULLeafFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle) {
+ return new (aPresShell)
+ mozilla::SimpleXULLeafFrame(aStyle, aPresShell->GetPresContext());
+}
+
+namespace mozilla {
+
+NS_IMPL_FRAMEARENA_HELPERS(SimpleXULLeafFrame)
+
+void SimpleXULLeafFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ const auto wm = GetWritingMode();
+ const auto& bp = aReflowInput.ComputedLogicalBorderPadding(wm);
+ aDesiredSize.ISize(wm) = bp.IStartEnd(wm) + aReflowInput.ComputedISize();
+ aDesiredSize.BSize(wm) =
+ bp.BStartEnd(wm) + (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE
+ ? aReflowInput.ComputedMinBSize()
+ : aReflowInput.ComputedBSize());
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+}
+
+} // namespace mozilla
diff --git a/layout/xul/SimpleXULLeafFrame.h b/layout/xul/SimpleXULLeafFrame.h
new file mode 100644
index 0000000000..468b1f3d2a
--- /dev/null
+++ b/layout/xul/SimpleXULLeafFrame.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A simple frame class for XUL frames that are leafs on the tree but need
+// background / border painting, and for some reason or another need special
+// code (like event handling code) which we haven't ported to the DOM.
+//
+// This should generally not be used for new frame classes.
+
+#ifndef mozilla_SimpleXULLeafFrame_h
+#define mozilla_SimpleXULLeafFrame_h
+
+#include "nsLeafFrame.h"
+
+namespace mozilla {
+
+// Shared class for thumb and scrollbar buttons.
+class SimpleXULLeafFrame : public nsLeafFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(SimpleXULLeafFrame)
+
+ // TODO: Look at appearance instead maybe?
+ nscoord GetIntrinsicISize() override { return 0; }
+ nscoord GetIntrinsicBSize() override { return 0; }
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+ explicit SimpleXULLeafFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext, ClassID aClassID)
+ : nsLeafFrame(aStyle, aPresContext, aClassID) {}
+
+ explicit SimpleXULLeafFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : SimpleXULLeafFrame(aStyle, aPresContext, kClassID) {}
+
+ friend nsIFrame* NS_NewSimpleXULLeafFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/layout/xul/crashtests/131008-1.xhtml b/layout/xul/crashtests/131008-1.xhtml
new file mode 100644
index 0000000000..fe15a46aa6
--- /dev/null
+++ b/layout/xul/crashtests/131008-1.xhtml
@@ -0,0 +1,11 @@
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="MainWindow"
+ title="IWindow Test">
+<div style="display: block; position:absolute">abc</div>
+
+
+</window>
diff --git a/layout/xul/crashtests/137216-1.xhtml b/layout/xul/crashtests/137216-1.xhtml
new file mode 100644
index 0000000000..e01541c622
--- /dev/null
+++ b/layout/xul/crashtests/137216-1.xhtml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <iframe style="position:absolute; display: block;"/>
+</window>
diff --git a/layout/xul/crashtests/1379332-2.xhtml b/layout/xul/crashtests/1379332-2.xhtml
new file mode 100644
index 0000000000..cab6145c44
--- /dev/null
+++ b/layout/xul/crashtests/1379332-2.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox style="position: relative;visibility: collapse;">
+ <hbox style="padding:5px; border: 5px solid black">
+ <hbox style="position: absolute; display: block; width: 10px; height: 10px">
+ </hbox>
+ </hbox>
+ </hbox>
+</window>
diff --git a/layout/xul/crashtests/140218-1.xml b/layout/xul/crashtests/140218-1.xml
new file mode 100644
index 0000000000..311afc2188
--- /dev/null
+++ b/layout/xul/crashtests/140218-1.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <treechildren style = " display: block; " />
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/151826-1.xhtml b/layout/xul/crashtests/151826-1.xhtml
new file mode 100644
index 0000000000..bb8ee2e200
--- /dev/null
+++ b/layout/xul/crashtests/151826-1.xhtml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ title = "Arrowscrollbox->Splitter Crash Testcase"
+ xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width = "300"
+ height = "200"
+ orient = "vertical"
+>
+<vbox flex="1">
+
+<scrollbox flex="1">
+<vbox flex="1">
+<vbox id="box_1">
+<hbox><label value="Test"/></hbox>
+</vbox>
+<splitter collapse="none"/>
+<vbox id="box_2">
+<hbox><label value="Test"/></hbox>
+</vbox>
+</vbox>
+</scrollbox>
+
+</vbox>
+</window>
diff --git a/layout/xul/crashtests/168724-1.xhtml b/layout/xul/crashtests/168724-1.xhtml
new file mode 100644
index 0000000000..61d4f48327
--- /dev/null
+++ b/layout/xul/crashtests/168724-1.xhtml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css" ?>
+
+<window
+ id="nodeCreator" title="Node Creator"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+<description context="context">Right-click here, and expect a crash.</description>
+
+<popupset id="context-set">
+<popup id="context">
+<deck selectedItem="0">
+<menuitem label="You should never see this" />
+</deck>
+</popup>
+</popupset>
+</window>
diff --git a/layout/xul/crashtests/289410-1.xhtml b/layout/xul/crashtests/289410-1.xhtml
new file mode 100644
index 0000000000..fa235b607e
--- /dev/null
+++ b/layout/xul/crashtests/289410-1.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window id="crash-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <scrollbox>
+ <tree id="crash-tree">
+ <treecols/>
+ <treechildren/>
+ </tree>
+ </scrollbox>
+
+</window>
diff --git a/layout/xul/crashtests/291702-1.xhtml b/layout/xul/crashtests/291702-1.xhtml
new file mode 100644
index 0000000000..6b36046d16
--- /dev/null
+++ b/layout/xul/crashtests/291702-1.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="horizontal"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is a label" style="-moz-box-flex: 1"/>
+ <label value="This is the second label" style="-moz-box-flex: -2"/>
+ <label value="This is another label" style="-moz-box-flex: -1"/>
+</window>
diff --git a/layout/xul/crashtests/291702-2.xhtml b/layout/xul/crashtests/291702-2.xhtml
new file mode 100644
index 0000000000..a47dbbdd41
--- /dev/null
+++ b/layout/xul/crashtests/291702-2.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="horizontal"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is a label" style="-moz-box-flex: 1073741824"/>
+ <label value="This is the second label" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+</window>
diff --git a/layout/xul/crashtests/291702-3.xhtml b/layout/xul/crashtests/291702-3.xhtml
new file mode 100644
index 0000000000..6f947f4887
--- /dev/null
+++ b/layout/xul/crashtests/291702-3.xhtml
@@ -0,0 +1,137 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="vertical"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+</window>
diff --git a/layout/xul/crashtests/294371-1.xhtml b/layout/xul/crashtests/294371-1.xhtml
new file mode 100644
index 0000000000..ca5b54914a
--- /dev/null
+++ b/layout/xul/crashtests/294371-1.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ id = "overflow crash"
+ title = "scrollbox crasher"
+ xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="sizemode width height screenX screenY"
+ width="320"
+ height="240">
+
+ <scrollbox flex="1">
+ <grid style="overflow: auto">
+ <columns>
+ <column flex="0"/>
+ </columns>
+ <rows>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ </rows>
+ </grid>
+ </scrollbox>
+
+</window>
diff --git a/layout/xul/crashtests/322786-1.xhtml b/layout/xul/crashtests/322786-1.xhtml
new file mode 100644
index 0000000000..79bb092c4b
--- /dev/null
+++ b/layout/xul/crashtests/322786-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <foo style="display: inline;">
+ <scrollbox/>
+ </foo>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/325377.xhtml b/layout/xul/crashtests/325377.xhtml
new file mode 100644
index 0000000000..8ea30473d8
--- /dev/null
+++ b/layout/xul/crashtests/325377.xhtml
@@ -0,0 +1,16 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testcase bug 325377 - Crash on reload with evil xul textcase, using menulist and nested tooltips">
+<menulist style="display: table-cell;">
+<tooltip style="display: none;">
+ <tooltip/>
+</tooltip>
+</menulist>
+
+<html:script>
+function removestyles(){
+document.getElementsByTagName('tooltip')[0].removeAttribute('style');
+}
+try { document.getElementsByTagName('tooltip')[0].offsetHeight; } catch(e) {}
+setTimeout(removestyles,0);
+</html:script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/326879-1.xhtml b/layout/xul/crashtests/326879-1.xhtml
new file mode 100644
index 0000000000..26965ae65e
--- /dev/null
+++ b/layout/xul/crashtests/326879-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+
+
+<script>
+
+
+function init() {
+
+ var menupopup = document.getElementsByTagName("menupopup")[0];
+ menupopup.style.MozBoxOrdinalGroup = null;
+};
+
+
+window.addEventListener("load", init, false);
+
+</script>
+
+
+<menulist>
+ <menupopup>
+ <menuitem label="Foo"/>
+ </menupopup>
+</menulist>
+
+
+
+</window>
diff --git a/layout/xul/crashtests/329327-1.xhtml b/layout/xul/crashtests/329327-1.xhtml
new file mode 100644
index 0000000000..fcfed07c4c
--- /dev/null
+++ b/layout/xul/crashtests/329327-1.xhtml
@@ -0,0 +1,2 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><menulist equalsize="always"><y/> <z width="-444981589286"/> </menulist></window>
diff --git a/layout/xul/crashtests/329407-1.xml b/layout/xul/crashtests/329407-1.xml
new file mode 100644
index 0000000000..0d41c0185f
--- /dev/null
+++ b/layout/xul/crashtests/329407-1.xml
@@ -0,0 +1,14 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+
+<body>
+
+ <xul:hbox>
+ <select/>
+ <select/>
+ </xul:hbox>
+
+</body>
+
+</html>
diff --git a/layout/xul/crashtests/336962-1.xhtml b/layout/xul/crashtests/336962-1.xhtml
new file mode 100644
index 0000000000..bd2129a853
--- /dev/null
+++ b/layout/xul/crashtests/336962-1.xhtml
@@ -0,0 +1,18 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script>
+
+function init() {
+ document.getElementById("foopy").style.display = "block";
+ document.getElementById("foopy").style.position = "absolute";
+}
+
+window.addEventListener("load", init, 0);
+
+</script>
+
+
+<box id="foopy" />
+
+
+</window>
diff --git a/layout/xul/crashtests/344228-1.xhtml b/layout/xul/crashtests/344228-1.xhtml
new file mode 100644
index 0000000000..d6015707bd
--- /dev/null
+++ b/layout/xul/crashtests/344228-1.xhtml
@@ -0,0 +1,27 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="setTimeout(boom, 30);" class="reftest-wait">
+
+<script>
+
+function remove(q1) { q1.parentNode.removeChild(q1); }
+
+function boom()
+{
+ var x = document.getElementById("x");
+ var y = document.getElementById("y");
+ remove(x);
+ remove(y);
+
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+<tree>
+ <treechildren id="y"/>
+ <richlistbox>
+ <hbox id="x"/>
+ <menulist/>
+ </richlistbox>
+</tree>
+
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/365151.xhtml b/layout/xul/crashtests/365151.xhtml
new file mode 100644
index 0000000000..001707f4eb
--- /dev/null
+++ b/layout/xul/crashtests/365151.xhtml
@@ -0,0 +1,39 @@
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom()" class="reftest-wait">
+
+
+<script>
+function boom()
+{
+ try {
+ var tree = document.getElementById("tree");
+ var col = tree.columns.getFirstColumn();
+ var treecols = document.getElementById("treecols");
+ treecols.parentNode.removeChild(treecols);
+ var x = col.x;
+ } finally {
+ document.documentElement.removeAttribute("class");
+ }
+}
+</script>
+
+
+<tree rows="6" id="tree">
+
+ <treecols id="treecols">
+ <treecol id="firstname" label="First Name"/>
+ </treecols>
+
+ <treechildren id="treechildren">
+ <treeitem>
+ <treerow>
+ <treecell label="Bob"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+
+</tree>
+
+</window>
diff --git a/layout/xul/crashtests/366112-1.xhtml b/layout/xul/crashtests/366112-1.xhtml
new file mode 100644
index 0000000000..ff95a722f3
--- /dev/null
+++ b/layout/xul/crashtests/366112-1.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <nativescrollbar />
+
+</window>
diff --git a/layout/xul/crashtests/366203-1.xhtml b/layout/xul/crashtests/366203-1.xhtml
new file mode 100644
index 0000000000..5d97782ea3
--- /dev/null
+++ b/layout/xul/crashtests/366203-1.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="setTimeout(boom, 500);">
+
+<script>
+function boom()
+{
+ tc1 = document.getElementById("tc1");
+ tc1.parentNode.removeChild(tc1);
+}
+</script>
+
+<tree rows="6">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true" style="-moz-box-flex: 3"/>
+ <treecol id="lastname" label="Last Name" style="-moz-box-flex: 7"/>
+ </treecols>
+
+ <treechildren id="tc1">
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Foo"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+
+ <treechildren>
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Bar"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+
+</window>
+
diff --git a/layout/xul/crashtests/367185-1.xhtml b/layout/xul/crashtests/367185-1.xhtml
new file mode 100644
index 0000000000..08fd39fa11
--- /dev/null
+++ b/layout/xul/crashtests/367185-1.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<title>Testcase bug - ASSERTION: shouldn't use unconstrained widths anymore with nested marquees</title>
+</head>
+<body>
+<xul:hbox style="margin: 0 100%;"><span><xul:hbox style="margin: 0 100%;"></xul:hbox></span></xul:hbox>
+</body>
+</html>
diff --git a/layout/xul/crashtests/369942-1.xhtml b/layout/xul/crashtests/369942-1.xhtml
new file mode 100644
index 0000000000..a05705843d
--- /dev/null
+++ b/layout/xul/crashtests/369942-1.xhtml
@@ -0,0 +1,36 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="reftest-wait">
+<head>
+
+<script>
+function boom()
+{
+ var span = document.getElementById("span");
+ var radio = document.getElementById("radio");
+
+ radio.appendChild(span);
+
+ document.documentElement.removeAttribute("class");
+}
+</script>
+
+
+<style>
+body {
+ text-align: center;
+ font-size: 9px;
+}
+</style>
+
+</head>
+
+
+<body onload="setTimeout(boom, 30);">
+
+<span id="span"><xul:wizard/><div>Industries</div></span>
+
+<xul:radio id="radio"/>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/376137-1.html b/layout/xul/crashtests/376137-1.html
new file mode 100644
index 0000000000..33d706f9c1
--- /dev/null
+++ b/layout/xul/crashtests/376137-1.html
@@ -0,0 +1,18 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<style>
+span { display:block; outline: 10px solid yellow; }
+</style>
+</head>
+
+<body>
+
+<div>
+ <div style="display: -moz-inline-box">
+ <span>M</span>
+ <span>N</span>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/376137-2.html b/layout/xul/crashtests/376137-2.html
new file mode 100644
index 0000000000..d3abb2d838
--- /dev/null
+++ b/layout/xul/crashtests/376137-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Bug 376137</title>
+<style>
+p { width: 100%; border: solid 1px;}
+</style>
+
+<div style="display: -moz-inline-box">
+ <div><p>M</p></div>
+ <div><p>N</p></div>
+</div>
+
diff --git a/layout/xul/crashtests/378961.html b/layout/xul/crashtests/378961.html
new file mode 100644
index 0000000000..42e9a64bd8
--- /dev/null
+++ b/layout/xul/crashtests/378961.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Testcase bug 378961 - Crash [@ nsSplitterFrameInner::RemoveListener] when dragging splitter and DOMAttrModified event removing window</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3C%3Fxml-stylesheet%20href%3D%22chrome%3A//global/skin%22%20type%3D%22text/css%22%3F%3E%0A%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%20orient%3D%22horizontal%22%3E%0A%3Ctextbox/%3E%3Csplitter/%3E%3Cbox/%3E%0A%0A%3Cscript%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0Afunction%20doe%28%29%20%7B%0Awindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%0A%7D%0Adocument.addEventListener%28%27DOMAttrModified%27%2C%20doe%2C%20true%29%3B%0A%3C/script%3E%0A%3C/window%3E" style="width: 500px;height:200px;"></iframe>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/381862.html b/layout/xul/crashtests/381862.html
new file mode 100644
index 0000000000..65721d1a3f
--- /dev/null
+++ b/layout/xul/crashtests/381862.html
@@ -0,0 +1,23 @@
+<html><head>
+<title>Testcase bug - Crash [@ nsBoxFrame::BuildDisplayListForChildren] with tree stuff in iframe toggling display</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%3E%0A%20%20%3Ctree%20style%3D%22display%3A%20block%3B%20position%3A%20absolute%3B%22%3E%0A%20%20%20%20%3Ctree%20style%3D%22display%3A%20table%3B%22%3E%0A%20%20%20%20%20%20%3Ctreeseparator%20style%3D%22display%3A%20block%3B%20position%3A%20absolute%3B%22%3E%0A%20%20%20%20%20%20%20%20%3Ctreechildren%20style%3D%22display%3A%20block%3B%22/%3E%0A%20%20%20%20%20%20%3C/treeseparator%3E%0A%20%20%20%20%20%20%3Ctreechildren%20style%3D%22display%3A%20none%3B%22/%3E%0A%20%20%20%20%3C/tree%3E%0A%20%20%3C/tree%3E%0A%3C/window%3E" id="content"></iframe>
+
+<script>
+function toggleIframe(){
+var x=document.getElementById('content');
+x.style.display = x.style.display == 'none' ? x.style.display = 'block' : x.style.display = 'none';
+setTimeout(toggleIframe,200);
+}
+setTimeout(toggleIframe,500);
+
+function removestyles(i){
+window.frames[0].document.getElementsByTagName('*')[1].removeAttribute('style');
+}
+
+setTimeout(removestyles,500,1);
+/*template*/
+</script>
+</body>
+</html>
diff --git a/layout/xul/crashtests/382746-1.xhtml b/layout/xul/crashtests/382746-1.xhtml
new file mode 100644
index 0000000000..c76a1531cd
--- /dev/null
+++ b/layout/xul/crashtests/382746-1.xhtml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<grid>
+ <rows>
+ <column>
+ <hbox/>
+ <hbox/>
+ </column>
+ <hbox/>
+ </rows>
+</grid>
+
+</window>
diff --git a/layout/xul/crashtests/382899-1.xhtml b/layout/xul/crashtests/382899-1.xhtml
new file mode 100644
index 0000000000..4b48eac240
--- /dev/null
+++ b/layout/xul/crashtests/382899-1.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<hbox equalsize="always"><grid/>x</hbox>
+
+</window>
diff --git a/layout/xul/crashtests/384037-1.xhtml b/layout/xul/crashtests/384037-1.xhtml
new file mode 100644
index 0000000000..04bac671cc
--- /dev/null
+++ b/layout/xul/crashtests/384037-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+
+<xul:splitter id="s" collapse="both" state="collapsed" />
+
+</body>
+</html>
+
diff --git a/layout/xul/crashtests/384105-1-inner.xhtml b/layout/xul/crashtests/384105-1-inner.xhtml
new file mode 100644
index 0000000000..ea9c0be8ad
--- /dev/null
+++ b/layout/xul/crashtests/384105-1-inner.xhtml
@@ -0,0 +1,21 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script id="script" xmlns="http://www.w3.org/1999/xhtml">
+function doe(){
+document.getElementById('a').removeAttribute('style');
+}
+setTimeout(doe,100);
+</script>
+<box id="a" style="position: absolute; display: block;">
+ <menuitem sizetopopup="always">
+ <menupopup style="position: absolute; display: block;"/>
+ </menuitem>
+
+ <box style="position: fixed; display: block;">
+ <tree>
+ <treecol>
+ <treecol/>
+ </treecol>
+ </tree>
+ </box>
+</box>
+</window>
diff --git a/layout/xul/crashtests/384105-1.html b/layout/xul/crashtests/384105-1.html
new file mode 100644
index 0000000000..8161342ec8
--- /dev/null
+++ b/layout/xul/crashtests/384105-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="384105-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/384373-1.xhtml b/layout/xul/crashtests/384373-1.xhtml
new file mode 100644
index 0000000000..603b53cdea
--- /dev/null
+++ b/layout/xul/crashtests/384373-1.xhtml
@@ -0,0 +1,10 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+onerror="var x=document.getElementsByTagName('*');x[Math.floor(Math.random()*x.length)].focus()"
+onblur="event.originalTarget.parentNode.parentNode.removeChild(event.originalTarget.parentNode)">
+<script xmlns="http://www.w3.org/1999/xhtml">setTimeout(function() {window.location.reload()}, 200);</script>
+
+<broadcasterset style="display: block;">
+ <broadcaster style="display: block;"></broadcaster>
+</broadcasterset>
+<preferences></preferences>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/384373-2.xhtml b/layout/xul/crashtests/384373-2.xhtml
new file mode 100644
index 0000000000..1d56394e31
--- /dev/null
+++ b/layout/xul/crashtests/384373-2.xhtml
@@ -0,0 +1,4 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onerror="document.getElementsByTagName('*')[1].focus()" onfocus="event.target.parentNode.removeChild(event.target)">
+<broadcaster style="display: block;"/>
+<preferences/>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/384373.html b/layout/xul/crashtests/384373.html
new file mode 100644
index 0000000000..a3658b86f8
--- /dev/null
+++ b/layout/xul/crashtests/384373.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 384373</title>
+<script>
+function reload() {
+ this.location.reload();
+}
+// Run the test for 1 second
+setTimeout(function() {
+ document.body.getBoundingClientRect();
+ document.documentElement.removeChild(document.body);
+ document.documentElement.className = "";
+ }, 2000);
+</script>
+</head>
+<body onload="document.body.getBoundingClientRect()">
+
+<iframe src="384373-1.xhtml"></iframe>
+<iframe onload="this.contentWindow.setTimeout(reload,500)" src="384373-2.xhtml"></iframe>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/384871-1-inner.xhtml b/layout/xul/crashtests/384871-1-inner.xhtml
new file mode 100644
index 0000000000..62efdb2608
--- /dev/null
+++ b/layout/xul/crashtests/384871-1-inner.xhtml
@@ -0,0 +1,9 @@
+<popup xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script xmlns="http://www.w3.org/1999/xhtml">
+function doe(){
+document.documentElement.autoPosition = 'on';
+window.location.reload();
+}
+setTimeout(doe, 300);
+</script>
+</popup> \ No newline at end of file
diff --git a/layout/xul/crashtests/384871-1.html b/layout/xul/crashtests/384871-1.html
new file mode 100644
index 0000000000..bcd9f98bc8
--- /dev/null
+++ b/layout/xul/crashtests/384871-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 500);
+</script>
+<body>
+<iframe src="384871-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/386642.xhtml b/layout/xul/crashtests/386642.xhtml
new file mode 100644
index 0000000000..50db21a095
--- /dev/null
+++ b/layout/xul/crashtests/386642.xhtml
@@ -0,0 +1,31 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 386642 Crash [@ IsCanvasFrame] while opening context menu or changing styles">
+<toolbarbutton type="menu" id="a">
+<menupopup id="b"/>
+</toolbarbutton>
+
+<style xmlns="http://www.w3.org/1999/xhtml">
+.one image {
+display: -moz-box;
+}
+image{
+display: none;
+}
+
+</style>
+<script><![CDATA[
+var gg=0;
+function doe() {
+ var a = document.getElementById('a');
+ if (!a.hasAttribute('class')) {
+ a.setAttribute('class', 'one');
+ } else {
+ a.removeAttribute('class');
+ }
+document.getElementById('b').hidePopup();
+}
+
+doe();
+setInterval(doe, 200);
+]]></script>
+</window>
diff --git a/layout/xul/crashtests/387080-1.xhtml b/layout/xul/crashtests/387080-1.xhtml
new file mode 100644
index 0000000000..4eb9bd784b
--- /dev/null
+++ b/layout/xul/crashtests/387080-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <description>
+ <foo height="1793689537164611773" width="20000238421986669650" />
+ </description>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/391974-1-inner.xhtml b/layout/xul/crashtests/391974-1-inner.xhtml
new file mode 100644
index 0000000000..f13aa2110f
--- /dev/null
+++ b/layout/xul/crashtests/391974-1-inner.xhtml
@@ -0,0 +1,19 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<menuitem>
+<tooltip/>
+<box/>
+</menuitem>
+
+<script xmlns="http://www.w3.org/1999/xhtml">
+function doe2() {
+document.getElementsByTagName('menuitem')[0].setAttribute('description', 'tetx');
+}
+
+function doe3() {
+document.getElementsByTagName('menuitem')[0].removeAttribute('description');
+document.getElementsByTagName('tooltip')[0].setAttribute('ordinal', '0');
+}
+setTimeout(doe2,150);
+setTimeout(doe3,200);
+</script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/391974-1.html b/layout/xul/crashtests/391974-1.html
new file mode 100644
index 0000000000..6946d66182
--- /dev/null
+++ b/layout/xul/crashtests/391974-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="391974-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/402912-1.xhtml b/layout/xul/crashtests/402912-1.xhtml
new file mode 100644
index 0000000000..b2cb98dc5a
--- /dev/null
+++ b/layout/xul/crashtests/402912-1.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+<xul:vbox equalsize="always"><xul:hbox flex="1"><span><xul:hbox width="10" height="10"/></span><xul:button /></xul:hbox><xul:hbox maxheight="0"/></xul:vbox>
+</body>
+</html>
diff --git a/layout/xul/crashtests/404192.xhtml b/layout/xul/crashtests/404192.xhtml
new file mode 100644
index 0000000000..4ad5af348b
--- /dev/null
+++ b/layout/xul/crashtests/404192.xhtml
@@ -0,0 +1,12 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+<xul:titlebar id="a" style="overflow: auto;"/>
+
+<script>
+function doe() {
+document.getElementsByTagName('*')[1].focus();
+document.getElementsByTagName('*')[0].focus();
+document.documentElement.removeAttribute("class");
+}
+setTimeout(doe, 200);
+</script>
+</html>
diff --git a/layout/xul/crashtests/408904-1.xhtml b/layout/xul/crashtests/408904-1.xhtml
new file mode 100644
index 0000000000..59f215c73b
--- /dev/null
+++ b/layout/xul/crashtests/408904-1.xhtml
@@ -0,0 +1 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><grid><rows><label/></rows><columns><column><label/></column></columns></grid></window>
diff --git a/layout/xul/crashtests/412479-1.xhtml b/layout/xul/crashtests/412479-1.xhtml
new file mode 100644
index 0000000000..b1086a816e
--- /dev/null
+++ b/layout/xul/crashtests/412479-1.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head></head>
+<body><xul:menubar style="display: table-column; padding: 10px 3000px;"/></body>
+</html>
diff --git a/layout/xul/crashtests/417509.xhtml b/layout/xul/crashtests/417509.xhtml
new file mode 100644
index 0000000000..81703ada37
--- /dev/null
+++ b/layout/xul/crashtests/417509.xhtml
@@ -0,0 +1,7 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<span id="a" datasources="" xmlns="http://www.w3.org/1999/xhtml"/>
+<script xmlns="http://www.w3.org/1999/xhtml">
+document.documentElement.appendChild(document.getElementById('a'));
+
+</script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/430356-1.xhtml b/layout/xul/crashtests/430356-1.xhtml
new file mode 100644
index 0000000000..8e7858904f
--- /dev/null
+++ b/layout/xul/crashtests/430356-1.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body style="visibility: collapse;">
+<tabpanels xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" style="width: max-content;"></tabpanels>
+</body>
+</html>
diff --git a/layout/xul/crashtests/464407-1.xhtml b/layout/xul/crashtests/464407-1.xhtml
new file mode 100644
index 0000000000..83666a6a46
--- /dev/null
+++ b/layout/xul/crashtests/464407-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+</head>
+<body>
+
+<xul:radio style="overflow: auto; height: 72057594037927940pt; display: table-cell;"/>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/470063-1.html b/layout/xul/crashtests/470063-1.html
new file mode 100644
index 0000000000..11c01b30e4
--- /dev/null
+++ b/layout/xul/crashtests/470063-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.removeChild(document.documentElement)
+ document.appendChild(document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "hbox"));
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/layout/xul/crashtests/470272.html b/layout/xul/crashtests/470272.html
new file mode 100644
index 0000000000..5caf12d636
--- /dev/null
+++ b/layout/xul/crashtests/470272.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+function doe2(i) {
+document.documentElement.offsetHeight;
+document.getElementById('a').setAttribute('style', 'display: -moz-inline-box;');
+document.documentElement.offsetHeight;
+}
+</script>
+</head>
+<body style="float: right; column-count: 2; height: 20%;" onload="setTimeout(doe2,0);">
+ <div style="display: none;"></div>
+ <ul style="display: -moz-inline-box;"></ul>
+ <span id="a">
+ <ul style="display: -moz-box; overflow: scroll;"></ul>
+ <span style="display: -moz-inline-box; height: 10px;">
+ <span style="position: absolute;"></span>
+ </span>
+ </span>
+</body>
+</html>
diff --git a/layout/xul/crashtests/538308-1.xhtml b/layout/xul/crashtests/538308-1.xhtml
new file mode 100644
index 0000000000..477c725ed1
--- /dev/null
+++ b/layout/xul/crashtests/538308-1.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="run()">
+
+ <tree id="tr" flex="1">
+ <treecols>
+ <treecol/>
+ </treecols>
+ <treechildren>
+ <html:optgroup id="group">
+ <html:option id="victim" label="never see this"/>
+ </html:optgroup>
+ </treechildren>
+ </tree>
+
+ <script type="text/javascript"><![CDATA[
+ function run() {
+ group = document.getElementById("group");
+ tc = document.createXULElement("treechildren");
+ group.appendChild(tc);
+
+ v = document.getElementById("victim");
+ v.remove();
+ v = null;
+
+ tree = document.getElementById("tr");
+ col = tree.columns[0];
+ alert(tree.view.getItemAtIndex(1, col));
+ }
+ ]]></script>
+</window>
diff --git a/layout/xul/crashtests/557174-1.xml b/layout/xul/crashtests/557174-1.xml
new file mode 100644
index 0000000000..02850a2db9
--- /dev/null
+++ b/layout/xul/crashtests/557174-1.xml
@@ -0,0 +1 @@
+<ther:window xmlns:ther="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" a="" e=""><HTML><ther:statusbar l="" c=""><ther:menulist d=""><ther:menu t="" i="" l=""><mat:h xmlns:mat="http://www.w3.org/1998/Math/MathML" w=""/></ther:menu><ther:menupopup p=""/><ther:menu a="" t="" l=""><ther:menuseparator u="" x=""><xht:html xmlns:xht="http://www.w3.org/1999/xhtml" x=""><xht:body d=""><xht:abbr d=""><xht:abbr p=""><xht:small s=""><xht:a s=""><xht:var e=""><xht:samp e=""><xht:code p=""><xht:b e=""><xht:b d=""><xht:del t=""><xht:h4 r=""><xht:var l=""><xht:i r=""><xht:em r=""><xht:em n=""><xht:map g=""><xht:isindex d=""/></xht:map></xht:em></xht:em></xht:i></xht:var></xht:h4></xht:del></xht:b></xht:b></xht:code></xht:samp></xht:var></xht:a></xht:small></xht:abbr></xht:abbr></xht:body></xht:html></ther:menuseparator></ther:menu></ther:menulist></ther:statusbar></HTML></ther:window> \ No newline at end of file
diff --git a/layout/xul/crashtests/564705-1.xhtml b/layout/xul/crashtests/564705-1.xhtml
new file mode 100644
index 0000000000..b0f29bef7a
--- /dev/null
+++ b/layout/xul/crashtests/564705-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><label value="&#x2026;" accesskey="b"></label></window>
+
diff --git a/layout/xul/crashtests/583957-1.html b/layout/xul/crashtests/583957-1.html
new file mode 100644
index 0000000000..48d29fc1c6
--- /dev/null
+++ b/layout/xul/crashtests/583957-1.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ window.addEventListener("DOMSubtreeModified", function(){});
+
+ var m = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
+ document.body.appendChild(m);
+ m.setAttribute("type", "checkbox");
+ m.setAttribute("checked", "true");
+ m.removeAttribute("type");
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/layout/xul/crashtests/617089.html b/layout/xul/crashtests/617089.html
new file mode 100644
index 0000000000..22e5f6d535
--- /dev/null
+++ b/layout/xul/crashtests/617089.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="display: -moz-inline-box;">
+ <table style="height: 101%;"><tbody><tr><td><div></div></td></tr></tbody></table>
+ <table style="height: 101%;"><tbody><tr><td><div></div></td></tr></tbody></table>
+ </div>
+ </body>
+</html>
diff --git a/layout/xul/crashtests/716503.html b/layout/xul/crashtests/716503.html
new file mode 100644
index 0000000000..250ad2ba40
--- /dev/null
+++ b/layout/xul/crashtests/716503.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+div::before {
+ content: "j";
+ display:-moz-inline-box;
+}
+</style>
+<body><div></div></body>
+</html>
diff --git a/layout/xul/crashtests/crashtests.list b/layout/xul/crashtests/crashtests.list
new file mode 100644
index 0000000000..3154435451
--- /dev/null
+++ b/layout/xul/crashtests/crashtests.list
@@ -0,0 +1,52 @@
+load chrome://reftest/content/crashtests/layout/xul/crashtests/131008-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/137216-1.xhtml
+load 140218-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/151826-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/168724-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/289410-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-3.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/294371-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/322786-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/325377.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/326879-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/329327-1.xhtml
+load 329407-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/336962-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/344228-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/365151.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/366112-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/366203-1.xhtml
+load 367185-1.xhtml
+load 369942-1.xhtml
+load 376137-1.html
+load 376137-2.html
+load 378961.html
+load 381862.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/382746-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/382899-1.xhtml
+load 384037-1.xhtml
+load 384105-1.html
+load 384373.html
+load 384871-1.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/386642.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/387080-1.xhtml
+load 391974-1.html
+load 402912-1.xhtml
+load 404192.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/408904-1.xhtml
+load 412479-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/417509.xhtml
+load 430356-1.xhtml
+asserts(0-1) load 464407-1.xhtml # Bugs 450974, 1267054, 718883
+load 470063-1.html
+load 470272.html
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/538308-1.xhtml
+load 557174-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/564705-1.xhtml
+load 583957-1.html
+load 617089.html
+load menulist-focused.xhtml
+load 716503.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/1379332-2.xhtml
diff --git a/layout/xul/crashtests/menulist-focused.xhtml b/layout/xul/crashtests/menulist-focused.xhtml
new file mode 100644
index 0000000000..7a09a838d7
--- /dev/null
+++ b/layout/xul/crashtests/menulist-focused.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+<xul:menulist focused="true"/>
+</body>
+</html>
diff --git a/layout/xul/moz.build b/layout/xul/moz.build
new file mode 100644
index 0000000000..6942bff353
--- /dev/null
+++ b/layout/xul/moz.build
@@ -0,0 +1,50 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "XUL")
+
+if CONFIG["ENABLE_TESTS"]:
+ MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+ MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"]
+ BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+
+EXPORTS += [
+ "nsIScrollbarMediator.h",
+ "nsXULPopupManager.h",
+ "nsXULTooltipListener.h",
+]
+
+UNIFIED_SOURCES += [
+ "MiddleCroppingLabelFrame.cpp",
+ "nsMenuPopupFrame.cpp",
+ "nsRepeatService.cpp",
+ "nsScrollbarButtonFrame.cpp",
+ "nsScrollbarFrame.cpp",
+ "nsSliderFrame.cpp",
+ "nsSplitterFrame.cpp",
+ "nsXULPopupManager.cpp",
+ "nsXULTooltipListener.cpp",
+ "SimpleXULLeafFrame.cpp",
+]
+
+DIRS += ["tree"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "../base",
+ "../generic",
+ "../painting",
+ "../style",
+ "/dom/base",
+ "/dom/xul",
+]
diff --git a/layout/xul/nsIPopupContainer.h b/layout/xul/nsIPopupContainer.h
new file mode 100644
index 0000000000..4870863781
--- /dev/null
+++ b/layout/xul/nsIPopupContainer.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIPopupContainer_h___
+#define nsIPopupContainer_h___
+
+#include "nsQueryFrame.h"
+class nsIContent;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+class nsIPopupContainer {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsIPopupContainer)
+
+ virtual mozilla::dom::Element* GetDefaultTooltip() = 0;
+
+ static nsIPopupContainer* GetPopupContainer(mozilla::PresShell* aShell);
+};
+
+#endif
diff --git a/layout/xul/nsIScrollbarMediator.h b/layout/xul/nsIScrollbarMediator.h
new file mode 100644
index 0000000000..ce26289fd9
--- /dev/null
+++ b/layout/xul/nsIScrollbarMediator.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIScrollbarMediator_h___
+#define nsIScrollbarMediator_h___
+
+#include "mozilla/ScrollTypes.h"
+#include "nsQueryFrame.h"
+#include "nsCoord.h"
+
+class nsScrollbarFrame;
+class nsIFrame;
+
+class nsIScrollbarMediator : public nsQueryFrame {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsIScrollbarMediator)
+
+ /**
+ * The aScrollbar argument denotes the scrollbar that's firing the
+ * notification. aScrollbar is never null. aDirection is either -1, 0, or 1.
+ */
+
+ /**
+ * When set to ENABLE_SNAP, additional scrolling will be performed after the
+ * scroll operation to maintain the constraints set by CSS Scroll snapping.
+ * The additional scrolling may include asynchronous smooth scrolls that
+ * continue to animate after the initial scroll position has been set.
+ * In case of DEFAULT, it means ENABLE_SNAP for CSS scroll snap v1,
+ * DISABLE_SNAP for the old scroll snap.
+ */
+
+ /**
+ * One of the following three methods is called when the scrollbar's button is
+ * clicked.
+ * @note These methods might destroy the frame, pres shell, and other objects.
+ */
+ virtual void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+ virtual void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+ virtual void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+
+ // Only implemented for nsGfxScrollFrame, not nsTreeBodyFrame.
+ virtual void ScrollByUnit(nsScrollbarFrame* aScrollbar,
+ mozilla::ScrollMode aMode, int32_t aDirection,
+ mozilla::ScrollUnit aUnit,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+
+ /**
+ * RepeatButtonScroll is called when the scrollbar's button is held down. When
+ * the button is first clicked the increment is set; RepeatButtonScroll adds
+ * this increment to the current position.
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ virtual void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) = 0;
+ /**
+ * aOldPos and aNewPos are scroll positions.
+ * The scroll positions start with zero at the left edge; implementors that
+ * want zero at the right edge for RTL content will need to adjust
+ * accordingly. (See nsHTMLScrollFrame::ThumbMoved in nsGfxScrollFrame.cpp.)
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ virtual void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) = 0;
+ /**
+ * Called when the scroll bar thumb, slider, or any other component is
+ * released.
+ */
+ virtual void ScrollbarReleased(nsScrollbarFrame* aScrollbar) = 0;
+ virtual void VisibilityChanged(bool aVisible) = 0;
+
+ /**
+ * Obtain the frame for the horizontal or vertical scrollbar, or null
+ * if there is no such box.
+ */
+ virtual nsScrollbarFrame* GetScrollbarBox(bool aVertical) = 0;
+ /**
+ * Show or hide scrollbars on 2 fingers touch.
+ * Subclasses should call their ScrollbarActivity's corresponding methods.
+ */
+ virtual void ScrollbarActivityStarted() const = 0;
+ virtual void ScrollbarActivityStopped() const = 0;
+
+ virtual bool IsScrollbarOnRight() const = 0;
+
+ /**
+ * Returns true if the mediator is asking the scrollbar to suppress
+ * repainting itself on changes.
+ */
+ virtual bool ShouldSuppressScrollbarRepaints() const = 0;
+};
+
+#endif
diff --git a/layout/xul/nsMenuPopupFrame.cpp b/layout/xul/nsMenuPopupFrame.cpp
new file mode 100644
index 0000000000..c82411285f
--- /dev/null
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -0,0 +1,2433 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMenuPopupFrame.h"
+#include "LayoutConstants.h"
+#include "XULButtonElement.h"
+#include "XULPopupElement.h"
+#include "mozilla/dom/XULPopupElement.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIFrameInlines.h"
+#include "nsAtom.h"
+#include "nsPresContext.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsCSSRendering.h"
+#include "nsNameSpaceManager.h"
+#include "nsIFrameInlines.h"
+#include "nsViewManager.h"
+#include "nsWidgetsCID.h"
+#include "nsPIDOMWindow.h"
+#include "nsFrameManager.h"
+#include "mozilla/dom/Document.h"
+#include "nsRect.h"
+#include "nsIScrollableFrame.h"
+#include "nsIPopupContainer.h"
+#include "nsIDocShell.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsContentUtils.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsPIWindowRoot.h"
+#include "nsIReflowCallback.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIBaseWindow.h"
+#include "nsISound.h"
+#include "nsIScreenManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStyleConsts.h"
+#include "nsStyleStructInlines.h"
+#include "nsTransitionManager.h"
+#include "nsDisplayList.h"
+#include "nsIDOMXULSelectCntrlEl.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include <algorithm>
+
+#include "X11UndefineNone.h"
+#include "nsXULPopupManager.h"
+
+using namespace mozilla;
+using mozilla::dom::Document;
+using mozilla::dom::Element;
+using mozilla::dom::Event;
+using mozilla::dom::XULButtonElement;
+
+int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1;
+
+TimeStamp nsMenuPopupFrame::sLastKeyTime;
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/WidgetUtilsGtk.h"
+# define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay()
+extern mozilla::LazyLogModule gWidgetPopupLog;
+# define LOG_WAYLAND(...) \
+ MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define IS_WAYLAND_DISPLAY() false
+# define LOG_WAYLAND(...)
+#endif
+
+// NS_NewMenuPopupFrame
+//
+// Wrapper for creating a new menu popup container
+//
+nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsMenuPopupFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
+
+NS_QUERYFRAME_HEAD(nsMenuPopupFrame)
+ NS_QUERYFRAME_ENTRY(nsMenuPopupFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
+
+//
+// nsMenuPopupFrame ctor
+//
+nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBlockFrame(aStyle, aPresContext, kClassID) {
+ // the preference name is backwards here. True means that the 'top' level is
+ // the default, and false means that the 'parent' level is the default.
+ if (sDefaultLevelIsTop >= 0) return;
+ sDefaultLevelIsTop =
+ Preferences::GetBool("ui.panel.default_level_parent", false);
+} // ctor
+
+nsMenuPopupFrame::~nsMenuPopupFrame() = default;
+
+static bool IsMouseTransparent(const ComputedStyle& aStyle) {
+ // If pointer-events: none; is set on the popup, then the widget should
+ // ignore mouse events, passing them through to the content behind.
+ return aStyle.PointerEvents() == StylePointerEvents::None;
+}
+
+static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle,
+ const nsPresContext& aPc) {
+ return {IsMouseTransparent(aStyle),
+ (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() *
+ aPc.CSSToDevPixelScale())
+ .Truncated()};
+}
+
+bool nsMenuPopupFrame::ShouldCreateWidgetUpfront() const {
+ if (mPopupType != PopupType::Menu) {
+ // Any panel with a type attribute, such as the autocomplete popup, is
+ // always generated right away.
+ return mContent->AsElement()->HasAttr(nsGkAtoms::type);
+ }
+
+ // Generate the widget up-front if the parent menu is a <menulist> unless its
+ // sizetopopup is set to "none".
+ return ShouldExpandToInflowParentOrAnchor();
+}
+
+void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
+
+ CreatePopupView();
+
+ // XXX Hack. The popup's view should float above all other views,
+ // so we use the nsView::SetFloating() to tell the view manager
+ // about that constraint.
+ nsView* ourView = GetView();
+ nsViewManager* viewManager = ourView->GetViewManager();
+ viewManager->SetViewFloating(ourView, true);
+
+ const auto& el = PopupElement();
+ mPopupType = PopupType::Panel;
+ if (el.IsMenu()) {
+ mPopupType = PopupType::Menu;
+ } else if (el.IsXULElement(nsGkAtoms::tooltip)) {
+ mPopupType = PopupType::Tooltip;
+ }
+
+ if (PresContext()->IsChrome()) {
+ mInContentShell = false;
+ }
+
+ // Support incontentshell=false attribute to allow popups to be displayed
+ // outside of the content shell. Chrome only.
+ if (el.NodePrincipal()->IsSystemPrincipal()) {
+ if (el.GetXULBoolAttr(nsGkAtoms::incontentshell)) {
+ mInContentShell = true;
+ } else if (el.AttrValueIs(kNameSpaceID_None, nsGkAtoms::incontentshell,
+ nsGkAtoms::_false, eCaseMatters)) {
+ mInContentShell = false;
+ }
+ }
+
+ // To improve performance, create the widget for the popup if needed. Popups
+ // such as menus will create their widgets later when the popup opens.
+ //
+ // FIXME(emilio): Doing this up-front for all menupopups causes a bunch of
+ // assertions, while it's supposed to be just an optimization.
+ if (!ourView->HasWidget() && ShouldCreateWidgetUpfront()) {
+ CreateWidgetForView(ourView);
+ }
+
+ AddStateBits(NS_FRAME_IN_POPUP);
+}
+
+bool nsMenuPopupFrame::HasRemoteContent() const {
+ return !mInContentShell && mPopupType == PopupType::Panel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::remote, nsGkAtoms::_true,
+ eIgnoreCase);
+}
+
+bool nsMenuPopupFrame::IsNoAutoHide() const {
+ // Panels with noautohide="true" don't hide when the mouse is clicked
+ // outside of them, or when another application is made active. Non-autohide
+ // panels cannot be used in content windows.
+ return !mInContentShell && mPopupType == PopupType::Panel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::noautohide,
+ nsGkAtoms::_true, eIgnoreCase);
+}
+
+widget::PopupLevel nsMenuPopupFrame::GetPopupLevel(bool aIsNoAutoHide) const {
+ // The popup level is determined as follows, in this order:
+ // 1. non-panels (menus and tooltips) are always topmost
+ // 2. any specified level attribute
+ // 3. if a titlebar attribute is set, use the 'floating' level
+ // 4. if this is a noautohide panel, use the 'parent' level
+ // 5. use the platform-specific default level
+
+ // If this is not a panel, this is always a top-most popup.
+ if (mPopupType != PopupType::Panel) {
+ return PopupLevel::Top;
+ }
+
+ // If the level attribute has been set, use that.
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::top, nsGkAtoms::parent, nsGkAtoms::floating, nullptr};
+ switch (mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) {
+ case 0:
+ return PopupLevel::Top;
+ case 1:
+ return PopupLevel::Parent;
+ case 2:
+ return PopupLevel::Floating;
+ }
+
+ // Panels with titlebars most likely want to be floating popups.
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::titlebar)) {
+ return PopupLevel::Floating;
+ }
+
+ // If this panel is a noautohide panel, the default is the parent level.
+ if (aIsNoAutoHide) {
+ return PopupLevel::Parent;
+ }
+
+ // Otherwise, the result depends on the platform.
+ return sDefaultLevelIsTop ? PopupLevel::Top : PopupLevel::Parent;
+}
+
+void nsMenuPopupFrame::PrepareWidget(bool aRecreate) {
+ nsView* ourView = GetView();
+ if (aRecreate) {
+ if (auto* widget = GetWidget()) {
+ // Widget's WebRender resources needs to be cleared before creating new
+ // widget.
+ widget->ClearCachedWebrenderResources();
+ }
+ ourView->DestroyWidget();
+ }
+ if (!ourView->HasWidget()) {
+ CreateWidgetForView(ourView);
+ } else {
+ PropagateStyleToWidget();
+ }
+}
+
+nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) {
+ // Create a widget for ourselves.
+ widget::InitData widgetData;
+ widgetData.mWindowType = widget::WindowType::Popup;
+ widgetData.mBorderStyle = widget::BorderStyle::Default;
+ widgetData.mForMenupopupFrame = true;
+ widgetData.mClipSiblings = true;
+ widgetData.mPopupHint = mPopupType;
+ widgetData.mNoAutoHide = IsNoAutoHide();
+
+ if (!mInContentShell) {
+ // A drag popup may be used for non-static translucent drag feedback
+ if (mPopupType == PopupType::Panel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::drag, eIgnoreCase)) {
+ widgetData.mIsDragPopup = true;
+ }
+ }
+
+ nsAutoString title;
+ if (widgetData.mNoAutoHide &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::titlebar,
+ nsGkAtoms::normal, eCaseMatters)) {
+ widgetData.mBorderStyle = widget::BorderStyle::Title;
+
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, title);
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::close,
+ nsGkAtoms::_true, eCaseMatters)) {
+ widgetData.mBorderStyle =
+ widgetData.mBorderStyle | widget::BorderStyle::Close;
+ }
+ }
+
+ bool remote = HasRemoteContent();
+
+ const auto mode = nsLayoutUtils::GetFrameTransparency(this, this);
+ widgetData.mHasRemoteContent = remote;
+ widgetData.mTransparencyMode = mode;
+ widgetData.mPopupLevel = GetPopupLevel(widgetData.mNoAutoHide);
+
+ // Panels which have a parent level need a parent widget. This allows them to
+ // always appear in front of the parent window but behind other windows that
+ // should be in front of it.
+ nsCOMPtr<nsIWidget> parentWidget;
+ if (widgetData.mPopupLevel != PopupLevel::Top) {
+ nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
+ if (!dsti) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ dsti->GetTreeOwner(getter_AddRefs(treeOwner));
+ if (!treeOwner) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(treeOwner));
+ if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget));
+ }
+
+ nsresult rv = aView->CreateWidgetForPopup(&widgetData, parentWidget);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsIWidget* widget = aView->GetWidget();
+ widget->SetTransparencyMode(mode);
+
+ PropagateStyleToWidget();
+
+ // most popups don't have a title so avoid setting the title if there isn't
+ // one
+ if (!title.IsEmpty()) {
+ widget->SetTitle(title);
+ }
+
+ return NS_OK;
+}
+
+void nsMenuPopupFrame::PropagateStyleToWidget(WidgetStyleFlags aFlags) const {
+ if (aFlags.isEmpty()) {
+ return;
+ }
+
+ nsIWidget* widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ if (aFlags.contains(WidgetStyle::ColorScheme)) {
+ widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this)));
+ }
+ if (aFlags.contains(WidgetStyle::InputRegion)) {
+ widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
+ }
+ if (aFlags.contains(WidgetStyle::Opacity)) {
+ widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity);
+ }
+ if (aFlags.contains(WidgetStyle::Shadow)) {
+ widget->SetWindowShadowStyle(GetShadowStyle());
+ }
+ if (aFlags.contains(WidgetStyle::Transform)) {
+ widget->SetWindowTransform(ComputeWidgetTransform());
+ }
+}
+
+bool nsMenuPopupFrame::IsMouseTransparent() const {
+ return ::IsMouseTransparent(*Style());
+}
+
+StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() const {
+ StyleWindowShadow shadow = StyleUIReset()->mWindowShadow;
+ if (shadow != StyleWindowShadow::Default) {
+ return shadow;
+ }
+
+ switch (StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::Tooltip:
+ return StyleWindowShadow::Tooltip;
+ case StyleAppearance::Menupopup:
+ return StyleWindowShadow::Menu;
+ default:
+ return StyleWindowShadow::Default;
+ }
+}
+
+void nsMenuPopupFrame::SetPopupState(nsPopupState aState) {
+ mPopupState = aState;
+
+ // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166
+ if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) {
+ if (nsIWidget* widget = GetWidget()) {
+ widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
+ }
+ }
+}
+
+// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() {
+ nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
+ // Set the state to visible if the popup is still open.
+ if (popup && popup->IsOpen()) {
+ popup->SetPopupState(ePopupShown);
+ }
+
+ if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) {
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(mPopup, "popup-shown", nullptr);
+ }
+ }
+ WidgetMouseEvent event(true, eXULPopupShown, nullptr,
+ WidgetMouseEvent::eReal);
+ return EventDispatcher::Dispatch(mPopup, mPresContext, &event);
+}
+
+NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) {
+ nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
+ // Ignore events not targeted at the popup itself (ie targeted at
+ // descendants):
+ if (mPopup != aEvent->GetTarget()) {
+ return NS_OK;
+ }
+ if (popup) {
+ // ResetPopupShownDispatcher will delete the reference to this, so keep
+ // another one until Run is finished.
+ RefPtr<nsXULPopupShownEvent> event = this;
+ // Only call Run if it the dispatcher was assigned. This avoids calling the
+ // Run method if the transitionend event fires multiple times.
+ if (popup->ClearPopupShownDispatcher()) {
+ return Run();
+ }
+ }
+
+ CancelListener();
+ return NS_OK;
+}
+
+void nsXULPopupShownEvent::CancelListener() {
+ mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable,
+ nsIDOMEventListener);
+
+void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
+ nsBlockFrame::DidSetComputedStyle(aOldStyle);
+
+ if (!aOldStyle) {
+ return;
+ }
+
+ WidgetStyleFlags flags;
+
+ if (aOldStyle->StyleUI()->mColorScheme != StyleUI()->mColorScheme) {
+ flags += WidgetStyle::ColorScheme;
+ }
+
+ auto& newUI = *StyleUIReset();
+ auto& oldUI = *aOldStyle->StyleUIReset();
+ if (newUI.mWindowOpacity != oldUI.mWindowOpacity) {
+ flags += WidgetStyle::Opacity;
+ }
+
+ if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) {
+ flags += WidgetStyle::Transform;
+ }
+
+ if (newUI.mWindowShadow != oldUI.mWindowShadow) {
+ flags += WidgetStyle::Shadow;
+ }
+
+ const auto& pc = *PresContext();
+ auto oldRegion = ComputeInputRegion(*aOldStyle, pc);
+ auto newRegion = ComputeInputRegion(*Style(), pc);
+ if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent ||
+ oldRegion.mMargin != newRegion.mMargin) {
+ flags += WidgetStyle::InputRegion;
+ }
+
+ PropagateStyleToWidget(flags);
+}
+
+void nsMenuPopupFrame::TweakMinPrefISize(nscoord& aSize) {
+ if (!ShouldExpandToInflowParentOrAnchor()) {
+ return;
+ }
+ // Make sure to accommodate for our scrollbar if needed. Do it only for
+ // menulists to match previous behavior.
+ //
+ // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be
+ // using scrollbar-gutter: stable on the scroller) isn't great, because even
+ // though we want a stable gutter, we want to draw on top of the gutter when
+ // there's no scrollbar, otherwise it looks rather weird.
+ //
+ // Automatically accommodating for the scrollbar otherwise would be bug
+ // 764076, but that has its own set of problems.
+ if (nsIScrollableFrame* sf = GetScrollFrame()) {
+ aSize += sf->GetDesiredScrollbarSizes().LeftRight();
+ }
+
+ nscoord menuListOrAnchorWidth = 0;
+ if (nsIFrame* menuList = GetInFlowParent()) {
+ menuListOrAnchorWidth = menuList->GetRect().width;
+ }
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ menuListOrAnchorWidth = std::max(menuListOrAnchorWidth, mScreenRect.width);
+ }
+ // Input margin doesn't have contents, so account for it for popup sizing
+ // purposes.
+ menuListOrAnchorWidth +=
+ 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
+ aSize = std::max(aSize, menuListOrAnchorWidth);
+}
+
+nscoord nsMenuPopupFrame::GetMinISize(gfxContext* aRC) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ result = nsBlockFrame::GetMinISize(aRC);
+ TweakMinPrefISize(result);
+ return result;
+}
+
+nscoord nsMenuPopupFrame::GetPrefISize(gfxContext* aRC) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ result = nsBlockFrame::GetPrefISize(aRC);
+ TweakMinPrefISize(result);
+ return result;
+}
+
+void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ const auto wm = GetWritingMode();
+ // Default to preserving our bounds.
+ aDesiredSize.SetSize(wm, GetLogicalSize(wm));
+
+ LayoutPopup(aPresContext, aDesiredSize, aReflowInput, aStatus);
+
+ aDesiredSize.SetBlockStartAscent(aDesiredSize.BSize(wm));
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+ FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay);
+}
+
+void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() {
+ if (!IsMenuList() || !IsOpen()) {
+ return;
+ }
+ nsIFrame* frame = GetCurrentMenuItemFrame();
+ if (!frame) {
+ return;
+ }
+ RefPtr<mozilla::PresShell> presShell = PresShell();
+ presShell->ScrollFrameIntoView(
+ frame, Nothing(), ScrollAxis(), ScrollAxis(),
+ ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly);
+}
+
+void nsMenuPopupFrame::LayoutPopup(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ if (IsNativeMenu()) {
+ return;
+ }
+
+ SchedulePaint();
+
+ const bool isOpen = IsOpen();
+ if (!isOpen) {
+ // If the popup is not open, only do layout while showing or if we're a
+ // menulist.
+ //
+ // This is needed because the SelectParent code wants to limit the height of
+ // the popup before opening it.
+ //
+ // TODO(emilio): We should consider adding a way to do that more reliably
+ // instead, but this preserves existing behavior.
+ const bool needsLayout = mPopupState == ePopupShowing ||
+ mPopupState == ePopupPositioning || IsMenuList();
+ if (!needsLayout) {
+ RemoveStateBits(NS_FRAME_FIRST_REFLOW);
+ return;
+ }
+ }
+
+ // Do a first reflow, with all our content, in order to find our preferred
+ // size. Then, we do a second reflow with the updated dimensions.
+ const bool needsPrefSize = mPrefSize == nsSize(-1, -1) || IsSubtreeDirty();
+ if (needsPrefSize) {
+ // Get the preferred, minimum and maximum size. If the menu is sized to the
+ // popup, then the popup's width is the menu's width.
+ ReflowOutput preferredSize(aReflowInput);
+ nsBlockFrame::Reflow(aPresContext, preferredSize, aReflowInput, aStatus);
+ mPrefSize = preferredSize.PhysicalSize();
+ }
+
+ // Get our desired position and final size, now that we have a preferred size.
+ auto constraints = GetRects(mPrefSize);
+ const auto finalSize = constraints.mUsedRect.Size();
+
+ // We need to do an extra reflow if we haven't reflowed, our size doesn't
+ // match with our final intended size, or our bsize is unconstrained (in which
+ // case we need to specify the final size so that percentages work).
+ const bool needDefiniteReflow =
+ aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || !needsPrefSize ||
+ finalSize != mPrefSize;
+
+ if (needDefiniteReflow) {
+ ReflowInput constrainedReflowInput(aReflowInput);
+ const auto& bp = aReflowInput.ComputedPhysicalBorderPadding();
+ // TODO: writing-mode handling not terribly correct, but it doesn't matter.
+ const nsSize finalContentSize(finalSize.width - bp.LeftRight(),
+ finalSize.height - bp.TopBottom());
+ constrainedReflowInput.SetComputedISize(finalContentSize.width);
+ constrainedReflowInput.SetComputedBSize(finalContentSize.height);
+ constrainedReflowInput.SetIResize(finalSize.width != mPrefSize.width);
+ constrainedReflowInput.SetBResize([&] {
+ if (finalSize.height != mPrefSize.height) {
+ return true;
+ }
+ if (needsPrefSize &&
+ aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE &&
+ aReflowInput.ComputedMaxBSize() == finalContentSize.height) {
+ // If we have measured, and maybe clamped our children via max-height,
+ // they might need to get percentages in the block axis re-resolved.
+ return true;
+ }
+ return false;
+ }());
+
+ aStatus.Reset();
+ nsBlockFrame::Reflow(aPresContext, aDesiredSize, constrainedReflowInput,
+ aStatus);
+ }
+
+ // Set our size, since nsAbsoluteContainingBlock won't.
+ SetRect(constraints.mUsedRect);
+
+ nsView* view = GetView();
+ if (isOpen) {
+ nsViewManager* viewManager = view->GetViewManager();
+ viewManager->ResizeView(view,
+ nsRect(nsPoint(), constraints.mUsedRect.Size()));
+ if (mPopupState == ePopupOpening) {
+ mPopupState = ePopupVisible;
+ }
+
+ viewManager->SetViewVisibility(view, ViewVisibility::Show);
+ SyncFrameViewProperties(view);
+ }
+
+ // Perform our move now. That will position the view and so on.
+ PerformMove(constraints);
+
+ // finally, if the popup just opened, send a popupshown event
+ bool openChanged = mIsOpenChanged;
+ if (openChanged) {
+ mIsOpenChanged = false;
+
+ // Make sure the current selection in a menulist is visible.
+ EnsureActiveMenuListItemIsVisible();
+
+ // If the animate attribute is set to open, check for a transition and wait
+ // for it to finish before firing the popupshown event.
+ if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::animate, nsGkAtoms::open,
+ eCaseMatters) &&
+ AnimationUtils::HasCurrentTransitions(mContent->AsElement(),
+ PseudoStyleType::NotPseudo)) {
+ mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, aPresContext);
+ mContent->AddSystemEventListener(u"transitionend"_ns,
+ mPopupShownDispatcher, false, false);
+ return;
+ }
+
+ // If there are no transitions, fire the popupshown event right away.
+ nsCOMPtr<nsIRunnable> event =
+ new nsXULPopupShownEvent(GetContent(), aPresContext);
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ }
+}
+
+bool nsMenuPopupFrame::IsMenuList() const {
+ return PopupElement().IsInMenuList();
+}
+
+bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const {
+ return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::sizetopopup,
+ nsGkAtoms::none, eCaseMatters);
+}
+
+nsIContent* nsMenuPopupFrame::GetTriggerContent(
+ nsMenuPopupFrame* aMenuPopupFrame) {
+ while (aMenuPopupFrame) {
+ if (aMenuPopupFrame->mTriggerContent) {
+ return aMenuPopupFrame->mTriggerContent;
+ }
+
+ auto* button = XULButtonElement::FromNodeOrNull(
+ aMenuPopupFrame->GetContent()->GetParent());
+ if (!button || !button->IsMenu()) {
+ break;
+ }
+
+ auto* popup = button->GetContainingPopupElement();
+ if (!popup) {
+ break;
+ }
+
+ // check up the menu hierarchy until a popup with a trigger node is found
+ aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ }
+
+ return nullptr;
+}
+
+void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor,
+ const nsAString& aAlign) {
+ mTriggerContent = nullptr;
+
+ if (aAnchor.EqualsLiteral("topleft"))
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ else if (aAnchor.EqualsLiteral("topright"))
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ else if (aAnchor.EqualsLiteral("bottomleft"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ else if (aAnchor.EqualsLiteral("bottomright"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ else if (aAnchor.EqualsLiteral("leftcenter"))
+ mPopupAnchor = POPUPALIGNMENT_LEFTCENTER;
+ else if (aAnchor.EqualsLiteral("rightcenter"))
+ mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER;
+ else if (aAnchor.EqualsLiteral("topcenter"))
+ mPopupAnchor = POPUPALIGNMENT_TOPCENTER;
+ else if (aAnchor.EqualsLiteral("bottomcenter"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER;
+ else
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+
+ if (aAlign.EqualsLiteral("topleft"))
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ else if (aAlign.EqualsLiteral("topright"))
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ else if (aAlign.EqualsLiteral("bottomleft"))
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ else if (aAlign.EqualsLiteral("bottomright"))
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ else
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+
+ mPosition = POPUPPOSITION_UNKNOWN;
+}
+
+void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
+ nsIContent* aTriggerContent,
+ const nsAString& aPosition,
+ int32_t aXPos, int32_t aYPos,
+ MenuPopupAnchorType aAnchorType,
+ bool aAttributesOverride) {
+ auto* widget = GetWidget();
+ bool recreateWidget = widget && widget->NeedsRecreateToReshow();
+ PrepareWidget(recreateWidget);
+
+ mPopupState = ePopupShowing;
+ mAnchorContent = aAnchorContent;
+ mTriggerContent = aTriggerContent;
+ mXPos = aXPos;
+ mYPos = aYPos;
+ mIsNativeMenu = false;
+ mIsTopLevelContextMenu = false;
+ mVFlip = false;
+ mHFlip = false;
+ mAlignmentOffset = 0;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+
+ mAnchorType = aAnchorType;
+
+ // if aAttributesOverride is true, then the popupanchor, popupalign and
+ // position attributes on the <menupopup> override those values passed in.
+ // If false, those attributes are only used if the values passed in are empty
+ if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
+ nsAutoString anchor, align, position, flip;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor,
+ anchor);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign,
+ align);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::position,
+ position);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::flip, flip);
+
+ if (aAttributesOverride) {
+ // if the attributes are set, clear the offset position. Otherwise,
+ // the offset is used to adjust the position from the anchor point
+ if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty())
+ position.Assign(aPosition);
+ else
+ mXPos = mYPos = 0;
+ } else if (!aPosition.IsEmpty()) {
+ position.Assign(aPosition);
+ }
+
+ if (flip.EqualsLiteral("none")) {
+ mFlip = FlipType_None;
+ } else if (flip.EqualsLiteral("both")) {
+ mFlip = FlipType_Both;
+ } else if (flip.EqualsLiteral("slide")) {
+ mFlip = FlipType_Slide;
+ }
+
+ position.CompressWhitespace();
+ int32_t spaceIdx = position.FindChar(' ');
+ // if there is a space in the position, assume it is the anchor and
+ // alignment as two separate tokens.
+ if (spaceIdx >= 0) {
+ InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx),
+ Substring(position, spaceIdx + 1));
+ } else if (position.EqualsLiteral("before_start")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ mPosition = POPUPPOSITION_BEFORESTART;
+ } else if (position.EqualsLiteral("before_end")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPosition = POPUPPOSITION_BEFOREEND;
+ } else if (position.EqualsLiteral("after_start")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_AFTERSTART;
+ } else if (position.EqualsLiteral("after_end")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ mPosition = POPUPPOSITION_AFTEREND;
+ } else if (position.EqualsLiteral("start_before")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ mPosition = POPUPPOSITION_STARTBEFORE;
+ } else if (position.EqualsLiteral("start_after")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPosition = POPUPPOSITION_STARTAFTER;
+ } else if (position.EqualsLiteral("end_before")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_ENDBEFORE;
+ } else if (position.EqualsLiteral("end_after")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ mPosition = POPUPPOSITION_ENDAFTER;
+ } else if (position.EqualsLiteral("overlap")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_OVERLAP;
+ } else if (position.EqualsLiteral("after_pointer")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_AFTERPOINTER;
+ // XXXndeakin this is supposed to anchor vertically after, but with the
+ // horizontal position as the mouse pointer.
+ mYPos += 21;
+ } else if (position.EqualsLiteral("selection")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_SELECTION;
+ } else {
+ InitPositionFromAnchorAlign(anchor, align);
+ }
+ }
+ // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in
+ // nsXULPopupManager::Rollup
+ mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0);
+
+ if (aAttributesOverride) {
+ // Use |left| and |top| dimension attributes to position the popup if
+ // present, as they may have been persisted.
+ nsAutoString left, top;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
+
+ nsresult err;
+ if (!left.IsEmpty()) {
+ int32_t x = left.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) {
+ mScreenRect.x = CSSPixel::ToAppUnits(x);
+ }
+ }
+ if (!top.IsEmpty()) {
+ int32_t y = top.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) {
+ mScreenRect.y = CSSPixel::ToAppUnits(y);
+ }
+ }
+ }
+}
+
+void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
+ int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu) {
+ auto* widget = GetWidget();
+ bool recreateWidget = widget && widget->NeedsRecreateToReshow();
+ PrepareWidget(recreateWidget);
+
+ mPopupState = ePopupShowing;
+ mAnchorContent = nullptr;
+ mTriggerContent = aTriggerContent;
+ mScreenRect =
+ nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
+ mXPos = 0;
+ mYPos = 0;
+ mFlip = FlipType_Default;
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+ mPosition = POPUPPOSITION_UNKNOWN;
+ mIsContextMenu = aIsContextMenu;
+ mIsTopLevelContextMenu = aIsContextMenu;
+ mIsNativeMenu = false;
+ mAnchorType = MenuPopupAnchorType_Point;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+}
+
+void nsMenuPopupFrame::InitializePopupAsNativeContextMenu(
+ nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) {
+ mTriggerContent = aTriggerContent;
+ mPopupState = ePopupShowing;
+ mAnchorContent = nullptr;
+ mScreenRect =
+ nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
+ mXPos = 0;
+ mYPos = 0;
+ mFlip = FlipType_Default;
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+ mPosition = POPUPPOSITION_UNKNOWN;
+ mIsContextMenu = true;
+ mIsTopLevelContextMenu = true;
+ mIsNativeMenu = true;
+ mAnchorType = MenuPopupAnchorType_Point;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+}
+
+void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
+ const nsAString& aPosition,
+ const nsIntRect& aRect,
+ bool aAttributesOverride) {
+ InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
+ MenuPopupAnchorType_Rect, aAttributesOverride);
+ mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel());
+}
+
+void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) {
+ mIsContextMenu = aIsContextMenu;
+
+ InvalidateFrameSubtree();
+
+ if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
+ mPopupState = ePopupOpening;
+ mIsOpenChanged = true;
+
+ // Clear mouse capture when a popup is opened.
+ if (mPopupType == PopupType::Menu) {
+ if (auto* activeESM = EventStateManager::GetActiveEventStateManager()) {
+ EventStateManager::ClearGlobalActiveContent(activeESM);
+ }
+
+ PresShell::ReleaseCapturingContent();
+ }
+
+ if (RefPtr menu = PopupElement().GetContainingMenu()) {
+ menu->PopupOpened();
+ }
+
+ // do we need an actual reflow here?
+ // is SetPopupPosition all that is needed?
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+
+ if (mPopupType == PopupType::Menu) {
+ nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1"));
+ if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP);
+ }
+ }
+}
+
+void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() {
+ // clear the trigger content if the popup is being closed. But don't clear
+ // it if the popup is just being made invisible as a popuphiding or command
+ if (mTriggerContent) {
+ // if the popup had a trigger node set, clear the global window popup node
+ // as well
+ Document* doc = mContent->GetUncomposedDoc();
+ if (doc) {
+ if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
+ nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
+ if (root) {
+ root->SetPopupNode(nullptr);
+ }
+ }
+ }
+ }
+ mTriggerContent = nullptr;
+}
+
+void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState,
+ bool aFromFrameDestruction) {
+ NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
+ "popup being set to unexpected state");
+
+ ClearPopupShownDispatcher();
+
+ // don't hide the popup when it isn't open
+ if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
+ mPopupState == ePopupPositioning) {
+ return;
+ }
+
+ if (aNewState == ePopupClosed) {
+ // clear the trigger content if the popup is being closed. But don't clear
+ // it if the popup is just being made invisible as a popuphiding or command
+ // event may want to retrieve it.
+ ClearTriggerContentIncludingDocument();
+ mAnchorContent = nullptr;
+ }
+
+ // when invisible and about to be closed, HidePopup has already been called,
+ // so just set the new state to closed and return
+ if (mPopupState == ePopupInvisible) {
+ if (aNewState == ePopupClosed) {
+ mPopupState = ePopupClosed;
+ }
+ return;
+ }
+
+ mPopupState = aNewState;
+
+ mIncrementalString.Truncate();
+
+ mIsOpenChanged = false;
+ mHFlip = mVFlip = false;
+
+ if (auto* widget = GetWidget()) {
+ // Ideally we should call ClearCachedWebrenderResources but there are
+ // intermittent failures (see bug 1748788), so we currently call
+ // ClearWebrenderAnimationResources instead.
+ widget->ClearWebrenderAnimationResources();
+ }
+
+ nsView* view = GetView();
+ nsViewManager* viewManager = view->GetViewManager();
+ viewManager->SetViewVisibility(view, ViewVisibility::Hide);
+
+ RefPtr popup = &PopupElement();
+ // XXX, bug 137033, In Windows, if mouse is outside the window when the
+ // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear
+ // current hover state, we should clear it manually. This code may not the
+ // best solution, but we can leave it here until we find the better approach.
+ if (!aFromFrameDestruction &&
+ popup->State().HasState(dom::ElementState::HOVER)) {
+ EventStateManager* esm = PresContext()->EventStateManager();
+ esm->SetContentState(nullptr, dom::ElementState::HOVER);
+ }
+ popup->PopupClosed(aDeselectMenu);
+}
+
+nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(
+ nsRect& anchorRect, const nsSize& aPrefSize, FlipStyle& aHFlip,
+ FlipStyle& aVFlip) const {
+ // flip the anchor and alignment for right-to-left
+ int8_t popupAnchor(mPopupAnchor);
+ int8_t popupAlign(mPopupAlignment);
+ if (IsDirectionRTL()) {
+ // no need to flip the centered anchor types vertically
+ if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) {
+ popupAnchor = -popupAnchor;
+ }
+ popupAlign = -popupAlign;
+ }
+
+ nsRect originalAnchorRect(anchorRect);
+
+ // first, determine at which corner of the anchor the popup should appear
+ nsPoint pnt;
+ switch (popupAnchor) {
+ case POPUPALIGNMENT_LEFTCENTER:
+ pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
+ anchorRect.y = pnt.y;
+ anchorRect.height = 0;
+ break;
+ case POPUPALIGNMENT_RIGHTCENTER:
+ pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
+ anchorRect.y = pnt.y;
+ anchorRect.height = 0;
+ break;
+ case POPUPALIGNMENT_TOPCENTER:
+ pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
+ anchorRect.x = pnt.x;
+ anchorRect.width = 0;
+ break;
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
+ anchorRect.x = pnt.x;
+ anchorRect.width = 0;
+ break;
+ case POPUPALIGNMENT_TOPRIGHT:
+ pnt = anchorRect.TopRight();
+ break;
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ pnt = anchorRect.BottomLeft();
+ break;
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ pnt = anchorRect.BottomRight();
+ break;
+ case POPUPALIGNMENT_TOPLEFT:
+ default:
+ pnt = anchorRect.TopLeft();
+ break;
+ }
+
+ // If the alignment is on the right edge of the popup, move the popup left
+ // by the width. Similarly, if the alignment is on the bottom edge of the
+ // popup, move the popup up by the height. In addition, account for the
+ // margins of the popup on the edge on which it is aligned.
+ nsMargin margin = GetMargin();
+ switch (popupAlign) {
+ case POPUPALIGNMENT_TOPRIGHT:
+ pnt.MoveBy(-aPrefSize.width - margin.right, margin.top);
+ break;
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ pnt.MoveBy(margin.left, -aPrefSize.height - margin.bottom);
+ break;
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ pnt.MoveBy(-aPrefSize.width - margin.right,
+ -aPrefSize.height - margin.bottom);
+ break;
+ case POPUPALIGNMENT_TOPLEFT:
+ default:
+ pnt.MoveBy(margin.left, margin.top);
+ break;
+ }
+
+ // If we aligning to the selected item in the popup, adjust the vertical
+ // position by the height of the menulist label and the selected item's
+ // position.
+ if (mPosition == POPUPPOSITION_SELECTION) {
+ MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT ||
+ popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT);
+ MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT ||
+ popupAlign == POPUPALIGNMENT_TOPRIGHT);
+
+ // Only adjust the popup if it just opened, otherwise the popup will move
+ // around if its gets resized or the selection changed. Cache the value in
+ // mPositionedOffset and use that instead for any future calculations.
+ if (mIsOpenChanged) {
+ if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) {
+ const nscoord itemHeight = selectedItemFrame->GetRect().height;
+ const nscoord itemOffset =
+ selectedItemFrame->GetOffsetToIgnoringScrolling(this).y;
+ // We want to line-up the anchor rect with the selected item, but if the
+ // selected item is outside of our bounds, we don't want to shift the
+ // popup up in a way that our box would no longer intersect with the
+ // anchor.
+ nscoord maxOffset = aPrefSize.height - itemHeight;
+ if (const nsIScrollableFrame* sf = GetScrollFrame()) {
+ // HACK: We ideally would want to use the offset from the bottom
+ // bottom of our scroll-frame to the bottom of our frame (so as to
+ // ensure that the bottom of the scrollport is inside the anchor
+ // rect).
+ //
+ // But at this point of the code, the scroll frame may not be laid out
+ // with a definite size (might be overflowing us).
+ //
+ // So, we assume the offset from the bottom is symmetric to the offset
+ // from the top. This holds for all the popups where this matters
+ // (menulists on macOS, effectively), and seems better than somehow
+ // moving the popup after the fact as we used to do.
+ const nsIFrame* f = do_QueryFrame(sf);
+ maxOffset -= f->GetOffsetTo(this).y;
+ }
+ mPositionedOffset =
+ originalAnchorRect.height + std::min(itemOffset, maxOffset);
+ }
+ }
+
+ pnt.y -= mPositionedOffset;
+ }
+
+ // Flipping horizontally is allowed as long as the popup is above or below
+ // the anchor. This will happen if both the anchor and alignment are top or
+ // both are bottom, but different values. Similarly, flipping vertically is
+ // allowed if the popup is to the left or right of the anchor. In this case,
+ // the values of the constants are such that both must be positive or both
+ // must be negative. A special case, used for overlap, allows flipping
+ // vertically as well.
+ // If we are flipping in both directions, we want to set a flip style both
+ // horizontally and vertically. However, we want to flip on the inside edge
+ // of the anchor. Consider the example of a typical dropdown menu.
+ // Vertically, we flip the popup on the outside edges of the anchor menu,
+ // however horizontally, we want to to use the inside edges so the popup
+ // still appears underneath the anchor menu instead of floating off the
+ // side of the menu.
+ switch (popupAnchor) {
+ case POPUPALIGNMENT_LEFTCENTER:
+ case POPUPALIGNMENT_RIGHTCENTER:
+ aHFlip = FlipStyle_Outside;
+ aVFlip = FlipStyle_Inside;
+ break;
+ case POPUPALIGNMENT_TOPCENTER:
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ aHFlip = FlipStyle_Inside;
+ aVFlip = FlipStyle_Outside;
+ break;
+ default: {
+ FlipStyle anchorEdge =
+ mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None;
+ aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge;
+ if (((popupAnchor > 0) == (popupAlign > 0)) ||
+ (popupAnchor == POPUPALIGNMENT_TOPLEFT &&
+ popupAlign == POPUPALIGNMENT_TOPLEFT))
+ aVFlip = FlipStyle_Outside;
+ else
+ aVFlip = anchorEdge;
+ break;
+ }
+ }
+
+ return pnt;
+}
+
+nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() const {
+ // This method adjusts a menulist's popup such that the selected item is under
+ // the cursor, aligned with the menulist label.
+ nsCOMPtr<nsIDOMXULSelectControlElement> select;
+ if (mAnchorContent) {
+ select = mAnchorContent->AsElement()->AsXULSelectControl();
+ }
+
+ if (!select) {
+ // If there isn't an anchor, then try just getting the parent of the popup.
+ select = mContent->GetParent()->AsElement()->AsXULSelectControl();
+ if (!select) {
+ return nullptr;
+ }
+ }
+
+ nsCOMPtr<Element> selectedElement;
+ select->GetSelectedItem(getter_AddRefs(selectedElement));
+ return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr;
+}
+
+nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin,
+ nscoord aScreenEnd,
+ nscoord* aOffset) const {
+ // The popup may be positioned such that either the left/top or bottom/right
+ // is outside the screen - but never both.
+ nscoord newPos =
+ std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint));
+ *aOffset = newPos - aScreenPoint;
+ aScreenPoint = newPos;
+ return std::min(aSize, aScreenEnd - aScreenPoint);
+}
+
+nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord aAnchorBegin, nscoord aAnchorEnd,
+ nscoord aMarginBegin, nscoord aMarginEnd,
+ FlipStyle aFlip, bool aEndAligned,
+ bool* aFlipSide) const {
+ // The flip side argument will be set to true if there wasn't room and we
+ // flipped to the opposite side.
+ *aFlipSide = false;
+
+ // all of the coordinates used here are in app units relative to the screen
+ nscoord popupSize = aSize;
+ if (aScreenPoint < aScreenBegin) {
+ // at its current position, the popup would extend past the left or top
+ // edge of the screen, so it will have to be moved or resized.
+ if (aFlip) {
+ // for inside flips, we flip on the opposite side of the anchor
+ nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
+ nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
+
+ // check whether there is more room to the left and right (or top and
+ // bottom) of the anchor and put the popup on the side with more room.
+ if (startpos - aScreenBegin >= aScreenEnd - endpos) {
+ aScreenPoint = aScreenBegin;
+ popupSize = startpos - aScreenPoint - aMarginEnd;
+ *aFlipSide = !aEndAligned;
+ } else {
+ // If the newly calculated position is different than the existing
+ // position, flip such that the popup is to the right or bottom of the
+ // anchor point instead . However, when flipping use the same margin
+ // size.
+ nscoord newScreenPoint = endpos + aMarginEnd;
+ if (newScreenPoint != aScreenPoint) {
+ *aFlipSide = aEndAligned;
+ aScreenPoint = newScreenPoint;
+ // check if the new position is still off the right or bottom edge of
+ // the screen. If so, resize the popup.
+ if (aScreenPoint + aSize > aScreenEnd) {
+ popupSize = aScreenEnd - aScreenPoint;
+ }
+ }
+ }
+ } else {
+ aScreenPoint = aScreenBegin;
+ }
+ } else if (aScreenPoint + aSize > aScreenEnd) {
+ // at its current position, the popup would extend past the right or
+ // bottom edge of the screen, so it will have to be moved or resized.
+ if (aFlip) {
+ // for inside flips, we flip on the opposite side of the anchor
+ nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
+ nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
+
+ // check whether there is more room to the left and right (or top and
+ // bottom) of the anchor and put the popup on the side with more room.
+ if (aScreenEnd - endpos >= startpos - aScreenBegin) {
+ *aFlipSide = aEndAligned;
+ if (mIsContextMenu) {
+ aScreenPoint = aScreenEnd - aSize;
+ } else {
+ aScreenPoint = endpos + aMarginBegin;
+ popupSize = aScreenEnd - aScreenPoint;
+ }
+ } else {
+ // if the newly calculated position is different than the existing
+ // position, we flip such that the popup is to the left or top of the
+ // anchor point instead.
+ nscoord newScreenPoint = startpos - aSize - aMarginBegin;
+ if (newScreenPoint != aScreenPoint) {
+ *aFlipSide = !aEndAligned;
+ aScreenPoint = newScreenPoint;
+
+ // check if the new position is still off the left or top edge of the
+ // screen. If so, resize the popup.
+ if (aScreenPoint < aScreenBegin) {
+ aScreenPoint = aScreenBegin;
+ if (!mIsContextMenu) {
+ popupSize = startpos - aScreenPoint - aMarginBegin;
+ }
+ }
+ }
+ }
+ } else {
+ aScreenPoint = aScreenEnd - aSize;
+ }
+ }
+
+ // Make sure that the point is within the screen boundaries and that the
+ // size isn't off the edge of the screen. This can happen when a large
+ // positive or negative margin is used.
+ if (aScreenPoint < aScreenBegin) {
+ aScreenPoint = aScreenBegin;
+ }
+ if (aScreenPoint > aScreenEnd) {
+ aScreenPoint = aScreenEnd - aSize;
+ }
+
+ // If popupSize ended up being negative, or the original size was actually
+ // smaller than the calculated popup size, just use the original size instead.
+ if (popupSize <= 0 || aSize < popupSize) {
+ popupSize = aSize;
+ }
+
+ return std::min(popupSize, aScreenEnd - aScreenPoint);
+}
+
+nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext,
+ nsIFrame* aAnchorFrame) const {
+ // Get the root frame for a reference
+ nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame();
+
+ // The dimensions of the anchor
+ nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
+
+ // Relative to the root
+ anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(
+ aAnchorFrame, anchorRect, rootFrame);
+ // Relative to the screen
+ anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
+
+ // In its own app units
+ return anchorRect.ScaleToOtherAppUnitsRoundOut(
+ aRootPresContext->AppUnitsPerDevPixel(),
+ PresContext()->AppUnitsPerDevPixel());
+}
+
+static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return nullptr;
+ }
+ if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) {
+ if (element->HasAttr(nsGkAtoms::delegatesanchor)) {
+ for (nsIFrame* f : aFrame->PrincipalChildList()) {
+ if (!f->IsPlaceholderFrame()) {
+ return f;
+ }
+ }
+ }
+ }
+ return aFrame;
+}
+
+auto nsMenuPopupFrame::GetRects(const nsSize& aPrefSize) const -> Rects {
+ if (NS_WARN_IF(aPrefSize == nsSize(-1, -1))) {
+ // Return early if the popup hasn't been laid out yet. On Windows, this can
+ // happen when using a drag popup before it opens.
+ return {};
+ }
+
+ nsPresContext* pc = PresContext();
+ nsIFrame* rootFrame = pc->PresShell()->GetRootFrame();
+ NS_ASSERTION(rootFrame->GetView() && GetView() &&
+ rootFrame->GetView() == GetView()->GetParent(),
+ "rootFrame's view is not our view's parent???");
+
+ // Indicators of whether the popup should be flipped or resized.
+ FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
+
+ const nsMargin margin = GetMargin();
+
+ // the screen rectangle of the root frame, in dev pixels.
+ const nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
+
+ const bool isNoAutoHide = IsNoAutoHide();
+ const PopupLevel popupLevel = GetPopupLevel(isNoAutoHide);
+
+ Rects result;
+
+ // Set the popup's size to the preferred size. Below, this size will be
+ // adjusted to fit on the screen or within the content area. If the anchor is
+ // sized to the popup, use the anchor's width instead of the preferred width.
+ result.mUsedRect = nsRect(nsPoint(), aPrefSize);
+
+ const bool anchored = IsAnchored();
+ if (anchored) {
+ // In order to deal with transforms, we need the root prescontext:
+ nsPresContext* rootPc = pc->GetRootPresContext();
+ if (NS_WARN_IF(!rootPc)) {
+ // If we can't reach a root pres context, don't bother continuing.
+ return result;
+ }
+
+ result.mAnchorRect = result.mUntransformedAnchorRect = [&] {
+ // If anchored to a rectangle, use that rectangle. Otherwise, determine
+ // the rectangle from the anchor.
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ return mScreenRect;
+ }
+ // if the frame is not specified, use the anchor node passed to OpenPopup.
+ // If that wasn't specified either, use the root frame. Note that
+ // mAnchorContent might be a different document so its presshell must be
+ // used.
+ nsIFrame* anchorFrame = GetAnchorFrame();
+ if (!anchorFrame) {
+ return rootScreenRect;
+ }
+ return ComputeAnchorRect(rootPc, anchorFrame);
+ }();
+
+ // if we are anchored, there are certain things we don't want to do when
+ // repositioning the popup to fit on the screen, such as end up positioned
+ // over the anchor, for instance a popup appearing over the menu label.
+ // When doing this reposition, we want to move the popup to the side with
+ // the most room. The combination of anchor and alignment dictate if we
+ // readjust above/below or to the left/right.
+ if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
+ // move the popup according to the anchor and alignment. This will also
+ // tell us which axis the popup is flush against in case we have to move
+ // it around later. The AdjustPositionForAnchorAlign method accounts for
+ // the popup's margin.
+ result.mUsedRect.MoveTo(AdjustPositionForAnchorAlign(
+ result.mAnchorRect, aPrefSize, hFlip, vFlip));
+ } else {
+ // With no anchor, the popup is positioned relative to the root frame.
+ result.mUsedRect.MoveTo(result.mAnchorRect.TopLeft() +
+ nsPoint(margin.left, margin.top));
+ }
+
+ // mXPos and mYPos specify an additional offset passed to OpenPopup that
+ // should be added to the position. We also add the offset to the anchor
+ // pos so a later flip/resize takes the offset into account.
+ // FIXME(emilio): Wayland doesn't seem to be accounting for this offset
+ // anywhere, and it probably should.
+ {
+ nsPoint offset(CSSPixel::ToAppUnits(mXPos), CSSPixel::ToAppUnits(mYPos));
+ if (IsDirectionRTL()) {
+ offset.x = -offset.x;
+ }
+ result.mUsedRect.MoveBy(offset);
+ result.mAnchorRect.MoveBy(offset);
+ }
+ } else {
+ // Not anchored, use mScreenRect
+ result.mUsedRect.MoveTo(mScreenRect.TopLeft());
+ result.mAnchorRect = result.mUntransformedAnchorRect =
+ nsRect(mScreenRect.TopLeft(), nsSize());
+
+ // Right-align RTL context menus, and apply margin and offsets as per the
+ // platform conventions.
+ if (mIsContextMenu && IsDirectionRTL()) {
+ result.mUsedRect.x -= aPrefSize.Width();
+ result.mUsedRect.MoveBy(-margin.right, margin.top);
+ } else {
+ result.mUsedRect.MoveBy(margin.left, margin.top);
+ }
+#ifdef XP_MACOSX
+ // OSX tooltips follow standard flip rule but other popups flip horizontally
+ // not vertically
+ if (mPopupType == PopupType::Tooltip) {
+ vFlip = FlipStyle_Outside;
+ } else {
+ hFlip = FlipStyle_Outside;
+ }
+#else
+ // Other OS screen positioned popups can be flipped vertically but never
+ // horizontally
+ vFlip = FlipStyle_Outside;
+#endif // #ifdef XP_MACOSX
+ }
+
+ const int32_t a2d = pc->AppUnitsPerDevPixel();
+
+ nsView* view = GetView();
+ NS_ASSERTION(view, "popup with no view");
+
+ nsIWidget* widget = view->GetWidget();
+
+ // If a panel has flip="none", don't constrain or flip it.
+ // Also, always do this for content shells, so that the popup doesn't extend
+ // outside the containing frame.
+ if (mInContentShell || mFlip != FlipType_None) {
+ const Maybe<nsRect> constraintRect =
+ GetConstraintRect(result.mAnchorRect, rootScreenRect, popupLevel);
+
+ if (constraintRect) {
+ // Ensure that anchorRect is on the constraint rect.
+ result.mAnchorRect = result.mAnchorRect.Intersect(*constraintRect);
+ // Shrink the popup down if it is larger than the constraint size
+ if (result.mUsedRect.width > constraintRect->width) {
+ result.mUsedRect.width = constraintRect->width;
+ }
+ if (result.mUsedRect.height > constraintRect->height) {
+ result.mUsedRect.height = constraintRect->height;
+ }
+ }
+
+ if (IS_WAYLAND_DISPLAY() && widget) {
+ // Shrink the popup down if it's larger than popup size received from
+ // Wayland compositor. We don't know screen size on Wayland so this is the
+ // only info we have there.
+ const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits(
+ widget->GetMoveToRectPopupSize(), a2d);
+ if (waylandSize.width > 0 && result.mUsedRect.width > waylandSize.width) {
+ LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget,
+ result.mUsedRect.width, waylandSize.width);
+ result.mUsedRect.width = waylandSize.width;
+ }
+ if (waylandSize.height > 0 &&
+ result.mUsedRect.height > waylandSize.height) {
+ LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget,
+ result.mUsedRect.height, waylandSize.height);
+ result.mUsedRect.height = waylandSize.height;
+ }
+ }
+
+ // At this point the anchor (anchorRect) is within the available screen
+ // area (constraintRect) and the popup is known to be no larger than the
+ // screen.
+ if (constraintRect) {
+ // We might want to "slide" an arrow if the panel is of the correct type -
+ // but we can only slide on one axis - the other axis must be "flipped or
+ // resized" as normal.
+ bool slideHorizontal = false, slideVertical = false;
+ if (mFlip == FlipType_Slide) {
+ int8_t position = GetAlignmentPosition();
+ slideHorizontal = position >= POPUPPOSITION_BEFORESTART &&
+ position <= POPUPPOSITION_AFTEREND;
+ slideVertical = position >= POPUPPOSITION_STARTBEFORE &&
+ position <= POPUPPOSITION_ENDAFTER;
+ }
+
+ // Next, check if there is enough space to show the popup at full size
+ // when positioned at screenPoint. If not, flip the popups to the opposite
+ // side of their anchor point, or resize them as necessary.
+ if (slideHorizontal) {
+ result.mUsedRect.width = SlideOrResize(
+ result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
+ constraintRect->XMost(), &result.mAlignmentOffset);
+ } else {
+ const bool endAligned =
+ IsDirectionRTL()
+ ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
+ : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
+ result.mUsedRect.width = FlipOrResize(
+ result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
+ constraintRect->XMost(), result.mAnchorRect.x,
+ result.mAnchorRect.XMost(), margin.left, margin.right, hFlip,
+ endAligned, &result.mHFlip);
+ }
+ if (slideVertical) {
+ result.mUsedRect.height = SlideOrResize(
+ result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
+ constraintRect->YMost(), &result.mAlignmentOffset);
+ } else {
+ bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
+ result.mUsedRect.height = FlipOrResize(
+ result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
+ constraintRect->YMost(), result.mAnchorRect.y,
+ result.mAnchorRect.YMost(), margin.top, margin.bottom, vFlip,
+ endAligned, &result.mVFlip);
+ }
+
+#ifdef DEBUG
+ NS_ASSERTION(constraintRect->Contains(result.mUsedRect),
+ "Popup is offscreen");
+ if (!constraintRect->Contains(result.mUsedRect)) {
+ NS_WARNING(nsPrintfCString("Popup is offscreen (%s vs. %s)",
+ ToString(constraintRect).c_str(),
+ ToString(result.mUsedRect).c_str())
+ .get());
+ }
+#endif
+ }
+ }
+ // snap the popup's position in screen coordinates to device pixels, see
+ // bug 622507, bug 961431
+ result.mUsedRect.x = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.x);
+ result.mUsedRect.y = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.y);
+
+ // determine the x and y position of the view by subtracting the desired
+ // screen position from the screen position of the root frame.
+ result.mViewPoint = result.mUsedRect.TopLeft() - rootScreenRect.TopLeft();
+
+ // Offset the position by the width and height of the borders and titlebar.
+ // Even though GetClientOffset should return (0, 0) when there is no titlebar
+ // or borders, we skip these calculations anyway for non-panels to save time
+ // since they will never have a titlebar.
+ if (mPopupType == PopupType::Panel && widget) {
+ result.mClientOffset = widget->GetClientOffset();
+ result.mViewPoint +=
+ LayoutDeviceIntPoint::ToAppUnits(result.mClientOffset, a2d);
+ }
+
+ return result;
+}
+
+void nsMenuPopupFrame::SetPopupPosition(bool aIsMove) {
+ if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
+ return;
+ }
+
+ auto rects = GetRects(mPrefSize);
+ if (rects.mUsedRect.Size() != mRect.Size()) {
+ MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IN_REFLOW));
+ // We need to resize on top of moving, trigger an actual reflow.
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+ return;
+ }
+ PerformMove(rects);
+}
+
+void nsMenuPopupFrame::PerformMove(const Rects& aRects) {
+ auto* ps = PresShell();
+
+ // We're just moving, sync frame position and offset as needed.
+ ps->GetViewManager()->MoveViewTo(GetView(), aRects.mViewPoint.x,
+ aRects.mViewPoint.y);
+
+ // Now that we've positioned the view, sync up the frame's origin.
+ nsBlockFrame::SetPosition(aRects.mViewPoint -
+ GetParent()->GetOffsetTo(ps->GetRootFrame()));
+
+ // If the popup is in the positioned state or if it is shown and the position
+ // or size changed, dispatch a popuppositioned event if the popup wants it.
+ if (mPopupState == ePopupPositioning ||
+ (mPopupState == ePopupShown &&
+ !aRects.mUsedRect.IsEqualEdges(mUsedScreenRect)) ||
+ (mPopupState == ePopupShown &&
+ aRects.mAlignmentOffset != mAlignmentOffset)) {
+ mUsedScreenRect = aRects.mUsedRect;
+ if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) {
+ mPendingPositionedEvent =
+ nsXULPopupPositionedEvent::DispatchIfNeeded(mContent->AsElement());
+ }
+ }
+
+ if (!mPositionedByMoveToRect) {
+ mUntransformedAnchorRect = aRects.mUntransformedAnchorRect;
+ }
+
+ mAlignmentOffset = aRects.mAlignmentOffset;
+ mLastClientOffset = aRects.mClientOffset;
+ mHFlip = aRects.mHFlip;
+ mVFlip = aRects.mVFlip;
+
+ // If this is a noautohide popup, set the screen coordinates of the popup.
+ // This way, the popup stays at the location where it was opened even when the
+ // window is moved. Popups at the parent level follow the parent window as it
+ // is moved and remained anchored, so we want to maintain the anchoring
+ // instead.
+ //
+ // FIXME: This suffers from issues like bug 1823552, where constraints imposed
+ // by the anchor are lost, but this is super-old behavior.
+ const bool fixPositionToPoint =
+ IsNoAutoHide() && (GetPopupLevel() != PopupLevel::Parent ||
+ mAnchorType == MenuPopupAnchorType_Rect);
+ if (fixPositionToPoint) {
+ // Account for the margin that will end up being added to the screen
+ // coordinate the next time SetPopupPosition is called.
+ const auto& margin = GetMargin();
+ mAnchorType = MenuPopupAnchorType_Point;
+ mScreenRect.x = aRects.mUsedRect.x - margin.left;
+ mScreenRect.y = aRects.mUsedRect.y - margin.top;
+ }
+
+ // For anchored popups that shouldn't follow the anchor, fix the original
+ // anchor rect.
+ if (IsAnchored() && !ShouldFollowAnchor() && !mUsedScreenRect.IsEmpty() &&
+ mAnchorType != MenuPopupAnchorType_Rect) {
+ mAnchorType = MenuPopupAnchorType_Rect;
+ mScreenRect = aRects.mUntransformedAnchorRect;
+ }
+
+ // NOTE(emilio): This call below is kind of a workaround, but we need to do
+ // this here because some position changes don't go through the
+ // view system -> popup manager, like:
+ //
+ // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847
+ //
+ // So this might be the last chance we have to set the remote browser's
+ // position.
+ //
+ // Ultimately this probably wants to get fixed in the widget size of things,
+ // but given this is worst-case a redundant DOM traversal, and that popups
+ // usually don't have all that much content, this is probably an ok
+ // workaround.
+ WidgetPositionOrSizeDidChange();
+}
+
+void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() {
+ // In the case this popup has remote contents having OOP iframes, it's
+ // possible that OOP iframe's nsSubDocumentFrame has been already reflowed
+ // thus, we will never have a chance to tell this parent browser's position
+ // update to the OOP documents without notifying it explicitly.
+ if (!HasRemoteContent()) {
+ return;
+ }
+ for (nsIContent* content = mContent->GetFirstChild(); content;
+ content = content->GetNextNode(mContent)) {
+ if (content->IsXULElement(nsGkAtoms::browser) &&
+ content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote,
+ nsGkAtoms::_true, eIgnoreCase)) {
+ if (auto* browserParent = dom::BrowserParent::GetFrom(content)) {
+ browserParent->NotifyPositionUpdatedForContentsInPopup();
+ }
+ }
+ }
+}
+
+Maybe<nsRect> nsMenuPopupFrame::GetConstraintRect(
+ const nsRect& aAnchorRect, const nsRect& aRootScreenRect,
+ PopupLevel aPopupLevel) const {
+ const nsPresContext* pc = PresContext();
+ const int32_t a2d = PresContext()->AppUnitsPerDevPixel();
+ Maybe<nsRect> result;
+
+ auto AddConstraint = [&result](const nsRect& aConstraint) {
+ if (result) {
+ *result = result->Intersect(aConstraint);
+ } else {
+ result.emplace(aConstraint);
+ }
+ };
+
+ // Determine the available screen space. It will be reduced by the OS chrome
+ // such as menubars. It addition, for content shells, it will be the area of
+ // the content rather than the screen.
+ // In Wayland we can't use the screen rect because we can't know absolute
+ // window position.
+ if (!IS_WAYLAND_DISPLAY()) {
+ const DesktopToLayoutDeviceScale scale =
+ pc->DeviceContext()->GetDesktopToDeviceScale();
+ // For content shells, get the screen where the root frame is located. This
+ // is because we need to constrain the content to this content area, so we
+ // should use the same screen. Otherwise, use the screen where the anchor is
+ // located.
+ const nsRect& rect = mInContentShell ? aRootScreenRect : aAnchorRect;
+ auto desktopRect = DesktopIntRect::RoundOut(
+ LayoutDeviceRect::FromAppUnits(rect, a2d) / scale);
+ desktopRect.width = std::max(1, desktopRect.width);
+ desktopRect.height = std::max(1, desktopRect.height);
+
+ RefPtr<nsIScreen> screen =
+ widget::ScreenManager::GetSingleton().ScreenForRect(desktopRect);
+ MOZ_ASSERT(screen, "We always fall back to the primary screen");
+ // Non-top-level popups (which will always be panels) should never overlap
+ // the OS bar.
+ const bool canOverlapOSBar =
+ aPopupLevel == PopupLevel::Top &&
+ LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) &&
+ !mInContentShell;
+ // Get the total screen area if the popup is allowed to overlap it.
+ const auto screenRect =
+ canOverlapOSBar ? screen->GetRect() : screen->GetAvailRect();
+ AddConstraint(LayoutDeviceRect::ToAppUnits(screenRect, a2d));
+ }
+
+ if (mInContentShell) {
+ // For content shells, clip to the client area rather than the screen area
+ AddConstraint(aRootScreenRect);
+ } else if (!mOverrideConstraintRect.IsEmpty()) {
+ AddConstraint(mOverrideConstraintRect);
+ // This is currently only used for <select> elements where we want to
+ // constrain vertically to the screen but not horizontally, so do the
+ // intersection and then reset the horizontal values.
+ //
+ // FIXME(emilio): This doesn't make any sense to me...
+ result->x = mOverrideConstraintRect.x;
+ result->width = mOverrideConstraintRect.width;
+ }
+
+ // Expand the allowable screen rect by the input margin (which can't be
+ // interacted with).
+ if (result) {
+ const nscoord inputMargin =
+ StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
+ result->Inflate(inputMargin);
+ }
+ return result;
+}
+
+ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return ConsumeOutsideClicks_True;
+ }
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return ConsumeOutsideClicks_ParentOnly;
+ }
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::never, eCaseMatters)) {
+ return ConsumeOutsideClicks_Never;
+ }
+
+ nsCOMPtr<nsIContent> parentContent = mContent->GetParent();
+ if (parentContent) {
+ dom::NodeInfo* ni = parentContent->NodeInfo();
+ if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) {
+ return ConsumeOutsideClicks_True; // Consume outside clicks for combo
+ // boxes on all platforms
+ }
+#if defined(XP_WIN)
+ // Don't consume outside clicks for menus in Windows
+ if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) ||
+ ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) ||
+ ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) ||
+ ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) &&
+ parentContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
+ eCaseMatters))) {
+ return ConsumeOutsideClicks_Never;
+ }
+#endif
+ }
+
+ return ConsumeOutsideClicks_True;
+}
+
+static nsIScrollableFrame* DoGetScrollFrame(const nsIFrame* aFrame) {
+ if (const nsIScrollableFrame* sf = do_QueryFrame(aFrame)) {
+ return const_cast<nsIScrollableFrame*>(sf);
+ }
+ for (nsIFrame* childFrame : aFrame->PrincipalChildList()) {
+ if (auto* sf = DoGetScrollFrame(childFrame)) {
+ return sf;
+ }
+ }
+ return nullptr;
+}
+
+// XXXroc this is megalame. Fossicking around for a frame of the right
+// type is a recipe for disaster in the long term.
+nsIScrollableFrame* nsMenuPopupFrame::GetScrollFrame() const {
+ return DoGetScrollFrame(this);
+}
+
+void nsMenuPopupFrame::ChangeByPage(bool aIsUp) {
+ // Only scroll by page within menulists.
+ if (!IsMenuList()) {
+ return;
+ }
+
+ nsIScrollableFrame* scrollframe = GetScrollFrame();
+
+ RefPtr popup = &PopupElement();
+ XULButtonElement* currentMenu = popup->GetActiveMenuChild();
+ XULButtonElement* newMenu = nullptr;
+ if (!currentMenu) {
+ // If there is no current menu item, get the first item. When moving up,
+ // just use this as the newMenu and leave currentMenu null so that no check
+ // for a later element is performed. When moving down, set currentMenu so
+ // that we look for one page down from the first item.
+ newMenu = popup->GetFirstMenuItem();
+ if (!aIsUp) {
+ currentMenu = newMenu;
+ }
+ }
+
+ if (currentMenu && currentMenu->GetPrimaryFrame()) {
+ const nscoord scrollHeight =
+ scrollframe ? scrollframe->GetScrollPortRect().height : mRect.height;
+ const nsRect currentRect = currentMenu->GetPrimaryFrame()->GetRect();
+ const XULButtonElement* startMenu = currentMenu;
+
+ // Get the position of the current item and add or subtract one popup's
+ // height to or from it.
+ const nscoord targetPos = aIsUp ? currentRect.YMost() - scrollHeight
+ : currentRect.y + scrollHeight;
+ // Look for the next child which is just past the target position. This
+ // child will need to be selected.
+ for (; currentMenu;
+ currentMenu = aIsUp ? popup->GetPrevMenuItemFrom(*currentMenu)
+ : popup->GetNextMenuItemFrom(*currentMenu)) {
+ if (!currentMenu->GetPrimaryFrame()) {
+ continue;
+ }
+ const nsRect curRect = currentMenu->GetPrimaryFrame()->GetRect();
+ const nscoord curPos = aIsUp ? curRect.y : curRect.YMost();
+ // If the right position was found, break out. Otherwise, look for another
+ // item.
+ if (aIsUp ? (curPos < targetPos) : (curPos > targetPos)) {
+ if (!newMenu || newMenu == startMenu) {
+ newMenu = currentMenu;
+ }
+ break;
+ }
+
+ // Assign this item to newMenu. This item will be selected in case we
+ // don't find any more.
+ newMenu = currentMenu;
+ }
+ }
+
+ // Select the new menuitem.
+ if (RefPtr newMenuRef = newMenu) {
+ popup->SetActiveMenuChild(newMenuRef);
+ }
+}
+
+dom::XULPopupElement& nsMenuPopupFrame::PopupElement() const {
+ auto* popup = dom::XULPopupElement::FromNode(GetContent());
+ MOZ_DIAGNOSTIC_ASSERT(popup);
+ return *popup;
+}
+
+XULButtonElement* nsMenuPopupFrame::GetCurrentMenuItem() const {
+ return PopupElement().GetActiveMenuChild();
+}
+
+nsIFrame* nsMenuPopupFrame::GetCurrentMenuItemFrame() const {
+ auto* child = GetCurrentMenuItem();
+ return child ? child->GetPrimaryFrame() : nullptr;
+}
+
+void nsMenuPopupFrame::HandleEnterKeyPress(WidgetEvent& aEvent) {
+ mIncrementalString.Truncate();
+ RefPtr popup = &PopupElement();
+ popup->HandleEnterKeyPress(aEvent);
+}
+
+XULButtonElement* nsMenuPopupFrame::FindMenuWithShortcut(
+ mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction) {
+ uint32_t charCode = aKeyEvent.CharCode();
+ uint32_t keyCode = aKeyEvent.KeyCode();
+
+ aDoAction = false;
+
+ // Enumerate over our list of frames.
+ const bool isMenu = !IsMenuList();
+ TimeStamp keyTime = aKeyEvent.WidgetEventPtr()->mTimeStamp;
+ if (charCode == 0) {
+ if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) {
+ if (!isMenu && !mIncrementalString.IsEmpty()) {
+ mIncrementalString.SetLength(mIncrementalString.Length() - 1);
+ return nullptr;
+ }
+#ifdef XP_WIN
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+#endif // #ifdef XP_WIN
+ }
+ return nullptr;
+ }
+ char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode));
+ if (isMenu) {
+ // Menu supports only first-letter navigation
+ mIncrementalString = uniChar;
+ } else if (IsWithinIncrementalTime(keyTime)) {
+ mIncrementalString.Append(uniChar);
+ } else {
+ // Interval too long, treat as new typing
+ mIncrementalString = uniChar;
+ }
+
+ // See bug 188199 & 192346, if all letters in incremental string are same,
+ // just try to match the first one
+ nsAutoString incrementalString(mIncrementalString);
+ uint32_t charIndex = 1, stringLength = incrementalString.Length();
+ while (charIndex < stringLength &&
+ incrementalString[charIndex] == incrementalString[charIndex - 1]) {
+ charIndex++;
+ }
+ if (charIndex == stringLength) {
+ incrementalString.Truncate(1);
+ stringLength = 1;
+ }
+
+ sLastKeyTime = keyTime;
+
+ auto* item =
+ PopupElement().FindMenuWithShortcut(incrementalString, aDoAction);
+ if (item) {
+ return item;
+ }
+
+ // If we don't match anything, rollback the last typing
+ mIncrementalString.SetLength(mIncrementalString.Length() - 1);
+
+ // didn't find a matching menu item
+#ifdef XP_WIN
+ // behavior on Windows - this item is in a menu popup off of the
+ // menu bar, so beep and do nothing else
+ if (isMenu) {
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+ }
+#endif // #ifdef XP_WIN
+
+ return nullptr;
+}
+
+nsIWidget* nsMenuPopupFrame::GetWidget() const {
+ return mView ? mView->GetWidget() : nullptr;
+}
+
+// helpers /////////////////////////////////////////////////////////////
+
+nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType)
+
+{
+ nsresult rv =
+ nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) {
+ MoveToAttributePosition();
+ }
+
+ if (aAttribute == nsGkAtoms::remote) {
+ // When the remote attribute changes, we need to create a new widget to
+ // ensure that it has the correct compositor and transparency settings to
+ // match the new value.
+ PrepareWidget(true);
+ }
+
+ if (aAttribute == nsGkAtoms::followanchor) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->UpdateFollowAnchor(this);
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::label) {
+ // set the label for the titlebar
+ nsView* view = GetView();
+ if (view) {
+ nsIWidget* widget = view->GetWidget();
+ if (widget) {
+ nsAutoString title;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ title);
+ if (!title.IsEmpty()) {
+ widget->SetTitle(title);
+ }
+ }
+ }
+ } else if (aAttribute == nsGkAtoms::ignorekeys) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ nsAutoString ignorekeys;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
+ ignorekeys);
+ pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true"));
+ }
+ }
+
+ return rv;
+}
+
+void nsMenuPopupFrame::MoveToAttributePosition() {
+ // Move the widget around when the user sets the |left| and |top| attributes.
+ // Note that this is not the best way to move the widget, as it results in
+ // lots of FE notifications and is likely to be slow as molasses. Use |moveTo|
+ // on the element if possible.
+ nsAutoString left, top;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
+ nsresult err1, err2;
+ mozilla::CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2));
+
+ if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) MoveTo(pos, false);
+
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+}
+
+void nsMenuPopupFrame::Destroy(DestroyContext& aContext) {
+ // XXX: Currently we don't fire popuphidden for these popups, that seems wrong
+ // but alas, also pre-existing.
+ HidePopup(/* aDeselectMenu = */ false, ePopupClosed,
+ /* aFromFrameDestruction = */ true);
+
+ if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
+ pm->PopupDestroyed(this);
+ }
+
+ nsBlockFrame::Destroy(aContext);
+}
+
+nsMargin nsMenuPopupFrame::GetMargin() const {
+ nsMargin margin;
+ StyleMargin()->GetMargin(margin);
+ if (mIsTopLevelContextMenu) {
+ const CSSIntPoint offset(
+ LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal),
+ LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical));
+ auto auOffset = CSSIntPoint::ToAppUnits(offset);
+ margin.top += auOffset.y;
+ margin.bottom += auOffset.y;
+ margin.left += auOffset.x;
+ margin.right += auOffset.x;
+ }
+ return margin;
+}
+
+void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs,
+ bool aByMoveToRect) {
+ nsIWidget* widget = GetWidget();
+ nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos);
+
+ // reposition the popup at the specified coordinates. Don't clear the anchor
+ // and position, because the popup can be reset to its anchor position by
+ // using (-1, -1) as coordinates.
+ //
+ // Subtract off the margin as it will be added to the position when
+ // SetPopupPosition is called.
+ {
+ nsMargin margin = GetMargin();
+ if (mIsContextMenu && IsDirectionRTL()) {
+ appUnitsPos.x += margin.right + mRect.Width();
+ } else {
+ appUnitsPos.x -= margin.left;
+ }
+ appUnitsPos.y -= margin.top;
+ }
+
+ if (mScreenRect.TopLeft() == appUnitsPos &&
+ (!widget || widget->GetClientOffset() == mLastClientOffset)) {
+ return;
+ }
+
+ mPositionedByMoveToRect = aByMoveToRect;
+ mScreenRect.MoveTo(appUnitsPos);
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ // This ensures that the anchor width is still honored, to prevent it from
+ // changing spuriously.
+ mScreenRect.height = 0;
+ // But we still need to make sure that our top left position ends up in
+ // appUnitsPos.
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mXPos = mYPos = 0;
+ } else {
+ mAnchorType = MenuPopupAnchorType_Point;
+ }
+
+ SetPopupPosition(true);
+
+ RefPtr<Element> popup = mContent->AsElement();
+ if (aUpdateAttrs &&
+ (popup->HasAttr(nsGkAtoms::left) || popup->HasAttr(nsGkAtoms::top))) {
+ nsAutoString left, top;
+ left.AppendInt(RoundedToInt(aPos).x);
+ top.AppendInt(RoundedToInt(aPos).y);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false);
+ }
+}
+
+void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos,
+ int32_t aYPos, bool aAttributesOverride) {
+ NS_ASSERTION(IsVisible(), "popup must be visible to move it");
+
+ nsPopupState oldstate = mPopupState;
+ InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos,
+ MenuPopupAnchorType_Node, aAttributesOverride);
+ // InitializePopup changed the state so reset it.
+ mPopupState = oldstate;
+
+ // Pass false here so that flipping and adjusting to fit on the screen happen.
+ SetPopupPosition(false);
+}
+
+int8_t nsMenuPopupFrame::GetAlignmentPosition() const {
+ // The code below handles most cases of alignment, anchor and position values.
+ // Those that are not handled just return POPUPPOSITION_UNKNOWN.
+
+ if (mPosition == POPUPPOSITION_OVERLAP ||
+ mPosition == POPUPPOSITION_AFTERPOINTER ||
+ mPosition == POPUPPOSITION_SELECTION) {
+ return mPosition;
+ }
+
+ int8_t position = mPosition;
+
+ if (position == POPUPPOSITION_UNKNOWN) {
+ switch (mPopupAnchor) {
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT
+ ? POPUPPOSITION_AFTEREND
+ : POPUPPOSITION_AFTERSTART;
+ break;
+ case POPUPALIGNMENT_TOPRIGHT:
+ case POPUPALIGNMENT_TOPLEFT:
+ case POPUPALIGNMENT_TOPCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
+ ? POPUPPOSITION_BEFOREEND
+ : POPUPPOSITION_BEFORESTART;
+ break;
+ case POPUPALIGNMENT_LEFTCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
+ ? POPUPPOSITION_STARTAFTER
+ : POPUPPOSITION_STARTBEFORE;
+ break;
+ case POPUPALIGNMENT_RIGHTCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
+ ? POPUPPOSITION_ENDAFTER
+ : POPUPPOSITION_ENDBEFORE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (mHFlip) {
+ position = POPUPPOSITION_HFLIP(position);
+ }
+
+ if (mVFlip) {
+ position = POPUPPOSITION_VFLIP(position);
+ }
+
+ return position;
+}
+
+/**
+ * KEEP THIS IN SYNC WITH nsIFrame::CreateView
+ * as much as possible. Until we get rid of views finally...
+ */
+void nsMenuPopupFrame::CreatePopupView() {
+ if (HasView()) {
+ return;
+ }
+
+ nsViewManager* viewManager = PresContext()->GetPresShell()->GetViewManager();
+ NS_ASSERTION(nullptr != viewManager, "null view manager");
+
+ // Create a view
+ nsView* parentView = viewManager->GetRootView();
+ auto visibility = ViewVisibility::Hide;
+
+ NS_ASSERTION(parentView, "no parent view");
+
+ // Create a view
+ nsView* view = viewManager->CreateView(GetRect(), parentView, visibility);
+ auto zIndex = ZIndex();
+ viewManager->SetViewZIndex(view, zIndex.isNothing(), zIndex.valueOr(0));
+ // XXX put view last in document order until we can do better
+ viewManager->InsertChild(parentView, view, nullptr, true);
+
+ // Remember our view
+ SetView(view);
+
+ NS_FRAME_LOG(
+ NS_FRAME_TRACE_CALLS,
+ ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
+}
+
+bool nsMenuPopupFrame::ShouldFollowAnchor() const {
+ if (mAnchorType != MenuPopupAnchorType_Node || !mAnchorContent) {
+ return false;
+ }
+
+ // Follow anchor mode is used when followanchor="true" is set or for arrow
+ // panels.
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::followanchor,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return true;
+ }
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::followanchor,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return false;
+ }
+
+ return mPopupType == PopupType::Panel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::arrow, eCaseMatters);
+}
+
+bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) {
+ if (!ShouldFollowAnchor()) {
+ return false;
+ }
+
+ if (nsIFrame* anchorFrame = GetAnchorFrame()) {
+ if (nsPresContext* rootPresContext = PresContext()->GetRootPresContext()) {
+ aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
+ }
+ }
+
+ return true;
+}
+
+bool nsMenuPopupFrame::IsDirectionRTL() const {
+ const nsIFrame* anchor = GetAnchorFrame();
+ const nsIFrame* f = anchor ? anchor : this;
+ return f->StyleVisibility()->mDirection == StyleDirection::Rtl;
+}
+
+nsIFrame* nsMenuPopupFrame::GetAnchorFrame() const {
+ nsIContent* anchor = mAnchorContent;
+ if (!anchor) {
+ return nullptr;
+ }
+ return MaybeDelegatedAnchorFrame(anchor->GetPrimaryFrame());
+}
+
+void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) {
+ // Don't update if the popup isn't visible or we shouldn't be following the
+ // anchor.
+ if (!IsVisible() || !ShouldFollowAnchor()) {
+ return;
+ }
+
+ bool shouldHide = false;
+
+ nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
+
+ // If the frame for the anchor has gone away, hide the popup.
+ nsIFrame* anchor = GetAnchorFrame();
+ if (!anchor || !rootPresContext) {
+ shouldHide = true;
+ } else if (!anchor->IsVisibleConsideringAncestors(
+ VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+ // If the anchor is now inside something that is invisible, hide the popup.
+ shouldHide = true;
+ } else {
+ // If the anchor is now inside a hidden parent popup, hide the popup.
+ nsIFrame* frame = anchor;
+ while (frame) {
+ nsMenuPopupFrame* popup = do_QueryFrame(frame);
+ if (popup && popup->PopupState() != ePopupShown) {
+ shouldHide = true;
+ break;
+ }
+
+ frame = frame->GetParent();
+ }
+ }
+
+ if (shouldHide) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ // As the caller will be iterating over the open popups, hide
+ // asyncronously.
+ pm->HidePopup(mContent->AsElement(),
+ {HidePopupOption::DeselectMenu, HidePopupOption::Async});
+ }
+
+ return;
+ }
+
+ nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
+
+ // If the rectangles are different, move the popup.
+ if (!anchorRect.IsEqualEdges(aRect)) {
+ aRect = anchorRect;
+ SetPopupPosition(true);
+ }
+}
diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
new file mode 100644
index 0000000000..e9f0b2a521
--- /dev/null
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -0,0 +1,643 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// nsMenuPopupFrame
+//
+
+#ifndef nsMenuPopupFrame_h__
+#define nsMenuPopupFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TimeStamp.h"
+#include "nsAtom.h"
+#include "nsGkAtoms.h"
+#include "nsCOMPtr.h"
+#include "nsIDOMEventListener.h"
+#include "nsXULPopupManager.h"
+
+#include "nsBlockFrame.h"
+
+#include "Units.h"
+
+class nsIWidget;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class KeyboardEvent;
+class XULButtonElement;
+class XULPopupElement;
+} // namespace dom
+namespace widget {
+enum class PopupLevel : uint8_t;
+}
+} // namespace mozilla
+
+enum ConsumeOutsideClicksResult {
+ ConsumeOutsideClicks_ParentOnly =
+ 0, // Only consume clicks on the parent anchor
+ ConsumeOutsideClicks_True = 1, // Always consume clicks
+ ConsumeOutsideClicks_Never = 2 // Never consume clicks
+};
+
+// How a popup may be flipped. Flipping to the outside edge is like how
+// a submenu would work. The entire popup is flipped to the opposite side
+// of the anchor.
+enum FlipStyle {
+ FlipStyle_None = 0,
+ FlipStyle_Outside = 1,
+ FlipStyle_Inside = 2
+};
+
+// Values for the flip attribute
+enum FlipType {
+ FlipType_Default = 0,
+ FlipType_None = 1, // don't try to flip or translate to stay onscreen
+ FlipType_Both = 2, // flip in both directions
+ FlipType_Slide = 3 // allow the arrow to "slide" instead of resizing
+};
+
+enum MenuPopupAnchorType {
+ MenuPopupAnchorType_Node = 0, // anchored to a node
+ MenuPopupAnchorType_Point = 1, // unanchored and positioned at a screen point
+ MenuPopupAnchorType_Rect = 2, // anchored at a screen rectangle
+};
+
+// values are selected so that the direction can be flipped just by
+// changing the sign
+#define POPUPALIGNMENT_NONE 0
+#define POPUPALIGNMENT_TOPLEFT 1
+#define POPUPALIGNMENT_TOPRIGHT -1
+#define POPUPALIGNMENT_BOTTOMLEFT 2
+#define POPUPALIGNMENT_BOTTOMRIGHT -2
+
+#define POPUPALIGNMENT_LEFTCENTER 16
+#define POPUPALIGNMENT_RIGHTCENTER -16
+#define POPUPALIGNMENT_TOPCENTER 17
+#define POPUPALIGNMENT_BOTTOMCENTER 18
+
+// The constants here are selected so that horizontally and vertically flipping
+// can be easily handled using the two flip macros below.
+#define POPUPPOSITION_UNKNOWN -1
+#define POPUPPOSITION_BEFORESTART 0
+#define POPUPPOSITION_BEFOREEND 1
+#define POPUPPOSITION_AFTERSTART 2
+#define POPUPPOSITION_AFTEREND 3
+#define POPUPPOSITION_STARTBEFORE 4
+#define POPUPPOSITION_ENDBEFORE 5
+#define POPUPPOSITION_STARTAFTER 6
+#define POPUPPOSITION_ENDAFTER 7
+#define POPUPPOSITION_OVERLAP 8
+#define POPUPPOSITION_AFTERPOINTER 9
+#define POPUPPOSITION_SELECTION 10
+
+#define POPUPPOSITION_HFLIP(v) (v ^ 1)
+#define POPUPPOSITION_VFLIP(v) (v ^ 2)
+
+nsIFrame* NS_NewMenuPopupFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsView;
+class nsMenuPopupFrame;
+
+// this class is used for dispatching popupshown events asynchronously.
+class nsXULPopupShownEvent final : public mozilla::Runnable,
+ public nsIDOMEventListener {
+ public:
+ nsXULPopupShownEvent(nsIContent* aPopup, nsPresContext* aPresContext)
+ : mozilla::Runnable("nsXULPopupShownEvent"),
+ mPopup(aPopup),
+ mPresContext(aPresContext) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ void CancelListener();
+
+ protected:
+ virtual ~nsXULPopupShownEvent() = default;
+
+ private:
+ const nsCOMPtr<nsIContent> mPopup;
+ const RefPtr<nsPresContext> mPresContext;
+};
+
+class nsMenuPopupFrame final : public nsBlockFrame {
+ using PopupLevel = mozilla::widget::PopupLevel;
+ using PopupType = mozilla::widget::PopupType;
+
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
+
+ explicit nsMenuPopupFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ ~nsMenuPopupFrame();
+
+ // as popups are opened asynchronously, the popup pending state is used to
+ // prevent multiple requests from attempting to open the same popup twice
+ nsPopupState PopupState() const { return mPopupState; }
+ void SetPopupState(nsPopupState);
+
+ /*
+ * When this popup is open, should clicks outside of it be consumed?
+ * Return true if the popup should rollup on an outside click,
+ * but consume that click so it can't be used for anything else.
+ * Return false to allow clicks outside the popup to activate content
+ * even when the popup is open.
+ * ---------------------------------------------------------------------
+ *
+ * Should clicks outside of a popup be eaten?
+ *
+ * Menus Autocomplete Comboboxes
+ * Mac Eat No Eat
+ * Win No No Eat
+ * Unix Eat No Eat
+ *
+ */
+ ConsumeOutsideClicksResult ConsumeOutsideClicks();
+
+ mozilla::dom::XULPopupElement& PopupElement() const;
+
+ nscoord GetPrefISize(gfxContext*) final;
+ nscoord GetMinISize(gfxContext*) final;
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ nsIWidget* GetWidget() const;
+
+ enum class WidgetStyle : uint8_t {
+ ColorScheme,
+ InputRegion,
+ Opacity,
+ Shadow,
+ Transform,
+ };
+ using WidgetStyleFlags = mozilla::EnumSet<WidgetStyle>;
+ static constexpr WidgetStyleFlags AllWidgetStyleFlags() {
+ return {WidgetStyle::ColorScheme, WidgetStyle::InputRegion,
+ WidgetStyle::Opacity, WidgetStyle::Shadow, WidgetStyle::Transform};
+ }
+ void PropagateStyleToWidget(WidgetStyleFlags = AllWidgetStyleFlags()) const;
+
+ // Overridden methods
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ // FIXME: This shouldn't run script (this can end up calling HidePopup).
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Destroy(DestroyContext&) override;
+
+ bool HasRemoteContent() const;
+
+ // Whether we should create a widget on Init().
+ bool ShouldCreateWidgetUpfront() const;
+
+ // Whether we should expand the menu to take the size of the parent menulist.
+ bool ShouldExpandToInflowParentOrAnchor() const;
+
+ // Returns true if the popup is a panel with the noautohide attribute set to
+ // true. These panels do not roll up automatically.
+ bool IsNoAutoHide() const;
+
+ PopupLevel GetPopupLevel() const { return GetPopupLevel(IsNoAutoHide()); }
+
+ // Ensure that a widget has already been created for this view, and create
+ // one if it hasn't. If aRecreate is true, destroys any existing widget and
+ // creates a new one, regardless of whether one has already been created.
+ void PrepareWidget(bool aRecreate = false);
+
+ MOZ_CAN_RUN_SCRIPT void EnsureActiveMenuListItemIsVisible();
+
+ nsresult CreateWidgetForView(nsView* aView);
+ mozilla::StyleWindowShadow GetShadowStyle() const;
+
+ void DidSetComputedStyle(ComputedStyle* aOldStyle) override;
+
+ // layout, position and display the popup as needed
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void LayoutPopup(nsPresContext*, ReflowOutput&, const ReflowInput&,
+ nsReflowStatus&);
+
+ // Set the position of the popup relative to the anchor content, anchored at a
+ // rectangle, or at a specific point if a screen position is set. The popup
+ // will be adjusted so that it is on screen. If aIsMove is true, then the
+ // popup is being moved, and should not be flipped.
+ void SetPopupPosition(bool aIsMove);
+
+ // Called when the Enter key is pressed while the popup is open. This will
+ // just pass the call down to the current menu, if any.
+ // Also, calling Enter will reset the current incremental search string,
+ // calculated in FindMenuWithShortcut.
+ MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(mozilla::WidgetEvent&);
+
+ // Locate and return the menu frame that should be activated for the supplied
+ // key event. If aDoAction is set to true by this method, then the menu's
+ // action should be carried out, as if the user had pressed the Enter key. If
+ // aDoAction is false, the menu should just be highlighted.
+ // This method also handles incremental searching in menus so the user can
+ // type the first few letters of an item/s name to select it.
+ mozilla::dom::XULButtonElement* FindMenuWithShortcut(
+ mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction);
+
+ mozilla::dom::XULButtonElement* GetCurrentMenuItem() const;
+ nsIFrame* GetCurrentMenuItemFrame() const;
+
+ PopupType GetPopupType() const { return mPopupType; }
+ bool IsContextMenu() const { return mIsContextMenu; }
+
+ bool IsOpen() const {
+ return mPopupState == ePopupOpening || mPopupState == ePopupVisible ||
+ mPopupState == ePopupShown;
+ }
+ bool IsVisible() {
+ return mPopupState == ePopupVisible || mPopupState == ePopupShown;
+ }
+ bool IsVisibleOrShowing() {
+ return IsOpen() || mPopupState == ePopupPositioning ||
+ mPopupState == ePopupShowing;
+ }
+ bool IsNativeMenu() const { return mIsNativeMenu; }
+ bool IsMouseTransparent() const;
+
+ // Return true if the popup is for a menulist.
+ bool IsMenuList() const;
+
+ bool IsDragSource() const { return mIsDragSource; }
+ void SetIsDragSource(bool aIsDragSource) { mIsDragSource = aIsDragSource; }
+
+ static nsIContent* GetTriggerContent(nsMenuPopupFrame* aMenuPopupFrame);
+ void ClearTriggerContent() { mTriggerContent = nullptr; }
+ void ClearTriggerContentIncludingDocument();
+
+ // returns true if the popup is in a content shell, or false for a popup in
+ // a chrome shell
+ bool IsInContentShell() const { return mInContentShell; }
+
+ // the Initialize methods are used to set the anchor position for
+ // each way of opening a popup.
+ void InitializePopup(nsIContent* aAnchorContent, nsIContent* aTriggerContent,
+ const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
+ MenuPopupAnchorType aAnchorType,
+ bool aAttributesOverride);
+
+ void InitializePopupAtRect(nsIContent* aTriggerContent,
+ const nsAString& aPosition, const nsIntRect& aRect,
+ bool aAttributesOverride);
+
+ /**
+ * @param aIsContextMenu if true, then the popup is
+ * positioned at a slight offset from aXPos/aYPos to ensure the
+ * (presumed) mouse position is not over the menu.
+ */
+ void InitializePopupAtScreen(nsIContent* aTriggerContent, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu);
+
+ // Called if this popup should be displayed as an OS-native context menu.
+ void InitializePopupAsNativeContextMenu(nsIContent* aTriggerContent,
+ int32_t aXPos, int32_t aYPos);
+
+ // indicate that the popup should be opened
+ void ShowPopup(bool aIsContextMenu);
+ // indicate that the popup should be hidden. The new state should either be
+ // ePopupClosed or ePopupInvisible.
+ MOZ_CAN_RUN_SCRIPT void HidePopup(bool aDeselectMenu, nsPopupState aNewState,
+ bool aFromFrameDestruction = false);
+
+ void ClearIncrementalString() { mIncrementalString.Truncate(); }
+ static bool IsWithinIncrementalTime(mozilla::TimeStamp time) {
+ return !sLastKeyTime.IsNull() &&
+ ((time - sLastKeyTime).ToMilliseconds() <=
+ mozilla::StaticPrefs::ui_menu_incremental_search_timeout());
+ }
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"MenuPopup"_ns, aResult);
+ }
+#endif
+
+ MOZ_CAN_RUN_SCRIPT void ChangeByPage(bool aIsUp);
+
+ // Move the popup to the screen coordinate |aPos| in CSS pixels.
+ // If aUpdateAttrs is true, and the popup already has left or top attributes,
+ // then those attributes are updated to the new location.
+ // The frame may be destroyed by this method.
+ void MoveTo(const mozilla::CSSPoint& aPos, bool aUpdateAttrs,
+ bool aByMoveToRect = false);
+
+ void MoveToAnchor(nsIContent* aAnchorContent, const nsAString& aPosition,
+ int32_t aXPos, int32_t aYPos, bool aAttributesOverride);
+
+ nsIScrollableFrame* GetScrollFrame() const;
+
+ void SetOverrideConstraintRect(const mozilla::CSSIntRect& aRect) {
+ mOverrideConstraintRect = mozilla::CSSIntRect::ToAppUnits(aRect);
+ }
+
+ struct Rects {
+ // For anchored popups, the anchor rectangle. For non-anchored popups, the
+ // size will be 0.
+ nsRect mAnchorRect;
+ // mAnchorRect before accounting for flipping / resizing / intersecting with
+ // the screen. This is needed for Wayland, which flips / resizes at the
+ // widget level.
+ nsRect mUntransformedAnchorRect;
+ // The final used rect we want to occupy.
+ nsRect mUsedRect;
+ // The alignment offset for sliding the panel, see
+ // nsMenuPopupFrame::mAlignmentOffset.
+ nscoord mAlignmentOffset = 0;
+ bool mHFlip = false;
+ bool mVFlip = false;
+ // The client offset of our widget.
+ mozilla::LayoutDeviceIntPoint mClientOffset;
+ nsPoint mViewPoint;
+ };
+
+ // For a popup that should appear anchored at the given rect, gets the anchor
+ // and constraint rects for that popup.
+ // This will be the available area of the screen the popup should be displayed
+ // on. Content popups, however, will also be constrained by the content area.
+ //
+ // For non-toplevel popups (which will always be panels), we will also
+ // constrain them to the available screen rect, ie they will not fall
+ // underneath the taskbar, dock or other fixed OS elements.
+ Rects GetRects(const nsSize& aPrefSize) const;
+ Maybe<nsRect> GetConstraintRect(const nsRect& aAnchorRect,
+ const nsRect& aRootScreenRect,
+ PopupLevel) const;
+ void PerformMove(const Rects&);
+
+ // Return true if the popup is positioned relative to an anchor.
+ bool IsAnchored() const { return mAnchorType != MenuPopupAnchorType_Point; }
+
+ // Return the anchor if there is one.
+ nsIContent* GetAnchor() const { return mAnchorContent; }
+
+ // Return the screen coordinates in CSS pixels of the popup,
+ // or (-1, -1, 0, 0) if anchored.
+ mozilla::CSSIntRect GetScreenAnchorRect() const {
+ return mozilla::CSSRect::FromAppUnitsRounded(mScreenRect);
+ }
+
+ mozilla::LayoutDeviceIntPoint GetLastClientOffset() const {
+ return mLastClientOffset;
+ }
+
+ // Return the alignment of the popup
+ int8_t GetAlignmentPosition() const;
+
+ // Return the offset applied to the alignment of the popup
+ nscoord GetAlignmentOffset() const { return mAlignmentOffset; }
+
+ // Clear the mPopupShownDispatcher, remove the listener and return true if
+ // mPopupShownDispatcher was non-null.
+ bool ClearPopupShownDispatcher() {
+ if (mPopupShownDispatcher) {
+ mPopupShownDispatcher->CancelListener();
+ mPopupShownDispatcher = nullptr;
+ return true;
+ }
+
+ return false;
+ }
+
+ void ShowWithPositionedEvent() { mPopupState = ePopupPositioning; }
+
+ // Checks for the anchor to change and either moves or hides the popup
+ // accordingly. The original position of the anchor should be supplied as
+ // the argument. If the popup needs to be hidden, HidePopup will be called by
+ // CheckForAnchorChange. If the popup needs to be moved, aRect will be updated
+ // with the new rectangle.
+ void CheckForAnchorChange(nsRect& aRect);
+
+ void WillDispatchPopupPositioned() { mPendingPositionedEvent = false; }
+
+ protected:
+ // returns the popup's level.
+ PopupLevel GetPopupLevel(bool aIsNoAutoHide) const;
+ void TweakMinPrefISize(nscoord&);
+
+ void InitPositionFromAnchorAlign(const nsAString& aAnchor,
+ const nsAString& aAlign);
+
+ // return the position where the popup should be, when it should be
+ // anchored at anchorRect. aHFlip and aVFlip will be set if the popup may be
+ // flipped in that direction if there is not enough space available.
+ nsPoint AdjustPositionForAnchorAlign(nsRect& aAnchorRect,
+ const nsSize& aPrefSize,
+ FlipStyle& aHFlip,
+ FlipStyle& aVFlip) const;
+
+ // For popups that are going to align to their selected item, get the frame of
+ // the selected item.
+ nsIFrame* GetSelectedItemForAlignment() const;
+
+ // check if the popup will fit into the available space and resize it. This
+ // method handles only one axis at a time so is called twice, once for
+ // horizontal and once for vertical. All arguments are specified for this
+ // one axis. All coordinates are in app units relative to the screen.
+ // aScreenPoint - the point where the popup should appear
+ // aSize - the size of the popup
+ // aScreenBegin - the left or top edge of the screen
+ // aScreenEnd - the right or bottom edge of the screen
+ // aAnchorBegin - the left or top edge of the anchor rectangle
+ // aAnchorEnd - the right or bottom edge of the anchor rectangle
+ // aMarginBegin - the left or top margin of the popup
+ // aMarginEnd - the right or bottom margin of the popup
+ // aFlip - how to flip or resize the popup when there isn't space
+ // aFlipSide - pointer to where current flip mode is stored
+ nscoord FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord aAnchorBegin, nscoord aAnchorEnd,
+ nscoord aMarginBegin, nscoord aMarginEnd,
+ FlipStyle aFlip, bool aIsOnEnd, bool* aFlipSide) const;
+
+ // check if the popup can fit into the available space by "sliding" (i.e.,
+ // by having the anchor arrow slide along one axis and only resizing if that
+ // can't provide the requested size). Only one axis can be slid - the other
+ // axis is "flipped" as normal. This method can handle either axis, but is
+ // only called for the sliding axis. All coordinates are in app units
+ // relative to the screen.
+ // aScreenPoint - the point where the popup should appear
+ // aSize - the size of the popup
+ // aScreenBegin - the left or top edge of the screen
+ // aScreenEnd - the right or bottom edge of the screen
+ // aOffset - the amount by which the arrow must be slid such that it is
+ // still aligned with the anchor.
+ // Result is the new size of the popup, which will typically be the same
+ // as aSize, unless aSize is greater than the screen width/height.
+ nscoord SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord* aOffset) const;
+
+ // Given an anchor frame, compute the anchor rectangle relative to the screen,
+ // using the popup frame's app units, and taking into account transforms.
+ nsRect ComputeAnchorRect(nsPresContext* aRootPresContext,
+ nsIFrame* aAnchorFrame) const;
+
+ // Move the popup to the position specified in its |left| and |top|
+ // attributes.
+ void MoveToAttributePosition();
+
+ // Create a popup view for this frame. The view is added a child of the root
+ // view, and is initially hidden.
+ void CreatePopupView();
+
+ nsView* GetViewInternal() const override { return mView; }
+ void SetViewInternal(nsView* aView) override { mView = aView; }
+
+ // Returns true if the popup should try to remain at the same relative
+ // location as the anchor while it is open. If the anchor becomes hidden
+ // either directly or indirectly because a parent popup or other element
+ // is no longer visible, or a parent deck page is changed, the popup hides
+ // as well. The second variation also sets the anchor rectangle, relative to
+ // the popup frame.
+ bool ShouldFollowAnchor() const;
+
+ nsIFrame* GetAnchorFrame() const;
+
+ public:
+ /**
+ * Return whether the popup direction should be RTL.
+ * If the popup has an anchor, its direction is the anchor direction.
+ * Otherwise, its the general direction of the UI.
+ *
+ * Return whether the popup direction should be RTL.
+ */
+ bool IsDirectionRTL() const;
+
+ bool ShouldFollowAnchor(nsRect& aRect);
+
+ // Returns parent menu widget for submenus that are in the same
+ // frame hierarchy, it's needed for Linux/Wayland which demands
+ // strict popup windows hierarchy.
+ nsIWidget* GetParentMenuWidget();
+
+ // Returns the effective margin for this popup. This is the CSS margin plus
+ // the context-menu shift, if needed.
+ nsMargin GetMargin() const;
+
+ // These are used by Wayland backend.
+ const nsRect& GetUntransformedAnchorRect() const {
+ return mUntransformedAnchorRect;
+ }
+ int GetPopupAlignment() const { return mPopupAlignment; }
+ int GetPopupAnchor() const { return mPopupAnchor; }
+ FlipType GetFlipType() const { return mFlip; }
+ bool IsFlippedByLayout() const { return mHFlip || mVFlip; }
+
+ void WidgetPositionOrSizeDidChange();
+
+ protected:
+ nsString mIncrementalString; // for incremental typing navigation
+
+ // the content that the popup is anchored to, if any, which may be in a
+ // different document than the popup.
+ nsCOMPtr<nsIContent> mAnchorContent;
+
+ // the content that triggered the popup, typically the node where the mouse
+ // was clicked. It will be cleared when the popup is hidden.
+ nsCOMPtr<nsIContent> mTriggerContent;
+
+ nsView* mView = nullptr;
+
+ RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher;
+
+ // The popup's screen rectangle in app units.
+ nsRect mUsedScreenRect;
+
+ // A popup's preferred size may be different than its actual size stored in
+ // mRect in the case where the popup was resized because it was too large
+ // for the screen. The preferred size mPrefSize holds the full size the popup
+ // would be before resizing. Computations are performed using this size.
+ nsSize mPrefSize{-1, -1};
+
+ // The position of the popup, in CSS pixels.
+ // The screen coordinates, if set to values other than -1,
+ // override mXPos and mYPos.
+ int32_t mXPos = 0;
+ int32_t mYPos = 0;
+ nsRect mScreenRect;
+ // Used for store rectangle which the popup is going to be anchored to, we
+ // need that for Wayland. It's important that this rect is unflipped, and
+ // without margins applied, as GTK is what takes care of determining how to
+ // flip etc. on Wayland.
+ nsRect mUntransformedAnchorRect;
+
+ // If the panel prefers to "slide" rather than resize, then the arrow gets
+ // positioned at this offset (along either the x or y axis, depending on
+ // mPosition)
+ nscoord mAlignmentOffset = 0;
+
+ // The value of the client offset of our widget the last time we positioned
+ // ourselves. We store this so that we can detect when it changes but the
+ // position of our widget didn't change.
+ mozilla::LayoutDeviceIntPoint mLastClientOffset;
+
+ PopupType mPopupType = PopupType::Panel; // type of popup
+ nsPopupState mPopupState = ePopupClosed; // open state of the popup
+
+ // popup alignment relative to the anchor node
+ int8_t mPopupAlignment = POPUPALIGNMENT_NONE;
+ int8_t mPopupAnchor = POPUPALIGNMENT_NONE;
+ int8_t mPosition = POPUPPOSITION_UNKNOWN;
+
+ FlipType mFlip = FlipType_Default; // Whether to flip
+
+ // Whether we were moved by the move-to-rect Wayland callback. In that case,
+ // we stop updating the anchor so that we can end up with a stable position.
+ bool mPositionedByMoveToRect = false;
+ // true if the open state changed since the last layout.
+ bool mIsOpenChanged = false;
+ // true for context menus and their submenus.
+ bool mIsContextMenu = false;
+ // true for the topmost context menu.
+ bool mIsTopLevelContextMenu = false;
+ // true if the popup is in a content shell.
+ bool mInContentShell = true;
+
+ // The flip modes that were used when the popup was opened
+ bool mHFlip = false;
+ bool mVFlip = false;
+
+ // Whether the most recent initialization of this menupopup happened via
+ // InitializePopupAsNativeContextMenu.
+ bool mIsNativeMenu = false;
+
+ // Whether we have a pending `popuppositioned` event.
+ bool mPendingPositionedEvent = false;
+
+ // Whether this popup is source of D&D operation. We can't close such
+ // popup on Wayland as it cancel whole D&D operation.
+ bool mIsDragSource = false;
+
+ // When POPUPPOSITION_SELECTION is used, this indicates the vertical offset
+ // that the original selected item was. This needs to be used in case the
+ // popup gets changed so that we can keep the popup at the same vertical
+ // offset.
+ // TODO(emilio): try to make this not mutable.
+ mutable nscoord mPositionedOffset = 0;
+
+ // How the popup is anchored.
+ MenuPopupAnchorType mAnchorType = MenuPopupAnchorType_Node;
+
+ nsRect mOverrideConstraintRect;
+
+ static int8_t sDefaultLevelIsTop;
+
+ static mozilla::TimeStamp sLastKeyTime;
+
+}; // class nsMenuPopupFrame
+
+#endif
diff --git a/layout/xul/nsRepeatService.cpp b/layout/xul/nsRepeatService.cpp
new file mode 100644
index 0000000000..b23c17396e
--- /dev/null
+++ b/layout/xul/nsRepeatService.cpp
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsRepeatService.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+
+static StaticAutoPtr<nsRepeatService> gRepeatService;
+
+nsRepeatService::nsRepeatService()
+ : mCallback(nullptr), mCallbackData(nullptr) {}
+
+nsRepeatService::~nsRepeatService() {
+ NS_ASSERTION(!mCallback && !mCallbackData,
+ "Callback was not removed before shutdown");
+}
+
+/* static */
+nsRepeatService* nsRepeatService::GetInstance() {
+ if (!gRepeatService) {
+ gRepeatService = new nsRepeatService();
+ }
+ return gRepeatService;
+}
+
+/*static*/
+void nsRepeatService::Shutdown() { gRepeatService = nullptr; }
+
+void nsRepeatService::Start(Callback aCallback, void* aCallbackData,
+ dom::Document* aDocument,
+ const nsACString& aCallbackName,
+ uint32_t aInitialDelay) {
+ MOZ_ASSERT(aCallback != nullptr, "null ptr");
+
+ mCallback = aCallback;
+ mCallbackData = aCallbackData;
+ mCallbackName = aCallbackName;
+
+ mRepeatTimer = NS_NewTimer(aDocument->EventTargetFor(TaskCategory::Other));
+
+ if (mRepeatTimer) {
+ InitTimerCallback(aInitialDelay);
+ }
+}
+
+void nsRepeatService::Stop(Callback aCallback, void* aCallbackData) {
+ if (mCallback != aCallback || mCallbackData != aCallbackData) return;
+
+ // printf("Stopping repeat timer\n");
+ if (mRepeatTimer) {
+ mRepeatTimer->Cancel();
+ mRepeatTimer = nullptr;
+ }
+ mCallback = nullptr;
+ mCallbackData = nullptr;
+}
+
+void nsRepeatService::InitTimerCallback(uint32_t aInitialDelay) {
+ if (!mRepeatTimer) {
+ return;
+ }
+
+ mRepeatTimer->InitWithNamedFuncCallback(
+ [](nsITimer* aTimer, void* aClosure) {
+ // Use gRepeatService instead of nsRepeatService::GetInstance() (because
+ // we don't want nsRepeatService::GetInstance() to re-create a new
+ // instance for us, if we happen to get invoked after
+ // nsRepeatService::Shutdown() has nulled out gRepeatService).
+ nsRepeatService* rs = gRepeatService;
+ if (!rs) {
+ return;
+ }
+
+ if (rs->mCallback) {
+ rs->mCallback(rs->mCallbackData);
+ }
+
+ rs->InitTimerCallback(REPEAT_DELAY);
+ },
+ nullptr, aInitialDelay, nsITimer::TYPE_ONE_SHOT, mCallbackName.Data());
+}
diff --git a/layout/xul/nsRepeatService.h b/layout/xul/nsRepeatService.h
new file mode 100644
index 0000000000..158e7ffd31
--- /dev/null
+++ b/layout/xul/nsRepeatService.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// nsRepeatService
+//
+#ifndef nsRepeatService_h__
+#define nsRepeatService_h__
+
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+#define INITAL_REPEAT_DELAY 250
+
+#ifdef XP_MACOSX
+# define REPEAT_DELAY 25
+#else
+# define REPEAT_DELAY 50
+#endif
+
+class nsITimer;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+} // namespace mozilla
+
+class nsRepeatService final {
+ public:
+ typedef void (*Callback)(void* aData);
+
+ ~nsRepeatService();
+
+ // Start dispatching timer events to the callback. There is no memory
+ // management of aData here; it is the caller's responsibility to call
+ // Stop() before aData's memory is released.
+ //
+ // aCallbackName is the label of the callback, used to pass to
+ // InitWithNamedCallbackFunc.
+ //
+ // aDocument is used to get the event target in Start(). We need an event
+ // target to init mRepeatTimer.
+ void Start(Callback aCallback, void* aCallbackData,
+ mozilla::dom::Document* aDocument, const nsACString& aCallbackName,
+ uint32_t aInitialDelay = INITAL_REPEAT_DELAY);
+ // Stop dispatching timer events to the callback. If the repeat service
+ // is not currently configured with the given callback and data, this
+ // is just ignored.
+ void Stop(Callback aCallback, void* aData);
+
+ static nsRepeatService* GetInstance();
+ static void Shutdown();
+
+ protected:
+ nsRepeatService();
+
+ private:
+ // helper function to initialize callback function to mRepeatTimer
+ void InitTimerCallback(uint32_t aInitialDelay);
+
+ Callback mCallback;
+ void* mCallbackData;
+ nsCString mCallbackName;
+ nsCOMPtr<nsITimer> mRepeatTimer;
+
+}; // class nsRepeatService
+
+#endif
diff --git a/layout/xul/nsScrollbarButtonFrame.cpp b/layout/xul/nsScrollbarButtonFrame.cpp
new file mode 100644
index 0000000000..e9bb69016a
--- /dev/null
+++ b/layout/xul/nsScrollbarButtonFrame.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsScrollbarButtonFrame.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "nsCOMPtr.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsLayoutUtils.h"
+#include "nsSliderFrame.h"
+#include "nsScrollbarFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsRepeatService.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Telemetry.h"
+
+using namespace mozilla;
+
+//
+// NS_NewToolbarFrame
+//
+// Creates a new Toolbar frame and returns it
+//
+nsIFrame* NS_NewScrollbarButtonFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsScrollbarButtonFrame(aStyle, aPresShell->GetPresContext());
+}
+NS_IMPL_FRAMEARENA_HELPERS(nsScrollbarButtonFrame)
+
+nsresult nsScrollbarButtonFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ // If a web page calls event.preventDefault() we still want to
+ // scroll when scroll arrow is clicked. See bug 511075.
+ if (!mContent->IsInNativeAnonymousSubtree() &&
+ nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ switch (aEvent->mMessage) {
+ case eMouseDown:
+ mCursorOnThis = true;
+ // if we didn't handle the press ourselves, pass it on to the superclass
+ if (HandleButtonPress(aPresContext, aEvent, aEventStatus)) {
+ return NS_OK;
+ }
+ break;
+ case eMouseUp:
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+ break;
+ case eMouseOut:
+ mCursorOnThis = false;
+ break;
+ case eMouseMove: {
+ nsPoint cursor = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, RelativeTo{this});
+ nsRect frameRect(nsPoint(0, 0), GetSize());
+ mCursorOnThis = frameRect.Contains(cursor);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return SimpleXULLeafFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+bool nsScrollbarButtonFrame::HandleButtonPress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ // Get the desired action for the scrollbar button.
+ LookAndFeel::IntID tmpAction;
+ uint16_t button = aEvent->AsMouseEvent()->mButton;
+ if (button == MouseButton::ePrimary) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonLeftMouseButtonAction;
+ } else if (button == MouseButton::eMiddle) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonMiddleMouseButtonAction;
+ } else if (button == MouseButton::eSecondary) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonRightMouseButtonAction;
+ } else {
+ return false;
+ }
+
+ // Get the button action metric from the pres. shell.
+ int32_t pressedButtonAction;
+ if (NS_FAILED(LookAndFeel::GetInt(tmpAction, &pressedButtonAction))) {
+ return false;
+ }
+
+ // get the scrollbar control
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+
+ if (scrollbar == nullptr) return false;
+
+ static dom::Element::AttrValuesArray strings[] = {
+ nsGkAtoms::increment, nsGkAtoms::decrement, nullptr};
+ int32_t index = mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters);
+ int32_t direction;
+ if (index == 0)
+ direction = 1;
+ else if (index == 1)
+ direction = -1;
+ else
+ return false;
+
+ bool repeat = pressedButtonAction != 2;
+ // set this attribute so we can style it later
+ AutoWeakFrame weakFrame(this);
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::active,
+ u"true"_ns, true);
+
+ PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState);
+
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+
+ if (nsScrollbarFrame* sb = do_QueryFrame(scrollbar)) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ switch (pressedButtonAction) {
+ case 0:
+ sb->SetIncrementToLine(direction);
+ if (m) {
+ m->ScrollByLine(sb, direction, ScrollSnapFlags::IntendedDirection);
+ }
+ break;
+ case 1:
+ sb->SetIncrementToPage(direction);
+ if (m) {
+ m->ScrollByPage(sb, direction,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition);
+ }
+ break;
+ case 2:
+ sb->SetIncrementToWhole(direction);
+ if (m) {
+ m->ScrollByWhole(sb, direction, ScrollSnapFlags::IntendedEndPosition);
+ }
+ break;
+ case 3:
+ default:
+ // We were told to ignore this click, or someone assigned a non-standard
+ // value to the button's action.
+ return false;
+ }
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+
+ if (!m) {
+ sb->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::No);
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+ }
+ }
+ if (repeat) {
+ StartRepeat();
+ }
+ return true;
+}
+
+NS_IMETHODIMP
+nsScrollbarButtonFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ PresShell::ReleaseCapturingContent();
+ // we're not active anymore
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::active, true);
+ StopRepeat();
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ if (m) {
+ m->ScrollbarReleased(sb);
+ }
+ }
+ return NS_OK;
+}
+
+void nsScrollbarButtonFrame::Notify() {
+ if (mCursorOnThis ||
+ LookAndFeel::GetInt(LookAndFeel::IntID::ScrollbarButtonAutoRepeatBehavior,
+ 0)) {
+ // get the scrollbar control
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ if (m) {
+ m->RepeatButtonScroll(sb);
+ } else {
+ sb->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::No);
+ }
+ }
+ }
+}
+
+nsresult nsScrollbarButtonFrame::GetChildWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result) {
+ // recursively search our children
+ for (nsIFrame* childFrame : start->PrincipalChildList()) {
+ // get the content node
+ nsIContent* child = childFrame->GetContent();
+
+ if (child) {
+ // see if it is the child
+ if (child->IsXULElement(atom)) {
+ result = childFrame;
+
+ return NS_OK;
+ }
+ }
+
+ // recursive search the child
+ GetChildWithTag(atom, childFrame, result);
+ if (result != nullptr) return NS_OK;
+ }
+
+ result = nullptr;
+ return NS_OK;
+}
+
+nsresult nsScrollbarButtonFrame::GetParentWithTag(nsAtom* toFind,
+ nsIFrame* start,
+ nsIFrame*& result) {
+ while (start) {
+ start = start->GetParent();
+
+ if (start) {
+ // get the content node
+ nsIContent* child = start->GetContent();
+
+ if (child && child->IsXULElement(toFind)) {
+ result = start;
+ return NS_OK;
+ }
+ }
+ }
+
+ result = nullptr;
+ return NS_OK;
+}
+
+void nsScrollbarButtonFrame::Destroy(DestroyContext& aContext) {
+ // Ensure our repeat service isn't going... it's possible that a scrollbar can
+ // disappear out from under you while you're in the process of scrolling.
+ StopRepeat();
+ SimpleXULLeafFrame::Destroy(aContext);
+}
diff --git a/layout/xul/nsScrollbarButtonFrame.h b/layout/xul/nsScrollbarButtonFrame.h
new file mode 100644
index 0000000000..0f2d2ab56c
--- /dev/null
+++ b/layout/xul/nsScrollbarButtonFrame.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+
+ Eric D Vaughan
+ This class lays out its children either vertically or horizontally
+
+**/
+
+#ifndef nsScrollbarButtonFrame_h___
+#define nsScrollbarButtonFrame_h___
+
+#include "SimpleXULLeafFrame.h"
+#include "mozilla/Attributes.h"
+#include "nsLeafFrame.h"
+#include "nsRepeatService.h"
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+class nsScrollbarButtonFrame final : public mozilla::SimpleXULLeafFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsScrollbarButtonFrame)
+
+ explicit nsScrollbarButtonFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : mozilla::SimpleXULLeafFrame(aStyle, aPresContext, kClassID) {}
+
+ // Overrides
+ void Destroy(DestroyContext&) override;
+
+ friend nsIFrame* NS_NewScrollbarButtonFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ static nsresult GetChildWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result);
+ static nsresult GetParentWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result);
+
+ bool HandleButtonPress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus);
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override {
+ return NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override {
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ protected:
+ void StartRepeat() {
+ nsRepeatService::GetInstance()->Start(Notify, this, mContent->OwnerDoc(),
+ "nsScrollbarButtonFrame"_ns);
+ }
+ void StopRepeat() { nsRepeatService::GetInstance()->Stop(Notify, this); }
+ void Notify();
+ static void Notify(void* aData) {
+ static_cast<nsScrollbarButtonFrame*>(aData)->Notify();
+ }
+
+ bool mCursorOnThis = false;
+};
+
+#endif
diff --git a/layout/xul/nsScrollbarFrame.cpp b/layout/xul/nsScrollbarFrame.cpp
new file mode 100644
index 0000000000..76304bf502
--- /dev/null
+++ b/layout/xul/nsScrollbarFrame.cpp
@@ -0,0 +1,595 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsScrollbarFrame.h"
+#include "nsSliderFrame.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsGkAtoms.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsStyleConsts.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+using namespace mozilla;
+using mozilla::dom::Element;
+
+//
+// NS_NewScrollbarFrame
+//
+// Creates a new scrollbar frame and returns it
+//
+nsIFrame* NS_NewScrollbarFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsScrollbarFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsScrollbarFrame)
+
+NS_QUERYFRAME_HEAD(nsScrollbarFrame)
+ NS_QUERYFRAME_ENTRY(nsScrollbarFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+void nsScrollbarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
+
+ // We want to be a reflow root since we use reflows to move the
+ // slider. Any reflow inside the scrollbar frame will be a reflow to
+ // move the slider and will thus not change anything outside of the
+ // scrollbar or change the size of the scrollbar frame.
+ AddStateBits(NS_FRAME_REFLOW_ROOT);
+}
+
+void nsScrollbarFrame::Destroy(DestroyContext& aContext) {
+ aContext.AddAnonymousContent(mUpTopButton.forget());
+ aContext.AddAnonymousContent(mDownTopButton.forget());
+ aContext.AddAnonymousContent(mSlider.forget());
+ aContext.AddAnonymousContent(mUpBottomButton.forget());
+ aContext.AddAnonymousContent(mDownBottomButton.forget());
+ nsContainerFrame::Destroy(aContext);
+}
+
+void nsScrollbarFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ // We always take all the space we're given, and our track size in the other
+ // axis.
+ const bool horizontal = IsHorizontal();
+ const auto wm = GetWritingMode();
+ const auto minSize = aReflowInput.ComputedMinSize();
+
+ aDesiredSize.ISize(wm) = aReflowInput.ComputedISize();
+ aDesiredSize.BSize(wm) = [&] {
+ if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) {
+ return aReflowInput.ComputedBSize();
+ }
+ // We don't want to change our size during incremental reflow, see the
+ // reflow root comment in init.
+ if (!aReflowInput.mParentReflowInput) {
+ return GetLogicalSize(wm).BSize(wm);
+ }
+ return minSize.BSize(wm);
+ }();
+
+ const nsSize containerSize = aDesiredSize.PhysicalSize();
+ const LogicalSize totalAvailSize = aDesiredSize.Size(wm);
+ LogicalPoint nextKidPos(wm);
+
+ MOZ_ASSERT(!wm.IsVertical());
+ const bool movesInInlineDirection = horizontal;
+
+ // Layout our kids left to right / top to bottom.
+ for (nsIFrame* kid : mFrames) {
+ MOZ_ASSERT(!kid->GetWritingMode().IsOrthogonalTo(wm),
+ "We don't expect orthogonal scrollbar parts");
+ const bool isSlider = kid->GetContent() == mSlider;
+ LogicalSize availSize = totalAvailSize;
+ {
+ // Assume we'll consume the same size before and after the slider. This is
+ // not a technically correct assumption if we have weird scrollbar button
+ // setups, but those will be going away, see bug 1824254.
+ const int32_t factor = isSlider ? 2 : 1;
+ if (movesInInlineDirection) {
+ availSize.ISize(wm) =
+ std::max(0, totalAvailSize.ISize(wm) - nextKidPos.I(wm) * factor);
+ } else {
+ availSize.BSize(wm) =
+ std::max(0, totalAvailSize.BSize(wm) - nextKidPos.B(wm) * factor);
+ }
+ }
+
+ ReflowInput kidRI(aPresContext, aReflowInput, kid, availSize);
+ if (isSlider) {
+ // We want for the slider to take all the remaining available space.
+ kidRI.SetComputedISize(availSize.ISize(wm));
+ kidRI.SetComputedBSize(availSize.BSize(wm));
+ } else if (movesInInlineDirection) {
+ // Otherwise we want all the space in the axis we're not advancing in, and
+ // the default / minimum size on the other axis.
+ kidRI.SetComputedBSize(availSize.BSize(wm));
+ } else {
+ kidRI.SetComputedISize(availSize.ISize(wm));
+ }
+
+ ReflowOutput kidDesiredSize(wm);
+ nsReflowStatus status;
+ const auto flags = ReflowChildFlags::Default;
+ ReflowChild(kid, aPresContext, kidDesiredSize, kidRI, wm, nextKidPos,
+ containerSize, flags, status);
+ // We haven't seen the slider yet, we can advance
+ FinishReflowChild(kid, aPresContext, kidDesiredSize, &kidRI, wm, nextKidPos,
+ containerSize, flags);
+ if (movesInInlineDirection) {
+ nextKidPos.I(wm) += kidDesiredSize.ISize(wm);
+ } else {
+ nextKidPos.B(wm) += kidDesiredSize.BSize(wm);
+ }
+ }
+
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+}
+
+nsresult nsScrollbarFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ // Update value in our children
+ UpdateChildrenAttributeValue(aAttribute, true);
+
+ // if the current position changes, notify any nsGfxScrollFrame
+ // parent we may have
+ if (aAttribute != nsGkAtoms::curpos) {
+ return rv;
+ }
+
+ nsIScrollableFrame* scrollable = do_QueryFrame(GetParent());
+ if (!scrollable) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIContent> content(mContent);
+ scrollable->CurPosAttributeChanged(content);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandlePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleMultiplePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleDrag(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+void nsScrollbarFrame::SetScrollbarMediatorContent(nsIContent* aMediator) {
+ mScrollbarMediator = aMediator;
+}
+
+nsIScrollbarMediator* nsScrollbarFrame::GetScrollbarMediator() {
+ if (!mScrollbarMediator) {
+ return nullptr;
+ }
+ nsIFrame* f = mScrollbarMediator->GetPrimaryFrame();
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(f);
+ nsIScrollbarMediator* sbm;
+
+ if (scrollFrame) {
+ nsIFrame* scrolledFrame = scrollFrame->GetScrolledFrame();
+ sbm = do_QueryFrame(scrolledFrame);
+ if (sbm) {
+ return sbm;
+ }
+ }
+ sbm = do_QueryFrame(f);
+ if (f && !sbm) {
+ f = f->PresShell()->GetRootScrollFrame();
+ if (f && f->GetContent() == mScrollbarMediator) {
+ return do_QueryFrame(f);
+ }
+ }
+ return sbm;
+}
+
+bool nsScrollbarFrame::IsHorizontal() const {
+ auto appearance = StyleDisplay()->EffectiveAppearance();
+ MOZ_ASSERT(appearance == StyleAppearance::ScrollbarHorizontal ||
+ appearance == StyleAppearance::ScrollbarVertical);
+ return appearance == StyleAppearance::ScrollbarHorizontal;
+}
+
+nsSize nsScrollbarFrame::ScrollbarMinSize() const {
+ nsPresContext* pc = PresContext();
+ const LayoutDeviceIntSize widget =
+ pc->Theme()->GetMinimumWidgetSize(pc, const_cast<nsScrollbarFrame*>(this),
+ StyleDisplay()->EffectiveAppearance());
+ return LayoutDeviceIntSize::ToAppUnits(widget, pc->AppUnitsPerDevPixel());
+}
+
+StyleScrollbarWidth nsScrollbarFrame::ScrollbarWidth() const {
+ return nsLayoutUtils::StyleForScrollbar(this)
+ ->StyleUIReset()
+ ->ScrollbarWidth();
+}
+
+nscoord nsScrollbarFrame::ScrollbarTrackSize() const {
+ nsPresContext* pc = PresContext();
+ auto overlay = pc->UseOverlayScrollbars() ? nsITheme::Overlay::Yes
+ : nsITheme::Overlay::No;
+ return LayoutDevicePixel::ToAppUnits(
+ pc->Theme()->GetScrollbarSize(pc, ScrollbarWidth(), overlay),
+ pc->AppUnitsPerDevPixel());
+}
+
+void nsScrollbarFrame::SetIncrementToLine(int32_t aDirection) {
+ mSmoothScroll = true;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::LINES;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ mIncrement = aDirection * nsSliderFrame::GetIncrement(content);
+}
+
+void nsScrollbarFrame::SetIncrementToPage(int32_t aDirection) {
+ mSmoothScroll = true;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::PAGES;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ mIncrement = aDirection * nsSliderFrame::GetPageIncrement(content);
+}
+
+void nsScrollbarFrame::SetIncrementToWhole(int32_t aDirection) {
+ // Don't repeat or use smooth scrolling if scrolling to beginning or end
+ // of a page.
+ mSmoothScroll = false;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::WHOLE;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ if (aDirection == -1)
+ mIncrement = -nsSliderFrame::GetCurrentPosition(content);
+ else
+ mIncrement = nsSliderFrame::GetMaxPosition(content) -
+ nsSliderFrame::GetCurrentPosition(content);
+}
+
+int32_t nsScrollbarFrame::MoveToNewPosition(
+ ImplementsScrollByUnit aImplementsScrollByUnit) {
+ if (aImplementsScrollByUnit == ImplementsScrollByUnit::Yes &&
+ StaticPrefs::apz_scrollbarbuttonrepeat_enabled()) {
+ nsIScrollbarMediator* m = GetScrollbarMediator();
+ MOZ_ASSERT(m);
+ // aImplementsScrollByUnit being Yes indicates the caller doesn't care
+ // about the return value.
+ // Note that this `MoveToNewPosition` is used for scrolling triggered by
+ // repeating scrollbar button press, so we'd use an intended-direction
+ // scroll snap flag.
+ m->ScrollByUnit(
+ this, mSmoothScroll ? ScrollMode::Smooth : ScrollMode::Instant,
+ mDirection, mScrollUnit, ScrollSnapFlags::IntendedDirection);
+ return 0;
+ }
+
+ // get the scrollbar's content node
+ RefPtr<Element> content = GetContent()->AsElement();
+
+ // get the current pos
+ int32_t curpos = nsSliderFrame::GetCurrentPosition(content);
+
+ // get the max pos
+ int32_t maxpos = nsSliderFrame::GetMaxPosition(content);
+
+ // increment the given amount
+ if (mIncrement) {
+ curpos += mIncrement;
+ }
+
+ // make sure the current position is between the current and max positions
+ if (curpos < 0) {
+ curpos = 0;
+ } else if (curpos > maxpos) {
+ curpos = maxpos;
+ }
+
+ // set the current position of the slider.
+ nsAutoString curposStr;
+ curposStr.AppendInt(curpos);
+
+ AutoWeakFrame weakFrame(this);
+ if (mSmoothScroll) {
+ content->SetAttr(kNameSpaceID_None, nsGkAtoms::smooth, u"true"_ns, false);
+ }
+ content->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, curposStr, false);
+ // notify the nsScrollbarFrame of the change
+ AttributeChanged(kNameSpaceID_None, nsGkAtoms::curpos,
+ dom::MutationEvent_Binding::MODIFICATION);
+ if (!weakFrame.IsAlive()) {
+ return curpos;
+ }
+ // notify all nsSliderFrames of the change
+ for (const auto& childList : ChildLists()) {
+ for (nsIFrame* f : childList.mList) {
+ nsSliderFrame* sliderFrame = do_QueryFrame(f);
+ if (sliderFrame) {
+ sliderFrame->AttributeChanged(kNameSpaceID_None, nsGkAtoms::curpos,
+ dom::MutationEvent_Binding::MODIFICATION);
+ if (!weakFrame.IsAlive()) {
+ return curpos;
+ }
+ }
+ }
+ }
+ content->UnsetAttr(kNameSpaceID_None, nsGkAtoms::smooth, false);
+ return curpos;
+}
+
+static already_AddRefed<Element> MakeScrollbarButton(
+ dom::NodeInfo* aNodeInfo, bool aVertical, bool aBottom, bool aDown,
+ AnonymousContentKey& aKey) {
+ MOZ_ASSERT(aNodeInfo);
+ MOZ_ASSERT(
+ aNodeInfo->Equals(nsGkAtoms::scrollbarbutton, nullptr, kNameSpaceID_XUL));
+
+ static constexpr nsLiteralString kSbattrValues[2][2] = {
+ {
+ u"scrollbar-up-top"_ns,
+ u"scrollbar-up-bottom"_ns,
+ },
+ {
+ u"scrollbar-down-top"_ns,
+ u"scrollbar-down-bottom"_ns,
+ },
+ };
+
+ static constexpr nsLiteralString kTypeValues[2] = {
+ u"decrement"_ns,
+ u"increment"_ns,
+ };
+
+ aKey = AnonymousContentKey::Type_ScrollbarButton;
+ if (aVertical) {
+ aKey |= AnonymousContentKey::Flag_Vertical;
+ }
+ if (aBottom) {
+ aKey |= AnonymousContentKey::Flag_ScrollbarButton_Bottom;
+ }
+ if (aDown) {
+ aKey |= AnonymousContentKey::Flag_ScrollbarButton_Down;
+ }
+
+ RefPtr<Element> e;
+ NS_TrustedNewXULElement(getter_AddRefs(e), do_AddRef(aNodeInfo));
+ e->SetAttr(kNameSpaceID_None, nsGkAtoms::sbattr,
+ kSbattrValues[aDown][aBottom], false);
+ e->SetAttr(kNameSpaceID_None, nsGkAtoms::type, kTypeValues[aDown], false);
+ return e.forget();
+}
+
+nsresult nsScrollbarFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ nsNodeInfoManager* nodeInfoManager = mContent->NodeInfo()->NodeInfoManager();
+ Element* el = GetContent()->AsElement();
+
+ // If there are children already in the node, don't create any anonymous
+ // content (this only apply to crashtests/369038-1.xhtml)
+ if (el->HasChildren()) {
+ return NS_OK;
+ }
+
+ nsAutoString orient;
+ el->GetAttr(nsGkAtoms::orient, orient);
+ bool vertical = orient.EqualsLiteral("vertical");
+
+ RefPtr<dom::NodeInfo> sbbNodeInfo =
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollbarbutton, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
+
+ bool createButtons = PresContext()->Theme()->ThemeSupportsScrollbarButtons();
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mUpTopButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false,
+ /* aDown */ false, key);
+ aElements.AppendElement(ContentInfo(mUpTopButton, key));
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mDownTopButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false,
+ /* aDown */ true, key);
+ aElements.AppendElement(ContentInfo(mDownTopButton, key));
+ }
+
+ {
+ AnonymousContentKey key = AnonymousContentKey::Type_Slider;
+ if (vertical) {
+ key |= AnonymousContentKey::Flag_Vertical;
+ }
+
+ NS_TrustedNewXULElement(
+ getter_AddRefs(mSlider),
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::slider, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE));
+ mSlider->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, orient, false);
+
+ aElements.AppendElement(ContentInfo(mSlider, key));
+
+ NS_TrustedNewXULElement(
+ getter_AddRefs(mThumb),
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::thumb, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE));
+ mThumb->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, orient, false);
+ mSlider->AppendChildTo(mThumb, false, IgnoreErrors());
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mUpBottomButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true,
+ /* aDown */ false, key);
+ aElements.AppendElement(ContentInfo(mUpBottomButton, key));
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mDownBottomButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true,
+ /* aDown */ true, key);
+ aElements.AppendElement(ContentInfo(mDownBottomButton, key));
+ }
+
+ // Don't cache styles if we are inside a <select> element, since we have
+ // some UA style sheet rules that depend on the <select>'s attributes.
+ if (GetContent()->GetParent() &&
+ GetContent()->GetParent()->IsHTMLElement(nsGkAtoms::select)) {
+ for (auto& info : aElements) {
+ info.mKey = AnonymousContentKey::None;
+ }
+ }
+
+ UpdateChildrenAttributeValue(nsGkAtoms::curpos, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::maxpos, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::disabled, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::pageincrement, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::increment, false);
+
+ return NS_OK;
+}
+
+void nsScrollbarFrame::UpdateChildrenAttributeValue(nsAtom* aAttribute,
+ bool aNotify) {
+ Element* el = GetContent()->AsElement();
+
+ nsAutoString value;
+ el->GetAttr(aAttribute, value);
+
+ if (!el->HasAttr(aAttribute)) {
+ if (mUpTopButton) {
+ mUpTopButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mSlider) {
+ mSlider->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::curpos || aAttribute == nsGkAtoms::maxpos) {
+ if (mUpTopButton) {
+ mUpTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ } else if (aAttribute == nsGkAtoms::disabled) {
+ if (mUpTopButton) {
+ mUpTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ } else if (aAttribute == nsGkAtoms::pageincrement ||
+ aAttribute == nsGkAtoms::increment) {
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ }
+}
+
+void nsScrollbarFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mUpTopButton) {
+ aElements.AppendElement(mUpTopButton);
+ }
+
+ if (mDownTopButton) {
+ aElements.AppendElement(mDownTopButton);
+ }
+
+ if (mSlider) {
+ aElements.AppendElement(mSlider);
+ }
+
+ if (mUpBottomButton) {
+ aElements.AppendElement(mUpBottomButton);
+ }
+
+ if (mDownBottomButton) {
+ aElements.AppendElement(mDownBottomButton);
+ }
+}
diff --git a/layout/xul/nsScrollbarFrame.h b/layout/xul/nsScrollbarFrame.h
new file mode 100644
index 0000000000..4b48295f02
--- /dev/null
+++ b/layout/xul/nsScrollbarFrame.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// nsScrollbarFrame
+//
+
+#ifndef nsScrollbarFrame_h__
+#define nsScrollbarFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ScrollTypes.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+
+class nsIScrollbarMediator;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+nsIFrame* NS_NewScrollbarFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsScrollbarFrame final : public nsContainerFrame,
+ public nsIAnonymousContentCreator {
+ using Element = mozilla::dom::Element;
+
+ public:
+ explicit nsScrollbarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID),
+ mSmoothScroll(false),
+ mScrollUnit(mozilla::ScrollUnit::DEVICE_PIXELS),
+ mDirection(0),
+ mIncrement(0),
+ mScrollbarMediator(nullptr),
+ mUpTopButton(nullptr),
+ mDownTopButton(nullptr),
+ mSlider(nullptr),
+ mThumb(nullptr),
+ mUpBottomButton(nullptr),
+ mDownBottomButton(nullptr) {}
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsScrollbarFrame)
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"ScrollbarFrame"_ns, aResult);
+ }
+#endif
+
+ // nsIFrame overrides
+ nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override;
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ mozilla::StyleScrollbarWidth ScrollbarWidth() const;
+ nscoord ScrollbarTrackSize() const;
+ nsSize ScrollbarMinSize() const;
+ bool IsHorizontal() const;
+
+ void Destroy(DestroyContext&) override;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ void SetScrollbarMediatorContent(nsIContent* aMediator);
+ nsIScrollbarMediator* GetScrollbarMediator();
+
+ /**
+ * The following three methods set the value of mIncrement when a
+ * scrollbar button is pressed.
+ */
+ void SetIncrementToLine(int32_t aDirection);
+ void SetIncrementToPage(int32_t aDirection);
+ void SetIncrementToWhole(int32_t aDirection);
+
+ /**
+ * If aImplementsScrollByUnit is Yes then this uses mSmoothScroll,
+ * mScrollUnit, and mDirection and calls ScrollByUnit on the
+ * nsIScrollbarMediator. The return value is 0. This is better because it is
+ * more modern and the scroll frame can perform the scroll via apz for
+ * example. The old way below is still supported for xul trees. If
+ * aImplementsScrollByUnit is No this adds mIncrement to the current
+ * position and updates the curpos attribute obeying mSmoothScroll.
+ * @returns The new position after clamping, in CSS Pixels
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ enum class ImplementsScrollByUnit { Yes, No };
+ int32_t MoveToNewPosition(ImplementsScrollByUnit aImplementsScrollByUnit);
+ int32_t GetIncrement() { return mIncrement; }
+
+ // nsIAnonymousContentCreator
+ nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
+ void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ void UpdateChildrenAttributeValue(nsAtom* aAttribute, bool aNotify);
+
+ protected:
+ bool mSmoothScroll;
+ mozilla::ScrollUnit mScrollUnit;
+ // Direction and multiple to scroll
+ int32_t mDirection;
+
+ // Amount to scroll, in CSSPixels
+ // Ignored in favour of mScrollUnit/mDirection for regular scroll frames.
+ // Trees use this though.
+ int32_t mIncrement;
+
+ private:
+ nsCOMPtr<nsIContent> mScrollbarMediator;
+
+ nsCOMPtr<Element> mUpTopButton;
+ nsCOMPtr<Element> mDownTopButton;
+ nsCOMPtr<Element> mSlider;
+ nsCOMPtr<Element> mThumb;
+ nsCOMPtr<Element> mUpBottomButton;
+ nsCOMPtr<Element> mDownBottomButton;
+}; // class nsScrollbarFrame
+
+#endif
diff --git a/layout/xul/nsSliderFrame.cpp b/layout/xul/nsSliderFrame.cpp
new file mode 100644
index 0000000000..eae71b12c9
--- /dev/null
+++ b/layout/xul/nsSliderFrame.cpp
@@ -0,0 +1,1578 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsSliderFrame.h"
+
+#include "mozilla/ComputedStyle.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "nsCOMPtr.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLParts.h"
+#include "nsCSSRendering.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsISupportsImpl.h"
+#include "nsScrollbarFrame.h"
+#include "nsRepeatService.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsDeviceContext.h"
+#include "nsRefreshDriver.h" // for nsAPostRefreshObserver
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/DisplayPortUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SVGIntegrationUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using mozilla::dom::Document;
+using mozilla::dom::Event;
+using mozilla::layers::AsyncDragMetrics;
+using mozilla::layers::InputAPZContext;
+using mozilla::layers::ScrollbarData;
+using mozilla::layers::ScrollDirection;
+
+bool nsSliderFrame::gMiddlePref = false;
+int32_t nsSliderFrame::gSnapMultiplier;
+
+// Turn this on if you want to debug slider frames.
+#undef DEBUG_SLIDER
+
+nsIFrame* NS_NewSliderFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsSliderFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSliderFrame)
+
+NS_QUERYFRAME_HEAD(nsSliderFrame)
+ NS_QUERYFRAME_ENTRY(nsSliderFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsSliderFrame::nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID),
+ mRatio(0.0f),
+ mDragStart(0),
+ mThumbStart(0),
+ mCurPos(0),
+ mChange(0),
+ mDragFinished(true),
+ mUserChanged(false),
+ mScrollingWithAPZ(false),
+ mSuppressionActive(false),
+ mThumbMinLength(0) {}
+
+// stop timer
+nsSliderFrame::~nsSliderFrame() {
+ if (mSuppressionActive) {
+ if (auto* presShell = PresShell()) {
+ presShell->SuppressDisplayport(false);
+ }
+ }
+}
+
+void nsSliderFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
+
+ static bool gotPrefs = false;
+ if (!gotPrefs) {
+ gotPrefs = true;
+
+ gMiddlePref = Preferences::GetBool("middlemouse.scrollbarPosition");
+ gSnapMultiplier = Preferences::GetInt("slider.snapMultiplier");
+ }
+
+ mCurPos = GetCurrentPosition(aContent);
+}
+
+void nsSliderFrame::RemoveFrame(DestroyContext& aContext, ChildListID aListID,
+ nsIFrame* aOldFrame) {
+ nsContainerFrame::RemoveFrame(aContext, aListID, aOldFrame);
+ if (mFrames.IsEmpty()) {
+ RemoveListener();
+ }
+}
+
+void nsSliderFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) {
+ bool wasEmpty = mFrames.IsEmpty();
+ nsContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine,
+ std::move(aFrameList));
+ if (wasEmpty) {
+ AddListener();
+ }
+}
+
+void nsSliderFrame::AppendFrames(ChildListID aListID,
+ nsFrameList&& aFrameList) {
+ // If we have no children and on was added then make sure we add the
+ // listener
+ bool wasEmpty = mFrames.IsEmpty();
+ nsContainerFrame::AppendFrames(aListID, std::move(aFrameList));
+ if (wasEmpty) {
+ AddListener();
+ }
+}
+
+int32_t nsSliderFrame::GetCurrentPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::curpos, 0);
+}
+
+int32_t nsSliderFrame::GetMinPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::minpos, 0);
+}
+
+int32_t nsSliderFrame::GetMaxPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::maxpos, 100);
+}
+
+int32_t nsSliderFrame::GetIncrement(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::increment, 1);
+}
+
+int32_t nsSliderFrame::GetPageIncrement(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::pageincrement, 10);
+}
+
+int32_t nsSliderFrame::GetIntegerAttribute(nsIContent* content, nsAtom* atom,
+ int32_t defaultValue) {
+ nsAutoString value;
+ if (content->IsElement()) {
+ content->AsElement()->GetAttr(kNameSpaceID_None, atom, value);
+ }
+ if (!value.IsEmpty()) {
+ nsresult error;
+
+ // convert it to an integer
+ defaultValue = value.ToInteger(&error);
+ }
+
+ return defaultValue;
+}
+
+nsresult nsSliderFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ nsresult rv =
+ nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+ // if the current position changes
+ if (aAttribute == nsGkAtoms::curpos) {
+ CurrentPositionChanged();
+ } else if (aAttribute == nsGkAtoms::minpos ||
+ aAttribute == nsGkAtoms::maxpos) {
+ // bounds check it.
+
+ nsScrollbarFrame* scrollbarBox = Scrollbar();
+ nsCOMPtr<nsIContent> scrollbar = scrollbarBox->GetContent();
+ int32_t current = GetCurrentPosition(scrollbar);
+ int32_t min = GetMinPosition(scrollbar);
+ int32_t max = GetMaxPosition(scrollbar);
+
+ if (current < min || current > max) {
+ int32_t direction = 0;
+ if (current < min || max < min) {
+ current = min;
+ direction = -1;
+ } else if (current > max) {
+ current = max;
+ direction = 1;
+ }
+
+ // set the new position and notify observers
+ nsIScrollbarMediator* mediator = scrollbarBox->GetScrollbarMediator();
+ scrollbarBox->SetIncrementToWhole(direction);
+ if (mediator) {
+ mediator->ScrollByWhole(scrollbarBox, direction,
+ ScrollSnapFlags::IntendedEndPosition);
+ }
+ // 'this' might be destroyed here
+
+ nsContentUtils::AddScriptRunner(new nsSetAttrRunnable(
+ scrollbar->AsElement(), nsGkAtoms::curpos, current));
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::minpos || aAttribute == nsGkAtoms::maxpos ||
+ aAttribute == nsGkAtoms::pageincrement ||
+ aAttribute == nsGkAtoms::increment) {
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ }
+
+ return rv;
+}
+
+namespace mozilla {
+
+// Draw any tick marks that show the position of find in page results.
+class nsDisplaySliderMarks final : public nsPaintedDisplayItem {
+ public:
+ nsDisplaySliderMarks(nsDisplayListBuilder* aBuilder, nsSliderFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplaySliderMarks);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplaySliderMarks)
+
+ NS_DISPLAY_DECL_NAME("SliderMarks", TYPE_SLIDER_MARKS)
+
+ void PaintMarks(nsDisplayListBuilder* aDisplayListBuilder,
+ wr::DisplayListBuilder* aBuilder, gfxContext* aCtx);
+
+ nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override {
+ *aSnap = false;
+ return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+ }
+
+ bool CreateWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+};
+
+// This is shared between the webrender and Paint() paths. For the former,
+// aBuilder should be assigned and aCtx will be null. For the latter, aBuilder
+// should be null and aCtx should be the gfxContext for painting.
+void nsDisplaySliderMarks::PaintMarks(nsDisplayListBuilder* aDisplayListBuilder,
+ wr::DisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ DrawTarget* drawTarget = nullptr;
+ if (aCtx) {
+ drawTarget = aCtx->GetDrawTarget();
+ } else {
+ MOZ_ASSERT(aBuilder);
+ }
+
+ Document* doc = mFrame->GetContent()->GetUncomposedDoc();
+ if (!doc) {
+ return;
+ }
+
+ nsGlobalWindowInner* window =
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow());
+ if (!window) {
+ return;
+ }
+
+ nsSliderFrame* sliderFrame = static_cast<nsSliderFrame*>(mFrame);
+
+ nsIFrame* scrollbarBox = sliderFrame->Scrollbar();
+ nsCOMPtr<nsIContent> scrollbar = scrollbarBox->GetContent();
+
+ int32_t minPos = sliderFrame->GetMinPosition(scrollbar);
+ int32_t maxPos = sliderFrame->GetMaxPosition(scrollbar);
+
+ // Use the text highlight color for the tick marks.
+ nscolor highlightColor =
+ LookAndFeel::Color(LookAndFeel::ColorID::TextHighlightBackground, mFrame);
+ DeviceColor fillColor = ToDeviceColor(highlightColor);
+ fillColor.a = 0.3; // make the mark mostly transparent
+
+ int32_t appUnitsPerDevPixel =
+ sliderFrame->PresContext()->AppUnitsPerDevPixel();
+ nsRect sliderRect = sliderFrame->GetRect();
+
+ nsPoint refPoint = aDisplayListBuilder->ToReferenceFrame(mFrame);
+
+ // Increase the height of the tick mark rectangle by one pixel. If the
+ // desktop scale is greater than 1, it should be increased more.
+ // The tick marks should be drawn ignoring any page zoom that is applied.
+ float increasePixels = sliderFrame->PresContext()
+ ->DeviceContext()
+ ->GetDesktopToDeviceScale()
+ .scale;
+ const bool isHorizontal = sliderFrame->Scrollbar()->IsHorizontal();
+ float increasePixelsX = isHorizontal ? increasePixels : 0;
+ float increasePixelsY = isHorizontal ? 0 : increasePixels;
+ nsSize initialSize =
+ isHorizontal ? nsSize(0, sliderRect.height) : nsSize(sliderRect.width, 0);
+
+ nsTArray<uint32_t>& marks = window->GetScrollMarks();
+ for (uint32_t m = 0; m < marks.Length(); m++) {
+ uint32_t markValue = marks[m];
+ if (markValue > (uint32_t)maxPos) {
+ markValue = maxPos;
+ }
+ if (markValue < (uint32_t)minPos) {
+ markValue = minPos;
+ }
+
+ // The values in the marks array range up to the window's
+ // scrollMax{X,Y} - scrollMin{X,Y} (the same as the slider's maxpos).
+ // Scale the values to fit within the slider's width or height.
+ nsRect markRect(refPoint, initialSize);
+ if (isHorizontal) {
+ markRect.x +=
+ (nscoord)((double)markValue / (maxPos - minPos) * sliderRect.width);
+ } else {
+ markRect.y +=
+ (nscoord)((double)markValue / (maxPos - minPos) * sliderRect.height);
+ }
+
+ if (drawTarget) {
+ Rect devPixelRect =
+ NSRectToSnappedRect(markRect, appUnitsPerDevPixel, *drawTarget);
+ devPixelRect.Inflate(increasePixelsX, increasePixelsY);
+ drawTarget->FillRect(devPixelRect, ColorPattern(fillColor));
+ } else {
+ LayoutDeviceIntRect dRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
+ markRect, appUnitsPerDevPixel);
+ dRect.Inflate(increasePixelsX, increasePixelsY);
+ wr::LayoutRect layoutRect = wr::ToLayoutRect(dRect);
+ aBuilder->PushRect(layoutRect, layoutRect, BackfaceIsHidden(), false,
+ false, wr::ToColorF(fillColor));
+ }
+ }
+}
+
+bool nsDisplaySliderMarks::CreateWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc, layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ PaintMarks(aDisplayListBuilder, &aBuilder, nullptr);
+ return true;
+}
+
+void nsDisplaySliderMarks::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ PaintMarks(aBuilder, nullptr, aCtx);
+}
+
+} // namespace mozilla
+
+void nsSliderFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (aBuilder->IsForEventDelivery() && isDraggingThumb()) {
+ // This is EVIL, we shouldn't be messing with event delivery just to get
+ // thumb mouse drag events to arrive at the slider!
+ aLists.Outlines()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this);
+ return;
+ }
+
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+
+ if (nsIFrame* thumb = mFrames.FirstChild()) {
+ BuildDisplayListForThumb(aBuilder, thumb, aLists);
+ }
+
+ // If this is an scrollbar for the root frame, draw any markers.
+ // Markers are not drawn for other scrollbars.
+ // XXX seems like this should be done in nsScrollbarFrame instead perhaps?
+ if (!aBuilder->IsForEventDelivery()) {
+ nsScrollbarFrame* scrollbar = Scrollbar();
+ if (nsIScrollableFrame* scrollFrame =
+ do_QueryFrame(scrollbar->GetParent())) {
+ if (scrollFrame->IsRootScrollFrameOfDocument()) {
+ nsGlobalWindowInner* window = nsGlobalWindowInner::Cast(
+ PresContext()->Document()->GetInnerWindow());
+ if (window &&
+ window->GetScrollMarksOnHScrollbar() == scrollbar->IsHorizontal() &&
+ window->GetScrollMarks().Length() > 0) {
+ aLists.Content()->AppendNewToTop<nsDisplaySliderMarks>(aBuilder,
+ this);
+ }
+ }
+ }
+ }
+}
+
+static bool UsesCustomScrollbarMediator(nsIFrame* scrollbarBox) {
+ if (nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox)) {
+ if (nsIScrollbarMediator* mediator =
+ scrollbarFrame->GetScrollbarMediator()) {
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(mediator);
+ // The scrollbar mediator is not the scroll frame.
+ // That means this scroll frame has a custom scrollbar mediator.
+ if (!scrollFrame) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void nsSliderFrame::BuildDisplayListForThumb(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aThumb,
+ const nsDisplayListSet& aLists) {
+ nsRect thumbRect(aThumb->GetRect());
+
+ nsRect sliderTrack = GetRect();
+ if (sliderTrack.width < thumbRect.width ||
+ sliderTrack.height < thumbRect.height) {
+ return;
+ }
+
+ // If this scrollbar is the scrollbar of an actively scrolled scroll frame,
+ // layerize the scrollbar thumb, wrap it in its own ContainerLayer and
+ // attach scrolling information to it.
+ // We do this here and not in the thumb's BuildDisplayList so that the event
+ // region that gets created for the thumb is included in the nsDisplayOwnLayer
+ // contents.
+
+ const layers::ScrollableLayerGuid::ViewID scrollTargetId =
+ aBuilder->GetCurrentScrollbarTarget();
+ const bool thumbGetsLayer =
+ scrollTargetId != layers::ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ if (thumbGetsLayer) {
+ const Maybe<ScrollDirection> scrollDirection =
+ aBuilder->GetCurrentScrollbarDirection();
+ MOZ_ASSERT(scrollDirection.isSome());
+ const bool isHorizontal = *scrollDirection == ScrollDirection::eHorizontal;
+ const OuterCSSCoord thumbLength = OuterCSSPixel::FromAppUnits(
+ isHorizontal ? thumbRect.width : thumbRect.height);
+ const OuterCSSCoord minThumbLength =
+ OuterCSSPixel::FromAppUnits(mThumbMinLength);
+
+ nsIFrame* scrollbarBox = Scrollbar();
+ bool isAsyncDraggable = !UsesCustomScrollbarMediator(scrollbarBox);
+
+ nsPoint scrollPortOrigin;
+ if (nsIScrollableFrame* scrollFrame =
+ do_QueryFrame(scrollbarBox->GetParent())) {
+ scrollPortOrigin = scrollFrame->GetScrollPortRect().TopLeft();
+ } else {
+ isAsyncDraggable = false;
+ }
+
+ // This rect is the range in which the scroll thumb can slide in.
+ sliderTrack = sliderTrack + scrollbarBox->GetPosition() - scrollPortOrigin;
+ const OuterCSSCoord sliderTrackStart = OuterCSSPixel::FromAppUnits(
+ isHorizontal ? sliderTrack.x : sliderTrack.y);
+ const OuterCSSCoord sliderTrackLength = OuterCSSPixel::FromAppUnits(
+ isHorizontal ? sliderTrack.width : sliderTrack.height);
+ const OuterCSSCoord thumbStart =
+ OuterCSSPixel::FromAppUnits(isHorizontal ? thumbRect.x : thumbRect.y);
+
+ const nsRect overflow = aThumb->InkOverflowRectRelativeToParent();
+ nsSize refSize = aBuilder->RootReferenceFrame()->GetSize();
+ nsRect dirty = aBuilder->GetVisibleRect().Intersect(thumbRect);
+ dirty = nsLayoutUtils::ComputePartialPrerenderArea(
+ aThumb, aBuilder->GetVisibleRect(), overflow, refSize);
+
+ nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
+ aBuilder, this, dirty, dirty);
+
+ // Clip the thumb layer to the slider track. This is necessary to ensure
+ // FrameLayerBuilder is able to merge content before and after the
+ // scrollframe into the same layer (otherwise it thinks the thumb could
+ // potentially move anywhere within the existing clip).
+ DisplayListClipState::AutoSaveRestore thumbClipState(aBuilder);
+ thumbClipState.ClipContainingBlockDescendants(
+ GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(this));
+
+ // Have the thumb's container layer capture the current clip, so
+ // it doesn't apply to the thumb's contents. This allows the contents
+ // to be fully rendered even if they're partially or fully offscreen,
+ // so async scrolling can still bring it into view.
+ DisplayListClipState::AutoSaveRestore thumbContentsClipState(aBuilder);
+ thumbContentsClipState.Clear();
+
+ nsDisplayListBuilder::AutoContainerASRTracker contASRTracker(aBuilder);
+ nsDisplayListCollection tempLists(aBuilder);
+ BuildDisplayListForChild(aBuilder, aThumb, tempLists);
+
+ // This is a bit of a hack. Collect up all descendant display items
+ // and merge them into a single Content() list.
+ nsDisplayList masterList(aBuilder);
+ masterList.AppendToTop(tempLists.BorderBackground());
+ masterList.AppendToTop(tempLists.BlockBorderBackgrounds());
+ masterList.AppendToTop(tempLists.Floats());
+ masterList.AppendToTop(tempLists.Content());
+ masterList.AppendToTop(tempLists.PositionedDescendants());
+ masterList.AppendToTop(tempLists.Outlines());
+
+ // Restore the saved clip so it applies to the thumb container layer.
+ thumbContentsClipState.Restore();
+
+ // Wrap the list to make it its own layer.
+ const ActiveScrolledRoot* ownLayerASR = contASRTracker.GetContainerASR();
+ aLists.Content()->AppendNewToTopWithIndex<nsDisplayOwnLayer>(
+ aBuilder, this,
+ /* aIndex = */ nsDisplayOwnLayer::OwnLayerForScrollThumb, &masterList,
+ ownLayerASR, nsDisplayOwnLayerFlags::None,
+ ScrollbarData::CreateForThumb(*scrollDirection, GetThumbRatio(),
+ thumbStart, thumbLength, minThumbLength,
+ isAsyncDraggable, sliderTrackStart,
+ sliderTrackLength, scrollTargetId),
+ true, false);
+
+ return;
+ }
+
+ BuildDisplayListForChild(aBuilder, aThumb, aLists);
+}
+
+void nsSliderFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ NS_ASSERTION(aReflowInput.AvailableWidth() != NS_UNCONSTRAINEDSIZE,
+ "Bogus avail width");
+ NS_ASSERTION(aReflowInput.AvailableHeight() != NS_UNCONSTRAINEDSIZE,
+ "Bogus avail height");
+
+ // We always take all the space we're given.
+ aDesiredSize.Width() = aReflowInput.ComputedWidth();
+ aDesiredSize.Height() = aReflowInput.ComputedHeight();
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+ // Get the thumb, should be our only child.
+ nsIFrame* thumbBox = mFrames.FirstChild();
+ if (NS_WARN_IF(!thumbBox)) {
+ return;
+ }
+
+ nsScrollbarFrame* scrollbarBox = Scrollbar();
+ nsIContent* scrollbar = scrollbarBox->GetContent();
+ const bool horizontal = scrollbarBox->IsHorizontal();
+ const auto wm = GetWritingMode();
+ nsSize availSize = aDesiredSize.PhysicalSize();
+ ReflowInput thumbRI(aPresContext, aReflowInput, thumbBox,
+ aReflowInput.AvailableSize(wm));
+
+ // Get the thumb's pref size.
+ nsSize thumbSize = thumbRI.ComputedMinSize(wm).GetPhysicalSize(wm);
+ if (horizontal) {
+ thumbSize.height = availSize.height;
+ } else {
+ thumbSize.width = availSize.width;
+ }
+
+ int32_t curPos = GetCurrentPosition(scrollbar);
+ int32_t minPos = GetMinPosition(scrollbar);
+ int32_t maxPos = GetMaxPosition(scrollbar);
+ int32_t pageIncrement = GetPageIncrement(scrollbar);
+
+ maxPos = std::max(minPos, maxPos);
+ curPos = clamped(curPos, minPos, maxPos);
+
+ // If modifying the logic here, be sure to modify the corresponding
+ // compositor-side calculation in ScrollThumbUtils::ApplyTransformForAxis().
+ nscoord& availableLength = horizontal ? availSize.width : availSize.height;
+ nscoord& thumbLength = horizontal ? thumbSize.width : thumbSize.height;
+ mThumbMinLength = thumbLength;
+
+ if ((pageIncrement + maxPos - minPos) > 0) {
+ float ratio = float(pageIncrement) / float(maxPos - minPos + pageIncrement);
+ thumbLength =
+ std::max(thumbLength, NSToCoordRound(availableLength * ratio));
+ }
+
+ // Round the thumb's length to device pixels.
+ nsPresContext* presContext = PresContext();
+ thumbLength = presContext->DevPixelsToAppUnits(
+ presContext->AppUnitsToDevPixels(thumbLength));
+
+ // mRatio translates the thumb position in app units to the value.
+ mRatio = (minPos != maxPos)
+ ? float(availableLength - thumbLength) / float(maxPos - minPos)
+ : 1;
+
+ // in reverse mode, curpos is reversed such that lower values are to the
+ // right or bottom and increase leftwards or upwards. In this case, use the
+ // offset from the end instead of the beginning.
+ bool reverse = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::reverse, eCaseMatters);
+ nscoord pos = reverse ? (maxPos - curPos) : (curPos - minPos);
+
+ // set the thumb's coord to be the current pos * the ratio.
+ nsPoint thumbPos;
+ if (horizontal) {
+ thumbPos.x = NSToCoordRound(pos * mRatio);
+ } else {
+ thumbPos.y = NSToCoordRound(pos * mRatio);
+ }
+
+ const LogicalPoint logicalPos(wm, thumbPos, availSize);
+ // TODO: It seems like a lot of this stuff should really belong in the thumb's
+ // reflow code rather than here, but since we rely on the frame tree structure
+ // heavily this matches the previous code more closely for now.
+ ReflowOutput thumbDesiredSize(wm);
+ const auto flags = ReflowChildFlags::Default;
+ nsReflowStatus status;
+ thumbRI.SetComputedISize(thumbSize.width);
+ thumbRI.SetComputedBSize(thumbSize.height);
+ ReflowChild(thumbBox, aPresContext, thumbDesiredSize, thumbRI, wm, logicalPos,
+ availSize, flags, status);
+ FinishReflowChild(thumbBox, aPresContext, thumbDesiredSize, &thumbRI, wm,
+ logicalPos, availSize, flags);
+}
+
+nsresult nsSliderFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ if (mAPZDragInitiated &&
+ *mAPZDragInitiated == InputAPZContext::GetInputBlockId() &&
+ aEvent->mMessage == eMouseDown) {
+ // If we get the mousedown after the APZ notification, then immediately
+ // switch into the state corresponding to an APZ thumb-drag. Note that
+ // we can't just do this in AsyncScrollbarDragInitiated() directly because
+ // the handling for this mousedown event in the presShell will reset the
+ // capturing content which makes isDraggingThumb() return false. We check
+ // the input block here to make sure that we correctly handle any ordering
+ // of {eMouseDown arriving, AsyncScrollbarDragInitiated() being called}.
+ mAPZDragInitiated = Nothing();
+ DragThumb(true);
+ mScrollingWithAPZ = true;
+ return NS_OK;
+ }
+
+ // If a web page calls event.preventDefault() we still want to
+ // scroll when scroll arrow is clicked. See bug 511075.
+ if (!mContent->IsInNativeAnonymousSubtree() &&
+ nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ if (!mDragFinished && !isDraggingThumb()) {
+ StopDrag();
+ return NS_OK;
+ }
+
+ nsScrollbarFrame* scrollbarBox = Scrollbar();
+ nsCOMPtr<nsIContent> scrollbar = scrollbarBox->GetContent();
+ bool isHorizontal = scrollbarBox->IsHorizontal();
+
+ if (isDraggingThumb()) {
+ switch (aEvent->mMessage) {
+ case eTouchMove:
+ case eMouseMove: {
+ if (mScrollingWithAPZ) {
+ break;
+ }
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ break;
+ }
+ if (mChange) {
+ // On Linux the destination point is determined by the initial click
+ // on the scrollbar track and doesn't change until the mouse button
+ // is released.
+#ifndef MOZ_WIDGET_GTK
+ // On the other platforms we need to update the destination point now.
+ mDestinationPoint = eventPoint;
+ StopRepeat();
+ StartRepeat();
+#endif
+ break;
+ }
+
+ nscoord pos = isHorizontal ? eventPoint.x : eventPoint.y;
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+
+ // take our current position and subtract the start location
+ pos -= mDragStart;
+ bool isMouseOutsideThumb = false;
+ if (gSnapMultiplier) {
+ nsSize thumbSize = thumbFrame->GetSize();
+ if (isHorizontal) {
+ // horizontal scrollbar - check if mouse is above or below thumb
+ // XXXbz what about looking at the .y of the thumb's rect? Is that
+ // always zero here?
+ if (eventPoint.y < -gSnapMultiplier * thumbSize.height ||
+ eventPoint.y >
+ thumbSize.height + gSnapMultiplier * thumbSize.height)
+ isMouseOutsideThumb = true;
+ } else {
+ // vertical scrollbar - check if mouse is left or right of thumb
+ if (eventPoint.x < -gSnapMultiplier * thumbSize.width ||
+ eventPoint.x >
+ thumbSize.width + gSnapMultiplier * thumbSize.width)
+ isMouseOutsideThumb = true;
+ }
+ }
+ if (aEvent->mClass == eTouchEventClass) {
+ *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ if (isMouseOutsideThumb) {
+ SetCurrentThumbPosition(scrollbar, mThumbStart, false, false);
+ return NS_OK;
+ }
+
+ // set it
+ SetCurrentThumbPosition(scrollbar, pos, false, true); // with snapping
+ } break;
+
+ case eTouchEnd:
+ case eMouseUp:
+ if (ShouldScrollForEvent(aEvent)) {
+ StopDrag();
+ // we MUST call nsFrame HandleEvent for mouse ups to maintain the
+ // selection state and capture state.
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ // return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ return NS_OK;
+ }
+
+ if (ShouldScrollToClickForEvent(aEvent)) {
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return NS_OK;
+ }
+ nscoord pos = isHorizontal ? eventPoint.x : eventPoint.y;
+
+ // adjust so that the middle of the thumb is placed under the click
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+ nsSize thumbSize = thumbFrame->GetSize();
+ nscoord thumbLength = isHorizontal ? thumbSize.width : thumbSize.height;
+
+ // set it
+ AutoWeakFrame weakFrame(this);
+ // should aMaySnap be true here?
+ SetCurrentThumbPosition(scrollbar, pos - thumbLength / 2, false, false);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK);
+
+ DragThumb(true);
+
+#ifdef MOZ_WIDGET_GTK
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->SetAttr(kNameSpaceID_None, nsGkAtoms::active, u"true"_ns, true);
+#endif
+
+ if (aEvent->mClass == eTouchEventClass) {
+ *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (isHorizontal)
+ mThumbStart = thumbFrame->GetPosition().x;
+ else
+ mThumbStart = thumbFrame->GetPosition().y;
+
+ mDragStart = pos - mThumbStart;
+ }
+#ifdef MOZ_WIDGET_GTK
+ else if (ShouldScrollForEvent(aEvent) && aEvent->mClass == eMouseEventClass &&
+ aEvent->AsMouseEvent()->mButton == MouseButton::eSecondary) {
+ // HandlePress and HandleRelease are usually called via
+ // nsIFrame::HandleEvent, but only for the left mouse button.
+ if (aEvent->mMessage == eMouseDown) {
+ HandlePress(aPresContext, aEvent, aEventStatus);
+ } else if (aEvent->mMessage == eMouseUp) {
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+ }
+
+ return NS_OK;
+ }
+#endif
+
+ // XXX hack until handle release is actually called in nsframe.
+ // if (aEvent->mMessage == eMouseOut ||
+ // aEvent->mMessage == NS_MOUSE_RIGHT_BUTTON_UP ||
+ // aEvent->mMessage == NS_MOUSE_LEFT_BUTTON_UP) {
+ // HandleRelease(aPresContext, aEvent, aEventStatus);
+ // }
+
+ if (aEvent->mMessage == eMouseOut && mChange)
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+// Helper function to collect the "scroll to click" metric. Beware of
+// caching this, users expect to be able to change the system preference
+// and see the browser change its behavior immediately.
+bool nsSliderFrame::GetScrollToClick() {
+ return LookAndFeel::GetInt(LookAndFeel::IntID::ScrollToClick, false);
+}
+
+nsScrollbarFrame* nsSliderFrame::Scrollbar() {
+ MOZ_ASSERT(GetParent());
+ MOZ_DIAGNOSTIC_ASSERT(
+ static_cast<nsScrollbarFrame*>(do_QueryFrame(GetParent())));
+ return static_cast<nsScrollbarFrame*>(GetParent());
+}
+
+void nsSliderFrame::PageUpDown(nscoord change) {
+ // on a page up or down get our page increment. We get this by getting the
+ // scrollbar we are in and asking it for the current position and the page
+ // increment. If we are not in a scrollbar we will get the values from our own
+ // node.
+ nsIFrame* scrollbarBox = Scrollbar();
+ nsCOMPtr<nsIContent> scrollbar = scrollbarBox->GetContent();
+
+ nscoord pageIncrement = GetPageIncrement(scrollbar);
+ int32_t curpos = GetCurrentPosition(scrollbar);
+ int32_t minpos = GetMinPosition(scrollbar);
+ int32_t maxpos = GetMaxPosition(scrollbar);
+
+ // get the new position and make sure it is in bounds
+ int32_t newpos = curpos + change * pageIncrement;
+ if (newpos < minpos || maxpos < minpos)
+ newpos = minpos;
+ else if (newpos > maxpos)
+ newpos = maxpos;
+
+ SetCurrentPositionInternal(scrollbar, newpos, true);
+}
+
+// called when the current position changed and we need to update the thumb's
+// location
+void nsSliderFrame::CurrentPositionChanged() {
+ nsScrollbarFrame* scrollbarBox = Scrollbar();
+ nsCOMPtr<nsIContent> scrollbar = scrollbarBox->GetContent();
+
+ // get the current position
+ int32_t curPos = GetCurrentPosition(scrollbar);
+
+ // do nothing if the position did not change
+ if (mCurPos == curPos) {
+ return;
+ }
+
+ // get our current min and max position from our content node
+ int32_t minPos = GetMinPosition(scrollbar);
+ int32_t maxPos = GetMaxPosition(scrollbar);
+
+ maxPos = std::max(minPos, maxPos);
+ curPos = clamped(curPos, minPos, maxPos);
+
+ // get the thumb's rect
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return;
+ }
+
+ bool reverse = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::reverse, eCaseMatters);
+ nscoord pos = reverse ? (maxPos - curPos) : (curPos - minPos);
+ const bool horizontal = Scrollbar()->IsHorizontal();
+
+ // figure out the new rect
+ nsRect thumbRect = thumbFrame->GetRect();
+ nsRect newThumbRect(thumbRect);
+ if (horizontal) {
+ newThumbRect.x = NSToCoordRound(pos * mRatio);
+ } else {
+ newThumbRect.y = NSToCoordRound(pos * mRatio);
+ }
+
+ // avoid putting the scroll thumb at subpixel positions which cause needless
+ // invalidations
+ nscoord appUnitsPerPixel = PresContext()->AppUnitsPerDevPixel();
+ nsPoint snappedThumbLocation =
+ ToAppUnits(newThumbRect.TopLeft().ToNearestPixels(appUnitsPerPixel),
+ appUnitsPerPixel);
+ if (horizontal) {
+ newThumbRect.x = snappedThumbLocation.x;
+ } else {
+ newThumbRect.y = snappedThumbLocation.y;
+ }
+
+ // set the rect
+ // XXX This out-of-band update of the frame tree is rather fishy!
+ thumbFrame->SetRect(newThumbRect);
+
+ // When the thumb changes position, the mThumbStart value stored in
+ // ScrollbarData for the purpose of telling APZ about the thumb
+ // position painted by the main thread is invalidated. The ScrollbarData
+ // is stored on the nsDisplayOwnLayer item built by *this* frame, so
+ // we need to mark this frame as needing its fisplay item rebuilt.
+ MarkNeedsDisplayItemRebuild();
+
+ // Request a repaint of the scrollbar
+ nsIScrollbarMediator* mediator = scrollbarBox->GetScrollbarMediator();
+ if (!mediator || !mediator->ShouldSuppressScrollbarRepaints()) {
+ SchedulePaint();
+ }
+
+ mCurPos = curPos;
+}
+
+static void UpdateAttribute(dom::Element* aScrollbar, nscoord aNewPos,
+ bool aNotify, bool aIsSmooth) {
+ nsAutoString str;
+ str.AppendInt(aNewPos);
+
+ if (aIsSmooth) {
+ aScrollbar->SetAttr(kNameSpaceID_None, nsGkAtoms::smooth, u"true"_ns,
+ false);
+ }
+ aScrollbar->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, str, aNotify);
+ if (aIsSmooth) {
+ aScrollbar->UnsetAttr(kNameSpaceID_None, nsGkAtoms::smooth, false);
+ }
+}
+
+// Use this function when you want to set the scroll position via the position
+// of the scrollbar thumb, e.g. when dragging the slider. This function scrolls
+// the content in such a way that thumbRect.x/.y becomes aNewThumbPos.
+void nsSliderFrame::SetCurrentThumbPosition(nsIContent* aScrollbar,
+ nscoord aNewThumbPos,
+ bool aIsSmooth, bool aMaySnap) {
+ int32_t newPos = NSToIntRound(aNewThumbPos / mRatio);
+ if (aMaySnap &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::snap,
+ nsGkAtoms::_true, eCaseMatters)) {
+ // If snap="true", then the slider may only be set to min + (increment * x).
+ // Otherwise, the slider may be set to any positive integer.
+ int32_t increment = GetIncrement(aScrollbar);
+ newPos = NSToIntRound(newPos / float(increment)) * increment;
+ }
+
+ SetCurrentPosition(aScrollbar, newPos, aIsSmooth);
+}
+
+// Use this function when you know the target scroll position of the scrolled
+// content. aNewPos should be passed to this function as a position as if the
+// minpos is 0. That is, the minpos will be added to the position by this
+// function. In a reverse direction slider, the newpos should be the distance
+// from the end.
+void nsSliderFrame::SetCurrentPosition(nsIContent* aScrollbar, int32_t aNewPos,
+ bool aIsSmooth) {
+ // get min and max position from our content node
+ int32_t minpos = GetMinPosition(aScrollbar);
+ int32_t maxpos = GetMaxPosition(aScrollbar);
+
+ // in reverse direction sliders, flip the value so that it goes from
+ // right to left, or bottom to top.
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir,
+ nsGkAtoms::reverse, eCaseMatters)) {
+ aNewPos = maxpos - aNewPos;
+ } else {
+ aNewPos += minpos;
+ }
+
+ // get the new position and make sure it is in bounds
+ if (aNewPos < minpos || maxpos < minpos) {
+ aNewPos = minpos;
+ } else if (aNewPos > maxpos) {
+ aNewPos = maxpos;
+ }
+
+ SetCurrentPositionInternal(aScrollbar, aNewPos, aIsSmooth);
+}
+
+void nsSliderFrame::SetCurrentPositionInternal(nsIContent* aScrollbar,
+ int32_t aNewPos,
+ bool aIsSmooth) {
+ nsCOMPtr<nsIContent> scrollbar = aScrollbar;
+ nsScrollbarFrame* scrollbarBox = Scrollbar();
+ AutoWeakFrame weakFrame(this);
+
+ mUserChanged = true;
+
+ // See if we have a mediator.
+ if (nsIScrollbarMediator* mediator = scrollbarBox->GetScrollbarMediator()) {
+ nscoord oldPos =
+ nsPresContext::CSSPixelsToAppUnits(GetCurrentPosition(scrollbar));
+ nscoord newPos = nsPresContext::CSSPixelsToAppUnits(aNewPos);
+ mediator->ThumbMoved(scrollbarBox, oldPos, newPos);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ UpdateAttribute(scrollbar->AsElement(), aNewPos, /* aNotify */ false,
+ aIsSmooth);
+ CurrentPositionChanged();
+ mUserChanged = false;
+ return;
+ }
+
+ UpdateAttribute(scrollbar->AsElement(), aNewPos, true, aIsSmooth);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ mUserChanged = false;
+
+#ifdef DEBUG_SLIDER
+ printf("Current Pos=%d\n", aNewPos);
+#endif
+}
+
+void nsSliderFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
+ if (aListID == FrameChildListID::Principal) {
+ AddListener();
+ }
+}
+
+nsresult nsSliderMediator::HandleEvent(dom::Event* aEvent) {
+ // Only process the event if the thumb is not being dragged.
+ if (mSlider && !mSlider->isDraggingThumb()) return mSlider->StartDrag(aEvent);
+
+ return NS_OK;
+}
+
+static bool ScrollFrameWillBuildScrollInfoLayer(nsIFrame* aScrollFrame) {
+ /*
+ * Note: if changing the conditions in this function, make a corresponding
+ * change to nsDisplayListBuilder::ShouldBuildScrollInfoItemsForHoisting()
+ * in nsDisplayList.cpp.
+ */
+ nsIFrame* current = aScrollFrame;
+ while (current) {
+ if (SVGIntegrationUtils::UsesSVGEffectsNotSupportedInCompositor(current)) {
+ return true;
+ }
+ current = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(current);
+ }
+ return false;
+}
+
+nsIScrollableFrame* nsSliderFrame::GetScrollFrame() {
+ return do_QueryFrame(Scrollbar()->GetParent());
+}
+
+void nsSliderFrame::StartAPZDrag(WidgetGUIEvent* aEvent) {
+ if (!aEvent->mFlags.mHandledByAPZ) {
+ return;
+ }
+
+ if (!gfxPlatform::GetPlatform()->SupportsApzDragInput()) {
+ return;
+ }
+
+ nsIFrame* scrollbarBox = Scrollbar();
+ nsContainerFrame* scrollFrame = scrollbarBox->GetParent();
+ if (!scrollFrame) {
+ return;
+ }
+
+ nsIContent* scrollableContent = scrollFrame->GetContent();
+ if (!scrollableContent) {
+ return;
+ }
+
+ // APZ dragging requires the scrollbar to be layerized, which doesn't
+ // happen for scroll info layers.
+ if (ScrollFrameWillBuildScrollInfoLayer(scrollFrame)) {
+ return;
+ }
+
+ // Custom scrollbar mediators are not supported in the APZ codepath.
+ if (UsesCustomScrollbarMediator(scrollbarBox)) {
+ return;
+ }
+
+ bool isHorizontal = Scrollbar()->IsHorizontal();
+
+ layers::ScrollableLayerGuid::ViewID scrollTargetId;
+ bool hasID = nsLayoutUtils::FindIDFor(scrollableContent, &scrollTargetId);
+ bool hasAPZView =
+ hasID && scrollTargetId != layers::ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ if (!hasAPZView) {
+ return;
+ }
+
+ if (!DisplayPortUtils::HasNonMinimalDisplayPort(scrollableContent)) {
+ return;
+ }
+
+ auto* presShell = PresShell();
+ uint64_t inputblockId = InputAPZContext::GetInputBlockId();
+ uint32_t presShellId = presShell->GetPresShellId();
+ AsyncDragMetrics dragMetrics(
+ scrollTargetId, presShellId, inputblockId,
+ OuterCSSPixel::FromAppUnits(mDragStart),
+ isHorizontal ? ScrollDirection::eHorizontal : ScrollDirection::eVertical);
+
+ // It's important to set this before calling
+ // nsIWidget::StartAsyncScrollbarDrag(), because in some configurations, that
+ // can call AsyncScrollbarDragRejected() synchronously, which clears the flag
+ // (and we want it to stay cleared in that case).
+ mScrollingWithAPZ = true;
+
+ // When we start an APZ drag, we wont get mouse events for the drag.
+ // APZ will consume them all and only notify us of the new scroll position.
+ bool waitForRefresh = InputAPZContext::HavePendingLayerization();
+ nsIWidget* widget = this->GetNearestWidget();
+ if (waitForRefresh) {
+ waitForRefresh = false;
+ if (nsPresContext* presContext = presShell->GetPresContext()) {
+ presContext->RegisterManagedPostRefreshObserver(
+ new ManagedPostRefreshObserver(
+ presContext, [widget = RefPtr<nsIWidget>(widget),
+ dragMetrics](bool aWasCanceled) {
+ if (!aWasCanceled) {
+ widget->StartAsyncScrollbarDrag(dragMetrics);
+ }
+ return ManagedPostRefreshObserver::Unregister::Yes;
+ }));
+ waitForRefresh = true;
+ }
+ }
+ if (!waitForRefresh) {
+ widget->StartAsyncScrollbarDrag(dragMetrics);
+ }
+}
+
+nsresult nsSliderFrame::StartDrag(Event* aEvent) {
+#ifdef DEBUG_SLIDER
+ printf("Begin dragging\n");
+#endif
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ WidgetGUIEvent* event = aEvent->WidgetEventPtr()->AsGUIEvent();
+
+ if (!ShouldScrollForEvent(event)) {
+ return NS_OK;
+ }
+
+ nsPoint pt;
+ if (!GetEventPoint(event, pt)) {
+ return NS_OK;
+ }
+ bool isHorizontal = Scrollbar()->IsHorizontal();
+ nscoord pos = isHorizontal ? pt.x : pt.y;
+
+ // If we should scroll-to-click, first place the middle of the slider thumb
+ // under the mouse.
+ nsCOMPtr<nsIContent> scrollbar;
+ nscoord newpos = pos;
+ bool scrollToClick = ShouldScrollToClickForEvent(event);
+ if (scrollToClick) {
+ // adjust so that the middle of the thumb is placed under the click
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+ nsSize thumbSize = thumbFrame->GetSize();
+ nscoord thumbLength = isHorizontal ? thumbSize.width : thumbSize.height;
+
+ newpos -= (thumbLength / 2);
+
+ scrollbar = Scrollbar()->GetContent();
+ }
+
+ DragThumb(true);
+
+ if (scrollToClick) {
+ // should aMaySnap be true here?
+ SetCurrentThumbPosition(scrollbar, newpos, false, false);
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+
+#ifdef MOZ_WIDGET_GTK
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->SetAttr(kNameSpaceID_None, nsGkAtoms::active, u"true"_ns, true);
+#endif
+
+ if (isHorizontal)
+ mThumbStart = thumbFrame->GetPosition().x;
+ else
+ mThumbStart = thumbFrame->GetPosition().y;
+
+ mDragStart = pos - mThumbStart;
+
+ mScrollingWithAPZ = false;
+ StartAPZDrag(event); // sets mScrollingWithAPZ=true if appropriate
+
+#ifdef DEBUG_SLIDER
+ printf("Pressed mDragStart=%d\n", mDragStart);
+#endif
+
+ if (!mScrollingWithAPZ) {
+ SuppressDisplayport();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsSliderFrame::StopDrag() {
+ AddListener();
+ DragThumb(false);
+
+ mScrollingWithAPZ = false;
+
+ UnsuppressDisplayport();
+
+#ifdef MOZ_WIDGET_GTK
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (thumbFrame) {
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->UnsetAttr(kNameSpaceID_None, nsGkAtoms::active, true);
+ }
+#endif
+
+ if (mChange) {
+ StopRepeat();
+ mChange = 0;
+ }
+ return NS_OK;
+}
+
+void nsSliderFrame::DragThumb(bool aGrabMouseEvents) {
+ mDragFinished = !aGrabMouseEvents;
+
+ if (aGrabMouseEvents) {
+ PresShell::SetCapturingContent(
+ GetContent(),
+ CaptureFlags::IgnoreAllowedState | CaptureFlags::PreventDragStart);
+ } else {
+ PresShell::ReleaseCapturingContent();
+ }
+}
+
+bool nsSliderFrame::isDraggingThumb() const {
+ return PresShell::GetCapturingContent() == GetContent();
+}
+
+void nsSliderFrame::AddListener() {
+ if (!mMediator) {
+ mMediator = new nsSliderMediator(this);
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return;
+ }
+ thumbFrame->GetContent()->AddSystemEventListener(u"mousedown"_ns, mMediator,
+ false, false);
+ thumbFrame->GetContent()->AddSystemEventListener(u"touchstart"_ns, mMediator,
+ false, false);
+}
+
+void nsSliderFrame::RemoveListener() {
+ NS_ASSERTION(mMediator, "No listener was ever added!!");
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) return;
+
+ thumbFrame->GetContent()->RemoveSystemEventListener(u"mousedown"_ns,
+ mMediator, false);
+ thumbFrame->GetContent()->RemoveSystemEventListener(u"touchstart"_ns,
+ mMediator, false);
+}
+
+bool nsSliderFrame::ShouldScrollForEvent(WidgetGUIEvent* aEvent) {
+ switch (aEvent->mMessage) {
+ case eTouchStart:
+ case eTouchEnd:
+ return true;
+ case eMouseDown:
+ case eMouseUp: {
+ uint16_t button = aEvent->AsMouseEvent()->mButton;
+#ifdef MOZ_WIDGET_GTK
+ return (button == MouseButton::ePrimary) ||
+ (button == MouseButton::eSecondary && GetScrollToClick()) ||
+ (button == MouseButton::eMiddle && gMiddlePref &&
+ !GetScrollToClick());
+#else
+ return (button == MouseButton::ePrimary) ||
+ (button == MouseButton::eMiddle && gMiddlePref);
+#endif
+ }
+ default:
+ return false;
+ }
+}
+
+bool nsSliderFrame::ShouldScrollToClickForEvent(WidgetGUIEvent* aEvent) {
+ if (!ShouldScrollForEvent(aEvent)) {
+ return false;
+ }
+
+ if (aEvent->mMessage != eMouseDown && aEvent->mMessage != eTouchStart) {
+ return false;
+ }
+
+#if defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
+ // On Mac and Linux, clicking the scrollbar thumb should never scroll to
+ // click.
+ if (IsEventOverThumb(aEvent)) {
+ return false;
+ }
+#endif
+
+ if (aEvent->mMessage == eTouchStart) {
+ return GetScrollToClick();
+ }
+
+ WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
+ if (mouseEvent->mButton == MouseButton::ePrimary) {
+#ifdef XP_MACOSX
+ bool invertPref = mouseEvent->IsAlt();
+#else
+ bool invertPref = mouseEvent->IsShift();
+#endif
+ return GetScrollToClick() != invertPref;
+ }
+
+#ifdef MOZ_WIDGET_GTK
+ if (mouseEvent->mButton == MouseButton::eSecondary) {
+ return !GetScrollToClick();
+ }
+#endif
+
+ return true;
+}
+
+bool nsSliderFrame::IsEventOverThumb(WidgetGUIEvent* aEvent) {
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return false;
+ }
+
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return false;
+ }
+
+ const nsRect thumbRect = thumbFrame->GetRect();
+ const bool isHorizontal = Scrollbar()->IsHorizontal();
+ nscoord eventPos = isHorizontal ? eventPoint.x : eventPoint.y;
+ nscoord thumbStart = isHorizontal ? thumbRect.x : thumbRect.y;
+ nscoord thumbEnd = isHorizontal ? thumbRect.XMost() : thumbRect.YMost();
+ return eventPos >= thumbStart && eventPos < thumbEnd;
+}
+
+NS_IMETHODIMP
+nsSliderFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ if (!ShouldScrollForEvent(aEvent) || ShouldScrollToClickForEvent(aEvent)) {
+ return NS_OK;
+ }
+
+ if (IsEventOverThumb(aEvent)) {
+ return NS_OK;
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) // display:none?
+ return NS_OK;
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ nsRect thumbRect = thumbFrame->GetRect();
+
+ nscoord change = 1;
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return NS_OK;
+ }
+
+ if (Scrollbar()->IsHorizontal() ? eventPoint.x < thumbRect.x
+ : eventPoint.y < thumbRect.y) {
+ change = -1;
+ }
+
+ mChange = change;
+ DragThumb(true);
+ // On Linux we want to keep scrolling in the direction indicated by |change|
+ // until the mouse is released. On the other platforms we want to stop
+ // scrolling as soon as the scrollbar thumb has reached the current mouse
+ // position.
+#ifdef MOZ_WIDGET_GTK
+ // Set the destination point to the very end of the scrollbar so that
+ // scrolling doesn't stop halfway through.
+ if (change > 0) {
+ mDestinationPoint = nsPoint(GetRect().width, GetRect().height);
+ } else {
+ mDestinationPoint = nsPoint(0, 0);
+ }
+#else
+ mDestinationPoint = eventPoint;
+#endif
+ StartRepeat();
+ PageScroll(change);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSliderFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ StopRepeat();
+
+ nsScrollbarFrame* sb = Scrollbar();
+ if (nsIScrollbarMediator* m = sb->GetScrollbarMediator()) {
+ m->ScrollbarReleased(sb);
+ }
+ return NS_OK;
+}
+
+void nsSliderFrame::Destroy(DestroyContext& aContext) {
+ // tell our mediator if we have one we are gone.
+ if (mMediator) {
+ mMediator->SetSlider(nullptr);
+ mMediator = nullptr;
+ }
+ StopRepeat();
+
+ // call base class Destroy()
+ nsContainerFrame::Destroy(aContext);
+}
+
+void nsSliderFrame::Notify() {
+ bool stop = false;
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ StopRepeat();
+ return;
+ }
+ nsRect thumbRect = thumbFrame->GetRect();
+
+ const bool isHorizontal = Scrollbar()->IsHorizontal();
+
+ // See if the thumb has moved past our destination point.
+ // if it has we want to stop.
+ if (isHorizontal) {
+ if (mChange < 0) {
+ if (thumbRect.x < mDestinationPoint.x) stop = true;
+ } else {
+ if (thumbRect.x + thumbRect.width > mDestinationPoint.x) stop = true;
+ }
+ } else {
+ if (mChange < 0) {
+ if (thumbRect.y < mDestinationPoint.y) stop = true;
+ } else {
+ if (thumbRect.y + thumbRect.height > mDestinationPoint.y) stop = true;
+ }
+ }
+
+ if (stop) {
+ StopRepeat();
+ } else {
+ PageScroll(mChange);
+ }
+}
+
+void nsSliderFrame::PageScroll(nscoord aChange) {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir,
+ nsGkAtoms::reverse, eCaseMatters)) {
+ aChange = -aChange;
+ }
+ nsScrollbarFrame* sb = Scrollbar();
+ sb->SetIncrementToPage(aChange);
+ if (nsIScrollbarMediator* m = sb->GetScrollbarMediator()) {
+ m->ScrollByPage(sb, aChange,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition);
+ return;
+ }
+ PageUpDown(aChange);
+}
+
+float nsSliderFrame::GetThumbRatio() const {
+ // mRatio is in thumb app units per scrolled css pixels. Convert it to a
+ // ratio of the thumb's CSS pixels per scrolled CSS pixels. (Note the thumb
+ // is in the scrollframe's parent's space whereas the scrolled CSS pixels
+ // are in the scrollframe's space).
+ return mRatio / AppUnitsPerCSSPixel();
+}
+
+void nsSliderFrame::AsyncScrollbarDragInitiated(uint64_t aDragBlockId) {
+ mAPZDragInitiated = Some(aDragBlockId);
+}
+
+void nsSliderFrame::AsyncScrollbarDragRejected() {
+ mScrollingWithAPZ = false;
+ // Only suppress the displayport if we're still dragging the thumb.
+ // Otherwise, no one will unsuppress it.
+ if (isDraggingThumb()) {
+ SuppressDisplayport();
+ }
+}
+
+void nsSliderFrame::SuppressDisplayport() {
+ if (!mSuppressionActive) {
+ PresShell()->SuppressDisplayport(true);
+ mSuppressionActive = true;
+ }
+}
+
+void nsSliderFrame::UnsuppressDisplayport() {
+ if (mSuppressionActive) {
+ PresShell()->SuppressDisplayport(false);
+ mSuppressionActive = false;
+ }
+}
+
+bool nsSliderFrame::OnlySystemGroupDispatch(EventMessage aMessage) const {
+ // If we are in a native anonymous subtree, do not dispatch mouse-move or
+ // pointer-move events targeted at this slider frame to web content. This
+ // matches the behaviour of other browsers.
+ return (aMessage == eMouseMove || aMessage == ePointerMove) &&
+ isDraggingThumb() && GetContent()->IsInNativeAnonymousSubtree();
+}
+
+bool nsSliderFrame::GetEventPoint(WidgetGUIEvent* aEvent, nsPoint& aPoint) {
+ LayoutDeviceIntPoint refPoint;
+ if (!GetEventPoint(aEvent, refPoint)) {
+ return false;
+ }
+ aPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, refPoint,
+ RelativeTo{this});
+ return true;
+}
+
+bool nsSliderFrame::GetEventPoint(WidgetGUIEvent* aEvent,
+ LayoutDeviceIntPoint& aPoint) {
+ NS_ENSURE_TRUE(aEvent, false);
+ WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
+ if (touchEvent) {
+ // return false if there is more than one touch on the page, or if
+ // we can't find a touch point
+ if (touchEvent->mTouches.Length() != 1) {
+ return false;
+ }
+
+ dom::Touch* touch = touchEvent->mTouches.SafeElementAt(0);
+ if (!touch) {
+ return false;
+ }
+ aPoint = touch->mRefPoint;
+ } else {
+ aPoint = aEvent->mRefPoint;
+ }
+ return true;
+}
+
+NS_IMPL_ISUPPORTS(nsSliderMediator, nsIDOMEventListener)
diff --git a/layout/xul/nsSliderFrame.h b/layout/xul/nsSliderFrame.h
new file mode 100644
index 0000000000..659e434886
--- /dev/null
+++ b/layout/xul/nsSliderFrame.h
@@ -0,0 +1,235 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSliderFrame_h__
+#define nsSliderFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsRepeatService.h"
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsIDOMEventListener.h"
+
+class nsITimer;
+class nsScrollbarFrame;
+class nsSliderFrame;
+
+namespace mozilla {
+class nsDisplaySliderMarks;
+class PresShell;
+} // namespace mozilla
+
+nsIFrame* NS_NewSliderFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsSliderMediator final : public nsIDOMEventListener {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsSliderFrame* mSlider;
+
+ explicit nsSliderMediator(nsSliderFrame* aSlider) { mSlider = aSlider; }
+
+ void SetSlider(nsSliderFrame* aSlider) { mSlider = aSlider; }
+
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ protected:
+ virtual ~nsSliderMediator() = default;
+};
+
+class nsSliderFrame final : public nsContainerFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsSliderFrame)
+ NS_DECL_QUERYFRAME
+
+ friend class nsSliderMediator;
+ friend class mozilla::nsDisplaySliderMarks;
+
+ explicit nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual ~nsSliderFrame();
+
+ // Get the point associated with this event. Returns true if a single valid
+ // point was found. Otherwise false.
+ bool GetEventPoint(mozilla::WidgetGUIEvent* aEvent, nsPoint& aPoint);
+ // Gets the event coordinates relative to the widget associated with this
+ // frame. Return true if a single valid point was found.
+ bool GetEventPoint(mozilla::WidgetGUIEvent* aEvent,
+ mozilla::LayoutDeviceIntPoint& aPoint);
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SliderFrame"_ns, aResult);
+ }
+#endif
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ // nsIFrame overrides
+ void Destroy(DestroyContext&) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ void BuildDisplayListForThumb(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aThumb,
+ const nsDisplayListSet& aLists);
+
+ nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ // nsContainerFrame overrides
+ void SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) override;
+ void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override;
+ void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) override;
+ void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override;
+
+ nsresult StartDrag(mozilla::dom::Event* aEvent);
+ nsresult StopDrag();
+
+ void StartAPZDrag(mozilla::WidgetGUIEvent* aEvent);
+
+ static int32_t GetCurrentPosition(nsIContent* content);
+ static int32_t GetMinPosition(nsIContent* content);
+ static int32_t GetMaxPosition(nsIContent* content);
+ static int32_t GetIncrement(nsIContent* content);
+ static int32_t GetPageIncrement(nsIContent* content);
+ static int32_t GetIntegerAttribute(nsIContent* content, nsAtom* atom,
+ int32_t defaultValue);
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override {
+ return NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override {
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ // Return the ratio the scrollbar thumb should move in proportion to the
+ // scrolled frame.
+ float GetThumbRatio() const;
+
+ // Notify the slider frame that an async scrollbar drag was started on the
+ // APZ side without consulting the main thread. The block id is the APZ
+ // input block id of the mousedown that started the drag.
+ void AsyncScrollbarDragInitiated(uint64_t aDragBlockId);
+
+ // Notify the slider frame that an async scrollbar drag requested in
+ // StartAPZDrag() was rejected by APZ, and the slider frame should
+ // fall back to main-thread dragging.
+ void AsyncScrollbarDragRejected();
+
+ bool OnlySystemGroupDispatch(mozilla::EventMessage aMessage) const override;
+
+ // Returns the associated scrollframe that contains this slider if any.
+ nsIScrollableFrame* GetScrollFrame();
+
+ private:
+ bool GetScrollToClick();
+ nsScrollbarFrame* Scrollbar();
+ bool ShouldScrollForEvent(mozilla::WidgetGUIEvent* aEvent);
+ bool ShouldScrollToClickForEvent(mozilla::WidgetGUIEvent* aEvent);
+ bool IsEventOverThumb(mozilla::WidgetGUIEvent* aEvent);
+
+ void PageUpDown(nscoord change);
+ void SetCurrentThumbPosition(nsIContent* aScrollbar, nscoord aNewPos,
+ bool aIsSmooth, bool aMaySnap);
+ void SetCurrentPosition(nsIContent* aScrollbar, int32_t aNewPos,
+ bool aIsSmooth);
+ void SetCurrentPositionInternal(nsIContent* aScrollbar, int32_t pos,
+ bool aIsSmooth);
+ void CurrentPositionChanged();
+
+ void DragThumb(bool aGrabMouseEvents);
+ void AddListener();
+ void RemoveListener();
+ bool isDraggingThumb() const;
+
+ void SuppressDisplayport();
+ void UnsuppressDisplayport();
+
+ void StartRepeat() {
+ nsRepeatService::GetInstance()->Start(Notify, this, mContent->OwnerDoc(),
+ "nsSliderFrame"_ns);
+ }
+ void StopRepeat() { nsRepeatService::GetInstance()->Stop(Notify, this); }
+ void Notify();
+ static void Notify(void* aData) {
+ (static_cast<nsSliderFrame*>(aData))->Notify();
+ }
+ void PageScroll(nscoord aChange);
+
+ nsPoint mDestinationPoint;
+ RefPtr<nsSliderMediator> mMediator;
+
+ float mRatio;
+
+ nscoord mDragStart;
+ nscoord mThumbStart;
+
+ int32_t mCurPos;
+
+ nscoord mChange;
+
+ bool mDragFinished;
+
+ // true if an attribute change has been caused by the user manipulating the
+ // slider. This allows notifications to tell how a slider's current position
+ // was changed.
+ bool mUserChanged;
+
+ // true if we've handed off the scrolling to APZ. This means that we should
+ // ignore scrolling events as the position will be updated by APZ. If we were
+ // to process these events then the scroll position update would conflict
+ // causing the scroll position to jump.
+ bool mScrollingWithAPZ;
+
+ // true if displayport suppression is active, for more performant
+ // scrollbar-dragging behaviour.
+ bool mSuppressionActive;
+
+ // If APZ initiated a scrollbar drag without main-thread involvement, it
+ // notifies us and this variable stores the input block id of the APZ input
+ // block that started the drag. This lets us handle the corresponding
+ // mousedown event properly, if it arrives after the scroll position has
+ // been shifted due to async scrollbar drag.
+ Maybe<uint64_t> mAPZDragInitiated;
+
+ nscoord mThumbMinLength;
+
+ static bool gMiddlePref;
+ static int32_t gSnapMultiplier;
+}; // class nsSliderFrame
+
+#endif
diff --git a/layout/xul/nsSplitterFrame.cpp b/layout/xul/nsSplitterFrame.cpp
new file mode 100644
index 0000000000..89d3ac1c25
--- /dev/null
+++ b/layout/xul/nsSplitterFrame.cpp
@@ -0,0 +1,964 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "LayoutConstants.h"
+#include "SimpleXULLeafFrame.h"
+#include "gfxContext.h"
+#include "mozilla/ReflowInput.h"
+#include "nsSplitterFrame.h"
+#include "nsGkAtoms.h"
+#include "nsXULElement.h"
+#include "nsPresContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsNameSpaceManager.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsIDOMEventListener.h"
+#include "nsICSSDeclaration.h"
+#include "nsFrameList.h"
+#include "nsHTMLParts.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "nsContainerFrame.h"
+#include "nsContentCID.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsContentUtils.h"
+#include "nsFlexContainerFrame.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/UniquePtr.h"
+#include "nsStyledElement.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+using mozilla::dom::Event;
+
+class nsSplitterInfo {
+ public:
+ nscoord min;
+ nscoord max;
+ nscoord current;
+ nscoord pref;
+ nscoord changed;
+ nsCOMPtr<nsIContent> childElem;
+};
+
+enum class ResizeType {
+ // Resize the closest sibling in a given direction.
+ Closest,
+ // Resize the farthest sibling in a given direction.
+ Farthest,
+ // Resize only flexible siblings in a given direction.
+ Flex,
+ // No space should be taken out of any children in that direction.
+ // FIXME(emilio): This is a rather odd name...
+ Grow,
+ // Only resize adjacent siblings.
+ Sibling,
+ // Don't resize anything in a given direction.
+ None,
+};
+static ResizeType ResizeTypeFromAttribute(const Element& aElement,
+ nsAtom* aAttribute) {
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::farthest, nsGkAtoms::flex, nsGkAtoms::grow,
+ nsGkAtoms::sibling, nsGkAtoms::none, nullptr};
+ switch (aElement.FindAttrValueIn(kNameSpaceID_None, aAttribute, strings,
+ eCaseMatters)) {
+ case 0:
+ return ResizeType::Farthest;
+ case 1:
+ return ResizeType::Flex;
+ case 2:
+ // Grow only applies to resizeAfter.
+ if (aAttribute == nsGkAtoms::resizeafter) {
+ return ResizeType::Grow;
+ }
+ break;
+ case 3:
+ return ResizeType::Sibling;
+ case 4:
+ return ResizeType::None;
+ default:
+ break;
+ }
+ return ResizeType::Closest;
+}
+
+class nsSplitterFrameInner final : public nsIDOMEventListener {
+ protected:
+ virtual ~nsSplitterFrameInner();
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ explicit nsSplitterFrameInner(nsSplitterFrame* aSplitter)
+ : mOuter(aSplitter) {}
+
+ void Disconnect() { mOuter = nullptr; }
+
+ nsresult MouseDown(Event* aMouseEvent);
+ nsresult MouseUp(Event* aMouseEvent);
+ nsresult MouseMove(Event* aMouseEvent);
+
+ void MouseDrag(nsPresContext* aPresContext, WidgetGUIEvent* aEvent);
+ void MouseUp(nsPresContext* aPresContext, WidgetGUIEvent* aEvent);
+
+ void AdjustChildren(nsPresContext* aPresContext);
+ void AdjustChildren(nsPresContext* aPresContext,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ bool aIsHorizontal);
+
+ void AddRemoveSpace(nscoord aDiff, nsTArray<nsSplitterInfo>& aChildInfos,
+ int32_t& aSpaceLeft);
+
+ void ResizeChildTo(nscoord& aDiff);
+
+ void UpdateState();
+
+ void AddListener();
+ void RemoveListener();
+
+ enum class State { Open, CollapsedBefore, CollapsedAfter, Dragging };
+ enum CollapseDirection { Before, After };
+
+ ResizeType GetResizeBefore();
+ ResizeType GetResizeAfter();
+ State GetState();
+
+ bool SupportsCollapseDirection(CollapseDirection aDirection);
+
+ void EnsureOrient();
+ void SetPreferredSize(nsIFrame* aChildBox, bool aIsHorizontal, nscoord aSize);
+
+ nsSplitterFrame* mOuter;
+ bool mDidDrag = false;
+ nscoord mDragStart = 0;
+ nsIFrame* mParentBox = nullptr;
+ bool mPressed = false;
+ nsTArray<nsSplitterInfo> mChildInfosBefore;
+ nsTArray<nsSplitterInfo> mChildInfosAfter;
+ State mState = State::Open;
+ nscoord mSplitterPos = 0;
+ bool mDragging = false;
+
+ const Element* SplitterElement() const {
+ return mOuter->GetContent()->AsElement();
+ }
+};
+
+NS_IMPL_ISUPPORTS(nsSplitterFrameInner, nsIDOMEventListener)
+
+ResizeType nsSplitterFrameInner::GetResizeBefore() {
+ return ResizeTypeFromAttribute(*SplitterElement(), nsGkAtoms::resizebefore);
+}
+
+ResizeType nsSplitterFrameInner::GetResizeAfter() {
+ return ResizeTypeFromAttribute(*SplitterElement(), nsGkAtoms::resizeafter);
+}
+
+nsSplitterFrameInner::~nsSplitterFrameInner() = default;
+
+nsSplitterFrameInner::State nsSplitterFrameInner::GetState() {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::dragging,
+ nsGkAtoms::collapsed, nullptr};
+ static Element::AttrValuesArray strings_substate[] = {
+ nsGkAtoms::before, nsGkAtoms::after, nullptr};
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::state, strings, eCaseMatters)) {
+ case 0:
+ return State::Dragging;
+ case 1:
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::substate, strings_substate,
+ eCaseMatters)) {
+ case 0:
+ return State::CollapsedBefore;
+ case 1:
+ return State::CollapsedAfter;
+ default:
+ if (SupportsCollapseDirection(After)) {
+ return State::CollapsedAfter;
+ }
+ return State::CollapsedBefore;
+ }
+ }
+ return State::Open;
+}
+
+//
+// NS_NewSplitterFrame
+//
+// Creates a new Toolbar frame and returns it
+//
+nsIFrame* NS_NewSplitterFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsSplitterFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSplitterFrame)
+
+nsSplitterFrame::nsSplitterFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : SimpleXULLeafFrame(aStyle, aPresContext, kClassID) {}
+
+void nsSplitterFrame::Destroy(DestroyContext& aContext) {
+ if (mInner) {
+ mInner->RemoveListener();
+ mInner->Disconnect();
+ mInner = nullptr;
+ }
+ SimpleXULLeafFrame::Destroy(aContext);
+}
+
+nsresult nsSplitterFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ SimpleXULLeafFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::state) {
+ mInner->UpdateState();
+ }
+
+ return rv;
+}
+
+/**
+ * Initialize us. If we are in a box get our alignment so we know what direction
+ * we are
+ */
+void nsSplitterFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ MOZ_ASSERT(!mInner);
+ mInner = new nsSplitterFrameInner(this);
+
+ SimpleXULLeafFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mInner->AddListener();
+ mInner->mParentBox = nullptr;
+}
+
+static bool IsValidParentBox(nsIFrame* aFrame) {
+ return aFrame->IsFlexContainerFrame();
+}
+
+static nsIFrame* GetValidParentBox(nsIFrame* aChild) {
+ return aChild->GetParent() && IsValidParentBox(aChild->GetParent())
+ ? aChild->GetParent()
+ : nullptr;
+}
+
+void nsSplitterFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
+ mInner->mParentBox = GetValidParentBox(this);
+ mInner->UpdateState();
+ }
+ return SimpleXULLeafFrame::Reflow(aPresContext, aDesiredSize, aReflowInput,
+ aStatus);
+}
+
+static bool SplitterIsHorizontal(const nsIFrame* aParentBox) {
+ // If our parent is horizontal, the splitter is vertical and vice-versa.
+ MOZ_ASSERT(aParentBox->IsFlexContainerFrame());
+ const FlexboxAxisInfo info(aParentBox);
+ return !info.mIsRowOriented;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandlePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleMultiplePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleDrag(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+void nsSplitterFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ SimpleXULLeafFrame::BuildDisplayList(aBuilder, aLists);
+
+ // if the mouse is captured always return us as the frame.
+ if (mInner->mDragging && aBuilder->IsForEventDelivery()) {
+ // XXX It's probably better not to check visibility here, right?
+ aLists.Outlines()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this);
+ return;
+ }
+}
+
+nsresult nsSplitterFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ AutoWeakFrame weakFrame(this);
+ RefPtr<nsSplitterFrameInner> inner(mInner);
+ switch (aEvent->mMessage) {
+ case eMouseMove:
+ inner->MouseDrag(aPresContext, aEvent);
+ break;
+
+ case eMouseUp:
+ if (aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) {
+ inner->MouseUp(aPresContext, aEvent);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ return SimpleXULLeafFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+void nsSplitterFrameInner::MouseUp(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent) {
+ if (mDragging && mOuter) {
+ AdjustChildren(aPresContext);
+ AddListener();
+ PresShell::ReleaseCapturingContent(); // XXXndeakin is this needed?
+ mDragging = false;
+ State newState = GetState();
+ // if the state is dragging then make it Open.
+ if (newState == State::Dragging) {
+ mOuter->mContent->AsElement()->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::state, u""_ns, true);
+ }
+
+ mPressed = false;
+
+ // if we dragged then fire a command event.
+ if (mDidDrag) {
+ RefPtr<nsXULElement> element =
+ nsXULElement::FromNode(mOuter->GetContent());
+ element->DoCommand();
+ }
+
+ // printf("MouseUp\n");
+ }
+
+ mChildInfosBefore.Clear();
+ mChildInfosAfter.Clear();
+}
+
+void nsSplitterFrameInner::MouseDrag(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent) {
+ if (!mDragging || !mOuter) {
+ return;
+ }
+
+ const bool isHorizontal = !mOuter->IsHorizontal();
+ nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, RelativeTo{mParentBox});
+ nscoord pos = isHorizontal ? pt.x : pt.y;
+
+ // take our current position and subtract the start location,
+ // mDragStart is in parent-box relative coordinates already.
+ pos -= mDragStart;
+
+ for (auto& info : mChildInfosBefore) {
+ info.changed = info.current;
+ }
+
+ for (auto& info : mChildInfosAfter) {
+ info.changed = info.current;
+ }
+ nscoord oldPos = pos;
+
+ ResizeChildTo(pos);
+
+ State currentState = GetState();
+ bool supportsBefore = SupportsCollapseDirection(Before);
+ bool supportsAfter = SupportsCollapseDirection(After);
+
+ const bool isRTL =
+ mOuter->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ bool pastEnd = oldPos > 0 && oldPos > pos;
+ bool pastBegin = oldPos < 0 && oldPos < pos;
+ if (isRTL) {
+ // Swap the boundary checks in RTL mode
+ std::swap(pastEnd, pastBegin);
+ }
+ const bool isCollapsedBefore = pastBegin && supportsBefore;
+ const bool isCollapsedAfter = pastEnd && supportsAfter;
+
+ // if we are in a collapsed position
+ if (isCollapsedBefore || isCollapsedAfter) {
+ // and we are not collapsed then collapse
+ if (currentState == State::Dragging) {
+ if (pastEnd) {
+ // printf("Collapse right\n");
+ if (supportsAfter) {
+ RefPtr<Element> outer = mOuter->mContent->AsElement();
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::substate, u"after"_ns,
+ true);
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::state, u"collapsed"_ns,
+ true);
+ }
+
+ } else if (pastBegin) {
+ // printf("Collapse left\n");
+ if (supportsBefore) {
+ RefPtr<Element> outer = mOuter->mContent->AsElement();
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::substate, u"before"_ns,
+ true);
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::state, u"collapsed"_ns,
+ true);
+ }
+ }
+ }
+ } else {
+ // if we are not in a collapsed position and we are not dragging make sure
+ // we are dragging.
+ if (currentState != State::Dragging) {
+ mOuter->mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::state, u"dragging"_ns, true);
+ }
+ AdjustChildren(aPresContext);
+ }
+
+ mDidDrag = true;
+}
+
+void nsSplitterFrameInner::AddListener() {
+ mOuter->GetContent()->AddEventListener(u"mouseup"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mousedown"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mousemove"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mouseout"_ns, this, false, false);
+}
+
+void nsSplitterFrameInner::RemoveListener() {
+ NS_ENSURE_TRUE_VOID(mOuter);
+ mOuter->GetContent()->RemoveEventListener(u"mouseup"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mousedown"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mousemove"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mouseout"_ns, this, false);
+}
+
+nsresult nsSplitterFrameInner::HandleEvent(dom::Event* aEvent) {
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("mouseup")) return MouseUp(aEvent);
+ if (eventType.EqualsLiteral("mousedown")) return MouseDown(aEvent);
+ if (eventType.EqualsLiteral("mousemove") ||
+ eventType.EqualsLiteral("mouseout"))
+ return MouseMove(aEvent);
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
+
+nsresult nsSplitterFrameInner::MouseUp(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ mPressed = false;
+
+ PresShell::ReleaseCapturingContent();
+
+ return NS_OK;
+}
+
+template <typename LengthLike>
+static nscoord ToLengthWithFallback(const LengthLike& aLengthLike,
+ nscoord aFallback) {
+ if (aLengthLike.ConvertsToLength()) {
+ return aLengthLike.ToLength();
+ }
+ return aFallback;
+}
+
+template <typename LengthLike>
+static nsSize ToLengthWithFallback(const LengthLike& aWidth,
+ const LengthLike& aHeight,
+ nscoord aFallback = 0) {
+ return {ToLengthWithFallback(aWidth, aFallback),
+ ToLengthWithFallback(aHeight, aFallback)};
+}
+
+static void ApplyMargin(nsSize& aSize, const nsMargin& aMargin) {
+ if (aSize.width != NS_UNCONSTRAINEDSIZE) {
+ aSize.width += aMargin.LeftRight();
+ }
+ if (aSize.height != NS_UNCONSTRAINEDSIZE) {
+ aSize.height += aMargin.TopBottom();
+ }
+}
+
+nsresult nsSplitterFrameInner::MouseDown(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ dom::MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ if (!mouseEvent) {
+ return NS_OK;
+ }
+
+ // only if left button
+ if (mouseEvent->Button() != 0) {
+ return NS_OK;
+ }
+
+ if (SplitterElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ mParentBox = GetValidParentBox(mOuter);
+ if (!mParentBox) {
+ return NS_OK;
+ }
+
+ // get our index
+ mDidDrag = false;
+
+ EnsureOrient();
+ const bool isHorizontal = !mOuter->IsHorizontal();
+
+ const nsIContent* outerContent = mOuter->GetContent();
+
+ const ResizeType resizeBefore = GetResizeBefore();
+ const ResizeType resizeAfter = GetResizeAfter();
+ const int32_t childCount = mParentBox->PrincipalChildList().GetLength();
+
+ mChildInfosBefore.Clear();
+ mChildInfosAfter.Clear();
+ int32_t count = 0;
+
+ bool foundOuter = false;
+ CSSOrderAwareFrameIterator iter(
+ mParentBox, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::Order);
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* childBox = iter.get();
+ if (childBox == mOuter) {
+ foundOuter = true;
+ if (!count) {
+ // We're at the beginning, nothing to do.
+ return NS_OK;
+ }
+ if (count == childCount - 1 && resizeAfter != ResizeType::Grow) {
+ // If it's the last index then we need to allow for resizeafter="grow"
+ return NS_OK;
+ }
+ }
+ count++;
+
+ nsIContent* content = childBox->GetContent();
+ // XXX flex seems untested, as it uses mBoxFlex rather than actual flexbox
+ // flex.
+ const nscoord flex = childBox->StyleXUL()->mBoxFlex;
+ const bool isBefore = !foundOuter;
+ const bool isResizable = [&] {
+ if (auto* element = nsXULElement::FromNode(content)) {
+ if (element->NodeInfo()->NameAtom() == nsGkAtoms::splitter) {
+ // skip over any splitters
+ return false;
+ }
+
+ // We need to check for hidden attribute too, since treecols with
+ // the hidden="true" attribute are not really hidden, just collapsed
+ if (element->GetXULBoolAttr(nsGkAtoms::fixed) ||
+ element->GetXULBoolAttr(nsGkAtoms::hidden)) {
+ return false;
+ }
+ }
+
+ // We need to check this here rather than in the switch before because we
+ // want `sibling` to work in the DOM order, not frame tree order.
+ if (resizeBefore == ResizeType::Sibling &&
+ content->GetNextElementSibling() == outerContent) {
+ return true;
+ }
+ if (resizeAfter == ResizeType::Sibling &&
+ content->GetPreviousElementSibling() == outerContent) {
+ return true;
+ }
+
+ const ResizeType resizeType = isBefore ? resizeBefore : resizeAfter;
+ switch (resizeType) {
+ case ResizeType::Grow:
+ case ResizeType::None:
+ case ResizeType::Sibling:
+ return false;
+ case ResizeType::Flex:
+ return flex > 0;
+ case ResizeType::Closest:
+ case ResizeType::Farthest:
+ break;
+ }
+ return true;
+ }();
+
+ if (!isResizable) {
+ continue;
+ }
+
+ nsSize curSize = childBox->GetSize();
+ const auto& pos = *childBox->StylePosition();
+ nsSize minSize = ToLengthWithFallback(pos.mMinWidth, pos.mMinHeight);
+ nsSize maxSize = ToLengthWithFallback(pos.mMaxWidth, pos.mMaxHeight,
+ NS_UNCONSTRAINEDSIZE);
+ nsSize prefSize(ToLengthWithFallback(pos.mWidth, curSize.width),
+ ToLengthWithFallback(pos.mHeight, curSize.height));
+
+ maxSize.width = std::max(maxSize.width, minSize.width);
+ maxSize.height = std::max(maxSize.height, minSize.height);
+ prefSize.width =
+ NS_CSS_MINMAX(prefSize.width, minSize.width, maxSize.width);
+ prefSize.height =
+ NS_CSS_MINMAX(prefSize.height, minSize.height, maxSize.height);
+
+ nsMargin m;
+ childBox->StyleMargin()->GetMargin(m);
+
+ ApplyMargin(curSize, m);
+ ApplyMargin(minSize, m);
+ ApplyMargin(maxSize, m);
+ ApplyMargin(prefSize, m);
+
+ auto& list = isBefore ? mChildInfosBefore : mChildInfosAfter;
+ nsSplitterInfo& info = *list.AppendElement();
+ info.childElem = content;
+ info.min = isHorizontal ? minSize.width : minSize.height;
+ info.max = isHorizontal ? maxSize.width : maxSize.height;
+ info.pref = isHorizontal ? prefSize.width : prefSize.height;
+ info.current = info.changed = isHorizontal ? curSize.width : curSize.height;
+ }
+
+ if (!foundOuter) {
+ return NS_OK;
+ }
+
+ mPressed = true;
+
+ const bool reverseDirection = [&] {
+ MOZ_ASSERT(mParentBox->IsFlexContainerFrame());
+ const FlexboxAxisInfo info(mParentBox);
+ if (!info.mIsRowOriented) {
+ return info.mIsMainAxisReversed;
+ }
+ const bool rtl =
+ mParentBox->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ return info.mIsMainAxisReversed != rtl;
+ }();
+
+ if (reverseDirection) {
+ // The before array is really the after array, and the order needs to be
+ // reversed. First reverse both arrays.
+ mChildInfosBefore.Reverse();
+ mChildInfosAfter.Reverse();
+
+ // Now swap the two arrays.
+ std::swap(mChildInfosBefore, mChildInfosAfter);
+ }
+
+ // if resizebefore is not Farthest, reverse the list because the first child
+ // in the list is the farthest, and we want the first child to be the closest.
+ if (resizeBefore != ResizeType::Farthest) {
+ mChildInfosBefore.Reverse();
+ }
+
+ // if the resizeafter is the Farthest we must reverse the list because the
+ // first child in the list is the closest we want the first child to be the
+ // Farthest.
+ if (resizeAfter == ResizeType::Farthest) {
+ mChildInfosAfter.Reverse();
+ }
+
+ int32_t c;
+ nsPoint pt =
+ nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(mouseEvent, mParentBox);
+ if (isHorizontal) {
+ c = pt.x;
+ mSplitterPos = mOuter->mRect.x;
+ } else {
+ c = pt.y;
+ mSplitterPos = mOuter->mRect.y;
+ }
+
+ mDragStart = c;
+
+ // printf("Pressed mDragStart=%d\n",mDragStart);
+
+ PresShell::SetCapturingContent(mOuter->GetContent(),
+ CaptureFlags::IgnoreAllowedState);
+
+ return NS_OK;
+}
+
+nsresult nsSplitterFrameInner::MouseMove(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ if (!mPressed) {
+ return NS_OK;
+ }
+
+ if (mDragging) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMEventListener> kungfuDeathGrip(this);
+ mOuter->mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::state,
+ u"dragging"_ns, true);
+
+ RemoveListener();
+ mDragging = true;
+
+ return NS_OK;
+}
+
+bool nsSplitterFrameInner::SupportsCollapseDirection(
+ nsSplitterFrameInner::CollapseDirection aDirection) {
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::before, nsGkAtoms::after, nsGkAtoms::both, nullptr};
+
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::collapse, strings, eCaseMatters)) {
+ case 0:
+ return (aDirection == Before);
+ case 1:
+ return (aDirection == After);
+ case 2:
+ return true;
+ }
+
+ return false;
+}
+
+static nsIFrame* SlowOrderAwareSibling(nsIFrame* aBox, bool aNext) {
+ nsIFrame* parent = aBox->GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+ CSSOrderAwareFrameIterator iter(
+ parent, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::Order);
+
+ nsIFrame* prevSibling = nullptr;
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* current = iter.get();
+ if (!aNext && current == aBox) {
+ return prevSibling;
+ }
+ if (aNext && prevSibling == aBox) {
+ return current;
+ }
+ prevSibling = current;
+ }
+ return nullptr;
+}
+
+void nsSplitterFrameInner::UpdateState() {
+ // State Transitions:
+ // Open -> Dragging
+ // Open -> CollapsedBefore
+ // Open -> CollapsedAfter
+ // CollapsedBefore -> Open
+ // CollapsedBefore -> Dragging
+ // CollapsedAfter -> Open
+ // CollapsedAfter -> Dragging
+ // Dragging -> Open
+ // Dragging -> CollapsedBefore (auto collapse)
+ // Dragging -> CollapsedAfter (auto collapse)
+
+ State newState = GetState();
+
+ if (newState == mState) {
+ // No change.
+ return;
+ }
+
+ if ((SupportsCollapseDirection(Before) || SupportsCollapseDirection(After)) &&
+ IsValidParentBox(mOuter->GetParent())) {
+ // Find the splitter's immediate sibling.
+ const bool prev =
+ newState == State::CollapsedBefore || mState == State::CollapsedBefore;
+ nsIFrame* splitterSibling = SlowOrderAwareSibling(mOuter, !prev);
+ if (splitterSibling) {
+ nsCOMPtr<nsIContent> sibling = splitterSibling->GetContent();
+ if (sibling && sibling->IsElement()) {
+ if (mState == State::CollapsedBefore ||
+ mState == State::CollapsedAfter) {
+ // CollapsedBefore -> Open
+ // CollapsedBefore -> Dragging
+ // CollapsedAfter -> Open
+ // CollapsedAfter -> Dragging
+ nsContentUtils::AddScriptRunner(new nsUnsetAttrRunnable(
+ sibling->AsElement(), nsGkAtoms::collapsed));
+ } else if ((mState == State::Open || mState == State::Dragging) &&
+ (newState == State::CollapsedBefore ||
+ newState == State::CollapsedAfter)) {
+ // Open -> CollapsedBefore / CollapsedAfter
+ // Dragging -> CollapsedBefore / CollapsedAfter
+ nsContentUtils::AddScriptRunner(new nsSetAttrRunnable(
+ sibling->AsElement(), nsGkAtoms::collapsed, u"true"_ns));
+ }
+ }
+ }
+ }
+ mState = newState;
+}
+
+void nsSplitterFrameInner::EnsureOrient() {
+ mOuter->mIsHorizontal = SplitterIsHorizontal(mParentBox);
+}
+
+void nsSplitterFrameInner::AdjustChildren(nsPresContext* aPresContext) {
+ EnsureOrient();
+ const bool isHorizontal = !mOuter->IsHorizontal();
+
+ AdjustChildren(aPresContext, mChildInfosBefore, isHorizontal);
+ AdjustChildren(aPresContext, mChildInfosAfter, isHorizontal);
+}
+
+static nsIFrame* GetChildBoxForContent(nsIFrame* aParentBox,
+ nsIContent* aContent) {
+ // XXX Can this use GetPrimaryFrame?
+ for (nsIFrame* f : aParentBox->PrincipalChildList()) {
+ if (f->GetContent() == aContent) {
+ return f;
+ }
+ }
+ return nullptr;
+}
+
+void nsSplitterFrameInner::AdjustChildren(nsPresContext* aPresContext,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ bool aIsHorizontal) {
+ /// printf("------- AdjustChildren------\n");
+
+ for (auto& info : aChildInfos) {
+ nscoord newPref = info.pref + (info.changed - info.current);
+ if (nsIFrame* childBox =
+ GetChildBoxForContent(mParentBox, info.childElem)) {
+ SetPreferredSize(childBox, aIsHorizontal, newPref);
+ }
+ }
+}
+
+void nsSplitterFrameInner::SetPreferredSize(nsIFrame* aChildBox,
+ bool aIsHorizontal, nscoord aSize) {
+ nsMargin margin;
+ aChildBox->StyleMargin()->GetMargin(margin);
+ if (aIsHorizontal) {
+ aSize -= (margin.left + margin.right);
+ } else {
+ aSize -= (margin.top + margin.bottom);
+ }
+
+ RefPtr element = nsStyledElement::FromNode(aChildBox->GetContent());
+ if (!element) {
+ return;
+ }
+
+ // We set both the attribute and the CSS value, so that XUL persist="" keeps
+ // working, see bug 1790712.
+
+ int32_t pixels = aSize / AppUnitsPerCSSPixel();
+ nsAutoString attrValue;
+ attrValue.AppendInt(pixels);
+ element->SetAttr(aIsHorizontal ? nsGkAtoms::width : nsGkAtoms::height,
+ attrValue, IgnoreErrors());
+
+ nsCOMPtr<nsICSSDeclaration> decl = element->Style();
+
+ nsAutoCString cssValue;
+ cssValue.AppendInt(pixels);
+ cssValue.AppendLiteral("px");
+ decl->SetProperty(aIsHorizontal ? "width"_ns : "height"_ns, cssValue, ""_ns,
+ IgnoreErrors());
+}
+
+void nsSplitterFrameInner::AddRemoveSpace(nscoord aDiff,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ int32_t& aSpaceLeft) {
+ aSpaceLeft = 0;
+
+ for (auto& info : aChildInfos) {
+ nscoord min = info.min;
+ nscoord max = info.max;
+ nscoord& c = info.changed;
+
+ // figure our how much space to add or remove
+ if (c + aDiff < min) {
+ aDiff += (c - min);
+ c = min;
+ } else if (c + aDiff > max) {
+ aDiff -= (max - c);
+ c = max;
+ } else {
+ c += aDiff;
+ aDiff = 0;
+ }
+
+ // there is not space left? We are done
+ if (aDiff == 0) {
+ break;
+ }
+ }
+
+ aSpaceLeft = aDiff;
+}
+
+/**
+ * Ok if we want to resize a child we will know the actual size in pixels we
+ * want it to be. This is not the preferred size. But the only way we can change
+ * a child is by manipulating its preferred size. So give the actual pixel size
+ * this method will figure out the preferred size and set it.
+ */
+
+void nsSplitterFrameInner::ResizeChildTo(nscoord& aDiff) {
+ nscoord spaceLeft = 0;
+
+ if (!mChildInfosBefore.IsEmpty()) {
+ AddRemoveSpace(aDiff, mChildInfosBefore, spaceLeft);
+ // If there is any space left over remove it from the diff we were
+ // originally given.
+ aDiff -= spaceLeft;
+ }
+
+ AddRemoveSpace(-aDiff, mChildInfosAfter, spaceLeft);
+
+ if (spaceLeft != 0 && !mChildInfosAfter.IsEmpty()) {
+ aDiff += spaceLeft;
+ AddRemoveSpace(spaceLeft, mChildInfosBefore, spaceLeft);
+ }
+}
diff --git a/layout/xul/nsSplitterFrame.h b/layout/xul/nsSplitterFrame.h
new file mode 100644
index 0000000000..72e1f4c51b
--- /dev/null
+++ b/layout/xul/nsSplitterFrame.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// nsSplitterFrame
+//
+
+#ifndef nsSplitterFrame_h__
+#define nsSplitterFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "SimpleXULLeafFrame.h"
+
+class nsSplitterFrameInner;
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+nsIFrame* NS_NewSplitterFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsSplitterFrame final : public mozilla::SimpleXULLeafFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsSplitterFrame)
+
+ explicit nsSplitterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ void Destroy(DestroyContext&) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SplitterFrame"_ns, aResult);
+ }
+#endif
+
+ bool IsHorizontal() const { return mIsHorizontal; }
+
+ // nsIFrame overrides
+ nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ private:
+ friend class nsSplitterFrameInner;
+ RefPtr<nsSplitterFrameInner> mInner;
+ bool mIsHorizontal = false;
+}; // class nsSplitterFrame
+
+#endif
diff --git a/layout/xul/nsXULPopupManager.cpp b/layout/xul/nsXULPopupManager.cpp
new file mode 100644
index 0000000000..fb881b86eb
--- /dev/null
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -0,0 +1,2887 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "XULButtonElement.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/UniquePtr.h"
+#include "nsGkAtoms.h"
+#include "nsISound.h"
+#include "nsXULPopupManager.h"
+#include "nsMenuPopupFrame.h"
+#include "nsContentUtils.h"
+#include "nsXULElement.h"
+#include "nsIDOMXULCommandDispatcher.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsGlobalWindow.h"
+#include "nsIContentInlines.h"
+#include "nsLayoutUtils.h"
+#include "nsViewManager.h"
+#include "nsITimer.h"
+#include "nsFocusManager.h"
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIBaseWindow.h"
+#include "nsCaret.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIWindowRoot.h"
+#include "nsFrameManager.h"
+#include "nsPresContextInlines.h"
+#include "nsIObserverService.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/HTMLSlotElement.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/UIEvent.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/PopupPositionedEvent.h"
+#include "mozilla/dom/PopupPositionedEventBinding.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "mozilla/dom/XULMenuElement.h"
+#include "mozilla/dom/XULMenuBarElement.h"
+#include "mozilla/dom/XULPopupElement.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/widget/nsAutoRollup.h"
+#include "mozilla/widget/NativeMenuSupport.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::widget::NativeMenu;
+
+static_assert(KeyboardEvent_Binding::DOM_VK_HOME ==
+ KeyboardEvent_Binding::DOM_VK_END + 1 &&
+ KeyboardEvent_Binding::DOM_VK_LEFT ==
+ KeyboardEvent_Binding::DOM_VK_END + 2 &&
+ KeyboardEvent_Binding::DOM_VK_UP ==
+ KeyboardEvent_Binding::DOM_VK_END + 3 &&
+ KeyboardEvent_Binding::DOM_VK_RIGHT ==
+ KeyboardEvent_Binding::DOM_VK_END + 4 &&
+ KeyboardEvent_Binding::DOM_VK_DOWN ==
+ KeyboardEvent_Binding::DOM_VK_END + 5,
+ "nsXULPopupManager assumes some keyCode values are consecutive");
+
+#define NS_DIRECTION_IS_INLINE(dir) \
+ (dir == eNavigationDirection_Start || dir == eNavigationDirection_End)
+#define NS_DIRECTION_IS_BLOCK(dir) \
+ (dir == eNavigationDirection_Before || dir == eNavigationDirection_After)
+#define NS_DIRECTION_IS_BLOCK_TO_EDGE(dir) \
+ (dir == eNavigationDirection_First || dir == eNavigationDirection_Last)
+
+static_assert(static_cast<uint8_t>(mozilla::StyleDirection::Ltr) == 0 &&
+ static_cast<uint8_t>(mozilla::StyleDirection::Rtl) == 1,
+ "Left to Right should be 0 and Right to Left should be 1");
+
+const nsNavigationDirection DirectionFromKeyCodeTable[2][6] = {
+ {
+ eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
+ eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
+ eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_LEFT
+ eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
+ eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_RIGHT
+ eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
+ },
+ {
+ eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
+ eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
+ eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_LEFT
+ eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
+ eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_RIGHT
+ eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
+ }};
+
+nsXULPopupManager* nsXULPopupManager::sInstance = nullptr;
+
+PendingPopup::PendingPopup(Element* aPopup, mozilla::dom::Event* aEvent)
+ : mPopup(aPopup), mEvent(aEvent), mModifiers(0) {
+ InitMousePoint();
+}
+
+void PendingPopup::InitMousePoint() {
+ // get the event coordinates relative to the root frame of the document
+ // containing the popup.
+ if (!mEvent) {
+ return;
+ }
+
+ WidgetEvent* event = mEvent->WidgetEventPtr();
+ WidgetInputEvent* inputEvent = event->AsInputEvent();
+ if (inputEvent) {
+ mModifiers = inputEvent->mModifiers;
+ }
+ Document* doc = mPopup->GetUncomposedDoc();
+ if (!doc) {
+ return;
+ }
+
+ PresShell* presShell = doc->GetPresShell();
+ nsPresContext* presContext;
+ if (presShell && (presContext = presShell->GetPresContext())) {
+ nsPresContext* rootDocPresContext = presContext->GetRootPresContext();
+ if (!rootDocPresContext) {
+ return;
+ }
+
+ nsIFrame* rootDocumentRootFrame =
+ rootDocPresContext->PresShell()->GetRootFrame();
+ if ((event->mClass == eMouseEventClass ||
+ event->mClass == eMouseScrollEventClass ||
+ event->mClass == eWheelEventClass) &&
+ !event->AsGUIEvent()->mWidget) {
+ // no widget, so just use the client point if available
+ MouseEvent* mouseEvent = mEvent->AsMouseEvent();
+ nsIntPoint clientPt(mouseEvent->ClientX(), mouseEvent->ClientY());
+
+ // XXX this doesn't handle IFRAMEs in transforms
+ nsPoint thisDocToRootDocOffset =
+ presShell->GetRootFrame()->GetOffsetToCrossDoc(rootDocumentRootFrame);
+ // convert to device pixels
+ mMousePoint.x = presContext->AppUnitsToDevPixels(
+ nsPresContext::CSSPixelsToAppUnits(clientPt.x) +
+ thisDocToRootDocOffset.x);
+ mMousePoint.y = presContext->AppUnitsToDevPixels(
+ nsPresContext::CSSPixelsToAppUnits(clientPt.y) +
+ thisDocToRootDocOffset.y);
+ } else if (rootDocumentRootFrame) {
+ nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ event, RelativeTo{rootDocumentRootFrame});
+ mMousePoint =
+ LayoutDeviceIntPoint(rootDocPresContext->AppUnitsToDevPixels(pnt.x),
+ rootDocPresContext->AppUnitsToDevPixels(pnt.y));
+ }
+ }
+}
+
+already_AddRefed<nsIContent> PendingPopup::GetTriggerContent() const {
+ nsCOMPtr<nsIContent> target =
+ do_QueryInterface(mEvent ? mEvent->GetTarget() : nullptr);
+ return target.forget();
+}
+
+uint16_t PendingPopup::MouseInputSource() const {
+ if (mEvent) {
+ mozilla::WidgetMouseEventBase* mouseEvent =
+ mEvent->WidgetEventPtr()->AsMouseEventBase();
+ if (mouseEvent) {
+ return mouseEvent->mInputSource;
+ }
+
+ RefPtr<XULCommandEvent> commandEvent = mEvent->AsXULCommandEvent();
+ if (commandEvent) {
+ return commandEvent->InputSource();
+ }
+ }
+
+ return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
+}
+
+XULPopupElement* nsMenuChainItem::Element() { return &mFrame->PopupElement(); }
+
+void nsMenuChainItem::SetParent(UniquePtr<nsMenuChainItem> aParent) {
+ MOZ_ASSERT_IF(aParent, !aParent->mChild);
+ auto oldParent = Detach();
+ mParent = std::move(aParent);
+ if (mParent) {
+ mParent->mChild = this;
+ }
+}
+
+UniquePtr<nsMenuChainItem> nsMenuChainItem::Detach() {
+ if (mParent) {
+ MOZ_ASSERT(mParent->mChild == this,
+ "Unexpected - parent's child not set to this");
+ mParent->mChild = nullptr;
+ }
+ return std::move(mParent);
+}
+
+void nsXULPopupManager::RemoveMenuChainItem(nsMenuChainItem* aItem) {
+ auto parent = aItem->Detach();
+ if (auto* child = aItem->GetChild()) {
+ MOZ_ASSERT(aItem != mPopups,
+ "Unexpected - popup with child at end of chain");
+ // This will kill aItem by changing child's mParent pointer.
+ child->SetParent(std::move(parent));
+ } else {
+ // An item without a child should be the first item in the chain, so set
+ // the first item pointer, pointed to by aRoot, to the parent.
+ MOZ_ASSERT(aItem == mPopups,
+ "Unexpected - popup with no child not at end of chain");
+ mPopups = std::move(parent);
+ }
+}
+
+void nsMenuChainItem::UpdateFollowAnchor() {
+ mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
+}
+
+void nsMenuChainItem::CheckForAnchorChange() {
+ if (mFollowAnchor) {
+ mFrame->CheckForAnchorChange(mCurrentRect);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsXULPopupManager, nsIDOMEventListener, nsIObserver)
+
+nsXULPopupManager::nsXULPopupManager()
+ : mActiveMenuBar(nullptr), mPopups(nullptr), mPendingPopup(nullptr) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ }
+}
+
+nsXULPopupManager::~nsXULPopupManager() {
+ NS_ASSERTION(!mPopups, "XUL popups still open");
+
+ if (mNativeMenu) {
+ mNativeMenu->RemoveObserver(this);
+ }
+}
+
+nsresult nsXULPopupManager::Init() {
+ sInstance = new nsXULPopupManager();
+ NS_ENSURE_TRUE(sInstance, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(sInstance);
+ return NS_OK;
+}
+
+void nsXULPopupManager::Shutdown() { NS_IF_RELEASE(sInstance); }
+
+NS_IMETHODIMP
+nsXULPopupManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) {
+ if (mKeyListener) {
+ mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
+ mKeyListener = nullptr;
+ }
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "xpcom-shutdown");
+ }
+ }
+
+ return NS_OK;
+}
+
+nsXULPopupManager* nsXULPopupManager::GetInstance() {
+ MOZ_ASSERT(sInstance);
+ return sInstance;
+}
+
+bool nsXULPopupManager::RollupTooltips() {
+ const RollupOptions options{0, FlushViews::Yes, nullptr, AllowAnimations::No};
+ return RollupInternal(RollupKind::Tooltip, options, nullptr);
+}
+
+bool nsXULPopupManager::Rollup(const RollupOptions& aOptions,
+ nsIContent** aLastRolledUp) {
+ return RollupInternal(RollupKind::Menu, aOptions, aLastRolledUp);
+}
+
+bool nsXULPopupManager::RollupNativeMenu() {
+ if (mNativeMenu) {
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ return menu->Close();
+ }
+ return false;
+}
+
+bool nsXULPopupManager::RollupInternal(RollupKind aKind,
+ const RollupOptions& aOptions,
+ nsIContent** aLastRolledUp) {
+ if (aLastRolledUp) {
+ *aLastRolledUp = nullptr;
+ }
+
+ // We can disable the autohide behavior via a pref to ease debugging.
+ if (StaticPrefs::ui_popup_disable_autohide()) {
+ // Required on linux to allow events to work on other targets.
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(false);
+ }
+ return false;
+ }
+
+ nsMenuChainItem* item = GetRollupItem(aKind);
+ if (!item) {
+ return false;
+ }
+ if (aLastRolledUp) {
+ // We need to get the popup that will be closed last, so that widget can
+ // keep track of it so it doesn't reopen if a mousedown event is going to
+ // processed. Keep going up the menu chain to get the first level menu of
+ // the same type. If a different type is encountered it means we have,
+ // for example, a menulist or context menu inside a panel, and we want to
+ // treat these as distinct. It's possible that this menu doesn't end up
+ // closing because the popuphiding event was cancelled, but in that case
+ // we don't need to deal with the menu reopening as it will already still
+ // be open.
+ nsMenuChainItem* first = item;
+ while (first->GetParent()) {
+ nsMenuChainItem* parent = first->GetParent();
+ if (first->Frame()->GetPopupType() != parent->Frame()->GetPopupType() ||
+ first->IsContextMenu() != parent->IsContextMenu()) {
+ break;
+ }
+ first = parent;
+ }
+
+ *aLastRolledUp = first->Element();
+ }
+
+ ConsumeOutsideClicksResult consumeResult =
+ item->Frame()->ConsumeOutsideClicks();
+ bool consume = consumeResult == ConsumeOutsideClicks_True;
+ bool rollup = true;
+
+ // If norolluponanchor is true, then don't rollup when clicking the anchor.
+ // This would be used to allow adjusting the caret position in an
+ // autocomplete field without hiding the popup for example.
+ bool noRollupOnAnchor =
+ (!consume && aOptions.mPoint &&
+ item->Frame()->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::norolluponanchor, nsGkAtoms::_true,
+ eCaseMatters));
+
+ // When ConsumeOutsideClicks_ParentOnly is used, always consume the click
+ // when the click was over the anchor. This way, clicking on a menu doesn't
+ // reopen the menu.
+ if ((consumeResult == ConsumeOutsideClicks_ParentOnly || noRollupOnAnchor) &&
+ aOptions.mPoint) {
+ nsMenuPopupFrame* popupFrame = item->Frame();
+ CSSIntRect anchorRect = [&] {
+ if (popupFrame->IsAnchored()) {
+ // Check if the popup has an anchor rectangle set. If not, get the
+ // rectangle from the anchor element.
+ auto r = popupFrame->GetScreenAnchorRect();
+ if (r.x != -1 && r.y != -1) {
+ // Prefer the untransformed anchor rect, so as to account for Wayland
+ // properly. Note we still need to check GetScreenAnchorRect() tho, so
+ // as to detect whether the anchor came from the popup opening call,
+ // or from an element (in which case we want to take the code-path
+ // below)..
+ auto untransformed = popupFrame->GetUntransformedAnchorRect();
+ if (!untransformed.IsEmpty()) {
+ return CSSIntRect::FromAppUnitsRounded(untransformed);
+ }
+ return r;
+ }
+ }
+
+ auto* anchor = Element::FromNodeOrNull(popupFrame->GetAnchor());
+ if (!anchor) {
+ return CSSIntRect();
+ }
+
+ // Check if the anchor has indicated another node to use for checking
+ // for roll-up. That way, we can anchor a popup on anonymous content
+ // or an individual icon, while clicking elsewhere within a button or
+ // other container doesn't result in us re-opening the popup.
+ nsAutoString consumeAnchor;
+ anchor->GetAttr(nsGkAtoms::consumeanchor, consumeAnchor);
+ if (!consumeAnchor.IsEmpty()) {
+ if (Element* newAnchor =
+ anchor->OwnerDoc()->GetElementById(consumeAnchor)) {
+ anchor = newAnchor;
+ }
+ }
+
+ nsIFrame* f = anchor->GetPrimaryFrame();
+ if (!f) {
+ return CSSIntRect();
+ }
+ return f->GetScreenRect();
+ }();
+
+ // It's possible that some other element is above the anchor at the same
+ // position, but the only thing that would happen is that the mouse
+ // event will get consumed, so here only a quick coordinates check is
+ // done rather than a slower complete check of what is at that location.
+ nsPresContext* presContext = item->Frame()->PresContext();
+ CSSIntPoint posCSSPixels =
+ presContext->DevPixelsToIntCSSPixels(*aOptions.mPoint);
+ if (anchorRect.Contains(posCSSPixels)) {
+ if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
+ consume = true;
+ }
+
+ if (noRollupOnAnchor) {
+ rollup = false;
+ }
+ }
+ }
+
+ if (!rollup) {
+ return false;
+ }
+
+ // If a number of popups to close has been specified, determine the last
+ // popup to close.
+ Element* lastPopup = nullptr;
+ uint32_t count = aOptions.mCount;
+ if (count && count != UINT32_MAX) {
+ nsMenuChainItem* last = item;
+ while (--count && last->GetParent()) {
+ last = last->GetParent();
+ }
+ if (last) {
+ lastPopup = last->Element();
+ }
+ }
+
+ nsPresContext* presContext = item->Frame()->PresContext();
+ RefPtr<nsViewManager> viewManager =
+ presContext->PresShell()->GetViewManager();
+
+ HidePopupOptions options{HidePopupOption::HideChain,
+ HidePopupOption::DeselectMenu,
+ HidePopupOption::IsRollup};
+ if (aOptions.mAllowAnimations == AllowAnimations::No) {
+ options += HidePopupOption::DisableAnimations;
+ }
+
+ HidePopup(item->Element(), options, lastPopup);
+
+ if (aOptions.mFlush == FlushViews::Yes) {
+ // The popup's visibility doesn't update until the minimize animation
+ // has finished, so call UpdateWidgetGeometry to update it right away.
+ viewManager->UpdateWidgetGeometry();
+ }
+
+ return consume;
+}
+
+////////////////////////////////////////////////////////////////////////
+bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent() {
+ // should rollup only for autocomplete widgets
+ // XXXndeakin this should really be something the popup has more control over
+
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item) {
+ return false;
+ }
+
+ nsIContent* content = item->Frame()->GetContent();
+ if (!content || !content->IsElement()) return false;
+
+ Element* element = content->AsElement();
+ if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
+ nsGkAtoms::_true, eCaseMatters))
+ return true;
+
+ if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
+ nsGkAtoms::_false, eCaseMatters))
+ return false;
+
+ nsAutoString value;
+ element->GetAttr(kNameSpaceID_None, nsGkAtoms::type, value);
+ return StringBeginsWith(value, u"autocomplete"_ns);
+}
+
+bool nsXULPopupManager::ShouldConsumeOnMouseWheelEvent() {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item) {
+ return false;
+ }
+
+ nsMenuPopupFrame* frame = item->Frame();
+ if (frame->GetPopupType() != PopupType::Panel) return true;
+
+ return !frame->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow, eCaseMatters);
+}
+
+// a menu should not roll up if activated by a mouse activate message (eg.
+// X-mouse)
+bool nsXULPopupManager::ShouldRollupOnMouseActivate() { return false; }
+
+uint32_t nsXULPopupManager::GetSubmenuWidgetChain(
+ nsTArray<nsIWidget*>* aWidgetChain) {
+ // this method is used by the widget code to determine the list of popups
+ // that are open. If a mouse click occurs outside one of these popups, the
+ // panels will roll up. If the click is inside a popup, they will not roll up
+ uint32_t count = 0, sameTypeCount = 0;
+
+ NS_ASSERTION(aWidgetChain, "null parameter");
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item) {
+ nsMenuChainItem* parent = item->GetParent();
+ if (!item->IsNoAutoHide()) {
+ nsCOMPtr<nsIWidget> widget = item->Frame()->GetWidget();
+ NS_ASSERTION(widget, "open popup has no widget");
+ if (widget) {
+ aWidgetChain->AppendElement(widget.get());
+ // In the case when a menulist inside a panel is open, clicking in the
+ // panel should still roll up the menu, so if a different type is found,
+ // stop scanning.
+ if (!sameTypeCount) {
+ count++;
+ if (!parent ||
+ item->Frame()->GetPopupType() !=
+ parent->Frame()->GetPopupType() ||
+ item->IsContextMenu() != parent->IsContextMenu()) {
+ sameTypeCount = count;
+ }
+ }
+ }
+ }
+ item = parent;
+ }
+
+ return sameTypeCount;
+}
+
+nsIWidget* nsXULPopupManager::GetRollupWidget() {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ return item ? item->Frame()->GetWidget() : nullptr;
+}
+
+void nsXULPopupManager::AdjustPopupsOnWindowChange(
+ nsPIDOMWindowOuter* aWindow) {
+ // When the parent window is moved, adjust any child popups. Dismissable
+ // menus and panels are expected to roll up when a window is moved, so there
+ // is no need to check these popups, only the noautohide popups.
+
+ // The items are added to a list so that they can be adjusted bottom to top.
+ nsTArray<nsMenuPopupFrame*> list;
+
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ // only move popups that are within the same window and where auto
+ // positioning has not been disabled
+ if (!item->IsNoAutoHide()) {
+ continue;
+ }
+ nsMenuPopupFrame* frame = item->Frame();
+ nsIContent* popup = frame->GetContent();
+ if (!popup) {
+ continue;
+ }
+ Document* document = popup->GetUncomposedDoc();
+ if (!document) {
+ continue;
+ }
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ if (!window) {
+ continue;
+ }
+ window = window->GetPrivateRoot();
+ if (window == aWindow) {
+ list.AppendElement(frame);
+ }
+ }
+
+ for (int32_t l = list.Length() - 1; l >= 0; l--) {
+ list[l]->SetPopupPosition(true);
+ }
+}
+
+void nsXULPopupManager::AdjustPopupsOnWindowChange(PresShell* aPresShell) {
+ if (aPresShell->GetDocument()) {
+ AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
+ }
+}
+
+static nsMenuPopupFrame* GetPopupToMoveOrResize(nsIFrame* aFrame) {
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame);
+ if (!menuPopupFrame) return nullptr;
+
+ // no point moving or resizing hidden popups
+ if (!menuPopupFrame->IsVisible()) return nullptr;
+
+ nsIWidget* widget = menuPopupFrame->GetWidget();
+ if (widget && !widget->IsVisible()) return nullptr;
+
+ return menuPopupFrame;
+}
+
+void nsXULPopupManager::PopupMoved(nsIFrame* aFrame,
+ const LayoutDeviceIntPoint& aPoint,
+ bool aByMoveToRect) {
+ nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
+ if (!menuPopupFrame) {
+ return;
+ }
+
+ nsView* view = menuPopupFrame->GetView();
+ if (!view) {
+ return;
+ }
+
+ menuPopupFrame->WidgetPositionOrSizeDidChange();
+
+ // Don't do anything if the popup is already at the specified location. This
+ // prevents recursive calls when a popup is positioned.
+ LayoutDeviceIntRect curDevBounds = view->RecalcWidgetBounds();
+ nsIWidget* widget = menuPopupFrame->GetWidget();
+ if (curDevBounds.TopLeft() == aPoint &&
+ (!widget ||
+ widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
+ return;
+ }
+
+ // Update the popup's position using SetPopupPosition if the popup is
+ // anchored and at the parent level as these maintain their position
+ // relative to the parent window (except if positioned by move to rect, in
+ // which case we better make sure that layout matches that). Otherwise, just
+ // update the popup to the specified screen coordinates.
+ if (menuPopupFrame->IsAnchored() &&
+ menuPopupFrame->GetPopupLevel() == widget::PopupLevel::Parent &&
+ !aByMoveToRect) {
+ menuPopupFrame->SetPopupPosition(true);
+ } else {
+ CSSPoint cssPos =
+ aPoint / menuPopupFrame->PresContext()->CSSToDevPixelScale();
+ menuPopupFrame->MoveTo(cssPos, false, aByMoveToRect);
+ }
+}
+
+void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
+ const LayoutDeviceIntSize& aSize) {
+ nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
+ if (!menuPopupFrame) {
+ return;
+ }
+
+ menuPopupFrame->WidgetPositionOrSizeDidChange();
+
+ nsView* view = menuPopupFrame->GetView();
+ if (!view) {
+ return;
+ }
+
+ const LayoutDeviceIntRect curDevBounds = view->RecalcWidgetBounds();
+ // If the size is what we think it is, we have nothing to do.
+ if (curDevBounds.Size() == aSize) {
+ return;
+ }
+
+ Element* popup = menuPopupFrame->GetContent()->AsElement();
+
+ // Only set the width and height if the popup already has these attributes.
+ if (!popup->HasAttr(nsGkAtoms::width) || !popup->HasAttr(nsGkAtoms::height)) {
+ return;
+ }
+
+ // The size is different. Convert the actual size to css pixels and store it
+ // as 'width' and 'height' attributes on the popup.
+ nsPresContext* presContext = menuPopupFrame->PresContext();
+
+ CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width),
+ presContext->DevPixelsToIntCSSPixels(aSize.height));
+
+ nsAutoString width, height;
+ width.AppendInt(newCSS.width);
+ height.AppendInt(newCSS.height);
+ // FIXME(emilio): aNotify should be consistent (probably true in the two calls
+ // below?).
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, false);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true);
+}
+
+nsMenuPopupFrame* nsXULPopupManager::GetPopupFrameForContent(
+ nsIContent* aContent, bool aShouldFlush) {
+ if (aShouldFlush) {
+ Document* document = aContent->GetUncomposedDoc();
+ if (document) {
+ if (RefPtr<PresShell> presShell = document->GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ }
+ }
+ }
+
+ return do_QueryFrame(aContent->GetPrimaryFrame());
+}
+
+nsMenuChainItem* nsXULPopupManager::GetRollupItem(RollupKind aKind) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->PopupState() == ePopupInvisible) {
+ continue;
+ }
+ MOZ_ASSERT_IF(item->Frame()->GetPopupType() == PopupType::Tooltip,
+ item->IsNoAutoHide());
+ const bool valid = aKind == RollupKind::Tooltip
+ ? item->Frame()->GetPopupType() == PopupType::Tooltip
+ : !item->IsNoAutoHide();
+ if (valid) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::SetActiveMenuBar(XULMenuBarElement* aMenuBar,
+ bool aActivate) {
+ if (aActivate) {
+ mActiveMenuBar = aMenuBar;
+ } else if (mActiveMenuBar == aMenuBar) {
+ mActiveMenuBar = nullptr;
+ }
+ UpdateKeyboardListeners();
+}
+
+static CloseMenuMode GetCloseMenuMode(nsIContent* aMenu) {
+ if (!aMenu->IsElement()) {
+ return CloseMenuMode_Auto;
+ }
+
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::none,
+ nsGkAtoms::single, nullptr};
+ switch (aMenu->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::closemenu, strings, eCaseMatters)) {
+ case 0:
+ return CloseMenuMode_None;
+ case 1:
+ return CloseMenuMode_Single;
+ default:
+ return CloseMenuMode_Auto;
+ }
+}
+
+auto nsXULPopupManager::MayShowMenu(nsIContent* aMenu) -> MayShowMenuResult {
+ if (mNativeMenu && aMenu->IsElement() &&
+ mNativeMenu->Element()->Contains(aMenu)) {
+ return {true};
+ }
+
+ auto* menu = XULButtonElement::FromNode(aMenu);
+ if (!menu) {
+ return {};
+ }
+
+ nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return {};
+ }
+ return {false, menu, popupFrame};
+}
+
+void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem) {
+ auto mayShowResult = MayShowMenu(aMenu);
+ if (NS_WARN_IF(!mayShowResult)) {
+ return;
+ }
+
+ if (mayShowResult.mIsNative) {
+ mNativeMenu->OpenSubmenu(aMenu->AsElement());
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = mayShowResult.mMenuPopupFrame;
+
+ // inherit whether or not we're a context menu from the parent
+ const bool onMenuBar = mayShowResult.mMenuButton->IsOnMenuBar();
+ const bool onmenu = mayShowResult.mMenuButton->IsOnMenu();
+ const bool parentIsContextMenu = mayShowResult.mMenuButton->IsOnContextMenu();
+
+ nsAutoString position;
+
+#ifdef XP_MACOSX
+ if (aMenu->IsXULElement(nsGkAtoms::menulist)) {
+ position.AssignLiteral("selection");
+ } else
+#endif
+
+ if (onMenuBar || !onmenu)
+ position.AssignLiteral("after_start");
+ else
+ position.AssignLiteral("end_before");
+
+ // there is no trigger event for menus
+ popupFrame->InitializePopup(aMenu, nullptr, position, 0, 0,
+ MenuPopupAnchorType_Node, true);
+ PendingPopup pendingPopup(&popupFrame->PopupElement(), nullptr);
+ BeginShowingPopup(pendingPopup, parentIsContextMenu, aSelectFirstItem);
+}
+
+static bool ShouldUseNativeContextMenus() {
+#ifdef HAS_NATIVE_MENU_SUPPORT
+ return mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus();
+#else
+ return false;
+#endif
+}
+
+void nsXULPopupManager::ShowPopup(Element* aPopup, nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu,
+ bool aAttributesOverride,
+ bool aSelectFirstItem, Event* aTriggerEvent) {
+#ifdef XP_MACOSX
+ // On Mac, use a native menu if possible since the non-native menu looks out
+ // of place. Native menus for anchored popups are not currently implemented,
+ // so fall back to the non-native path below if `aAnchorContent` is given. We
+ // also fall back if the position string is not empty so we don't break tests
+ // that either themselves call or test app features that call
+ // `openPopup(null, "position")`.
+ if (!aAnchorContent && aPosition.IsEmpty() && ShouldUseNativeContextMenus() &&
+ aPopup->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup) &&
+ ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
+ aTriggerEvent)) {
+ return;
+ }
+#endif
+
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return;
+ }
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition, aXPos,
+ aYPos, MenuPopupAnchorType_Node,
+ aAttributesOverride);
+
+ BeginShowingPopup(pendingPopup, aIsContextMenu, aSelectFirstItem);
+}
+
+void nsXULPopupManager::ShowPopupAtScreen(Element* aPopup, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu,
+ Event* aTriggerEvent) {
+ if (aIsContextMenu && ShouldUseNativeContextMenus() &&
+ ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
+ aTriggerEvent)) {
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) return;
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos,
+ aIsContextMenu);
+ BeginShowingPopup(pendingPopup, aIsContextMenu, false);
+}
+
+bool nsXULPopupManager::ShowPopupAsNativeMenu(Element* aPopup, int32_t aXPos,
+ int32_t aYPos,
+ bool aIsContextMenu,
+ Event* aTriggerEvent) {
+ if (mNativeMenu) {
+ NS_WARNING("Native menu still open when trying to open another");
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ (void)menu->Close();
+ menu->RemoveObserver(this);
+ mNativeMenu = nullptr;
+ }
+
+ RefPtr<NativeMenu> menu;
+#ifdef HAS_NATIVE_MENU_SUPPORT
+ menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu(aPopup);
+#endif
+
+ if (!menu) {
+ return false;
+ }
+
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame) {
+ return true;
+ }
+
+ // Hide the menu from our accessibility code so that we don't dispatch custom
+ // accessibility notifications which would conflict with the system ones.
+ aPopup->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns, true);
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAsNativeContextMenu(triggerContent, aXPos, aYPos);
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ nsEventStatus status = FirePopupShowingEvent(pendingPopup, presContext);
+
+ // if the event was cancelled, don't open the popup, reset its state back
+ // to closed and clear its trigger content.
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ if ((popupFrame = GetPopupFrameForContent(aPopup, true))) {
+ popupFrame->SetPopupState(ePopupClosed);
+ popupFrame->ClearTriggerContent();
+ }
+ return true;
+ }
+
+ mNativeMenu = menu;
+ mNativeMenu->AddObserver(this);
+ nsIFrame* frame = presContext->PresShell()->GetCurrentEventFrame();
+ if (!frame) {
+ frame = presContext->PresShell()->GetRootFrame();
+ }
+ mNativeMenu->ShowAsContextMenu(frame, CSSIntPoint(aXPos, aYPos),
+ aIsContextMenu);
+
+ // While the native menu is open, it consumes mouseup events.
+ // Clear any :active state, mouse capture state and drag tracking now.
+ EventStateManager* activeESM = static_cast<EventStateManager*>(
+ EventStateManager::GetActiveEventStateManager());
+ if (activeESM) {
+ EventStateManager::ClearGlobalActiveContent(activeESM);
+ activeESM->StopTrackingDragGesture(true);
+ }
+ PresShell::ReleaseCapturingContent();
+
+ return true;
+}
+
+void nsXULPopupManager::OnNativeMenuOpened() {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
+
+ nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
+ if (popupFrame) {
+ popupFrame->SetPopupState(ePopupShown);
+ }
+}
+
+void nsXULPopupManager::OnNativeMenuClosed() {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
+
+ bool shouldHideChain =
+ mNativeMenuActivatedItemCloseMenuMode == Some(CloseMenuMode_Auto);
+
+ nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
+ if (popupFrame) {
+ popupFrame->ClearTriggerContentIncludingDocument();
+ popupFrame->SetPopupState(ePopupClosed);
+ }
+ mNativeMenu->RemoveObserver(this);
+ mNativeMenu = nullptr;
+ mNativeMenuActivatedItemCloseMenuMode = Nothing();
+ mNativeMenuSubmenuStates.Clear();
+
+ // Stop hiding the menu from accessibility code, in case it gets opened as a
+ // non-native menu in the future.
+ popup->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
+ true);
+
+ if (shouldHideChain && mPopups &&
+ mPopups->GetPopupType() == PopupType::Menu) {
+ // A menu item was activated before this menu closed, and the item requested
+ // the entire popup chain to be closed, which includes any open non-native
+ // menus.
+ // Close the non-native menus now. This matches the HidePopup call in
+ // nsXULMenuCommandEvent::Run.
+ HidePopup(mPopups->Element(), {HidePopupOption::HideChain});
+ }
+}
+
+void nsXULPopupManager::OnNativeSubMenuWillOpen(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShowing);
+}
+
+void nsXULPopupManager::OnNativeSubMenuDidOpen(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShown);
+}
+
+void nsXULPopupManager::OnNativeSubMenuClosed(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.Remove(aPopupElement);
+}
+
+void nsXULPopupManager::OnNativeMenuWillActivateItem(
+ mozilla::dom::Element* aMenuItemElement) {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ CloseMenuMode cmm = GetCloseMenuMode(aMenuItemElement);
+ mNativeMenuActivatedItemCloseMenuMode = Some(cmm);
+
+ if (cmm == CloseMenuMode_Auto) {
+ // If any non-native menus are visible (for example because the context menu
+ // was opened on a non-native menu item, e.g. in a bookmarks folder), hide
+ // the non-native menus before executing the item.
+ HideOpenMenusBeforeExecutingMenu(CloseMenuMode_Auto);
+ }
+}
+
+void nsXULPopupManager::ShowPopupAtScreenRect(
+ Element* aPopup, const nsAString& aPosition, const nsIntRect& aRect,
+ bool aIsContextMenu, bool aAttributesOverride, Event* aTriggerEvent) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) return;
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAtRect(triggerContent, aPosition, aRect,
+ aAttributesOverride);
+
+ BeginShowingPopup(pendingPopup, aIsContextMenu, false);
+}
+
+void nsXULPopupManager::ShowTooltipAtScreen(
+ Element* aPopup, nsIContent* aTriggerContent,
+ const LayoutDeviceIntPoint& aScreenPoint) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return;
+ }
+
+ PendingPopup pendingPopup(aPopup, nullptr);
+
+ nsPresContext* pc = popupFrame->PresContext();
+ pendingPopup.SetMousePoint([&] {
+ // Event coordinates are relative to the root widget
+ if (nsPresContext* rootPresContext = pc->GetRootPresContext()) {
+ if (nsCOMPtr<nsIWidget> rootWidget = rootPresContext->GetRootWidget()) {
+ return aScreenPoint - rootWidget->WidgetToScreenOffset();
+ }
+ }
+ return aScreenPoint;
+ }());
+
+ auto screenCSSPoint =
+ CSSIntPoint::Round(aScreenPoint / pc->CSSToDevPixelScale());
+ popupFrame->InitializePopupAtScreen(aTriggerContent, screenCSSPoint.x,
+ screenCSSPoint.y, false);
+
+ BeginShowingPopup(pendingPopup, false, false);
+}
+
+static void CheckCaretDrawingState() {
+ // There is 1 caret per document, we need to find the focused
+ // document and erase its caret.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ fm->GetFocusedWindow(getter_AddRefs(window));
+ if (!window) return;
+
+ auto* piWindow = nsPIDOMWindowOuter::From(window);
+ MOZ_ASSERT(piWindow);
+
+ nsCOMPtr<Document> focusedDoc = piWindow->GetDoc();
+ if (!focusedDoc) return;
+
+ PresShell* presShell = focusedDoc->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) return;
+ caret->SchedulePaint();
+ }
+}
+
+void nsXULPopupManager::ShowPopupCallback(Element* aPopup,
+ nsMenuPopupFrame* aPopupFrame,
+ bool aIsContextMenu,
+ bool aSelectFirstItem) {
+ PopupType popupType = aPopupFrame->GetPopupType();
+ const bool isMenu = popupType == PopupType::Menu;
+
+ // Popups normally hide when an outside click occurs. Panels may use
+ // the noautohide attribute to disable this behaviour. It is expected
+ // that the application will hide these popups manually. The tooltip
+ // listener will handle closing the tooltip also.
+ bool isNoAutoHide =
+ aPopupFrame->IsNoAutoHide() || popupType == PopupType::Tooltip;
+
+ auto item = MakeUnique<nsMenuChainItem>(aPopupFrame, isNoAutoHide,
+ aIsContextMenu, popupType);
+
+ // install keyboard event listeners for navigating menus. For panels, the
+ // escape key may be used to close the panel. However, the ignorekeys
+ // attribute may be used to disable adding these event listeners for popups
+ // that want to handle their own keyboard events.
+ nsAutoString ignorekeys;
+ aPopup->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys, ignorekeys);
+ if (ignorekeys.EqualsLiteral("true")) {
+ item->SetIgnoreKeys(eIgnoreKeys_True);
+ } else if (ignorekeys.EqualsLiteral("shortcuts")) {
+ item->SetIgnoreKeys(eIgnoreKeys_Shortcuts);
+ }
+
+ if (isMenu) {
+ // if the menu is on a menubar, use the menubar's listener instead
+ if (auto* menu = aPopupFrame->PopupElement().GetContainingMenu()) {
+ item->SetOnMenuBar(menu->IsOnMenuBar());
+ }
+ }
+
+ // use a weak frame as the popup will set an open attribute if it is a menu
+ AutoWeakFrame weakFrame(aPopupFrame);
+ aPopupFrame->ShowPopup(aIsContextMenu);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ item->UpdateFollowAnchor();
+
+ // popups normally hide when an outside click occurs. Panels may use
+ // the noautohide attribute to disable this behaviour. It is expected
+ // that the application will hide these popups manually. The tooltip
+ // listener will handle closing the tooltip also.
+ nsIContent* oldmenu = nullptr;
+ if (mPopups) {
+ oldmenu = mPopups->Element();
+ }
+ item->SetParent(std::move(mPopups));
+ mPopups = std::move(item);
+ SetCaptureState(oldmenu);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ RefPtr popup = &aPopupFrame->PopupElement();
+ popup->PopupOpened(aSelectFirstItem);
+
+ if (isMenu) {
+ UpdateMenuItems(aPopup);
+ }
+
+ // Caret visibility may have been affected, ensure that
+ // the caret isn't now drawn when it shouldn't be.
+ CheckCaretDrawingState();
+}
+
+nsMenuChainItem* nsXULPopupManager::FindPopup(Element* aPopup) const {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->GetContent() == aPopup) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::HidePopup(Element* aPopup, HidePopupOptions aOptions,
+ Element* aLastPopup) {
+ if (mNativeMenu && mNativeMenu->Element() == aPopup) {
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ (void)menu->Close();
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return;
+ }
+
+ nsMenuChainItem* foundPopup = FindPopup(aPopup);
+
+ RefPtr<Element> popupToHide, nextPopup, lastPopup;
+
+ if (foundPopup) {
+ if (foundPopup->IsNoAutoHide()) {
+ // If this is a noautohide panel, remove it but don't close any other
+ // panels.
+ popupToHide = aPopup;
+ // XXX This preserves behavior but why is it the right thing to do?
+ aOptions -= HidePopupOption::DeselectMenu;
+ } else {
+ // At this point, foundPopup will be set to the found item in the list. If
+ // foundPopup is the topmost menu, the one to remove, then there are no
+ // other popups to hide. If foundPopup is not the topmost menu, then there
+ // may be open submenus below it. In this case, we need to make sure that
+ // those submenus are closed up first. To do this, we scan up the menu
+ // list to find the topmost popup with only menus between it and
+ // foundPopup and close that menu first. In synchronous mode, the
+ // FirePopupHidingEvent method will be called which in turn calls
+ // HidePopupCallback to close up the next popup in the chain. These two
+ // methods will be called in sequence recursively to close up all the
+ // necessary popups. In asynchronous mode, a similar process occurs except
+ // that the FirePopupHidingEvent method is called asynchronously. In
+ // either case, nextPopup is set to the content node of the next popup to
+ // close, and lastPopup is set to the last popup in the chain to close,
+ // which will be aPopup, or null to close up all menus.
+
+ nsMenuChainItem* topMenu = foundPopup;
+ // Use IsMenu to ensure that foundPopup is a menu and scan down the child
+ // list until a non-menu is found. If foundPopup isn't a menu at all,
+ // don't scan and just close up this menu.
+ if (foundPopup->IsMenu()) {
+ nsMenuChainItem* child = foundPopup->GetChild();
+ while (child && child->IsMenu()) {
+ topMenu = child;
+ child = child->GetChild();
+ }
+ }
+
+ popupToHide = topMenu->Element();
+ popupFrame = topMenu->Frame();
+
+ const bool hideChain = aOptions.contains(HidePopupOption::HideChain);
+
+ // Close up another popup if there is one, and we are either hiding the
+ // entire chain or the item to hide isn't the topmost popup.
+ nsMenuChainItem* parent = topMenu->GetParent();
+ if (parent && (hideChain || topMenu != foundPopup)) {
+ while (parent && parent->IsNoAutoHide()) {
+ parent = parent->GetParent();
+ }
+
+ if (parent) {
+ nextPopup = parent->Element();
+ }
+ }
+
+ lastPopup = aLastPopup ? aLastPopup : (hideChain ? nullptr : aPopup);
+ }
+ } else if (popupFrame->PopupState() == ePopupPositioning) {
+ // When the popup is in the popuppositioning state, it will not be in the
+ // mPopups list. We need another way to find it and make sure it does not
+ // continue the popup showing process.
+ popupToHide = aPopup;
+ }
+
+ if (!popupToHide) {
+ return;
+ }
+
+ nsPopupState state = popupFrame->PopupState();
+ if (state == ePopupHiding) {
+ // If the popup is already being hidden, don't fire another popuphiding
+ // event. But finish hiding it sync if we need to.
+ if (aOptions.contains(HidePopupOption::DisableAnimations) &&
+ !aOptions.contains(HidePopupOption::Async)) {
+ HidePopupCallback(popupToHide, popupFrame, nullptr, nullptr,
+ popupFrame->GetPopupType(), aOptions);
+ }
+ return;
+ }
+
+ // Change the popup state to hiding. Don't set the hiding state if the
+ // popup is invisible, otherwise nsMenuPopupFrame::HidePopup will
+ // run again. In the invisible state, we just want the events to fire.
+ if (state != ePopupInvisible) {
+ popupFrame->SetPopupState(ePopupHiding);
+ }
+
+ // For menus, popupToHide is always the frontmost item in the list to hide.
+ if (aOptions.contains(HidePopupOption::Async)) {
+ nsCOMPtr<nsIRunnable> event =
+ new nsXULPopupHidingEvent(popupToHide, nextPopup, lastPopup,
+ popupFrame->GetPopupType(), aOptions);
+ aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ } else {
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ FirePopupHidingEvent(popupToHide, nextPopup, lastPopup, presContext,
+ popupFrame->GetPopupType(), aOptions);
+ }
+}
+
+void nsXULPopupManager::HideMenu(nsIContent* aMenu) {
+ if (mNativeMenu && aMenu->IsElement() &&
+ mNativeMenu->Element()->Contains(aMenu)) {
+ mNativeMenu->CloseSubmenu(aMenu->AsElement());
+ return;
+ }
+
+ auto* button = XULButtonElement::FromNode(aMenu);
+ if (!button || !button->IsMenu()) {
+ return;
+ }
+ auto* popup = button->GetMenuPopupContent();
+ if (!popup) {
+ return;
+ }
+ HidePopup(popup, {HidePopupOption::DeselectMenu});
+}
+
+// This is used to hide the popup after a transition finishes.
+class TransitionEnder final : public nsIDOMEventListener {
+ private:
+ // Effectively const but is cycle collected
+ MOZ_KNOWN_LIVE RefPtr<Element> mElement;
+
+ protected:
+ virtual ~TransitionEnder() = default;
+
+ public:
+ HidePopupOptions mOptions;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)
+
+ TransitionEnder(Element* aElement, HidePopupOptions aOptions)
+ : mElement(aElement), mOptions(aOptions) {}
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
+ mElement->RemoveSystemEventListener(u"transitionend"_ns, this, false);
+ mElement->RemoveSystemEventListener(u"transitioncancel"_ns, this, false);
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(mElement->GetPrimaryFrame());
+ if (!popupFrame || popupFrame->PopupState() != ePopupHiding) {
+ return NS_OK;
+ }
+
+ // Now hide the popup. There could be other properties transitioning, but
+ // we'll assume they all end at the same time and just hide the popup upon
+ // the first one ending.
+ if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
+ pm->HidePopupCallback(mElement, popupFrame, nullptr, nullptr,
+ popupFrame->GetPopupType(), mOptions);
+ }
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TransitionEnder)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TransitionEnder)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransitionEnder)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(TransitionEnder, mElement);
+void nsXULPopupManager::HidePopupCallback(
+ Element* aPopup, nsMenuPopupFrame* aPopupFrame, Element* aNextPopup,
+ Element* aLastPopup, PopupType aPopupType, HidePopupOptions aOptions) {
+ if (mCloseTimer && mTimerMenu == aPopupFrame) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+ mTimerMenu = nullptr;
+ }
+
+ // The popup to hide is aPopup. Search the list again to find the item that
+ // corresponds to the popup to hide aPopup. This is done because it's
+ // possible someone added another item (attempted to open another popup)
+ // or removed a popup frame during the event processing so the item isn't at
+ // the front anymore.
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Element() == aPopup) {
+ RemoveMenuChainItem(item);
+ SetCaptureState(aPopup);
+ break;
+ }
+ }
+
+ AutoWeakFrame weakFrame(aPopupFrame);
+ aPopupFrame->HidePopup(aOptions.contains(HidePopupOption::DeselectMenu),
+ ePopupClosed);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // send the popuphidden event synchronously. This event has no default
+ // behaviour.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupHidden, nullptr,
+ WidgetMouseEvent::eReal);
+ RefPtr<nsPresContext> presContext = aPopupFrame->PresContext();
+ EventDispatcher::Dispatch(aPopup, presContext, &event, nullptr, &status);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Force any popups that might be anchored on elements within this popup to
+ // update.
+ UpdatePopupPositions(presContext->RefreshDriver());
+
+ // if there are more popups to close, look for the next one
+ if (aNextPopup && aPopup != aLastPopup) {
+ nsMenuChainItem* foundMenu = FindPopup(aNextPopup);
+
+ // continue hiding the chain of popups until the last popup aLastPopup
+ // is reached, or until a popup of a different type is reached. This
+ // last check is needed so that a menulist inside a non-menu panel only
+ // closes the menu and not the panel as well.
+ if (foundMenu && (aLastPopup || aPopupType == foundMenu->GetPopupType())) {
+ nsCOMPtr<Element> popupToHide = foundMenu->Element();
+ nsMenuChainItem* parent = foundMenu->GetParent();
+
+ nsCOMPtr<Element> nextPopup;
+ if (parent && popupToHide != aLastPopup) nextPopup = parent->Element();
+
+ nsMenuPopupFrame* popupFrame = foundMenu->Frame();
+ nsPopupState state = popupFrame->PopupState();
+ if (state == ePopupHiding) return;
+ if (state != ePopupInvisible) popupFrame->SetPopupState(ePopupHiding);
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup, presContext,
+ foundMenu->GetPopupType(), aOptions);
+ }
+ }
+}
+
+void nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup,
+ int32_t aDelay) {
+ // Don't close up immediately.
+ // Kick off a close timer.
+ KillMenuTimer();
+
+ // Kick off the timer.
+ nsIEventTarget* target =
+ aPopup->PopupElement().OwnerDoc()->EventTargetFor(TaskCategory::Other);
+ NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mCloseTimer),
+ [](nsITimer* aTimer, void* aClosure) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->KillMenuTimer();
+ }
+ },
+ nullptr, aDelay, nsITimer::TYPE_ONE_SHOT, "KillMenuTimer", target);
+ // the popup will call PopupDestroyed if it is destroyed, which checks if it
+ // is set to mTimerMenu, so it should be safe to keep a reference to it
+ mTimerMenu = aPopup;
+}
+
+void nsXULPopupManager::HidePopupsInList(
+ const nsTArray<nsMenuPopupFrame*>& aFrames) {
+ // Create a weak frame list. This is done in a separate array with the
+ // right capacity predetermined to avoid multiple allocations.
+ nsTArray<WeakFrame> weakPopups(aFrames.Length());
+ uint32_t f;
+ for (f = 0; f < aFrames.Length(); f++) {
+ WeakFrame* wframe = weakPopups.AppendElement();
+ if (wframe) *wframe = aFrames[f];
+ }
+
+ for (f = 0; f < weakPopups.Length(); f++) {
+ // check to ensure that the frame is still alive before hiding it.
+ if (weakPopups[f].IsAlive()) {
+ auto* frame = static_cast<nsMenuPopupFrame*>(weakPopups[f].GetFrame());
+ frame->HidePopup(true, ePopupInvisible);
+ }
+ }
+
+ SetCaptureState(nullptr);
+}
+
+bool nsXULPopupManager::IsChildOfDocShell(Document* aDoc,
+ nsIDocShellTreeItem* aExpected) {
+ nsCOMPtr<nsIDocShellTreeItem> docShellItem(aDoc->GetDocShell());
+ while (docShellItem) {
+ if (docShellItem == aExpected) return true;
+
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ docShellItem->GetInProcessParent(getter_AddRefs(parent));
+ docShellItem = parent;
+ }
+
+ return false;
+}
+
+void nsXULPopupManager::HidePopupsInDocShell(
+ nsIDocShellTreeItem* aDocShellToHide) {
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+
+ // Iterate to get the set of popup frames to hide
+ nsMenuChainItem* item = mPopups.get();
+ while (item) {
+ // Get the parent before calling detach so that we can keep iterating.
+ nsMenuChainItem* parent = item->GetParent();
+ if (item->Frame()->PopupState() != ePopupInvisible &&
+ IsChildOfDocShell(item->Element()->OwnerDoc(), aDocShellToHide)) {
+ nsMenuPopupFrame* frame = item->Frame();
+ RemoveMenuChainItem(item);
+ popupsToHide.AppendElement(frame);
+ }
+ item = parent;
+ }
+
+ HidePopupsInList(popupsToHide);
+}
+
+void nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
+ item->CheckForAnchorChange();
+ }
+ }
+}
+
+void nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame() == aPopup) {
+ item->UpdateFollowAnchor();
+ break;
+ }
+ }
+}
+
+void nsXULPopupManager::HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode) {
+ if (aMode == CloseMenuMode_None) {
+ return;
+ }
+
+ // When a menuitem is selected to be executed, first hide all the open
+ // popups, but don't remove them yet. This is needed when a menu command
+ // opens a modal dialog. The views associated with the popups needed to be
+ // hidden and the accesibility events fired before the command executes, but
+ // the popuphiding/popuphidden events are fired afterwards.
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item) {
+ // if it isn't a <menupopup>, don't close it automatically
+ if (!item->IsMenu()) {
+ break;
+ }
+
+ nsMenuChainItem* next = item->GetParent();
+ popupsToHide.AppendElement(item->Frame());
+ if (aMode == CloseMenuMode_Single) {
+ // only close one level of menu
+ break;
+ }
+ item = next;
+ }
+
+ // Now hide the popups. If the closemenu mode is auto, deselect the menu,
+ // otherwise only one popup is closing, so keep the parent menu selected.
+ HidePopupsInList(popupsToHide);
+}
+
+void nsXULPopupManager::ExecuteMenu(nsIContent* aMenu,
+ nsXULMenuCommandEvent* aEvent) {
+ CloseMenuMode cmm = GetCloseMenuMode(aMenu);
+ HideOpenMenusBeforeExecutingMenu(cmm);
+ aEvent->SetCloseMenuMode(cmm);
+ nsCOMPtr<nsIRunnable> event = aEvent;
+ aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+}
+
+bool nsXULPopupManager::ActivateNativeMenuItem(nsIContent* aItem,
+ mozilla::Modifiers aModifiers,
+ int16_t aButton,
+ mozilla::ErrorResult& aRv) {
+ if (mNativeMenu && aItem->IsElement() &&
+ mNativeMenu->Element()->Contains(aItem)) {
+ mNativeMenu->ActivateItem(aItem->AsElement(), aModifiers, aButton, aRv);
+ return true;
+ }
+ return false;
+}
+
+nsEventStatus nsXULPopupManager::FirePopupShowingEvent(
+ const PendingPopup& aPendingPopup, nsPresContext* aPresContext) {
+ // Cache the pending popup so that the trigger node and other properties can
+ // be retrieved during the popupshowing event. It will be cleared below after
+ // the event has fired.
+ AutoRestore<const PendingPopup*> restorePendingPopup(mPendingPopup);
+ mPendingPopup = &aPendingPopup;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
+ WidgetMouseEvent::eReal);
+
+ // coordinates are relative to the root widget
+ nsPresContext* rootPresContext = aPresContext->GetRootPresContext();
+ if (rootPresContext) {
+ event.mWidget =
+ rootPresContext->PresShell()->GetViewManager()->GetRootWidget();
+ } else {
+ event.mWidget = nullptr;
+ }
+
+ event.mInputSource = aPendingPopup.MouseInputSource();
+ event.mRefPoint = aPendingPopup.mMousePoint;
+ event.mModifiers = aPendingPopup.mModifiers;
+ RefPtr<nsIContent> popup = aPendingPopup.mPopup;
+ EventDispatcher::Dispatch(popup, aPresContext, &event, nullptr, &status);
+
+ return status;
+}
+
+void nsXULPopupManager::BeginShowingPopup(const PendingPopup& aPendingPopup,
+ bool aIsContextMenu,
+ bool aSelectFirstItem) {
+ RefPtr<Element> popup = aPendingPopup.mPopup;
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ if (NS_WARN_IF(!popupFrame)) {
+ return;
+ }
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ RefPtr<PresShell> presShell = presContext->PresShell();
+ presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+
+ PopupType popupType = popupFrame->GetPopupType();
+
+ nsEventStatus status = FirePopupShowingEvent(aPendingPopup, presContext);
+
+ // if a panel, blur whatever has focus so that the panel can take the focus.
+ // This is done after the popupshowing event in case that event is cancelled.
+ // Using noautofocus="true" will disable this behaviour, which is needed for
+ // the autocomplete widget as it manages focus itself.
+ if (popupType == PopupType::Panel &&
+ !popup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::noautofocus,
+ nsGkAtoms::_true, eCaseMatters)) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ Document* doc = popup->GetUncomposedDoc();
+
+ // Only remove the focus if the currently focused item is ouside the
+ // popup. It isn't a big deal if the current focus is in a child popup
+ // inside the popup as that shouldn't be visible. This check ensures that
+ // a node inside the popup that is focused during a popupshowing event
+ // remains focused.
+ RefPtr<Element> currentFocus = fm->GetFocusedElement();
+ if (doc && currentFocus &&
+ !nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, popup)) {
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
+ fm->ClearFocus(outerWindow);
+ }
+ }
+ }
+
+ popup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
+
+ // get the frame again in case it went away
+ popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return;
+ }
+ // if the event was cancelled or the popup was closed in the mean time, don't
+ // open the popup, reset its state back to closed and clear its trigger
+ // content.
+ if (popupFrame->PopupState() == ePopupClosed ||
+ status == nsEventStatus_eConsumeNoDefault) {
+ popupFrame->SetPopupState(ePopupClosed);
+ popupFrame->ClearTriggerContent();
+ return;
+ }
+ // Now check if we need to fire the popuppositioned event. If not, call
+ // ShowPopupCallback directly.
+ // The popuppositioned event only fires on arrow panels for now.
+ if (popup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow,
+ eCaseMatters)) {
+ popupFrame->ShowWithPositionedEvent();
+ presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+ } else {
+ ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
+ }
+}
+
+void nsXULPopupManager::FirePopupHidingEvent(Element* aPopup,
+ Element* aNextPopup,
+ Element* aLastPopup,
+ nsPresContext* aPresContext,
+ PopupType aPopupType,
+ HidePopupOptions aOptions) {
+ nsCOMPtr<nsIContent> popup = aPopup;
+ RefPtr<PresShell> presShell = aPresContext->PresShell();
+ Unused << presShell; // This presShell may be keeping things alive
+ // on non GTK platforms
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupHiding, nullptr,
+ WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
+
+ // when a panel is closed, blur whatever has focus inside the popup
+ if (aPopupType == PopupType::Panel &&
+ (!aPopup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::noautofocus,
+ nsGkAtoms::_true, eCaseMatters))) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ Document* doc = aPopup->GetUncomposedDoc();
+
+ // Remove the focus from the focused node only if it is inside the popup.
+ RefPtr<Element> currentFocus = fm->GetFocusedElement();
+ if (doc && currentFocus &&
+ nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, aPopup)) {
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
+ fm->ClearFocus(outerWindow);
+ }
+ }
+ }
+
+ aPopup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
+
+ // get frame again in case it went away
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return;
+ }
+
+ // If the event was cancelled, don't hide the popup, and reset its
+ // state back to open. Only popups in chrome shells can prevent a popup
+ // from hiding.
+ if (status == nsEventStatus_eConsumeNoDefault &&
+ !popupFrame->IsInContentShell()) {
+ // XXXndeakin
+ // If an attempt was made to hide this popup before the popupshown event
+ // fired, then ePopupShown is set here even though it should be
+ // ePopupVisible. This probably isn't worth the hassle of handling.
+ popupFrame->SetPopupState(ePopupShown);
+ return;
+ }
+
+ const bool shouldAnimate = [&] {
+ if (!LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations)) {
+ // Animations are not supported by the platform, avoid transitioning.
+ return false;
+ }
+ if (aOptions.contains(HidePopupOption::DisableAnimations)) {
+ // Animations are not allowed by our caller.
+ return false;
+ }
+ if (aNextPopup) {
+ // If there is a next popup, indicating that mutliple popups are rolling
+ // up, don't wait and hide the popup right away since the effect would
+ // likely be undesirable.
+ return false;
+ }
+ nsAutoString animate;
+ if (!aPopup->GetAttr(nsGkAtoms::animate, animate)) {
+ return false;
+ }
+ // If animate="false" then don't transition at all.
+ if (animate.EqualsLiteral("false")) {
+ return false;
+ }
+ // If animate="cancel", only show the transition if cancelling the popup
+ // or rolling up.
+ if (animate.EqualsLiteral("cancel") &&
+ !aOptions.contains(HidePopupOption::IsRollup)) {
+ return false;
+ }
+ return true;
+ }();
+ // If we should animate the popup, check if it has a closing transition
+ // and wait for it to finish.
+ // The transition would still occur either way, but if we don't wait the
+ // view will be hidden and you won't be able to see it.
+ if (shouldAnimate && AnimationUtils::HasCurrentTransitions(
+ aPopup, PseudoStyleType::NotPseudo)) {
+ RefPtr<TransitionEnder> ender = new TransitionEnder(aPopup, aOptions);
+ aPopup->AddSystemEventListener(u"transitionend"_ns, ender, false, false);
+ aPopup->AddSystemEventListener(u"transitioncancel"_ns, ender, false, false);
+ return;
+ }
+
+ HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType,
+ aOptions);
+}
+
+bool nsXULPopupManager::IsPopupOpen(Element* aPopup) {
+ if (mNativeMenu && mNativeMenu->Element() == aPopup) {
+ return true;
+ }
+
+ // a popup is open if it is in the open list. The assertions ensure that the
+ // frame is in the correct state. If the popup is in the hiding or invisible
+ // state, it will still be in the open popup list until it is closed.
+ if (nsMenuChainItem* item = FindPopup(aPopup)) {
+ NS_ASSERTION(item->Frame()->IsOpen() ||
+ item->Frame()->PopupState() == ePopupHiding ||
+ item->Frame()->PopupState() == ePopupInvisible,
+ "popup in open list not actually open");
+ Unused << item;
+ return true;
+ }
+ return false;
+}
+
+nsIFrame* nsXULPopupManager::GetTopPopup(PopupType aType) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->IsVisible() &&
+ (item->GetPopupType() == aType || aType == PopupType::Any)) {
+ return item->Frame();
+ }
+ }
+ return nullptr;
+}
+
+nsIContent* nsXULPopupManager::GetTopActiveMenuItemContent() {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (!item->Frame()->IsVisible()) {
+ continue;
+ }
+ if (auto* content = item->Frame()->PopupElement().GetActiveMenuChild()) {
+ return content;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame*>& aPopups) {
+ aPopups.Clear();
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ // Skip panels which are not visible as well as popups that are transparent
+ // to mouse events.
+ if (item->Frame()->IsVisible() && !item->Frame()->IsMouseTransparent()) {
+ aPopups.AppendElement(item->Frame());
+ }
+ }
+}
+
+already_AddRefed<nsINode> nsXULPopupManager::GetLastTriggerNode(
+ Document* aDocument, bool aIsTooltip) {
+ if (!aDocument) return nullptr;
+
+ RefPtr<nsINode> node;
+
+ // If a pending popup is set, it means that a popupshowing event is being
+ // fired. In this case, just use the cached node, as the popup is not yet in
+ // the list of open popups.
+ RefPtr<nsIContent> openingPopup =
+ mPendingPopup ? mPendingPopup->mPopup : nullptr;
+ if (openingPopup && openingPopup->GetUncomposedDoc() == aDocument &&
+ aIsTooltip == openingPopup->IsXULElement(nsGkAtoms::tooltip)) {
+ node = nsMenuPopupFrame::GetTriggerContent(
+ GetPopupFrameForContent(openingPopup, false));
+ } else if (mNativeMenu && !aIsTooltip) {
+ RefPtr<dom::Element> popup = mNativeMenu->Element();
+ if (popup->GetUncomposedDoc() == aDocument) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, false);
+ node = nsMenuPopupFrame::GetTriggerContent(popupFrame);
+ }
+ } else {
+ for (nsMenuChainItem* item = mPopups.get(); item;
+ item = item->GetParent()) {
+ // look for a popup of the same type and document.
+ if ((item->GetPopupType() == PopupType::Tooltip) == aIsTooltip &&
+ item->Element()->GetUncomposedDoc() == aDocument) {
+ node = nsMenuPopupFrame::GetTriggerContent(item->Frame());
+ if (node) {
+ break;
+ }
+ }
+ }
+ }
+
+ return node.forget();
+}
+
+bool nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup) {
+ // if a popup's IsOpen method returns true, then the popup must always be in
+ // the popup chain scanned in IsPopupOpen.
+ NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(&aPopup->PopupElement()),
+ "popup frame state doesn't match XULPopupManager open state");
+
+ nsPopupState state = aPopup->PopupState();
+
+ // if the popup is not in the open popup chain, then it must have a state that
+ // is either closed, in the process of being shown, or invisible.
+ NS_ASSERTION(IsPopupOpen(&aPopup->PopupElement()) || state == ePopupClosed ||
+ state == ePopupShowing || state == ePopupPositioning ||
+ state == ePopupInvisible,
+ "popup not in XULPopupManager open list is open");
+
+ // don't show popups unless they are closed or invisible
+ if (state != ePopupClosed && state != ePopupInvisible) return false;
+
+ // Don't show popups that we already have in our popup chain
+ if (IsPopupOpen(&aPopup->PopupElement())) {
+ NS_WARNING("Refusing to show duplicate popup");
+ return false;
+ }
+
+ // if the popup was just rolled up, don't reopen it
+ if (mozilla::widget::nsAutoRollup::GetLastRollup() == aPopup->GetContent()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = aPopup->PresContext()->GetDocShell();
+
+ nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(docShell);
+ if (!baseWin) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
+ if (!root) {
+ return false;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> rootWin = root->GetWindow();
+
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess(),
+ "Cannot have XUL in content process showing popups.");
+
+ // chrome shells can always open popups, but other types of shells can only
+ // open popups when they are focused and visible
+ if (docShell->ItemType() != nsIDocShellTreeItem::typeChrome) {
+ // only allow popups in active windows
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (!fm || !rootWin) {
+ return false;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow();
+ if (activeWindow != rootWin) {
+ return false;
+ }
+
+ // only allow popups in visible frames
+ // TODO: This visibility check should be replaced with a check of
+ // bc->IsActive(). It is okay for now since this is only called
+ // in the parent process. Bug 1698533.
+ bool visible;
+ baseWin->GetVisibility(&visible);
+ if (!visible) {
+ return false;
+ }
+ }
+
+ // platforms respond differently when an popup is opened in a minimized
+ // window, so this is always disabled.
+ nsCOMPtr<nsIWidget> mainWidget;
+ baseWin->GetMainWidget(getter_AddRefs(mainWidget));
+ if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
+ return false;
+ }
+
+#ifdef XP_MACOSX
+ if (rootWin) {
+ auto globalWin = nsGlobalWindowOuter::Cast(rootWin.get());
+ if (globalWin->IsInModalState()) {
+ return false;
+ }
+ }
+#endif
+
+ // cannot open a popup that is a submenu of a menupopup that isn't open.
+ if (auto* menu = aPopup->PopupElement().GetContainingMenu()) {
+ if (auto* parent = XULPopupElement::FromNodeOrNull(menu->GetMenuParent())) {
+ nsMenuPopupFrame* f = do_QueryFrame(parent->GetPrimaryFrame());
+ if (f && !f->IsOpen()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup) {
+ // when a popup frame is destroyed, just unhook it from the list of popups
+ CancelMenuTimer(aPopup);
+
+ nsMenuChainItem* item = FindPopup(&aPopup->PopupElement());
+ if (!item) {
+ return;
+ }
+
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+ // XXXndeakin shouldn't this only happen for menus?
+ if (!item->IsNoAutoHide() && item->Frame()->PopupState() != ePopupInvisible) {
+ // Iterate through any child menus and hide them as well, since the
+ // parent is going away. We won't remove them from the list yet, just
+ // hide them, as they will be removed from the list when this function
+ // gets called for that child frame.
+ for (auto* child = item->GetChild(); child; child = child->GetChild()) {
+ // If the popup is a child frame of the menu that was destroyed, add it
+ // to the list of popups to hide. Don't bother with the events since the
+ // frames are going away. If the child menu is not a child frame, for
+ // example, a context menu, use HidePopup instead, but call it
+ // asynchronously since we are in the middle of frame destruction.
+ if (nsLayoutUtils::IsProperAncestorFrame(item->Frame(), child->Frame())) {
+ popupsToHide.AppendElement(child->Frame());
+ } else {
+ // HidePopup will take care of hiding any of its children, so
+ // break out afterwards
+ HidePopup(child->Element(), {HidePopupOption::Async});
+ break;
+ }
+ }
+ }
+
+ RemoveMenuChainItem(item);
+ HidePopupsInList(popupsToHide);
+}
+
+bool nsXULPopupManager::HasContextMenu(nsMenuPopupFrame* aPopup) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item && item->Frame() != aPopup) {
+ if (item->IsContextMenu()) return true;
+ item = item->GetParent();
+ }
+
+ return false;
+}
+
+void nsXULPopupManager::SetCaptureState(nsIContent* aOldPopup) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && aOldPopup == item->Element()) return;
+
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(false);
+ mWidget = nullptr;
+ }
+
+ if (item) {
+ nsMenuPopupFrame* popup = item->Frame();
+ mWidget = popup->GetWidget();
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(true);
+ }
+ }
+
+ UpdateKeyboardListeners();
+}
+
+void nsXULPopupManager::UpdateKeyboardListeners() {
+ nsCOMPtr<EventTarget> newTarget;
+ bool isForMenu = false;
+ if (nsMenuChainItem* item = GetTopVisibleMenu()) {
+ if (item->IgnoreKeys() != eIgnoreKeys_True) {
+ newTarget = item->Element()->GetComposedDoc();
+ }
+ isForMenu = item->GetPopupType() == PopupType::Menu;
+ } else if (mActiveMenuBar && mActiveMenuBar->IsActiveByKeyboard()) {
+ // Only listen for key events iff menubar is activated via key, see
+ // bug 1818241.
+ newTarget = mActiveMenuBar->GetComposedDoc();
+ isForMenu = true;
+ }
+
+ if (mKeyListener != newTarget) {
+ OwningNonNull<nsXULPopupManager> kungFuDeathGrip(*this);
+ if (mKeyListener) {
+ mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
+ mKeyListener = nullptr;
+ nsContentUtils::NotifyInstalledMenuKeyboardListener(false);
+ }
+
+ if (newTarget) {
+ newTarget->AddEventListener(u"keypress"_ns, this, true);
+ newTarget->AddEventListener(u"keydown"_ns, this, true);
+ newTarget->AddEventListener(u"keyup"_ns, this, true);
+ nsContentUtils::NotifyInstalledMenuKeyboardListener(isForMenu);
+ mKeyListener = newTarget;
+ }
+ }
+}
+
+void nsXULPopupManager::UpdateMenuItems(Element* aPopup) {
+ // Walk all of the menu's children, checking to see if any of them has a
+ // command attribute. If so, then several attributes must potentially be
+ // updated.
+
+ nsCOMPtr<Document> document = aPopup->GetUncomposedDoc();
+ if (!document) {
+ return;
+ }
+
+ // When a menu is opened, make sure that command updating is unlocked first.
+ nsCOMPtr<nsIDOMXULCommandDispatcher> commandDispatcher =
+ document->GetCommandDispatcher();
+ if (commandDispatcher) {
+ commandDispatcher->Unlock();
+ }
+
+ for (nsCOMPtr<nsIContent> grandChild = aPopup->GetFirstChild(); grandChild;
+ grandChild = grandChild->GetNextSibling()) {
+ if (grandChild->IsXULElement(nsGkAtoms::menugroup)) {
+ if (grandChild->GetChildCount() == 0) {
+ continue;
+ }
+ grandChild = grandChild->GetFirstChild();
+ }
+ if (grandChild->IsXULElement(nsGkAtoms::menuitem)) {
+ // See if we have a command attribute.
+ Element* grandChildElement = grandChild->AsElement();
+ nsAutoString command;
+ grandChildElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
+ command);
+ if (!command.IsEmpty()) {
+ // We do! Look it up in our document
+ RefPtr<dom::Element> commandElement = document->GetElementById(command);
+ if (commandElement) {
+ nsAutoString commandValue;
+ // The menu's disabled state needs to be updated to match the command.
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandValue, true);
+ else
+ grandChildElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ true);
+
+ // The menu's label, accesskey checked and hidden states need to be
+ // updated to match the command. Note that unlike the disabled state
+ // if the command has *no* value, we assume the menu is supplying its
+ // own.
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
+ commandValue, true);
+ }
+ }
+ }
+ if (!grandChild->GetNextSibling() &&
+ grandChild->GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
+ grandChild = grandChild->GetParent();
+ }
+ }
+}
+
+// Notify
+//
+// The item selection timer has fired, we might have to readjust the
+// selected item. There are two cases here that we are trying to deal with:
+// (1) diagonal movement from a parent menu to a submenu passing briefly over
+// other items, and
+// (2) moving out from a submenu to a parent or grandparent menu.
+// In both cases, |mTimerMenu| is the menu item that might have an open submenu
+// and the first item in |mPopups| is the item the mouse is currently over,
+// which could be none of them.
+//
+// case (1):
+// As the mouse moves from the parent item of a submenu (we'll call 'A')
+// diagonally into the submenu, it probably passes through one or more
+// sibilings (B). As the mouse passes through B, it becomes the current menu
+// item and the timer is set and mTimerMenu is set to A. Before the timer
+// fires, the mouse leaves the menu containing A and B and enters the submenus.
+// Now when the timer fires, |mPopups| is null (!= |mTimerMenu|) so we have to
+// see if anything in A's children is selected (recall that even disabled items
+// are selected, the style just doesn't show it). If that is the case, we need
+// to set the selected item back to A.
+//
+// case (2);
+// Item A has an open submenu, and in it there is an item (B) which also has an
+// open submenu (so there are 3 menus displayed right now). The mouse then
+// leaves B's child submenu and selects an item that is a sibling of A, call it
+// C. When the mouse enters C, the timer is set and |mTimerMenu| is A and
+// |mPopups| is C. As the timer fires, the mouse is still within C. The correct
+// behavior is to set the current item to C and close up the chain parented at
+// A.
+//
+// This brings up the question of is the logic of case (1) enough? The answer
+// is no, and is discussed in bugzilla bug 29400. Case (1) asks if A's submenu
+// has a selected child, and if it does, set the selected item to A. Because B
+// has a submenu open, it is selected and as a result, A is set to be the
+// selected item even though the mouse rests in C -- very wrong.
+//
+// The solution is to use the same idea, but instead of only checking one
+// level, drill all the way down to the deepest open submenu and check if it
+// has something selected. Since the mouse is in a grandparent, it won't, and
+// we know that we can safely close up A and all its children.
+//
+// The code below melds the two cases together.
+//
+void nsXULPopupManager::KillMenuTimer() {
+ if (mCloseTimer && mTimerMenu) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+
+ if (mTimerMenu->IsOpen()) {
+ HidePopup(&mTimerMenu->PopupElement(), {HidePopupOption::Async});
+ }
+ }
+
+ mTimerMenu = nullptr;
+}
+
+void nsXULPopupManager::CancelMenuTimer(nsMenuPopupFrame* aMenu) {
+ if (mCloseTimer && mTimerMenu == aMenu) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+ mTimerMenu = nullptr;
+ }
+}
+
+bool nsXULPopupManager::HandleShortcutNavigation(KeyboardEvent& aKeyEvent,
+ nsMenuPopupFrame* aFrame) {
+ // On Windows, don't check shortcuts when the accelerator key is down.
+#ifdef XP_WIN
+ WidgetInputEvent* evt = aKeyEvent.WidgetEventPtr()->AsInputEvent();
+ if (evt && evt->IsAccel()) {
+ return false;
+ }
+#endif
+
+ if (!aFrame) {
+ if (nsMenuChainItem* item = GetTopVisibleMenu()) {
+ aFrame = item->Frame();
+ }
+ }
+
+ if (aFrame) {
+ bool action = false;
+ RefPtr result = aFrame->FindMenuWithShortcut(aKeyEvent, action);
+ if (!result) {
+ return false;
+ }
+ RefPtr popup = &aFrame->PopupElement();
+ popup->SetActiveMenuChild(result, XULMenuParentElement::ByKey::Yes);
+ if (action) {
+ WidgetEvent* evt = aKeyEvent.WidgetEventPtr();
+ result->HandleEnterKeyPress(*evt);
+ }
+ return true;
+ }
+
+ // Only do shortcut navigation when the menubar is activated via keyboard.
+ if (mActiveMenuBar) {
+ RefPtr menubar = mActiveMenuBar;
+ if (RefPtr result = menubar->FindMenuWithShortcut(aKeyEvent)) {
+ result->OpenMenuPopup(true);
+ return true;
+ }
+#ifdef XP_WIN
+ // Behavior on Windows - this item is on the menu bar, beep and deactivate
+ // the menu bar.
+ // TODO(emilio): This is rather odd, and I cannot get the beep to work,
+ // but this matches what old code was doing...
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+ menubar->SetActive(false);
+#endif
+ }
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) {
+ if (nsMenuChainItem* nextitem = GetTopVisibleMenu()) {
+ nextitem->Element()->OwnerDoc()->FlushPendingNotifications(
+ FlushType::Frames);
+ }
+
+ // navigate up through the open menus, looking for the topmost one
+ // in the same hierarchy
+ nsMenuChainItem* item = nullptr;
+ nsMenuChainItem* nextitem = GetTopVisibleMenu();
+ while (nextitem) {
+ item = nextitem;
+ nextitem = item->GetParent();
+
+ if (!nextitem) {
+ break;
+ }
+ // stop if the parent isn't a menu
+ if (!nextitem->IsMenu()) {
+ break;
+ }
+
+ // Check to make sure that the parent is actually the parent menu. It won't
+ // be if the parent is in a different frame hierarchy, for example, for a
+ // context menu opened on another menu.
+ XULPopupElement& expectedParent = nextitem->Frame()->PopupElement();
+ auto* menu = item->Frame()->PopupElement().GetContainingMenu();
+ if (!menu || menu->GetMenuParent() != &expectedParent) {
+ break;
+ }
+ }
+
+ nsIFrame* itemFrame;
+ if (item) {
+ itemFrame = item->Frame();
+ } else if (mActiveMenuBar) {
+ itemFrame = mActiveMenuBar->GetPrimaryFrame();
+ if (!itemFrame) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+
+ nsNavigationDirection theDirection;
+ NS_ASSERTION(aKeyCode >= KeyboardEvent_Binding::DOM_VK_END &&
+ aKeyCode <= KeyboardEvent_Binding::DOM_VK_DOWN,
+ "Illegal key code");
+ theDirection = NS_DIRECTION_FROM_KEY_CODE(itemFrame, aKeyCode);
+
+ bool selectFirstItem = true;
+#ifdef MOZ_WIDGET_GTK
+ {
+ XULButtonElement* currentItem = nullptr;
+ if (item && mActiveMenuBar && NS_DIRECTION_IS_INLINE(theDirection)) {
+ currentItem = item->Frame()->PopupElement().GetActiveMenuChild();
+ // If nothing is selected in the menu and we have a menubar, let it
+ // handle the movement not to steal focus from it.
+ if (!currentItem) {
+ item = nullptr;
+ }
+ }
+ // On menu change, only select first item if an item is already selected.
+ selectFirstItem = !!currentItem;
+ }
+#endif
+
+ // if a popup is open, first check for navigation within the popup
+ if (item && HandleKeyboardNavigationInPopup(item, theDirection)) {
+ return true;
+ }
+
+ // no popup handled the key, so check the active menubar, if any
+ if (!mActiveMenuBar) {
+ return false;
+ }
+ RefPtr menubar = mActiveMenuBar;
+ if (NS_DIRECTION_IS_INLINE(theDirection)) {
+ RefPtr prevActiveItem = menubar->GetActiveMenuChild();
+ const bool open = prevActiveItem && prevActiveItem->IsMenuPopupOpen();
+ RefPtr nextItem = theDirection == eNavigationDirection_End
+ ? menubar->GetNextMenuItem()
+ : menubar->GetPrevMenuItem();
+ menubar->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ if (open && nextItem) {
+ nextItem->OpenMenuPopup(selectFirstItem);
+ }
+ return true;
+ }
+ if (NS_DIRECTION_IS_BLOCK(theDirection)) {
+ // Open the menu and select its first item.
+ if (RefPtr currentMenu = menubar->GetActiveMenuChild()) {
+ ShowMenu(currentMenu, selectFirstItem);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardNavigationInPopup(
+ nsMenuChainItem* item, nsMenuPopupFrame* aFrame,
+ nsNavigationDirection aDir) {
+ NS_ASSERTION(aFrame, "aFrame is null");
+ NS_ASSERTION(!item || item->Frame() == aFrame,
+ "aFrame is expected to be equal to item->Frame()");
+
+ using Wrap = XULMenuParentElement::Wrap;
+ RefPtr<XULPopupElement> menu = &aFrame->PopupElement();
+
+ aFrame->ClearIncrementalString();
+ RefPtr currentItem = aFrame->GetCurrentMenuItem();
+
+ // This method only gets called if we're open.
+ if (!currentItem && NS_DIRECTION_IS_INLINE(aDir)) {
+ // We've been opened, but we haven't had anything selected.
+ // We can handle End, but our parent handles Start.
+ if (aDir == eNavigationDirection_End) {
+ if (RefPtr nextItem = menu->GetNextMenuItem(Wrap::No)) {
+ menu->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ const bool isContainer = currentItem && !currentItem->IsMenuItem();
+ const bool isOpen = currentItem && currentItem->IsMenuPopupOpen();
+ if (isOpen) {
+ // For an open popup, have the child process the event
+ nsMenuChainItem* child = item ? item->GetChild() : nullptr;
+ if (child && HandleKeyboardNavigationInPopup(child, aDir)) {
+ return true;
+ }
+ } else if (aDir == eNavigationDirection_End && isContainer &&
+ !currentItem->IsDisabled()) {
+ currentItem->OpenMenuPopup(true);
+ return true;
+ }
+
+ // For block progression, we can move in either direction
+ if (NS_DIRECTION_IS_BLOCK(aDir) || NS_DIRECTION_IS_BLOCK_TO_EDGE(aDir)) {
+ RefPtr<XULButtonElement> nextItem = nullptr;
+
+ if (aDir == eNavigationDirection_Before ||
+ aDir == eNavigationDirection_After) {
+ // Cursor navigation does not wrap on Mac or for menulists on Windows.
+ auto wrap =
+#ifdef XP_WIN
+ aFrame->IsMenuList() ? Wrap::No : Wrap::Yes;
+#elif defined XP_MACOSX
+ Wrap::No;
+#else
+ Wrap::Yes;
+#endif
+
+ if (aDir == eNavigationDirection_Before) {
+ nextItem = menu->GetPrevMenuItem(wrap);
+ } else {
+ nextItem = menu->GetNextMenuItem(wrap);
+ }
+ } else if (aDir == eNavigationDirection_First) {
+ nextItem = menu->GetFirstMenuItem();
+ } else {
+ nextItem = menu->GetLastMenuItem();
+ }
+
+ if (nextItem) {
+ menu->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ return true;
+ }
+ } else if (currentItem && isOpen && aDir == eNavigationDirection_Start) {
+ // close a submenu when Left is pressed
+ if (nsMenuPopupFrame* popupFrame =
+ currentItem->GetMenuPopup(FlushType::None)) {
+ HidePopup(&popupFrame->PopupElement(), {});
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardEventWithKeyCode(
+ KeyboardEvent* aKeyEvent, nsMenuChainItem* aTopVisibleMenuItem) {
+ uint32_t keyCode = aKeyEvent->KeyCode();
+
+ // Escape should close panels, but the other keys should have no effect.
+ if (aTopVisibleMenuItem &&
+ aTopVisibleMenuItem->GetPopupType() != PopupType::Menu) {
+ if (keyCode == KeyboardEvent_Binding::DOM_VK_ESCAPE) {
+ HidePopup(aTopVisibleMenuItem->Element(), {HidePopupOption::IsRollup});
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+ }
+ return true;
+ }
+
+ bool consume = (aTopVisibleMenuItem || mActiveMenuBar);
+ switch (keyCode) {
+ case KeyboardEvent_Binding::DOM_VK_UP:
+ case KeyboardEvent_Binding::DOM_VK_DOWN:
+#ifndef XP_MACOSX
+ // roll up the popup when alt+up/down are pressed within a menulist.
+ if (aKeyEvent->AltKey() && aTopVisibleMenuItem &&
+ aTopVisibleMenuItem->Frame()->IsMenuList()) {
+ Rollup({});
+ break;
+ }
+ [[fallthrough]];
+#endif
+
+ case KeyboardEvent_Binding::DOM_VK_LEFT:
+ case KeyboardEvent_Binding::DOM_VK_RIGHT:
+ case KeyboardEvent_Binding::DOM_VK_HOME:
+ case KeyboardEvent_Binding::DOM_VK_END:
+ HandleKeyboardNavigation(keyCode);
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
+ case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
+ if (aTopVisibleMenuItem) {
+ aTopVisibleMenuItem->Frame()->ChangeByPage(
+ keyCode == KeyboardEvent_Binding::DOM_VK_PAGE_UP);
+ }
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_ESCAPE:
+ // Pressing Escape hides one level of menus only. If no menu is open,
+ // check if a menubar is active and inform it that a menu closed. Even
+ // though in this latter case, a menu didn't actually close, the effect
+ // ends up being the same. Similar for the tab key below.
+ if (aTopVisibleMenuItem) {
+ HidePopup(aTopVisibleMenuItem->Element(), {HidePopupOption::IsRollup});
+ } else if (mActiveMenuBar) {
+ RefPtr menubar = mActiveMenuBar;
+ menubar->SetActive(false);
+ }
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_TAB:
+#ifndef XP_MACOSX
+ case KeyboardEvent_Binding::DOM_VK_F10:
+#endif
+ if (aTopVisibleMenuItem &&
+ !aTopVisibleMenuItem->Frame()->PopupElement().AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::activateontab, nsGkAtoms::_true,
+ eCaseMatters)) {
+ // Close popups or deactivate menubar when Tab or F10 are pressed
+ Rollup({});
+ break;
+ } else if (mActiveMenuBar) {
+ RefPtr menubar = mActiveMenuBar;
+ menubar->SetActive(false);
+ break;
+ }
+ // Intentional fall-through to RETURN case
+ [[fallthrough]];
+
+ case KeyboardEvent_Binding::DOM_VK_RETURN: {
+ // If there is a popup open, check if the current item needs to be opened.
+ // Otherwise, tell the active menubar, if any, to activate the menu. The
+ // Enter method will return a menu if one needs to be opened as a result.
+ WidgetEvent* event = aKeyEvent->WidgetEventPtr();
+ if (aTopVisibleMenuItem) {
+ aTopVisibleMenuItem->Frame()->HandleEnterKeyPress(*event);
+ } else if (mActiveMenuBar) {
+ RefPtr menubar = mActiveMenuBar;
+ menubar->HandleEnterKeyPress(*event);
+ }
+ break;
+ }
+
+ default:
+ return false;
+ }
+
+ if (consume) {
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+ }
+ return true;
+}
+
+nsresult nsXULPopupManager::HandleEvent(Event* aEvent) {
+ RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
+ NS_ENSURE_TRUE(keyEvent, NS_ERROR_UNEXPECTED);
+
+ // handlers shouldn't be triggered by non-trusted events.
+ if (!keyEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ nsAutoString eventType;
+ keyEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("keyup")) {
+ return KeyUp(keyEvent);
+ }
+ if (eventType.EqualsLiteral("keydown")) {
+ return KeyDown(keyEvent);
+ }
+ if (eventType.EqualsLiteral("keypress")) {
+ return KeyPress(keyEvent);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
+
+nsresult nsXULPopupManager::UpdateIgnoreKeys(bool aIgnoreKeys) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item) {
+ item->SetIgnoreKeys(aIgnoreKeys ? eIgnoreKeys_True : eIgnoreKeys_Shortcuts);
+ }
+ UpdateKeyboardListeners();
+ return NS_OK;
+}
+
+nsPopupState nsXULPopupManager::GetPopupState(Element* aPopupElement) {
+ if (mNativeMenu && mNativeMenu->Element()->Contains(aPopupElement)) {
+ if (aPopupElement != mNativeMenu->Element()) {
+ // Submenu state is stored in mNativeMenuSubmenuStates.
+ return mNativeMenuSubmenuStates.MaybeGet(aPopupElement)
+ .valueOr(ePopupClosed);
+ }
+ // mNativeMenu->Element()'s state is stored in its nsMenuPopupFrame.
+ }
+
+ nsMenuPopupFrame* menuPopupFrame =
+ do_QueryFrame(aPopupElement->GetPrimaryFrame());
+ if (menuPopupFrame) {
+ return menuPopupFrame->PopupState();
+ }
+ return ePopupClosed;
+}
+
+nsresult nsXULPopupManager::KeyUp(KeyboardEvent* aKeyEvent) {
+ // don't do anything if a menu isn't open or a menubar isn't active
+ if (!mActiveMenuBar) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item || item->GetPopupType() != PopupType::Menu) return NS_OK;
+
+ if (item->IgnoreKeys() == eIgnoreKeys_Shortcuts) {
+ aKeyEvent->StopCrossProcessForwarding();
+ return NS_OK;
+ }
+ }
+
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+
+ return NS_OK; // I am consuming event
+}
+
+nsresult nsXULPopupManager::KeyDown(KeyboardEvent* aKeyEvent) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && item->Frame()->PopupElement().IsLocked()) {
+ return NS_OK;
+ }
+
+ if (HandleKeyboardEventWithKeyCode(aKeyEvent, item)) {
+ return NS_OK;
+ }
+
+ // don't do anything if a menu isn't open or a menubar isn't active
+ if (!mActiveMenuBar && (!item || item->GetPopupType() != PopupType::Menu))
+ return NS_OK;
+
+ // Since a menu was open, stop propagation of the event to keep other event
+ // listeners from becoming confused.
+ if (!item || item->IgnoreKeys() != eIgnoreKeys_Shortcuts) {
+ aKeyEvent->StopPropagation();
+ }
+
+ // If the key just pressed is the access key (usually Alt),
+ // dismiss and unfocus the menu.
+ uint32_t menuAccessKey = LookAndFeel::GetMenuAccessKey();
+ if (menuAccessKey) {
+ uint32_t theChar = aKeyEvent->KeyCode();
+
+ if (theChar == menuAccessKey) {
+ bool ctrl = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_CONTROL &&
+ aKeyEvent->CtrlKey());
+ bool alt = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_ALT &&
+ aKeyEvent->AltKey());
+ bool shift = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_SHIFT &&
+ aKeyEvent->ShiftKey());
+ bool meta = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_META &&
+ aKeyEvent->MetaKey());
+ if (!(ctrl || alt || shift || meta)) {
+ // The access key just went down and no other
+ // modifiers are already down.
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && !item->Frame()->IsMenuList()) {
+ Rollup({});
+ } else if (mActiveMenuBar) {
+ RefPtr menubar = mActiveMenuBar;
+ menubar->SetActive(false);
+ }
+
+ // Clear the item to avoid bugs as it may have been deleted during
+ // rollup.
+ item = nullptr;
+ }
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+ }
+
+ aKeyEvent->StopCrossProcessForwarding();
+ return NS_OK;
+}
+
+nsresult nsXULPopupManager::KeyPress(KeyboardEvent* aKeyEvent) {
+ // Don't check prevent default flag -- menus always get first shot at key
+ // events.
+
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && (item->Frame()->PopupElement().IsLocked() ||
+ item->GetPopupType() != PopupType::Menu)) {
+ return NS_OK;
+ }
+
+ // if a menu is open or a menubar is active, it consumes the key event
+ bool consume = (item || mActiveMenuBar);
+
+ WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
+ bool isAccel = evt && evt->IsAccel();
+
+ // When ignorekeys="shortcuts" is used, we don't call preventDefault on the
+ // key event when the accelerator key is pressed. This allows another
+ // listener to handle keys. For instance, this allows global shortcuts to
+ // still apply while a menu is open.
+ if (item && item->IgnoreKeys() == eIgnoreKeys_Shortcuts && isAccel) {
+ consume = false;
+ }
+
+ HandleShortcutNavigation(*aKeyEvent, nullptr);
+
+ aKeyEvent->StopCrossProcessForwarding();
+ if (consume) {
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+
+ return NS_OK; // I am consuming event
+}
+
+NS_IMETHODIMP
+nsXULPopupHidingEvent::Run() {
+ RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
+ Document* document = mPopup->GetUncomposedDoc();
+ if (pm && document) {
+ if (RefPtr<nsPresContext> presContext = document->GetPresContext()) {
+ nsCOMPtr<Element> popup = mPopup;
+ nsCOMPtr<Element> nextPopup = mNextPopup;
+ nsCOMPtr<Element> lastPopup = mLastPopup;
+ pm->FirePopupHidingEvent(popup, nextPopup, lastPopup, presContext,
+ mPopupType, mOptions);
+ }
+ }
+ return NS_OK;
+}
+
+bool nsXULPopupPositionedEvent::DispatchIfNeeded(Element* aPopup) {
+ // The popuppositioned event only fires on arrow panels for now.
+ if (aPopup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow,
+ eCaseMatters)) {
+ nsCOMPtr<nsIRunnable> event = new nsXULPopupPositionedEvent(aPopup);
+ aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ return true;
+ }
+
+ return false;
+}
+
+static void AlignmentPositionToString(nsMenuPopupFrame* aFrame,
+ nsAString& aString) {
+ aString.Truncate();
+ int8_t position = aFrame->GetAlignmentPosition();
+ switch (position) {
+ case POPUPPOSITION_AFTERSTART:
+ return aString.AssignLiteral("after_start");
+ case POPUPPOSITION_AFTEREND:
+ return aString.AssignLiteral("after_end");
+ case POPUPPOSITION_BEFORESTART:
+ return aString.AssignLiteral("before_start");
+ case POPUPPOSITION_BEFOREEND:
+ return aString.AssignLiteral("before_end");
+ case POPUPPOSITION_STARTBEFORE:
+ return aString.AssignLiteral("start_before");
+ case POPUPPOSITION_ENDBEFORE:
+ return aString.AssignLiteral("end_before");
+ case POPUPPOSITION_STARTAFTER:
+ return aString.AssignLiteral("start_after");
+ case POPUPPOSITION_ENDAFTER:
+ return aString.AssignLiteral("end_after");
+ case POPUPPOSITION_OVERLAP:
+ return aString.AssignLiteral("overlap");
+ case POPUPPOSITION_AFTERPOINTER:
+ return aString.AssignLiteral("after_pointer");
+ case POPUPPOSITION_SELECTION:
+ return aString.AssignLiteral("selection");
+ default:
+ // Leave as an empty string.
+ break;
+ }
+}
+
+NS_IMETHODIMP
+MOZ_CAN_RUN_SCRIPT_BOUNDARY
+nsXULPopupPositionedEvent::Run() {
+ RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return NS_OK;
+ }
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return NS_OK;
+ }
+
+ popupFrame->WillDispatchPopupPositioned();
+
+ // At this point, hidePopup may have been called but it currently has no
+ // way to stop this event. However, if hidePopup was called, the popup
+ // will now be in the hiding or closed state. If we are in the shown or
+ // positioning state instead, we can assume that we are still clear to
+ // open/move the popup
+ nsPopupState state = popupFrame->PopupState();
+ if (state != ePopupPositioning && state != ePopupShown) {
+ return NS_OK;
+ }
+
+ // Note that the offset might be along either the X or Y axis, but for the
+ // sake of simplicity we use a point with only the X axis set so we can
+ // use ToNearestPixels().
+ int32_t popupOffset = nsPoint(popupFrame->GetAlignmentOffset(), 0)
+ .ToNearestPixels(AppUnitsPerCSSPixel())
+ .x;
+
+ PopupPositionedEventInit init;
+ init.mComposed = true;
+ init.mIsAnchored = popupFrame->IsAnchored();
+ init.mAlignmentOffset = popupOffset;
+ AlignmentPositionToString(popupFrame, init.mAlignmentPosition);
+ RefPtr<PopupPositionedEvent> event =
+ PopupPositionedEvent::Constructor(mPopup, u"popuppositioned"_ns, init);
+ event->SetTrusted(true);
+
+ mPopup->DispatchEvent(*event);
+
+ // Get the popup frame and make sure it is still in the positioning
+ // state. If it isn't, someone may have tried to reshow or hide it
+ // during the popuppositioned event.
+ // Alternately, this event may have been fired in reponse to moving the
+ // popup rather than opening it. In that case, we are done.
+ popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+ if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
+ pm->ShowPopupCallback(mPopup, popupFrame, false, false);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXULMenuCommandEvent::Run() {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return NS_OK;
+ }
+
+ RefPtr menu = XULButtonElement::FromNode(mMenu);
+ MOZ_ASSERT(menu);
+ if (mFlipChecked) {
+ if (menu->GetXULBoolAttr(nsGkAtoms::checked)) {
+ menu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
+ } else {
+ menu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true);
+ }
+ }
+
+ // The order of the nsViewManager and PresShell COM pointers is
+ // important below. We want the pres shell to get released before the
+ // associated view manager on exit from this function.
+ // See bug 54233.
+ // XXXndeakin is this still needed?
+ RefPtr<nsPresContext> presContext = menu->OwnerDoc()->GetPresContext();
+ RefPtr<PresShell> presShell =
+ presContext ? presContext->PresShell() : nullptr;
+ RefPtr<nsViewManager> kungFuDeathGrip =
+ presShell ? presShell->GetViewManager() : nullptr;
+ Unused << kungFuDeathGrip; // Not referred to directly within this function
+
+ // Deselect ourselves.
+ if (mCloseMenuMode != CloseMenuMode_None) {
+ if (RefPtr parent = menu->GetMenuParent()) {
+ if (parent->GetActiveMenuChild() == menu) {
+ parent->SetActiveMenuChild(nullptr);
+ }
+ }
+ }
+
+ AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput);
+ nsContentUtils::DispatchXULCommand(
+ menu, mIsTrusted, nullptr, presShell, mModifiers & MODIFIER_CONTROL,
+ mModifiers & MODIFIER_ALT, mModifiers & MODIFIER_SHIFT,
+ mModifiers & MODIFIER_META, 0, mButton);
+
+ if (mCloseMenuMode != CloseMenuMode_None) {
+ if (RefPtr popup = menu->GetContainingPopupElement()) {
+ HidePopupOptions options{HidePopupOption::DeselectMenu};
+ if (mCloseMenuMode == CloseMenuMode_Auto) {
+ options += HidePopupOption::HideChain;
+ }
+ pm->HidePopup(popup, options);
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/layout/xul/nsXULPopupManager.h b/layout/xul/nsXULPopupManager.h
new file mode 100644
index 0000000000..d056322deb
--- /dev/null
+++ b/layout/xul/nsXULPopupManager.h
@@ -0,0 +1,898 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The XUL Popup Manager keeps track of all open popups.
+ */
+
+#ifndef nsXULPopupManager_h__
+#define nsXULPopupManager_h__
+
+#include "mozilla/Logging.h"
+#include "nsHashtablesFwd.h"
+#include "nsIContent.h"
+#include "nsIRollupListener.h"
+#include "nsIDOMEventListener.h"
+#include "Units.h"
+#include "nsPoint.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/widget/InitData.h"
+#include "mozilla/widget/NativeMenu.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file.
+#include "mozilla/dom/Element.h"
+
+// X.h defines KeyPress
+#ifdef KeyPress
+# undef KeyPress
+#endif
+
+/**
+ * There are two types that are used:
+ * - dismissable popups such as menus, which should close up when there is a
+ * click outside the popup. In this situation, the entire chain of menus
+ * above should also be closed.
+ * - panels, which stay open until a request is made to close them. This
+ * type is used by tooltips.
+ *
+ * When a new popup is opened, it is appended to the popup chain, stored in a
+ * linked list in mPopups.
+ * Popups are stored in this list linked from newest to oldest. When a click
+ * occurs outside one of the open dismissable popups, the chain is closed by
+ * calling Rollup.
+ */
+
+class nsContainerFrame;
+class nsITimer;
+class nsIDocShellTreeItem;
+class nsMenuPopupFrame;
+class nsPIDOMWindowOuter;
+class nsRefreshDriver;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Event;
+class KeyboardEvent;
+class UIEvent;
+class XULButtonElement;
+class XULMenuBarElement;
+class XULPopupElement;
+} // namespace dom
+} // namespace mozilla
+
+// XUL popups can be in several different states. When opening a popup, the
+// state changes as follows:
+// ePopupClosed - initial state
+// ePopupShowing - during the period when the popupshowing event fires
+// ePopupOpening - between the popupshowing event and being visible. Creation
+// of the child frames, layout and reflow occurs in this
+// state. The popup is stored in the popup manager's list of
+// open popups during this state.
+// ePopupVisible - layout is done and the popup's view and widget are made
+// visible. The popup is visible on screen but may be
+// transitioning. The popupshown event has not yet fired.
+// ePopupShown - the popup has been shown and is fully ready. This state is
+// assigned just before the popupshown event fires.
+// When closing a popup:
+// ePopupHidden - during the period when the popuphiding event fires and
+// the popup is removed.
+// ePopupClosed - the popup's widget is made invisible.
+enum nsPopupState {
+ // state when a popup is not open
+ ePopupClosed,
+ // state from when a popup is requested to be shown to after the
+ // popupshowing event has been fired.
+ ePopupShowing,
+ // state while a popup is waiting to be laid out and positioned
+ ePopupPositioning,
+ // state while a popup is open but the widget is not yet visible
+ ePopupOpening,
+ // state while a popup is visible and waiting for the popupshown event
+ ePopupVisible,
+ // state while a popup is open and visible on screen
+ ePopupShown,
+ // state from when a popup is requested to be hidden to when it is closed.
+ ePopupHiding,
+ // state which indicates that the popup was hidden without firing the
+ // popuphiding or popuphidden events. It is used when executing a menu
+ // command because the menu needs to be hidden before the command event
+ // fires, yet the popuphiding and popuphidden events are fired after. This
+ // state can also occur when the popup is removed because the document is
+ // unloaded.
+ ePopupInvisible
+};
+
+// when a menu command is executed, the closemenu attribute may be used
+// to define how the menu should be closed up
+enum CloseMenuMode {
+ CloseMenuMode_Auto, // close up the chain of menus, default value
+ CloseMenuMode_None, // don't close up any menus
+ CloseMenuMode_Single // close up only the menu the command is inside
+};
+
+/**
+ * nsNavigationDirection: an enum expressing navigation through the menus in
+ * terms which are independent of the directionality of the chrome. The
+ * terminology, derived from XSL-FO and CSS3 (e.g.
+ * http://www.w3.org/TR/css3-text/#TextLayout), is BASE (Before, After, Start,
+ * End), with the addition of First and Last (mapped to Home and End
+ * respectively).
+ *
+ * In languages such as English where the inline progression is left-to-right
+ * and the block progression is top-to-bottom (lr-tb), these terms will map out
+ * as in the following diagram
+ *
+ * --- inline progression --->
+ *
+ * First |
+ * ... |
+ * Before |
+ * +--------+ block
+ * Start | | End progression
+ * +--------+ |
+ * After |
+ * ... |
+ * Last V
+ *
+ */
+
+enum nsNavigationDirection {
+ eNavigationDirection_Last,
+ eNavigationDirection_First,
+ eNavigationDirection_Start,
+ eNavigationDirection_Before,
+ eNavigationDirection_End,
+ eNavigationDirection_After
+};
+
+enum nsIgnoreKeys {
+ eIgnoreKeys_False,
+ eIgnoreKeys_True,
+ eIgnoreKeys_Shortcuts,
+};
+
+enum class HidePopupOption : uint8_t {
+ // If the entire chain of menus should be closed.
+ HideChain,
+ // If the parent <menu> of the popup should not be deselected. This will not
+ // be set when the menu is closed by pressing the Escape key.
+ DeselectMenu,
+ // If the first popuphiding event should be sent asynchrously. This should
+ // be set if HidePopup is called from a frame.
+ Async,
+ // If this popup is hiding due to being cancelled.
+ IsRollup,
+ // Whether animations should be disabled for rolled-up popups.
+ DisableAnimations,
+};
+
+using HidePopupOptions = mozilla::EnumSet<HidePopupOption>;
+
+/**
+ * DirectionFromKeyCodeTable: two arrays, the first for left-to-right and the
+ * other for right-to-left, that map keycodes to values of
+ * nsNavigationDirection.
+ */
+extern const nsNavigationDirection DirectionFromKeyCodeTable[2][6];
+
+#define NS_DIRECTION_FROM_KEY_CODE(frame, keycode) \
+ (DirectionFromKeyCodeTable[static_cast<uint8_t>( \
+ (frame)->StyleVisibility()->mDirection)][( \
+ keycode)-mozilla::dom::KeyboardEvent_Binding::DOM_VK_END])
+
+// Used to hold information about a popup that is about to be opened.
+struct PendingPopup {
+ using Element = mozilla::dom::Element;
+ using Event = mozilla::dom::Event;
+
+ PendingPopup(Element* aPopup, Event* aEvent);
+
+ const RefPtr<Element> mPopup;
+ const RefPtr<Event> mEvent;
+
+ // Device pixels relative to the showing popup's presshell's
+ // root prescontext's root frame.
+ mozilla::LayoutDeviceIntPoint mMousePoint;
+
+ // Cached modifiers used to trigger the popup.
+ mozilla::Modifiers mModifiers;
+
+ already_AddRefed<nsIContent> GetTriggerContent() const;
+
+ void InitMousePoint();
+
+ void SetMousePoint(mozilla::LayoutDeviceIntPoint aMousePoint) {
+ mMousePoint = aMousePoint;
+ }
+
+ uint16_t MouseInputSource() const;
+};
+
+// nsMenuChainItem holds info about an open popup. Items are stored in a
+// doubly linked list. Note that the linked list is stored beginning from
+// the lowest child in a chain of menus, as this is the active submenu.
+class nsMenuChainItem {
+ using PopupType = mozilla::widget::PopupType;
+
+ nsMenuPopupFrame* mFrame; // the popup frame
+ PopupType mPopupType; // the popup type of the frame
+ bool mNoAutoHide; // true for noautohide panels
+ bool mIsContext; // true for context menus
+ bool mOnMenuBar; // true if the menu is on a menu bar
+ nsIgnoreKeys mIgnoreKeys; // indicates how keyboard listeners should be used
+
+ // True if the popup should maintain its position relative to the anchor when
+ // the anchor moves.
+ bool mFollowAnchor;
+
+ // The last seen position of the anchor, relative to the screen.
+ nsRect mCurrentRect;
+
+ mozilla::UniquePtr<nsMenuChainItem> mParent;
+ // Back pointer, safe because mChild keeps us alive.
+ nsMenuChainItem* mChild = nullptr;
+
+ public:
+ nsMenuChainItem(nsMenuPopupFrame* aFrame, bool aNoAutoHide, bool aIsContext,
+ PopupType aPopupType)
+ : mFrame(aFrame),
+ mPopupType(aPopupType),
+ mNoAutoHide(aNoAutoHide),
+ mIsContext(aIsContext),
+ mOnMenuBar(false),
+ mIgnoreKeys(eIgnoreKeys_False),
+ mFollowAnchor(false) {
+ NS_ASSERTION(aFrame, "null frame passed to nsMenuChainItem constructor");
+ MOZ_COUNT_CTOR(nsMenuChainItem);
+ }
+
+ MOZ_COUNTED_DTOR(nsMenuChainItem)
+
+ mozilla::dom::XULPopupElement* Element();
+ nsMenuPopupFrame* Frame() { return mFrame; }
+ PopupType GetPopupType() { return mPopupType; }
+ bool IsNoAutoHide() { return mNoAutoHide; }
+ void SetNoAutoHide(bool aNoAutoHide) { mNoAutoHide = aNoAutoHide; }
+ bool IsMenu() { return mPopupType == PopupType::Menu; }
+ bool IsContextMenu() { return mIsContext; }
+ nsIgnoreKeys IgnoreKeys() { return mIgnoreKeys; }
+ void SetIgnoreKeys(nsIgnoreKeys aIgnoreKeys) { mIgnoreKeys = aIgnoreKeys; }
+ bool IsOnMenuBar() { return mOnMenuBar; }
+ void SetOnMenuBar(bool aOnMenuBar) { mOnMenuBar = aOnMenuBar; }
+ nsMenuChainItem* GetParent() { return mParent.get(); }
+ nsMenuChainItem* GetChild() { return mChild; }
+ bool FollowsAnchor() { return mFollowAnchor; }
+ void UpdateFollowAnchor();
+ void CheckForAnchorChange();
+
+ // set the parent of this item to aParent, also changing the parent
+ // to have this as a child.
+ void SetParent(mozilla::UniquePtr<nsMenuChainItem> aParent);
+ // Removes the parent pointer and returns it.
+ mozilla::UniquePtr<nsMenuChainItem> Detach();
+};
+
+// this class is used for dispatching popuphiding events asynchronously.
+class nsXULPopupHidingEvent : public mozilla::Runnable {
+ using PopupType = mozilla::widget::PopupType;
+ using Element = mozilla::dom::Element;
+
+ public:
+ nsXULPopupHidingEvent(Element* aPopup, Element* aNextPopup,
+ Element* aLastPopup, PopupType aPopupType,
+ HidePopupOptions aOptions)
+ : mozilla::Runnable("nsXULPopupHidingEvent"),
+ mPopup(aPopup),
+ mNextPopup(aNextPopup),
+ mLastPopup(aLastPopup),
+ mPopupType(aPopupType),
+ mOptions(aOptions) {
+ NS_ASSERTION(aPopup,
+ "null popup supplied to nsXULPopupHidingEvent constructor");
+ // aNextPopup and aLastPopup may be null
+ }
+
+ NS_IMETHOD Run() override;
+
+ private:
+ nsCOMPtr<Element> mPopup;
+ nsCOMPtr<Element> mNextPopup;
+ nsCOMPtr<Element> mLastPopup;
+ PopupType mPopupType;
+ HidePopupOptions mOptions;
+};
+
+// this class is used for dispatching popuppositioned events asynchronously.
+class nsXULPopupPositionedEvent : public mozilla::Runnable {
+ using Element = mozilla::dom::Element;
+
+ public:
+ explicit nsXULPopupPositionedEvent(Element* aPopup)
+ : mozilla::Runnable("nsXULPopupPositionedEvent"), mPopup(aPopup) {
+ MOZ_ASSERT(aPopup);
+ }
+
+ NS_IMETHOD Run() override;
+
+ // Asynchronously dispatch a popuppositioned event at aPopup if this is a
+ // panel that should receieve such events. Return true if the event was sent.
+ static bool DispatchIfNeeded(Element* aPopup);
+
+ private:
+ const nsCOMPtr<Element> mPopup;
+};
+
+// this class is used for dispatching menu command events asynchronously.
+class nsXULMenuCommandEvent : public mozilla::Runnable {
+ using Element = mozilla::dom::Element;
+
+ public:
+ nsXULMenuCommandEvent(Element* aMenu, bool aIsTrusted,
+ mozilla::Modifiers aModifiers, bool aUserInput,
+ bool aFlipChecked, int16_t aButton)
+ : mozilla::Runnable("nsXULMenuCommandEvent"),
+ mMenu(aMenu),
+ mModifiers(aModifiers),
+ mButton(aButton),
+ mIsTrusted(aIsTrusted),
+ mUserInput(aUserInput),
+ mFlipChecked(aFlipChecked),
+ mCloseMenuMode(CloseMenuMode_Auto) {
+ NS_ASSERTION(aMenu,
+ "null menu supplied to nsXULMenuCommandEvent constructor");
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;
+
+ void SetCloseMenuMode(CloseMenuMode aCloseMenuMode) {
+ mCloseMenuMode = aCloseMenuMode;
+ }
+
+ private:
+ RefPtr<Element> mMenu;
+
+ mozilla::Modifiers mModifiers;
+ int16_t mButton;
+ bool mIsTrusted;
+ bool mUserInput;
+ bool mFlipChecked;
+ CloseMenuMode mCloseMenuMode;
+};
+
+class nsXULPopupManager final : public nsIDOMEventListener,
+ public nsIRollupListener,
+ public nsIObserver,
+ public mozilla::widget::NativeMenu::Observer {
+ public:
+ friend class nsXULPopupHidingEvent;
+ friend class nsXULPopupPositionedEvent;
+ friend class nsXULMenuCommandEvent;
+ friend class TransitionEnder;
+
+ using PopupType = mozilla::widget::PopupType;
+ using Element = mozilla::dom::Element;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ // nsIRollupListener
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ bool Rollup(const RollupOptions&,
+ nsIContent** aLastRolledUp = nullptr) override;
+ bool ShouldRollupOnMouseWheelEvent() override;
+ bool ShouldConsumeOnMouseWheelEvent() override;
+ bool ShouldRollupOnMouseActivate() override;
+ uint32_t GetSubmenuWidgetChain(nsTArray<nsIWidget*>* aWidgetChain) override;
+ nsIWidget* GetRollupWidget() override;
+ bool RollupNativeMenu() override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RollupTooltips();
+
+ enum class RollupKind { Tooltip, Menu };
+ MOZ_CAN_RUN_SCRIPT
+ bool RollupInternal(RollupKind, const RollupOptions&,
+ nsIContent** aLastRolledUp);
+
+ // NativeMenu::Observer
+ void OnNativeMenuOpened() override;
+ void OnNativeMenuClosed() override;
+ void OnNativeSubMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnNativeSubMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnNativeSubMenuClosed(mozilla::dom::Element* aPopupElement) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnNativeMenuWillActivateItem(
+ mozilla::dom::Element* aMenuItemElement) override;
+
+ static nsXULPopupManager* sInstance;
+
+ // initialize and shutdown methods called by nsLayoutStatics
+ static nsresult Init();
+ static void Shutdown();
+
+ // returns a weak reference to the popup manager instance, could return null
+ // if a popup manager could not be allocated
+ static nsXULPopupManager* GetInstance();
+
+ // This should be called when a window is moved or resized to adjust the
+ // popups accordingly.
+ void AdjustPopupsOnWindowChange(nsPIDOMWindowOuter* aWindow);
+ void AdjustPopupsOnWindowChange(mozilla::PresShell* aPresShell);
+
+ // inform the popup manager that a menu bar has been activated or deactivated,
+ // either because one of its menus has opened or closed, or that the menubar
+ // has been focused such that its menus may be navigated with the keyboard.
+ // aActivate should be true when the menubar should be focused, and false
+ // when the active menu bar should be defocused. In the latter case, if
+ // aMenuBar isn't currently active, yet another menu bar is, that menu bar
+ // will remain active.
+ void SetActiveMenuBar(mozilla::dom::XULMenuBarElement* aMenuBar,
+ bool aActivate);
+
+ struct MayShowMenuResult {
+ const bool mIsNative = false;
+ mozilla::dom::XULButtonElement* const mMenuButton = nullptr;
+ nsMenuPopupFrame* const mMenuPopupFrame = nullptr;
+
+ explicit operator bool() const {
+ MOZ_ASSERT(!!mMenuButton == !!mMenuPopupFrame);
+ return mIsNative || mMenuButton;
+ }
+ };
+
+ MayShowMenuResult MayShowMenu(nsIContent* aMenu);
+
+ /**
+ * Open a <menu> given its content node. If aSelectFirstItem is
+ * set to true, the first item on the menu will automatically be
+ * selected.
+ */
+ void ShowMenu(nsIContent* aMenu, bool aSelectFirstItem);
+
+ /**
+ * Open a popup, either anchored or unanchored. If aSelectFirstItem is
+ * true, then the first item in the menu is selected. The arguments are
+ * similar to those for XULPopupElement::OpenPopup.
+ *
+ * aTriggerEvent should be the event that triggered the event. This is used
+ * to determine the coordinates and trigger node for the popup. This may be
+ * null if the popup was not triggered by an event.
+ *
+ * This fires the popupshowing event synchronously.
+ */
+ void ShowPopup(Element* aPopup, nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu, bool aAttributesOverride,
+ bool aSelectFirstItem, mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a popup at a specific screen position specified by aXPos and aYPos,
+ * measured in CSS pixels.
+ *
+ * This fires the popupshowing event synchronously.
+ *
+ * If aIsContextMenu is true, the popup is positioned at a slight
+ * offset from aXPos/aYPos to ensure that it is not under the mouse
+ * cursor.
+ */
+ void ShowPopupAtScreen(Element* aPopup, int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /* Open a popup anchored at a screen rectangle specified by aRect.
+ * The remaining arguments are similar to ShowPopup.
+ */
+ void ShowPopupAtScreenRect(Element* aPopup, const nsAString& aPosition,
+ const nsIntRect& aRect, bool aIsContextMenu,
+ bool aAttributesOverride,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a popup as a native menu, at a specific screen position specified by
+ * aXPos and aYPos, measured in CSS pixels.
+ *
+ * This fires the popupshowing event synchronously.
+ *
+ * Returns whether native menus are supported for aPopup on this platform.
+ * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowPopupAsNativeMenu(
+ Element* aPopup, int32_t aXPos, int32_t aYPos, bool aIsContextMenu,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a tooltip at a specific screen position specified by aXPos and aYPos,
+ * measured in device pixels. This fires the popupshowing event synchronously.
+ */
+ void ShowTooltipAtScreen(Element* aPopup, nsIContent* aTriggerContent,
+ const mozilla::LayoutDeviceIntPoint&);
+
+ /*
+ * Hide a popup aPopup. If the popup is in a <menu>, then also inform the
+ * menu that the popup is being hidden.
+ * aLastPopup - optional popup to close last when hiding a chain of menus.
+ * If null, then all popups will be closed.
+ */
+ void HidePopup(Element* aPopup, HidePopupOptions,
+ Element* aLastPopup = nullptr);
+
+ /*
+ * Hide the popup of a <menu>.
+ */
+ void HideMenu(nsIContent* aMenu);
+
+ /**
+ * Hide a popup after a short delay. This is used when rolling over menu
+ * items. This timer is stored in mCloseTimer. The timer may be cancelled and
+ * the popup closed by calling KillMenuTimer.
+ */
+ void HidePopupAfterDelay(nsMenuPopupFrame* aPopup, int32_t aDelay);
+
+ /**
+ * Hide all of the popups from a given docshell. This should be called when
+ * the document is hidden.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void HidePopupsInDocShell(nsIDocShellTreeItem* aDocShellToHide);
+
+ /**
+ * Check if any popups need to be repositioned or hidden after a style or
+ * layout change. This will update, for example, any arrow type panels when
+ * the anchor that is is pointing to has moved, resized or gone away.
+ * Only those popups that pertain to the supplied aRefreshDriver are updated.
+ */
+ void UpdatePopupPositions(nsRefreshDriver* aRefreshDriver);
+
+ /**
+ * Enable or disable anchor following on the popup if needed.
+ */
+ void UpdateFollowAnchor(nsMenuPopupFrame* aPopup);
+
+ /**
+ * Execute a menu command from the triggering event aEvent.
+ *
+ * aMenu - a menuitem to execute
+ * aEvent - an nsXULMenuCommandEvent that contains all the info from the mouse
+ * event which triggered the menu to be executed, may not be null
+ */
+ MOZ_CAN_RUN_SCRIPT void ExecuteMenu(nsIContent* aMenu,
+ nsXULMenuCommandEvent* aEvent);
+
+ /**
+ * If a native menu is open, and aItem is an item in the menu's subtree,
+ * execute the item with the help of the native menu and close the menu.
+ * Returns true if a native menu was open.
+ */
+ bool ActivateNativeMenuItem(nsIContent* aItem, mozilla::Modifiers aModifiers,
+ int16_t aButton, mozilla::ErrorResult& aRv);
+
+ /**
+ * Return true if the popup for the supplied content node is open.
+ */
+ bool IsPopupOpen(Element* aPopup);
+
+ /**
+ * Return the frame for the topmost open popup of a given type, or null if
+ * no popup of that type is open. If aType is PopupType::Any, a menu of any
+ * type is returned.
+ */
+ nsIFrame* GetTopPopup(PopupType aType);
+
+ /**
+ * Returns the topmost active menuitem that's currently visible, if any.
+ */
+ nsIContent* GetTopActiveMenuItemContent();
+
+ /**
+ * Return an array of all the open and visible popup frames for
+ * menus, in order from top to bottom.
+ */
+ void GetVisiblePopups(nsTArray<nsIFrame*>& aPopups);
+
+ /**
+ * Get the node that last triggered a popup or tooltip in the document
+ * aDocument. aDocument must be non-null and be a document contained within
+ * the same window hierarchy as the popup to retrieve.
+ */
+ already_AddRefed<nsINode> GetLastTriggerPopupNode(
+ mozilla::dom::Document* aDocument) {
+ return GetLastTriggerNode(aDocument, false);
+ }
+
+ already_AddRefed<nsINode> GetLastTriggerTooltipNode(
+ mozilla::dom::Document* aDocument) {
+ return GetLastTriggerNode(aDocument, true);
+ }
+
+ /**
+ * Return false if a popup may not be opened. This will return false if the
+ * popup is already open, if the popup is in a content shell that is not
+ * focused, or if it is a submenu of another menu that isn't open.
+ */
+ bool MayShowPopup(nsMenuPopupFrame* aFrame);
+
+ /**
+ * Indicate that the popup associated with aView has been moved to the
+ * specified device pixel coordinates.
+ */
+ void PopupMoved(nsIFrame* aFrame, const mozilla::LayoutDeviceIntPoint& aPoint,
+ bool aByMoveToRect);
+
+ /**
+ * Indicate that the popup associated with aView has been resized to the
+ * given device pixel size aSize.
+ */
+ void PopupResized(nsIFrame* aFrame,
+ const mozilla::LayoutDeviceIntSize& aSize);
+
+ /**
+ * Called when a popup frame is destroyed. In this case, just remove the
+ * item and later popups from the list. No point going through HidePopup as
+ * the frames have gone away.
+ */
+ MOZ_CAN_RUN_SCRIPT void PopupDestroyed(nsMenuPopupFrame* aFrame);
+
+ /**
+ * Returns true if there is a context menu open. If aPopup is specified,
+ * then the context menu must be later in the chain than aPopup. If aPopup
+ * is null, returns true if any context menu at all is open.
+ */
+ bool HasContextMenu(nsMenuPopupFrame* aPopup);
+
+ /**
+ * Update the commands for the menus within the menu popup for a given
+ * content node. aPopup should be a XUL menupopup element. This method
+ * changes attributes on the children of aPopup, and deals only with the
+ * content of the popup, not the frames.
+ */
+ void UpdateMenuItems(Element* aPopup);
+
+ /**
+ * Stop the timer which hides a popup after a delay, started by a previous
+ * call to HidePopupAfterDelay. In addition, the popup awaiting to be hidden
+ * is closed asynchronously.
+ */
+ void KillMenuTimer();
+
+ /**
+ * Cancel the timer which closes menus after delay, but only if the menu to
+ * close is aMenuParent. When a submenu is opened, the user might move the
+ * mouse over a sibling menuitem which would normally close the menu. This
+ * menu is closed via a timer. However, if the user moves the mouse over the
+ * submenu before the timer fires, we should instead cancel the timer. This
+ * ensures that the user can move the mouse diagonally over a menu.
+ */
+ void CancelMenuTimer(nsMenuPopupFrame*);
+
+ /**
+ * Handles navigation for menu accelkeys. If aFrame is specified, then the
+ * key is handled by that popup, otherwise if aFrame is null, the key is
+ * handled by the active popup or menubar.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleShortcutNavigation(
+ mozilla::dom::KeyboardEvent& aKeyEvent, nsMenuPopupFrame* aFrame);
+
+ /**
+ * Handles cursor navigation within a menu. Returns true if the key has
+ * been handled.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigation(uint32_t aKeyCode);
+
+ /**
+ * Handle keyboard navigation within a menu popup specified by aFrame.
+ * Returns true if the key was handled and other default handling
+ * should not occur.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
+ nsMenuPopupFrame* aFrame, nsNavigationDirection aDir) {
+ return HandleKeyboardNavigationInPopup(nullptr, aFrame, aDir);
+ }
+
+ /**
+ * Handles the keyboard event with keyCode value. Returns true if the event
+ * has been handled.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardEventWithKeyCode(
+ mozilla::dom::KeyboardEvent* aKeyEvent,
+ nsMenuChainItem* aTopVisibleMenuItem);
+
+ // Sets mIgnoreKeys of the Top Visible Menu Item
+ nsresult UpdateIgnoreKeys(bool aIgnoreKeys);
+
+ nsPopupState GetPopupState(mozilla::dom::Element* aPopupElement);
+
+ mozilla::dom::Event* GetOpeningPopupEvent() const {
+ return mPendingPopup->mEvent.get();
+ }
+
+ MOZ_CAN_RUN_SCRIPT nsresult KeyUp(mozilla::dom::KeyboardEvent* aKeyEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::KeyboardEvent* aKeyEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::KeyboardEvent* aKeyEvent);
+
+ protected:
+ nsXULPopupManager();
+ ~nsXULPopupManager();
+
+ // get the nsMenuPopupFrame, if any, for the given content node
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsMenuPopupFrame* GetPopupFrameForContent(nsIContent* aContent,
+ bool aShouldFlush);
+
+ // Get the menu to start rolling up.
+ nsMenuChainItem* GetRollupItem(RollupKind);
+
+ // Return the topmost menu, skipping over invisible popups
+ nsMenuChainItem* GetTopVisibleMenu() {
+ return GetRollupItem(RollupKind::Menu);
+ }
+
+ // Removes the chain item from the chain and deletes it.
+ void RemoveMenuChainItem(nsMenuChainItem*);
+
+ // Hide all of the visible popups from the given list. This function can
+ // cause style changes and frame destruction.
+ MOZ_CAN_RUN_SCRIPT void HidePopupsInList(
+ const nsTArray<nsMenuPopupFrame*>& aFrames);
+
+ // Hide, but don't close, visible menus. Called before executing a menu item.
+ // The caller promises to close the menus properly (with a call to HidePopup)
+ // once the item has been executed.
+ MOZ_CAN_RUN_SCRIPT void HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode);
+
+ // callbacks for ShowPopup and HidePopup as events may be done asynchronously
+ MOZ_CAN_RUN_SCRIPT void ShowPopupCallback(Element* aPopup,
+ nsMenuPopupFrame* aPopupFrame,
+ bool aIsContextMenu,
+ bool aSelectFirstItem);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void HidePopupCallback(
+ Element* aPopup, nsMenuPopupFrame* aPopupFrame, Element* aNextPopup,
+ Element* aLastPopup, PopupType aPopupType, HidePopupOptions);
+
+ /**
+ * Trigger frame construction and reflow in the popup, fire a popupshowing
+ * event on the popup and then open the popup.
+ *
+ * aPendingPopup - information about the popup to open
+ * aIsContextMenu - true for context menus
+ * aSelectFirstItem - true to select the first item in the menu
+ * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void BeginShowingPopup(
+ const PendingPopup& aPendingPopup, bool aIsContextMenu,
+ bool aSelectFirstItem);
+
+ /**
+ * Fire a popuphiding event and then hide the popup. This will be called
+ * recursively if aNextPopup and aLastPopup are set in order to hide a chain
+ * of open menus. If these are not set, only one popup is closed. However,
+ * if the popup type indicates a menu, yet the next popup is not a menu,
+ * then this ends the closing of popups. This allows a menulist inside a
+ * non-menu to close up the menu but not close up the panel it is contained
+ * within.
+ *
+ * The caller must keep a strong reference to aPopup, aNextPopup and
+ * aLastPopup.
+ *
+ * aPopup - the popup to hide
+ * aNextPopup - the next popup to hide
+ * aLastPopup - the last popup in the chain to hide
+ * aPresContext - nsPresContext for the popup's frame
+ * aPopupType - the PopupType of the frame.
+ * aOptions - the relevant options to hide the popup. Only a subset is looked
+ * at.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void FirePopupHidingEvent(Element* aPopup, Element* aNextPopup,
+ Element* aLastPopup, nsPresContext* aPresContext,
+ PopupType aPopupType, HidePopupOptions aOptions);
+
+ /**
+ * Handle keyboard navigation within a menu popup specified by aItem.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ bool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
+ nsNavigationDirection aDir) {
+ return HandleKeyboardNavigationInPopup(aItem, aItem->Frame(), aDir);
+ }
+
+ private:
+ /**
+ * Handle keyboard navigation within a menu popup aFrame. If aItem is
+ * supplied, then it is expected to have a frame equal to aFrame.
+ * If aItem is non-null, then the navigation may be redirected to
+ * an open submenu if one exists. Returns true if the key was
+ * handled and other default handling should not occur.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
+ nsMenuChainItem* aItem, nsMenuPopupFrame* aFrame,
+ nsNavigationDirection aDir);
+
+ protected:
+ already_AddRefed<nsINode> GetLastTriggerNode(
+ mozilla::dom::Document* aDocument, bool aIsTooltip);
+
+ /**
+ * Fire a popupshowing event for aPopup.
+ */
+ MOZ_CAN_RUN_SCRIPT nsEventStatus FirePopupShowingEvent(
+ const PendingPopup& aPendingPopup, nsPresContext* aPresContext);
+
+ /**
+ * Set mouse capturing for the current popup. This traps mouse clicks that
+ * occur outside the popup so that it can be closed up. aOldPopup should be
+ * set to the popup that was previously the current popup.
+ */
+ void SetCaptureState(nsIContent* aOldPopup);
+
+ /**
+ * Key event listeners are attached to the document containing the current
+ * menu for menu and shortcut navigation. Only one listener is needed at a
+ * time, stored in mKeyListener, so switch it only if the document changes.
+ * Having menus in different documents is very rare, so the listeners will
+ * usually only be attached when the first menu opens and removed when all
+ * menus have closed.
+ *
+ * This is also used when only a menubar is active without any open menus,
+ * so that keyboard navigation between menus on the menubar may be done.
+ */
+ // TODO: Convert UpdateKeyboardListeners() to MOZ_CAN_RUN_SCRIPT and get rid
+ // of the kungFuDeathGrip in it.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void UpdateKeyboardListeners();
+
+ /*
+ * Returns true if the docshell for aDoc is aExpected or a child of aExpected.
+ */
+ bool IsChildOfDocShell(mozilla::dom::Document* aDoc,
+ nsIDocShellTreeItem* aExpected);
+
+ // Finds a chain item in mPopups.
+ nsMenuChainItem* FindPopup(Element* aPopup) const;
+
+ // the document the key event listener is attached to
+ nsCOMPtr<mozilla::dom::EventTarget> mKeyListener;
+
+ // widget that is currently listening to rollup events
+ nsCOMPtr<nsIWidget> mWidget;
+
+ // set to the currently active menu bar, if any
+ mozilla::dom::XULMenuBarElement* mActiveMenuBar;
+
+ // linked list of normal menus and panels. mPopups points to the innermost
+ // popup, which keeps alive all their parents.
+ mozilla::UniquePtr<nsMenuChainItem> mPopups;
+
+ // timer used for HidePopupAfterDelay
+ nsCOMPtr<nsITimer> mCloseTimer;
+ nsMenuPopupFrame* mTimerMenu = nullptr;
+
+ // Information about the popup that is currently firing a popupshowing event.
+ const PendingPopup* mPendingPopup;
+
+ // If a popup is displayed as a native menu, this is non-null while the
+ // native menu is open.
+ // mNativeMenu has a strong reference to the menupopup nsIContent.
+ RefPtr<mozilla::widget::NativeMenu> mNativeMenu;
+
+ // If the currently open native menu activated an item, this is the item's
+ // close menu mode. Nothing() if mNativeMenu is null or if no item was
+ // activated.
+ mozilla::Maybe<CloseMenuMode> mNativeMenuActivatedItemCloseMenuMode;
+
+ // If a popup is displayed as a native menu, this map contains the popup state
+ // for any of its non-closed submenus. This state cannot be stored on the
+ // submenus' nsMenuPopupFrames, because we usually don't generate frames for
+ // the contents of native menus.
+ // If a submenu is not present in this map, it means it's closed.
+ // This map is empty if mNativeMenu is null.
+ nsTHashMap<RefPtr<mozilla::dom::Element>, nsPopupState>
+ mNativeMenuSubmenuStates;
+};
+
+#endif
diff --git a/layout/xul/nsXULTooltipListener.cpp b/layout/xul/nsXULTooltipListener.cpp
new file mode 100644
index 0000000000..0efc8fbb80
--- /dev/null
+++ b/layout/xul/nsXULTooltipListener.cpp
@@ -0,0 +1,664 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsXULTooltipListener.h"
+
+#include "XULButtonElement.h"
+#include "XULTreeElement.h"
+#include "nsXULElement.h"
+#include "mozilla/dom/Document.h"
+#include "nsGkAtoms.h"
+#include "nsMenuPopupFrame.h"
+#include "nsIDragService.h"
+#include "nsIDragSession.h"
+#include "nsITreeView.h"
+#include "nsIScriptContext.h"
+#include "nsPIDOMWindow.h"
+#include "nsXULPopupManager.h"
+#include "nsIPopupContainer.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTreeColumns.h"
+#include "nsContentUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include "mozilla/dom/XULTreeElementBinding.h"
+#include "mozilla/TextEvents.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsXULTooltipListener* nsXULTooltipListener::sInstance = nullptr;
+
+//////////////////////////////////////////////////////////////////////////
+//// nsISupports
+
+nsXULTooltipListener::nsXULTooltipListener()
+ : mTooltipShownOnce(false),
+ mIsSourceTree(false),
+ mNeedTitletip(false),
+ mLastTreeRow(-1) {}
+
+nsXULTooltipListener::~nsXULTooltipListener() {
+ MOZ_ASSERT(sInstance == this);
+ sInstance = nullptr;
+
+ HideTooltip();
+}
+
+NS_IMPL_ISUPPORTS(nsXULTooltipListener, nsIDOMEventListener)
+
+void nsXULTooltipListener::MouseOut(Event* aEvent) {
+ // reset flag so that tooltip will display on the next MouseMove
+ mTooltipShownOnce = false;
+ mPreviousMouseMoveTarget = nullptr;
+
+ // if the timer is running and no tooltip is shown, we
+ // have to cancel the timer here so that it doesn't
+ // show the tooltip if we move the mouse out of the window
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (mTooltipTimer && !currentTooltip) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ return;
+ }
+
+#ifdef DEBUG_crap
+ if (mNeedTitletip) return;
+#endif
+
+ // check to see if the mouse left the targetNode, and if so,
+ // hide the tooltip
+ if (currentTooltip) {
+ // which node did the mouse leave?
+ EventTarget* eventTarget = aEvent->GetComposedTarget();
+ nsCOMPtr<nsINode> targetNode = nsINode::FromEventTargetOrNull(eventTarget);
+ if (targetNode && targetNode->IsContent() &&
+ !targetNode->AsContent()->GetContainingShadow()) {
+ eventTarget = aEvent->GetTarget();
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ nsCOMPtr<nsINode> tooltipNode =
+ pm->GetLastTriggerTooltipNode(currentTooltip->GetComposedDoc());
+
+ // If the target node is the current tooltip target node, the mouse
+ // left the node the tooltip appeared on, so close the tooltip. However,
+ // don't do this if the mouse moved onto the tooltip in case the
+ // tooltip appears positioned near the mouse.
+ nsCOMPtr<EventTarget> relatedTarget =
+ aEvent->AsMouseEvent()->GetRelatedTarget();
+ nsIContent* relatedContent =
+ nsIContent::FromEventTargetOrNull(relatedTarget);
+ if (tooltipNode == targetNode && relatedContent != currentTooltip) {
+ HideTooltip();
+ // reset special tree tracking
+ if (mIsSourceTree) {
+ mLastTreeRow = -1;
+ mLastTreeCol = nullptr;
+ }
+ }
+ }
+ }
+}
+
+void nsXULTooltipListener::MouseMove(Event* aEvent) {
+ if (!ShowTooltips()) {
+ return;
+ }
+
+ // stash the coordinates of the event so that we can still get back to it from
+ // within the timer callback. On win32, we'll get a MouseMove event even when
+ // a popup goes away -- even when the mouse doesn't change position! To get
+ // around this, we make sure the mouse has really moved before proceeding.
+ MouseEvent* mouseEvent = aEvent->AsMouseEvent();
+ if (!mouseEvent) {
+ return;
+ }
+ auto newMouseScreenPoint = mouseEvent->ScreenPointLayoutDevicePix();
+
+ // filter out false win32 MouseMove event
+ if (mMouseScreenPoint == newMouseScreenPoint) {
+ return;
+ }
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ nsCOMPtr<EventTarget> eventTarget = aEvent->GetComposedTarget();
+ nsIContent* content = nsIContent::FromEventTargetOrNull(eventTarget);
+
+ bool isSameTarget = true;
+ nsCOMPtr<nsIContent> tempContent = do_QueryReferent(mPreviousMouseMoveTarget);
+ if (tempContent && tempContent != content) {
+ isSameTarget = false;
+ }
+
+ // filter out minor movements due to crappy optical mice and shaky hands
+ // to prevent tooltips from hiding prematurely. Do not filter out movements
+ // if we are changing targets, as they may register new tooltips.
+ if (currentTooltip && isSameTarget &&
+ abs(mMouseScreenPoint.x - newMouseScreenPoint.x) <=
+ kTooltipMouseMoveTolerance &&
+ abs(mMouseScreenPoint.y - newMouseScreenPoint.y) <=
+ kTooltipMouseMoveTolerance) {
+ return;
+ }
+ mMouseScreenPoint = newMouseScreenPoint;
+ mPreviousMouseMoveTarget = do_GetWeakReference(content);
+
+ nsCOMPtr<nsIContent> sourceContent =
+ do_QueryInterface(aEvent->GetCurrentTarget());
+ mSourceNode = do_GetWeakReference(sourceContent);
+ mIsSourceTree = sourceContent->IsXULElement(nsGkAtoms::treechildren);
+ if (mIsSourceTree) CheckTreeBodyMove(mouseEvent);
+
+ // as the mouse moves, we want to make sure we reset the timer to show it,
+ // so that the delay is from when the mouse stops moving, not when it enters
+ // the node.
+ KillTooltipTimer();
+
+ // Hide the current tooltip if we change target nodes. If the new target
+ // has the same tooltip, we will open it again. We cannot compare
+ // the targets' tooltips because popupshowing events can set the tooltip.
+ if (!isSameTarget) {
+ HideTooltip();
+ mTooltipShownOnce = false;
+ }
+
+ // If the mouse moves while the tooltip is up, hide it. If nothing is
+ // showing and the tooltip hasn't been displayed since the mouse entered
+ // the node, then start the timer to show the tooltip.
+ // If we have moved to a different target, we need to display the new tooltip,
+ // as the previous target's tooltip will have just been hidden.
+ if ((!currentTooltip && !mTooltipShownOnce) || !isSameTarget) {
+ // don't show tooltips attached to elements outside of a menu popup
+ // when hovering over an element inside it. The popupsinherittooltip
+ // attribute may be used to disable this behaviour, which is useful for
+ // large menu hierarchies such as bookmarks.
+ if (!sourceContent->IsElement() ||
+ !sourceContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::popupsinherittooltip,
+ nsGkAtoms::_true, eCaseMatters)) {
+ for (nsIContent* targetContent =
+ nsIContent::FromEventTargetOrNull(eventTarget);
+ targetContent && targetContent != sourceContent;
+ targetContent = targetContent->GetParent()) {
+ if (targetContent->IsAnyOfXULElements(
+ nsGkAtoms::menupopup, nsGkAtoms::panel, nsGkAtoms::tooltip)) {
+ mSourceNode = nullptr;
+ return;
+ }
+ }
+ }
+
+ mTargetNode = do_GetWeakReference(eventTarget);
+ if (mTargetNode) {
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTooltipTimer), sTooltipCallback, this,
+ LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay, 500),
+ nsITimer::TYPE_ONE_SHOT, "sTooltipCallback",
+ sourceContent->OwnerDoc()->EventTargetFor(TaskCategory::Other));
+ if (NS_FAILED(rv)) {
+ mTargetNode = nullptr;
+ mSourceNode = nullptr;
+ }
+ }
+ return;
+ }
+
+ if (mIsSourceTree) return;
+ // Hide the tooltip if it is currently showing.
+ if (currentTooltip) {
+ HideTooltip();
+ // set a flag so that the tooltip is only displayed once until the mouse
+ // leaves the node
+ mTooltipShownOnce = true;
+ }
+}
+
+NS_IMETHODIMP
+nsXULTooltipListener::HandleEvent(Event* aEvent) {
+ nsAutoString type;
+ aEvent->GetType(type);
+ if (type.EqualsLiteral("wheel") || type.EqualsLiteral("mousedown") ||
+ type.EqualsLiteral("mouseup") || type.EqualsLiteral("dragstart")) {
+ HideTooltip();
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("keydown")) {
+ // Hide the tooltip if a non-modifier key is pressed.
+ WidgetKeyboardEvent* keyEvent = aEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (KeyEventHidesTooltip(*keyEvent)) {
+ HideTooltip();
+ }
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("popuphiding")) {
+ DestroyTooltip();
+ return NS_OK;
+ }
+
+ // Note that mousemove, mouseover and mouseout might be
+ // fired even during dragging due to widget's bug.
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ NS_ENSURE_TRUE(dragService, NS_OK);
+ nsCOMPtr<nsIDragSession> dragSession;
+ dragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (dragSession) {
+ return NS_OK;
+ }
+
+ // Not dragging.
+
+ if (type.EqualsLiteral("mousemove")) {
+ MouseMove(aEvent);
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("mouseout")) {
+ MouseOut(aEvent);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////
+//// nsXULTooltipListener
+
+bool nsXULTooltipListener::ShowTooltips() {
+ return StaticPrefs::browser_chrome_toolbar_tips();
+}
+
+bool nsXULTooltipListener::KeyEventHidesTooltip(
+ const WidgetKeyboardEvent& aEvent) {
+ switch (StaticPrefs::browser_chrome_toolbar_tips_hide_on_keydown()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return !aEvent.IsModifierKeyEvent();
+ }
+}
+
+void nsXULTooltipListener::AddTooltipSupport(nsIContent* aNode) {
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(this == sInstance);
+
+ aNode->AddSystemEventListener(u"mouseout"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mousemove"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mousedown"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mouseup"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"dragstart"_ns, this, true, false);
+}
+
+void nsXULTooltipListener::RemoveTooltipSupport(nsIContent* aNode) {
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(this == sInstance);
+
+ // The last reference to us can go after some of these calls.
+ RefPtr<nsXULTooltipListener> instance = this;
+
+ aNode->RemoveSystemEventListener(u"mouseout"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mousemove"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mousedown"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mouseup"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"dragstart"_ns, this, true);
+}
+
+void nsXULTooltipListener::CheckTreeBodyMove(MouseEvent* aMouseEvent) {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+ if (!sourceNode) return;
+
+ // get the documentElement of the document the tree is in
+ Document* doc = sourceNode->GetComposedDoc();
+
+ RefPtr<XULTreeElement> tree = GetSourceTree();
+ Element* root = doc ? doc->GetRootElement() : nullptr;
+ if (root && root->GetPrimaryFrame() && tree) {
+ CSSIntPoint pos = aMouseEvent->ScreenPoint(CallerType::System);
+
+ // subtract off the documentElement's position
+ // XXX Isn't this just converting to client points?
+ CSSIntRect rect = root->GetPrimaryFrame()->GetScreenRect();
+ pos -= rect.TopLeft();
+
+ ErrorResult rv;
+ TreeCellInfo cellInfo;
+ tree->GetCellAt(pos.x, pos.y, cellInfo, rv);
+
+ int32_t row = cellInfo.mRow;
+ RefPtr<nsTreeColumn> col = cellInfo.mCol;
+
+ // determine if we are going to need a titletip
+ // XXX check the disabletitletips attribute on the tree content
+ mNeedTitletip = false;
+ if (row >= 0 && cellInfo.mChildElt.EqualsLiteral("text")) {
+ mNeedTitletip = tree->IsCellCropped(row, col, rv);
+ }
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (currentTooltip && (row != mLastTreeRow || col != mLastTreeCol)) {
+ HideTooltip();
+ }
+
+ mLastTreeRow = row;
+ mLastTreeCol = col;
+ }
+}
+
+nsresult nsXULTooltipListener::ShowTooltip() {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+
+ // get the tooltip content designated for the target node
+ nsCOMPtr<nsIContent> tooltipNode;
+ GetTooltipFor(sourceNode, getter_AddRefs(tooltipNode));
+ if (!tooltipNode || sourceNode == tooltipNode)
+ return NS_ERROR_FAILURE; // the target node doesn't need a tooltip
+
+ // set the node in the document that triggered the tooltip and show it
+ if (tooltipNode->GetComposedDoc() &&
+ nsContentUtils::IsChromeDoc(tooltipNode->GetComposedDoc())) {
+ // Make sure the target node is still attached to some document.
+ // It might have been deleted.
+ if (sourceNode->IsInComposedDoc()) {
+ if (!mIsSourceTree) {
+ mLastTreeRow = -1;
+ mLastTreeCol = nullptr;
+ }
+
+ mCurrentTooltip = do_GetWeakReference(tooltipNode);
+ LaunchTooltip();
+ mTargetNode = nullptr;
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (!currentTooltip) return NS_OK;
+
+ // listen for popuphidden on the tooltip node, so that we can
+ // be sure DestroyPopup is called even if someone else closes the tooltip
+ currentTooltip->AddSystemEventListener(u"popuphiding"_ns, this, false,
+ false);
+
+ // listen for mousedown, mouseup, keydown, and mouse events at
+ // document level
+ if (Document* doc = sourceNode->GetComposedDoc()) {
+ // Probably, we should listen to untrusted events for hiding tooltips
+ // on content since tooltips might disturb something of web
+ // applications. If we don't specify the aWantsUntrusted of
+ // AddSystemEventListener(), the event target sets it to TRUE if the
+ // target is in content.
+ doc->AddSystemEventListener(u"wheel"_ns, this, true);
+ doc->AddSystemEventListener(u"mousedown"_ns, this, true);
+ doc->AddSystemEventListener(u"mouseup"_ns, this, true);
+ doc->AddSystemEventListener(u"keydown"_ns, this, true);
+ }
+ mSourceNode = nullptr;
+ }
+ }
+
+ return NS_OK;
+}
+
+static void SetTitletipLabel(XULTreeElement* aTree, Element* aTooltip,
+ int32_t aRow, nsTreeColumn* aCol) {
+ nsCOMPtr<nsITreeView> view = aTree->GetView();
+ if (view) {
+ nsAutoString label;
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ view->GetCellText(aRow, aCol, label);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Couldn't get the cell text!");
+ aTooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::label, label, true);
+ }
+}
+
+void nsXULTooltipListener::LaunchTooltip() {
+ RefPtr<Element> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (!currentTooltip) {
+ return;
+ }
+
+ if (mIsSourceTree && mNeedTitletip) {
+ RefPtr<XULTreeElement> tree = GetSourceTree();
+
+ SetTitletipLabel(tree, currentTooltip, mLastTreeRow, mLastTreeCol);
+ if (!(currentTooltip = do_QueryReferent(mCurrentTooltip))) {
+ // Because of mutation events, currentTooltip can be null.
+ return;
+ }
+ currentTooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::titletip, u"true"_ns,
+ true);
+ } else {
+ currentTooltip->UnsetAttr(kNameSpaceID_None, nsGkAtoms::titletip, true);
+ }
+
+ if (!(currentTooltip = do_QueryReferent(mCurrentTooltip))) {
+ // Because of mutation events, currentTooltip can be null.
+ return;
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return;
+ }
+
+ auto cleanup = MakeScopeExit([&] {
+ // Clear the current tooltip if the popup was not opened successfully.
+ if (!pm->IsPopupOpen(currentTooltip)) {
+ mCurrentTooltip = nullptr;
+ }
+ });
+
+ RefPtr<Element> target = do_QueryReferent(mTargetNode);
+ if (!target) {
+ return;
+ }
+
+ pm->ShowTooltipAtScreen(currentTooltip, target, mMouseScreenPoint);
+}
+
+nsresult nsXULTooltipListener::HideTooltip() {
+ if (nsCOMPtr<Element> currentTooltip = do_QueryReferent(mCurrentTooltip)) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->HidePopup(currentTooltip, {});
+ }
+ }
+
+ DestroyTooltip();
+ return NS_OK;
+}
+
+static void GetImmediateChild(nsIContent* aContent, nsAtom* aTag,
+ nsIContent** aResult) {
+ *aResult = nullptr;
+ for (nsCOMPtr<nsIContent> childContent = aContent->GetFirstChild();
+ childContent; childContent = childContent->GetNextSibling()) {
+ if (childContent->IsXULElement(aTag)) {
+ childContent.forget(aResult);
+ return;
+ }
+ }
+}
+
+nsresult nsXULTooltipListener::FindTooltip(nsIContent* aTarget,
+ nsIContent** aTooltip) {
+ if (!aTarget) return NS_ERROR_NULL_POINTER;
+
+ // before we go on, make sure that target node still has a window
+ Document* document = aTarget->GetComposedDoc();
+ if (!document) {
+ NS_WARNING("Unable to retrieve the tooltip node document.");
+ return NS_ERROR_FAILURE;
+ }
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ if (!window) {
+ return NS_OK;
+ }
+
+ if (window->Closed()) {
+ return NS_OK;
+ }
+
+ // non-XUL elements should just use the default tooltip
+ if (!aTarget->IsXULElement()) {
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ if (RefPtr<Element> tooltip = popupContainer->GetDefaultTooltip()) {
+ tooltip.forget(aTooltip);
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+
+ // On Windows, the OS shows the tooltip, so we don't want Gecko to do it
+#ifdef XP_WIN
+ if (nsIFrame* f = aTarget->GetPrimaryFrame()) {
+ if (f->StyleDisplay()->GetWindowButtonType()) {
+ return NS_OK;
+ }
+ }
+#endif
+
+ nsAutoString tooltipText;
+ aTarget->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext,
+ tooltipText);
+
+ if (!tooltipText.IsEmpty()) {
+ // specifying tooltiptext means we will always use the default tooltip
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ if (RefPtr<Element> tooltip = popupContainer->GetDefaultTooltip()) {
+ tooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::label, tooltipText, true);
+ tooltip.forget(aTooltip);
+ }
+ return NS_OK;
+ }
+
+ nsAutoString tooltipId;
+ aTarget->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltip,
+ tooltipId);
+
+ // if tooltip == _child, look for first <tooltip> child
+ if (tooltipId.EqualsLiteral("_child")) {
+ GetImmediateChild(aTarget, nsGkAtoms::tooltip, aTooltip);
+ return NS_OK;
+ }
+
+ if (!tooltipId.IsEmpty()) {
+ DocumentOrShadowRoot* documentOrShadowRoot =
+ aTarget->GetUncomposedDocOrConnectedShadowRoot();
+ // tooltip must be an id, use getElementById to find it
+ if (documentOrShadowRoot) {
+ nsCOMPtr<nsIContent> tooltipEl =
+ documentOrShadowRoot->GetElementById(tooltipId);
+
+ if (tooltipEl) {
+ mNeedTitletip = false;
+ tooltipEl.forget(aTooltip);
+ return NS_OK;
+ }
+ }
+ }
+
+ // titletips should just use the default tooltip
+ if (mIsSourceTree && mNeedTitletip) {
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ NS_IF_ADDREF(*aTooltip = popupContainer->GetDefaultTooltip());
+ }
+
+ return NS_OK;
+}
+
+nsresult nsXULTooltipListener::GetTooltipFor(nsIContent* aTarget,
+ nsIContent** aTooltip) {
+ *aTooltip = nullptr;
+ nsCOMPtr<nsIContent> tooltip;
+ nsresult rv = FindTooltip(aTarget, getter_AddRefs(tooltip));
+ if (NS_FAILED(rv) || !tooltip) {
+ return rv;
+ }
+
+ // Submenus can't be used as tooltips, see bug 288763.
+ if (nsIContent* parent = tooltip->GetParent()) {
+ if (auto* button = XULButtonElement::FromNode(parent)) {
+ if (button->IsMenu()) {
+ NS_WARNING("Menu cannot be used as a tooltip");
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ tooltip.swap(*aTooltip);
+ return rv;
+}
+
+nsresult nsXULTooltipListener::DestroyTooltip() {
+ nsCOMPtr<nsIDOMEventListener> kungFuDeathGrip(this);
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (currentTooltip) {
+ // release tooltip before removing listener to prevent our destructor from
+ // being called recursively (bug 120863)
+ mCurrentTooltip = nullptr;
+
+ // clear out the tooltip node on the document
+ if (nsCOMPtr<Document> doc = currentTooltip->GetComposedDoc()) {
+ // remove the mousedown and keydown listener from document
+ doc->RemoveSystemEventListener(u"wheel"_ns, this, true);
+ doc->RemoveSystemEventListener(u"mousedown"_ns, this, true);
+ doc->RemoveSystemEventListener(u"mouseup"_ns, this, true);
+ doc->RemoveSystemEventListener(u"keydown"_ns, this, true);
+ }
+
+ // remove the popuphidden listener from tooltip
+ currentTooltip->RemoveSystemEventListener(u"popuphiding"_ns, this, false);
+ }
+
+ // kill any ongoing timers
+ KillTooltipTimer();
+ mSourceNode = nullptr;
+ mLastTreeCol = nullptr;
+
+ return NS_OK;
+}
+
+void nsXULTooltipListener::KillTooltipTimer() {
+ if (mTooltipTimer) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ mTargetNode = nullptr;
+ }
+}
+
+void nsXULTooltipListener::sTooltipCallback(nsITimer* aTimer, void* aListener) {
+ RefPtr<nsXULTooltipListener> instance = sInstance;
+ if (instance) instance->ShowTooltip();
+}
+
+XULTreeElement* nsXULTooltipListener::GetSourceTree() {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+ if (mIsSourceTree && sourceNode) {
+ RefPtr<XULTreeElement> xulEl =
+ XULTreeElement::FromNodeOrNull(sourceNode->GetParent());
+ return xulEl;
+ }
+
+ return nullptr;
+}
diff --git a/layout/xul/nsXULTooltipListener.h b/layout/xul/nsXULTooltipListener.h
new file mode 100644
index 0000000000..1a665cfb33
--- /dev/null
+++ b/layout/xul/nsXULTooltipListener.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsXULTooltipListener_h__
+#define nsXULTooltipListener_h__
+
+#include "nsIDOMEventListener.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "Units.h"
+#include "nsIWeakReferenceUtils.h"
+#include "mozilla/Attributes.h"
+
+class nsIContent;
+class nsTreeColumn;
+
+namespace mozilla {
+namespace dom {
+class Event;
+class MouseEvent;
+class XULTreeElement;
+} // namespace dom
+class WidgetKeyboardEvent;
+} // namespace mozilla
+
+class nsXULTooltipListener final : public nsIDOMEventListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ void MouseOut(mozilla::dom::Event* aEvent);
+ void MouseMove(mozilla::dom::Event* aEvent);
+
+ void AddTooltipSupport(nsIContent* aNode);
+ void RemoveTooltipSupport(nsIContent* aNode);
+ static nsXULTooltipListener* GetInstance() {
+ if (!sInstance) sInstance = new nsXULTooltipListener();
+ return sInstance;
+ }
+
+ static bool KeyEventHidesTooltip(const mozilla::WidgetKeyboardEvent&);
+ static bool ShowTooltips();
+
+ protected:
+ nsXULTooltipListener();
+ ~nsXULTooltipListener();
+
+ void KillTooltipTimer();
+
+ void CheckTreeBodyMove(mozilla::dom::MouseEvent* aMouseEvent);
+ mozilla::dom::XULTreeElement* GetSourceTree();
+
+ nsresult ShowTooltip();
+ void LaunchTooltip();
+ nsresult HideTooltip();
+ nsresult DestroyTooltip();
+ // This method tries to find a tooltip for aTarget.
+ nsresult FindTooltip(nsIContent* aTarget, nsIContent** aTooltip);
+ // This method calls FindTooltip and checks that the tooltip
+ // can be really used (i.e. tooltip is not a menu).
+ nsresult GetTooltipFor(nsIContent* aTarget, nsIContent** aTooltip);
+
+ static nsXULTooltipListener* sInstance;
+
+ nsWeakPtr mSourceNode;
+ nsWeakPtr mTargetNode;
+ nsWeakPtr mCurrentTooltip;
+ nsWeakPtr mPreviousMouseMoveTarget;
+
+ // a timer for showing the tooltip
+ nsCOMPtr<nsITimer> mTooltipTimer;
+ static void sTooltipCallback(nsITimer* aTimer, void* aListener);
+
+ // Screen coordinates of the last mousemove event, stored so that the tooltip
+ // can be opened at this location.
+ //
+ // TODO(emilio): This duplicates a lot of code with ChromeTooltipListener.
+ mozilla::LayoutDeviceIntPoint mMouseScreenPoint;
+
+ // Tolerance for mousemove event
+ static constexpr mozilla::LayoutDeviceIntCoord kTooltipMouseMoveTolerance = 7;
+
+ // flag specifying if the tooltip has already been displayed by a MouseMove
+ // event. The flag is reset on MouseOut so that the tooltip will display
+ // the next time the mouse enters the node (bug #395668).
+ bool mTooltipShownOnce;
+
+ // special members for handling trees
+ bool mIsSourceTree;
+ bool mNeedTitletip;
+ int32_t mLastTreeRow;
+ RefPtr<nsTreeColumn> mLastTreeCol;
+};
+
+#endif // nsXULTooltipListener
diff --git a/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml b/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml
new file mode 100644
index 0000000000..a790928f92
--- /dev/null
+++ b/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <checkbox id="c1"/>
+ <checkbox id="c2" checked="true"/>
+</window>
diff --git a/layout/xul/reftest/checkbox-dynamic-change.xhtml b/layout/xul/reftest/checkbox-dynamic-change.xhtml
new file mode 100644
index 0000000000..116e142a3c
--- /dev/null
+++ b/layout/xul/reftest/checkbox-dynamic-change.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+ <checkbox id="c1" checked="true"/>
+ <checkbox id="c2"/>
+ <script>
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ let c1 = document.getElementById("c1");
+ let c2 = document.getElementById("c2");
+ c1.removeAttribute("checked");
+ c2.setAttribute("checked", true);
+ document.documentElement.className = "";
+ });
+ });
+ </script>
+</window>
diff --git a/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml b/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml
new file mode 100644
index 0000000000..1474cbe310
--- /dev/null
+++ b/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<html:style><![CDATA[
+
+window { -moz-box-align: start; -moz-box-pack: start }
+hbox { background: yellow }
+vbox { background: blue; width: 15px; min-height: 15px }
+
+]]></html:style>
+
+<hbox><vbox /><label value="a b c d e f" /></hbox>
+
+</window>
diff --git a/layout/xul/reftest/image-scaling-min-height-1.xhtml b/layout/xul/reftest/image-scaling-min-height-1.xhtml
new file mode 100644
index 0000000000..5c45d6b0c9
--- /dev/null
+++ b/layout/xul/reftest/image-scaling-min-height-1.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<html:style><![CDATA[
+
+window { -moz-box-align: start; -moz-box-pack: start }
+hbox { background: yellow }
+image { background: blue; min-width: 15px; min-height: 15px }
+
+]]></html:style>
+
+<hbox><image /><label value="a b c d e f" /></hbox>
+
+</window>
diff --git a/layout/xul/reftest/image-size-ref.xhtml b/layout/xul/reftest/image-size-ref.xhtml
new file mode 100644
index 0000000000..28598d4430
--- /dev/null
+++ b/layout/xul/reftest/image-size-ref.xhtml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+<html:style>
+div { margin: 0px; line-height: 0px; }
+div div { background: blue; display: inline; float: left; }
+</html:style>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 20px;"/><html:img
+ src="image4x3.png" style="width: 10px; height: 70px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 80px; height: 64px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 72px; height: 58px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 74px; height: 53px; border: solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 18px; height: 11px; border: solid green; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 20px;"/><html:img
+ src="image4x3.png" style="width: 10px; height: 70px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="height: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 60px; height: 25px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 75px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 64px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 72px; height: 58px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 24px; height: 22px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 24px; height: 22px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 67px; height: 60px; padding: 4px 2px 8px 1px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 11px; height: 18px; padding: 4px 2px 8px 1px; box-sizing: border-box;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 30px; height: 22.5px"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width 30px; height: 22.5px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 60px; height: 49px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 96px; height: 76px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 106px; height: 77px; border: solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 60px; height: 45px;"/><html:img
+ src="image4x3.png" style="width: 120px; height: 90px;"/><html:img
+ src="image4x3.png" style="width 60px; height: 45px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 60px; height: 49px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 96px; height: 76px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; padding: 8px;"/><html:img
+ src="image4x3.png" style="border: 1px solid blue"/><html:img
+ width="0" height="0" style="border: 1px solid green" /><html:img
+ width="0" height="0" style="border: 1px solid green" /><html:span style="display: inline-block; -moz-default-appearance: checkbox; appearance: auto" />
+</html:div>
+
+</window>
diff --git a/layout/xul/reftest/image-size.xhtml b/layout/xul/reftest/image-size.xhtml
new file mode 100644
index 0000000000..9a4e00429a
--- /dev/null
+++ b/layout/xul/reftest/image-size.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<hbox align="end">
+ <image src="image4x3.png"/>
+ <image src="image4x3.png" style="width: 80px; height: 20px"/>
+ <image src="image4x3.png" style="width: 10px; height: 70px"/>
+ <image src="image4x3.png" style="width: 80px"/>
+ <image src="image4x3.png" style="height: 60px"/>
+ <image src="image4x3.png" style="width: 20px"/>
+ <image src="image4x3.png" style="height: 15px"/>
+ <image src="image4x3.png" style="border: 8px solid green;"/>
+ <image src="image4x3.png" style="width: 80px; border: 8px solid yellow;"/>
+ <image src="image4x3.png" style="height: 58px; border: 8px solid green;"/>
+ <image src="image4x3.png" style="width: 24px; border: 8px solid yellow;"/>
+ <image src="image4x3.png" style="height: 22px; border: 8px solid green;"/>
+ <image src="image4x3.png"
+ style="width: 74px; border: 1px solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+ <image src="image4x3.png"
+ style="height: 11px; border: 1px solid green; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="width: auto; height: auto;"/>
+ <image src="image4x3.png" style="width: 80px; height: 20px;"/>
+ <image src="image4x3.png" style="width: 10px; height: 70px;"/>
+ <image src="image4x3.png" style="width: 80px;"/>
+ <image src="image4x3.png" style="height: 60px;"/>
+ <image src="image4x3.png" style="width: 20px;"/>
+ <image src="image4x3.png" style="height: 15px;"/>
+ <image src="image4x3.png" style="width: 60px; height: 25px;"/>
+ <image src="image4x3.png" style="width: 20px; height: 75px;"/>
+ <image src="image4x3.png" style="width: 80px; padding: 8px;"/>
+ <image src="image4x3.png" style="height: 58px; padding: 8px;"/>
+ <image src="image4x3.png" style="width: 24px; padding: 8px;"/>
+ <image src="image4x3.png" style="height: 22px; padding: 8px;"/>
+ <image src="image4x3.png" style="width: 67px; padding: 4px 2px 8px 1px"/>
+ <image src="image4x3.png" style="height: 18px; padding: 4px 2px 8px 1px"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 20px"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-height: 15px"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 30px; max-height: 25px"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 20px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-height: 15px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 30px; max-height: 25px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="width: 24px; border: 8px solid green;"/>
+</hbox>
+<hbox align="end">
+ <image src="image4x3.png" style="max-height: 22px; border: 8px solid green;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="min-width: 20px;"/>
+ <image src="image4x3.png" style="min-height: 20px;"/>
+ <image src="image4x3.png" style="min-width: 20px; min-height: 25px"/>
+ <image src="image4x3.png" style="min-width: 60px; border: 8px solid green;"/>
+ <image src="image4x3.png" style="min-height: 88px; border: 8px solid yellow;"/>
+ <image src="image4x3.png" style="min-width: 90px; min-height: 76px; border: 8px solid green;"/>
+ <image src="image4x3.png" style="min-width: 112px; min-height: 76px; border: 8px solid yellow;"/>
+ <image src="image4x3.png"
+ style="min-width: 106px; border: 1px solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="min-width: 60px;"/>
+ <image src="image4x3.png" style="min-height: 90px;"/>
+ <image src="image4x3.png" style="min-width 41px; min-height: 45px;"/>
+ <image src="image4x3.png" style="min-width: 60px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-height: 88px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-width: 90px; min-height: 76px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-width: 112px; min-height: 76px; padding: 8px;"/>
+ <image src="" style="list-style-image: url(image4x3.png); border: 1px solid blue"/>
+ <image id="dyn-1" src="image4x3.png" style="border: 1px solid green"/>
+ <image id="dyn-2" style="list-style-image: url(image4x3.png); border: 1px solid green"/>
+ <image style="-moz-default-appearance: checkbox; appearance: auto"/>
+</hbox>
+<script>
+ window.addEventListener("load", function() {
+ document.getElementById("dyn-1").removeAttribute("src");
+ document.getElementById("dyn-2").style.listStyleImage = "";
+ });
+</script>
+</window>
diff --git a/layout/xul/reftest/image4x3.png b/layout/xul/reftest/image4x3.png
new file mode 100644
index 0000000000..6719bf5cec
--- /dev/null
+++ b/layout/xul/reftest/image4x3.png
Binary files differ
diff --git a/layout/xul/reftest/popup-explicit-size-ref.xhtml b/layout/xul/reftest/popup-explicit-size-ref.xhtml
new file mode 100644
index 0000000000..85a8a6832a
--- /dev/null
+++ b/layout/xul/reftest/popup-explicit-size-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window align="start" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="One"/>
+ <label value="Two"/>
+</window>
diff --git a/layout/xul/reftest/popup-explicit-size.xhtml b/layout/xul/reftest/popup-explicit-size.xhtml
new file mode 100644
index 0000000000..a4a87c2c8b
--- /dev/null
+++ b/layout/xul/reftest/popup-explicit-size.xhtml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window align="start" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="One"/>
+ <menupopup height="40"/>
+ <label value="Two"/>
+</window>
diff --git a/layout/xul/reftest/radio-dynamic-change-ref.xhtml b/layout/xul/reftest/radio-dynamic-change-ref.xhtml
new file mode 100644
index 0000000000..73ff14d6cd
--- /dev/null
+++ b/layout/xul/reftest/radio-dynamic-change-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <radio id="r1"/>
+ <radio id="r2" selected="true"/>
+</window>
diff --git a/layout/xul/reftest/radio-dynamic-change.xhtml b/layout/xul/reftest/radio-dynamic-change.xhtml
new file mode 100644
index 0000000000..508e99ec02
--- /dev/null
+++ b/layout/xul/reftest/radio-dynamic-change.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+ <radio id="r1" selected="true"/>
+ <radio id="r2"/>
+ <script>
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ let r1 = document.getElementById("r1");
+ let r2 = document.getElementById("r2");
+ r1.removeAttribute("selected");
+ r2.setAttribute("selected", true);
+ document.documentElement.className = "";
+ });
+ });
+ </script>
+</window>
diff --git a/layout/xul/reftest/reftest.list b/layout/xul/reftest/reftest.list
new file mode 100644
index 0000000000..a2b0b6c6fe
--- /dev/null
+++ b/layout/xul/reftest/reftest.list
@@ -0,0 +1,14 @@
+== chrome://reftest/content/xul/reftest/popup-explicit-size.xhtml chrome://reftest/content/xul/reftest/popup-explicit-size-ref.xhtml
+fuzzy(0-16,0-128) random-if(Android) == chrome://reftest/content/xul/reftest/image-size.xhtml chrome://reftest/content/xul/reftest/image-size-ref.xhtml
+== chrome://reftest/content/xul/reftest/image-scaling-min-height-1.xhtml chrome://reftest/content/xul/reftest/image-scaling-min-height-1-ref.xhtml
+== chrome://reftest/content/xul/reftest/textbox-text-transform.xhtml chrome://reftest/content/xul/reftest/textbox-text-transform-ref.xhtml
+
+== chrome://reftest/content/xul/reftest/checkbox-dynamic-change.xhtml chrome://reftest/content/xul/reftest/checkbox-dynamic-change-ref.xhtml
+== chrome://reftest/content/xul/reftest/radio-dynamic-change.xhtml chrome://reftest/content/xul/reftest/radio-dynamic-change-ref.xhtml
+
+# These test find marks appearing on the scrollbar
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks.html chrome://reftest/content/xul/reftest/scrollbar-marks-ref.html
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks2.html chrome://reftest/content/xul/reftest/scrollbar-marks-ref.html
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks2.html chrome://reftest/content/xul/reftest/scrollbar-marks.html
+# This test is fuzzy as the marks cannot be positioned exactly as the real ones are measured in dev pixels.
+fuzzy(0-10,0-170) fuzzy-if(winWidget&&isDebugBuild&&layersGPUAccelerated&&!is64Bit,1-1,74-170) == chrome://reftest/content/xul/reftest/scrollbar-marks-overlay.html chrome://reftest/content/xul/reftest/scrollbar-marks-overlay-ref.html
diff --git a/layout/xul/reftest/scrollbar-marks-overlay-ref.html b/layout/xul/reftest/scrollbar-marks-overlay-ref.html
new file mode 100644
index 0000000000..8d940c64d6
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-overlay-ref.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ // Account for scrollbar buttons on Windows
+const hasScrollbarButtons = navigator.platform.indexOf("Win") >= 0;
+const scrollbarButtonSize = 16;
+
+function assignMarks()
+{
+ let frame0 = document.getElementById('frame0');
+ let width = frame0.getBoundingClientRect().width;
+
+ let innerRect0 = frame0.contentDocument.documentElement.getBoundingClientRect();
+ let markWidth = width - innerRect0.width - 2;
+
+ let scrollButtonHeight = hasScrollbarButtons ? scrollbarButtonSize : 0;
+ let sliderHeight = 200 - scrollButtonHeight * 2;
+
+ let one = document.getElementById('one');
+ one.style.width = markWidth + "px";
+ one.style.top = (Math.floor(30 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let two = document.getElementById('two');
+ two.style.width = markWidth + "px";
+ two.style.top = (Math.floor(70 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let three = document.getElementById('three');
+ three.style.width = markWidth + "px";
+ three.style.top = (Math.floor(110 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let frame1 = document.getElementById('frame1');
+ let height = frame1.getBoundingClientRect().height;
+
+ let innerRect1 = frame1.contentDocument.documentElement.getBoundingClientRect();
+ let markHeight = height - innerRect1.height - 2;
+
+ let scrollButtonWidth = hasScrollbarButtons ? scrollbarButtonSize : 0;
+ let sliderWidth = 300 - scrollButtonWidth * 2;
+
+ let four = document.getElementById('four');
+ four.style.height = markHeight + "px";
+ four.style.left = (Math.floor(45 / frames[1].scrollMaxX * sliderWidth) + scrollButtonWidth) + "px";
+
+ let five = document.getElementById('five');
+ five.style.height = markHeight + "px";
+ five.style.left = (Math.floor(165 / frames[1].scrollMaxX * sliderWidth) + scrollButtonWidth) + "px";
+
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="assignMarks()">
+<div style='border: 1px solid red; position: absolute; width: 300px; padding: 0;'>
+ <iframe id='frame0' style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 400px;"></p>'></iframe>
+ <div id='one' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <div id='two' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <div id='three' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <iframe id='frame1' style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 100%; width: 600px;"></p>'></iframe>
+ <div id='four' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; bottom: 0px;'></div>
+ <div id='five' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; bottom: 0px;'></div>
+</div>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks-overlay.html b/layout/xul/reftest/scrollbar-marks-overlay.html
new file mode 100644
index 0000000000..823fd6fd52
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-overlay.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ frames[0].setScrollMarks([30, 70, 110]);
+ frames[1].setScrollMarks([45, 165], true);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+<div style='border: 1px solid red; position: absolute; width: 300px; padding: 0;'>
+ <iframe style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 400px;"></p>'></iframe>
+ <iframe style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 100%; width: 600px;"></p>'></iframe>
+</div>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks-ref.html b/layout/xul/reftest/scrollbar-marks-ref.html
new file mode 100644
index 0000000000..204c5db7fa
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
+
diff --git a/layout/xul/reftest/scrollbar-marks.html b/layout/xul/reftest/scrollbar-marks.html
new file mode 100644
index 0000000000..c60d06c804
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ window.setScrollMarks([20]);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks2.html b/layout/xul/reftest/scrollbar-marks2.html
new file mode 100644
index 0000000000..e39c6e1192
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks2.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ // Two find marks should be drawn.
+ window.setScrollMarks([20, 140]);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
diff --git a/layout/xul/reftest/textbox-text-transform-ref.xhtml b/layout/xul/reftest/textbox-text-transform-ref.xhtml
new file mode 100644
index 0000000000..74d03a1ec9
--- /dev/null
+++ b/layout/xul/reftest/textbox-text-transform-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<label value="UPPERCASE"/>
+<label value="lowercase"/>
+</window>
diff --git a/layout/xul/reftest/textbox-text-transform.xhtml b/layout/xul/reftest/textbox-text-transform.xhtml
new file mode 100644
index 0000000000..5c542cf80e
--- /dev/null
+++ b/layout/xul/reftest/textbox-text-transform.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<label style="text-transform: uppercase" value="uppercase"/>
+<label style="text-transform: lowercase" value="LOWERCASE"/>
+</window>
diff --git a/layout/xul/test/browser.ini b/layout/xul/test/browser.ini
new file mode 100644
index 0000000000..0f79707843
--- /dev/null
+++ b/layout/xul/test/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+
+[browser_bug685470.js]
+[browser_bug703210.js]
+skip-if = true # Bugs 1382428, 1567736, 1565339
+[browser_bug706743.js]
+skip-if = true # Bug 1157576
+[browser_bug1163304.js]
+skip-if =
+ os != 'linux' && os != 'win' // Due to testing menubar behavior with keyboard
+[browser_bug1754298.js]
diff --git a/layout/xul/test/browser_bug1163304.js b/layout/xul/test/browser_bug1163304.js
new file mode 100644
index 0000000000..cebc857c9a
--- /dev/null
+++ b/layout/xul/test/browser_bug1163304.js
@@ -0,0 +1,83 @@
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function () {
+ const promiseFocusInSearchBar = BrowserTestUtils.waitForEvent(
+ BrowserSearch.searchBar.textbox,
+ "focus"
+ );
+ BrowserSearch.searchBar.focus();
+ await promiseFocusInSearchBar;
+
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils();
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available when searchbar has focus"
+ );
+
+ let searchPopup = document.getElementById("PopupSearchAutoComplete");
+
+ // Open popup of the searchbar
+ // Oddly, F4 key press is sometimes not handled by the search bar.
+ // It's out of scope of this test, so, let's retry to open it if failed.
+ await (async () => {
+ async function tryToOpen() {
+ try {
+ BrowserSearch.searchBar.focus();
+ EventUtils.synthesizeKey("KEY_F4");
+ await TestUtils.waitForCondition(
+ () => searchPopup.state == "open",
+ "The popup isn't opened",
+ 5,
+ 100
+ );
+ } catch (e) {
+ // timed out, let's just return false without asserting the failure.
+ return false;
+ }
+ return true;
+ }
+ for (let i = 0; i < 5; i++) {
+ if (await tryToOpen()) {
+ return;
+ }
+ }
+ ok(false, "Failed to open the popup of searchbar");
+ })();
+
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available even when the popup of searchbar is open"
+ );
+
+ // Activate the menubar, then, the popup should be closed
+ is(searchPopup.state, "open", "The popup of searchbar shouldn't be closed");
+ let hiddenPromise = BrowserTestUtils.waitForEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Alt");
+ await hiddenPromise;
+ await new Promise(r => setTimeout(r, 0));
+
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_DISABLED,
+ "IME should not be available when menubar is active"
+ );
+ // Inactivate the menubar (and restore the focus to the searchbar
+ EventUtils.synthesizeKey("KEY_Escape");
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available after focus is back to the searchbar"
+ );
+});
diff --git a/layout/xul/test/browser_bug1754298.js b/layout/xul/test/browser_bug1754298.js
new file mode 100644
index 0000000000..05701e4f08
--- /dev/null
+++ b/layout/xul/test/browser_bug1754298.js
@@ -0,0 +1,35 @@
+add_task(async function () {
+ const PAGE = `
+<!doctype html>
+<select>
+<option value="1">AA Option</option>
+<option value="2">BB Option</option>
+<option value="3">&nbsp;CC Option</option>
+<option value="4">&nbsp;&nbsp;DD Option</option>
+<option value="5">&nbsp;&nbsp;&nbsp;EE Option</option>
+</select>`;
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, browser);
+ let popup = await popupShownPromise;
+ EventUtils.sendString("C", window);
+ EventUtils.sendKey("RETURN", window);
+ ok(
+ await TestUtils.waitForCondition(() => {
+ return SpecialPowers.spawn(
+ browser,
+ [],
+ () => content.document.querySelector("select").value
+ ).then(value => value == 3);
+ }),
+ "Unexpected value for select element (expected 3)!"
+ );
+ }
+ );
+});
diff --git a/layout/xul/test/browser_bug685470.js b/layout/xul/test/browser_bug685470.js
new file mode 100644
index 0000000000..46997b2e3b
--- /dev/null
+++ b/layout/xul/test/browser_bug685470.js
@@ -0,0 +1,38 @@
+add_task(async function () {
+ const html =
+ '<p id="p1" title="tooltip is here">This paragraph has a tooltip.</p>';
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html," + html
+ );
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ { type: "mousemove" },
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ // Wait until the tooltip timeout triggers that would normally have opened the popup.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ is(
+ document.getElementById("aHTMLTooltip").state,
+ "closed",
+ "local tooltip is closed"
+ );
+ is(
+ document.getElementById("remoteBrowserTooltip").state,
+ "closed",
+ "remote tooltip is closed"
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/browser_bug703210.js b/layout/xul/test/browser_bug703210.js
new file mode 100644
index 0000000000..5026875310
--- /dev/null
+++ b/layout/xul/test/browser_bug703210.js
@@ -0,0 +1,56 @@
+add_task(async function () {
+ const url =
+ "data:text/html," +
+ "<html onmousemove='event.stopPropagation()'" +
+ " onmouseenter='event.stopPropagation()' onmouseleave='event.stopPropagation()'" +
+ " onmouseover='event.stopPropagation()' onmouseout='event.stopPropagation()'>" +
+ '<p id="p1" title="tooltip is here">This paragraph has a tooltip.</p>' +
+ '<p id="p2">This paragraph doesn\'t have tooltip.</p></html>';
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown",
+ false,
+ event => {
+ is(event.originalTarget.localName, "tooltip", "tooltip is showing");
+ return true;
+ }
+ );
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ false,
+ event => {
+ is(event.originalTarget.localName, "tooltip", "tooltip is hidden");
+ return true;
+ }
+ );
+
+ // Send a mousemove at a known position to start the test.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p2",
+ { type: "mousemove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p2",
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/browser_bug706743.js b/layout/xul/test/browser_bug706743.js
new file mode 100644
index 0000000000..c28721e831
--- /dev/null
+++ b/layout/xul/test/browser_bug706743.js
@@ -0,0 +1,158 @@
+add_task(async function () {
+ const url =
+ "data:text/html,<html><head></head><body>" +
+ '<a id="target" href="about:blank" title="This is tooltip text" ' +
+ 'style="display:block;height:20px;margin:10px;" ' +
+ 'onclick="return false;">here is an anchor element</a></body></html>';
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ // Send a mousemove at a known position to start the test.
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ -5,
+ { type: "mousemove" },
+ browser
+ );
+
+ // show tooltip by mousemove into target.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ // hide tooltip by mousemove to outside.
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ // mousemove into the target and start drag by emulation via nsIDragService.
+ // Note that on some platforms, we cannot actually start the drag by
+ // synthesized events. E.g., Windows waits an actual mousemove event after
+ // dragstart.
+
+ // Emulate a buggy mousemove event. widget might dispatch mousemove event
+ // during drag.
+
+ function tooltipNotExpected() {
+ ok(false, "tooltip is shown during drag");
+ }
+ addEventListener("popupshown", tooltipNotExpected, true);
+
+ let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+ dragService.startDragSessionForTests(
+ Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
+ Ci.nsIDragService.DRAGDROP_ACTION_COPY |
+ Ci.nsIDragService.DRAGDROP_ACTION_LINK
+ );
+ try {
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ } finally {
+ removeEventListener("popupshown", tooltipNotExpected, true);
+ dragService.endDragSession(true);
+ }
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ -5,
+ { type: "mousemove" },
+ browser
+ );
+
+ // If tooltip listener used a flag for managing D&D state, we would need
+ // to test if the tooltip is shown after drag.
+
+ // show tooltip by mousemove into target.
+ popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ // hide tooltip by mousemove to outside.
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ // Show tooltip after mousedown
+ popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousedown" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mouseup" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+
+ ok(true, "tooltips appear properly");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/chrome.ini b/layout/xul/test/chrome.ini
new file mode 100644
index 0000000000..67be92b0ce
--- /dev/null
+++ b/layout/xul/test/chrome.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ windowminmaxsize1.xhtml
+ windowminmaxsize2.xhtml
+ windowminmaxsize3.xhtml
+ windowminmaxsize4.xhtml
+ windowminmaxsize5.xhtml
+ windowminmaxsize6.xhtml
+ windowminmaxsize7.xhtml
+ windowminmaxsize8.xhtml
+ windowminmaxsize9.xhtml
+ windowminmaxsize10.xhtml
+ titledpanelwindow.xhtml
+
+[test_bug159346.xhtml]
+[test_bug381167.xhtml]
+[test_bug398982-1.xhtml]
+[test_bug398982-2.xhtml]
+[test_bug467442.xhtml]
+[test_bug477754.xhtml]
+[test_bug703150.xhtml]
+[test_bug987230.xhtml]
+skip-if = os == 'linux' # No native mousedown event on Linux
+[test_bug1197913.xhtml]
+[test_popupReflowPos.xhtml]
+[test_popupSizeTo.xhtml]
+[test_popupZoom.xhtml]
+[test_submenuClose.xhtml]
+[test_windowminmaxsize.xhtml]
+[test_resizer_ctrl_click.xhtml]
+[test_resizer_incontent.xhtml]
+[test_splitter.xhtml]
+skip-if = toolkit == 'android' # no XUL theme
+[test_splitter_sibling.xhtml]
+skip-if = toolkit == 'android' # no XUL theme
+[test_toolbarbutton_ctrl_click.xhtml]
+[test_menuitem_ctrl_click.xhtml]
diff --git a/layout/xul/test/file_bug386386.sjs b/layout/xul/test/file_bug386386.sjs
new file mode 100644
index 0000000000..4cd23a7909
--- /dev/null
+++ b/layout/xul/test/file_bug386386.sjs
@@ -0,0 +1,14 @@
+// SJS file for test_bug386386.html
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader(
+ "Content-Type",
+ "application/xhtml+xml;charset=utf-8",
+ false
+ );
+ response.write(
+ "%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3Cwindow%3E%3C/window%3E"
+ );
+}
diff --git a/layout/xul/test/mochitest.ini b/layout/xul/test/mochitest.ini
new file mode 100644
index 0000000000..1db641a777
--- /dev/null
+++ b/layout/xul/test/mochitest.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ file_bug386386.sjs
+[test_bug386386.html]
+allow_xul_xbl = true
+skip-if =
+ http3
+[test_bug394800.xhtml]
+allow_xul_xbl = true
+skip-if =
+ http3
+[test_bug511075.html]
+skip-if = toolkit == 'android' #bug 798806
+[test_bug563416.html]
+skip-if = toolkit == 'android'
+[test_drag_thumb_in_link.html]
+skip-if = toolkit == 'android'
diff --git a/layout/xul/test/test_bug1197913.xhtml b/layout/xul/test/test_bug1197913.xhtml
new file mode 100644
index 0000000000..539f02128f
--- /dev/null
+++ b/layout/xul/test/test_bug1197913.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1197913
+-->
+<window title="Mozilla Bug 1197913"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(nextTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197913"
+ target="_blank">Mozilla Bug 1197913</a>
+ </body>
+
+ <hbox align="center" pack="center">
+ <menulist>
+ <menupopup>
+ <menuitem label="Car" />
+ <menuitem label="Taxi" id="target" />
+ <menuitem label="Bus" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ let menulist = document.getElementsByTagName("menulist")[0];
+ let menuitem = document.getElementById("target");
+
+ function onDOMMenuItemActive(e) {
+ menuitem.removeEventListener("DOMMenuItemActive", onDOMMenuItemActive);
+
+ synthesizeMouse(menuitem, 5, 0, { type: "mousemove" });
+ synthesizeMouse(menuitem, -1, 0, { type: "mousemove" });
+
+ setTimeout(() => {
+ ok(menuitem.getAttribute("_moz-menuactive"), "Should be active");
+ SimpleTest.finish();
+ });
+ }
+
+ function onPopupShown(e) {
+ menulist.removeEventListener("popupshown", onPopupShown);
+ menuitem.addEventListener("DOMMenuItemActive", onDOMMenuItemActive);
+ synthesizeMouse(menuitem, 5, 0, { type: "mousemove" });
+ synthesizeMouse(menuitem, 6, 0, { type: "mousemove" });
+ }
+
+ function nextTest(e) {
+ menulist.addEventListener("popupshown", onPopupShown);
+ synthesizeMouseAtCenter(menulist, {});
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_bug159346.xhtml b/layout/xul/test/test_bug159346.xhtml
new file mode 100644
index 0000000000..c33823f755
--- /dev/null
+++ b/layout/xul/test/test_bug159346.xhtml
@@ -0,0 +1,143 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 159346">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=159346
+-->
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+<scrollbar id="scrollbar" curpos="0" maxpos="500"/>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+var scrollbar = document.getElementById("scrollbar");
+var downButton;
+
+var domWinUtils = SpecialPowers.DOMWindowUtils;
+domWinUtils.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbarbutton[type="increment"][sbattr="scrollbar-down-bottom"] { display: -moz-box; min-width: 3px; min-height: 3px; }', domWinUtils.AGENT_SHEET);
+
+function init()
+{
+ downButton = SpecialPowers.unwrap(
+ SpecialPowers.InspectorUtils.getChildrenForNode(scrollbar, true, false)[4]);
+ if (!downButton) {
+ ok(navigator.userAgent.indexOf("Linux") !== -1 ||
+ navigator.userAgent.indexOf("Mac") !== -1, "Theme doesn't support scrollbar buttons");
+ SimpleTest.finish();
+ return;
+ }
+ SimpleTest.executeSoon(doTest1);
+}
+
+function getCurrentPos()
+{
+ return Number(scrollbar.getAttribute("curpos"));
+}
+
+function doTest1()
+{
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #1");
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by auto repeat #1");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ is(getCurrentPos(), lastPos,
+ "scrollbar changed curpos after mouseup #1");
+ SimpleTest.executeSoon(doTest2);
+ }, 1000);
+ }, 1000);
+}
+
+function doTest2()
+{
+ SpecialPowers.setIntPref("ui.scrollbarButtonAutoRepeatBehavior", 0);
+
+ scrollbar.setAttribute("curpos", 0);
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #2");
+ lastPos = getCurrentPos();
+
+ synthesizeMouse(downButton, -10, -10, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ is(getCurrentPos(), lastPos,
+ "scrollbar changed curpos by auto repeat when cursor is outside of scrollbar button #2");
+ synthesizeMouseAtCenter(downButton, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousemove after cursor is back on the scrollbar button #2");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+ SimpleTest.executeSoon(doTest3);
+ }, 1000);
+ }, 1000);
+}
+
+function doTest3()
+{
+ SpecialPowers.setIntPref("ui.scrollbarButtonAutoRepeatBehavior", 1);
+
+ scrollbar.setAttribute("curpos", 0);
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #3");
+ synthesizeMouse(downButton, -10, -10, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by auto repeat when cursor is outside of scrollbar button #3");
+ synthesizeMouseAtCenter(downButton, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousemove after cursor is back on the scrollbar button #3");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+
+ SpecialPowers.clearUserPref("ui.scrollbarButtonAutoRepeatBehavior");
+ SimpleTest.finish();
+ }, 1000);
+ }, 1000);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+]]>
+</script>
+
+<body id="html_body" xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=159346">Mozilla Bug 159346</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+<script>
+addLoadEvent(init);
+</script>
+</body>
+
+
+</window>
diff --git a/layout/xul/test/test_bug381167.xhtml b/layout/xul/test/test_bug381167.xhtml
new file mode 100644
index 0000000000..750dabae33
--- /dev/null
+++ b/layout/xul/test/test_bug381167.xhtml
@@ -0,0 +1,52 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=381167
+-->
+<head>
+ <title>Test for Bug 381167</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=381167">Mozilla Bug 381167</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<xul:tree>
+ <xul:tree>
+ <xul:treechildren/>
+ <xul:treecol/>
+ </xul:tree>
+</xul:tree>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 381167 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function closeit() {
+ var evt = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ ctrlKey: true,
+ keyCode: 'W'.charCodeAt(0),
+ charCode: 0,
+ });
+ window.dispatchEvent(evt);
+
+ setTimeout(finish, 200);
+}
+window.addEventListener('load', closeit);
+
+function finish()
+{
+ ok(true, "This is a mochikit version of a crash test. To complete is to pass.");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug386386.html b/layout/xul/test/test_bug386386.html
new file mode 100644
index 0000000000..d3187c9142
--- /dev/null
+++ b/layout/xul/test/test_bug386386.html
@@ -0,0 +1,34 @@
+<html>
+<head><title>Testcase for bug 386386</title>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386386
+-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+
+<iframe id="test386386" src="file_bug386386.sjs"></iframe>
+
+<script class="testbody" type="application/javascript">
+
+function boom()
+{
+ var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var doc = document.getElementById("test386386").contentDocument;
+ var observes = doc.createElementNS(XUL_NS, 'observes');
+ doc.removeChild(doc.documentElement);
+ doc.appendChild(observes);
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+
+function do_test() {
+ setTimeout(boom, 200);
+}
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(do_test);
+</script>
+
+</body>
+</html>
diff --git a/layout/xul/test/test_bug394800.xhtml b/layout/xul/test/test_bug394800.xhtml
new file mode 100644
index 0000000000..26fc50f771
--- /dev/null
+++ b/layout/xul/test/test_bug394800.xhtml
@@ -0,0 +1,39 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=394800
+-->
+ <title>Test Mozilla bug 394800</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function do_test()
+{
+ var x = document.getElementById("x");
+ x.parentNode.removeChild(x);
+ is(0, 0, "this is a crash/assertion test, so we're ok if we survived this far");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+
+<body>
+
+<xul:menulist><xul:tooltip/><div><span><xul:hbox id="x"/></span></div></xul:menulist>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=394800">Mozilla Bug 394800</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<script>
+ addLoadEvent(do_test);
+</script>
+
+</body>
+</html>
diff --git a/layout/xul/test/test_bug398982-1.xhtml b/layout/xul/test/test_bug398982-1.xhtml
new file mode 100644
index 0000000000..da6598b70d
--- /dev/null
+++ b/layout/xul/test/test_bug398982-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<menuitem xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="position: absolute; display: block;">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=398982
+-->
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<tooltip type="zzz">
+<treecols/>
+</tooltip>
+
+<script xmlns="http://www.w3.org/1999/xhtml" class="testbody" type="application/javascript">
+<![CDATA[
+function doe() {
+ document.getElementsByTagName('menuitem')[0].removeAttribute('style');
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+function do_test() {
+ setTimeout(doe, 200);
+}
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(do_test);
+]]>
+</script>
+<html:body></html:body> <!-- XXX SimpleTest.showReport() requires a html:body -->
+</menuitem>
diff --git a/layout/xul/test/test_bug398982-2.xhtml b/layout/xul/test/test_bug398982-2.xhtml
new file mode 100644
index 0000000000..865e688ea3
--- /dev/null
+++ b/layout/xul/test/test_bug398982-2.xhtml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 398982">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=398982
+-->
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<popupgroup style="position: absolute; display: block;">
+<tooltip type="zzz">
+<treecols/>
+</tooltip>
+
+<script xmlns="http://www.w3.org/1999/xhtml" class="testbody" type="application/javascript">
+<![CDATA[
+function doe() {
+ document.getElementsByTagName('popupgroup')[0].removeAttribute('style');
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+function do_test() {
+ setTimeout(doe, 200);
+}
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(do_test);
+]]>
+</script>
+</popupgroup>
+<html:body></html:body> <!-- XXX SimpleTest.showReport() requires a html:body -->
+</window>
diff --git a/layout/xul/test/test_bug467442.xhtml b/layout/xul/test/test_bug467442.xhtml
new file mode 100644
index 0000000000..f0f84c3f86
--- /dev/null
+++ b/layout/xul/test/test_bug467442.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=467442
+-->
+<window title="Mozilla Bug 467442"
+ onload="onload()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test code goes here -->
+ <popupset>
+ <panel id="panel">
+ Hello.
+ </panel>
+ </popupset>
+ <hbox>
+ <button id="anchor" label="Anchor hello on here" style="transform: translate(100px, 0)"/>
+ </hbox>
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function onload() {
+ /** Test for Bug 467442 **/
+ let panel = document.getElementById("panel");
+ let anchor = document.getElementById("anchor");
+
+ panel.addEventListener("popupshown", function onpopupshown() {
+ let panelRect = panel.getBoundingClientRect();
+ let marginLeft = parseFloat(getComputedStyle(panel).marginLeft);
+ let anchorRect = anchor.getBoundingClientRect();
+ is(panelRect.left - marginLeft, anchorRect.left, "Panel should be anchored to the button");
+ panel.addEventListener("popuphidden", function onpopuphidden() {
+ SimpleTest.finish();
+ }, { once: true });
+ panel.hidePopup();
+ }, { once: true });
+
+ panel.openPopup(anchor, "after_start", 0, 0, false, false);
+ }
+
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=467442"
+ target="_blank">Mozilla Bug 467442</a>
+ </body>
+</window>
diff --git a/layout/xul/test/test_bug477754.xhtml b/layout/xul/test/test_bug477754.xhtml
new file mode 100644
index 0000000000..338f95c62e
--- /dev/null
+++ b/layout/xul/test/test_bug477754.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=477754
+-->
+<window title="Mozilla Bug 477754"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=477754"
+ target="_blank">Mozilla Bug 477754</a>
+ </body>
+
+ <hbox pack="center">
+ <label id="anchor" style="direction: rtl;" value="Anchor"/>
+ </hbox>
+ <panel id="testPopup" onpopupshown="doTest();">
+ <label value="I am a popup"/>
+ </panel>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ /** Test for Bug 477754 **/
+ SimpleTest.waitForExplicitFinish();
+
+ let testPopup, testAnchor;
+
+ addEventListener("load", function () {
+ removeEventListener("load", arguments.callee, false);
+
+ testPopup = document.getElementById("testPopup");
+ testAnchor = document.getElementById("anchor");
+
+ testPopup.openPopup(testAnchor, "after_start", 10, 0, false, false);
+ }, false);
+
+ function doTest() {
+ let anchorRect = testAnchor.getBoundingClientRect();
+ let popupRect = testPopup.getBoundingClientRect();
+ let marginRight = parseFloat(getComputedStyle(testPopup).marginRight)
+ is(Math.round(anchorRect.right - popupRect.right - marginRight), 10,
+ "RTL popup's right offset should be equal to the x offset passed to openPopup");
+ testPopup.hidePopup();
+ SimpleTest.finish();
+ }
+
+ ]]></script>
+</window>
diff --git a/layout/xul/test/test_bug511075.html b/layout/xul/test/test_bug511075.html
new file mode 100644
index 0000000000..34e784ba56
--- /dev/null
+++ b/layout/xul/test/test_bug511075.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=511075
+-->
+<head>
+ <title>Test for Bug 511075</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #scroller {
+ border: 1px solid black;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=511075">Mozilla Bug 511075</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 511075 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var tests = [
+ function() {
+ ok(true, "Setting location.hash should scroll.");
+ nextTest();
+ // Click the top scroll arrow.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = 5;
+ // On MacOSX the top scroll arrow can be below the slider just above
+ // the bottom scroll arrow.
+ if (navigator.platform.includes("Mac"))
+ y = scroller.getBoundingClientRect().height - 40;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the top scroll arrow should scroll.");
+ nextTest();
+ // Click the bottom scroll arrow.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = scroller.getBoundingClientRect().height - 25;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the bottom scroll arrow should scroll.");
+ nextTest();
+ // Click the scrollbar.
+ var x = scroller.getBoundingClientRect().width - 5;
+ synthesizeMouse(scroller, x, 40, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, 40, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the scrollbar should scroll");
+ nextTest();
+ // Click the scrollbar.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = scroller.getBoundingClientRect().height - 50;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ scroller.onscroll = null;
+ ok(true, "Clicking the scrollbar should scroll");
+ finish();
+ }
+];
+
+document.onmousedown = function () { return false; };
+document.onmouseup = function () { return true; };
+
+
+var scroller;
+var timer = 0;
+
+function failure() {
+ ok(false, scroller.onscroll + " did not run!");
+ scroller.onscroll = null;
+ finish();
+}
+
+function nextTest() {
+ clearTimeout(timer);
+ scroller.onscroll = tests.shift();
+ timer = setTimeout(failure, 2000);
+}
+
+function runTests() {
+ scroller = document.getElementById("scroller");
+ nextTest();
+ window.location.hash = "initialPosition";
+}
+
+function finish() {
+ document.onmousedown = null;
+ document.onmouseup = null;
+ clearTimeout(timer);
+ window.location.hash = "topPosition";
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+<div id="scroller" style="overflow: scroll; width: 100px; height: 150px;">
+<a id="topPosition" name="topPosition">top</a>
+<div style="width: 20000px; height: 20000px;"></div>
+<a id="initialPosition" name="initialPosition">initialPosition</a>
+<div style="width: 20000px; height: 20000px;"></div>
+</div>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug563416.html b/layout/xul/test/test_bug563416.html
new file mode 100644
index 0000000000..22abb5bdc3
--- /dev/null
+++ b/layout/xul/test/test_bug563416.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=563416
+-->
+<head>
+ <title>Test for Bug 563416</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563416">Mozilla Bug 563416</a>
+<p id="display"><iframe id="test" srcdoc='<textarea style="box-sizing:content-box; overflow: hidden; -moz-appearance:none; height: 0px; padding: 0px;" cols="20" rows="10">hsldkjvmshlkkajskdlfksdjflskdjflskdjflskdjflskdjfddddddddd</textarea>'></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 563416 **/
+
+var result = -1;
+var expected = -2;
+var i = 0;
+
+function runTest() {
+ i = 0;
+ var frame = document.getElementById('test');
+ frame.onload = function() {
+ var t = frame.contentDocument.documentElement.getElementsByTagName("textarea")[0];
+ expected = t.clientWidth + 10;
+ t.style.width = expected + 'px';
+ result = t.clientWidth;
+ if (i == 0) {
+ i++;
+ setTimeout(function(){frame.contentWindow.location.reload();},0);
+ }
+ else {
+ is(result, expected, "setting style.width changes clientWidth");
+ SimpleTest.finish();
+ }
+ }
+ frame.contentWindow.location.reload();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug703150.xhtml b/layout/xul/test/test_bug703150.xhtml
new file mode 100644
index 0000000000..4a7230bd49
--- /dev/null
+++ b/layout/xul/test/test_bug703150.xhtml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 703150">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=703150
+-->
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+<scrollbar id="scrollbar" curpos="0" maxpos="500"/>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+function doTest()
+{
+ var scrollbar = document.getElementById("scrollbar");
+ var scrollbarThumb = null;
+ for (let child of SpecialPowers.InspectorUtils.getChildrenForNode(scrollbar, true, false)) {
+ if (child.nodeName === "slider") {
+ scrollbarThumb = SpecialPowers.unwrap(child.childNodes[0]);
+ }
+ }
+
+ ok(scrollbarThumb, "Should find thumb");
+ is(scrollbarThumb.nodeName, "thumb", "Should find thumb");
+
+ function mousedownHandler(aEvent)
+ {
+ aEvent.stopPropagation();
+ }
+ window.addEventListener("mousedown", mousedownHandler, true);
+
+ // Wait for finishing reflow...
+ SimpleTest.executeSoon(function () {
+ synthesizeMouseAtCenter(scrollbarThumb, { type: "mousedown" });
+
+ is(scrollbar.getAttribute("curpos"), "0",
+ "scrollbar thumb has been moved already");
+
+ synthesizeMouseAtCenter(scrollbar, { type: "mousemove" });
+
+ ok(scrollbar.getAttribute("curpos") > 0,
+ "scrollbar thumb hasn't been dragged");
+
+ synthesizeMouseAtCenter(scrollbarThumb, { type: "mouseup" });
+
+ window.removeEventListener("mousedown", mousedownHandler, true);
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+]]>
+</script>
+
+<body id="html_body" xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=703150">Mozilla Bug 703150</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+<script>
+addLoadEvent(doTest);
+</script>
+</body>
+
+
+</window>
diff --git a/layout/xul/test/test_bug987230.xhtml b/layout/xul/test/test_bug987230.xhtml
new file mode 100644
index 0000000000..3161ad9d0e
--- /dev/null
+++ b/layout/xul/test/test_bug987230.xhtml
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=987230
+-->
+<window title="Mozilla Bug 987230"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(startTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=987230"
+ target="_blank">Mozilla Bug 987230</a>
+ </body>
+
+ <vbox>
+ <toolbar>
+ <toolbarbutton id="toolbarbutton-anchor"
+ label="Anchor"
+ consumeanchor="toolbarbutton-anchor"
+ onclick="onAnchorClick(event)"
+ style="padding: 50px !important; list-style-image: url(chrome://branding/content/icon32.png)"/>
+ </toolbar>
+ <spacer flex="1"/>
+ <hbox id="hbox-anchor"
+ style="padding: 20px"
+ onclick="onAnchorClick(event)">
+ <hbox id="inner-anchor"
+ consumeanchor="hbox-anchor"
+ >
+ Another anchor
+ </hbox>
+ </hbox>
+ <spacer flex="1"/>
+ </vbox>
+
+ <panel id="mypopup"
+ type="arrow"
+ onpopupshown="onMyPopupShown(event)"
+ onpopuphidden="onMyPopupHidden(event)">This is a test popup</panel>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 987230 **/
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.requestCompleteLog();
+
+ function onMyPopupHidden(e) {
+ ok(true, "Popup hidden");
+ if (outerAnchor.id == "toolbarbutton-anchor") {
+ popupHasShown = false;
+ outerAnchor = document.getElementById("hbox-anchor");
+ anchor = document.getElementById("inner-anchor");
+ nextTest();
+ } else {
+ //XXXgijs set mouse position back outside the iframe:
+ let frameRect = window.frameElement.getBoundingClientRect();
+ let scale = window.devicePixelRatio;
+ let outsideOfFrameX = (window.mozInnerScreenX + frameRect.width + 100) * scale;
+ let outsideOfFrameY = Math.max(0, window.mozInnerScreenY - 100) * scale;
+
+ info("Mousemove: " + outsideOfFrameX + ", " + outsideOfFrameY +
+ " (from innerscreen " + window.mozInnerScreenX + ", " + window.mozInnerScreenY +
+ " and rect width " + frameRect.width + " and scale " + scale + ")");
+ synthesizeNativeMouseEvent({
+ type: "mousemove",
+ screenX: outsideOfFrameX,
+ screenY: outsideOfFrameY,
+ scale: "inScreenPixels",
+ elementOnWidget: null,
+ });
+ SimpleTest.finish();
+ }
+ }
+
+ let popupHasShown = false;
+ function onMyPopupShown(e) {
+ popupHasShown = true;
+ synthesizeNativeMouseEvent({ type: "click", target: outerAnchor, offsetX: 5, offsetY: 5 });
+ }
+
+ function onAnchorClick(e) {
+ info("click: " + e.target.id);
+ ok(!popupHasShown, "Popup should only be shown once");
+ popup.openPopup(anchor, "bottomcenter topright");
+ }
+
+ let popup, outerAnchor, anchor;
+
+ function startTest() {
+ popup = document.getElementById("mypopup");
+ outerAnchor = document.getElementById("toolbarbutton-anchor");
+ anchor = outerAnchor.icon;
+ nextTest();
+ }
+
+ function nextTest(e) {
+ synthesizeMouse(outerAnchor, 5, 5, {});
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_drag_thumb_in_link.html b/layout/xul/test/test_drag_thumb_in_link.html
new file mode 100644
index 0000000000..7c39fd0f28
--- /dev/null
+++ b/layout/xul/test/test_drag_thumb_in_link.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=367028
+-->
+<head>
+<title>Test for Bug 367028</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<style>
+#scroller {
+ display: block;
+ width: 200px;
+ height: 100px;
+ overflow: scroll;
+ background: beige;
+ border: 1px solid black;
+}
+
+#biggerblock {
+ display: block;
+ width: 100px;
+ height: 150px;
+ line-height: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ background: khaki;
+}
+</style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=367028">Mozilla Bug 367028</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<a id="scroller" href="#">
+ block anchor<span id="biggerblock">bigger block</span>
+</a>
+<script type="application/javascript">
+
+function waitForEvent(aTarget, aEvent) {
+ return new Promise(aResolve => {
+ aTarget.addEventListener(aEvent, aResolve, { once: true });
+ });
+}
+
+/** Test for Bug 367028 **/
+
+add_task(async function drag_thumb_in_link() {
+ let scroller = document.getElementById("scroller");
+ scroller.ondragstart = function(e) {
+ e.preventDefault();
+ ok(false, "dragging on scroller bar should not trigger drag-and-drop operation");
+ scroller.ondragstart = null;
+ };
+
+ // Click the scroll bar.
+ let x = scroller.getBoundingClientRect().width - 5;
+ let y = scroller.getBoundingClientRect().height - 70;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type : "mousemove" }, window);
+
+ let scrollPromise = waitForEvent(scroller, "scroll");
+ x = scroller.getBoundingClientRect().width + 20;
+ y = scroller.getBoundingClientRect().height - 30;
+ synthesizeMouse(scroller, x, y, { type : "mousemove" }, window);
+ synthesizeMouse(scroller, x, y, { type : "mouseup" }, window);
+ await scrollPromise;
+
+ ok(true, "Dragging scroller bar should scroll");
+ scroller.ondragstart = null;
+});
+
+</script>
+</body>
+</html>
diff --git a/layout/xul/test/test_menuitem_ctrl_click.xhtml b/layout/xul/test/test_menuitem_ctrl_click.xhtml
new file mode 100644
index 0000000000..af0b82a6ed
--- /dev/null
+++ b/layout/xul/test/test_menuitem_ctrl_click.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1630828
+-->
+<window title="Mozilla Bug 1630828"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<!-- test results are displayed in the html:body -->
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1630828"
+ target="_blank">Mozilla Bug 1630828</a>
+</body>
+
+<hbox align="center" pack="center">
+ <menulist id="menu">
+ <menupopup id="popup">
+ <menuitem label="Target" id="target" />
+ </menupopup>
+ </menulist>
+</hbox>
+<!-- test code goes here -->
+<script type="application/javascript">
+<![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+function waitForEvent(target, event) {
+ info(`Waiting for ${event} event.`);
+ return new Promise(resolve => {
+ target.addEventListener(event, resolve, { once: true });
+ });
+}
+
+function waitForIdle() {
+ return new Promise(resolve => {
+ SpecialPowers.Services.tm.idleDispatchToMainThread(resolve);
+ });
+}
+
+add_setup(async function() {
+ await SimpleTest.promiseFocus();
+});
+
+add_task(async function test_ctrl_click() {
+ const isMac = AppConstants.platform === "macosx";
+
+ let popup = document.getElementById("popup");
+ let promise = waitForEvent(popup, "popupshown");
+ let menu = document.getElementById("menu");
+ synthesizeMouseAtCenter(menu, {});
+ // Wait for popup open.
+ await promise;
+
+ let commandReceived = false;
+ menu.addEventListener("command", function(e) {
+ ok(!isMac, `${AppConstants.platform} receives command event`);
+ commandReceived = true;
+ });
+
+ // Ctrl click in Mac won't dispatch command event and close popup, so we wait
+ // for idle instead.
+ promise = isMac ? waitForIdle() : waitForEvent(popup, "popuphidden");
+ let target = document.getElementById("target");
+ synthesizeMouseAtCenter(target, { ctrlKey: true });
+ await promise;
+
+ is(commandReceived, !isMac, `Check command event for ${AppConstants.platform}`);
+ is(popup.state, isMac ? "open" : "closed", `Check popup state for ${AppConstants.platform}`);
+});
+
+]]>
+</script>
+</window>
diff --git a/layout/xul/test/test_popupReflowPos.xhtml b/layout/xul/test/test_popupReflowPos.xhtml
new file mode 100644
index 0000000000..a26a833d13
--- /dev/null
+++ b/layout/xul/test/test_popupReflowPos.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window title="XUL Panel reflow placement test"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function openPopup()
+ {
+ synthesizeMouseAtCenter(document.getElementById("thebutton"), {}, window);
+ }
+
+ function popupShown(event)
+ {
+ document.getElementById("parent").className = "";
+ var popup = document.getElementById("thepopup");
+
+ var buttonbcr = document.getElementById("thebutton").getBoundingClientRect();
+ var popupbcr = popup.getOuterScreenRect();
+ var popupMarginLeft = parseFloat(getComputedStyle(popup).marginLeft);
+ var popupMarginTop = parseFloat(getComputedStyle(popup).marginTop);
+
+ ok(Math.abs(popupbcr.x - popupMarginLeft - window.mozInnerScreenX - buttonbcr.x) < 3, "x pos is correct");
+ ok(Math.abs(popupbcr.y - popupMarginTop - window.mozInnerScreenY - buttonbcr.bottom) < 3, "y pos is correct");
+
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+ <html:style>
+ .mbox {
+ display: inline-block;
+ width: 33%;
+ height: 50px;
+ background: green;
+ vertical-align: middle;
+ }
+ .orange {
+ background: orange;
+ }
+ .change > .mbox {
+ width: 60px;
+ }
+ </html:style>
+
+ <html:div style="width: 300px; height: 200px;">
+ <html:div id="parent" class="change" style="background: red; border: 1px solid black; width: 300px; height: 200px;">
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox orange">
+
+ <button label="Show" type="menu" id="thebutton">
+ <menupopup id="thepopup" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <menuitem label="New"/>
+ <menuitem label="Open"/>
+ <menuitem label="Save"/>
+ <menuseparator/>
+ <menuitem label="Exit"/>
+ </menupopup>
+ </button>
+
+ </html:div>
+ </html:div>
+ </html:div>
+
+</window>
diff --git a/layout/xul/test/test_popupSizeTo.xhtml b/layout/xul/test/test_popupSizeTo.xhtml
new file mode 100644
index 0000000000..6e60f28e0a
--- /dev/null
+++ b/layout/xul/test/test_popupSizeTo.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+XUL Panel sizeTo tests
+-->
+<window title="XUL Panel sizeTo tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function openPopup()
+ {
+ document.getElementById("panel").
+ openPopupAtScreen(Math.round(window.mozInnerScreenX) + window.innerWidth - 130,
+ Math.round(window.mozInnerScreenY) + window.innerHeight - 130);
+ }
+
+ function sizeAndCheck(width, height) {
+ var panel = document.getElementById("panel");
+ panel.sizeTo(width, height);
+ is(panel.getBoundingClientRect().width, width, "width is correct");
+ is(panel.getBoundingClientRect().height, height, "height is correct");
+
+ }
+ function popupShown(event)
+ {
+ var panel = document.getElementById("panel");
+ var bcr = panel.getBoundingClientRect();
+ // resize to 10px bigger in both dimensions.
+ sizeAndCheck(bcr.width+10, bcr.height+10);
+ // Same width, different height (based on *new* size from last sizeAndCheck)
+ sizeAndCheck(bcr.width+10, bcr.height);
+ // Same height, different width (also based on *new* size from last sizeAndCheck)
+ sizeAndCheck(bcr.width, bcr.height);
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+<panel id="panel" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+ <hbox width="50" height="50" flex="1"/>
+</panel>
+
+</window>
diff --git a/layout/xul/test/test_popupZoom.xhtml b/layout/xul/test/test_popupZoom.xhtml
new file mode 100644
index 0000000000..641ed0756c
--- /dev/null
+++ b/layout/xul/test/test_popupZoom.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window title="XUL Panel zoom test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ var savedzoom;
+
+ function openPopup()
+ {
+ docviewer = window.docShell.contentViewer;
+ savedzoom = SpecialPowers.getFullZoom(window);
+ SpecialPowers.setFullZoom(window, 2);
+
+ document.getElementById("panel").
+ openPopup(document.getElementById("anchor"), "after_start", 0, 0, false, false, null);
+ }
+
+ function popupShown(event)
+ {
+ var panel = document.getElementById("panel");
+ var panelMarginLeft = parseFloat(getComputedStyle(panel).marginLeft);
+ var panelMarginTop = parseFloat(getComputedStyle(panel).marginTop);
+ var panelbcr = panel.getBoundingClientRect();
+ var anchorbcr = document.getElementById("anchor").getBoundingClientRect();
+
+ ok(Math.abs(panelbcr.x - panelMarginLeft - anchorbcr.x) < 3, "x pos is correct");
+ ok(Math.abs(panelbcr.y - panelMarginTop - anchorbcr.bottom) < 3, "y pos is correct");
+
+ SpecialPowers.setFullZoom(window, savedzoom);
+
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+<description id="anchor" value="Sometext to this some texts"/>
+<panel id="panel" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+ <hbox width="50" height="50" flex="1"/>
+</panel>
+
+
+</window>
diff --git a/layout/xul/test/test_resizer_ctrl_click.xhtml b/layout/xul/test/test_resizer_ctrl_click.xhtml
new file mode 100644
index 0000000000..225d3c6518
--- /dev/null
+++ b/layout/xul/test/test_resizer_ctrl_click.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for the resizer element
+ -->
+<window title="Titlebar" width="200" height="200"
+ onload="setTimeout(test_resizer_ctrl_click, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<resizer id="resizer" dir="bottomend" width="16" height="16"/>
+
+<!-- test code goes here -->
+<script type="application/javascript"><![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function test_resizer_ctrl_click()
+{
+ let resizer = document.getElementById("resizer");
+ let isCommandFired = false;
+
+ resizer.addEventListener("click", function(aEvent) {
+ // Delay check for command event, because it is fired after click event.
+ setTimeout(() => {
+ ok(isCommandFired, "Check if command event is fired");
+ SimpleTest.finish();
+ }, 0);
+ });
+ resizer.addEventListener("command", function(aEvent) {
+ isCommandFired = true;
+ ok(aEvent.ctrlKey, "Check ctrlKey for command event");
+ ok(!aEvent.shiftKey, "Check shiftKey for command event");
+ ok(!aEvent.altKey, "Check altKey for command event");
+ ok(!aEvent.metaKey, "Check metaKey for command event");
+ is(aEvent.inputSource, MouseEvent.MOZ_SOURCE_MOUSE,
+ "Check inputSource for command event");
+ });
+ synthesizeMouseAtCenter(resizer, { ctrlKey: true });
+}
+
+]]>
+</script>
+
+</window>
diff --git a/layout/xul/test/test_resizer_incontent.xhtml b/layout/xul/test/test_resizer_incontent.xhtml
new file mode 100644
index 0000000000..2d29dd3f8d
--- /dev/null
+++ b/layout/xul/test/test_resizer_incontent.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+This test ensures that a resizer in content doesn't resize the window.
+-->
+<window title="XUL resizer in content test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function testResizer()
+ {
+ var oldScreenX = window.screenX;
+ var oldScreenY = window.screenY;
+ var oldWidth = window.outerWidth;
+ var oldHeight = window.outerHeight;
+ var resizer = document.getElementById("resizer");
+ synthesizeMouseAtCenter(resizer, { type:"mousedown" });
+ synthesizeMouse(resizer, 32, 32, { type:"mousemove" });
+ synthesizeMouse(resizer, 32, 32, { type:"mouseup" });
+ is(window.screenX, oldScreenX, "window not moved for non-chrome window screenX");
+ is(window.screenY, oldScreenY, "window not moved for non-chrome window screenY");
+ is(window.outerWidth, oldWidth, "window not moved for non-chrome window outerWidth");
+ is(window.outerHeight, oldHeight, "window not moved for non-chrome window outerHeight");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(testResizer);
+ ]]></script>
+
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+
+</window>
diff --git a/layout/xul/test/test_splitter.xhtml b/layout/xul/test/test_splitter.xhtml
new file mode 100644
index 0000000000..32c4118c8c
--- /dev/null
+++ b/layout/xul/test/test_splitter.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<?xml-stylesheet href="data:text/css, * { flex-shrink: 0 } hbox { border: 1px solid red; } vbox { border: 1px solid green }" type="text/css"?>
+<!--
+XUL <splitter> collapsing tests
+-->
+<window title="XUL splitter collapsing tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="horizontal">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ async function dragSplitter(offsetX) {
+ info(`Dragging splitter ${splitter.id} to ${offsetX}`);
+
+ const splitterRect = splitter.getBoundingClientRect();
+ const splitterWidth = splitterRect.width;
+ synthesizeMouse(splitter, splitterWidth / 2, 2, {type: "mousedown"});
+ synthesizeMouse(splitter, splitterWidth / 2, 1, {type: "mousemove"});
+ await new Promise(SimpleTest.executeSoon);
+ is(splitter.getAttribute("state"), "dragging", "The splitter should be dragged");
+ synthesizeMouse(splitter, offsetX, 1, {type: "mousemove"});
+ synthesizeMouse(splitter, offsetX, 1, {type: "mouseup"});
+ await new Promise(SimpleTest.executeSoon);
+ const newSplitterRect = splitter.getBoundingClientRect();
+ is(
+ offsetX > 0,
+ newSplitterRect.left > splitterRect.left,
+ `Should move in the right direction ${splitterRect.left} -> ${newSplitterRect.left}, ${offsetX}`
+ );
+ }
+
+ function shouldBeCollapsed(where) {
+ is(splitter.getAttribute("state"), "collapsed", "The splitter should be collapsed");
+ is(splitter.getAttribute("substate"), where, "The splitter should be collapsed " + where);
+ }
+
+ function shouldNotBeCollapsed() {
+ is(splitter.getAttribute("state"), "", "The splitter should not be collapsed");
+ }
+
+ async function runPass(isRTL, rightCollapsed, leftCollapsed) {
+ const containerWidth = splitter.parentNode.getBoundingClientRect().width;
+ await dragSplitter(containerWidth);
+ if (rightCollapsed) {
+ shouldBeCollapsed(isRTL ? "before" : "after");
+ } else {
+ shouldNotBeCollapsed();
+ }
+ await dragSplitter(-containerWidth * 2);
+ if (leftCollapsed) {
+ shouldBeCollapsed(isRTL ? "after" : "before");
+ } else {
+ shouldNotBeCollapsed();
+ }
+ await dragSplitter(containerWidth / 2);
+ // the splitter should never be collapsed in the middle
+ shouldNotBeCollapsed();
+ }
+
+ var splitter;
+ var activeBox = null;
+ function setActiveBox(element) {
+ if (activeBox) {
+ activeBox.style.display = "none";
+ }
+ if (element) {
+ element.style.display = "";
+ element.getBoundingClientRect();
+ }
+ activeBox = element;
+ }
+
+ async function runTests(rtl, splitterId) {
+ info(`Running tests for ${splitterId}`);
+ splitter = document.getElementById(splitterId);
+ setActiveBox(splitter.parentNode);
+ await runPass(rtl, false, false);
+ splitter.setAttribute("collapse", "before");
+ await runPass(rtl, rtl, !rtl);
+ splitter.setAttribute("collapse", "after");
+ await runPass(rtl, !rtl, rtl);
+ splitter.setAttribute("collapse", "both");
+ await runPass(rtl, true, true);
+ }
+
+ async function runAllTests() {
+ await runTests(false, "ltr-splitter");
+ await runTests(true, "rtl-splitter");
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(function() {SimpleTest.executeSoon(runAllTests);});
+ ]]></script>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: ltr;">
+ <vbox style="height: 300px; flex: 1 auto"/>
+ <splitter id="ltr-splitter" style="width: 5px"/>
+ <vbox style="height: 300px; flex: 1 auto;"/>
+ </hbox>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: rtl;">
+ <vbox style="height: 300px; flex: 1 auto" />
+ <splitter id="rtl-splitter" style="width: 5px"/>
+ <vbox style="height: 300px; flex: 1 auto" />
+ </hbox>
+
+</window>
diff --git a/layout/xul/test/test_splitter_sibling.xhtml b/layout/xul/test/test_splitter_sibling.xhtml
new file mode 100644
index 0000000000..a2e00890c5
--- /dev/null
+++ b/layout/xul/test/test_splitter_sibling.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<?xml-stylesheet href="data:text/css, hbox { border: 1px solid red; } vbox { border: 1px solid green }" type="text/css"?>
+<window title="XUL splitter resizebefore/after tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="horizontal">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <hbox style="width: 200px; height: 200px; direction: ltr; display: none">
+ <vbox style="height: 200px; width: 40px" />
+ <splitter id="ltr-splitter-before" style="width: 5px" resizebefore="sibling" resizeafter="none"/>
+ <vbox style="height: 200px;" flex="1"/>
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: rtl; display: none">
+ <vbox style="height: 200px; width: 40px" />
+ <splitter id="rtl-splitter-before" style="width: 5px" resizebefore="sibling" resizeafter="none"/>
+ <vbox style="height: 200px;" flex="1"/>
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: ltr; display: none">
+ <vbox style="height: 200px;" flex="1"/>
+ <splitter id="ltr-splitter-after" style="width: 5px" resizeafter="sibling" resizebefore="none"/>
+ <vbox style="height: 200px; width: 40px" />
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: rtl; display: none">
+ <vbox style="height: 200px;" flex="1"/>
+ <splitter id="rtl-splitter-after" style="width: 5px" resizeafter="sibling" resizebefore="none"/>
+ <vbox style="height: 200px; width: 40px" />
+ </hbox>
+
+ <script><![CDATA[
+ async function dragSplitter(splitter, offsetX) {
+ info(`Dragging splitter ${splitter.id} to ${offsetX}`);
+
+ const splitterRect = splitter.getBoundingClientRect();
+ const splitterWidth = splitterRect.width;
+ synthesizeMouse(splitter, splitterWidth / 2, 2, {type: "mousedown"});
+ synthesizeMouse(splitter, splitterWidth / 2, 1, {type: "mousemove"});
+ await new Promise(SimpleTest.executeSoon);
+ is(splitter.getAttribute("state"), "dragging", "The splitter should be dragged");
+ synthesizeMouse(splitter, offsetX, 1, {type: "mousemove"});
+ synthesizeMouse(splitter, offsetX, 1, {type: "mouseup"});
+ await new Promise(SimpleTest.executeSoon);
+ const newSplitterRect = splitter.getBoundingClientRect();
+ is(
+ offsetX > 0,
+ newSplitterRect.left > splitterRect.left,
+ `Should move in the right direction ${splitterRect.left} -> ${newSplitterRect.left}, ${offsetX}`
+ );
+ }
+
+ add_task(async function() {
+ for (let splitter of document.querySelectorAll("splitter")) {
+ info(`Testing ${splitter.id}`);
+ splitter.parentNode.style.display = "";
+ const isBefore = splitter.getAttribute("resizebefore") == "sibling";
+ const isRtl = getComputedStyle(splitter).direction == "rtl";
+
+ const resizableElement = isBefore ? splitter.previousElementSibling : splitter.nextElementSibling;
+ const nonResizableElement = isBefore ? splitter.nextElementSibling : splitter.previousElementSibling;
+ const oldWidth = resizableElement.getBoundingClientRect().width;
+
+ await dragSplitter(splitter, 10);
+
+ is(nonResizableElement.style.width, "", "Shouldn't have set width");
+ isnot(resizableElement.style.width, "40px", "Should've changed width");
+
+ const newWidth = resizableElement.getBoundingClientRect().width;
+
+ info(`Went from ${oldWidth} -> ${newWidth}\n`);
+
+ if (isRtl == isBefore) {
+ ok(newWidth < oldWidth, "Should've shrunk");
+ } else {
+ ok(newWidth > oldWidth, "Should've grown");
+ }
+ splitter.parentNode.style.display = "none";
+ }
+ });
+ ]]></script>
+</window>
diff --git a/layout/xul/test/test_submenuClose.xhtml b/layout/xul/test/test_submenuClose.xhtml
new file mode 100644
index 0000000000..47337e61b9
--- /dev/null
+++ b/layout/xul/test/test_submenuClose.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181560
+-->
+<window title="Mozilla Bug 1181560"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(nextTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181560"
+ target="_blank">Mozilla Bug 1181560</a>
+ </body>
+
+ <vbox>
+ <menubar>
+ <menu id="menu" label="MyMenu">
+ <menupopup>
+ <menuitem label="A"/>
+ <menu id="b" label="B">
+ <menupopup>
+ <menuitem label="B1"/>
+ </menupopup>
+ </menu>
+ <menu id="c" label="C">
+ <menupopup>
+ <menuitem label="C1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+ </vbox>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 1181560 **/
+ SimpleTest.waitForExplicitFinish();
+
+ let menuB, menuC, mainMenu, menuBOpen, menuCOpen;
+ let menuBOpenCount = 0;
+
+ function handleBOpens() {
+ menuBOpenCount++;
+ menuBOpen = true;
+ ok(!menuCOpen, "Menu C should not be open when menu B has opened");
+ if (menuBOpenCount >= 2) {
+ SimpleTest.finish();
+ return;
+ }
+ sendKey("LEFT", window);
+ sendKey("DOWN", window);
+ sendKey("RIGHT", window);
+ }
+
+ function handleBCloses() {
+ menuBOpen = false;
+ }
+
+ function handleCOpens() {
+ menuCOpen = true;
+ ok(!menuBOpen, "Menu B should not be open when menu C has opened");
+ synthesizeMouseAtCenter(menuB, {}, window);
+ }
+
+ function handleCCloses() {
+ menuCOpen = false;
+ }
+
+ function nextTest(e) {
+ mainMenu = document.getElementById("menu");
+ menuB = document.getElementById("b");
+ menuC = document.getElementById("c");
+ menuB.menupopup.addEventListener("popupshown", handleBOpens);
+ menuB.menupopup.addEventListener("popuphidden", handleBCloses);
+ menuC.menupopup.addEventListener("popupshown", handleCOpens);
+ menuC.menupopup.addEventListener("popuphidden", handleCCloses);
+ mainMenu.addEventListener("popupshown", ev => {
+ synthesizeMouseAtCenter(menuB, {}, window);
+ });
+ mainMenu.open = true;
+ }
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml
new file mode 100644
index 0000000000..6ad5f18ae7
--- /dev/null
+++ b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for the toolbarbutton element
+ -->
+<window title="Titlebar" width="200" height="200"
+ onload="setTimeout(test_resizer_ctrl_click, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<toolbarbutton id="toolbarbutton" width="16" height="16"/>
+
+<!-- test code goes here -->
+<script type="application/javascript"><![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function test_resizer_ctrl_click()
+{
+ let toolbarbutton = document.getElementById("toolbarbutton");
+ let isCommandFired = false;
+
+ toolbarbutton.addEventListener("click", function(aEvent) {
+ // Delay check for command event, because it is fired after click event.
+ setTimeout(() => {
+ ok(isCommandFired, "Check if command event is fired");
+ SimpleTest.finish();
+ }, 0);
+ });
+ toolbarbutton.addEventListener("command", function(aEvent) {
+ isCommandFired = true;
+ ok(aEvent.ctrlKey, "Check ctrlKey for command event");
+ ok(!aEvent.shiftKey, "Check shiftKey for command event");
+ ok(!aEvent.altKey, "Check altKey for command event");
+ ok(!aEvent.metaKey, "Check metaKey for command event");
+ is(aEvent.inputSource, MouseEvent.MOZ_SOURCE_MOUSE,
+ "Check inputSource for command event");
+ });
+ synthesizeMouseAtCenter(toolbarbutton, { ctrlKey: true });
+}
+
+]]>
+</script>
+
+</window>
diff --git a/layout/xul/test/test_windowminmaxsize.xhtml b/layout/xul/test/test_windowminmaxsize.xhtml
new file mode 100644
index 0000000000..187732dd3d
--- /dev/null
+++ b/layout/xul/test/test_windowminmaxsize.xhtml
@@ -0,0 +1,193 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Window Minimum and Maximum Size Tests" onload="nextTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<html:style>
+<![CDATA[
+ panel::part(content) {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ }
+]]>
+</html:style>
+
+<panel id="panel" onpopupshown="doPanelTest(this)" onpopuphidden="nextPopupTest(this)"
+ orient="vertical"
+ align="start" pack="start" style="appearance: none; margin: 0; border: 0; padding: 0;">
+ <hbox id="popupresizer" dir="bottomright"
+ style="width: 60px; height: 60px; appearance: none; margin: 0; border: 0; padding: 0;"/>
+</panel>
+
+<script>
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var gTestId = -1;
+
+// width and height in the tests below specify the expected size of the window.
+// note, win8 has a minimum inner window size of around 122 pixels. Don't go below this on min-width tests.
+var tests = [
+ { testname: "unconstrained",
+ src: "windowminmaxsize1.xhtml",
+ width: 150, height: 150 },
+ { testname: "constraint min style",
+ src: "windowminmaxsize2.xhtml",
+ width: 180, height: 210 },
+ { testname: "constraint max style",
+ src: "windowminmaxsize3.xhtml",
+ width: 125, height: 140 },
+ { testname: "constraint min attributes",
+ src: "windowminmaxsize4.xhtml",
+ width: 240, height: 220 },
+ { testname: "constraint min attributes with width and height set",
+ src: "windowminmaxsize5.xhtml",
+ width: 215, height: 235 },
+ { testname: "constraint max attributes",
+ src: "windowminmaxsize6.xhtml",
+ width: 125, height: 95 },
+ // this gets the inner width as <window minwidth='210'> makes the box 210 pixels wide
+ { testname: "constraint min width attribute only",
+ src: "windowminmaxsize7.xhtml",
+ width: 210, height: 150 },
+ { testname: "constraint max width attribute only",
+ src: "windowminmaxsize8.xhtml",
+ width: 128, height: 150 },
+ { testname: "constraint max width attribute with minheight",
+ src: "windowminmaxsize9.xhtml",
+ width: 195, height: 180 },
+ { testname: "constraint minwidth, minheight, maxwidth and maxheight set",
+ src: "windowminmaxsize10.xhtml",
+ width: 150, height: 150 }
+];
+
+var popupTests = [
+ { testname: "popup unconstrained",
+ width: 60, height: 60
+ },
+ { testname: "popup with minimum size",
+ minWidth: 150, minHeight: 180,
+ width: 150, height: 180
+ },
+ { testname: "popup with maximum size",
+ maxWidth: 50, maxHeight: 45,
+ width: 50, height: 45,
+ }
+];
+
+function nextTest()
+{
+ info(`Running test ${gTestId}`);
+ // Run through each of the tests above by opening a simple window with
+ // the attributes or style defined for that test. The comparisons will be
+ // done by windowOpened. gTestId holds the index into the tests array.
+ if (++gTestId >= tests.length) {
+ // Now do the popup tests
+ gTestId = -1;
+ SimpleTest.waitForFocus(function () { nextPopupTest(document.getElementById("panel")) } );
+ }
+ else {
+ info(`opening ${tests[gTestId].src}`);
+ tests[gTestId].window = window.browsingContext.topChromeWindow.open(tests[gTestId].src, "_blank", "chrome,resizable=yes");
+ SimpleTest.waitForFocus(windowOpened, tests[gTestId].window);
+ }
+}
+
+function windowOpened(otherWindow)
+{
+ // Check the width and the width plus one due to bug 696746.
+ ok(otherWindow.innerWidth == tests[gTestId].width ||
+ otherWindow.innerWidth == tests[gTestId].width + 1,
+ tests[gTestId].testname + " width of " + otherWindow.innerWidth + " matches " + tests[gTestId].width);
+ is(otherWindow.innerHeight, tests[gTestId].height, tests[gTestId].testname + " height");
+
+ otherWindow.close();
+ nextTest();
+}
+
+function doPanelTest(panel)
+{
+ var rect = panel.getBoundingClientRect();
+ is(rect.width, popupTests[gTestId].width, popupTests[gTestId].testname + " width");
+ is(rect.height, popupTests[gTestId].height, popupTests[gTestId].testname + " height");
+
+ panel.hidePopup();
+}
+
+function nextPopupTest(panel)
+{
+ if (++gTestId >= popupTests.length) {
+ // Next, check a panel that has a titlebar to ensure that it is accounted for
+ // properly in the size.
+ var titledPanelWindow = window.browsingContext.topChromeWindow.open("titledpanelwindow.xhtml", "_blank", "chrome,resizable=yes");
+ SimpleTest.waitForFocus(titledPanelWindowOpened, titledPanelWindow);
+ }
+ else {
+ function setStyle(attr) {
+ if (attr in popupTests[gTestId])
+ panel.style[attr] = popupTests[gTestId][attr] + "px";
+ else
+ panel.style[attr] = "";
+ }
+ setStyle("minWidth");
+ setStyle("minHeight");
+ setStyle("maxWidth");
+ setStyle("maxHeight");
+
+ // Prevent event loop starvation as a result of popup events being
+ // synchronous. See bug 1131576.
+ SimpleTest.executeSoon(() => {
+ // Non-chrome shells require focus to open a popup.
+ SimpleTest.waitForFocus(() => { panel.openPopup() });
+ });
+ }
+}
+
+function titledPanelWindowOpened(panelwindow)
+{
+ info("titledPanelWindowOpened");
+ var panel = panelwindow.document.documentElement.firstChild;
+ panel.addEventListener("popupshown", () => doTitledPanelTest(panel));
+ panel.addEventListener("popuphidden", () => done(panelwindow));
+ // See above as for why.
+ SimpleTest.executeSoon(() => {
+ SimpleTest.waitForFocus(() => { panel.openPopup() }, panelwindow);
+ });
+}
+
+function doTitledPanelTest(panel)
+{
+ info("doTitledPanelTest");
+ var rect = panel.getBoundingClientRect();
+ is(rect.width, 120, "panel with titlebar width");
+ is(rect.height, 140, "panel with titlebar height");
+ panel.hidePopup();
+}
+
+function done(panelwindow)
+{
+ panelwindow.close();
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+</window>
diff --git a/layout/xul/test/titledpanelwindow.xhtml b/layout/xul/test/titledpanelwindow.xhtml
new file mode 100644
index 0000000000..4289f8deab
--- /dev/null
+++ b/layout/xul/test/titledpanelwindow.xhtml
@@ -0,0 +1,5 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;'>
+ <panel noautohide='true' titlebar='normal' style="background: white; min-width: 120px; min-height: 140px"/>
+ <label value='Test'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize1.xhtml b/layout/xul/test/windowminmaxsize1.xhtml
new file mode 100644
index 0000000000..59f361aced
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize1.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;'>
+<resizer dir='bottomright' flex='1' style="min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;"/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize10.xhtml b/layout/xul/test/windowminmaxsize10.xhtml
new file mode 100644
index 0000000000..8b568d986f
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize10.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 120px; max-width: 480px; min-height: 110px; max-height: 470px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize2.xhtml b/layout/xul/test/windowminmaxsize2.xhtml
new file mode 100644
index 0000000000..fb72903dbd
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize2.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 180px; min-height: 210px;'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize3.xhtml b/layout/xul/test/windowminmaxsize3.xhtml
new file mode 100644
index 0000000000..ed6acbe2be
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize3.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; max-width: 125px; max-height: 140px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize4.xhtml b/layout/xul/test/windowminmaxsize4.xhtml
new file mode 100644
index 0000000000..e29a48016e
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize4.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 240px; min-height: 220px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize5.xhtml b/layout/xul/test/windowminmaxsize5.xhtml
new file mode 100644
index 0000000000..7cbce93cc1
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize5.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 215px; min-height: 235px' width='190' height='220'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize6.xhtml b/layout/xul/test/windowminmaxsize6.xhtml
new file mode 100644
index 0000000000..abba98027a
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize6.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; max-width: 125px; max-height: 95px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize7.xhtml b/layout/xul/test/windowminmaxsize7.xhtml
new file mode 100644
index 0000000000..f14e2ca4f8
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize7.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 210px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize8.xhtml b/layout/xul/test/windowminmaxsize8.xhtml
new file mode 100644
index 0000000000..8beff9d32a
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize8.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; max-width: 128px'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize9.xhtml b/layout/xul/test/windowminmaxsize9.xhtml
new file mode 100644
index 0000000000..b4a06d4ff2
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize9.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; max-width: 195px; min-height: 180px' width='230' height='120'>
+<resizer dir='bottomright' flex='1' style='min-width: 150px; min-height: 150px; appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/tree/crashtests/307298-1.xhtml b/layout/xul/tree/crashtests/307298-1.xhtml
new file mode 100644
index 0000000000..6c04a01321
--- /dev/null
+++ b/layout/xul/tree/crashtests/307298-1.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="var tree = document.getElementById('tree'), treeitem = document.getElementById('treeitem'); tree.parentNode.insertBefore(treeitem, tree);">
+
+<tree flex="1" id="tree">
+ <treecols>
+ <treecol id="name" label="Name" primary="true" flex="1"/>
+ </treecols>
+
+ <treechildren>
+ <treeitem id="treeitem">
+ <treerow>
+ <treecell label="Click the button below to crash"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/309732-1.xhtml b/layout/xul/tree/crashtests/309732-1.xhtml
new file mode 100644
index 0000000000..a7e40b75b9
--- /dev/null
+++ b/layout/xul/tree/crashtests/309732-1.xhtml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+
+ <script>
+ function boom()
+ {
+ document.documentElement.appendChild(document.getElementById("TC"));
+ document.documentElement.appendChild(document.getElementById("TI"));
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+<tree flex="1">
+ <treecols>
+ <treecol label="Name"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem id="TI">
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/layout/xul/tree/crashtests/309732-2.xhtml b/layout/xul/tree/crashtests/309732-2.xhtml
new file mode 100644
index 0000000000..354c58dacf
--- /dev/null
+++ b/layout/xul/tree/crashtests/309732-2.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+ <script>
+ function boom()
+ {
+ document.documentElement.appendChild(document.getElementById('TC'));
+ document.getElementById('TI').hidden = false;
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+
+ <tree flex="1">
+ <treecols>
+ <treecol label="Name" flex="1"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem>
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+ <treeitem id="TI" hidden="true"/>
+</window>
diff --git a/layout/xul/tree/crashtests/366583-1.xhtml b/layout/xul/tree/crashtests/366583-1.xhtml
new file mode 100644
index 0000000000..fd12709905
--- /dev/null
+++ b/layout/xul/tree/crashtests/366583-1.xhtml
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom1();"
+ class="reftest-wait">
+
+<script>
+
+var tree;
+
+function boom1()
+{
+ tree = document.getElementById("tree");
+ tree.style.position = "fixed";
+ setTimeout(boom2, 30);
+}
+
+function boom2()
+{
+ tree.style.overflow = "visible";
+ document.documentElement.removeAttribute("class");
+}
+</script>
+
+<tree rows="6" id="tree" style="display: list-item; overflow: auto; visibility: collapse;">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true" style="-moz-box-flex: 3"/>
+ <treecol id="lastname" label="Last Name" style="-moz-box-flex: 7"/>
+ </treecols>
+
+ <treechildren>
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Foo"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+
+</window>
diff --git a/layout/xul/tree/crashtests/380217-1.xhtml b/layout/xul/tree/crashtests/380217-1.xhtml
new file mode 100644
index 0000000000..251b3c450d
--- /dev/null
+++ b/layout/xul/tree/crashtests/380217-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="document.documentElement.style.content = '\'a\'';">
+
+<html:style>
+* { position: fixed; }
+*:not(style) {
+ /* At the time this testcase was added, the above `float` styling would
+ have automatically forced "display:block" for these elements, so we
+ should preserve that styling to preserve the integrity of the crashtest
+ since blockification behavior for -moz-box is changing. */
+ display: block;
+}
+</html:style>
+
+<tree rows="6">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true"/>
+ </treecols>
+ <treechildren>
+ <treeitem>
+ <treerow>
+ <treecell/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/382444-1-inner.html b/layout/xul/tree/crashtests/382444-1-inner.html
new file mode 100644
index 0000000000..01805e6b34
--- /dev/null
+++ b/layout/xul/tree/crashtests/382444-1-inner.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<title>Testcase bug - Crash [@ nsINodeInfo::Equals] with underflow event, tree stuff and removing window</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%3E%0A%3Ctree%20style%3D%22overflow%3A%20auto%3B%20display%3A%20-moz-inline-box%3B%22%3E%0A%3Ctreeitem%20style%3D%22overflow%3A%20scroll%3B%20display%3A%20table-cell%3B%22%3E%0A%3Ctreechildren%20style%3D%22%20display%3A%20table-row%3B%22%3E%0A%3Ctreeitem%20id%3D%22a%22%20style%3D%22display%3A%20table-cell%3B%22%3E%0A%3C/treeitem%3E%0A%3C/treechildren%3E%0A%3C/treeitem%3E%0A%0A%3C/tree%3E%0A%0A%3Cscript%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0Afunction%20doe%28%29%20%7B%0Adocument.getElementById%28%27a%27%29.parentNode.removeChild%28document.getElementById%28%27a%27%29%29%3B%0A%7D%0AsetTimeout%28doe%2C%20100%29%3B%0Adocument.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0Awindow.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/window%3E" id="content"></iframe>
+
+<script>
+function doe() {
+window.location.reload();
+}
+setTimeout(doe, 500);
+</script>
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/382444-1.html b/layout/xul/tree/crashtests/382444-1.html
new file mode 100644
index 0000000000..8926cf16d7
--- /dev/null
+++ b/layout/xul/tree/crashtests/382444-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 500);
+</script>
+<body>
+<iframe src="382444-1-inner.html"></iframe>
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/391178-1.xhtml b/layout/xul/tree/crashtests/391178-1.xhtml
new file mode 100644
index 0000000000..0f4b16cd99
--- /dev/null
+++ b/layout/xul/tree/crashtests/391178-1.xhtml
@@ -0,0 +1,41 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script>
+
+var ccc;
+
+function boom()
+{
+ var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var hbox = document.createElementNS(XUL_NS, 'hbox');
+ var tree = document.createElementNS(XUL_NS, 'tree');
+ var treecol = document.createElementNS(XUL_NS, 'treecol');
+
+ ccc = document.getElementById("ccc");
+
+ ccc.style.position = "fixed";
+
+ hbox.appendChild(treecol);
+ tree.appendChild(hbox);
+ ccc.appendChild(tree);
+
+ setTimeout(boom2, 200);
+}
+
+function boom2()
+{
+ ccc.style.position = "";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<div id="ccc">
+</div>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/391178-2.xhtml b/layout/xul/tree/crashtests/391178-2.xhtml
new file mode 100644
index 0000000000..423b5d1bfe
--- /dev/null
+++ b/layout/xul/tree/crashtests/391178-2.xhtml
@@ -0,0 +1,20 @@
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" class="reftest-wait">
+
+<tree id="a" style="display: block; position: fixed;">
+ <box style=" display: block; position: fixed;">
+ <treecol style=" display: -moz-box;"/>
+ </box>
+ <box style="display: block; position: fixed;">
+ <treechildren style="display: block; position: absolute;"/>
+ </box>
+</tree>
+
+<script xmlns="http://www.w3.org/1999/xhtml">
+function removestyles(){
+ document.getElementById('a').removeAttribute('style');
+ document.documentElement.removeAttribute("class");
+}
+setTimeout(removestyles, 100);
+</script>
+</window>
diff --git a/layout/xul/tree/crashtests/393665-1.xhtml b/layout/xul/tree/crashtests/393665-1.xhtml
new file mode 100644
index 0000000000..6fb5ec0c9e
--- /dev/null
+++ b/layout/xul/tree/crashtests/393665-1.xhtml
@@ -0,0 +1,3 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <treechildren style="display: block" />
+</window>
diff --git a/layout/xul/tree/crashtests/399227-1.xhtml b/layout/xul/tree/crashtests/399227-1.xhtml
new file mode 100644
index 0000000000..3ae4dfa764
--- /dev/null
+++ b/layout/xul/tree/crashtests/399227-1.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+
+ <script>
+ function boom()
+ {
+ var tree = document.getElementById("thetree");
+ var selection = tree.view.selection;
+
+ selection.select(0);
+ tree.parentNode.removeChild(tree);
+
+ // This is expected to throw an error (it used to crash).
+ try {
+ selection.rangedSelect(1, 1, false);
+ }
+ catch (ex) {}
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+<tree flex="1" id="thetree">
+ <treecols>
+ <treecol label="Name"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem id="TI1">
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="TI2">
+ <treerow>
+ <treecell label="Second treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/layout/xul/tree/crashtests/399692-1.xhtml b/layout/xul/tree/crashtests/399692-1.xhtml
new file mode 100644
index 0000000000..97eec26742
--- /dev/null
+++ b/layout/xul/tree/crashtests/399692-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+</head>
+<body>
+
+<xul:treechildren style="display: inline;" />
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/399715-1.xhtml b/layout/xul/tree/crashtests/399715-1.xhtml
new file mode 100644
index 0000000000..ea0a20cfa2
--- /dev/null
+++ b/layout/xul/tree/crashtests/399715-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<body style="float: right;" onload="document.body.style.cssFloat = '';">
+
+<xul:tree><xul:hbox><xul:treecol /></xul:hbox></xul:tree>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/409807-1.xhtml b/layout/xul/tree/crashtests/409807-1.xhtml
new file mode 100644
index 0000000000..a3af3da41b
--- /dev/null
+++ b/layout/xul/tree/crashtests/409807-1.xhtml
@@ -0,0 +1,25 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="boom();">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var tree = document.getElementById("tree");
+ var tc = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "treechildren");
+
+ document.addEventListener("DOMAttrModified", m, false);
+
+ tree.appendChild(tc);
+
+ function m()
+ {
+ document.removeEventListener("DOMAttrModified", m, false);
+ tree.removeChild(tc);
+ }
+}
+
+</script>
+
+<tree id="tree" />
+
+</window>
diff --git a/layout/xul/tree/crashtests/414170-1.xhtml b/layout/xul/tree/crashtests/414170-1.xhtml
new file mode 100644
index 0000000000..82ea63bcfd
--- /dev/null
+++ b/layout/xul/tree/crashtests/414170-1.xhtml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom();">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var option = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ document.getElementById("tc").appendChild(option);
+}
+
+</script>
+
+<tree><treechildren id="tc"><hbox/></treechildren></tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/479931-1.xhtml b/layout/xul/tree/crashtests/479931-1.xhtml
new file mode 100644
index 0000000000..458a192501
--- /dev/null
+++ b/layout/xul/tree/crashtests/479931-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var o = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ var q = document.getElementById("q");
+ q.appendChild(o);
+}
+
+</script>
+</head>
+<body onload="boom();">
+
+<xul:tree><xul:treechildren id="q"><div/></xul:treechildren></xul:tree>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/585815-iframe.xhtml b/layout/xul/tree/crashtests/585815-iframe.xhtml
new file mode 100644
index 0000000000..90c20fca80
--- /dev/null
+++ b/layout/xul/tree/crashtests/585815-iframe.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="setInterval(run, 25)">
+
+<tree style="-moz-box-flex: 1" rows="2">
+ <treecols>
+ <treecol id="sender" label="Sender" style="-moz-box-flex: 1"/>
+ <treecol id="subject" label="Subject" style="-moz-box-flex: 2"/>
+ </treecols>
+ <treechildren>
+ <treeitem>
+ <treerow>
+ <treecell label="joe@somewhere.com"/>
+ <treecell label="Top secret plans"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+<script type="text/javascript"><![CDATA[
+function run() {
+ var tree = document.getElementsByTagName("tree")[0];
+ var sel = tree.view.selection;
+ sel.rangedSelect(0, 0, true);
+ sel.rangedSelect(1000, 1001, true);
+ sel.adjustSelection(1, 0x7fffffff);
+}
+]]></script>
+
+</window>
diff --git a/layout/xul/tree/crashtests/585815.html b/layout/xul/tree/crashtests/585815.html
new file mode 100644
index 0000000000..7c3b27f6aa
--- /dev/null
+++ b/layout/xul/tree/crashtests/585815.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 585815</title>
+<script>
+function done()
+{
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="setTimeout(done,1000)">
+
+<iframe src="585815-iframe.xhtml"></iframe>
+
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/601427.html b/layout/xul/tree/crashtests/601427.html
new file mode 100644
index 0000000000..2a2999052e
--- /dev/null
+++ b/layout/xul/tree/crashtests/601427.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+
+var onPaintFunctions =
+[
+ function() { document.documentElement.style.MozAppearance = "treeheadersortarrow"; },
+ function() { document.documentElement.style.position = "fixed"; },
+ function() { document.documentElement.removeAttribute("class"); }
+];
+
+var i = 0;
+
+function advance()
+{
+ var f = onPaintFunctions[i++];
+ if (f)
+ f();
+}
+
+function start()
+{
+ window.addEventListener("MozAfterPaint", advance, true);
+ advance();
+}
+
+window.addEventListener("load", start);
+
+</script>
+</html>
diff --git a/layout/xul/tree/crashtests/730441-3.xhtml b/layout/xul/tree/crashtests/730441-3.xhtml
new file mode 100644
index 0000000000..c3fe199a83
--- /dev/null
+++ b/layout/xul/tree/crashtests/730441-3.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+###!!! ASSERTION: You can't dereference a NULL nsCOMPtr with operator->().: 'mRawPtr != 0', file ../../../../dist/include/nsCOMPtr.h, line 796
+
+Program received signal SIGSEGV, Segmentation fault.
+0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571
+571 boxObject->GetElement(getter_AddRefs(element));
+(gdb) bt 3
+#0 0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571
+#1 0xb736c76f in NS_InvokeByIndex_P () at xpcom/reflect/xptcall/md/unix/xptcinvoke_gcc_x86_unix.cpp:69
+#2 0xb6171901 in XPCWrappedNative::CallMethod (ccx=..., mode=XPCWrappedNative::CALL_METHOD)
+ at js/src/xpconnect/src/xpcwrappednative.cpp:2722
+(More stack frames follow...)
+(gdb) list 566
+561 nsTreeContentView::SetTree(nsITreeBoxObject* aTree)
+562 {
+563 ClearRows();
+564
+565 mBoxObject = aTree;
+566
+567 if (aTree && !mRoot) {
+568 // Get our root element
+569 nsCOMPtr<nsIBoxObject> boxObject = do_QueryInterface(mBoxObject);
+570 nsCOMPtr<Element> element;
+571 boxObject->GetElement(getter_AddRefs(element));
+(gdb) p boxObject
+$16 = {mRawPtr = 0x0}
+
+|aTree| does not implement |nsIBoxObject|, so |do_QueryInterface(mBoxObject)|
+returns null. Then we have |null->GetElement()|.
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="document.getElementById('tree').view.setTree({});">
+<tree id="tree">
+ <treechildren/>
+</tree>
+</window>
+
diff --git a/layout/xul/tree/crashtests/crashtests.list b/layout/xul/tree/crashtests/crashtests.list
new file mode 100644
index 0000000000..81f6ab7e6c
--- /dev/null
+++ b/layout/xul/tree/crashtests/crashtests.list
@@ -0,0 +1,18 @@
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/307298-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/366583-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/380217-1.xhtml
+load 382444-1.html
+load 391178-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/391178-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/393665-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/399227-1.xhtml
+load 399692-1.xhtml
+load 399715-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/409807-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/414170-1.xhtml
+load 479931-1.xhtml
+load 585815.html
+pref(widget.windows.window_occlusion_tracking.enabled,false) load 601427.html # Bug 1819154
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/730441-3.xhtml
diff --git a/layout/xul/tree/moz.build b/layout/xul/tree/moz.build
new file mode 100644
index 0000000000..f99b7bd41c
--- /dev/null
+++ b/layout/xul/tree/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "XUL")
+
+XPIDL_SOURCES += [
+ "nsITreeSelection.idl",
+ "nsITreeView.idl",
+]
+
+XPIDL_MODULE = "layout_xul_tree"
+
+EXPORTS += [
+ "nsTreeColumns.h",
+ "nsTreeUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsTreeBodyFrame.cpp",
+ "nsTreeColumns.cpp",
+ "nsTreeContentView.cpp",
+ "nsTreeImageListener.cpp",
+ "nsTreeSelection.cpp",
+ "nsTreeStyleCache.cpp",
+ "nsTreeUtils.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "..",
+ "../../base",
+ "../../forms",
+ "../../generic",
+ "../../painting",
+ "../../style",
+ "/dom/base",
+ "/dom/xul",
+]
diff --git a/layout/xul/tree/nsITreeSelection.idl b/layout/xul/tree/nsITreeSelection.idl
new file mode 100644
index 0000000000..d265b639ee
--- /dev/null
+++ b/layout/xul/tree/nsITreeSelection.idl
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl XULTreeElement;
+
+[scriptable, uuid(ab6fe746-300b-4ab4-abb9-1c0e3977874c)]
+interface nsITreeSelection : nsISupports
+{
+ /**
+ * The tree widget for this selection.
+ */
+ attribute XULTreeElement tree;
+
+ /**
+ * This attribute is a boolean indicating single selection.
+ */
+ readonly attribute boolean single;
+
+ /**
+ * The number of rows currently selected in this tree.
+ */
+ readonly attribute long count;
+
+ /**
+ * Indicates whether or not the row at the specified index is
+ * part of the selection.
+ */
+ boolean isSelected(in long index);
+
+ /**
+ * Deselect all rows and select the row at the specified index.
+ */
+ void select(in long index);
+
+ /**
+ * Perform a timed select.
+ */
+ void timedSelect(in long index, in long delay);
+
+ /**
+ * Toggle the selection state of the row at the specified index.
+ */
+ void toggleSelect(in long index);
+
+ /**
+ * Select the range specified by the indices. If augment is true,
+ * then we add the range to the selection without clearing out anything
+ * else. If augment is false, everything is cleared except for the specified range.
+ */
+ void rangedSelect(in long startIndex, in long endIndex, in boolean augment);
+
+ /**
+ * Clears the range.
+ */
+ void clearRange(in long startIndex, in long endIndex);
+
+ /**
+ * Clears the selection.
+ */
+ void clearSelection();
+
+ /**
+ * Selects all rows.
+ */
+ void selectAll();
+
+ /**
+ * Iterate the selection using these methods.
+ */
+ long getRangeCount();
+ void getRangeAt(in long i, out long min, out long max);
+
+ /**
+ * Can be used to invalidate the selection.
+ */
+ void invalidateSelection();
+
+ /**
+ * Called when the row count changes to adjust selection indices.
+ */
+ void adjustSelection(in long index, in long count);
+
+ /**
+ * This attribute is a boolean indicating whether or not the
+ * "select" event should fire when the selection is changed using
+ * one of our methods. A view can use this to temporarily suppress
+ * the selection while manipulating all of the indices, e.g., on
+ * a sort.
+ * Note: setting this attribute to false will fire a select event.
+ */
+ attribute boolean selectEventsSuppressed;
+
+ /**
+ * The current item (the one that gets a focus rect in addition to being
+ * selected).
+ */
+ attribute long currentIndex;
+
+ /**
+ * The selection "pivot". This is the first item the user selected as
+ * part of a ranged select.
+ */
+ readonly attribute long shiftSelectPivot;
+};
+
+/**
+ * The following interface is not scriptable and MUST NEVER BE MADE scriptable.
+ * Native treeselections implement it, and we use this to check whether a
+ * treeselection is native (and therefore suitable for use by untrusted content).
+ */
+[uuid(1bd59678-5cb3-4316-b246-31a91b19aabe)]
+interface nsINativeTreeSelection : nsITreeSelection
+{
+ [noscript] void ensureNative();
+};
diff --git a/layout/xul/tree/nsITreeView.idl b/layout/xul/tree/nsITreeView.idl
new file mode 100644
index 0000000000..7f2480ceaf
--- /dev/null
+++ b/layout/xul/tree/nsITreeView.idl
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsITreeSelection;
+
+webidl DataTransfer;
+webidl TreeColumn;
+webidl XULTreeElement;
+
+[scriptable, uuid(091116f0-0bdc-4b32-b9c8-c8d5a37cb088)]
+interface nsITreeView : nsISupports
+{
+ /**
+ * The total number of rows in the tree (including the offscreen rows).
+ */
+ readonly attribute long rowCount;
+
+ /**
+ * The selection for this view.
+ */
+ attribute nsITreeSelection selection;
+
+ /**
+ * A whitespace delimited list of properties. For each property X the view
+ * gives back will cause the pseudoclasses ::-moz-tree-cell(x),
+ * ::-moz-tree-row(x), ::-moz-tree-twisty(x), ::-moz-tree-image(x),
+ * ::-moz-tree-cell-text(x). to be matched on the pseudoelement
+ * ::moz-tree-row.
+ */
+ AString getRowProperties(in long index);
+
+ /**
+ * A whitespace delimited list of properties for a given cell. Each
+ * property, x, that the view gives back will cause the pseudoclasses
+ * ::-moz-tree-cell(x), ::-moz-tree-row(x), ::-moz-tree-twisty(x),
+ * ::-moz-tree-image(x), ::-moz-tree-cell-text(x). to be matched on the
+ * cell.
+ */
+ AString getCellProperties(in long row, in TreeColumn col);
+
+ /**
+ * Called to get properties to paint a column background. For shading the sort
+ * column, etc.
+ */
+ AString getColumnProperties(in TreeColumn col);
+
+ /**
+ * Methods that can be used to test whether or not a twisty should be drawn,
+ * and if so, whether an open or closed twisty should be used.
+ */
+ boolean isContainer(in long index);
+ boolean isContainerOpen(in long index);
+ boolean isContainerEmpty(in long index);
+
+ /**
+ * isSeparator is used to determine if the row at index is a separator.
+ * A value of true will result in the tree drawing a horizontal separator.
+ * The tree uses the ::moz-tree-separator pseudoclass to draw the separator.
+ */
+ boolean isSeparator(in long index);
+
+ /**
+ * Specifies if there is currently a sort on any column. Used mostly by dragdrop
+ * to affect drop feedback.
+ */
+ boolean isSorted();
+
+ const short DROP_BEFORE = -1;
+ const short DROP_ON = 0;
+ const short DROP_AFTER = 1;
+ /**
+ * Methods used by the drag feedback code to determine if a drag is allowable at
+ * the current location. To get the behavior where drops are only allowed on
+ * items, such as the mailNews folder pane, always return false when
+ * the orientation is not DROP_ON.
+ */
+ boolean canDrop(in long index, in long orientation, in DataTransfer dataTransfer);
+
+ /**
+ * Called when the user drops something on this view. The |orientation| param
+ * specifies before/on/after the given |row|.
+ */
+ void drop(in long row, in long orientation, in DataTransfer dataTransfer);
+
+ /**
+ * Methods used by the tree to draw thread lines in the tree.
+ * getParentIndex is used to obtain the index of a parent row.
+ * If there is no parent row, getParentIndex returns -1.
+ */
+ long getParentIndex(in long rowIndex);
+
+ /**
+ * hasNextSibling is used to determine if the row at rowIndex has a nextSibling
+ * that occurs *after* the index specified by afterIndex. Code that is forced
+ * to march down the view looking at levels can optimize the march by starting
+ * at afterIndex+1.
+ */
+ boolean hasNextSibling(in long rowIndex, in long afterIndex);
+
+ /**
+ * The level is an integer value that represents
+ * the level of indentation. It is multiplied by the width specified in the
+ * :moz-tree-indentation pseudoelement to compute the exact indendation.
+ */
+ long getLevel(in long index);
+
+ /**
+ * The image path for a given cell. For defining an icon for a cell.
+ * If the empty string is returned, the :moz-tree-image pseudoelement
+ * will be used.
+ */
+ AString getImageSrc(in long row, in TreeColumn col);
+
+ /**
+ * The value for a given cell. This method is only called for columns
+ * of type other than |text|.
+ */
+ AString getCellValue(in long row, in TreeColumn col);
+
+ /**
+ * The text for a given cell. If a column consists only of an image, then
+ * the empty string is returned.
+ */
+ AString getCellText(in long row, in TreeColumn col);
+
+ /**
+ * Called during initialization to link the view to the front end box object.
+ */
+ void setTree(in XULTreeElement tree);
+
+ /**
+ * Called on the view when an item is opened or closed.
+ */
+ void toggleOpenState(in long index);
+
+ /**
+ * Called on the view when a header is clicked.
+ */
+ void cycleHeader(in TreeColumn col);
+
+ /**
+ * Should be called from a XUL onselect handler whenever the selection changes.
+ */
+ [binaryname(SelectionChangedXPCOM)]
+ void selectionChanged();
+
+ /**
+ * Called on the view when a cell in a non-selectable cycling column (e.g., unread/flag/etc.) is clicked.
+ */
+ void cycleCell(in long row, in TreeColumn col);
+
+ /**
+ * isEditable is called to ask the view if the cell contents are editable.
+ * A value of true will result in the tree popping up a text field when
+ * the user tries to inline edit the cell.
+ */
+ boolean isEditable(in long row, in TreeColumn col);
+
+ /**
+ * setCellValue is called when the value of the cell has been set by the user.
+ * This method is only called for columns of type other than |text|.
+ */
+ void setCellValue(in long row, in TreeColumn col, in AString value);
+
+ /**
+ * setCellText is called when the contents of the cell have been edited by the user.
+ */
+ void setCellText(in long row, in TreeColumn col, in AString value);
+};
diff --git a/layout/xul/tree/nsTreeBodyFrame.cpp b/layout/xul/tree/nsTreeBodyFrame.cpp
new file mode 100644
index 0000000000..c7e8272aa0
--- /dev/null
+++ b/layout/xul/tree/nsTreeBodyFrame.cpp
@@ -0,0 +1,4363 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SimpleXULLeafFrame.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "mozilla/Likely.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/intl/Segmenter.h"
+
+#include "gfxUtils.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsFontMetrics.h"
+#include "nsITreeView.h"
+#include "nsPresContext.h"
+#include "nsNameSpaceManager.h"
+
+#include "nsTreeBodyFrame.h"
+#include "nsTreeSelection.h"
+#include "nsTreeImageListener.h"
+
+#include "nsGkAtoms.h"
+#include "nsCSSAnonBoxes.h"
+
+#include "gfxContext.h"
+#include "nsIContent.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "mozilla/intl/Segmenter.h"
+#include "nsCSSRendering.h"
+#include "nsString.h"
+#include "nsContainerFrame.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsVariant.h"
+#include "nsWidgetsCID.h"
+#include "nsIFrameInlines.h"
+#include "nsTreeContentView.h"
+#include "nsTreeUtils.h"
+#include "nsStyleConsts.h"
+#include "nsITheme.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "mozilla/dom/NodeInfo.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsDisplayList.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include <algorithm>
+#include "ScrollbarActivity.h"
+
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+# include "nsIWritablePropertyBag2.h"
+#endif
+#include "nsBidiUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+using namespace mozilla::layout;
+
+enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto };
+
+// FIXME: Maybe unify with MiddleCroppingBlockFrame?
+static void CropStringForWidth(nsAString& aText, gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics, nscoord aWidth,
+ CroppingStyle aCropType) {
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ // See if the width is even smaller than the ellipsis
+ // If so, clear the text completely.
+ const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
+ aFontMetrics.SetTextRunRTL(false);
+ nscoord ellipsisWidth =
+ nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget);
+
+ if (ellipsisWidth > aWidth) {
+ aText.Truncate(0);
+ return;
+ }
+ if (ellipsisWidth == aWidth) {
+ aText.Assign(kEllipsis);
+ return;
+ }
+
+ // We will be drawing an ellipsis, thank you very much.
+ // Subtract out the required width of the ellipsis.
+ // This is the total remaining width we have to play with.
+ aWidth -= ellipsisWidth;
+
+ using mozilla::intl::GraphemeClusterBreakIteratorUtf16;
+ using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16;
+
+ // Now we crop. This is quite basic: it will not be really accurate in the
+ // presence of complex scripts with contextual shaping, etc., as it measures
+ // each grapheme cluster in isolation, not in its proper context.
+ switch (aCropType) {
+ case CropAuto:
+ case CropNone:
+ case CropRight: {
+ const Span text(aText);
+ GraphemeClusterBreakIteratorUtf16 iter(text);
+ uint32_t pos = 0;
+ nscoord totalWidth = 0;
+
+ while (Maybe<uint32_t> nextPos = iter.Next()) {
+ const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(pos, *nextPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+ pos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (pos < aText.Length()) {
+ aText.Replace(pos, aText.Length() - pos, kEllipsis);
+ }
+ } break;
+
+ case CropLeft: {
+ const Span text(aText);
+ GraphemeClusterBreakReverseIteratorUtf16 iter(text);
+ uint32_t pos = text.Length();
+ nscoord totalWidth = 0;
+
+ // nextPos is decreasing since we use a reverse iterator.
+ while (Maybe<uint32_t> nextPos = iter.Next()) {
+ const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(*nextPos, pos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ pos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (pos > 0) {
+ aText.Replace(0, pos, kEllipsis);
+ }
+ } break;
+
+ case CropCenter: {
+ const Span text(aText);
+ nscoord totalWidth = 0;
+ GraphemeClusterBreakIteratorUtf16 leftIter(text);
+ GraphemeClusterBreakReverseIteratorUtf16 rightIter(text);
+ uint32_t leftPos = 0;
+ uint32_t rightPos = text.Length();
+
+ while (leftPos < rightPos) {
+ Maybe<uint32_t> nextPos = leftIter.Next();
+ nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ leftPos = *nextPos;
+ totalWidth += charWidth;
+
+ if (leftPos >= rightPos) {
+ break;
+ }
+
+ nextPos = rightIter.Next();
+ charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ rightPos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (leftPos < rightPos) {
+ aText.Replace(leftPos, rightPos - leftPos, kEllipsis);
+ }
+ } break;
+ }
+}
+
+// Function that cancels all the image requests in our cache.
+void nsTreeBodyFrame::CancelImageRequests() {
+ for (nsTreeImageCacheEntry entry : mImageCache.Values()) {
+ // If our imgIRequest object was registered with the refresh driver
+ // then we need to deregister it.
+ nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request,
+ nullptr);
+ entry.request->UnlockImage();
+ entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ }
+}
+
+//
+// NS_NewTreeFrame
+//
+// Creates a new tree frame
+//
+nsIFrame* NS_NewTreeBodyFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsTreeBodyFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsTreeBodyFrame)
+
+NS_QUERYFRAME_HEAD(nsTreeBodyFrame)
+ NS_QUERYFRAME_ENTRY(nsIScrollbarMediator)
+ NS_QUERYFRAME_ENTRY(nsTreeBodyFrame)
+NS_QUERYFRAME_TAIL_INHERITING(SimpleXULLeafFrame)
+
+// Constructor
+nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : SimpleXULLeafFrame(aStyle, aPresContext, kClassID),
+ mImageCache(),
+ mTopRowIndex(0),
+ mPageLength(0),
+ mHorzPosition(0),
+ mOriginalHorzWidth(-1),
+ mHorzWidth(0),
+ mAdjustWidth(0),
+ mRowHeight(0),
+ mIndentation(0),
+ mUpdateBatchNest(0),
+ mRowCount(0),
+ mMouseOverRow(-1),
+ mFocused(false),
+ mHasFixedRowCount(false),
+ mVerticalOverflow(false),
+ mHorizontalOverflow(false),
+ mReflowCallbackPosted(false),
+ mCheckingOverflow(false) {
+ mColumns = new nsTreeColumns(this);
+}
+
+// Destructor
+nsTreeBodyFrame::~nsTreeBodyFrame() {
+ CancelImageRequests();
+ DetachImageListeners();
+}
+
+static void GetBorderPadding(ComputedStyle* aStyle, nsMargin& aMargin) {
+ aMargin.SizeTo(0, 0, 0, 0);
+ aStyle->StylePadding()->GetPadding(aMargin);
+ aMargin += aStyle->StyleBorder()->GetComputedBorder();
+}
+
+static void AdjustForBorderPadding(ComputedStyle* aStyle, nsRect& aRect) {
+ nsMargin borderPadding(0, 0, 0, 0);
+ GetBorderPadding(aStyle, borderPadding);
+ aRect.Deflate(borderPadding);
+}
+
+void nsTreeBodyFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ SimpleXULLeafFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mIndentation = GetIndentation();
+ mRowHeight = GetRowHeight();
+
+ // Call GetBaseElement so that mTree is assigned.
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+ if (MOZ_LIKELY(tree)) {
+ nsAutoString rows;
+ if (tree->GetAttr(nsGkAtoms::rows, rows)) {
+ nsresult err;
+ mPageLength = rows.ToInteger(&err);
+ mHasFixedRowCount = true;
+ }
+ }
+
+ if (PresContext()->UseOverlayScrollbars()) {
+ mScrollbarActivity =
+ new ScrollbarActivity(static_cast<nsIScrollbarMediator*>(this));
+ }
+}
+
+void nsTreeBodyFrame::Destroy(DestroyContext& aContext) {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->Destroy();
+ mScrollbarActivity = nullptr;
+ }
+
+ mScrollEvent.Revoke();
+ // Make sure we cancel any posted callbacks.
+ if (mReflowCallbackPosted) {
+ PresShell()->CancelReflowCallback(this);
+ mReflowCallbackPosted = false;
+ }
+
+ if (mColumns) mColumns->SetTree(nullptr);
+
+ RefPtr tree = mTree;
+
+ if (nsCOMPtr<nsITreeView> view = std::move(mView)) {
+ nsCOMPtr<nsITreeSelection> sel;
+ view->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(nullptr);
+ }
+ view->SetTree(nullptr);
+ }
+
+ // Make this call now because view->SetTree can run js which can undo this
+ // call.
+ if (tree) {
+ tree->BodyDestroyed(mTopRowIndex);
+ }
+ if (mTree && mTree != tree) {
+ mTree->BodyDestroyed(mTopRowIndex);
+ }
+
+ SimpleXULLeafFrame::Destroy(aContext);
+}
+
+void nsTreeBodyFrame::EnsureView() {
+ if (mView) {
+ return;
+ }
+
+ if (PresShell()->IsReflowLocked()) {
+ if (!mReflowCallbackPosted) {
+ mReflowCallbackPosted = true;
+ PresShell()->PostReflowCallback(this);
+ }
+ return;
+ }
+
+ AutoWeakFrame weakFrame(this);
+
+ RefPtr<XULTreeElement> tree = GetBaseElement();
+ if (!tree) {
+ return;
+ }
+ nsCOMPtr<nsITreeView> treeView = tree->GetView();
+ if (!treeView || !weakFrame.IsAlive()) {
+ return;
+ }
+ int32_t rowIndex = tree->GetCachedTopVisibleRow();
+
+ // Set our view.
+ SetView(treeView);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Scroll to the given row.
+ // XXX is this optimal if we haven't laid out yet?
+ ScrollToRow(rowIndex);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+}
+
+void nsTreeBodyFrame::ManageReflowCallback() {
+ const nscoord horzWidth = CalcHorzWidth(GetScrollParts());
+ if (!mReflowCallbackPosted) {
+ if (!mLastReflowRect || !mLastReflowRect->IsEqualEdges(mRect) ||
+ mHorzWidth != horzWidth) {
+ PresShell()->PostReflowCallback(this);
+ mReflowCallbackPosted = true;
+ mOriginalHorzWidth = mHorzWidth;
+ }
+ } else if (mHorzWidth != horzWidth && mOriginalHorzWidth == horzWidth) {
+ // FIXME(emilio): This doesn't seem sound to me, if the rect changes in the
+ // block axis.
+ PresShell()->CancelReflowCallback(this);
+ mReflowCallbackPosted = false;
+ mOriginalHorzWidth = -1;
+ }
+ mLastReflowRect = Some(mRect);
+ mHorzWidth = horzWidth;
+}
+
+nscoord nsTreeBodyFrame::GetIntrinsicBSize() {
+ return mHasFixedRowCount ? mRowHeight * mPageLength : 0;
+}
+
+void nsTreeBodyFrame::DidReflow(nsPresContext* aPresContext,
+ const ReflowInput* aReflowInput) {
+ ManageReflowCallback();
+ SimpleXULLeafFrame::DidReflow(aPresContext, aReflowInput);
+}
+
+bool nsTreeBodyFrame::ReflowFinished() {
+ if (!mView) {
+ AutoWeakFrame weakFrame(this);
+ EnsureView();
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+ }
+ if (mView) {
+ CalcInnerBox();
+ ScrollParts parts = GetScrollParts();
+ mHorzWidth = CalcHorzWidth(parts);
+ if (!mHasFixedRowCount) {
+ mPageLength =
+ (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount;
+ }
+
+ int32_t lastPageTopRow = std::max(0, mRowCount - mPageLength);
+ if (mTopRowIndex > lastPageTopRow)
+ ScrollToRowInternal(parts, lastPageTopRow);
+
+ XULTreeElement* treeContent = GetBaseElement();
+ if (treeContent && treeContent->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::keepcurrentinview,
+ nsGkAtoms::_true, eCaseMatters)) {
+ // make sure that the current selected item is still
+ // visible after the tree changes size.
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ int32_t currentIndex;
+ sel->GetCurrentIndex(&currentIndex);
+ if (currentIndex != -1) {
+ EnsureRowIsVisibleInternal(parts, currentIndex);
+ }
+ }
+ }
+
+ if (!FullScrollbarsUpdate(false)) {
+ return false;
+ }
+ }
+
+ mReflowCallbackPosted = false;
+ return false;
+}
+
+void nsTreeBodyFrame::ReflowCallbackCanceled() {
+ mReflowCallbackPosted = false;
+}
+
+nsresult nsTreeBodyFrame::GetView(nsITreeView** aView) {
+ *aView = nullptr;
+ AutoWeakFrame weakFrame(this);
+ EnsureView();
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ NS_IF_ADDREF(*aView = mView);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::SetView(nsITreeView* aView) {
+ if (aView == mView) {
+ return NS_OK;
+ }
+
+ // First clear out the old view.
+ nsCOMPtr<nsITreeView> oldView = std::move(mView);
+ if (oldView) {
+ AutoWeakFrame weakFrame(this);
+
+ nsCOMPtr<nsITreeSelection> sel;
+ oldView->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(nullptr);
+ }
+ oldView->SetTree(nullptr);
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+
+ // Only reset the top row index and delete the columns if we had an old
+ // non-null view.
+ mTopRowIndex = 0;
+ }
+
+ // Tree, meet the view.
+ mView = aView;
+
+ // Changing the view causes us to refetch our data. This will
+ // necessarily entail a full invalidation of the tree.
+ Invalidate();
+
+ RefPtr<XULTreeElement> treeContent = GetBaseElement();
+ if (treeContent) {
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* accService = GetAccService()) {
+ accService->TreeViewChanged(PresContext()->GetPresShell(), treeContent,
+ mView);
+ }
+#endif // #ifdef ACCESSIBILITY
+ FireDOMEvent(u"TreeViewChanged"_ns, treeContent);
+ }
+
+ if (aView) {
+ // Give the view a new empty selection object to play with, but only if it
+ // doesn't have one already.
+ nsCOMPtr<nsITreeSelection> sel;
+ aView->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(treeContent);
+ } else {
+ NS_NewTreeSelection(treeContent, getter_AddRefs(sel));
+ aView->SetSelection(sel);
+ }
+
+ // View, meet the tree.
+ AutoWeakFrame weakFrame(this);
+ aView->SetTree(treeContent);
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ aView->GetRowCount(&mRowCount);
+
+ if (!PresShell()->IsReflowLocked()) {
+ // The scrollbar will need to be updated.
+ FullScrollbarsUpdate(false);
+ } else if (!mReflowCallbackPosted) {
+ mReflowCallbackPosted = true;
+ PresShell()->PostReflowCallback(this);
+ }
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsITreeSelection> nsTreeBodyFrame::GetSelection() const {
+ nsCOMPtr<nsITreeSelection> sel;
+ if (nsCOMPtr<nsITreeView> view = GetExistingView()) {
+ view->GetSelection(getter_AddRefs(sel));
+ }
+ return sel.forget();
+}
+
+nsresult nsTreeBodyFrame::SetFocused(bool aFocused) {
+ if (mFocused != aFocused) {
+ mFocused = aFocused;
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ sel->InvalidateSelection();
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::GetTreeBody(Element** aElement) {
+ // NS_ASSERTION(mContent, "no content, see bug #104878");
+ if (!mContent) return NS_ERROR_NULL_POINTER;
+
+ RefPtr<Element> element = mContent->AsElement();
+ element.forget(aElement);
+ return NS_OK;
+}
+
+int32_t nsTreeBodyFrame::RowHeight() const {
+ return nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+}
+
+int32_t nsTreeBodyFrame::RowWidth() {
+ return nsPresContext::AppUnitsToIntCSSPixels(CalcHorzWidth(GetScrollParts()));
+}
+
+int32_t nsTreeBodyFrame::GetHorizontalPosition() const {
+ return nsPresContext::AppUnitsToIntCSSPixels(mHorzPosition);
+}
+
+Maybe<CSSIntRegion> nsTreeBodyFrame::GetSelectionRegion() {
+ if (!mView) {
+ return Nothing();
+ }
+
+ AutoWeakFrame wf(this);
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ if (!selection || !wf.IsAlive()) {
+ return Nothing();
+ }
+
+ RefPtr<nsPresContext> presContext = PresContext();
+ nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel());
+
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ nsPoint origin = GetOffsetTo(rootFrame);
+
+ CSSIntRegion region;
+
+ // iterate through the visible rows and add the selected ones to the
+ // drag region
+ int32_t x = nsPresContext::AppUnitsToIntCSSPixels(origin.x);
+ int32_t y = nsPresContext::AppUnitsToIntCSSPixels(origin.y);
+ int32_t top = y;
+ int32_t end = LastVisibleRow();
+ int32_t rowHeight = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+ for (int32_t i = mTopRowIndex; i <= end; i++) {
+ bool isSelected;
+ selection->IsSelected(i, &isSelected);
+ if (isSelected) {
+ region.OrWith(CSSIntRect(x, y, rect.width, rowHeight));
+ }
+ y += rowHeight;
+ }
+
+ // clip to the tree boundary in case one row extends past it
+ region.AndWith(CSSIntRect(x, top, rect.width, rect.height));
+
+ return Some(region);
+}
+
+nsresult nsTreeBodyFrame::Invalidate() {
+ if (mUpdateBatchNest) return NS_OK;
+
+ InvalidateFrame();
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateColumn(nsTreeColumn* aCol) {
+ if (mUpdateBatchNest) return NS_OK;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(-1, -1, aCol, aCol);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ nsRect columnRect;
+ nsresult rv = aCol->GetRect(this, mInnerBox.y, mInnerBox.height, &columnRect);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // When false then column is out of view
+ if (OffsetForHorzScroll(columnRect, true))
+ InvalidateFrameWithRect(columnRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateRow(int32_t aIndex) {
+ if (mUpdateBatchNest) return NS_OK;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(aIndex, aIndex, nullptr, nullptr);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ aIndex -= mTopRowIndex;
+ if (aIndex < 0 || aIndex > mPageLength) return NS_OK;
+
+ nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * aIndex,
+ mInnerBox.width, mRowHeight);
+ InvalidateFrameWithRect(rowRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateCell(int32_t aIndex, nsTreeColumn* aCol) {
+ if (mUpdateBatchNest) return NS_OK;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(aIndex, aIndex, aCol, aCol);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ aIndex -= mTopRowIndex;
+ if (aIndex < 0 || aIndex > mPageLength) return NS_OK;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ nsRect cellRect;
+ nsresult rv = aCol->GetRect(this, mInnerBox.y + mRowHeight * aIndex,
+ mRowHeight, &cellRect);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (OffsetForHorzScroll(cellRect, true)) InvalidateFrameWithRect(cellRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateRange(int32_t aStart, int32_t aEnd) {
+ if (mUpdateBatchNest) return NS_OK;
+
+ if (aStart == aEnd) return InvalidateRow(aStart);
+
+ int32_t last = LastVisibleRow();
+ if (aStart > aEnd || aEnd < mTopRowIndex || aStart > last) return NS_OK;
+
+ if (aStart < mTopRowIndex) aStart = mTopRowIndex;
+
+ if (aEnd > last) aEnd = last;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ int32_t end =
+ mRowCount > 0 ? ((mRowCount <= aEnd) ? mRowCount - 1 : aEnd) : 0;
+ FireInvalidateEvent(aStart, end, nullptr, nullptr);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ nsRect rangeRect(mInnerBox.x,
+ mInnerBox.y + mRowHeight * (aStart - mTopRowIndex),
+ mInnerBox.width, mRowHeight * (aEnd - aStart + 1));
+ InvalidateFrameWithRect(rangeRect);
+
+ return NS_OK;
+}
+
+static void FindScrollParts(nsIFrame* aCurrFrame,
+ nsTreeBodyFrame::ScrollParts* aResult) {
+ if (!aResult->mColumnsScrollFrame) {
+ nsIScrollableFrame* f = do_QueryFrame(aCurrFrame);
+ if (f) {
+ aResult->mColumnsFrame = aCurrFrame;
+ aResult->mColumnsScrollFrame = f;
+ }
+ }
+
+ if (nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame)) {
+ if (!sf->IsHorizontal()) {
+ if (!aResult->mVScrollbar) {
+ aResult->mVScrollbar = sf;
+ }
+ } else {
+ if (!aResult->mHScrollbar) {
+ aResult->mHScrollbar = sf;
+ }
+ }
+ // don't bother searching inside a scrollbar
+ return;
+ }
+
+ nsIFrame* child = aCurrFrame->PrincipalChildList().FirstChild();
+ while (child && !child->GetContent()->IsRootOfNativeAnonymousSubtree() &&
+ (!aResult->mVScrollbar || !aResult->mHScrollbar ||
+ !aResult->mColumnsScrollFrame)) {
+ FindScrollParts(child, aResult);
+ child = child->GetNextSibling();
+ }
+}
+
+nsTreeBodyFrame::ScrollParts nsTreeBodyFrame::GetScrollParts() {
+ ScrollParts result = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
+ XULTreeElement* tree = GetBaseElement();
+ if (nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr) {
+ // The way we do this, searching through the entire frame subtree, is pretty
+ // dumb! We should know where these frames are.
+ FindScrollParts(treeFrame, &result);
+ if (result.mHScrollbar) {
+ result.mHScrollbar->SetScrollbarMediatorContent(GetContent());
+ result.mHScrollbarContent = result.mHScrollbar->GetContent()->AsElement();
+ }
+ if (result.mVScrollbar) {
+ result.mVScrollbar->SetScrollbarMediatorContent(GetContent());
+ result.mVScrollbarContent = result.mVScrollbar->GetContent()->AsElement();
+ }
+ }
+ return result;
+}
+
+void nsTreeBodyFrame::UpdateScrollbars(const ScrollParts& aParts) {
+ nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+
+ AutoWeakFrame weakFrame(this);
+
+ if (aParts.mVScrollbar) {
+ nsAutoString curPos;
+ curPos.AppendInt(mTopRowIndex * rowHeightAsPixels);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos,
+ curPos, true);
+ // 'this' might be deleted here
+ }
+
+ if (weakFrame.IsAlive() && aParts.mHScrollbar) {
+ nsAutoString curPos;
+ curPos.AppendInt(mHorzPosition);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos,
+ curPos, true);
+ // 'this' might be deleted here
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+}
+
+void nsTreeBodyFrame::CheckOverflow(const ScrollParts& aParts) {
+ bool verticalOverflowChanged = false;
+ bool horizontalOverflowChanged = false;
+
+ if (!mVerticalOverflow && mRowCount > mPageLength) {
+ mVerticalOverflow = true;
+ verticalOverflowChanged = true;
+ } else if (mVerticalOverflow && mRowCount <= mPageLength) {
+ mVerticalOverflow = false;
+ verticalOverflowChanged = true;
+ }
+
+ if (aParts.mColumnsFrame) {
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ if (bounds.width != 0) {
+ /* Ignore overflows that are less than half a pixel. Yes these happen
+ all over the place when flex boxes are compressed real small.
+ Probably a result of a rounding errors somewhere in the layout code. */
+ bounds.width += nsPresContext::CSSPixelsToAppUnits(0.5f);
+ if (!mHorizontalOverflow && bounds.width < mHorzWidth) {
+ mHorizontalOverflow = true;
+ horizontalOverflowChanged = true;
+ } else if (mHorizontalOverflow && bounds.width >= mHorzWidth) {
+ mHorizontalOverflow = false;
+ horizontalOverflowChanged = true;
+ }
+ }
+ }
+
+ if (!horizontalOverflowChanged && !verticalOverflowChanged) {
+ return;
+ }
+
+ AutoWeakFrame weakFrame(this);
+
+ RefPtr<nsPresContext> presContext = PresContext();
+ RefPtr<mozilla::PresShell> presShell = presContext->GetPresShell();
+ nsCOMPtr<nsIContent> content = mContent;
+
+ if (verticalOverflowChanged) {
+ InternalScrollPortEvent event(
+ true, mVerticalOverflow ? eScrollPortOverflow : eScrollPortUnderflow,
+ nullptr);
+ event.mOrient = InternalScrollPortEvent::eVertical;
+ EventDispatcher::Dispatch(content, presContext, &event);
+ }
+
+ if (horizontalOverflowChanged) {
+ InternalScrollPortEvent event(
+ true, mHorizontalOverflow ? eScrollPortOverflow : eScrollPortUnderflow,
+ nullptr);
+ event.mOrient = InternalScrollPortEvent::eHorizontal;
+ EventDispatcher::Dispatch(content, presContext, &event);
+ }
+
+ // The synchronous event dispatch above can trigger reflow notifications.
+ // Flush those explicitly now, so that we can guard against potential infinite
+ // recursion. See bug 905909.
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ NS_ASSERTION(!mCheckingOverflow,
+ "mCheckingOverflow should not already be set");
+ // Don't use AutoRestore since we want to not touch mCheckingOverflow if we
+ // fail the weakFrame.IsAlive() check below
+ mCheckingOverflow = true;
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ mCheckingOverflow = false;
+}
+
+void nsTreeBodyFrame::InvalidateScrollbars(const ScrollParts& aParts,
+ AutoWeakFrame& aWeakColumnsFrame) {
+ if (mUpdateBatchNest || !mView) return;
+ AutoWeakFrame weakFrame(this);
+
+ if (aParts.mVScrollbar) {
+ // Do Vertical Scrollbar
+ nsAutoString maxposStr;
+
+ nscoord rowHeightAsPixels =
+ nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+
+ int32_t size = rowHeightAsPixels *
+ (mRowCount > mPageLength ? mRowCount - mPageLength : 0);
+ maxposStr.AppendInt(size);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos,
+ maxposStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Also set our page increment and decrement.
+ nscoord pageincrement = mPageLength * rowHeightAsPixels;
+ nsAutoString pageStr;
+ pageStr.AppendInt(pageincrement);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::pageincrement, pageStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+ }
+
+ if (aParts.mHScrollbar && aParts.mColumnsFrame &&
+ aWeakColumnsFrame.IsAlive()) {
+ // And now Horizontal scrollbar
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ nsAutoString maxposStr;
+
+ maxposStr.AppendInt(mHorzWidth > bounds.width ? mHorzWidth - bounds.width
+ : 0);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos,
+ maxposStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ nsAutoString pageStr;
+ pageStr.AppendInt(bounds.width);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::pageincrement, pageStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ pageStr.Truncate();
+ pageStr.AppendInt(nsPresContext::CSSPixelsToAppUnits(16));
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::increment,
+ pageStr, true);
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+}
+
+// Takes client x/y in pixels, converts them to appunits, and converts into
+// values relative to this nsTreeBodyFrame frame.
+nsPoint nsTreeBodyFrame::AdjustClientCoordsToBoxCoordSpace(int32_t aX,
+ int32_t aY) {
+ nsPoint point(nsPresContext::CSSPixelsToAppUnits(aX),
+ nsPresContext::CSSPixelsToAppUnits(aY));
+
+ nsPresContext* presContext = PresContext();
+ point -= GetOffsetTo(presContext->GetPresShell()->GetRootFrame());
+
+ // Adjust by the inner box coords, so that we're in the inner box's
+ // coordinate space.
+ point -= mInnerBox.TopLeft();
+ return point;
+} // AdjustClientCoordsToBoxCoordSpace
+
+int32_t nsTreeBodyFrame::GetRowAt(int32_t aX, int32_t aY) {
+ if (!mView) {
+ return 0;
+ }
+
+ nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY);
+
+ // Check if the coordinates are above our visible space.
+ if (point.y < 0) {
+ return -1;
+ }
+
+ return GetRowAtInternal(point.x, point.y);
+}
+
+nsresult nsTreeBodyFrame::GetCellAt(int32_t aX, int32_t aY, int32_t* aRow,
+ nsTreeColumn** aCol,
+ nsACString& aChildElt) {
+ if (!mView) return NS_OK;
+
+ nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY);
+
+ // Check if the coordinates are above our visible space.
+ if (point.y < 0) {
+ *aRow = -1;
+ return NS_OK;
+ }
+
+ nsTreeColumn* col;
+ nsCSSAnonBoxPseudoStaticAtom* child;
+ GetCellAt(point.x, point.y, aRow, &col, &child);
+
+ if (col) {
+ NS_ADDREF(*aCol = col);
+ if (child == nsCSSAnonBoxes::mozTreeCell())
+ aChildElt.AssignLiteral("cell");
+ else if (child == nsCSSAnonBoxes::mozTreeTwisty())
+ aChildElt.AssignLiteral("twisty");
+ else if (child == nsCSSAnonBoxes::mozTreeImage())
+ aChildElt.AssignLiteral("image");
+ else if (child == nsCSSAnonBoxes::mozTreeCellText())
+ aChildElt.AssignLiteral("text");
+ }
+
+ return NS_OK;
+}
+
+//
+// GetCoordsForCellItem
+//
+// Find the x/y location and width/height (all in PIXELS) of the given object
+// in the given column.
+//
+// XXX IMPORTANT XXX:
+// Hyatt says in the bug for this, that the following needs to be done:
+// (1) You need to deal with overflow when computing cell rects. See other
+// column iteration examples... if you don't deal with this, you'll mistakenly
+// extend the cell into the scrollbar's rect.
+//
+// (2) You are adjusting the cell rect by the *row" border padding. That's
+// wrong. You need to first adjust a row rect by its border/padding, and then
+// the cell rect fits inside the adjusted row rect. It also can have
+// border/padding as well as margins. The vertical direction isn't that
+// important, but you need to get the horizontal direction right.
+//
+// (3) GetImageSize() does not include margins (but it does include
+// border/padding). You need to make sure to add in the image's margins as well.
+//
+nsresult nsTreeBodyFrame::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol,
+ const nsACString& aElement,
+ int32_t* aX, int32_t* aY,
+ int32_t* aWidth,
+ int32_t* aHeight) {
+ *aX = 0;
+ *aY = 0;
+ *aWidth = 0;
+ *aHeight = 0;
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord currX = mInnerBox.x - mHorzPosition;
+
+ // The Rect for the requested item.
+ nsRect theRect;
+
+ nsPresContext* presContext = PresContext();
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ // The Rect for the current cell.
+ nscoord colWidth;
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ currCol->GetWidthInTwips(this, &colWidth);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "invalid column");
+
+ nsRect cellRect(currX, mInnerBox.y + mRowHeight * (aRow - mTopRowIndex),
+ colWidth, mRowHeight);
+
+ // Check the ID of the current column to see if it matches. If it doesn't
+ // increment the current X value and continue to the next column.
+ if (currCol != aCol) {
+ currX += cellRect.width;
+ continue;
+ }
+ // Now obtain the properties for our cell.
+ PrefillPropertyArray(aRow, currCol);
+
+ nsAutoString properties;
+ view->GetCellProperties(aRow, currCol, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+
+ // We don't want to consider any of the decorations that may be present
+ // on the current row, so we have to deflate the rect by the border and
+ // padding and offset its left and top coordinates appropriately.
+ AdjustForBorderPadding(rowContext, cellRect);
+
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ constexpr auto cell = "cell"_ns;
+ if (currCol->IsCycler() || cell.Equals(aElement)) {
+ // If the current Column is a Cycler, then the Rect is just the cell - the
+ // margins. Similarly, if we're just being asked for the cell rect,
+ // provide it.
+
+ theRect = cellRect;
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ theRect.Deflate(cellMargin);
+ break;
+ }
+
+ // Since we're not looking for the cell, and since the cell isn't a cycler,
+ // we're looking for some subcomponent, and now we need to subtract the
+ // borders and padding of the cell from cellRect so this does not
+ // interfere with our computations.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ UniquePtr<gfxContext> rc =
+ presContext->PresShell()->CreateReferenceRenderingContext();
+
+ // Now we'll start making our way across the cell, starting at the edge of
+ // the cell and proceeding until we hit the right edge. |cellX| is the
+ // working X value that we will increment as we crawl from left to right.
+ nscoord cellX = cellRect.x;
+ nscoord remainWidth = cellRect.width;
+
+ if (currCol->IsPrimary()) {
+ // If the current Column is a Primary, then we need to take into account
+ // the indentation and possibly a twisty.
+
+ // The amount of indentation is the indentation width (|mIndentation|) by
+ // the level.
+ int32_t level;
+ view->GetLevel(aRow, &level);
+ if (!isRTL) cellX += mIndentation * level;
+ remainWidth -= mIndentation * level;
+
+ // Find the twisty rect by computing its size.
+ nsRect imageRect;
+ nsRect twistyRect(cellRect);
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+ GetTwistyRect(aRow, currCol, imageRect, twistyRect, presContext,
+ twistyContext);
+
+ if ("twisty"_ns.Equals(aElement)) {
+ // If we're looking for the twisty Rect, just return the size
+ theRect = twistyRect;
+ break;
+ }
+
+ // Now we need to add in the margins of the twisty element, so that we
+ // can find the offset of the next element in the cell.
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ // Adjust our working X value with the twisty width (image size, margins,
+ // borders, padding.
+ if (!isRTL) cellX += twistyRect.width;
+ }
+
+ // Cell Image
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ nsRect imageSize = GetImageSize(aRow, currCol, false, imageContext);
+ if ("image"_ns.Equals(aElement)) {
+ theRect = imageSize;
+ theRect.x = cellX;
+ theRect.y = cellRect.y;
+ break;
+ }
+
+ // Add in the margins of the cell image.
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageSize.Inflate(imageMargin);
+
+ // Increment cellX by the image width
+ if (!isRTL) cellX += imageSize.width;
+
+ // Cell Text
+ nsAutoString cellText;
+ view->GetCellText(aRow, currCol, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ // Create a scratch rect to represent the text rectangle, with the current
+ // X and Y coords, and a guess at the width and height. The width is the
+ // remaining width we have left to traverse in the cell, which will be the
+ // widest possible value for the text rect, and the row height.
+ nsRect textRect(cellX, cellRect.y, remainWidth, cellRect.height);
+
+ // Measure the width of the text. If the width of the text is greater than
+ // the remaining width available, then we just assume that the text has
+ // been cropped and use the remaining rect as the text Rect. Otherwise,
+ // we add in borders and padding to the text dimension and give that back.
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext);
+ nscoord height = fm->MaxHeight();
+
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ // Center the text. XXX Obey vertical-align style prop?
+ if (height < textRect.height) {
+ textRect.y += (textRect.height - height) / 2;
+ textRect.height = height;
+ }
+
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(textContext, bp);
+ textRect.height += bp.top + bp.bottom;
+
+ AdjustForCellText(cellText, aRow, currCol, *rc, *fm, textRect);
+
+ theRect = textRect;
+ }
+
+ if (isRTL) theRect.x = mInnerBox.width - theRect.x - theRect.width;
+
+ *aX = nsPresContext::AppUnitsToIntCSSPixels(theRect.x);
+ *aY = nsPresContext::AppUnitsToIntCSSPixels(theRect.y);
+ *aWidth = nsPresContext::AppUnitsToIntCSSPixels(theRect.width);
+ *aHeight = nsPresContext::AppUnitsToIntCSSPixels(theRect.height);
+
+ return NS_OK;
+}
+
+int32_t nsTreeBodyFrame::GetRowAtInternal(nscoord aX, nscoord aY) {
+ if (mRowHeight <= 0) return -1;
+
+ // Now just mod by our total inner box height and add to our top row index.
+ int32_t row = (aY / mRowHeight) + mTopRowIndex;
+
+ // Check if the coordinates are below our visible space (or within our visible
+ // space but below any row).
+ if (row > mTopRowIndex + mPageLength || row >= mRowCount) return -1;
+
+ return row;
+}
+
+void nsTreeBodyFrame::CheckTextForBidi(nsAutoString& aText) {
+ // We could check to see whether the prescontext already has bidi enabled,
+ // but usually it won't, so it's probably faster to avoid the call to
+ // GetPresContext() when it's not needed.
+ if (HasRTLChars(aText)) {
+ PresContext()->SetBidiEnabled();
+ }
+}
+
+void nsTreeBodyFrame::AdjustForCellText(nsAutoString& aText, int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics,
+ nsRect& aTextRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ nscoord maxWidth = aTextRect.width;
+ bool widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan(
+ aText, aFontMetrics, drawTarget, maxWidth);
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (aColumn->Overflow()) {
+ DebugOnly<nsresult> rv;
+ nsTreeColumn* nextColumn = aColumn->GetNext();
+ while (nextColumn && widthIsGreater) {
+ while (nextColumn) {
+ nscoord width;
+ rv = nextColumn->GetWidthInTwips(this, &width);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid");
+
+ if (width != 0) {
+ break;
+ }
+
+ nextColumn = nextColumn->GetNext();
+ }
+
+ if (nextColumn) {
+ nsAutoString nextText;
+ view->GetCellText(aRowIndex, nextColumn, nextText);
+ // We don't measure or draw this text so no need to check it for
+ // bidi-ness
+
+ if (nextText.Length() == 0) {
+ nscoord width;
+ rv = nextColumn->GetWidthInTwips(this, &width);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid");
+
+ maxWidth += width;
+ widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan(
+ aText, aFontMetrics, drawTarget, maxWidth);
+
+ nextColumn = nextColumn->GetNext();
+ } else {
+ nextColumn = nullptr;
+ }
+ }
+ }
+ }
+
+ CroppingStyle cropType = CroppingStyle::CropRight;
+ if (aColumn->GetCropStyle() == 1) {
+ cropType = CroppingStyle::CropCenter;
+ } else if (aColumn->GetCropStyle() == 2) {
+ cropType = CroppingStyle::CropLeft;
+ }
+ CropStringForWidth(aText, aRenderingContext, aFontMetrics, maxWidth,
+ cropType);
+
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
+ aText, this, aFontMetrics, aRenderingContext);
+
+ switch (aColumn->GetTextAlignment()) {
+ case mozilla::StyleTextAlign::Right:
+ aTextRect.x += aTextRect.width - width;
+ break;
+ case mozilla::StyleTextAlign::Center:
+ aTextRect.x += (aTextRect.width - width) / 2;
+ break;
+ default:
+ break;
+ }
+
+ aTextRect.width = width;
+}
+
+nsCSSAnonBoxPseudoStaticAtom* nsTreeBodyFrame::GetItemWithinCellAt(
+ nscoord aX, const nsRect& aCellRect, int32_t aRowIndex,
+ nsTreeColumn* aColumn) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Obtain the properties for our cell.
+ PrefillPropertyArray(aRowIndex, aColumn);
+ nsAutoString properties;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellProperties(aRowIndex, aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the cell.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect cellRect(aCellRect);
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ cellRect.Deflate(cellMargin);
+
+ // Adjust the rect for its border and padding.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ if (aX < cellRect.x || aX >= cellRect.x + cellRect.width) {
+ // The user clicked within the cell's margins/borders/padding. This
+ // constitutes a click on the cell.
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ nscoord currX = cellRect.x;
+ nscoord remainingWidth = cellRect.width;
+
+ // Handle right alignment hit testing.
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ nsPresContext* presContext = PresContext();
+ UniquePtr<gfxContext> rc =
+ presContext->PresShell()->CreateReferenceRenderingContext();
+
+ if (aColumn->IsPrimary()) {
+ // If we're the primary column, we have indentation and a twisty.
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+
+ if (!isRTL) currX += mIndentation * level;
+ remainingWidth -= mIndentation * level;
+
+ if ((isRTL && aX > currX + remainingWidth) || (!isRTL && aX < currX)) {
+ // The user clicked within the indentation.
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ // Always leave space for the twisty.
+ nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ bool hasTwisty = false;
+ bool isContainer = false;
+ view->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ bool isContainerEmpty = false;
+ view->IsContainerEmpty(aRowIndex, &isContainerEmpty);
+ if (!isContainerEmpty) hasTwisty = true;
+ }
+
+ // Resolve style for the twisty.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, presContext,
+ twistyContext);
+
+ // We will treat a click as hitting the twisty if it happens on the margins,
+ // borders, padding, or content of the twisty object. By allowing a "slop"
+ // into the margin, we make it a little bit easier for a user to hit the
+ // twisty. (We don't want to be too picky here.)
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+ if (isRTL) twistyRect.x = currX + remainingWidth - twistyRect.width;
+
+ // Now we test to see if aX is actually within the twistyRect. If it is,
+ // and if the item should have a twisty, then we return "twisty". If it is
+ // within the rect but we shouldn't have a twisty, then we return "cell".
+ if (aX >= twistyRect.x && aX < twistyRect.x + twistyRect.width) {
+ if (hasTwisty)
+ return nsCSSAnonBoxes::mozTreeTwisty();
+ else
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ if (!isRTL) currX += twistyRect.width;
+ remainingWidth -= twistyRect.width;
+ }
+
+ // Now test to see if the user hit the icon for the cell.
+ nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height);
+
+ // Resolve style for the image.
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ nsRect iconSize = GetImageSize(aRowIndex, aColumn, false, imageContext);
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ iconSize.Inflate(imageMargin);
+ iconRect.width = iconSize.width;
+ if (isRTL) iconRect.x = currX + remainingWidth - iconRect.width;
+
+ if (aX >= iconRect.x && aX < iconRect.x + iconRect.width) {
+ // The user clicked on the image.
+ return nsCSSAnonBoxes::mozTreeImage();
+ }
+
+ if (!isRTL) currX += iconRect.width;
+ remainingWidth -= iconRect.width;
+
+ nsAutoString cellText;
+ view->GetCellText(aRowIndex, aColumn, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ nsRect textRect(currX, cellRect.y, remainingWidth, cellRect.height);
+
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ AdjustForBorderPadding(textContext, textRect);
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext);
+ AdjustForCellText(cellText, aRowIndex, aColumn, *rc, *fm, textRect);
+
+ if (aX >= textRect.x && aX < textRect.x + textRect.width)
+ return nsCSSAnonBoxes::mozTreeCellText();
+ else
+ return nsCSSAnonBoxes::mozTreeCell();
+}
+
+void nsTreeBodyFrame::GetCellAt(nscoord aX, nscoord aY, int32_t* aRow,
+ nsTreeColumn** aCol,
+ nsCSSAnonBoxPseudoStaticAtom** aChildElt) {
+ *aCol = nullptr;
+ *aChildElt = nullptr;
+
+ *aRow = GetRowAtInternal(aX, aY);
+ if (*aRow < 0) return;
+
+ // Determine the column hit.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect cellRect;
+ nsresult rv = currCol->GetRect(
+ this, mInnerBox.y + mRowHeight * (*aRow - mTopRowIndex), mRowHeight,
+ &cellRect);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("column has no frame");
+ continue;
+ }
+
+ if (!OffsetForHorzScroll(cellRect, false)) continue;
+
+ if (aX >= cellRect.x && aX < cellRect.x + cellRect.width) {
+ // We know the column hit now.
+ *aCol = currCol;
+
+ if (currCol->IsCycler())
+ // Cyclers contain only images. Fill this in immediately and return.
+ *aChildElt = nsCSSAnonBoxes::mozTreeImage();
+ else
+ *aChildElt = GetItemWithinCellAt(aX, cellRect, *aRow, currCol);
+ break;
+ }
+ }
+}
+
+nsresult nsTreeBodyFrame::GetCellWidth(int32_t aRow, nsTreeColumn* aCol,
+ gfxContext* aRenderingContext,
+ nscoord& aDesiredSize,
+ nscoord& aCurrentSize) {
+ MOZ_ASSERT(aCol, "aCol must not be null");
+ MOZ_ASSERT(aRenderingContext, "aRenderingContext must not be null");
+
+ // The rect for the current cell.
+ nscoord colWidth;
+ nsresult rv = aCol->GetWidthInTwips(this, &colWidth);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsRect cellRect(0, 0, colWidth, mRowHeight);
+
+ int32_t overflow =
+ cellRect.x + cellRect.width - (mInnerBox.x + mInnerBox.width);
+ if (overflow > 0) cellRect.width -= overflow;
+
+ // Adjust borders and padding for the cell.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(cellContext, bp);
+
+ aCurrentSize = cellRect.width;
+ aDesiredSize = bp.left + bp.right;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+
+ if (aCol->IsPrimary()) {
+ // If the current Column is a Primary, then we need to take into account
+ // the indentation and possibly a twisty.
+
+ // The amount of indentation is the indentation width (|mIndentation|) by
+ // the level.
+ int32_t level;
+ view->GetLevel(aRow, &level);
+ aDesiredSize += mIndentation * level;
+
+ // Find the twisty rect by computing its size.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ nsRect twistyRect(cellRect);
+ GetTwistyRect(aRow, aCol, imageSize, twistyRect, PresContext(),
+ twistyContext);
+
+ // Add in the margins of the twisty element.
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ aDesiredSize += twistyRect.width;
+ }
+
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ // Account for the width of the cell image.
+ nsRect imageSize = GetImageSize(aRow, aCol, false, imageContext);
+ // Add in the margins of the cell image.
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageSize.Inflate(imageMargin);
+
+ aDesiredSize += imageSize.width;
+
+ // Get the cell text.
+ nsAutoString cellText;
+ view->GetCellText(aRow, aCol, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ // Get the borders and padding for the text.
+ GetBorderPadding(textContext, bp);
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext());
+ // Get the width of the text itself
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(cellText, this, *fm,
+ *aRenderingContext);
+ nscoord totalTextWidth = width + bp.left + bp.right;
+ aDesiredSize += totalTextWidth;
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::IsCellCropped(int32_t aRow, nsTreeColumn* aCol,
+ bool* _retval) {
+ nscoord currentSize, desiredSize;
+ nsresult rv;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ UniquePtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext();
+
+ rv = GetCellWidth(aRow, aCol, rc.get(), desiredSize, currentSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = desiredSize > currentSize;
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::CreateTimer(const LookAndFeel::IntID aID,
+ nsTimerCallbackFunc aFunc, int32_t aType,
+ nsITimer** aTimer, const char* aName) {
+ // Get the delay from the look and feel service.
+ int32_t delay = LookAndFeel::GetInt(aID, 0);
+
+ nsCOMPtr<nsITimer> timer;
+
+ // Create a new timer only if the delay is greater than zero.
+ // Zero value means that this feature is completely disabled.
+ if (delay > 0) {
+ MOZ_TRY_VAR(timer,
+ NS_NewTimerWithFuncCallback(
+ aFunc, this, delay, aType, aName,
+ mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other)));
+ }
+
+ timer.forget(aTimer);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::RowCountChanged(int32_t aIndex, int32_t aCount) {
+ if (aCount == 0 || !mView) {
+ return NS_OK; // Nothing to do.
+ }
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireRowCountChangedEvent(aIndex, aCount);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ AutoWeakFrame weakFrame(this);
+
+ // Adjust our selection.
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ sel->AdjustSelection(aIndex, aCount);
+ }
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+
+ if (mUpdateBatchNest) return NS_OK;
+
+ mRowCount += aCount;
+#ifdef DEBUG
+ int32_t rowCount = mRowCount;
+ mView->GetRowCount(&rowCount);
+ NS_ASSERTION(
+ rowCount == mRowCount,
+ "row count did not change by the amount suggested, check caller");
+#endif
+
+ int32_t count = Abs(aCount);
+ int32_t last = LastVisibleRow();
+ if (aIndex >= mTopRowIndex && aIndex <= last) InvalidateRange(aIndex, last);
+
+ ScrollParts parts = GetScrollParts();
+
+ if (mTopRowIndex == 0) {
+ // Just update the scrollbar and return.
+ FullScrollbarsUpdate(false);
+ return NS_OK;
+ }
+
+ bool needsInvalidation = false;
+ // Adjust our top row index.
+ if (aCount > 0) {
+ if (mTopRowIndex > aIndex) {
+ // Rows came in above us. Augment the top row index.
+ mTopRowIndex += aCount;
+ }
+ } else if (aCount < 0) {
+ if (mTopRowIndex > aIndex + count - 1) {
+ // No need to invalidate. The remove happened
+ // completely above us (offscreen).
+ mTopRowIndex -= count;
+ } else if (mTopRowIndex >= aIndex) {
+ // This is a full-blown invalidate.
+ if (mTopRowIndex + mPageLength > mRowCount - 1) {
+ mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength);
+ }
+ needsInvalidation = true;
+ }
+ }
+
+ FullScrollbarsUpdate(needsInvalidation);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::BeginUpdateBatch() {
+ ++mUpdateBatchNest;
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::EndUpdateBatch() {
+ NS_ASSERTION(mUpdateBatchNest > 0, "badly nested update batch");
+
+ if (--mUpdateBatchNest != 0) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (!view) {
+ return NS_OK;
+ }
+
+ Invalidate();
+ int32_t countBeforeUpdate = mRowCount;
+ view->GetRowCount(&mRowCount);
+ if (countBeforeUpdate != mRowCount) {
+ if (mTopRowIndex + mPageLength > mRowCount - 1) {
+ mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength);
+ }
+ FullScrollbarsUpdate(false);
+ }
+
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::PrefillPropertyArray(int32_t aRowIndex,
+ nsTreeColumn* aCol) {
+ MOZ_ASSERT(!aCol || aCol->GetFrame(), "invalid column passed");
+ mScratchArray.Clear();
+
+ // focus
+ if (mFocused)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::focus);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::blur);
+
+ // sort
+ bool sorted = false;
+ mView->IsSorted(&sorted);
+ if (sorted) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::sorted);
+
+ // drag session
+ if (mSlots && mSlots->mIsDragging)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dragSession);
+
+ if (aRowIndex != -1) {
+ if (aRowIndex == mMouseOverRow)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::hover);
+
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ if (selection) {
+ // selected
+ bool isSelected;
+ selection->IsSelected(aRowIndex, &isSelected);
+ if (isSelected)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::selected);
+
+ // current
+ int32_t currentIndex;
+ selection->GetCurrentIndex(&currentIndex);
+ if (aRowIndex == currentIndex)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::current);
+ }
+
+ // container or leaf
+ bool isContainer = false;
+ mView->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::container);
+
+ // open or closed
+ bool isOpen = false;
+ mView->IsContainerOpen(aRowIndex, &isOpen);
+ if (isOpen)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::open);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::closed);
+ } else {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::leaf);
+ }
+
+ // drop orientation
+ if (mSlots && mSlots->mDropAllowed && mSlots->mDropRow == aRowIndex) {
+ if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropBefore);
+ else if (mSlots->mDropOrient == nsITreeView::DROP_ON)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropOn);
+ else if (mSlots->mDropOrient == nsITreeView::DROP_AFTER)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropAfter);
+ }
+
+ // odd or even
+ if (aRowIndex % 2)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::odd);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::even);
+
+ XULTreeElement* tree = GetBaseElement();
+ if (tree && tree->HasAttr(kNameSpaceID_None, nsGkAtoms::editing)) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::editing);
+ }
+
+ // multiple columns
+ if (mColumns->GetColumnAt(1))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::multicol);
+ }
+
+ if (aCol) {
+ mScratchArray.AppendElement(aCol->GetAtom());
+
+ if (aCol->IsPrimary())
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::primary);
+
+ if (aCol->GetType() == TreeColumn_Binding::TYPE_CHECKBOX) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checkbox);
+
+ if (aRowIndex != -1) {
+ nsAutoString value;
+ mView->GetCellValue(aRowIndex, aCol, value);
+ if (value.EqualsLiteral("true"))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checked);
+ }
+ }
+
+ // Read special properties from attributes on the column content node
+ if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertbefore,
+ nsGkAtoms::_true, eCaseMatters))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertbefore);
+ if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertafter,
+ nsGkAtoms::_true, eCaseMatters))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertafter);
+ }
+}
+
+nsITheme* nsTreeBodyFrame::GetTwistyRect(int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ nsRect& aImageRect,
+ nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ ComputedStyle* aTwistyContext) {
+ // The twisty rect extends all the way to the end of the cell. This is
+ // incorrect. We need to determine the twisty rect's true width. This is
+ // done by examining the ComputedStyle for a width first. If it has one, we
+ // use that. If it doesn't, we use the image's natural width. If the image
+ // hasn't loaded and if no width is specified, then we just bail. If there is
+ // a -moz-appearance involved, adjust the rect by the minimum widget size
+ // provided by the theme implementation.
+ aImageRect = GetImageSize(aRowIndex, aColumn, true, aTwistyContext);
+ if (aImageRect.height > aTwistyRect.height)
+ aImageRect.height = aTwistyRect.height;
+ if (aImageRect.width > aTwistyRect.width)
+ aImageRect.width = aTwistyRect.width;
+ else
+ aTwistyRect.width = aImageRect.width;
+
+ bool useTheme = false;
+ nsITheme* theme = nullptr;
+ StyleAppearance appearance =
+ aTwistyContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance))
+ useTheme = true;
+ }
+
+ if (useTheme) {
+ LayoutDeviceIntSize minTwistySizePx =
+ theme->GetMinimumWidgetSize(aPresContext, this, appearance);
+
+ // GMWS() returns size in pixels, we need to convert it back to app units
+ nsSize minTwistySize;
+ minTwistySize.width =
+ aPresContext->DevPixelsToAppUnits(minTwistySizePx.width);
+ minTwistySize.height =
+ aPresContext->DevPixelsToAppUnits(minTwistySizePx.height);
+
+ if (aTwistyRect.width < minTwistySize.width) {
+ aTwistyRect.width = minTwistySize.width;
+ }
+ }
+
+ return useTheme ? theme : nullptr;
+}
+
+nsresult nsTreeBodyFrame::GetImage(int32_t aRowIndex, nsTreeColumn* aCol,
+ bool aUseContext,
+ ComputedStyle* aComputedStyle,
+ imgIContainer** aResult) {
+ *aResult = nullptr;
+
+ nsAutoString imageSrc;
+ mView->GetImageSrc(aRowIndex, aCol, imageSrc);
+ RefPtr<imgRequestProxy> styleRequest;
+ if (aUseContext || imageSrc.IsEmpty()) {
+ // Obtain the URL from the ComputedStyle.
+ styleRequest =
+ aComputedStyle->StyleList()->mListStyleImage.GetImageRequest();
+ if (!styleRequest) return NS_OK;
+ nsCOMPtr<nsIURI> uri;
+ styleRequest->GetURI(getter_AddRefs(uri));
+ nsAutoCString spec;
+ nsresult rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(spec, imageSrc);
+ }
+
+ // Look the image up in our cache.
+ nsTreeImageCacheEntry entry;
+ if (mImageCache.Get(imageSrc, &entry)) {
+ // Find out if the image has loaded.
+ uint32_t status;
+ imgIRequest* imgReq = entry.request;
+ imgReq->GetImageStatus(&status);
+ imgReq->GetImage(aResult); // We hand back the image here. The GetImage
+ // call addrefs *aResult.
+ bool animated = true; // Assuming animated is the safe option
+
+ // We can only call GetAnimated if we're decoded
+ if (*aResult && (status & imgIRequest::STATUS_DECODE_COMPLETE))
+ (*aResult)->GetAnimated(&animated);
+
+ if ((!(status & imgIRequest::STATUS_LOAD_COMPLETE)) || animated) {
+ // We either aren't done loading, or we're animating. Add our row as a
+ // listener for invalidations.
+ nsCOMPtr<imgINotificationObserver> obs;
+ imgReq->GetNotificationObserver(getter_AddRefs(obs));
+
+ if (obs) {
+ static_cast<nsTreeImageListener*>(obs.get())->AddCell(aRowIndex, aCol);
+ }
+
+ return NS_OK;
+ }
+ }
+
+ if (!*aResult) {
+ // Create a new nsTreeImageListener object and pass it our row and column
+ // information.
+ nsTreeImageListener* listener = new nsTreeImageListener(this);
+ if (!listener) return NS_ERROR_OUT_OF_MEMORY;
+
+ mCreatedListeners.Insert(listener);
+
+ listener->AddCell(aRowIndex, aCol);
+ nsCOMPtr<imgINotificationObserver> imgNotificationObserver = listener;
+
+ Document* doc = mContent->GetComposedDoc();
+ if (!doc)
+ // The page is currently being torn down. Why bother.
+ return NS_ERROR_FAILURE;
+
+ RefPtr<imgRequestProxy> imageRequest;
+ if (styleRequest) {
+ styleRequest->SyncClone(imgNotificationObserver, doc,
+ getter_AddRefs(imageRequest));
+ } else {
+ nsCOMPtr<nsIURI> srcURI;
+ nsContentUtils::NewURIWithDocumentCharset(
+ getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI());
+ if (!srcURI) return NS_ERROR_FAILURE;
+
+ auto referrerInfo = MakeRefPtr<mozilla::dom::ReferrerInfo>(*doc);
+
+ // XXXbz what's the origin principal for this stuff that comes from our
+ // view? I guess we should assume that it's the node's principal...
+ nsresult rv = nsContentUtils::LoadImage(
+ srcURI, mContent, doc, mContent->NodePrincipal(), 0, referrerInfo,
+ imgNotificationObserver, nsIRequest::LOAD_NORMAL, u""_ns,
+ getter_AddRefs(imageRequest));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // NOTE(heycam): If it's an SVG image, and we need to want the image to
+ // able to respond to media query changes, it needs to be added to the
+ // document's ImageTracker. For now, assume we don't need this.
+ }
+ listener->UnsuppressInvalidation();
+
+ if (!imageRequest) return NS_ERROR_FAILURE;
+
+ // We don't want discarding/decode-on-draw for xul images
+ imageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY);
+ imageRequest->LockImage();
+
+ // In a case it was already cached.
+ imageRequest->GetImage(aResult);
+ nsTreeImageCacheEntry cacheEntry(imageRequest, imgNotificationObserver);
+ mImageCache.InsertOrUpdate(imageSrc, cacheEntry);
+ }
+ return NS_OK;
+}
+
+nsRect nsTreeBodyFrame::GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol,
+ bool aUseContext,
+ ComputedStyle* aComputedStyle) {
+ // XXX We should respond to visibility rules for collapsed vs. hidden.
+
+ // This method returns the width of the twisty INCLUDING borders and padding.
+ // It first checks the ComputedStyle for a width. If none is found, it tries
+ // to use the default image width for the twisty. If no image is found, it
+ // defaults to border+padding.
+ nsRect r(0, 0, 0, 0);
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(aComputedStyle, bp);
+ r.Inflate(bp);
+
+ // Now r contains our border+padding info. We now need to get our width and
+ // height.
+ bool needWidth = false;
+ bool needHeight = false;
+
+ // We have to load image even though we already have a size.
+ // Don't change this, otherwise things start to go awry.
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, getter_AddRefs(image));
+
+ const nsStylePosition* myPosition = aComputedStyle->StylePosition();
+ if (myPosition->mWidth.ConvertsToLength()) {
+ int32_t val = myPosition->mWidth.ToLength();
+ r.width += val;
+ } else {
+ needWidth = true;
+ }
+
+ if (myPosition->mHeight.ConvertsToLength()) {
+ int32_t val = myPosition->mHeight.ToLength();
+ r.height += val;
+ } else {
+ needHeight = true;
+ }
+
+ if (image) {
+ if (needWidth || needHeight) {
+ // Get the natural image size.
+
+ if (needWidth) {
+ // Get the size from the image.
+ nscoord width;
+ image->GetWidth(&width);
+ r.width += nsPresContext::CSSPixelsToAppUnits(width);
+ }
+
+ if (needHeight) {
+ nscoord height;
+ image->GetHeight(&height);
+ r.height += nsPresContext::CSSPixelsToAppUnits(height);
+ }
+ }
+ }
+
+ return r;
+}
+
+// GetImageDestSize returns the destination size of the image.
+// The width and height do not include borders and padding.
+// The width and height have not been adjusted to fit in the row height
+// or cell width.
+// The width and height reflect the destination size specified in CSS,
+// or the image region specified in CSS, or the natural size of the
+// image.
+// If only the destination width has been specified in CSS, the height is
+// calculated to maintain the aspect ratio of the image.
+// If only the destination height has been specified in CSS, the width is
+// calculated to maintain the aspect ratio of the image.
+nsSize nsTreeBodyFrame::GetImageDestSize(ComputedStyle* aComputedStyle,
+ imgIContainer* image) {
+ nsSize size(0, 0);
+
+ // We need to get the width and height.
+ bool needWidth = false;
+ bool needHeight = false;
+
+ // Get the style position to see if the CSS has specified the
+ // destination width/height.
+ const nsStylePosition* myPosition = aComputedStyle->StylePosition();
+
+ if (myPosition->mWidth.ConvertsToLength()) {
+ // CSS has specified the destination width.
+ size.width = myPosition->mWidth.ToLength();
+ } else {
+ // We'll need to get the width of the image/region.
+ needWidth = true;
+ }
+
+ if (myPosition->mHeight.ConvertsToLength()) {
+ // CSS has specified the destination height.
+ size.height = myPosition->mHeight.ToLength();
+ } else {
+ // We'll need to get the height of the image/region.
+ needHeight = true;
+ }
+
+ if (needWidth || needHeight) {
+ // We need to get the size of the image/region.
+ nsSize imageSize(0, 0);
+ if (image) {
+ nscoord width;
+ image->GetWidth(&width);
+ imageSize.width = nsPresContext::CSSPixelsToAppUnits(width);
+ nscoord height;
+ image->GetHeight(&height);
+ imageSize.height = nsPresContext::CSSPixelsToAppUnits(height);
+ }
+
+ if (needWidth) {
+ if (!needHeight && imageSize.height != 0) {
+ // The CSS specified the destination height, but not the destination
+ // width. We need to calculate the width so that we maintain the
+ // image's aspect ratio.
+ size.width = imageSize.width * size.height / imageSize.height;
+ } else {
+ size.width = imageSize.width;
+ }
+ }
+
+ if (needHeight) {
+ if (!needWidth && imageSize.width != 0) {
+ // The CSS specified the destination width, but not the destination
+ // height. We need to calculate the height so that we maintain the
+ // image's aspect ratio.
+ size.height = imageSize.height * size.width / imageSize.width;
+ } else {
+ size.height = imageSize.height;
+ }
+ }
+ }
+
+ return size;
+}
+
+// GetImageSourceRect returns the source rectangle of the image to be
+// displayed.
+// The width and height reflect the image region specified in CSS, or
+// the natural size of the image.
+// The width and height do not include borders and padding.
+// The width and height do not reflect the destination size specified
+// in CSS.
+nsRect nsTreeBodyFrame::GetImageSourceRect(ComputedStyle* aComputedStyle,
+ imgIContainer* image) {
+ if (!image) {
+ return nsRect();
+ }
+
+ nsRect r;
+ // Use the actual image size.
+ nscoord coord;
+ if (NS_SUCCEEDED(image->GetWidth(&coord))) {
+ r.width = nsPresContext::CSSPixelsToAppUnits(coord);
+ }
+ if (NS_SUCCEEDED(image->GetHeight(&coord))) {
+ r.height = nsPresContext::CSSPixelsToAppUnits(coord);
+ }
+ return r;
+}
+
+int32_t nsTreeBodyFrame::GetRowHeight() {
+ // Look up the correct height. It is equal to the specified height
+ // + the specified margins.
+ mScratchArray.Clear();
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+ if (rowContext) {
+ const nsStylePosition* myPosition = rowContext->StylePosition();
+
+ nscoord minHeight = 0;
+ if (myPosition->mMinHeight.ConvertsToLength()) {
+ minHeight = myPosition->mMinHeight.ToLength();
+ }
+
+ nscoord height = 0;
+ if (myPosition->mHeight.ConvertsToLength()) {
+ height = myPosition->mHeight.ToLength();
+ }
+
+ if (height < minHeight) height = minHeight;
+
+ if (height > 0) {
+ height = nsPresContext::AppUnitsToIntCSSPixels(height);
+ height += height % 2;
+ height = nsPresContext::CSSPixelsToAppUnits(height);
+
+ // XXX Check box-sizing to determine if border/padding should augment the
+ // height Inflate the height by our margins.
+ nsRect rowRect(0, 0, 0, height);
+ nsMargin rowMargin;
+ rowContext->StyleMargin()->GetMargin(rowMargin);
+ rowRect.Inflate(rowMargin);
+ height = rowRect.height;
+ return height;
+ }
+ }
+
+ return nsPresContext::CSSPixelsToAppUnits(18); // As good a default as any.
+}
+
+int32_t nsTreeBodyFrame::GetIndentation() {
+ // Look up the correct indentation. It is equal to the specified indentation
+ // width.
+ mScratchArray.Clear();
+ ComputedStyle* indentContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeIndentation());
+ if (indentContext) {
+ const nsStylePosition* myPosition = indentContext->StylePosition();
+ if (myPosition->mWidth.ConvertsToLength()) {
+ return myPosition->mWidth.ToLength();
+ }
+ }
+
+ return nsPresContext::CSSPixelsToAppUnits(16); // As good a default as any.
+}
+
+void nsTreeBodyFrame::CalcInnerBox() {
+ mInnerBox.SetRect(0, 0, mRect.width, mRect.height);
+ AdjustForBorderPadding(mComputedStyle, mInnerBox);
+}
+
+nscoord nsTreeBodyFrame::CalcHorzWidth(const ScrollParts& aParts) {
+ // Compute the adjustment to the last column. This varies depending on the
+ // visibility of the columnpicker and the scrollbar.
+ if (aParts.mColumnsFrame)
+ mAdjustWidth = mRect.width - aParts.mColumnsFrame->GetRect().width;
+ else
+ mAdjustWidth = 0;
+
+ nscoord width = 0;
+
+ // We calculate this from the scrollable frame, so that it
+ // properly covers all contingencies of what could be
+ // scrollable (columns, body, etc...)
+
+ if (aParts.mColumnsScrollFrame) {
+ width = aParts.mColumnsScrollFrame->GetScrollRange().width +
+ aParts.mColumnsScrollFrame->GetScrollPortRect().width;
+ }
+
+ // If no horz scrolling periphery is present, then just return our width
+ if (width == 0) width = mRect.width;
+
+ return width;
+}
+
+Maybe<nsIFrame::Cursor> nsTreeBodyFrame::GetCursor(const nsPoint& aPoint) {
+ // Check the GetScriptHandlingObject so we don't end up running code when
+ // the document is a zombie.
+ bool dummy;
+ if (mView && GetContent()->GetComposedDoc()->GetScriptHandlingObject(dummy)) {
+ int32_t row;
+ nsTreeColumn* col;
+ nsCSSAnonBoxPseudoStaticAtom* child;
+ GetCellAt(aPoint.x, aPoint.y, &row, &col, &child);
+
+ if (child) {
+ // Our scratch array is already prefilled.
+ RefPtr<ComputedStyle> childContext = GetPseudoComputedStyle(child);
+ StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword;
+ if (kind == StyleCursorKind::Auto) {
+ kind = StyleCursorKind::Default;
+ }
+ return Some(
+ Cursor{kind, AllowCustomCursorImage::Yes, std::move(childContext)});
+ }
+ }
+ return SimpleXULLeafFrame::GetCursor(aPoint);
+}
+
+static uint32_t GetDropEffect(WidgetGUIEvent* aEvent) {
+ NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type");
+ WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
+ nsContentUtils::SetDataTransferInEvent(dragEvent);
+
+ uint32_t action = 0;
+ if (dragEvent->mDataTransfer) {
+ action = dragEvent->mDataTransfer->DropEffectInt();
+ }
+ return action;
+}
+
+nsresult nsTreeBodyFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ if (aEvent->mMessage == eMouseOver || aEvent->mMessage == eMouseMove) {
+ nsPoint pt =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this});
+ int32_t xTwips = pt.x - mInnerBox.x;
+ int32_t yTwips = pt.y - mInnerBox.y;
+ int32_t newrow = GetRowAtInternal(xTwips, yTwips);
+ if (mMouseOverRow != newrow) {
+ // redraw the old and the new row
+ if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow);
+ mMouseOverRow = newrow;
+ if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow);
+ }
+ } else if (aEvent->mMessage == eMouseOut) {
+ if (mMouseOverRow != -1) {
+ InvalidateRow(mMouseOverRow);
+ mMouseOverRow = -1;
+ }
+ } else if (aEvent->mMessage == eDragEnter) {
+ if (!mSlots) {
+ mSlots = MakeUnique<Slots>();
+ }
+
+ // Cache several things we'll need throughout the course of our work. These
+ // will all get released on a drag exit.
+
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ // Cache the drag session.
+ mSlots->mIsDragging = true;
+ mSlots->mDropRow = -1;
+ mSlots->mDropOrient = -1;
+ mSlots->mDragAction = GetDropEffect(aEvent);
+ } else if (aEvent->mMessage == eDragOver) {
+ // The mouse is hovering over this tree. If we determine things are
+ // different from the last time, invalidate the drop feedback at the old
+ // position, query the view to see if the current location is droppable,
+ // and then invalidate the drop feedback at the new location if it is.
+ // The mouse may or may not have changed position from the last time
+ // we were called, so optimize out a lot of the extra notifications by
+ // checking if anything changed first. For drop feedback we use drop,
+ // dropBefore and dropAfter property.
+ if (!mView || !mSlots) {
+ return NS_OK;
+ }
+
+ // Save last values, we will need them.
+ int32_t lastDropRow = mSlots->mDropRow;
+ int16_t lastDropOrient = mSlots->mDropOrient;
+#ifndef XP_MACOSX
+ int16_t lastScrollLines = mSlots->mScrollLines;
+#endif
+
+ // Find out the current drag action
+ uint32_t lastDragAction = mSlots->mDragAction;
+ mSlots->mDragAction = GetDropEffect(aEvent);
+
+ // Compute the row mouse is over and the above/below/on state.
+ // Below we'll use this to see if anything changed.
+ // Also check if we want to auto-scroll.
+ ComputeDropPosition(aEvent, &mSlots->mDropRow, &mSlots->mDropOrient,
+ &mSlots->mScrollLines);
+
+ // While we're here, handle tracking of scrolling during a drag.
+ if (mSlots->mScrollLines) {
+ if (mSlots->mDropAllowed) {
+ // Invalidate primary cell at old location.
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(lastDropRow, lastDropOrient);
+ }
+#ifdef XP_MACOSX
+ ScrollByLines(mSlots->mScrollLines);
+#else
+ if (!lastScrollLines) {
+ // Cancel any previously initialized timer.
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ // Set a timer to trigger the tree scrolling.
+ CreateTimer(LookAndFeel::IntID::TreeLazyScrollDelay, LazyScrollCallback,
+ nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::LazyScrollCallback");
+ }
+#endif
+ // Bail out to prevent spring loaded timer and feedback line settings.
+ return NS_OK;
+ }
+
+ // If changed from last time, invalidate primary cell at the old location
+ // and if allowed, invalidate primary cell at the new location. If nothing
+ // changed, just bail.
+ if (mSlots->mDropRow != lastDropRow ||
+ mSlots->mDropOrient != lastDropOrient ||
+ mSlots->mDragAction != lastDragAction) {
+ // Invalidate row at the old location.
+ if (mSlots->mDropAllowed) {
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(lastDropRow, lastDropOrient);
+ }
+
+ if (mSlots->mTimer) {
+ // Timer is active but for a different row than the current one, kill
+ // it.
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ if (mSlots->mDropRow >= 0) {
+ if (!mSlots->mTimer && mSlots->mDropOrient == nsITreeView::DROP_ON) {
+ // Either there wasn't a timer running or it was just killed above.
+ // If over a folder, start up a timer to open the folder.
+ bool isContainer = false;
+ mView->IsContainer(mSlots->mDropRow, &isContainer);
+ if (isContainer) {
+ bool isOpen = false;
+ mView->IsContainerOpen(mSlots->mDropRow, &isOpen);
+ if (!isOpen) {
+ // This node isn't expanded, set a timer to expand it.
+ CreateTimer(LookAndFeel::IntID::TreeOpenDelay, OpenCallback,
+ nsITimer::TYPE_ONE_SHOT,
+ getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::OpenCallback");
+ }
+ }
+ }
+
+ // The dataTransfer was initialized by the call to GetDropEffect above.
+ bool canDropAtNewLocation = false;
+ mView->CanDrop(mSlots->mDropRow, mSlots->mDropOrient,
+ aEvent->AsDragEvent()->mDataTransfer,
+ &canDropAtNewLocation);
+
+ if (canDropAtNewLocation) {
+ // Invalidate row at the new location.
+ mSlots->mDropAllowed = canDropAtNewLocation;
+ InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient);
+ }
+ }
+ }
+
+ // Indicate that the drop is allowed by preventing the default behaviour.
+ if (mSlots->mDropAllowed) *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ } else if (aEvent->mMessage == eDrop) {
+ // this event was meant for another frame, so ignore it
+ if (!mSlots) return NS_OK;
+
+ // Tell the view where the drop happened.
+
+ // Remove the drop folder and all its parents from the array.
+ int32_t parentIndex;
+ nsresult rv = mView->GetParentIndex(mSlots->mDropRow, &parentIndex);
+ while (NS_SUCCEEDED(rv) && parentIndex >= 0) {
+ mSlots->mArray.RemoveElement(parentIndex);
+ rv = mView->GetParentIndex(parentIndex, &parentIndex);
+ }
+
+ NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type");
+ WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
+ nsContentUtils::SetDataTransferInEvent(dragEvent);
+
+ mView->Drop(mSlots->mDropRow, mSlots->mDropOrient,
+ dragEvent->mDataTransfer);
+ mSlots->mDropRow = -1;
+ mSlots->mDropOrient = -1;
+ mSlots->mIsDragging = false;
+ *aEventStatus =
+ nsEventStatus_eConsumeNoDefault; // already handled the drop
+ } else if (aEvent->mMessage == eDragExit) {
+ // this event was meant for another frame, so ignore it
+ if (!mSlots) return NS_OK;
+
+ // Clear out all our tracking vars.
+
+ if (mSlots->mDropAllowed) {
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient);
+ } else
+ mSlots->mDropAllowed = false;
+ mSlots->mIsDragging = false;
+ mSlots->mScrollLines = 0;
+ // If a drop is occuring, the exit event will fire just before the drop
+ // event, so don't reset mDropRow or mDropOrient as these fields are used
+ // by the drop event.
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ if (!mSlots->mArray.IsEmpty()) {
+ // Close all spring loaded folders except the drop folder.
+ CreateTimer(LookAndFeel::IntID::TreeCloseDelay, CloseCallback,
+ nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::CloseCallback");
+ }
+ }
+
+ return NS_OK;
+}
+
+namespace mozilla {
+
+class nsDisplayTreeBody final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayTreeBody(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayTreeBody);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTreeBody)
+
+ nsDisplayItemGeometry* AllocateGeometry(
+ nsDisplayListBuilder* aBuilder) override {
+ return new nsDisplayTreeBodyGeometry(this, aBuilder, IsWindowActive());
+ }
+
+ void Destroy(nsDisplayListBuilder* aBuilder) override {
+ aBuilder->UnregisterThemeGeometry(this);
+ nsPaintedDisplayItem::Destroy(aBuilder);
+ }
+
+ bool IsWindowActive() const {
+ DocumentState docState =
+ mFrame->PresContext()->Document()->GetDocumentState();
+ return !docState.HasState(DocumentState::WINDOW_INACTIVE);
+ }
+
+ void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder,
+ const nsDisplayItemGeometry* aGeometry,
+ nsRegion* aInvalidRegion) const override {
+ auto geometry = static_cast<const nsDisplayTreeBodyGeometry*>(aGeometry);
+
+ if (IsWindowActive() != geometry->mWindowIsActive) {
+ bool snap;
+ aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap));
+ }
+
+ nsPaintedDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry,
+ aInvalidRegion);
+ }
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override {
+ MOZ_ASSERT(aBuilder);
+ Unused << static_cast<nsTreeBodyFrame*>(mFrame)->PaintTreeBody(
+ *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), aBuilder);
+ }
+
+ NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY)
+
+ nsRect GetComponentAlphaBounds(
+ nsDisplayListBuilder* aBuilder) const override {
+ bool snap;
+ return GetBounds(aBuilder, &snap);
+ }
+};
+
+} // namespace mozilla
+
+#ifdef XP_MACOSX
+static bool IsInSourceList(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame; frame;
+ frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) {
+ if (frame->StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::MozMacSourceList) {
+ return true;
+ }
+ }
+ return false;
+}
+#endif
+
+// Painting routines
+void nsTreeBodyFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // REVIEW: why did we paint if we were collapsed? that makes no sense!
+ if (!IsVisibleForPainting()) return; // We're invisible. Don't paint.
+
+ // Handles painting our background, border, and outline.
+ SimpleXULLeafFrame::BuildDisplayList(aBuilder, aLists);
+
+ // Bail out now if there's no view or we can't run script because the
+ // document is a zombie
+ if (!mView || !GetContent()->GetComposedDoc()->GetWindow()) return;
+
+ nsDisplayItem* item = MakeDisplayItem<nsDisplayTreeBody>(aBuilder, this);
+ aLists.Content()->AppendToTop(item);
+
+#ifdef XP_MACOSX
+ XULTreeElement* tree = GetBaseElement();
+ nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ nsITheme* theme = PresContext()->Theme();
+ // On Mac, we support native theming of selected rows. On 10.10 and higher,
+ // this means applying vibrancy which require us to register the theme
+ // geometrics for the row. In order to make the vibrancy effect to work
+ // properly, we also need an ancestor frame to be themed as a source list.
+ if (selection && theme && IsInSourceList(treeFrame)) {
+ // Loop through our onscreen rows. If the row is selected and a
+ // -moz-appearance is provided, RegisterThemeGeometry might be necessary.
+ const auto end = std::min(mRowCount, LastVisibleRow() + 1);
+ for (auto i = FirstVisibleRow(); i < end; i++) {
+ bool isSelected;
+ selection->IsSelected(i, &isSelected);
+ if (isSelected) {
+ PrefillPropertyArray(i, nullptr);
+ nsAutoString properties;
+ view->GetRowProperties(i, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+ auto appearance = rowContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ if (theme->ThemeSupportsWidget(PresContext(), this, appearance)) {
+ nsITheme::ThemeGeometryType type =
+ theme->ThemeGeometryTypeForWidget(this, appearance);
+ if (type != nsITheme::eThemeGeometryTypeUnknown) {
+ nsRect rowRect(mInnerBox.x,
+ mInnerBox.y + mRowHeight * (i - FirstVisibleRow()),
+ mInnerBox.width, mRowHeight);
+ aBuilder->RegisterThemeGeometry(
+ type, item,
+ LayoutDeviceIntRect::FromUnknownRect(
+ (rowRect + aBuilder->ToReferenceFrame(this))
+ .ToNearestPixels(
+ PresContext()->AppUnitsPerDevPixel())));
+ }
+ }
+ }
+ }
+ }
+ }
+#endif
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintTreeBody(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect,
+ nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ // Update our available height and our page count.
+ CalcInnerBox();
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ aRenderingContext.Save();
+ aRenderingContext.Clip(NSRectToSnappedRect(
+ mInnerBox + aPt, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
+ int32_t oldPageCount = mPageLength;
+ if (!mHasFixedRowCount) {
+ mPageLength =
+ (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount;
+ }
+
+ if (oldPageCount != mPageLength ||
+ mHorzWidth != CalcHorzWidth(GetScrollParts())) {
+ // Schedule a ResizeReflow that will update our info properly.
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+ }
+#ifdef DEBUG
+ int32_t rowCount = mRowCount;
+ mView->GetRowCount(&rowCount);
+ NS_WARNING_ASSERTION(mRowCount == rowCount, "row count changed unexpectedly");
+#endif
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Loop through our columns and paint them (e.g., for sorting). This is only
+ // relevant when painting backgrounds, since columns contain no content.
+ // Content is contained in the rows.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect colRect;
+ nsresult rv =
+ currCol->GetRect(this, mInnerBox.y, mInnerBox.height, &colRect);
+ // Don't paint hidden columns.
+ if (NS_FAILED(rv) || colRect.width == 0) continue;
+
+ if (OffsetForHorzScroll(colRect, false)) {
+ nsRect dirtyRect;
+ colRect += aPt;
+ if (dirtyRect.IntersectRect(aDirtyRect, colRect)) {
+ result &= PaintColumn(currCol, colRect, PresContext(),
+ aRenderingContext, aDirtyRect);
+ }
+ }
+ }
+ // Loop through our on-screen rows.
+ for (int32_t i = mTopRowIndex;
+ i < mRowCount && i <= mTopRowIndex + mPageLength; i++) {
+ nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * (i - mTopRowIndex),
+ mInnerBox.width, mRowHeight);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, rowRect + aPt) &&
+ rowRect.y < (mInnerBox.y + mInnerBox.height)) {
+ result &= PaintRow(i, rowRect + aPt, PresContext(), aRenderingContext,
+ aDirtyRect, aPt, aBuilder);
+ }
+ }
+
+ if (mSlots && mSlots->mDropAllowed &&
+ (mSlots->mDropOrient == nsITreeView::DROP_BEFORE ||
+ mSlots->mDropOrient == nsITreeView::DROP_AFTER)) {
+ nscoord yPos = mInnerBox.y +
+ mRowHeight * (mSlots->mDropRow - mTopRowIndex) -
+ mRowHeight / 2;
+ nsRect feedbackRect(mInnerBox.x, yPos, mInnerBox.width, mRowHeight);
+ if (mSlots->mDropOrient == nsITreeView::DROP_AFTER)
+ feedbackRect.y += mRowHeight;
+
+ nsRect dirtyRect;
+ feedbackRect += aPt;
+ if (dirtyRect.IntersectRect(aDirtyRect, feedbackRect)) {
+ result &= PaintDropFeedback(feedbackRect, PresContext(),
+ aRenderingContext, aDirtyRect, aPt);
+ }
+ }
+ aRenderingContext.Restore();
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintColumn(nsTreeColumn* aColumn,
+ const nsRect& aColumnRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Now obtain the properties for our cell.
+ PrefillPropertyArray(-1, aColumn);
+ nsAutoString properties;
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetColumnProperties(aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the column. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* colContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeColumn());
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect colRect(aColumnRect);
+ nsMargin colMargin;
+ colContext->StyleMargin()->GetMargin(colMargin);
+ colRect.Deflate(colMargin);
+
+ return PaintBackgroundLayer(colContext, aPresContext, aRenderingContext,
+ colRect, aDirtyRect);
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintRow(int32_t aRowIndex,
+ const nsRect& aRowRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ // We have been given a rect for our row. We treat this row like a full-blown
+ // frame, meaning that it can have borders, margins, padding, and a
+ // background.
+
+ // Without a view, we have no data. Check for this up front.
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (!view) {
+ return ImgDrawResult::SUCCESS;
+ }
+
+ nsresult rv;
+
+ // Now obtain the properties for our row.
+ // XXX Automatically fill in the following props: open, closed, container,
+ // leaf, selected, focused
+ PrefillPropertyArray(aRowIndex, nullptr);
+
+ nsAutoString properties;
+ view->GetRowProperties(aRowIndex, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the row. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+
+ // Obtain the margins for the row and then deflate our rect by that
+ // amount. The row is assumed to be contained within the deflated rect.
+ nsRect rowRect(aRowRect);
+ nsMargin rowMargin;
+ rowContext->StyleMargin()->GetMargin(rowMargin);
+ rowRect.Deflate(rowMargin);
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Paint our borders and background for our row rect.
+ nsITheme* theme = nullptr;
+ auto appearance = rowContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ }
+
+ if (theme && theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) {
+ nsRect dirty;
+ dirty.IntersectRect(rowRect, aDirtyRect);
+ theme->DrawWidgetBackground(&aRenderingContext, this, appearance, rowRect,
+ dirty);
+ } else {
+ result &= PaintBackgroundLayer(rowContext, aPresContext, aRenderingContext,
+ rowRect, aDirtyRect);
+ }
+
+ // Adjust the rect for its border and padding.
+ nsRect originalRowRect = rowRect;
+ AdjustForBorderPadding(rowContext, rowRect);
+
+ bool isSeparator = false;
+ view->IsSeparator(aRowIndex, &isSeparator);
+ if (isSeparator) {
+ // The row is a separator.
+
+ nscoord primaryX = rowRect.x;
+ nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn();
+ if (primaryCol) {
+ // Paint the primary cell.
+ nsRect cellRect;
+ rv = primaryCol->GetRect(this, rowRect.y, rowRect.height, &cellRect);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("primary column is invalid");
+ return result;
+ }
+
+ if (OffsetForHorzScroll(cellRect, false)) {
+ cellRect.x += aPt.x;
+ nsRect dirtyRect;
+ nsRect checkRect(cellRect.x, originalRowRect.y, cellRect.width,
+ originalRowRect.height);
+ if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) {
+ result &=
+ PaintCell(aRowIndex, primaryCol, cellRect, aPresContext,
+ aRenderingContext, aDirtyRect, primaryX, aPt, aBuilder);
+ }
+ }
+
+ // Paint the left side of the separator.
+ nscoord currX;
+ nsTreeColumn* previousCol = primaryCol->GetPrevious();
+ if (previousCol) {
+ nsRect prevColRect;
+ rv = previousCol->GetRect(this, 0, 0, &prevColRect);
+ if (NS_SUCCEEDED(rv)) {
+ currX = (prevColRect.x - mHorzPosition) + prevColRect.width + aPt.x;
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "The column before the primary column is "
+ "invalid");
+ currX = rowRect.x;
+ }
+ } else {
+ currX = rowRect.x;
+ }
+
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+ if (level == 0) currX += mIndentation;
+
+ if (currX > rowRect.x) {
+ nsRect separatorRect(rowRect);
+ separatorRect.width -= rowRect.x + rowRect.width - currX;
+ result &= PaintSeparator(aRowIndex, separatorRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ }
+ }
+
+ // Paint the right side (whole) separator.
+ nsRect separatorRect(rowRect);
+ if (primaryX > rowRect.x) {
+ separatorRect.width -= primaryX - rowRect.x;
+ separatorRect.x += primaryX - rowRect.x;
+ }
+ result &= PaintSeparator(aRowIndex, separatorRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ } else {
+ // Now loop over our cells. Only paint a cell if it intersects with our
+ // dirty rect.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect cellRect;
+ rv = currCol->GetRect(this, rowRect.y, rowRect.height, &cellRect);
+ // Don't paint cells in hidden columns.
+ if (NS_FAILED(rv) || cellRect.width == 0) continue;
+
+ if (OffsetForHorzScroll(cellRect, false)) {
+ cellRect.x += aPt.x;
+
+ // for primary columns, use the row's vertical size so that the
+ // lines get drawn properly
+ nsRect checkRect = cellRect;
+ if (currCol->IsPrimary())
+ checkRect = nsRect(cellRect.x, originalRowRect.y, cellRect.width,
+ originalRowRect.height);
+
+ nsRect dirtyRect;
+ nscoord dummy;
+ if (dirtyRect.IntersectRect(aDirtyRect, checkRect))
+ result &=
+ PaintCell(aRowIndex, currCol, cellRect, aPresContext,
+ aRenderingContext, aDirtyRect, dummy, aPt, aBuilder);
+ }
+ }
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintSeparator(int32_t aRowIndex,
+ const nsRect& aSeparatorRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ // Resolve style for the separator.
+ ComputedStyle* separatorContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeSeparator());
+ bool useTheme = false;
+ nsITheme* theme = nullptr;
+ StyleAppearance appearance =
+ separatorContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance))
+ useTheme = true;
+ }
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // use -moz-appearance if provided.
+ if (useTheme) {
+ nsRect dirty;
+ dirty.IntersectRect(aSeparatorRect, aDirtyRect);
+ theme->DrawWidgetBackground(&aRenderingContext, this, appearance,
+ aSeparatorRect, dirty);
+ } else {
+ const nsStylePosition* stylePosition = separatorContext->StylePosition();
+
+ // Obtain the height for the separator or use the default value.
+ nscoord height;
+ if (stylePosition->mHeight.ConvertsToLength()) {
+ height = stylePosition->mHeight.ToLength();
+ } else {
+ // Use default height 2px.
+ height = nsPresContext::CSSPixelsToAppUnits(2);
+ }
+
+ // Obtain the margins for the separator and then deflate our rect by that
+ // amount. The separator is assumed to be contained within the deflated
+ // rect.
+ nsRect separatorRect(aSeparatorRect.x, aSeparatorRect.y,
+ aSeparatorRect.width, height);
+ nsMargin separatorMargin;
+ separatorContext->StyleMargin()->GetMargin(separatorMargin);
+ separatorRect.Deflate(separatorMargin);
+
+ // Center the separator.
+ separatorRect.y += (aSeparatorRect.height - height) / 2;
+
+ result &=
+ PaintBackgroundLayer(separatorContext, aPresContext, aRenderingContext,
+ separatorRect, aDirtyRect);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintCell(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCellRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Now obtain the properties for our cell.
+ // XXX Automatically fill in the following props: open, closed, container,
+ // leaf, selected, focused, and the col ID.
+ PrefillPropertyArray(aRowIndex, aColumn);
+ nsAutoString properties;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellProperties(aRowIndex, aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the cell. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect cellRect(aCellRect);
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ cellRect.Deflate(cellMargin);
+
+ // Paint our borders and background for our row rect.
+ ImgDrawResult result = PaintBackgroundLayer(
+ cellContext, aPresContext, aRenderingContext, cellRect, aDirtyRect);
+
+ // Adjust the rect for its border and padding.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ nscoord currX = cellRect.x;
+ nscoord remainingWidth = cellRect.width;
+
+ // Now we paint the contents of the cells.
+ // Directionality of the tree determines the order in which we paint.
+ // StyleDirection::Ltr means paint from left to right.
+ // StyleDirection::Rtl means paint from right to left.
+
+ if (aColumn->IsPrimary()) {
+ // If we're the primary column, we need to indent and paint the twisty and
+ // any connecting lines between siblings.
+
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+
+ if (!isRTL) currX += mIndentation * level;
+ remainingWidth -= mIndentation * level;
+
+ // Resolve the style to use for the connecting lines.
+ ComputedStyle* lineContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeLine());
+
+ if (mIndentation && level &&
+ lineContext->StyleVisibility()->IsVisibleOrCollapsed()) {
+ // Paint the thread lines.
+
+ // Get the size of the twisty. We don't want to paint the twisty
+ // before painting of connecting lines since it would paint lines over
+ // the twisty. But we need to leave a place for it.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ nsRect twistyRect(aCellRect);
+ GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext,
+ twistyContext);
+
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ const nsStyleBorder* borderStyle = lineContext->StyleBorder();
+ // Resolve currentcolor values against the treeline context
+ nscolor color = borderStyle->mBorderLeftColor.CalcColor(*lineContext);
+ ColorPattern colorPatt(ToDeviceColor(color));
+
+ StyleBorderStyle style = borderStyle->GetBorderStyle(eSideLeft);
+ StrokeOptions strokeOptions;
+ nsLayoutUtils::InitDashPattern(strokeOptions, style);
+
+ nscoord srcX = currX + twistyRect.width - mIndentation / 2;
+ nscoord lineY = (aRowIndex - mTopRowIndex) * mRowHeight + aPt.y;
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+ nsPresContext* pc = PresContext();
+
+ // Don't paint off our cell.
+ if (srcX <= cellRect.x + cellRect.width) {
+ nscoord destX = currX + twistyRect.width;
+ if (destX > cellRect.x + cellRect.width)
+ destX = cellRect.x + cellRect.width;
+ if (isRTL) {
+ srcX = currX + remainingWidth - (srcX - cellRect.x);
+ destX = currX + remainingWidth - (destX - cellRect.x);
+ }
+ Point p1(pc->AppUnitsToGfxUnits(srcX),
+ pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2));
+ Point p2(pc->AppUnitsToGfxUnits(destX),
+ pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2));
+ SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget,
+ strokeOptions.mLineWidth);
+ drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions);
+ }
+
+ int32_t currentParent = aRowIndex;
+ for (int32_t i = level; i > 0; i--) {
+ if (srcX <= cellRect.x + cellRect.width) {
+ // Paint full vertical line only if we have next sibling.
+ bool hasNextSibling;
+ view->HasNextSibling(currentParent, aRowIndex, &hasNextSibling);
+ if (hasNextSibling || i == level) {
+ Point p1(pc->AppUnitsToGfxUnits(srcX),
+ pc->AppUnitsToGfxUnits(lineY));
+ Point p2;
+ p2.x = pc->AppUnitsToGfxUnits(srcX);
+
+ if (hasNextSibling)
+ p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight);
+ else if (i == level)
+ p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2);
+
+ SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget,
+ strokeOptions.mLineWidth);
+ drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions);
+ }
+ }
+
+ int32_t parent;
+ if (NS_FAILED(view->GetParentIndex(currentParent, &parent)) ||
+ parent < 0)
+ break;
+ currentParent = parent;
+ srcX -= mIndentation;
+ }
+ }
+
+ // Always leave space for the twisty.
+ nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ result &= PaintTwisty(aRowIndex, aColumn, twistyRect, aPresContext,
+ aRenderingContext, aDirtyRect, remainingWidth, currX);
+ }
+
+ // Now paint the icon for our cell.
+ nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, iconRect)) {
+ result &= PaintImage(aRowIndex, aColumn, iconRect, aPresContext,
+ aRenderingContext, aDirtyRect, remainingWidth, currX,
+ aBuilder);
+ }
+
+ // Now paint our element, but only if we aren't a cycler column.
+ // XXX until we have the ability to load images, allow the view to
+ // insert text into cycler columns...
+ if (!aColumn->IsCycler()) {
+ nsRect elementRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, elementRect)) {
+ switch (aColumn->GetType()) {
+ case TreeColumn_Binding::TYPE_TEXT:
+ result &= PaintText(aRowIndex, aColumn, elementRect, aPresContext,
+ aRenderingContext, aDirtyRect, currX);
+ break;
+ case TreeColumn_Binding::TYPE_CHECKBOX:
+ result &= PaintCheckbox(aRowIndex, aColumn, elementRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ break;
+ }
+ }
+ }
+
+ aCurrX = currX;
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintTwisty(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTwistyRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord rightEdge = aCurrX + aRemainingWidth;
+ // Paint the twisty, but only if we are a non-empty container.
+ bool shouldPaint = false;
+ bool isContainer = false;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ bool isContainerEmpty = false;
+ view->IsContainerEmpty(aRowIndex, &isContainerEmpty);
+ if (!isContainerEmpty) shouldPaint = true;
+ }
+
+ // Resolve style for the twisty.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ // Obtain the margins for the twisty and then deflate our rect by that
+ // amount. The twisty is assumed to be contained within the deflated rect.
+ nsRect twistyRect(aTwistyRect);
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Deflate(twistyMargin);
+
+ nsRect imageSize;
+ nsITheme* theme = GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect,
+ aPresContext, twistyContext);
+
+ // Subtract out the remaining width. This is done even when we don't actually
+ // paint a twisty in this cell, so that cells in different rows still line up.
+ nsRect copyRect(twistyRect);
+ copyRect.Inflate(twistyMargin);
+ aRemainingWidth -= copyRect.width;
+ if (!isRTL) aCurrX += copyRect.width;
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (shouldPaint) {
+ // Paint our borders and background for our image rect.
+ result &= PaintBackgroundLayer(twistyContext, aPresContext,
+ aRenderingContext, twistyRect, aDirtyRect);
+
+ if (theme) {
+ if (isRTL) twistyRect.x = rightEdge - twistyRect.width;
+ // yeah, I know it says we're drawing a background, but a twisty is really
+ // a fg object since it doesn't have anything that gecko would want to
+ // draw over it. Besides, we have to prevent imagelib from drawing it.
+ nsRect dirty;
+ dirty.IntersectRect(twistyRect, aDirtyRect);
+ theme->DrawWidgetBackground(
+ &aRenderingContext, this,
+ twistyContext->StyleDisplay()->EffectiveAppearance(), twistyRect,
+ dirty);
+ } else {
+ // Time to paint the twisty.
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(twistyContext, bp);
+ twistyRect.Deflate(bp);
+ if (isRTL) twistyRect.x = rightEdge - twistyRect.width;
+ imageSize.Deflate(bp);
+
+ // Get the image for drawing.
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aColumn, true, twistyContext, getter_AddRefs(image));
+ if (image) {
+ nsPoint anchorPoint = twistyRect.TopLeft();
+
+ // Center the image. XXX Obey vertical-align style prop?
+ if (imageSize.height < twistyRect.height) {
+ anchorPoint.y += (twistyRect.height - imageSize.height) / 2;
+ }
+
+ // Apply context paint if applicable
+ SVGImageContext svgContext;
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
+ *twistyContext, image);
+
+ // Paint the image.
+ result &= nsLayoutUtils::DrawSingleUnscaledImage(
+ aRenderingContext, aPresContext, image, SamplingFilter::POINT,
+ anchorPoint, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE,
+ &imageSize);
+ }
+ }
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintImage(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aImageRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX,
+ nsDisplayListBuilder* aBuilder) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord rightEdge = aCurrX + aRemainingWidth;
+ // Resolve style for the image.
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ // Obtain the margins for the image and then deflate our rect by that
+ // amount. The image is assumed to be contained within the deflated rect.
+ nsRect imageRect(aImageRect);
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageRect.Deflate(imageMargin);
+
+ // Get the image.
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aColumn, false, imageContext, getter_AddRefs(image));
+
+ // Get the image destination size.
+ nsSize imageDestSize = GetImageDestSize(imageContext, image);
+ if (!imageDestSize.width || !imageDestSize.height) {
+ return ImgDrawResult::SUCCESS;
+ }
+
+ // Get the borders and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(imageContext, bp);
+
+ // destRect will be passed as the aDestRect argument in the DrawImage method.
+ // Start with the imageDestSize width and height.
+ nsRect destRect(0, 0, imageDestSize.width, imageDestSize.height);
+ // Inflate destRect for borders and padding so that we can compare/adjust
+ // with respect to imageRect.
+ destRect.Inflate(bp);
+
+ // The destRect width and height have not been adjusted to fit within the
+ // cell width and height.
+ // We must adjust the width even if image is null, because the width is used
+ // to update the aRemainingWidth and aCurrX values.
+ // Since the height isn't used unless the image is not null, we will adjust
+ // the height inside the if (image) block below.
+
+ if (destRect.width > imageRect.width) {
+ // The destRect is too wide to fit within the cell width.
+ // Adjust destRect width to fit within the cell width.
+ destRect.width = imageRect.width;
+ } else {
+ // The cell is wider than the destRect.
+ // In a cycler column, the image is centered horizontally.
+ if (!aColumn->IsCycler()) {
+ // If this column is not a cycler, we won't center the image horizontally.
+ // We adjust the imageRect width so that the image is placed at the start
+ // of the cell.
+ imageRect.width = destRect.width;
+ }
+ }
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (image) {
+ if (isRTL) imageRect.x = rightEdge - imageRect.width;
+ // Paint our borders and background for our image rect
+ result &= PaintBackgroundLayer(imageContext, aPresContext,
+ aRenderingContext, imageRect, aDirtyRect);
+
+ // The destRect x and y have not been set yet. Let's do that now.
+ // Initially, we use the imageRect x and y.
+ destRect.x = imageRect.x;
+ destRect.y = imageRect.y;
+
+ if (destRect.width < imageRect.width) {
+ // The destRect width is smaller than the cell width.
+ // Center the image horizontally in the cell.
+ // Adjust the destRect x accordingly.
+ destRect.x += (imageRect.width - destRect.width) / 2;
+ }
+
+ // Now it's time to adjust the destRect height to fit within the cell
+ // height.
+ if (destRect.height > imageRect.height) {
+ // The destRect height is larger than the cell height.
+ // Adjust destRect height to fit within the cell height.
+ destRect.height = imageRect.height;
+ } else if (destRect.height < imageRect.height) {
+ // The destRect height is smaller than the cell height.
+ // Center the image vertically in the cell.
+ // Adjust the destRect y accordingly.
+ destRect.y += (imageRect.height - destRect.height) / 2;
+ }
+
+ // It's almost time to paint the image.
+ // Deflate destRect for the border and padding.
+ destRect.Deflate(bp);
+
+ // Compute the area where our whole image would be mapped, to get the
+ // desired subregion onto our actual destRect:
+ nsRect wholeImageDest;
+ CSSIntSize rawImageCSSIntSize;
+ if (NS_SUCCEEDED(image->GetWidth(&rawImageCSSIntSize.width)) &&
+ NS_SUCCEEDED(image->GetHeight(&rawImageCSSIntSize.height))) {
+ // Get the image source rectangle - the rectangle containing the part of
+ // the image that we are going to display. sourceRect will be passed as
+ // the aSrcRect argument in the DrawImage method.
+ nsRect sourceRect = GetImageSourceRect(imageContext, image);
+
+ // Let's say that the image is 100 pixels tall and that the CSS has
+ // specified that the destination height should be 50 pixels tall. Let's
+ // say that the cell height is only 20 pixels. So, in those 20 visible
+ // pixels, we want to see the top 20/50ths of the image. So, the
+ // sourceRect.height should be 100 * 20 / 50, which is 40 pixels.
+ // Essentially, we are scaling the image as dictated by the CSS
+ // destination height and width, and we are then clipping the scaled
+ // image by the cell width and height.
+ nsSize rawImageSize(CSSPixel::ToAppUnits(rawImageCSSIntSize));
+ wholeImageDest = nsLayoutUtils::GetWholeImageDestination(
+ rawImageSize, sourceRect, nsRect(destRect.TopLeft(), imageDestSize));
+ } else {
+ // GetWidth/GetHeight failed, so we can't easily map a subregion of the
+ // source image onto the destination area.
+ // * If this happens with a RasterImage, it probably means the image is
+ // in an error state, and we shouldn't draw anything. Hence, we leave
+ // wholeImageDest as an empty rect (its initial state).
+ // * If this happens with a VectorImage, it probably means the image has
+ // no explicit width or height attribute -- but we can still proceed and
+ // just treat the destination area as our whole SVG image area. Hence, we
+ // set wholeImageDest to the full destRect.
+ if (image->GetType() == imgIContainer::TYPE_VECTOR) {
+ wholeImageDest = destRect;
+ }
+ }
+
+ const auto* styleEffects = imageContext->StyleEffects();
+ gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext);
+ if (!styleEffects->IsOpaque()) {
+ autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
+ styleEffects->mOpacity);
+ }
+
+ uint32_t drawFlags = aBuilder && aBuilder->UseHighQualityScaling()
+ ? imgIContainer::FLAG_HIGH_QUALITY_SCALING
+ : imgIContainer::FLAG_NONE;
+ result &= nsLayoutUtils::DrawImage(
+ aRenderingContext, imageContext, aPresContext, image,
+ nsLayoutUtils::GetSamplingFilterForFrame(this), wholeImageDest,
+ destRect, destRect.TopLeft(), aDirtyRect, drawFlags);
+ }
+
+ // Update the aRemainingWidth and aCurrX values.
+ imageRect.Inflate(imageMargin);
+ aRemainingWidth -= imageRect.width;
+ if (!isRTL) {
+ aCurrX += imageRect.width;
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintText(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTextRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ // Now obtain the text for our cell.
+ nsAutoString text;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellText(aRowIndex, aColumn, text);
+
+ // We're going to paint this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(text);
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (text.Length() == 0) {
+ // Don't paint an empty string. XXX What about background/borders? Still
+ // paint?
+ return result;
+ }
+
+ int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ // Resolve style for the text. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ // Obtain the margins for the text and then deflate our rect by that
+ // amount. The text is assumed to be contained within the deflated rect.
+ nsRect textRect(aTextRect);
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(textContext, bp);
+ textRect.Deflate(bp);
+
+ // Compute our text size.
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext());
+
+ nscoord height = fontMet->MaxHeight();
+ nscoord baseline = fontMet->MaxAscent();
+
+ // Center the text. XXX Obey vertical-align style prop?
+ if (height < textRect.height) {
+ textRect.y += (textRect.height - height) / 2;
+ textRect.height = height;
+ }
+
+ // Set our font.
+ AdjustForCellText(text, aRowIndex, aColumn, aRenderingContext, *fontMet,
+ textRect);
+ textRect.Inflate(bp);
+
+ // Subtract out the remaining width.
+ if (!isRTL) aCurrX += textRect.width + textMargin.LeftRight();
+
+ result &= PaintBackgroundLayer(textContext, aPresContext, aRenderingContext,
+ textRect, aDirtyRect);
+
+ // Time to paint our text.
+ textRect.Deflate(bp);
+
+ // Set our color.
+ ColorPattern color(ToDeviceColor(textContext->StyleText()->mColor));
+
+ // Draw decorations.
+ StyleTextDecorationLine decorations =
+ textContext->StyleTextReset()->mTextDecorationLine;
+
+ nscoord offset;
+ nscoord size;
+ if (decorations & (StyleTextDecorationLine::OVERLINE |
+ StyleTextDecorationLine::UNDERLINE)) {
+ fontMet->GetUnderline(offset, size);
+ if (decorations & StyleTextDecorationLine::OVERLINE) {
+ nsRect r(textRect.x, textRect.y, textRect.width, size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ if (decorations & StyleTextDecorationLine::UNDERLINE) {
+ nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width,
+ size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ }
+ if (decorations & StyleTextDecorationLine::LINE_THROUGH) {
+ fontMet->GetStrikeout(offset, size);
+ nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ const auto* styleEffects = textContext->StyleEffects();
+ gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext);
+ if (!styleEffects->IsOpaque()) {
+ autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
+ styleEffects->mOpacity);
+ }
+
+ aRenderingContext.SetColor(
+ sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor()));
+ nsLayoutUtils::DrawString(
+ this, *fontMet, &aRenderingContext, text.get(), text.Length(),
+ textRect.TopLeft() + nsPoint(0, baseline), cellContext);
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintCheckbox(int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ const nsRect& aCheckboxRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Resolve style for the checkbox.
+ ComputedStyle* checkboxContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCheckbox());
+
+ nscoord rightEdge = aCheckboxRect.XMost();
+
+ // Obtain the margins for the checkbox and then deflate our rect by that
+ // amount. The checkbox is assumed to be contained within the deflated rect.
+ nsRect checkboxRect(aCheckboxRect);
+ nsMargin checkboxMargin;
+ checkboxContext->StyleMargin()->GetMargin(checkboxMargin);
+ checkboxRect.Deflate(checkboxMargin);
+
+ nsRect imageSize = GetImageSize(aRowIndex, aColumn, true, checkboxContext);
+
+ if (imageSize.height > checkboxRect.height) {
+ imageSize.height = checkboxRect.height;
+ }
+ if (imageSize.width > checkboxRect.width) {
+ imageSize.width = checkboxRect.width;
+ }
+
+ if (StyleVisibility()->mDirection == StyleDirection::Rtl) {
+ checkboxRect.x = rightEdge - checkboxRect.width;
+ }
+
+ // Paint our borders and background for our image rect.
+ ImgDrawResult result =
+ PaintBackgroundLayer(checkboxContext, aPresContext, aRenderingContext,
+ checkboxRect, aDirtyRect);
+
+ // Time to paint the checkbox.
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(checkboxContext, bp);
+ checkboxRect.Deflate(bp);
+
+ // Get the image for drawing.
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aColumn, true, checkboxContext, getter_AddRefs(image));
+ if (image) {
+ nsPoint pt = checkboxRect.TopLeft();
+
+ if (imageSize.height < checkboxRect.height) {
+ pt.y += (checkboxRect.height - imageSize.height) / 2;
+ }
+
+ if (imageSize.width < checkboxRect.width) {
+ pt.x += (checkboxRect.width - imageSize.width) / 2;
+ }
+
+ // Apply context paint if applicable
+ SVGImageContext svgContext;
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
+ *checkboxContext, image);
+ // Paint the image.
+ result &= nsLayoutUtils::DrawSingleUnscaledImage(
+ aRenderingContext, aPresContext, image, SamplingFilter::POINT, pt,
+ &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintDropFeedback(
+ const nsRect& aDropFeedbackRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt) {
+ // Paint the drop feedback in between rows.
+
+ nscoord currX;
+
+ // Adjust for the primary cell.
+ nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn();
+
+ if (primaryCol) {
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ primaryCol->GetXInTwips(this, &currX);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "primary column is invalid?");
+
+ currX += aPt.x - mHorzPosition;
+ } else {
+ currX = aDropFeedbackRect.x;
+ }
+
+ PrefillPropertyArray(mSlots->mDropRow, primaryCol);
+
+ // Resolve the style to use for the drop feedback.
+ ComputedStyle* feedbackContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeDropFeedback());
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Paint only if it is visible.
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (feedbackContext->StyleVisibility()->IsVisibleOrCollapsed()) {
+ int32_t level;
+ view->GetLevel(mSlots->mDropRow, &level);
+
+ // If our previous or next row has greater level use that for
+ // correct visual indentation.
+ if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) {
+ if (mSlots->mDropRow > 0) {
+ int32_t previousLevel;
+ view->GetLevel(mSlots->mDropRow - 1, &previousLevel);
+ if (previousLevel > level) level = previousLevel;
+ }
+ } else {
+ if (mSlots->mDropRow < mRowCount - 1) {
+ int32_t nextLevel;
+ view->GetLevel(mSlots->mDropRow + 1, &nextLevel);
+ if (nextLevel > level) level = nextLevel;
+ }
+ }
+
+ currX += mIndentation * level;
+
+ if (primaryCol) {
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+ nsRect imageSize;
+ nsRect twistyRect;
+ GetTwistyRect(mSlots->mDropRow, primaryCol, imageSize, twistyRect,
+ aPresContext, twistyContext);
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+ currX += twistyRect.width;
+ }
+
+ const nsStylePosition* stylePosition = feedbackContext->StylePosition();
+
+ // Obtain the width for the drop feedback or use default value.
+ nscoord width;
+ if (stylePosition->mWidth.ConvertsToLength()) {
+ width = stylePosition->mWidth.ToLength();
+ } else {
+ // Use default width 50px.
+ width = nsPresContext::CSSPixelsToAppUnits(50);
+ }
+
+ // Obtain the height for the drop feedback or use default value.
+ nscoord height;
+ if (stylePosition->mHeight.ConvertsToLength()) {
+ height = stylePosition->mHeight.ToLength();
+ } else {
+ // Use default height 2px.
+ height = nsPresContext::CSSPixelsToAppUnits(2);
+ }
+
+ // Obtain the margins for the drop feedback and then deflate our rect
+ // by that amount.
+ nsRect feedbackRect(currX, aDropFeedbackRect.y, width, height);
+ nsMargin margin;
+ feedbackContext->StyleMargin()->GetMargin(margin);
+ feedbackRect.Deflate(margin);
+
+ feedbackRect.y += (aDropFeedbackRect.height - height) / 2;
+
+ // Finally paint the drop feedback.
+ result &= PaintBackgroundLayer(feedbackContext, aPresContext,
+ aRenderingContext, feedbackRect, aDirtyRect);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintBackgroundLayer(
+ ComputedStyle* aComputedStyle, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext, const nsRect& aRect,
+ const nsRect& aDirtyRect) {
+ const nsStyleBorder* myBorder = aComputedStyle->StyleBorder();
+ nsCSSRendering::PaintBGParams params =
+ nsCSSRendering::PaintBGParams::ForAllLayers(
+ *aPresContext, aDirtyRect, aRect, this,
+ nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES);
+ ImgDrawResult result = nsCSSRendering::PaintStyleImageLayerWithSC(
+ params, aRenderingContext, aComputedStyle, *myBorder);
+
+ result &= nsCSSRendering::PaintBorderWithStyleBorder(
+ aPresContext, aRenderingContext, this, aDirtyRect, aRect, *myBorder,
+ mComputedStyle, PaintBorderFlags::SyncDecodeImages);
+
+ nsCSSRendering::PaintNonThemedOutline(aPresContext, aRenderingContext, this,
+ aDirtyRect, aRect, aComputedStyle);
+
+ return result;
+}
+
+// Scrolling
+nsresult nsTreeBodyFrame::EnsureRowIsVisible(int32_t aRow) {
+ ScrollParts parts = GetScrollParts();
+ nsresult rv = EnsureRowIsVisibleInternal(parts, aRow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ UpdateScrollbars(parts);
+ return rv;
+}
+
+nsresult nsTreeBodyFrame::EnsureRowIsVisibleInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ if (!mView || !mPageLength) {
+ return NS_OK;
+ }
+
+ if (mTopRowIndex <= aRow && mTopRowIndex + mPageLength > aRow) return NS_OK;
+
+ if (aRow < mTopRowIndex)
+ ScrollToRowInternal(aParts, aRow);
+ else {
+ // Bring it just on-screen.
+ int32_t distance = aRow - (mTopRowIndex + mPageLength) + 1;
+ ScrollToRowInternal(aParts, mTopRowIndex + distance);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::EnsureCellIsVisible(int32_t aRow,
+ nsTreeColumn* aCol) {
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ ScrollParts parts = GetScrollParts();
+
+ nscoord result = -1;
+ nsresult rv;
+
+ nscoord columnPos;
+ rv = aCol->GetXInTwips(this, &columnPos);
+ if (NS_FAILED(rv)) return rv;
+
+ nscoord columnWidth;
+ rv = aCol->GetWidthInTwips(this, &columnWidth);
+ if (NS_FAILED(rv)) return rv;
+
+ // If the start of the column is before the
+ // start of the horizontal view, then scroll
+ if (columnPos < mHorzPosition) result = columnPos;
+ // If the end of the column is past the end of
+ // the horizontal view, then scroll
+ else if ((columnPos + columnWidth) > (mHorzPosition + mInnerBox.width))
+ result = ((columnPos + columnWidth) - (mHorzPosition + mInnerBox.width)) +
+ mHorzPosition;
+
+ if (result != -1) {
+ rv = ScrollHorzInternal(parts, result);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ rv = EnsureRowIsVisibleInternal(parts, aRow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ UpdateScrollbars(parts);
+ return rv;
+}
+
+void nsTreeBodyFrame::ScrollToRow(int32_t aRow) {
+ ScrollParts parts = GetScrollParts();
+ ScrollToRowInternal(parts, aRow);
+ UpdateScrollbars(parts);
+}
+
+nsresult nsTreeBodyFrame::ScrollToRowInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ ScrollInternal(aParts, aRow);
+
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::ScrollByLines(int32_t aNumLines) {
+ if (!mView) {
+ return;
+ }
+ int32_t newIndex = mTopRowIndex + aNumLines;
+ ScrollToRow(newIndex);
+}
+
+void nsTreeBodyFrame::ScrollByPages(int32_t aNumPages) {
+ if (!mView) {
+ return;
+ }
+ int32_t newIndex = mTopRowIndex + aNumPages * mPageLength;
+ ScrollToRow(newIndex);
+}
+
+nsresult nsTreeBodyFrame::ScrollInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ if (!mView) {
+ return NS_OK;
+ }
+
+ // Note that we may be "over scrolled" at this point; that is the
+ // current mTopRowIndex may be larger than mRowCount - mPageLength.
+ // This can happen when items are removed for example. (bug 1085050)
+
+ int32_t maxTopRowIndex = std::max(0, mRowCount - mPageLength);
+ aRow = mozilla::clamped(aRow, 0, maxTopRowIndex);
+ if (aRow == mTopRowIndex) {
+ return NS_OK;
+ }
+ mTopRowIndex = aRow;
+ Invalidate();
+ PostScrollEvent();
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::ScrollHorzInternal(const ScrollParts& aParts,
+ int32_t aPosition) {
+ if (!mView || !aParts.mColumnsScrollFrame || !aParts.mHScrollbar)
+ return NS_OK;
+
+ if (aPosition == mHorzPosition) return NS_OK;
+
+ if (aPosition < 0 || aPosition > mHorzWidth) return NS_OK;
+
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ if (aPosition > (mHorzWidth - bounds.width))
+ aPosition = mHorzWidth - bounds.width;
+
+ mHorzPosition = aPosition;
+
+ Invalidate();
+
+ // Update the column scroll view
+ AutoWeakFrame weakFrame(this);
+ aParts.mColumnsScrollFrame->ScrollTo(nsPoint(mHorzPosition, 0),
+ ScrollMode::Instant);
+ if (!weakFrame.IsAlive()) {
+ return NS_ERROR_FAILURE;
+ }
+ // And fire off an event about it all
+ PostScrollEvent();
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::ScrollByPage(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ ScrollByPages(aDirection);
+}
+
+void nsTreeBodyFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ int32_t newIndex = aDirection < 0 ? 0 : mTopRowIndex;
+ ScrollToRow(newIndex);
+}
+
+void nsTreeBodyFrame::ScrollByLine(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ ScrollByLines(aDirection);
+}
+
+void nsTreeBodyFrame::ScrollByUnit(
+ nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection,
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags /* = Disabled */) {
+ MOZ_ASSERT_UNREACHABLE("Can't get here, we pass false to MoveToNewPosition");
+}
+
+void nsTreeBodyFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) {
+ ScrollParts parts = GetScrollParts();
+ int32_t increment = aScrollbar->GetIncrement();
+ int32_t direction = 0;
+ if (increment < 0) {
+ direction = -1;
+ } else if (increment > 0) {
+ direction = 1;
+ }
+ bool isHorizontal = aScrollbar->IsHorizontal();
+
+ AutoWeakFrame weakFrame(this);
+ if (isHorizontal) {
+ int32_t curpos = aScrollbar->MoveToNewPosition(
+ nsScrollbarFrame::ImplementsScrollByUnit::No);
+ if (weakFrame.IsAlive()) {
+ ScrollHorzInternal(parts, curpos);
+ }
+ } else {
+ ScrollToRowInternal(parts, mTopRowIndex + direction);
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+ if (weakFrame.IsAlive()) {
+ UpdateScrollbars(parts);
+ }
+}
+
+void nsTreeBodyFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) {
+ ScrollParts parts = GetScrollParts();
+
+ if (aOldPos == aNewPos) return;
+
+ AutoWeakFrame weakFrame(this);
+
+ // Vertical Scrollbar
+ if (parts.mVScrollbar == aScrollbar) {
+ nscoord rh = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+ nscoord newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos);
+ nscoord newrow = (rh > 0) ? (newIndex / rh) : 0;
+ ScrollInternal(parts, newrow);
+ // Horizontal Scrollbar
+ } else if (parts.mHScrollbar == aScrollbar) {
+ int32_t newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos);
+ ScrollHorzInternal(parts, newIndex);
+ }
+ if (weakFrame.IsAlive()) {
+ UpdateScrollbars(parts);
+ }
+}
+
+// The style cache.
+ComputedStyle* nsTreeBodyFrame::GetPseudoComputedStyle(
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement) {
+ return mStyleCache.GetComputedStyle(PresContext(), mContent, mComputedStyle,
+ aPseudoElement, mScratchArray);
+}
+
+XULTreeElement* nsTreeBodyFrame::GetBaseElement() {
+ if (!mTree) {
+ nsIFrame* parent = GetParent();
+ while (parent) {
+ nsIContent* content = parent->GetContent();
+ if (content && content->IsXULElement(nsGkAtoms::tree)) {
+ mTree = XULTreeElement::FromNodeOrNull(content->AsElement());
+ break;
+ }
+
+ parent = parent->GetInFlowParent();
+ }
+ }
+
+ return mTree;
+}
+
+nsresult nsTreeBodyFrame::ClearStyleAndImageCaches() {
+ mStyleCache.Clear();
+ CancelImageRequests();
+ mImageCache.Clear();
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::RemoveImageCacheEntry(int32_t aRowIndex,
+ nsTreeColumn* aCol) {
+ nsAutoString imageSrc;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (NS_FAILED(view->GetImageSrc(aRowIndex, aCol, imageSrc))) {
+ return;
+ }
+ nsTreeImageCacheEntry entry;
+ if (!mImageCache.Get(imageSrc, &entry)) {
+ return;
+ }
+ nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr);
+ entry.request->UnlockImage();
+ entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ mImageCache.Remove(imageSrc);
+}
+
+/* virtual */
+void nsTreeBodyFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
+ SimpleXULLeafFrame::DidSetComputedStyle(aOldComputedStyle);
+
+ // Clear the style cache; the pointers are no longer even valid
+ mStyleCache.Clear();
+ // XXX The following is hacky, but it's not incorrect,
+ // and appears to fix a few bugs with style changes, like text zoom and
+ // dpi changes
+ mIndentation = GetIndentation();
+ mRowHeight = GetRowHeight();
+}
+
+bool nsTreeBodyFrame::OffsetForHorzScroll(nsRect& rect, bool clip) {
+ rect.x -= mHorzPosition;
+
+ // Scrolled out before
+ if (rect.XMost() <= mInnerBox.x) return false;
+
+ // Scrolled out after
+ if (rect.x > mInnerBox.XMost()) return false;
+
+ if (clip) {
+ nscoord leftEdge = std::max(rect.x, mInnerBox.x);
+ nscoord rightEdge = std::min(rect.XMost(), mInnerBox.XMost());
+ rect.x = leftEdge;
+ rect.width = rightEdge - leftEdge;
+
+ // Should have returned false above
+ NS_ASSERTION(rect.width >= 0, "horz scroll code out of sync");
+ }
+
+ return true;
+}
+
+bool nsTreeBodyFrame::CanAutoScroll(int32_t aRowIndex) {
+ // Check first for partially visible last row.
+ if (aRowIndex == mRowCount - 1) {
+ nscoord y = mInnerBox.y + (aRowIndex - mTopRowIndex) * mRowHeight;
+ if (y < mInnerBox.height && y + mRowHeight > mInnerBox.height) return true;
+ }
+
+ if (aRowIndex > 0 && aRowIndex < mRowCount - 1) return true;
+
+ return false;
+}
+
+// Given a dom event, figure out which row in the tree the mouse is over,
+// if we should drop before/after/on that row or we should auto-scroll.
+// Doesn't query the content about if the drag is allowable, that's done
+// elsewhere.
+//
+// For containers, we break up the vertical space of the row as follows: if in
+// the topmost 25%, the drop is _before_ the row the mouse is over; if in the
+// last 25%, _after_; in the middle 50%, we consider it a drop _on_ the
+// container.
+//
+// For non-containers, if the mouse is in the top 50% of the row, the drop is
+// _before_ and the bottom 50% _after_
+void nsTreeBodyFrame::ComputeDropPosition(WidgetGUIEvent* aEvent, int32_t* aRow,
+ int16_t* aOrient,
+ int16_t* aScrollLines) {
+ *aOrient = -1;
+ *aScrollLines = 0;
+
+ // Convert the event's point to our coordinates. We want it in
+ // the coordinates of our inner box's coordinates.
+ nsPoint pt =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this});
+ int32_t xTwips = pt.x - mInnerBox.x;
+ int32_t yTwips = pt.y - mInnerBox.y;
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ *aRow = GetRowAtInternal(xTwips, yTwips);
+ if (*aRow >= 0) {
+ // Compute the top/bottom of the row in question.
+ int32_t yOffset = yTwips - mRowHeight * (*aRow - mTopRowIndex);
+
+ bool isContainer = false;
+ view->IsContainer(*aRow, &isContainer);
+ if (isContainer) {
+ // for a container, use a 25%/50%/25% breakdown
+ if (yOffset < mRowHeight / 4)
+ *aOrient = nsITreeView::DROP_BEFORE;
+ else if (yOffset > mRowHeight - (mRowHeight / 4))
+ *aOrient = nsITreeView::DROP_AFTER;
+ else
+ *aOrient = nsITreeView::DROP_ON;
+ } else {
+ // for a non-container use a 50%/50% breakdown
+ if (yOffset < mRowHeight / 2)
+ *aOrient = nsITreeView::DROP_BEFORE;
+ else
+ *aOrient = nsITreeView::DROP_AFTER;
+ }
+ }
+
+ if (CanAutoScroll(*aRow)) {
+ // Get the max value from the look and feel service.
+ int32_t scrollLinesMax =
+ LookAndFeel::GetInt(LookAndFeel::IntID::TreeScrollLinesMax, 0);
+ scrollLinesMax--;
+ if (scrollLinesMax < 0) scrollLinesMax = 0;
+
+ // Determine if we're w/in a margin of the top/bottom of the tree during a
+ // drag. This will ultimately cause us to scroll, but that's done elsewhere.
+ nscoord height = (3 * mRowHeight) / 4;
+ if (yTwips < height) {
+ // scroll up
+ *aScrollLines =
+ NSToIntRound(-scrollLinesMax * (1 - (float)yTwips / height) - 1);
+ } else if (yTwips > mRect.height - height) {
+ // scroll down
+ *aScrollLines = NSToIntRound(
+ scrollLinesMax * (1 - (float)(mRect.height - yTwips) / height) + 1);
+ }
+ }
+} // ComputeDropPosition
+
+void nsTreeBodyFrame::OpenCallback(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (!self) {
+ return;
+ }
+
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ nsCOMPtr<nsITreeView> view = self->GetExistingView();
+ if (self->mSlots->mDropRow >= 0) {
+ self->mSlots->mArray.AppendElement(self->mSlots->mDropRow);
+ view->ToggleOpenState(self->mSlots->mDropRow);
+ }
+}
+
+void nsTreeBodyFrame::CloseCallback(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (!self) {
+ return;
+ }
+
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ nsCOMPtr<nsITreeView> view = self->GetExistingView();
+ auto array = std::move(self->mSlots->mArray);
+ if (!view) {
+ return;
+ }
+ for (auto elem : Reversed(array)) {
+ view->ToggleOpenState(elem);
+ }
+}
+
+void nsTreeBodyFrame::LazyScrollCallback(nsITimer* aTimer, void* aClosure) {
+ nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (self) {
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ if (self->mView) {
+ // Set a new timer to scroll the tree repeatedly.
+ self->CreateTimer(LookAndFeel::IntID::TreeScrollDelay, ScrollCallback,
+ nsITimer::TYPE_REPEATING_SLACK,
+ getter_AddRefs(self->mSlots->mTimer),
+ "nsTreeBodyFrame::ScrollCallback");
+ self->ScrollByLines(self->mSlots->mScrollLines);
+ // ScrollByLines may have deleted |self|.
+ }
+ }
+}
+
+void nsTreeBodyFrame::ScrollCallback(nsITimer* aTimer, void* aClosure) {
+ nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (self) {
+ // Don't scroll if we are already at the top or bottom of the view.
+ if (self->mView && self->CanAutoScroll(self->mSlots->mDropRow)) {
+ self->ScrollByLines(self->mSlots->mScrollLines);
+ } else {
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+ }
+ }
+}
+
+// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsTreeBodyFrame::ScrollEvent::Run() {
+ if (mInner) {
+ mInner->FireScrollEvent();
+ }
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::FireScrollEvent() {
+ mScrollEvent.Forget();
+ WidgetGUIEvent event(true, eScroll, nullptr);
+ // scroll events fired at elements don't bubble
+ event.mFlags.mBubbles = false;
+ RefPtr<nsIContent> content = GetContent();
+ RefPtr<nsPresContext> presContext = PresContext();
+ EventDispatcher::Dispatch(content, presContext, &event);
+}
+
+void nsTreeBodyFrame::PostScrollEvent() {
+ if (mScrollEvent.IsPending()) return;
+
+ RefPtr<ScrollEvent> event = new ScrollEvent(this);
+ nsresult rv =
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, do_AddRef(event));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to dispatch ScrollEvent");
+ } else {
+ mScrollEvent = std::move(event);
+ }
+}
+
+void nsTreeBodyFrame::ScrollbarActivityStarted() const {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->ActivityStarted();
+ }
+}
+
+void nsTreeBodyFrame::ScrollbarActivityStopped() const {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->ActivityStopped();
+ }
+}
+
+void nsTreeBodyFrame::DetachImageListeners() { mCreatedListeners.Clear(); }
+
+void nsTreeBodyFrame::RemoveTreeImageListener(nsTreeImageListener* aListener) {
+ if (aListener) {
+ mCreatedListeners.Remove(aListener);
+ }
+}
+
+#ifdef ACCESSIBILITY
+static void InitCustomEvent(CustomEvent* aEvent, const nsAString& aType,
+ nsIWritablePropertyBag2* aDetail) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aEvent->GetParentObject())) {
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx);
+ if (!ToJSValue(cx, aDetail, &detail)) {
+ jsapi.ClearException();
+ return;
+ }
+
+ aEvent->InitCustomEvent(cx, aType, /* aCanBubble = */ true,
+ /* aCancelable = */ false, detail);
+}
+
+void nsTreeBodyFrame::FireRowCountChangedEvent(int32_t aIndex, int32_t aCount) {
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+ if (!tree) return;
+
+ RefPtr<Document> doc = tree->OwnerDoc();
+ MOZ_ASSERT(doc);
+
+ RefPtr<Event> event =
+ doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors());
+
+ CustomEvent* treeEvent = event->AsCustomEvent();
+ if (!treeEvent) {
+ return;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ if (!propBag) {
+ return;
+ }
+
+ // Set 'index' data - the row index rows are changed from.
+ propBag->SetPropertyAsInt32(u"index"_ns, aIndex);
+
+ // Set 'count' data - the number of changed rows.
+ propBag->SetPropertyAsInt32(u"count"_ns, aCount);
+
+ InitCustomEvent(treeEvent, u"TreeRowCountChanged"_ns, propBag);
+
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(tree, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx,
+ int32_t aEndRowIdx,
+ nsTreeColumn* aStartCol,
+ nsTreeColumn* aEndCol) {
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+ if (!tree) return;
+
+ RefPtr<Document> doc = tree->OwnerDoc();
+
+ RefPtr<Event> event =
+ doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors());
+
+ CustomEvent* treeEvent = event->AsCustomEvent();
+ if (!treeEvent) {
+ return;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ if (!propBag) {
+ return;
+ }
+
+ if (aStartRowIdx != -1 && aEndRowIdx != -1) {
+ // Set 'startrow' data - the start index of invalidated rows.
+ propBag->SetPropertyAsInt32(u"startrow"_ns, aStartRowIdx);
+
+ // Set 'endrow' data - the end index of invalidated rows.
+ propBag->SetPropertyAsInt32(u"endrow"_ns, aEndRowIdx);
+ }
+
+ if (aStartCol && aEndCol) {
+ // Set 'startcolumn' data - the start index of invalidated rows.
+ int32_t startColIdx = aStartCol->GetIndex();
+
+ propBag->SetPropertyAsInt32(u"startcolumn"_ns, startColIdx);
+
+ // Set 'endcolumn' data - the start index of invalidated rows.
+ int32_t endColIdx = aEndCol->GetIndex();
+ propBag->SetPropertyAsInt32(u"endcolumn"_ns, endColIdx);
+ }
+
+ InitCustomEvent(treeEvent, u"TreeInvalidated"_ns, propBag);
+
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(tree, event);
+ asyncDispatcher->PostDOMEvent();
+}
+#endif
+
+class nsOverflowChecker : public Runnable {
+ public:
+ explicit nsOverflowChecker(nsTreeBodyFrame* aFrame)
+ : mozilla::Runnable("nsOverflowChecker"), mFrame(aFrame) {}
+ NS_IMETHOD Run() override {
+ if (mFrame.IsAlive()) {
+ nsTreeBodyFrame* tree = static_cast<nsTreeBodyFrame*>(mFrame.GetFrame());
+ nsTreeBodyFrame::ScrollParts parts = tree->GetScrollParts();
+ tree->CheckOverflow(parts);
+ }
+ return NS_OK;
+ }
+
+ private:
+ WeakFrame mFrame;
+};
+
+bool nsTreeBodyFrame::FullScrollbarsUpdate(bool aNeedsFullInvalidation) {
+ ScrollParts parts = GetScrollParts();
+ AutoWeakFrame weakFrame(this);
+ AutoWeakFrame weakColumnsFrame(parts.mColumnsFrame);
+ UpdateScrollbars(parts);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+ if (aNeedsFullInvalidation) {
+ Invalidate();
+ }
+ InvalidateScrollbars(parts, weakColumnsFrame);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+
+ // Overflow checking dispatches synchronous events, which can cause infinite
+ // recursion during reflow. Do the first overflow check synchronously, but
+ // force any nested checks to round-trip through the event loop. See bug
+ // 905909.
+ RefPtr<nsOverflowChecker> checker = new nsOverflowChecker(this);
+ if (!mCheckingOverflow) {
+ nsContentUtils::AddScriptRunner(checker);
+ } else {
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, checker.forget());
+ }
+ return weakFrame.IsAlive();
+}
+
+void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) {
+ nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr);
+}
diff --git a/layout/xul/tree/nsTreeBodyFrame.h b/layout/xul/tree/nsTreeBodyFrame.h
new file mode 100644
index 0000000000..35fa7eb1f0
--- /dev/null
+++ b/layout/xul/tree/nsTreeBodyFrame.h
@@ -0,0 +1,607 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeBodyFrame_h
+#define nsTreeBodyFrame_h
+
+#include "mozilla/AtomArray.h"
+#include "mozilla/Attributes.h"
+
+#include "nsITreeView.h"
+#include "nsIScrollbarMediator.h"
+#include "nsITimer.h"
+#include "nsIReflowCallback.h"
+#include "nsTArray.h"
+#include "nsTreeStyleCache.h"
+#include "nsTreeColumns.h"
+#include "nsTHashMap.h"
+#include "nsTHashSet.h"
+#include "imgIRequest.h"
+#include "imgINotificationObserver.h"
+#include "nsScrollbarFrame.h"
+#include "nsThreadUtils.h"
+#include "SimpleXULLeafFrame.h"
+#include "mozilla/LookAndFeel.h"
+
+class nsFontMetrics;
+class nsOverflowChecker;
+class nsTreeImageListener;
+
+namespace mozilla {
+class PresShell;
+namespace layout {
+class ScrollbarActivity;
+} // namespace layout
+} // namespace mozilla
+
+// An entry in the tree's image cache
+struct nsTreeImageCacheEntry {
+ nsTreeImageCacheEntry() = default;
+ nsTreeImageCacheEntry(imgIRequest* aRequest,
+ imgINotificationObserver* aListener)
+ : request(aRequest), listener(aListener) {}
+
+ nsCOMPtr<imgIRequest> request;
+ nsCOMPtr<imgINotificationObserver> listener;
+};
+
+// The actual frame that paints the cells and rows.
+class nsTreeBodyFrame final : public mozilla::SimpleXULLeafFrame,
+ public nsIScrollbarMediator,
+ public nsIReflowCallback {
+ typedef mozilla::layout::ScrollbarActivity ScrollbarActivity;
+ typedef mozilla::image::ImgDrawResult ImgDrawResult;
+
+ public:
+ explicit nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ ~nsTreeBodyFrame();
+
+ nscoord GetIntrinsicBSize() override;
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsTreeBodyFrame)
+
+ // Callback handler methods for refresh driver based animations.
+ // Calls to these functions are forwarded from nsTreeImageListener. These
+ // mirror how nsImageFrame works.
+ void OnImageIsAnimated(imgIRequest* aRequest);
+
+ // non-virtual signatures like nsITreeBodyFrame
+ already_AddRefed<nsTreeColumns> Columns() const {
+ RefPtr<nsTreeColumns> cols = mColumns;
+ return cols.forget();
+ }
+ already_AddRefed<nsITreeView> GetExistingView() const {
+ nsCOMPtr<nsITreeView> view = mView;
+ return view.forget();
+ }
+ already_AddRefed<nsITreeSelection> GetSelection() const;
+ nsresult GetView(nsITreeView** aView);
+ nsresult SetView(nsITreeView* aView);
+ bool GetFocused() const { return mFocused; }
+ nsresult SetFocused(bool aFocused);
+ nsresult GetTreeBody(mozilla::dom::Element** aElement);
+ int32_t RowHeight() const;
+ int32_t RowWidth();
+ int32_t GetHorizontalPosition() const;
+ mozilla::Maybe<mozilla::CSSIntRegion> GetSelectionRegion();
+ int32_t FirstVisibleRow() const { return mTopRowIndex; }
+ int32_t LastVisibleRow() const { return mTopRowIndex + mPageLength; }
+ int32_t PageLength() const { return mPageLength; }
+ nsresult EnsureRowIsVisible(int32_t aRow);
+ nsresult EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol);
+ void ScrollToRow(int32_t aRow);
+ void ScrollByLines(int32_t aNumLines);
+ void ScrollByPages(int32_t aNumPages);
+ nsresult Invalidate();
+ nsresult InvalidateColumn(nsTreeColumn* aCol);
+ nsresult InvalidateRow(int32_t aRow);
+ nsresult InvalidateCell(int32_t aRow, nsTreeColumn* aCol);
+ nsresult InvalidateRange(int32_t aStart, int32_t aEnd);
+ int32_t GetRowAt(int32_t aX, int32_t aY);
+ nsresult GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, nsTreeColumn** aCol,
+ nsACString& aChildElt);
+ nsresult GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol,
+ const nsACString& aElt, int32_t* aX,
+ int32_t* aY, int32_t* aWidth, int32_t* aHeight);
+ nsresult IsCellCropped(int32_t aRow, nsTreeColumn* aCol, bool* aResult);
+ nsresult RowCountChanged(int32_t aIndex, int32_t aCount);
+ nsresult BeginUpdateBatch();
+ nsresult EndUpdateBatch();
+ nsresult ClearStyleAndImageCaches();
+ void RemoveImageCacheEntry(int32_t aRowIndex, nsTreeColumn* aCol);
+
+ void CancelImageRequests();
+
+ void ManageReflowCallback();
+
+ void DidReflow(nsPresContext*, const ReflowInput*) override;
+
+ // nsIReflowCallback
+ bool ReflowFinished() override;
+ void ReflowCallbackCanceled() override;
+
+ // nsIScrollbarMediator
+ void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ void ScrollByUnit(nsScrollbarFrame* aScrollbar, mozilla::ScrollMode aMode,
+ int32_t aDirection, mozilla::ScrollUnit aUnit,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) override;
+ void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) override;
+ void ScrollbarReleased(nsScrollbarFrame* aScrollbar) override {}
+ void VisibilityChanged(bool aVisible) override { Invalidate(); }
+ nsScrollbarFrame* GetScrollbarBox(bool aVertical) override {
+ ScrollParts parts = GetScrollParts();
+ return aVertical ? parts.mVScrollbar : parts.mHScrollbar;
+ }
+ void ScrollbarActivityStarted() const override;
+ void ScrollbarActivityStopped() const override;
+ bool IsScrollbarOnRight() const override {
+ return StyleVisibility()->mDirection == mozilla::StyleDirection::Ltr;
+ }
+ bool ShouldSuppressScrollbarRepaints() const override { return false; }
+
+ // Overridden from nsIFrame to cache our pres context.
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+ void Destroy(DestroyContext&) override;
+
+ mozilla::Maybe<Cursor> GetCursor(const nsPoint&) override;
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override;
+
+ friend nsIFrame* NS_NewTreeBodyFrame(mozilla::PresShell* aPresShell);
+ friend class nsTreeColumn;
+
+ struct ScrollParts {
+ nsScrollbarFrame* mVScrollbar;
+ RefPtr<mozilla::dom::Element> mVScrollbarContent;
+ nsScrollbarFrame* mHScrollbar;
+ RefPtr<mozilla::dom::Element> mHScrollbarContent;
+ nsIFrame* mColumnsFrame;
+ nsIScrollableFrame* mColumnsScrollFrame;
+ };
+
+ ImgDrawResult PaintTreeBody(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder);
+
+ // Get the base element, <tree>
+ mozilla::dom::XULTreeElement* GetBaseElement();
+
+ bool GetVerticalOverflow() const { return mVerticalOverflow; }
+ bool GetHorizontalOverflow() const { return mHorizontalOverflow; }
+
+ // This returns the property array where atoms are stored for style during
+ // draw, whether the row currently being drawn is selected, hovered, etc.
+ const mozilla::AtomArray& GetPropertyArrayForCurrentDrawingItem() {
+ return mScratchArray;
+ }
+
+ protected:
+ friend class nsOverflowChecker;
+
+ // This method paints a specific column background of the tree.
+ ImgDrawResult PaintColumn(nsTreeColumn* aColumn, const nsRect& aColumnRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a single row in the tree.
+ ImgDrawResult PaintRow(int32_t aRowIndex, const nsRect& aRowRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder);
+
+ // This method paints a single separator in the tree.
+ ImgDrawResult PaintSeparator(int32_t aRowIndex, const nsRect& aSeparatorRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a specific cell in a given row of the tree.
+ ImgDrawResult PaintCell(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aCellRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX,
+ nsPoint aPt, nsDisplayListBuilder* aBuilder);
+
+ // This method paints the twisty inside a cell in the primary column of an
+ // tree.
+ ImgDrawResult PaintTwisty(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth,
+ nscoord& aCurrX);
+
+ // This method paints the image inside the cell of an tree.
+ ImgDrawResult PaintImage(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aImageRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth,
+ nscoord& aCurrX, nsDisplayListBuilder* aBuilder);
+
+ // This method paints the text string inside a particular cell of the tree.
+ ImgDrawResult PaintText(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aTextRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX);
+
+ // This method paints the checkbox inside a particular cell of the tree.
+ ImgDrawResult PaintCheckbox(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aCheckboxRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a drop feedback of the tree.
+ ImgDrawResult PaintDropFeedback(const nsRect& aDropFeedbackRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt);
+
+ // This method is called with a specific ComputedStyle and rect to
+ // paint the background rect as if it were a full-blown frame.
+ ImgDrawResult PaintBackgroundLayer(ComputedStyle* aComputedStyle,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect);
+
+ // An internal hit test. aX and aY are expected to be in twips in the
+ // coordinate system of this frame.
+ int32_t GetRowAtInternal(nscoord aX, nscoord aY);
+
+ // Check for bidi characters in the text, and if there are any, ensure
+ // that the prescontext is in bidi mode.
+ void CheckTextForBidi(nsAutoString& aText);
+
+ void AdjustForCellText(nsAutoString& aText, int32_t aRowIndex,
+ nsTreeColumn* aColumn, gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics, nsRect& aTextRect);
+
+ // A helper used when hit testing.
+ nsCSSAnonBoxPseudoStaticAtom* GetItemWithinCellAt(nscoord aX,
+ const nsRect& aCellRect,
+ int32_t aRowIndex,
+ nsTreeColumn* aColumn);
+
+ // An internal hit test. aX and aY are expected to be in twips in the
+ // coordinate system of this frame.
+ void GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, nsTreeColumn** aCol,
+ nsCSSAnonBoxPseudoStaticAtom** aChildElt);
+
+ // Retrieve the area for the twisty for a cell.
+ nsITheme* GetTwistyRect(int32_t aRowIndex, nsTreeColumn* aColumn,
+ nsRect& aImageRect, nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ ComputedStyle* aTwistyContext);
+
+ // Fetch an image from the image cache.
+ nsresult GetImage(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext,
+ ComputedStyle* aComputedStyle, imgIContainer** aResult);
+
+ // Returns the size of a given image. This size *includes* border and
+ // padding. It does not include margins.
+ nsRect GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext,
+ ComputedStyle* aComputedStyle);
+
+ // Returns the destination size of the image, not including borders and
+ // padding.
+ nsSize GetImageDestSize(ComputedStyle*, imgIContainer*);
+
+ // Returns the source rectangle of the image to be displayed.
+ nsRect GetImageSourceRect(ComputedStyle*, imgIContainer*);
+
+ // Returns the height of rows in the tree.
+ int32_t GetRowHeight();
+
+ // Returns our indentation width.
+ int32_t GetIndentation();
+
+ // Calculates our width/height once border and padding have been removed.
+ void CalcInnerBox();
+
+ // Calculate the total width of our scrollable portion
+ nscoord CalcHorzWidth(const ScrollParts& aParts);
+
+ // Looks up a ComputedStyle in the style cache. On a cache miss we resolve
+ // the pseudo-styles passed in and place them into the cache.
+ ComputedStyle* GetPseudoComputedStyle(
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement);
+
+ // Retrieves the scrollbars and scrollview relevant to this treebody. We
+ // traverse the frame tree under our base element, in frame order, looking
+ // for the first relevant vertical scrollbar, horizontal scrollbar, and
+ // scrollable frame (with associated content and scrollable view). These
+ // are all volatile and should not be retained.
+ ScrollParts GetScrollParts();
+
+ // Update the curpos of the scrollbar.
+ void UpdateScrollbars(const ScrollParts& aParts);
+
+ // Update the maxpos of the scrollbar.
+ void InvalidateScrollbars(const ScrollParts& aParts,
+ AutoWeakFrame& aWeakColumnsFrame);
+
+ // Check overflow and generate events.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void CheckOverflow(const ScrollParts& aParts);
+
+ // Calls UpdateScrollbars, Invalidate aNeedsFullInvalidation if true,
+ // InvalidateScrollbars and finally CheckOverflow.
+ // returns true if the frame is still alive after the method call.
+ bool FullScrollbarsUpdate(bool aNeedsFullInvalidation);
+
+ // Use to auto-fill some of the common properties without the view having to
+ // do it. Examples include container, open, selected, and focus.
+ void PrefillPropertyArray(int32_t aRowIndex, nsTreeColumn* aCol);
+
+ // Our internal scroll method, used by all the public scroll methods.
+ nsresult ScrollInternal(const ScrollParts& aParts, int32_t aRow);
+ nsresult ScrollToRowInternal(const ScrollParts& aParts, int32_t aRow);
+ nsresult ScrollHorzInternal(const ScrollParts& aParts, int32_t aPosition);
+ nsresult EnsureRowIsVisibleInternal(const ScrollParts& aParts, int32_t aRow);
+
+ // Convert client pixels into appunits in our coordinate space.
+ nsPoint AdjustClientCoordsToBoxCoordSpace(int32_t aX, int32_t aY);
+
+ void EnsureView();
+
+ nsresult GetCellWidth(int32_t aRow, nsTreeColumn* aCol,
+ gfxContext* aRenderingContext, nscoord& aDesiredSize,
+ nscoord& aCurrentSize);
+
+ // Translate the given rect horizontally from tree coordinates into the
+ // coordinate system of our nsTreeBodyFrame. If clip is true, then clip the
+ // rect to its intersection with mInnerBox in the horizontal direction.
+ // Return whether the result has a nonempty intersection with mInnerBox
+ // after projecting both onto the horizontal coordinate axis.
+ bool OffsetForHorzScroll(nsRect& rect, bool clip);
+
+ bool CanAutoScroll(int32_t aRowIndex);
+
+ // Calc the row and above/below/on status given where the mouse currently is
+ // hovering. Also calc if we're in the region in which we want to auto-scroll
+ // the tree. A positive value of |aScrollLines| means scroll down, a negative
+ // value means scroll up, a zero value means that we aren't in drag scroll
+ // region.
+ void ComputeDropPosition(mozilla::WidgetGUIEvent* aEvent, int32_t* aRow,
+ int16_t* aOrient, int16_t* aScrollLines);
+
+ void InvalidateDropFeedback(int32_t aRow, int16_t aOrientation) {
+ InvalidateRow(aRow);
+ if (aOrientation != nsITreeView::DROP_ON)
+ InvalidateRow(aRow + aOrientation);
+ }
+
+ public:
+ /**
+ * Remove an nsITreeImageListener from being tracked by this frame. Only tree
+ * image listeners that are created by this frame are tracked.
+ *
+ * @param aListener A pointer to an nsTreeImageListener to no longer
+ * track.
+ */
+ void RemoveTreeImageListener(nsTreeImageListener* aListener);
+
+ protected:
+ // Create a new timer. This method is used to delay various actions like
+ // opening/closing folders or tree scrolling.
+ // aID is type of the action, aFunc is the function to be called when
+ // the timer fires and aType is type of timer - one shot or repeating.
+ nsresult CreateTimer(const mozilla::LookAndFeel::IntID aID,
+ nsTimerCallbackFunc aFunc, int32_t aType,
+ nsITimer** aTimer, const char* aName);
+
+ static void OpenCallback(nsITimer* aTimer, void* aClosure);
+
+ static void CloseCallback(nsITimer* aTimer, void* aClosure);
+
+ static void LazyScrollCallback(nsITimer* aTimer, void* aClosure);
+
+ static void ScrollCallback(nsITimer* aTimer, void* aClosure);
+
+ class ScrollEvent : public mozilla::Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+ explicit ScrollEvent(nsTreeBodyFrame* aInner)
+ : mozilla::Runnable("nsTreeBodyFrame::ScrollEvent"), mInner(aInner) {}
+ void Revoke() { mInner = nullptr; }
+
+ private:
+ nsTreeBodyFrame* mInner;
+ };
+
+ void PostScrollEvent();
+ MOZ_CAN_RUN_SCRIPT void FireScrollEvent();
+
+ /**
+ * Clear the pointer to this frame for all nsTreeImageListeners that were
+ * created by this frame.
+ */
+ void DetachImageListeners();
+
+#ifdef ACCESSIBILITY
+ /**
+ * Fires 'treeRowCountChanged' event asynchronously. The event is a
+ * CustomEvent that is used to expose the following information structures
+ * via a property bag.
+ *
+ * @param aIndex the row index rows are added/removed from
+ * @param aCount the number of added/removed rows (the sign points to
+ * an operation, plus - addition, minus - removing)
+ */
+ void FireRowCountChangedEvent(int32_t aIndex, int32_t aCount);
+
+ /**
+ * Fires 'treeInvalidated' event asynchronously. The event is a CustomEvent
+ * that is used to expose the information structures described by method
+ * arguments via a property bag.
+ *
+ * @param aStartRow the start index of invalidated rows, -1 means that
+ * columns have been invalidated only
+ * @param aEndRow the end index of invalidated rows, -1 means that columns
+ * have been invalidated only
+ * @param aStartCol the start invalidated column, nullptr means that only
+ * rows have been invalidated
+ * @param aEndCol the end invalidated column, nullptr means that rows have
+ * been invalidated only
+ */
+ void FireInvalidateEvent(int32_t aStartRow, int32_t aEndRow,
+ nsTreeColumn* aStartCol, nsTreeColumn* aEndCol);
+#endif
+
+ protected: // Data Members
+ class Slots {
+ public:
+ Slots() = default;
+
+ ~Slots() {
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ }
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ // If the drop is actually allowed here or not.
+ bool mDropAllowed = false;
+
+ // True while dragging over the tree.
+ bool mIsDragging = false;
+
+ // The row the mouse is hovering over during a drop.
+ int32_t mDropRow = -1;
+
+ // Where we want to draw feedback (above/on this row/below) if allowed.
+ int16_t mDropOrient = -1;
+
+ // Number of lines to be scrolled.
+ int16_t mScrollLines = 0;
+
+ // The drag action that was received for this slot
+ uint32_t mDragAction = 0;
+
+ // Timer for opening/closing spring loaded folders or scrolling the tree.
+ nsCOMPtr<nsITimer> mTimer;
+
+ // An array used to keep track of all spring loaded folders.
+ nsTArray<int32_t> mArray;
+ };
+
+ mozilla::UniquePtr<Slots> mSlots;
+
+ nsRevocableEventPtr<ScrollEvent> mScrollEvent;
+
+ RefPtr<ScrollbarActivity> mScrollbarActivity;
+
+ // The <tree> element containing this treebody.
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+
+ // Cached column information.
+ RefPtr<nsTreeColumns> mColumns;
+
+ // The current view for this tree widget. We get all of our row and cell data
+ // from the view.
+ nsCOMPtr<nsITreeView> mView;
+
+ // A cache of all the ComputedStyles we have seen for rows and cells of the
+ // tree. This is a mapping from a list of atoms to a corresponding
+ // ComputedStyle. This cache stores every combination that occurs in the
+ // tree, so for n distinct properties, this cache could have 2 to the n
+ // entries (the power set of all row properties).
+ nsTreeStyleCache mStyleCache;
+
+ // A hashtable that maps from URLs to image request/listener pairs. The URL
+ // is provided by the view or by the ComputedStyle. The ComputedStyle
+ // represents a resolved :-moz-tree-cell-image (or twisty) pseudo-element.
+ // It maps directly to an imgIRequest.
+ nsTHashMap<nsStringHashKey, nsTreeImageCacheEntry> mImageCache;
+
+ // A scratch array used when looking up cached ComputedStyles.
+ mozilla::AtomArray mScratchArray;
+
+ // The index of the first visible row and the # of rows visible onscreen.
+ // The tree only examines onscreen rows, starting from
+ // this index and going up to index+pageLength.
+ int32_t mTopRowIndex;
+ int32_t mPageLength;
+
+ // The horizontal scroll position
+ nscoord mHorzPosition;
+
+ // The original desired horizontal width before changing it and posting a
+ // reflow callback. In some cases, the desired horizontal width can first be
+ // different from the current desired horizontal width, only to return to
+ // the same value later during the same reflow. In this case, we can cancel
+ // the posted reflow callback and prevent an unnecessary reflow.
+ nscoord mOriginalHorzWidth;
+ // Our desired horizontal width (the width for which we actually have tree
+ // columns).
+ nscoord mHorzWidth;
+ // The amount by which to adjust the width of the last cell.
+ // This depends on whether or not the columnpicker and scrollbars are present.
+ nscoord mAdjustWidth;
+
+ // Our last reflowed rect, used for invalidation, see ManageReflowCallback().
+ Maybe<nsRect> mLastReflowRect;
+
+ // Cached heights and indent info.
+ nsRect mInnerBox; // 4-byte aligned
+ int32_t mRowHeight;
+ int32_t mIndentation;
+
+ int32_t mUpdateBatchNest;
+
+ // Cached row count.
+ int32_t mRowCount;
+
+ // The row the mouse is hovering over.
+ int32_t mMouseOverRow;
+
+ // Whether or not we're currently focused.
+ bool mFocused;
+
+ // Do we have a fixed number of onscreen rows?
+ bool mHasFixedRowCount;
+
+ bool mVerticalOverflow;
+ bool mHorizontalOverflow;
+
+ bool mReflowCallbackPosted;
+
+ // Set while we flush layout to take account of effects of
+ // overflow/underflow event handlers
+ bool mCheckingOverflow;
+
+ // Hash set to keep track of which listeners we created and thus
+ // have pointers to us.
+ nsTHashSet<nsTreeImageListener*> mCreatedListeners;
+
+}; // class nsTreeBodyFrame
+
+#endif
diff --git a/layout/xul/tree/nsTreeColumns.cpp b/layout/xul/tree/nsTreeColumns.cpp
new file mode 100644
index 0000000000..62a8299c6f
--- /dev/null
+++ b/layout/xul/tree/nsTreeColumns.cpp
@@ -0,0 +1,462 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsTreeColumns.h"
+#include "nsTreeUtils.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsContentUtils.h"
+#include "nsTreeBodyFrame.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include "mozilla/dom/TreeColumnsBinding.h"
+#include "mozilla/dom/XULTreeElement.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// Column class that caches all the info about our column.
+nsTreeColumn::nsTreeColumn(nsTreeColumns* aColumns, dom::Element* aElement)
+ : mContent(aElement), mColumns(aColumns), mIndex(0), mPrevious(nullptr) {
+ NS_ASSERTION(aElement && aElement->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL),
+ "nsTreeColumn's content must be a <xul:treecol>");
+
+ Invalidate(IgnoreErrors());
+}
+
+nsTreeColumn::~nsTreeColumn() {
+ if (mNext) {
+ mNext->SetPrevious(nullptr);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTreeColumn)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mContent)
+ if (tmp->mNext) {
+ tmp->mNext->SetPrevious(nullptr);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNext)
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTreeColumn)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNext)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumn)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumn)
+
+// QueryInterface implementation for nsTreeColumn
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumn)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(nsTreeColumn)
+NS_INTERFACE_MAP_END
+
+nsIFrame* nsTreeColumn::GetFrame() { return mContent->GetPrimaryFrame(); }
+
+bool nsTreeColumn::IsLastVisible(nsTreeBodyFrame* aBodyFrame) {
+ NS_ASSERTION(GetFrame(), "should have checked for this already");
+
+ // cyclers are fixed width, don't adjust them
+ if (IsCycler()) return false;
+
+ // we're certainly not the last visible if we're not visible
+ if (GetFrame()->GetRect().width == 0) return false;
+
+ // try to find a visible successor
+ for (nsTreeColumn* next = GetNext(); next; next = next->GetNext()) {
+ nsIFrame* frame = next->GetFrame();
+ if (frame && frame->GetRect().width > 0) return false;
+ }
+ return true;
+}
+
+nsresult nsTreeColumn::GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY,
+ nscoord aHeight, nsRect* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = nsRect();
+ return NS_ERROR_FAILURE;
+ }
+
+ const bool isRTL =
+ aBodyFrame->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ *aResult = frame->GetRect();
+ if (frame->StyleVisibility()->IsCollapse()) {
+ aResult->SizeTo(nsSize());
+ }
+ aResult->y = aY;
+ aResult->height = aHeight;
+ if (isRTL)
+ aResult->x += aBodyFrame->mAdjustWidth;
+ else if (IsLastVisible(aBodyFrame))
+ aResult->width += aBodyFrame->mAdjustWidth;
+ return NS_OK;
+}
+
+nsresult nsTreeColumn::GetXInTwips(nsTreeBodyFrame* aBodyFrame,
+ nscoord* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = 0;
+ return NS_ERROR_FAILURE;
+ }
+ *aResult = frame->GetRect().x;
+ return NS_OK;
+}
+
+nsresult nsTreeColumn::GetWidthInTwips(nsTreeBodyFrame* aBodyFrame,
+ nscoord* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = 0;
+ return NS_ERROR_FAILURE;
+ }
+ *aResult = frame->GetRect().width;
+ if (IsLastVisible(aBodyFrame)) *aResult += aBodyFrame->mAdjustWidth;
+ return NS_OK;
+}
+
+void nsTreeColumn::GetId(nsAString& aId) const { aId = GetId(); }
+
+void nsTreeColumn::Invalidate(ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Fetch the Id.
+ mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, mId);
+
+ // If we have an Id, cache the Id as an atom.
+ if (!mId.IsEmpty()) {
+ mAtom = NS_Atomize(mId);
+ }
+
+ // Cache our index.
+ nsTreeUtils::GetColumnIndex(mContent, &mIndex);
+
+ const nsStyleVisibility* vis = frame->StyleVisibility();
+
+ // Cache our text alignment policy.
+ const nsStyleText* textStyle = frame->StyleText();
+
+ mTextAlignment = textStyle->mTextAlign;
+ // START or END alignment sometimes means RIGHT
+ if ((mTextAlignment == StyleTextAlign::Start &&
+ vis->mDirection == StyleDirection::Rtl) ||
+ (mTextAlignment == StyleTextAlign::End &&
+ vis->mDirection == StyleDirection::Ltr)) {
+ mTextAlignment = StyleTextAlign::Right;
+ } else if (mTextAlignment == StyleTextAlign::Start ||
+ mTextAlignment == StyleTextAlign::End) {
+ mTextAlignment = StyleTextAlign::Left;
+ }
+
+ // Figure out if we're the primary column (that has to have indentation
+ // and twisties drawn.
+ mIsPrimary = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::primary,
+ nsGkAtoms::_true, eCaseMatters);
+
+ // Figure out if we're a cycling column (one that doesn't cause a selection
+ // to happen).
+ mIsCycler = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::cycler,
+ nsGkAtoms::_true, eCaseMatters);
+
+ mIsEditable = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_true, eCaseMatters);
+
+ mOverflow = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::overflow,
+ nsGkAtoms::_true, eCaseMatters);
+
+ // Figure out our column type. Default type is text.
+ mType = TreeColumn_Binding::TYPE_TEXT;
+ static Element::AttrValuesArray typestrings[] = {nsGkAtoms::checkbox,
+ nullptr};
+ switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
+ typestrings, eCaseMatters)) {
+ case 0:
+ mType = TreeColumn_Binding::TYPE_CHECKBOX;
+ break;
+ }
+
+ // Fetch the crop style.
+ mCropStyle = 0;
+ static Element::AttrValuesArray cropstrings[] = {
+ nsGkAtoms::center, nsGkAtoms::left, nsGkAtoms::start, nullptr};
+ switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::crop,
+ cropstrings, eCaseMatters)) {
+ case 0:
+ mCropStyle = 1;
+ break;
+ case 1:
+ case 2:
+ mCropStyle = 2;
+ break;
+ }
+}
+
+nsIContent* nsTreeColumn::GetParentObject() const { return mContent; }
+
+/* virtual */
+JSObject* nsTreeColumn::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::TreeColumn_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+Element* nsTreeColumn::Element() { return mContent; }
+
+int32_t nsTreeColumn::GetX(mozilla::ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().x);
+}
+
+int32_t nsTreeColumn::GetWidth(mozilla::ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().width);
+}
+
+already_AddRefed<nsTreeColumn> nsTreeColumn::GetPreviousColumn() {
+ return do_AddRef(mPrevious);
+}
+
+nsTreeColumns::nsTreeColumns(nsTreeBodyFrame* aTree) : mTree(aTree) {}
+
+nsTreeColumns::~nsTreeColumns() { nsTreeColumns::InvalidateColumns(); }
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(nsTreeColumns)
+
+// QueryInterface implementation for nsTreeColumns
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumns)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumns)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumns)
+
+nsIContent* nsTreeColumns::GetParentObject() const {
+ return mTree ? mTree->GetBaseElement() : nullptr;
+}
+
+/* virtual */
+JSObject* nsTreeColumns::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::TreeColumns_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+XULTreeElement* nsTreeColumns::GetTree() const {
+ if (!mTree) {
+ return nullptr;
+ }
+
+ return XULTreeElement::FromNodeOrNull(mTree->GetBaseElement());
+}
+
+uint32_t nsTreeColumns::Count() {
+ EnsureColumns();
+ uint32_t count = 0;
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ ++count;
+ }
+ return count;
+}
+
+nsTreeColumn* nsTreeColumns::GetLastColumn() {
+ EnsureColumns();
+ nsTreeColumn* currCol = mFirstColumn;
+ while (currCol) {
+ nsTreeColumn* next = currCol->GetNext();
+ if (!next) {
+ return currCol;
+ }
+ currCol = next;
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetSortedColumn() {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None,
+ nsGkAtoms::sortDirection)) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetKeyColumn() {
+ EnsureColumns();
+
+ nsTreeColumn* first = nullptr;
+ nsTreeColumn* primary = nullptr;
+ nsTreeColumn* sorted = nullptr;
+
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ // Skip hidden columns.
+ if (currCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ continue;
+
+ // Skip non-text column
+ if (currCol->GetType() != TreeColumn_Binding::TYPE_TEXT) continue;
+
+ if (!first) first = currCol;
+
+ if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None,
+ nsGkAtoms::sortDirection)) {
+ // Use sorted column as the key.
+ sorted = currCol;
+ break;
+ }
+
+ if (currCol->IsPrimary())
+ if (!primary) primary = currCol;
+ }
+
+ if (sorted) return sorted;
+ if (primary) return primary;
+ return first;
+}
+
+nsTreeColumn* nsTreeColumns::GetColumnFor(dom::Element* aElement) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->mContent == aElement) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::NamedGetter(const nsAString& aId, bool& aFound) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->GetId().Equals(aId)) {
+ aFound = true;
+ return currCol;
+ }
+ }
+ aFound = false;
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetNamedColumn(const nsAString& aId) {
+ bool dummy;
+ return NamedGetter(aId, dummy);
+}
+
+void nsTreeColumns::GetSupportedNames(nsTArray<nsString>& aNames) {
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ aNames.AppendElement(currCol->GetId());
+ }
+}
+
+nsTreeColumn* nsTreeColumns::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->GetIndex() == static_cast<int32_t>(aIndex)) {
+ aFound = true;
+ return currCol;
+ }
+ }
+ aFound = false;
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetColumnAt(uint32_t aIndex) {
+ bool dummy;
+ return IndexedGetter(aIndex, dummy);
+}
+
+void nsTreeColumns::InvalidateColumns() {
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ currCol->SetColumns(nullptr);
+ }
+ mFirstColumn = nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetPrimaryColumn() {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->IsPrimary()) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+void nsTreeColumns::EnsureColumns() {
+ if (mTree && !mFirstColumn) {
+ nsIContent* treeContent = mTree->GetBaseElement();
+ if (!treeContent) return;
+
+ nsIContent* colsContent =
+ nsTreeUtils::GetDescendantChild(treeContent, nsGkAtoms::treecols);
+ if (!colsContent) return;
+
+ nsIContent* colContent =
+ nsTreeUtils::GetDescendantChild(colsContent, nsGkAtoms::treecol);
+ if (!colContent) return;
+
+ nsIFrame* colFrame = colContent->GetPrimaryFrame();
+ if (!colFrame) return;
+
+ colFrame = colFrame->GetParent();
+ if (!colFrame || !colFrame->GetContent()) return;
+
+ nsTreeColumn* currCol = nullptr;
+
+ // Enumerate the columns in visible order
+ CSSOrderAwareFrameIterator iter(
+ colFrame, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll);
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* colFrame = iter.get();
+ nsIContent* colContent = colFrame->GetContent();
+ if (!colContent->IsXULElement(nsGkAtoms::treecol)) {
+ continue;
+ }
+ // Create a new column structure.
+ nsTreeColumn* col = new nsTreeColumn(this, colContent->AsElement());
+
+ if (currCol) {
+ currCol->SetNext(col);
+ col->SetPrevious(currCol);
+ } else {
+ mFirstColumn = col;
+ }
+ currCol = col;
+ }
+ }
+}
diff --git a/layout/xul/tree/nsTreeColumns.h b/layout/xul/tree/nsTreeColumns.h
new file mode 100644
index 0000000000..677206a89d
--- /dev/null
+++ b/layout/xul/tree/nsTreeColumns.h
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeColumns_h__
+#define nsTreeColumns_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsCoord.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsQueryObject.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsAtom;
+class nsTreeBodyFrame;
+class nsTreeColumns;
+class nsIFrame;
+class nsIContent;
+struct nsRect;
+
+namespace mozilla {
+enum class StyleTextAlignKeyword : uint8_t;
+using StyleTextAlign = StyleTextAlignKeyword;
+class ErrorResult;
+namespace dom {
+class Element;
+class XULTreeElement;
+} // namespace dom
+} // namespace mozilla
+
+#define NS_TREECOLUMN_IMPL_CID \
+ { /* 02cd1963-4b5d-4a6c-9223-814d3ade93a3 */ \
+ 0x02cd1963, 0x4b5d, 0x4a6c, { \
+ 0x92, 0x23, 0x81, 0x4d, 0x3a, 0xde, 0x93, 0xa3 \
+ } \
+ }
+
+// This class is our column info. We use it to iterate our columns and to
+// obtain information about each column.
+class nsTreeColumn final : public nsISupports, public nsWrapperCache {
+ public:
+ nsTreeColumn(nsTreeColumns* aColumns, mozilla::dom::Element* aElement);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_TREECOLUMN_IMPL_CID)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn)
+
+ // WebIDL
+ nsIContent* GetParentObject() const;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ mozilla::dom::Element* Element();
+
+ nsTreeColumns* GetColumns() const { return mColumns; }
+
+ int32_t GetX(mozilla::ErrorResult& aRv);
+ int32_t GetWidth(mozilla::ErrorResult& aRv);
+
+ void GetId(nsAString& aId) const;
+ int32_t Index() const { return mIndex; }
+
+ bool Primary() const { return mIsPrimary; }
+ bool Cycler() const { return mIsCycler; }
+ bool Editable() const { return mIsEditable; }
+ int16_t Type() const { return mType; }
+
+ nsTreeColumn* GetNext() const { return mNext; }
+ nsTreeColumn* GetPrevious() const { return mPrevious; }
+
+ already_AddRefed<nsTreeColumn> GetPreviousColumn();
+
+ void Invalidate(mozilla::ErrorResult& aRv);
+
+ friend class nsTreeBodyFrame;
+ friend class nsTreeColumns;
+
+ protected:
+ ~nsTreeColumn();
+ nsIFrame* GetFrame();
+ nsIFrame* GetFrame(nsTreeBodyFrame* aBodyFrame);
+ // Don't call this if GetWidthInTwips or GetRect fails
+ bool IsLastVisible(nsTreeBodyFrame* aBodyFrame);
+
+ /**
+ * Returns a rect with x and width taken from the frame's rect and specified
+ * y and height. May fail in case there's no frame for the column.
+ */
+ nsresult GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, nscoord aHeight,
+ nsRect* aResult);
+
+ nsresult GetXInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult);
+ nsresult GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult);
+
+ void SetColumns(nsTreeColumns* aColumns) { mColumns = aColumns; }
+
+ public:
+ const nsAString& GetId() const { return mId; }
+ nsAtom* GetAtom() { return mAtom; }
+ int32_t GetIndex() { return mIndex; }
+
+ protected:
+ bool IsPrimary() { return mIsPrimary; }
+ bool IsCycler() { return mIsCycler; }
+ bool IsEditable() { return mIsEditable; }
+ bool Overflow() { return mOverflow; }
+
+ int16_t GetType() { return mType; }
+
+ int8_t GetCropStyle() { return mCropStyle; }
+ mozilla::StyleTextAlign GetTextAlignment() { return mTextAlignment; }
+
+ void SetNext(nsTreeColumn* aNext) {
+ NS_ASSERTION(!mNext, "already have a next sibling");
+ mNext = aNext;
+ }
+ void SetPrevious(nsTreeColumn* aPrevious) { mPrevious = aPrevious; }
+
+ private:
+ /**
+ * Non-null nsIContent for the associated <treecol> element.
+ */
+ RefPtr<mozilla::dom::Element> mContent;
+
+ nsTreeColumns* mColumns;
+
+ nsString mId;
+ RefPtr<nsAtom> mAtom;
+
+ int32_t mIndex;
+
+ bool mIsPrimary;
+ bool mIsCycler;
+ bool mIsEditable;
+ bool mOverflow;
+
+ int16_t mType;
+
+ int8_t mCropStyle;
+ mozilla::StyleTextAlign mTextAlignment;
+
+ RefPtr<nsTreeColumn> mNext;
+ nsTreeColumn* mPrevious;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsTreeColumn, NS_TREECOLUMN_IMPL_CID)
+
+class nsTreeColumns final : public nsISupports, public nsWrapperCache {
+ private:
+ ~nsTreeColumns();
+
+ public:
+ explicit nsTreeColumns(nsTreeBodyFrame* aTree);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumns)
+
+ nsIContent* GetParentObject() const;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ mozilla::dom::XULTreeElement* GetTree() const;
+ uint32_t Count();
+ uint32_t Length() { return Count(); }
+
+ nsTreeColumn* GetFirstColumn() {
+ EnsureColumns();
+ return mFirstColumn;
+ }
+ nsTreeColumn* GetLastColumn();
+
+ nsTreeColumn* GetPrimaryColumn();
+ nsTreeColumn* GetSortedColumn();
+ nsTreeColumn* GetKeyColumn();
+
+ nsTreeColumn* GetColumnFor(mozilla::dom::Element* aElement);
+
+ nsTreeColumn* IndexedGetter(uint32_t aIndex, bool& aFound);
+ nsTreeColumn* GetColumnAt(uint32_t aIndex);
+ nsTreeColumn* NamedGetter(const nsAString& aId, bool& aFound);
+ nsTreeColumn* GetNamedColumn(const nsAString& aId);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ void InvalidateColumns();
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ void SetTree(nsTreeBodyFrame* aTree) { mTree = aTree; }
+
+ // Builds our cache of column info.
+ void EnsureColumns();
+
+ private:
+ nsTreeBodyFrame* mTree;
+
+ /**
+ * The first column in the list of columns. All of the columns are supposed
+ * to be "alive", i.e. have a frame. This is achieved by clearing the columns
+ * list each time a treecol changes size.
+ *
+ * XXX this means that new nsTreeColumn objects are unnecessarily created
+ * for untouched columns.
+ */
+ RefPtr<nsTreeColumn> mFirstColumn;
+};
+
+#endif // nsTreeColumns_h__
diff --git a/layout/xul/tree/nsTreeContentView.cpp b/layout/xul/tree/nsTreeContentView.cpp
new file mode 100644
index 0000000000..edb5368e2a
--- /dev/null
+++ b/layout/xul/tree/nsTreeContentView.cpp
@@ -0,0 +1,1269 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsTreeUtils.h"
+#include "nsTreeContentView.h"
+#include "ChildIterator.h"
+#include "nsError.h"
+#include "nsXULSortService.h"
+#include "nsTreeBodyFrame.h"
+#include "nsTreeColumns.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/TreeContentViewBinding.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// A content model view implementation for the tree.
+
+#define ROW_FLAG_CONTAINER 0x01
+#define ROW_FLAG_OPEN 0x02
+#define ROW_FLAG_EMPTY 0x04
+#define ROW_FLAG_SEPARATOR 0x08
+
+class Row {
+ public:
+ Row(Element* aContent, int32_t aParentIndex)
+ : mContent(aContent),
+ mParentIndex(aParentIndex),
+ mSubtreeSize(0),
+ mFlags(0) {}
+
+ ~Row() = default;
+
+ void SetContainer(bool aContainer) {
+ aContainer ? mFlags |= ROW_FLAG_CONTAINER : mFlags &= ~ROW_FLAG_CONTAINER;
+ }
+ bool IsContainer() { return mFlags & ROW_FLAG_CONTAINER; }
+
+ void SetOpen(bool aOpen) {
+ aOpen ? mFlags |= ROW_FLAG_OPEN : mFlags &= ~ROW_FLAG_OPEN;
+ }
+ bool IsOpen() { return !!(mFlags & ROW_FLAG_OPEN); }
+
+ void SetEmpty(bool aEmpty) {
+ aEmpty ? mFlags |= ROW_FLAG_EMPTY : mFlags &= ~ROW_FLAG_EMPTY;
+ }
+ bool IsEmpty() { return !!(mFlags & ROW_FLAG_EMPTY); }
+
+ void SetSeparator(bool aSeparator) {
+ aSeparator ? mFlags |= ROW_FLAG_SEPARATOR : mFlags &= ~ROW_FLAG_SEPARATOR;
+ }
+ bool IsSeparator() { return !!(mFlags & ROW_FLAG_SEPARATOR); }
+
+ // Weak reference to a content item.
+ Element* mContent;
+
+ // The parent index of the item, set to -1 for the top level items.
+ int32_t mParentIndex;
+
+ // Subtree size for this item.
+ int32_t mSubtreeSize;
+
+ private:
+ // State flags
+ int8_t mFlags;
+};
+
+// We don't reference count the reference to the document
+// If the document goes away first, we'll be informed and we
+// can drop our reference.
+// If we go away first, we'll get rid of ourselves from the
+// document's observer list.
+
+nsTreeContentView::nsTreeContentView(void)
+ : mTree(nullptr), mSelection(nullptr), mDocument(nullptr) {}
+
+nsTreeContentView::~nsTreeContentView(void) {
+ // Remove ourselves from mDocument's observers.
+ if (mDocument) mDocument->RemoveObserver(this);
+}
+
+nsresult NS_NewTreeContentView(nsITreeView** aResult) {
+ *aResult = new nsTreeContentView;
+ if (!*aResult) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsTreeContentView, mTree, mSelection,
+ mBody)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeContentView)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeContentView)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeContentView)
+ NS_INTERFACE_MAP_ENTRY(nsITreeView)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITreeView)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+NS_INTERFACE_MAP_END
+
+JSObject* nsTreeContentView::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TreeContentView_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* nsTreeContentView::GetParentObject() { return mTree; }
+
+NS_IMETHODIMP
+nsTreeContentView::GetRowCount(int32_t* aRowCount) {
+ *aRowCount = mRows.Length();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetSelection(nsITreeSelection** aSelection) {
+ NS_IF_ADDREF(*aSelection = GetSelection());
+
+ return NS_OK;
+}
+
+bool nsTreeContentView::CanTrustTreeSelection(nsISupports* aValue) {
+ // Untrusted content is only allowed to specify known-good views
+ if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) return true;
+ nsCOMPtr<nsINativeTreeSelection> nativeTreeSel = do_QueryInterface(aValue);
+ return nativeTreeSel && NS_SUCCEEDED(nativeTreeSel->EnsureNative());
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetSelection(nsITreeSelection* aSelection) {
+ ErrorResult rv;
+ SetSelection(aSelection, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetSelection(nsITreeSelection* aSelection,
+ ErrorResult& aError) {
+ if (aSelection && !CanTrustTreeSelection(aSelection)) {
+ aError.ThrowSecurityError("Not allowed to set tree selection");
+ return;
+ }
+
+ mSelection = aSelection;
+}
+
+void nsTreeContentView::GetRowProperties(int32_t aRow, nsAString& aProperties,
+ ErrorResult& aError) {
+ aProperties.Truncate();
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+ nsIContent* realRow;
+ if (row->IsSeparator())
+ realRow = row->mContent;
+ else
+ realRow = nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+
+ if (realRow && realRow->IsElement()) {
+ realRow->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::properties,
+ aProperties);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetRowProperties(int32_t aIndex, nsAString& aProps) {
+ ErrorResult rv;
+ GetRowProperties(aIndex, aProps, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aProperties,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) {
+ cell->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, aProperties);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProps) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellProperties(aRow, *aCol, aProps, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetColumnProperties(nsTreeColumn& aColumn,
+ nsAString& aProperties) {
+ RefPtr<Element> element = aColumn.Element();
+
+ if (element) {
+ element->GetAttr(nsGkAtoms::properties, aProperties);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetColumnProperties(nsTreeColumn* aCol, nsAString& aProps) {
+ NS_ENSURE_ARG(aCol);
+
+ GetColumnProperties(*aCol, aProps);
+ return NS_OK;
+}
+
+bool nsTreeContentView::IsContainer(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsContainer();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainer(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainer(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsContainerOpen(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsOpen();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainerOpen(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainerOpen(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsContainerEmpty(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsEmpty();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainerEmpty(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainerEmpty(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsSeparator(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsSeparator();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsSeparator(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsSeparator(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsSorted(bool* _retval) {
+ *_retval = IsSorted();
+
+ return NS_OK;
+}
+
+bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ }
+ return false;
+}
+
+bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer,
+ ErrorResult& aError) {
+ return CanDrop(aRow, aOrientation, aError);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::CanDrop(int32_t aIndex, int32_t aOrientation,
+ DataTransfer* aDataTransfer, bool* _retval) {
+ ErrorResult rv;
+ *_retval = CanDrop(aIndex, aOrientation, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ }
+}
+
+void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer, ErrorResult& aError) {
+ Drop(aRow, aOrientation, aError);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer) {
+ ErrorResult rv;
+ Drop(aRow, aOrientation, rv);
+ return rv.StealNSResult();
+}
+
+int32_t nsTreeContentView::GetParentIndex(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return 0;
+ }
+
+ return mRows[aRow]->mParentIndex;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetParentIndex(int32_t aRowIndex, int32_t* _retval) {
+ ErrorResult rv;
+ *_retval = GetParentIndex(aRowIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::HasNextSibling(int32_t aRow, int32_t aAfterIndex,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ // We have a next sibling if the row is not the last in the subtree.
+ int32_t parentIndex = mRows[aRow]->mParentIndex;
+ if (parentIndex < 0) {
+ return uint32_t(aRow) < mRows.Length() - 1;
+ }
+
+ // Compute the last index in this subtree.
+ int32_t lastIndex = parentIndex + (mRows[parentIndex])->mSubtreeSize;
+ Row* row = mRows[lastIndex].get();
+ while (row->mParentIndex != parentIndex) {
+ lastIndex = row->mParentIndex;
+ row = mRows[lastIndex].get();
+ }
+
+ return aRow < lastIndex;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* _retval) {
+ ErrorResult rv;
+ *_retval = HasNextSibling(aRowIndex, aAfterIndex, rv);
+ return rv.StealNSResult();
+}
+
+int32_t nsTreeContentView::GetLevel(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return 0;
+ }
+
+ int32_t level = 0;
+ Row* row = mRows[aRow].get();
+ while (row->mParentIndex >= 0) {
+ level++;
+ row = mRows[row->mParentIndex].get();
+ }
+ return level;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetLevel(int32_t aIndex, int32_t* _retval) {
+ ErrorResult rv;
+ *_retval = GetLevel(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aSrc, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::src, aSrc);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetImageSrc(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aValue, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellValue(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aText, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ // Check for a "label" attribute - this is valid on an <treeitem>
+ // with a single implied column.
+ if (row->mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText) &&
+ !aText.IsEmpty()) {
+ return;
+ }
+
+ if (row->mContent->IsXULElement(nsGkAtoms::treeitem)) {
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellText(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetTree(XULTreeElement* aTree, ErrorResult& aError) {
+ aError = SetTree(aTree);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetTree(XULTreeElement* aTree) {
+ ClearRows();
+
+ mTree = aTree;
+
+ if (aTree) {
+ // Add ourselves to document's observers.
+ Document* document = mTree->GetComposedDoc();
+ if (document) {
+ document->AddObserver(this);
+ mDocument = document;
+ }
+
+ RefPtr<dom::Element> bodyElement = mTree->GetTreeBody();
+ if (bodyElement) {
+ mBody = std::move(bodyElement);
+ int32_t index = 0;
+ Serialize(mBody, -1, &index, mRows);
+ }
+ }
+
+ return NS_OK;
+}
+
+void nsTreeContentView::ToggleOpenState(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ // We don't serialize content right here, since content might be generated
+ // lazily.
+ Row* row = mRows[aRow].get();
+
+ if (row->IsOpen())
+ row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"false"_ns,
+ true);
+ else
+ row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns,
+ true);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::ToggleOpenState(int32_t aIndex) {
+ ErrorResult rv;
+ ToggleOpenState(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::CycleHeader(nsTreeColumn& aColumn,
+ ErrorResult& aError) {
+ if (!mTree) return;
+
+ RefPtr<Element> column = aColumn.Element();
+ nsAutoString sort;
+ column->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, sort);
+ if (!sort.IsEmpty()) {
+ nsAutoString sortdirection;
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::ascending, nsGkAtoms::descending, nullptr};
+ switch (column->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sortDirection,
+ strings, eCaseMatters)) {
+ case 0:
+ sortdirection.AssignLiteral("descending");
+ break;
+ case 1:
+ sortdirection.AssignLiteral("natural");
+ break;
+ default:
+ sortdirection.AssignLiteral("ascending");
+ break;
+ }
+
+ nsAutoString hints;
+ column->GetAttr(kNameSpaceID_None, nsGkAtoms::sorthints, hints);
+ sortdirection.Append(' ');
+ sortdirection += hints;
+
+ XULWidgetSort(mTree, sort, sortdirection);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::CycleHeader(nsTreeColumn* aCol) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ CycleHeader(*aCol, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SelectionChangedXPCOM() { return NS_OK; }
+
+NS_IMETHODIMP
+nsTreeContentView::CycleCell(int32_t aRow, nsTreeColumn* aCol) { return NS_OK; }
+
+bool nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn& aColumn,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell && cell->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn* aCol, bool* _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ *_retval = IsEditable(aRow, *aCol, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aValue) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ SetCellValue(aRow, *aCol, aValue, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::label, aValue, true);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aValue) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ SetCellText(aRow, *aCol, aValue, rv);
+ return rv.StealNSResult();
+}
+
+Element* nsTreeContentView::GetItemAtIndex(int32_t aIndex,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aIndex)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return nullptr;
+ }
+
+ return mRows[aIndex]->mContent;
+}
+
+int32_t nsTreeContentView::GetIndexOfItem(Element* aItem) {
+ return FindContent(aItem);
+}
+
+void nsTreeContentView::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+ if (aElement == mTree || aElement == mBody) {
+ mTree->ClearStyleAndImageCaches();
+ mTree->Invalidate();
+ }
+
+ // We don't consider non-XUL nodes.
+ nsIContent* parent = nullptr;
+ if (!aElement->IsXULElement() ||
+ ((parent = aElement->GetParent()) && !parent->IsXULElement())) {
+ return;
+ }
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::treecol, nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator,
+ nsGkAtoms::treerow, nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = aElement; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Handle changes of the hidden attribute.
+ if (aAttribute == nsGkAtoms::hidden &&
+ aElement->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ bool hidden = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters);
+
+ int32_t index = FindContent(aElement);
+ if (hidden && index >= 0) {
+ // Hide this row along with its children.
+ int32_t count = RemoveRow(index);
+ if (mTree) mTree->RowCountChanged(index, -count);
+ } else if (!hidden && index < 0) {
+ // Show this row along with its children.
+ nsCOMPtr<nsIContent> parent = aElement->GetParent();
+ if (parent) {
+ InsertRowFor(parent, aElement);
+ }
+ }
+
+ return;
+ }
+
+ if (aElement->IsXULElement(nsGkAtoms::treecol)) {
+ if (aAttribute == nsGkAtoms::properties) {
+ if (mTree) {
+ RefPtr<nsTreeColumns> cols = mTree->GetColumns();
+ if (cols) {
+ RefPtr<nsTreeColumn> col = cols->GetColumnFor(aElement);
+ mTree->InvalidateColumn(col);
+ }
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treeitem)) {
+ int32_t index = FindContent(aElement);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ if (aAttribute == nsGkAtoms::container) {
+ bool isContainer =
+ aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters);
+ row->SetContainer(isContainer);
+ if (mTree) mTree->InvalidateRow(index);
+ } else if (aAttribute == nsGkAtoms::open) {
+ bool isOpen = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
+ nsGkAtoms::_true, eCaseMatters);
+ bool wasOpen = row->IsOpen();
+ if (!isOpen && wasOpen)
+ CloseContainer(index);
+ else if (isOpen && !wasOpen)
+ OpenContainer(index);
+ } else if (aAttribute == nsGkAtoms::empty) {
+ bool isEmpty =
+ aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
+ nsGkAtoms::_true, eCaseMatters);
+ row->SetEmpty(isEmpty);
+ if (mTree) mTree->InvalidateRow(index);
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treeseparator)) {
+ int32_t index = FindContent(aElement);
+ if (index >= 0) {
+ if (aAttribute == nsGkAtoms::properties && mTree) {
+ mTree->InvalidateRow(index);
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treerow)) {
+ if (aAttribute == nsGkAtoms::properties) {
+ nsCOMPtr<nsIContent> parent = aElement->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) {
+ mTree->InvalidateRow(index);
+ }
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treecell)) {
+ if (aAttribute == nsGkAtoms::properties || aAttribute == nsGkAtoms::mode ||
+ aAttribute == nsGkAtoms::src || aAttribute == nsGkAtoms::value ||
+ aAttribute == nsGkAtoms::label) {
+ nsIContent* parent = aElement->GetParent();
+ if (parent) {
+ nsCOMPtr<nsIContent> grandParent = parent->GetParent();
+ if (grandParent && grandParent->IsXULElement()) {
+ int32_t index = FindContent(grandParent);
+ if (index >= 0 && mTree) {
+ // XXX Should we make an effort to invalidate only cell ?
+ mTree->InvalidateRow(index);
+ }
+ }
+ }
+ }
+ }
+}
+
+void nsTreeContentView::ContentAppended(nsIContent* aFirstNewContent) {
+ for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
+ // Our contentinserted doesn't use the index
+ ContentInserted(cur);
+ }
+}
+
+void nsTreeContentView::ContentInserted(nsIContent* aChild) {
+ NS_ASSERTION(aChild, "null ptr");
+ nsIContent* container = aChild->GetParent();
+
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+
+ // Don't allow non-XUL nodes.
+ if (!aChild->IsXULElement() || !container->IsXULElement()) return;
+
+ if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
+ nsGkAtoms::treechildren, nsGkAtoms::treerow,
+ nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = container; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
+ int32_t index = FindContent(container);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ row->SetEmpty(false);
+ if (mTree) mTree->InvalidateRow(index);
+ if (row->IsContainer() && row->IsOpen()) {
+ int32_t count = EnsureSubtree(index);
+ if (mTree) mTree->RowCountChanged(index + 1, count);
+ }
+ }
+ } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ InsertRowFor(container, aChild);
+ } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
+ int32_t index = FindContent(container);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ }
+ }
+}
+
+void nsTreeContentView::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ NS_ASSERTION(aChild, "null ptr");
+
+ nsIContent* container = aChild->GetParent();
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+
+ // We don't consider non-XUL nodes.
+ if (!aChild->IsXULElement() || !container->IsXULElement()) return;
+
+ if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
+ nsGkAtoms::treechildren, nsGkAtoms::treerow,
+ nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = container; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
+ int32_t index = FindContent(container);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ row->SetEmpty(true);
+ int32_t count = RemoveSubtree(index);
+ // Invalidate also the row to update twisty.
+ if (mTree) {
+ mTree->InvalidateRow(index);
+ mTree->RowCountChanged(index + 1, -count);
+ }
+ }
+ } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ int32_t index = FindContent(aChild);
+ if (index >= 0) {
+ int32_t count = RemoveRow(index);
+ if (mTree) mTree->RowCountChanged(index, -count);
+ }
+ } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
+ int32_t index = FindContent(container);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ }
+ }
+}
+
+void nsTreeContentView::NodeWillBeDestroyed(nsINode* aNode) {
+ // XXXbz do we need this strong ref? Do we drop refs to self in ClearRows?
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ ClearRows();
+}
+
+// Recursively serialize content, starting with aContent.
+void nsTreeContentView::Serialize(nsIContent* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ // Don't allow non-XUL nodes.
+ if (!aContent->IsXULElement()) return;
+
+ dom::FlattenedChildIterator iter(aContent);
+ for (nsIContent* content = iter.GetNextChild(); content;
+ content = iter.GetNextChild()) {
+ int32_t count = aRows.Length();
+
+ if (content->IsXULElement(nsGkAtoms::treeitem)) {
+ SerializeItem(content->AsElement(), aParentIndex, aIndex, aRows);
+ } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
+ SerializeSeparator(content->AsElement(), aParentIndex, aIndex, aRows);
+ }
+
+ *aIndex += aRows.Length() - count;
+ }
+}
+
+void nsTreeContentView::SerializeItem(Element* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ return;
+
+ aRows.AppendElement(MakeUnique<Row>(aContent, aParentIndex));
+ Row* row = aRows.LastElement().get();
+
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetContainer(true);
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetOpen(true);
+ nsIContent* child =
+ nsTreeUtils::GetImmediateChild(aContent, nsGkAtoms::treechildren);
+ if (child && child->IsXULElement()) {
+ // Now, recursively serialize our child.
+ int32_t count = aRows.Length();
+ int32_t index = 0;
+ Serialize(child, aParentIndex + *aIndex + 1, &index, aRows);
+ row->mSubtreeSize += aRows.Length() - count;
+ } else
+ row->SetEmpty(true);
+ } else if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetEmpty(true);
+ }
+ }
+}
+
+void nsTreeContentView::SerializeSeparator(Element* aContent,
+ int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ return;
+
+ auto row = MakeUnique<Row>(aContent, aParentIndex);
+ row->SetSeparator(true);
+ aRows.AppendElement(std::move(row));
+}
+
+void nsTreeContentView::GetIndexInSubtree(nsIContent* aContainer,
+ nsIContent* aContent,
+ int32_t* aIndex) {
+ if (!aContainer->IsXULElement()) return;
+
+ for (nsIContent* content = aContainer->GetFirstChild(); content;
+ content = content->GetNextSibling()) {
+ if (content == aContent) break;
+
+ if (content->IsXULElement(nsGkAtoms::treeitem)) {
+ if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters)) {
+ (*aIndex)++;
+ if (content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters) &&
+ content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::open, nsGkAtoms::_true,
+ eCaseMatters)) {
+ nsIContent* child =
+ nsTreeUtils::GetImmediateChild(content, nsGkAtoms::treechildren);
+ if (child && child->IsXULElement())
+ GetIndexInSubtree(child, aContent, aIndex);
+ }
+ }
+ } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
+ if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ (*aIndex)++;
+ }
+ }
+}
+
+int32_t nsTreeContentView::EnsureSubtree(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+
+ nsIContent* child;
+ child =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treechildren);
+ if (!child || !child->IsXULElement()) {
+ return 0;
+ }
+
+ AutoTArray<UniquePtr<Row>, 8> rows;
+ int32_t index = 0;
+ Serialize(child, aIndex, &index, rows);
+ // Insert |rows| into |mRows| at position |aIndex|, by first creating empty
+ // UniquePtr entries and then Move'ing |rows|'s entries into them. (Note
+ // that we can't simply use InsertElementsAt with an array argument, since
+ // the destination can't steal ownership from its const source argument.)
+ UniquePtr<Row>* newRows = mRows.InsertElementsAt(aIndex + 1, rows.Length());
+ for (nsTArray<Row>::index_type i = 0; i < rows.Length(); i++) {
+ newRows[i] = std::move(rows[i]);
+ }
+ int32_t count = rows.Length();
+
+ row->mSubtreeSize += count;
+ UpdateSubtreeSizes(row->mParentIndex, count);
+
+ // Update parent indexes, but skip newly added rows.
+ // They already have correct values.
+ UpdateParentIndexes(aIndex, count + 1, count);
+
+ return count;
+}
+
+int32_t nsTreeContentView::RemoveSubtree(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ int32_t count = row->mSubtreeSize;
+
+ mRows.RemoveElementsAt(aIndex + 1, count);
+
+ row->mSubtreeSize -= count;
+ UpdateSubtreeSizes(row->mParentIndex, -count);
+
+ UpdateParentIndexes(aIndex, 0, -count);
+
+ return count;
+}
+
+void nsTreeContentView::InsertRowFor(nsIContent* aParent, nsIContent* aChild) {
+ int32_t grandParentIndex = -1;
+ bool insertRow = false;
+
+ nsCOMPtr<nsIContent> grandParent = aParent->GetParent();
+
+ if (grandParent->IsXULElement(nsGkAtoms::tree)) {
+ // Allow insertion to the outermost container.
+ insertRow = true;
+ } else {
+ // Test insertion to an inner container.
+
+ // First try to find this parent in our array of rows, if we find one
+ // we can be sure that all other parents are open too.
+ grandParentIndex = FindContent(grandParent);
+ if (grandParentIndex >= 0) {
+ // Got it, now test if it is open.
+ if (mRows[grandParentIndex]->IsOpen()) insertRow = true;
+ }
+ }
+
+ if (insertRow) {
+ int32_t index = 0;
+ GetIndexInSubtree(aParent, aChild, &index);
+
+ int32_t count = InsertRow(grandParentIndex, index, aChild);
+ if (mTree) mTree->RowCountChanged(grandParentIndex + index + 1, count);
+ }
+}
+
+int32_t nsTreeContentView::InsertRow(int32_t aParentIndex, int32_t aIndex,
+ nsIContent* aContent) {
+ AutoTArray<UniquePtr<Row>, 8> rows;
+ if (aContent->IsXULElement(nsGkAtoms::treeitem)) {
+ SerializeItem(aContent->AsElement(), aParentIndex, &aIndex, rows);
+ } else if (aContent->IsXULElement(nsGkAtoms::treeseparator)) {
+ SerializeSeparator(aContent->AsElement(), aParentIndex, &aIndex, rows);
+ }
+
+ // We can't use InsertElementsAt since the destination can't steal
+ // ownership from its const source argument.
+ int32_t count = rows.Length();
+ for (nsTArray<Row>::index_type i = 0; i < size_t(count); i++) {
+ mRows.InsertElementAt(aParentIndex + aIndex + i + 1, std::move(rows[i]));
+ }
+
+ UpdateSubtreeSizes(aParentIndex, count);
+
+ // Update parent indexes, but skip added rows.
+ // They already have correct values.
+ UpdateParentIndexes(aParentIndex + aIndex, count + 1, count);
+
+ return count;
+}
+
+int32_t nsTreeContentView::RemoveRow(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ int32_t count = row->mSubtreeSize + 1;
+ int32_t parentIndex = row->mParentIndex;
+
+ mRows.RemoveElementsAt(aIndex, count);
+
+ UpdateSubtreeSizes(parentIndex, -count);
+
+ UpdateParentIndexes(aIndex, 0, -count);
+
+ return count;
+}
+
+void nsTreeContentView::ClearRows() {
+ mRows.Clear();
+ mBody = nullptr;
+ // Remove ourselves from mDocument's observers.
+ if (mDocument) {
+ mDocument->RemoveObserver(this);
+ mDocument = nullptr;
+ }
+}
+
+void nsTreeContentView::OpenContainer(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ row->SetOpen(true);
+
+ int32_t count = EnsureSubtree(aIndex);
+ if (mTree) {
+ mTree->InvalidateRow(aIndex);
+ mTree->RowCountChanged(aIndex + 1, count);
+ }
+}
+
+void nsTreeContentView::CloseContainer(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ row->SetOpen(false);
+
+ int32_t count = RemoveSubtree(aIndex);
+ if (mTree) {
+ mTree->InvalidateRow(aIndex);
+ mTree->RowCountChanged(aIndex + 1, -count);
+ }
+}
+
+int32_t nsTreeContentView::FindContent(nsIContent* aContent) {
+ for (uint32_t i = 0; i < mRows.Length(); i++) {
+ if (mRows[i]->mContent == aContent) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+void nsTreeContentView::UpdateSubtreeSizes(int32_t aParentIndex,
+ int32_t count) {
+ while (aParentIndex >= 0) {
+ Row* row = mRows[aParentIndex].get();
+ row->mSubtreeSize += count;
+ aParentIndex = row->mParentIndex;
+ }
+}
+
+void nsTreeContentView::UpdateParentIndexes(int32_t aIndex, int32_t aSkip,
+ int32_t aCount) {
+ int32_t count = mRows.Length();
+ for (int32_t i = aIndex + aSkip; i < count; i++) {
+ Row* row = mRows[i].get();
+ if (row->mParentIndex > aIndex) {
+ row->mParentIndex += aCount;
+ }
+ }
+}
+
+Element* nsTreeContentView::GetCell(nsIContent* aContainer,
+ nsTreeColumn& aCol) {
+ int32_t colIndex(aCol.GetIndex());
+
+ // Traverse through cells, try to find the cell by index in a row.
+ Element* result = nullptr;
+ int32_t j = 0;
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* cell = iter.GetNextChild(); cell;
+ cell = iter.GetNextChild()) {
+ if (cell->IsXULElement(nsGkAtoms::treecell)) {
+ if (j == colIndex) {
+ result = cell->AsElement();
+ break;
+ }
+ j++;
+ }
+ }
+
+ return result;
+}
+
+bool nsTreeContentView::IsValidRowIndex(int32_t aRowIndex) {
+ return aRowIndex >= 0 && aRowIndex < int32_t(mRows.Length());
+}
diff --git a/layout/xul/tree/nsTreeContentView.h b/layout/xul/tree/nsTreeContentView.h
new file mode 100644
index 0000000000..8138ab44fc
--- /dev/null
+++ b/layout/xul/tree/nsTreeContentView.h
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeContentView_h__
+#define nsTreeContentView_h__
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsStubDocumentObserver.h"
+#include "nsITreeView.h"
+#include "nsITreeSelection.h"
+#include "nsWrapperCache.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+
+class nsSelection;
+class nsTreeColumn;
+class Row;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class DataTransfer;
+class Document;
+class Element;
+class XULTreeElement;
+} // namespace dom
+} // namespace mozilla
+
+nsresult NS_NewTreeContentView(nsITreeView** aResult);
+
+class nsTreeContentView final : public nsITreeView,
+ public nsStubDocumentObserver,
+ public nsWrapperCache {
+ typedef mozilla::dom::Element Element;
+
+ public:
+ nsTreeContentView(void);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(nsTreeContentView,
+ nsITreeView)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject();
+
+ int32_t RowCount() { return mRows.Length(); }
+ nsITreeSelection* GetSelection() { return mSelection; }
+ void SetSelection(nsITreeSelection* aSelection, mozilla::ErrorResult& aError);
+ void GetRowProperties(int32_t aRow, nsAString& aProperties,
+ mozilla::ErrorResult& aError);
+ void GetCellProperties(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aProperies, mozilla::ErrorResult& aError);
+ void GetColumnProperties(nsTreeColumn& aColumn, nsAString& aProperies);
+ bool IsContainer(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsContainerOpen(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsContainerEmpty(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsSeparator(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsSorted() { return false; }
+ bool CanDrop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aDataTransfer,
+ mozilla::ErrorResult& aError);
+ void Drop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aDataTransfer,
+ mozilla::ErrorResult& aError);
+ int32_t GetParentIndex(int32_t aRow, mozilla::ErrorResult& aError);
+ bool HasNextSibling(int32_t aRow, int32_t aAfterIndex,
+ mozilla::ErrorResult& aError);
+ int32_t GetLevel(int32_t aRow, mozilla::ErrorResult& aError);
+ void GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, nsAString& aSrc,
+ mozilla::ErrorResult& aError);
+ void GetCellValue(int32_t aRow, nsTreeColumn& aColumn, nsAString& aValue,
+ mozilla::ErrorResult& aError);
+ void GetCellText(int32_t aRow, nsTreeColumn& aColumn, nsAString& aText,
+ mozilla::ErrorResult& aError);
+ void SetTree(mozilla::dom::XULTreeElement* aTree,
+ mozilla::ErrorResult& aError);
+ void ToggleOpenState(int32_t aRow, mozilla::ErrorResult& aError);
+ void CycleHeader(nsTreeColumn& aColumn, mozilla::ErrorResult& aError);
+ void SelectionChanged() {}
+ void CycleCell(int32_t aRow, nsTreeColumn& aColumn) {}
+ bool IsEditable(int32_t aRow, nsTreeColumn& aColumn,
+ mozilla::ErrorResult& aError);
+ void SetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue, mozilla::ErrorResult& aError);
+ void SetCellText(int32_t aRow, nsTreeColumn& aColumn, const nsAString& aText,
+ mozilla::ErrorResult& aError);
+ Element* GetItemAtIndex(int32_t aRow, mozilla::ErrorResult& aError);
+ int32_t GetIndexOfItem(Element* aItem);
+
+ NS_DECL_NSITREEVIEW
+
+ // nsIDocumentObserver
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+ static bool CanTrustTreeSelection(nsISupports* aValue);
+
+ protected:
+ ~nsTreeContentView(void);
+
+ // Recursive methods which deal with serializing of nested content.
+ void Serialize(nsIContent* aContent, int32_t aParentIndex, int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void SerializeItem(Element* aContent, int32_t aParentIndex, int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void SerializeSeparator(Element* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void GetIndexInSubtree(nsIContent* aContainer, nsIContent* aContent,
+ int32_t* aResult);
+
+ // Helper methods which we use to manage our plain array of rows.
+ int32_t EnsureSubtree(int32_t aIndex);
+
+ int32_t RemoveSubtree(int32_t aIndex);
+
+ int32_t InsertRow(int32_t aParentIndex, int32_t aIndex, nsIContent* aContent);
+
+ void InsertRowFor(nsIContent* aParent, nsIContent* aChild);
+
+ int32_t RemoveRow(int32_t aIndex);
+
+ void ClearRows();
+
+ void OpenContainer(int32_t aIndex);
+
+ void CloseContainer(int32_t aIndex);
+
+ int32_t FindContent(nsIContent* aContent);
+
+ void UpdateSubtreeSizes(int32_t aIndex, int32_t aCount);
+
+ void UpdateParentIndexes(int32_t aIndex, int32_t aSkip, int32_t aCount);
+
+ bool CanDrop(int32_t aRow, int32_t aOrientation,
+ mozilla::ErrorResult& aError);
+ void Drop(int32_t aRow, int32_t aOrientation, mozilla::ErrorResult& aError);
+
+ // Content helpers.
+ Element* GetCell(nsIContent* aContainer, nsTreeColumn& aCol);
+
+ private:
+ bool IsValidRowIndex(int32_t aRowIndex);
+
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ nsCOMPtr<nsIContent> mBody;
+ mozilla::dom::Document* mDocument; // WEAK
+ nsTArray<mozilla::UniquePtr<Row>> mRows;
+};
+
+#endif // nsTreeContentView_h__
diff --git a/layout/xul/tree/nsTreeImageListener.cpp b/layout/xul/tree/nsTreeImageListener.cpp
new file mode 100644
index 0000000000..a560ada948
--- /dev/null
+++ b/layout/xul/tree/nsTreeImageListener.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsTreeImageListener.h"
+#include "XULTreeElement.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "nsIContent.h"
+#include "nsTreeColumns.h"
+
+using mozilla::dom::XULTreeElement;
+
+NS_IMPL_ISUPPORTS(nsTreeImageListener, imgINotificationObserver)
+
+nsTreeImageListener::nsTreeImageListener(nsTreeBodyFrame* aTreeFrame)
+ : mTreeFrame(aTreeFrame),
+ mInvalidationSuppressed(true),
+ mInvalidationArea(nullptr) {}
+
+nsTreeImageListener::~nsTreeImageListener() { delete mInvalidationArea; }
+
+void nsTreeImageListener::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType == imgINotificationObserver::IS_ANIMATED) {
+ if (mTreeFrame) {
+ mTreeFrame->OnImageIsAnimated(aRequest);
+ }
+ return;
+ }
+
+ if (aType == imgINotificationObserver::SIZE_AVAILABLE) {
+ // Ensure the animation (if any) is started. Note: There is no
+ // corresponding call to Decrement for this. This Increment will be
+ // 'cleaned up' by the Request when it is destroyed, but only then.
+ aRequest->IncrementAnimationConsumers();
+
+ if (mTreeFrame) {
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ if (image) {
+ nsPresContext* presContext = mTreeFrame->PresContext();
+ image->SetAnimationMode(presContext->ImageAnimationMode());
+ }
+ }
+ }
+
+ if (aType == imgINotificationObserver::FRAME_UPDATE) {
+ Invalidate();
+ }
+}
+
+void nsTreeImageListener::AddCell(int32_t aIndex, nsTreeColumn* aCol) {
+ if (!mInvalidationArea) {
+ mInvalidationArea = new InvalidationArea(aCol);
+ mInvalidationArea->AddRow(aIndex);
+ } else {
+ InvalidationArea* currArea;
+ for (currArea = mInvalidationArea; currArea;
+ currArea = currArea->GetNext()) {
+ if (currArea->GetCol() == aCol) {
+ currArea->AddRow(aIndex);
+ break;
+ }
+ }
+ if (!currArea) {
+ currArea = new InvalidationArea(aCol);
+ currArea->SetNext(mInvalidationArea);
+ mInvalidationArea = currArea;
+ mInvalidationArea->AddRow(aIndex);
+ }
+ }
+}
+
+void nsTreeImageListener::Invalidate() {
+ if (!mInvalidationSuppressed) {
+ for (InvalidationArea* currArea = mInvalidationArea; currArea;
+ currArea = currArea->GetNext()) {
+ // Loop from min to max, invalidating each cell that was listening for
+ // this image.
+ for (int32_t i = currArea->GetMin(); i <= currArea->GetMax(); ++i) {
+ if (mTreeFrame) {
+ RefPtr<XULTreeElement> tree =
+ XULTreeElement::FromNodeOrNull(mTreeFrame->GetBaseElement());
+ if (tree) {
+ tree->InvalidateCell(i, currArea->GetCol());
+ }
+ }
+ }
+ }
+ }
+}
+
+nsTreeImageListener::InvalidationArea::InvalidationArea(nsTreeColumn* aCol)
+ : mCol(aCol),
+ mMin(-1), // min should start out "undefined"
+ mMax(0),
+ mNext(nullptr) {}
+
+void nsTreeImageListener::InvalidationArea::AddRow(int32_t aIndex) {
+ if (mMin == -1)
+ mMin = mMax = aIndex;
+ else if (aIndex < mMin)
+ mMin = aIndex;
+ else if (aIndex > mMax)
+ mMax = aIndex;
+}
+
+NS_IMETHODIMP
+nsTreeImageListener::ClearFrame() {
+ mTreeFrame = nullptr;
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeImageListener.h b/layout/xul/tree/nsTreeImageListener.h
new file mode 100644
index 0000000000..f5e6e70512
--- /dev/null
+++ b/layout/xul/tree/nsTreeImageListener.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeImageListener_h__
+#define nsTreeImageListener_h__
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsTreeBodyFrame.h"
+#include "mozilla/Attributes.h"
+
+class nsTreeColumn;
+
+// This class handles image load observation.
+class nsTreeImageListener final : public imgINotificationObserver {
+ public:
+ explicit nsTreeImageListener(nsTreeBodyFrame* aTreeFrame);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ NS_IMETHOD ClearFrame();
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ ~nsTreeImageListener();
+
+ void UnsuppressInvalidation() { mInvalidationSuppressed = false; }
+ void Invalidate();
+ void AddCell(int32_t aIndex, nsTreeColumn* aCol);
+
+ private:
+ nsTreeBodyFrame* mTreeFrame;
+
+ // A guard that prevents us from recursive painting.
+ bool mInvalidationSuppressed;
+
+ class InvalidationArea {
+ public:
+ explicit InvalidationArea(nsTreeColumn* aCol);
+ ~InvalidationArea() { delete mNext; }
+
+ friend class nsTreeImageListener;
+
+ protected:
+ void AddRow(int32_t aIndex);
+ nsTreeColumn* GetCol() { return mCol.get(); }
+ int32_t GetMin() { return mMin; }
+ int32_t GetMax() { return mMax; }
+ InvalidationArea* GetNext() { return mNext; }
+ void SetNext(InvalidationArea* aNext) { mNext = aNext; }
+
+ private:
+ RefPtr<nsTreeColumn> mCol;
+ int32_t mMin;
+ int32_t mMax;
+ InvalidationArea* mNext;
+ };
+
+ InvalidationArea* mInvalidationArea;
+};
+
+#endif // nsTreeImageListener_h__
diff --git a/layout/xul/tree/nsTreeSelection.cpp b/layout/xul/tree/nsTreeSelection.cpp
new file mode 100644
index 0000000000..952a9bf376
--- /dev/null
+++ b/layout/xul/tree/nsTreeSelection.cpp
@@ -0,0 +1,724 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/Element.h"
+#include "nsCOMPtr.h"
+#include "nsTreeSelection.h"
+#include "XULTreeElement.h"
+#include "nsITreeView.h"
+#include "nsString.h"
+#include "nsIContent.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsComponentManagerUtils.h"
+#include "nsTreeColumns.h"
+
+using namespace mozilla;
+using dom::XULTreeElement;
+
+// A helper class for managing our ranges of selection.
+struct nsTreeRange {
+ nsTreeSelection* mSelection;
+
+ nsTreeRange* mPrev;
+ nsTreeRange* mNext;
+
+ int32_t mMin;
+ int32_t mMax;
+
+ nsTreeRange(nsTreeSelection* aSel, int32_t aSingleVal)
+ : mSelection(aSel),
+ mPrev(nullptr),
+ mNext(nullptr),
+ mMin(aSingleVal),
+ mMax(aSingleVal) {}
+ nsTreeRange(nsTreeSelection* aSel, int32_t aMin, int32_t aMax)
+ : mSelection(aSel),
+ mPrev(nullptr),
+ mNext(nullptr),
+ mMin(aMin),
+ mMax(aMax) {}
+
+ ~nsTreeRange() { delete mNext; }
+
+ void Connect(nsTreeRange* aPrev = nullptr, nsTreeRange* aNext = nullptr) {
+ if (aPrev)
+ aPrev->mNext = this;
+ else
+ mSelection->mFirstRange = this;
+
+ if (aNext) aNext->mPrev = this;
+
+ mPrev = aPrev;
+ mNext = aNext;
+ }
+
+ nsresult RemoveRange(int32_t aStart, int32_t aEnd) {
+ // This should so be a loop... sigh...
+ // We start past the range to remove, so no more to remove
+ if (aEnd < mMin) return NS_OK;
+ // We are the last range to be affected
+ if (aEnd < mMax) {
+ if (aStart <= mMin) {
+ // Just chop the start of the range off
+ mMin = aEnd + 1;
+ } else {
+ // We need to split the range
+ nsTreeRange* range = new nsTreeRange(mSelection, aEnd + 1, mMax);
+ if (!range) return NS_ERROR_OUT_OF_MEMORY;
+
+ mMax = aStart - 1;
+ range->Connect(this, mNext);
+ }
+ return NS_OK;
+ }
+ nsTreeRange* next = mNext;
+ if (aStart <= mMin) {
+ // The remove includes us, remove ourselves from the list
+ if (mPrev)
+ mPrev->mNext = next;
+ else
+ mSelection->mFirstRange = next;
+
+ if (next) next->mPrev = mPrev;
+ mPrev = mNext = nullptr;
+ delete this;
+ } else if (aStart <= mMax) {
+ // Just chop the end of the range off
+ mMax = aStart - 1;
+ }
+ return next ? next->RemoveRange(aStart, aEnd) : NS_OK;
+ }
+
+ nsresult Remove(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) {
+ // We have found the range that contains us.
+ if (mMin == mMax) {
+ // Delete the whole range.
+ if (mPrev) mPrev->mNext = mNext;
+ if (mNext) mNext->mPrev = mPrev;
+ nsTreeRange* first = mSelection->mFirstRange;
+ if (first == this) mSelection->mFirstRange = mNext;
+ mNext = mPrev = nullptr;
+ delete this;
+ } else if (aIndex == mMin)
+ mMin++;
+ else if (aIndex == mMax)
+ mMax--;
+ else {
+ // We have to break this range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex + 1, mMax);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(this, mNext);
+ mMax = aIndex - 1;
+ }
+ } else if (mNext)
+ return mNext->Remove(aIndex);
+
+ return NS_OK;
+ }
+
+ nsresult Add(int32_t aIndex) {
+ if (aIndex < mMin) {
+ // We have found a spot to insert.
+ if (aIndex + 1 == mMin)
+ mMin = aIndex;
+ else if (mPrev && mPrev->mMax + 1 == aIndex)
+ mPrev->mMax = aIndex;
+ else {
+ // We have to create a new range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(mPrev, this);
+ }
+ } else if (mNext)
+ mNext->Add(aIndex);
+ else {
+ // Insert on to the end.
+ if (mMax + 1 == aIndex)
+ mMax = aIndex;
+ else {
+ // We have to create a new range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(this, nullptr);
+ }
+ }
+ return NS_OK;
+ }
+
+ bool Contains(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) return true;
+
+ if (mNext) return mNext->Contains(aIndex);
+
+ return false;
+ }
+
+ int32_t Count() {
+ int32_t total = mMax - mMin + 1;
+ if (mNext) total += mNext->Count();
+ return total;
+ }
+
+ static void CollectRanges(nsTreeRange* aRange, nsTArray<int32_t>& aRanges) {
+ nsTreeRange* cur = aRange;
+ while (cur) {
+ aRanges.AppendElement(cur->mMin);
+ aRanges.AppendElement(cur->mMax);
+ cur = cur->mNext;
+ }
+ }
+
+ static void InvalidateRanges(XULTreeElement* aTree,
+ nsTArray<int32_t>& aRanges) {
+ if (aTree) {
+ RefPtr<nsXULElement> tree = aTree;
+ for (uint32_t i = 0; i < aRanges.Length(); i += 2) {
+ aTree->InvalidateRange(aRanges[i], aRanges[i + 1]);
+ }
+ }
+ }
+
+ void Invalidate() {
+ nsTArray<int32_t> ranges;
+ CollectRanges(this, ranges);
+ InvalidateRanges(mSelection->mTree, ranges);
+ }
+
+ void RemoveAllBut(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) {
+ // Invalidate everything in this list.
+ nsTArray<int32_t> ranges;
+ CollectRanges(mSelection->mFirstRange, ranges);
+
+ mMin = aIndex;
+ mMax = aIndex;
+
+ nsTreeRange* first = mSelection->mFirstRange;
+ if (mPrev) mPrev->mNext = mNext;
+ if (mNext) mNext->mPrev = mPrev;
+ mNext = mPrev = nullptr;
+
+ if (first != this) {
+ delete mSelection->mFirstRange;
+ mSelection->mFirstRange = this;
+ }
+ InvalidateRanges(mSelection->mTree, ranges);
+ } else if (mNext)
+ mNext->RemoveAllBut(aIndex);
+ }
+
+ void Insert(nsTreeRange* aRange) {
+ if (mMin >= aRange->mMax)
+ aRange->Connect(mPrev, this);
+ else if (mNext)
+ mNext->Insert(aRange);
+ else
+ aRange->Connect(this, nullptr);
+ }
+};
+
+nsTreeSelection::nsTreeSelection(XULTreeElement* aTree)
+ : mTree(aTree),
+ mSuppressed(false),
+ mCurrentIndex(-1),
+ mShiftSelectPivot(-1),
+ mFirstRange(nullptr) {}
+
+nsTreeSelection::~nsTreeSelection() {
+ delete mFirstRange;
+ if (mSelectTimer) mSelectTimer->Cancel();
+}
+
+NS_IMPL_CYCLE_COLLECTION(nsTreeSelection, mTree)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeSelection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeSelection)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsITreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsINativeTreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP nsTreeSelection::GetTree(XULTreeElement** aTree) {
+ NS_IF_ADDREF(*aTree = mTree);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetTree(XULTreeElement* aTree) {
+ if (mSelectTimer) {
+ mSelectTimer->Cancel();
+ mSelectTimer = nullptr;
+ }
+
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetSingle(bool* aSingle) {
+ if (!mTree) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ *aSingle = mTree->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype,
+ u"single"_ns, eCaseMatters);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::IsSelected(int32_t aIndex, bool* aResult) {
+ if (mFirstRange)
+ *aResult = mFirstRange->Contains(aIndex);
+ else
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::TimedSelect(int32_t aIndex, int32_t aMsec) {
+ bool suppressSelect = mSuppressed;
+
+ if (aMsec != -1) mSuppressed = true;
+
+ nsresult rv = Select(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (aMsec != -1) {
+ mSuppressed = suppressSelect;
+ if (!mSuppressed) {
+ if (mSelectTimer) mSelectTimer->Cancel();
+
+ if (!mTree) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsIEventTarget* target =
+ mTree->OwnerDoc()->EventTargetFor(TaskCategory::Other);
+ NS_NewTimerWithFuncCallback(getter_AddRefs(mSelectTimer), SelectCallback,
+ this, aMsec, nsITimer::TYPE_ONE_SHOT,
+ "nsTreeSelection::SelectCallback", target);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::Select(int32_t aIndex) {
+ mShiftSelectPivot = -1;
+
+ nsresult rv = SetCurrentIndex(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (mFirstRange) {
+ bool alreadySelected = mFirstRange->Contains(aIndex);
+
+ if (alreadySelected) {
+ int32_t count = mFirstRange->Count();
+ if (count > 1) {
+ // We need to deselect everything but our item.
+ mFirstRange->RemoveAllBut(aIndex);
+ FireOnSelectHandler();
+ }
+ return NS_OK;
+ } else {
+ // Clear out our selection.
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ }
+ }
+
+ // Create our new selection.
+ mFirstRange = new nsTreeRange(this, aIndex);
+ if (!mFirstRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ mFirstRange->Invalidate();
+
+ // Fire the select event
+ FireOnSelectHandler();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ToggleSelect(int32_t aIndex) {
+ // There are six cases that can occur on a ToggleSelect with our
+ // range code.
+ // (1) A new range should be made for a selection.
+ // (2) A single range is removed from the selection.
+ // (3) The item is added to an existing range.
+ // (4) The item is removed from an existing range.
+ // (5) The addition of the item causes two ranges to be merged.
+ // (6) The removal of the item causes two ranges to be split.
+ mShiftSelectPivot = -1;
+ nsresult rv = SetCurrentIndex(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mFirstRange)
+ Select(aIndex);
+ else {
+ if (!mFirstRange->Contains(aIndex)) {
+ bool single;
+ rv = GetSingle(&single);
+ if (NS_SUCCEEDED(rv) && !single) rv = mFirstRange->Add(aIndex);
+ } else
+ rv = mFirstRange->Remove(aIndex);
+ if (NS_SUCCEEDED(rv)) {
+ if (mTree) mTree->InvalidateRow(aIndex);
+
+ FireOnSelectHandler();
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsTreeSelection::RangedSelect(int32_t aStartIndex,
+ int32_t aEndIndex, bool aAugment) {
+ bool single;
+ nsresult rv = GetSingle(&single);
+ if (NS_FAILED(rv)) return rv;
+
+ if ((mFirstRange || (aStartIndex != aEndIndex)) && single) return NS_OK;
+
+ if (!aAugment) {
+ // Clear our selection.
+ if (mFirstRange) {
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ mFirstRange = nullptr;
+ }
+ }
+
+ if (aStartIndex == -1) {
+ if (mShiftSelectPivot != -1)
+ aStartIndex = mShiftSelectPivot;
+ else if (mCurrentIndex != -1)
+ aStartIndex = mCurrentIndex;
+ else
+ aStartIndex = aEndIndex;
+ }
+
+ mShiftSelectPivot = aStartIndex;
+ rv = SetCurrentIndex(aEndIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex;
+ int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex;
+
+ if (aAugment && mFirstRange) {
+ // We need to remove all the items within our selected range from the
+ // selection, and then we insert our new range into the list.
+ nsresult rv = mFirstRange->RemoveRange(start, end);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ nsTreeRange* range = new nsTreeRange(this, start, end);
+ if (!range) return NS_ERROR_OUT_OF_MEMORY;
+
+ range->Invalidate();
+
+ if (aAugment && mFirstRange)
+ mFirstRange->Insert(range);
+ else
+ mFirstRange = range;
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ClearRange(int32_t aStartIndex,
+ int32_t aEndIndex) {
+ nsresult rv = SetCurrentIndex(aEndIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (mFirstRange) {
+ int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex;
+ int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex;
+
+ mFirstRange->RemoveRange(start, end);
+
+ if (mTree) mTree->InvalidateRange(start, end);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ClearSelection() {
+ if (mFirstRange) {
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ mFirstRange = nullptr;
+ }
+ mShiftSelectPivot = -1;
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SelectAll() {
+ if (!mTree) return NS_OK;
+
+ nsCOMPtr<nsITreeView> view = mTree->GetView();
+ if (!view) return NS_OK;
+
+ int32_t rowCount;
+ view->GetRowCount(&rowCount);
+ bool single;
+ nsresult rv = GetSingle(&single);
+ if (NS_FAILED(rv)) return rv;
+
+ if (rowCount == 0 || (rowCount > 1 && single)) return NS_OK;
+
+ mShiftSelectPivot = -1;
+
+ // Invalidate not necessary when clearing selection, since
+ // we're going to invalidate the world on the SelectAll.
+ delete mFirstRange;
+
+ mFirstRange = new nsTreeRange(this, 0, rowCount - 1);
+ mFirstRange->Invalidate();
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetRangeCount(int32_t* aResult) {
+ int32_t count = 0;
+ nsTreeRange* curr = mFirstRange;
+ while (curr) {
+ count++;
+ curr = curr->mNext;
+ }
+
+ *aResult = count;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetRangeAt(int32_t aIndex, int32_t* aMin,
+ int32_t* aMax) {
+ *aMin = *aMax = -1;
+ int32_t i = -1;
+ nsTreeRange* curr = mFirstRange;
+ while (curr) {
+ i++;
+ if (i == aIndex) {
+ *aMin = curr->mMin;
+ *aMax = curr->mMax;
+ break;
+ }
+ curr = curr->mNext;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetCount(int32_t* count) {
+ if (mFirstRange)
+ *count = mFirstRange->Count();
+ else // No range available, so there's no selected row.
+ *count = 0;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetSelectEventsSuppressed(
+ bool* aSelectEventsSuppressed) {
+ *aSelectEventsSuppressed = mSuppressed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetSelectEventsSuppressed(
+ bool aSelectEventsSuppressed) {
+ mSuppressed = aSelectEventsSuppressed;
+ if (!mSuppressed) FireOnSelectHandler();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetCurrentIndex(int32_t* aCurrentIndex) {
+ *aCurrentIndex = mCurrentIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetCurrentIndex(int32_t aIndex) {
+ if (!mTree) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (mCurrentIndex == aIndex) {
+ return NS_OK;
+ }
+ if (mCurrentIndex != -1 && mTree) mTree->InvalidateRow(mCurrentIndex);
+
+ mCurrentIndex = aIndex;
+ if (!mTree) return NS_OK;
+
+ if (aIndex != -1) mTree->InvalidateRow(aIndex);
+
+ // Fire DOMMenuItemActive or DOMMenuItemInactive event for tree.
+ NS_ENSURE_STATE(mTree);
+
+ constexpr auto DOMMenuItemActive = u"DOMMenuItemActive"_ns;
+ constexpr auto DOMMenuItemInactive = u"DOMMenuItemInactive"_ns;
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ mTree, (aIndex != -1 ? DOMMenuItemActive : DOMMenuItemInactive),
+ CanBubble::eYes, ChromeOnlyDispatch::eNo);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+#define ADD_NEW_RANGE(macro_range, macro_selection, macro_start, macro_end) \
+ { \
+ int32_t start = macro_start; \
+ int32_t end = macro_end; \
+ if (start > end) { \
+ end = start; \
+ } \
+ nsTreeRange* macro_new_range = \
+ new nsTreeRange(macro_selection, start, end); \
+ if (macro_range) \
+ macro_range->Insert(macro_new_range); \
+ else \
+ macro_range = macro_new_range; \
+ }
+
+NS_IMETHODIMP
+nsTreeSelection::AdjustSelection(int32_t aIndex, int32_t aCount) {
+ NS_ASSERTION(aCount != 0, "adjusting by zero");
+ if (!aCount) return NS_OK;
+
+ // adjust mShiftSelectPivot, if necessary
+ if ((mShiftSelectPivot != 1) && (aIndex <= mShiftSelectPivot)) {
+ // if we are deleting and the delete includes the shift select pivot, reset
+ // it
+ if (aCount < 0 && (mShiftSelectPivot <= (aIndex - aCount - 1))) {
+ mShiftSelectPivot = -1;
+ } else {
+ mShiftSelectPivot += aCount;
+ }
+ }
+
+ // adjust mCurrentIndex, if necessary
+ if ((mCurrentIndex != -1) && (aIndex <= mCurrentIndex)) {
+ // if we are deleting and the delete includes the current index, reset it
+ if (aCount < 0 && (mCurrentIndex <= (aIndex - aCount - 1))) {
+ mCurrentIndex = -1;
+ } else {
+ mCurrentIndex += aCount;
+ }
+ }
+
+ // no selection, so nothing to do.
+ if (!mFirstRange) return NS_OK;
+
+ bool selChanged = false;
+ nsTreeRange* oldFirstRange = mFirstRange;
+ nsTreeRange* curr = mFirstRange;
+ mFirstRange = nullptr;
+ while (curr) {
+ if (aCount > 0) {
+ // inserting
+ if (aIndex > curr->mMax) {
+ // adjustment happens after the range, so no change
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax);
+ } else if (aIndex <= curr->mMin) {
+ // adjustment happens before the start of the range, so shift down
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount,
+ curr->mMax + aCount);
+ selChanged = true;
+ } else {
+ // adjustment happen inside the range.
+ // break apart the range and create two ranges
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1);
+ ADD_NEW_RANGE(mFirstRange, this, aIndex + aCount, curr->mMax + aCount);
+ selChanged = true;
+ }
+ } else {
+ // deleting
+ if (aIndex > curr->mMax) {
+ // adjustment happens after the range, so no change
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax);
+ } else {
+ // remember, aCount is negative
+ selChanged = true;
+ int32_t lastIndexOfAdjustment = aIndex - aCount - 1;
+ if (aIndex <= curr->mMin) {
+ if (lastIndexOfAdjustment < curr->mMin) {
+ // adjustment happens before the start of the range, so shift up
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount,
+ curr->mMax + aCount);
+ } else if (lastIndexOfAdjustment >= curr->mMax) {
+ // adjustment contains the range. remove the range by not adding it
+ // to the newRange
+ } else {
+ // adjustment starts before the range, and ends in the middle of it,
+ // so trim the range
+ ADD_NEW_RANGE(mFirstRange, this, aIndex, curr->mMax + aCount)
+ }
+ } else if (lastIndexOfAdjustment >= curr->mMax) {
+ // adjustment starts in the middle of the current range, and contains
+ // the end of the range, so trim the range
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1)
+ } else {
+ // range contains the adjustment, so shorten the range
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax + aCount)
+ }
+ }
+ }
+ curr = curr->mNext;
+ }
+
+ delete oldFirstRange;
+
+ // Fire the select event
+ if (selChanged) FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeSelection::InvalidateSelection() {
+ if (mFirstRange) mFirstRange->Invalidate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeSelection::GetShiftSelectPivot(int32_t* aIndex) {
+ *aIndex = mShiftSelectPivot;
+ return NS_OK;
+}
+
+nsresult nsTreeSelection::FireOnSelectHandler() {
+ if (mSuppressed || !mTree) {
+ return NS_OK;
+ }
+
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo);
+ return NS_OK;
+}
+
+void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) {
+ RefPtr<nsTreeSelection> self = static_cast<nsTreeSelection*>(aClosure);
+ if (self) {
+ self->FireOnSelectHandler();
+ aTimer->Cancel();
+ self->mSelectTimer = nullptr;
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////////
+
+nsresult NS_NewTreeSelection(XULTreeElement* aTree,
+ nsITreeSelection** aResult) {
+ *aResult = new nsTreeSelection(aTree);
+ if (!*aResult) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeSelection.h b/layout/xul/tree/nsTreeSelection.h
new file mode 100644
index 0000000000..3b0eb6b21e
--- /dev/null
+++ b/layout/xul/tree/nsTreeSelection.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeSelection_h__
+#define nsTreeSelection_h__
+
+#include "nsITreeSelection.h"
+#include "nsITimer.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "XULTreeElement.h"
+
+class nsTreeColumn;
+struct nsTreeRange;
+
+class nsTreeSelection final : public nsINativeTreeSelection {
+ public:
+ explicit nsTreeSelection(mozilla::dom::XULTreeElement* aTree);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsTreeSelection)
+ NS_DECL_NSITREESELECTION
+
+ // nsINativeTreeSelection: Untrusted code can use us
+ NS_IMETHOD EnsureNative() override { return NS_OK; }
+
+ friend struct nsTreeRange;
+
+ protected:
+ ~nsTreeSelection();
+
+ nsresult FireOnSelectHandler();
+ static void SelectCallback(nsITimer* aTimer, void* aClosure);
+
+ protected:
+ // The tree will hold on to us through the view and let go when it dies.
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+
+ bool mSuppressed; // Whether or not we should be firing onselect events.
+ int32_t mCurrentIndex; // The item to draw the rect around. The last one
+ // clicked, etc.
+ int32_t mShiftSelectPivot; // Used when multiple SHIFT+selects are performed
+ // to pivot on.
+
+ nsTreeRange* mFirstRange; // Our list of ranges.
+
+ nsCOMPtr<nsITimer> mSelectTimer;
+};
+
+nsresult NS_NewTreeSelection(mozilla::dom::XULTreeElement* aTree,
+ nsITreeSelection** aResult);
+
+#endif
diff --git a/layout/xul/tree/nsTreeStyleCache.cpp b/layout/xul/tree/nsTreeStyleCache.cpp
new file mode 100644
index 0000000000..d5823ac38e
--- /dev/null
+++ b/layout/xul/tree/nsTreeStyleCache.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsTreeStyleCache.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ServoStyleSet.h"
+#include "nsPresContextInlines.h"
+
+using namespace mozilla;
+
+nsTreeStyleCache::Transition::Transition(DFAState aState, nsAtom* aSymbol)
+ : mState(aState), mInputSymbol(aSymbol) {}
+
+bool nsTreeStyleCache::Transition::operator==(const Transition& aOther) const {
+ return aOther.mState == mState && aOther.mInputSymbol == mInputSymbol;
+}
+
+uint32_t nsTreeStyleCache::Transition::Hash() const {
+ // Make a 32-bit integer that combines the low-order 16 bits of the state and
+ // the input symbol.
+ uint32_t hb = mState << 16;
+ uint32_t lb = (NS_PTR_TO_UINT32(mInputSymbol.get()) << 16) >> 16;
+ return hb + lb;
+}
+
+// The ComputedStyle cache impl
+ComputedStyle* nsTreeStyleCache::GetComputedStyle(
+ nsPresContext* aPresContext, nsIContent* aContent, ComputedStyle* aStyle,
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, const AtomArray& aInputWord) {
+ MOZ_ASSERT(nsCSSAnonBoxes::IsTreePseudoElement(aPseudoElement));
+
+ uint32_t count = aInputWord.Length();
+
+ // Go ahead and init the transition table.
+ if (!mTransitionTable) {
+ // Automatic miss. Build the table
+ mTransitionTable = MakeUnique<TransitionTable>();
+ }
+
+ // The first transition is always made off the supplied pseudo-element.
+ Transition transition(0, aPseudoElement);
+ DFAState currState = mTransitionTable->Get(transition);
+
+ if (!currState) {
+ // We had a miss. Make a new state and add it to our hash.
+ currState = mNextState;
+ mNextState++;
+ mTransitionTable->InsertOrUpdate(transition, currState);
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ Transition transition(currState, aInputWord[i]);
+ currState = mTransitionTable->Get(transition);
+
+ if (!currState) {
+ // We had a miss. Make a new state and add it to our hash.
+ currState = mNextState;
+ mNextState++;
+ mTransitionTable->InsertOrUpdate(transition, currState);
+ }
+ }
+
+ // We're in a final state.
+ // Look up our ComputedStyle for this state.
+ ComputedStyle* result = nullptr;
+ if (mCache) {
+ result = mCache->GetWeak(currState);
+ }
+ if (!result) {
+ // We missed the cache. Resolve this pseudo-style.
+ RefPtr<ComputedStyle> newResult =
+ aPresContext->StyleSet()->ResolveXULTreePseudoStyle(
+ aContent->AsElement(), aPseudoElement, aStyle, aInputWord);
+
+ // Normally we rely on nsIFrame::Init / RestyleManager to call this, but
+ // these are weird and don't use a frame, yet ::-moz-tree-twisty definitely
+ // pokes at list-style-image.
+ newResult->StartImageLoads(*aPresContext->Document());
+
+ // Even though xul-tree pseudos are defined in nsCSSAnonBoxList, nothing has
+ // ever treated them as an anon box, and they don't ever get boxes anyway.
+ //
+ // This is really weird, and probably nothing really relies on the result of
+ // these assert, but it's here just to avoid changing them accidentally.
+ MOZ_ASSERT(newResult->GetPseudoType() == PseudoStyleType::XULTree);
+ MOZ_ASSERT(!newResult->IsAnonBox());
+ MOZ_ASSERT(!newResult->IsPseudoElement());
+
+ // Put the ComputedStyle in our table, transferring the owning reference to
+ // the table.
+ if (!mCache) {
+ mCache = MakeUnique<ComputedStyleCache>();
+ }
+ result = newResult.get();
+ mCache->InsertOrUpdate(currState, std::move(newResult));
+ }
+
+ return result;
+}
diff --git a/layout/xul/tree/nsTreeStyleCache.h b/layout/xul/tree/nsTreeStyleCache.h
new file mode 100644
index 0000000000..b0c0e4d9dc
--- /dev/null
+++ b/layout/xul/tree/nsTreeStyleCache.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeStyleCache_h__
+#define nsTreeStyleCache_h__
+
+#include "mozilla/AtomArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMArray.h"
+#include "nsTHashMap.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/ComputedStyle.h"
+
+class nsIContent;
+
+class nsTreeStyleCache {
+ public:
+ nsTreeStyleCache() : mNextState(0) {}
+
+ ~nsTreeStyleCache() { Clear(); }
+
+ void Clear() {
+ mTransitionTable = nullptr;
+ mCache = nullptr;
+ mNextState = 0;
+ }
+
+ mozilla::ComputedStyle* GetComputedStyle(
+ nsPresContext* aPresContext, nsIContent* aContent,
+ mozilla::ComputedStyle* aStyle,
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement,
+ const mozilla::AtomArray& aInputWord);
+
+ protected:
+ typedef uint32_t DFAState;
+
+ class Transition final {
+ public:
+ Transition(DFAState aState, nsAtom* aSymbol);
+ bool operator==(const Transition& aOther) const;
+ uint32_t Hash() const;
+
+ private:
+ DFAState mState;
+ RefPtr<nsAtom> mInputSymbol;
+ };
+
+ typedef nsTHashMap<nsGenericHashKey<Transition>, DFAState> TransitionTable;
+
+ // A transition table for a deterministic finite automaton. The DFA
+ // takes as its input a single pseudoelement and an ordered set of properties.
+ // It transitions on an input word that is the concatenation of the
+ // pseudoelement supplied with the properties in the array.
+ //
+ // It transitions from state to state by looking up entries in the transition
+ // table (which is a mapping from (S,i)->S', where S is the current state, i
+ // is the next property in the input word, and S' is the state to transition
+ // to.
+ //
+ // If S' is not found, it is constructed and entered into the hashtable
+ // under the key (S,i).
+ //
+ // Once the entire word has been consumed, the final state is used
+ // to reference the cache table to locate the ComputedStyle.
+ mozilla::UniquePtr<TransitionTable> mTransitionTable;
+
+ // The cache of all active ComputedStyles. This is a hash from
+ // a final state in the DFA, Sf, to the resultant ComputedStyle.
+ typedef nsRefPtrHashtable<nsUint32HashKey, mozilla::ComputedStyle>
+ ComputedStyleCache;
+ mozilla::UniquePtr<ComputedStyleCache> mCache;
+
+ // An integer counter that is used when we need to make new states in the
+ // DFA.
+ DFAState mNextState;
+};
+
+#endif // nsTreeStyleCache_h__
diff --git a/layout/xul/tree/nsTreeUtils.cpp b/layout/xul/tree/nsTreeUtils.cpp
new file mode 100644
index 0000000000..10767e22d2
--- /dev/null
+++ b/layout/xul/tree/nsTreeUtils.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsReadableUtils.h"
+#include "nsTreeUtils.h"
+#include "ChildIterator.h"
+#include "nsCRT.h"
+#include "nsAtom.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+
+using namespace mozilla;
+
+nsresult nsTreeUtils::TokenizeProperties(const nsAString& aProperties,
+ AtomArray& aPropertiesArray) {
+ nsAString::const_iterator end;
+ aProperties.EndReading(end);
+
+ nsAString::const_iterator iter;
+ aProperties.BeginReading(iter);
+
+ do {
+ // Skip whitespace
+ while (iter != end && nsCRT::IsAsciiSpace(*iter)) ++iter;
+
+ // If only whitespace, we're done
+ if (iter == end) break;
+
+ // Note the first non-whitespace character
+ nsAString::const_iterator first = iter;
+
+ // Advance to the next whitespace character
+ while (iter != end && !nsCRT::IsAsciiSpace(*iter)) ++iter;
+
+ // XXX this would be nonsensical
+ NS_ASSERTION(iter != first, "eh? something's wrong here");
+ if (iter == first) break;
+
+ RefPtr<nsAtom> atom = NS_Atomize(Substring(first, iter));
+ aPropertiesArray.AppendElement(atom);
+ } while (iter != end);
+
+ return NS_OK;
+}
+
+nsIContent* nsTreeUtils::GetImmediateChild(nsIContent* aContainer,
+ nsAtom* aTag) {
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* child = iter.GetNextChild(); child;
+ child = iter.GetNextChild()) {
+ if (child->IsXULElement(aTag)) {
+ return child;
+ }
+ // <slot> is in the flattened tree, but <tree> code is used to work with
+ // <xbl:children> which is not, so recurse in <slot> here.
+ if (child->IsHTMLElement(nsGkAtoms::slot)) {
+ if (nsIContent* c = GetImmediateChild(child, aTag)) {
+ return c;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+nsIContent* nsTreeUtils::GetDescendantChild(nsIContent* aContainer,
+ nsAtom* aTag) {
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* child = iter.GetNextChild(); child;
+ child = iter.GetNextChild()) {
+ if (child->IsXULElement(aTag)) {
+ return child;
+ }
+
+ child = GetDescendantChild(child, aTag);
+ if (child) {
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult nsTreeUtils::UpdateSortIndicators(dom::Element* aColumn,
+ const nsAString& aDirection) {
+ aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, aDirection,
+ true);
+ aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, u"true"_ns, true);
+
+ // Unset sort attribute(s) on the other columns
+ nsCOMPtr<nsIContent> parentContent = aColumn->GetParent();
+ if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols,
+ kNameSpaceID_XUL)) {
+ for (nsINode* childContent = parentContent->GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ if (childContent != aColumn &&
+ childContent->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL)) {
+ childContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::sortDirection, true);
+ childContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::sortActive, true);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTreeUtils::GetColumnIndex(dom::Element* aColumn, int32_t* aResult) {
+ nsIContent* parentContent = aColumn->GetParent();
+ if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols,
+ kNameSpaceID_XUL)) {
+ int32_t colIndex = 0;
+
+ for (nsINode* childContent = parentContent->GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ if (childContent->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL)) {
+ if (childContent == aColumn) {
+ *aResult = colIndex;
+ return NS_OK;
+ }
+ ++colIndex;
+ }
+ }
+ }
+
+ *aResult = -1;
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeUtils.h b/layout/xul/tree/nsTreeUtils.h
new file mode 100644
index 0000000000..d0588f1273
--- /dev/null
+++ b/layout/xul/tree/nsTreeUtils.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeUtils_h__
+#define nsTreeUtils_h__
+
+#include "mozilla/AtomArray.h"
+#include "nsError.h"
+#include "nsString.h"
+#include "nsTreeStyleCache.h"
+
+class nsAtom;
+class nsIContent;
+namespace mozilla {
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+class nsTreeUtils {
+ public:
+ /**
+ * Parse a whitespace separated list of properties into an array
+ * of atoms.
+ */
+ static nsresult TokenizeProperties(const nsAString& aProperties,
+ mozilla::AtomArray& aPropertiesArray);
+
+ static nsIContent* GetImmediateChild(nsIContent* aContainer, nsAtom* aTag);
+
+ static nsIContent* GetDescendantChild(nsIContent* aContainer, nsAtom* aTag);
+
+ static nsresult UpdateSortIndicators(mozilla::dom::Element* aColumn,
+ const nsAString& aDirection);
+
+ static nsresult GetColumnIndex(mozilla::dom::Element* aColumn,
+ int32_t* aResult);
+};
+
+#endif // nsTreeUtils_h__