diff options
Diffstat (limited to 'layout/xul')
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="…" 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 Binary files differnew file mode 100644 index 0000000000..6719bf5cec --- /dev/null +++ b/layout/xul/reftest/image4x3.png 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"> CC Option</option> +<option value="4"> DD Option</option> +<option value="5"> 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(¤tIndex); + 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(¤tIndex); + 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__ |