diff options
Diffstat (limited to 'layout/xul')
202 files changed, 32426 insertions, 0 deletions
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/189814-1.xhtml b/layout/xul/crashtests/189814-1.xhtml new file mode 100644 index 0000000000..79462348c6 --- /dev/null +++ b/layout/xul/crashtests/189814-1.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<window + id="sliderprint" title="Print These Sliders" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + style="background-color: white"> + + <label> + With the Classic theme, printing causes the browser to crash. adding style="-moz-appearance: none" to the + thumb prevents the crash. The crash doesn't happen at all with Modern. + </label> + <spacer height="10"/> + <hbox> + + <slider style="height: 174px; width: 24px" orient="vertical"> + <thumb/> + </slider> + + </hbox> + +</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..ef7942ba6b --- /dev/null +++ b/layout/xul/crashtests/crashtests.list @@ -0,0 +1,53 @@ +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 +load chrome://reftest/content/crashtests/layout/xul/crashtests/189814-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..9acbdf4826 --- /dev/null +++ b/layout/xul/moz.build @@ -0,0 +1,58 @@ +# -*- 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 += [ + "nsBox.cpp", + "nsBoxFrame.cpp", + "nsBoxLayout.cpp", + "nsBoxLayoutState.cpp", + "nsImageBoxFrame.cpp", + "nsLeafBoxFrame.cpp", + "nsMenuBarFrame.cpp", + "nsMenuBarListener.cpp", + "nsMenuPopupFrame.cpp", + "nsRepeatService.cpp", + "nsScrollbarButtonFrame.cpp", + "nsScrollbarFrame.cpp", + "nsSliderFrame.cpp", + "nsSplitterFrame.cpp", + "nsSprocketLayout.cpp", + "nsTextBoxFrame.cpp", + "nsXULPopupManager.cpp", + "nsXULTooltipListener.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/nsBox.cpp b/layout/xul/nsBox.cpp new file mode 100644 index 0000000000..3dd11d8a3f --- /dev/null +++ b/layout/xul/nsBox.cpp @@ -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/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/StaticPtr.h" +#include "nsIFrame.h" + +#include "nsBoxLayoutState.h" +#include "nsBoxFrame.h" +#include "nsDOMAttributeMap.h" +#include "nsPresContext.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsContainerFrame.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsITheme.h" +#include "nsBoxLayout.h" +#include "nsLayoutUtils.h" +#include "mozilla/dom/Attr.h" +#include "mozilla/dom/Element.h" +#include <algorithm> + +using namespace mozilla; + +nsresult nsIFrame::BeginXULLayout(nsBoxLayoutState& aState) { + // mark ourselves as dirty so no child under us + // can post an incremental layout. + // XXXldb Is this still needed? + AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); + + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + // If the parent is dirty, all the children are dirty (ReflowInput + // does this too). + nsIFrame* box; + for (box = GetChildXULBox(this); box; box = GetNextXULBox(box)) + box->MarkSubtreeDirty(); + } + + // Another copy-over from ReflowInput. + // Since we are in reflow, we don't need to store these properties anymore. + RemoveProperty(UsedBorderProperty()); + RemoveProperty(UsedPaddingProperty()); + RemoveProperty(UsedMarginProperty()); + + return NS_OK; +} + +nsresult nsIFrame::EndXULLayout(nsBoxLayoutState& aState) { + return SyncXULLayout(aState); +} + +nsresult nsIFrame::GetXULClientRect(nsRect& aClientRect) { + aClientRect = mRect; + aClientRect.MoveTo(0, 0); + + nsMargin borderPadding; + GetXULBorderAndPadding(borderPadding); + + aClientRect.Deflate(borderPadding); + + if (aClientRect.width < 0) aClientRect.width = 0; + + if (aClientRect.height < 0) aClientRect.height = 0; + + return NS_OK; +} + +void nsIFrame::SetXULBounds(nsBoxLayoutState& aState, const nsRect& aRect, + bool aRemoveOverflowAreas) { + nsRect rect(mRect); + + ReflowChildFlags flags = GetXULLayoutFlags() | aState.LayoutFlags(); + + if ((flags & ReflowChildFlags::NoMoveFrame) == + ReflowChildFlags::NoMoveFrame) { + SetSize(aRect.Size()); + } else { + SetRect(aRect); + } + + // Nuke the overflow area. The caller is responsible for restoring + // it if necessary. + if (aRemoveOverflowAreas) { + // remove the previously stored overflow area + ClearOverflowRects(); + } + + if (!(flags & ReflowChildFlags::NoMoveView)) { + nsContainerFrame::PositionFrameView(this); + if ((rect.x != aRect.x) || (rect.y != aRect.y)) + nsContainerFrame::PositionChildViews(this); + } +} + +nsresult nsIFrame::GetXULBorderAndPadding(nsMargin& aBorderAndPadding) { + aBorderAndPadding.SizeTo(0, 0, 0, 0); + nsresult rv = GetXULBorder(aBorderAndPadding); + if (NS_FAILED(rv)) return rv; + + nsMargin padding; + rv = GetXULPadding(padding); + if (NS_FAILED(rv)) return rv; + + aBorderAndPadding += padding; + + return rv; +} + +nsresult nsIFrame::GetXULBorder(nsMargin& aBorder) { + aBorder.SizeTo(0, 0, 0, 0); + + StyleAppearance appearance = StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + // Go to the theme for the border. + nsPresContext* pc = PresContext(); + nsITheme* theme = pc->Theme(); + if (theme->ThemeSupportsWidget(pc, this, appearance)) { + LayoutDeviceIntMargin margin = + theme->GetWidgetBorder(pc->DeviceContext(), this, appearance); + aBorder = + LayoutDevicePixel::ToAppUnits(margin, pc->AppUnitsPerDevPixel()); + return NS_OK; + } + } + + aBorder = StyleBorder()->GetComputedBorder(); + + return NS_OK; +} + +nsresult nsIFrame::GetXULPadding(nsMargin& aBorderAndPadding) { + StyleAppearance appearance = StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + // Go to the theme for the padding. + nsPresContext* pc = PresContext(); + nsITheme* theme = pc->Theme(); + if (theme->ThemeSupportsWidget(pc, this, appearance)) { + LayoutDeviceIntMargin padding; + bool useThemePadding = theme->GetWidgetPadding(pc->DeviceContext(), this, + appearance, &padding); + if (useThemePadding) { + aBorderAndPadding = + LayoutDevicePixel::ToAppUnits(padding, pc->AppUnitsPerDevPixel()); + return NS_OK; + } + } + } + + aBorderAndPadding.SizeTo(0, 0, 0, 0); + StylePadding()->GetPadding(aBorderAndPadding); + + return NS_OK; +} + +nsresult nsIFrame::GetXULMargin(nsMargin& aMargin) { + aMargin.SizeTo(0, 0, 0, 0); + StyleMargin()->GetMargin(aMargin); + + return NS_OK; +} + +void nsIFrame::XULSizeNeedsRecalc(nsSize& aSize) { + aSize.width = -1; + aSize.height = -1; +} + +void nsIFrame::XULCoordNeedsRecalc(nscoord& aCoord) { aCoord = -1; } + +bool nsIFrame::XULNeedsRecalc(const nsSize& aSize) { + return (aSize.width == -1 || aSize.height == -1); +} + +bool nsIFrame::XULNeedsRecalc(nscoord aCoord) { return (aCoord == -1); } + +nsSize nsIFrame::GetUncachedXULPrefSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize pref(0, 0); + DISPLAY_PREF_SIZE(this, pref); + + if (IsXULCollapsed()) { + return pref; + } + + AddXULBorderAndPadding(pref); + bool widthSet, heightSet; + nsIFrame::AddXULPrefSize(this, pref, widthSet, heightSet); + + nsSize minSize = GetXULMinSize(aBoxLayoutState); + nsSize maxSize = GetXULMaxSize(aBoxLayoutState); + return XULBoundsCheck(minSize, pref, maxSize); +} + +nsSize nsIFrame::GetUncachedXULMinSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize min(0, 0); + DISPLAY_MIN_SIZE(this, min); + + if (IsXULCollapsed()) { + return min; + } + + AddXULBorderAndPadding(min); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(this, min, widthSet, heightSet); + return min; +} + +nsSize nsIFrame::GetUncachedXULMaxSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + DISPLAY_MAX_SIZE(this, maxSize); + + if (IsXULCollapsed()) { + return maxSize; + } + + AddXULBorderAndPadding(maxSize); + bool widthSet, heightSet; + nsIFrame::AddXULMaxSize(this, maxSize, widthSet, heightSet); + return maxSize; +} + +bool nsIFrame::IsXULCollapsed() { + return StyleVisibility()->mVisible == StyleVisibility::Collapse; +} + +nsresult nsIFrame::XULLayout(nsBoxLayoutState& aState) { + NS_ASSERTION(aState.GetRenderingContext(), "must have rendering context"); + + nsIFrame* box = static_cast<nsIFrame*>(this); + DISPLAY_LAYOUT(box); + + box->BeginXULLayout(aState); + + box->DoXULLayout(aState); + + box->EndXULLayout(aState); + + return NS_OK; +} + +bool nsIFrame::DoesClipChildrenInBothAxes() { + const nsStyleDisplay* display = StyleDisplay(); + return display->mOverflowX == StyleOverflow::Clip && + display->mOverflowY == StyleOverflow::Clip; +} + +nsresult nsIFrame::SyncXULLayout(nsBoxLayoutState& aBoxLayoutState) { + /* + if (IsXULCollapsed()) { + CollapseChild(aBoxLayoutState, this, true); + return NS_OK; + } + */ + + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + XULRedraw(aBoxLayoutState); + } + + RemoveStateBits(NS_FRAME_HAS_DIRTY_CHILDREN | NS_FRAME_IS_DIRTY | + NS_FRAME_FIRST_REFLOW | NS_FRAME_IN_REFLOW); + + nsPresContext* presContext = aBoxLayoutState.PresContext(); + + ReflowChildFlags flags = GetXULLayoutFlags() | aBoxLayoutState.LayoutFlags(); + + nsRect inkOverflow; + + if (XULComputesOwnOverflowArea()) { + inkOverflow = InkOverflowRect(); + } else { + nsRect rect(nsPoint(0, 0), GetSize()); + OverflowAreas overflowAreas(rect, rect); + if (!DoesClipChildrenInBothAxes() && !IsXULCollapsed()) { + // See if our child frames caused us to overflow after being laid + // out. If so, store the overflow area. This normally can't happen + // in XUL, but it can happen with the CSS 'outline' property and + // possibly with other exotic stuff (e.g. relatively positioned + // frames in HTML inside XUL). + nsLayoutUtils::UnionChildOverflow(this, overflowAreas); + } + + FinishAndStoreOverflow(overflowAreas, GetSize()); + inkOverflow = overflowAreas.InkOverflow(); + } + + nsView* view = GetView(); + if (view) { + // Make sure the frame's view is properly sized and positioned and has + // things like opacity correct + nsContainerFrame::SyncFrameViewAfterReflow(presContext, this, view, + inkOverflow, flags); + } + + return NS_OK; +} + +nsresult nsIFrame::XULRedraw(nsBoxLayoutState& aState) { + if (aState.PaintingDisabled()) return NS_OK; + + // Unclear whether we could get away with just InvalidateFrame(). + InvalidateFrameSubtree(); + + return NS_OK; +} + +bool nsIFrame::AddXULPrefSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet, + bool& aHeightSet) { + aWidthSet = false; + aHeightSet = false; + + // add in the css min, max, pref + const nsStylePosition* position = aBox->StylePosition(); + + // see if the width or height was specifically set + // XXX Handle eStyleUnit_Enumerated? + // (Handling the eStyleUnit_Enumerated types requires + // GetXULPrefSize/GetXULMinSize methods that don't consider + // (min-/max-/)(width/height) properties.) + const auto& width = position->mWidth; + if (width.ConvertsToLength()) { + aSize.width = std::max(0, width.ToLength()); + aWidthSet = true; + } + + const auto& height = position->mHeight; + if (height.ConvertsToLength()) { + aSize.height = std::max(0, height.ToLength()); + aHeightSet = true; + } + + nsIContent* content = aBox->GetContent(); + // ignore 'height' and 'width' attributes if the actual element is not XUL + // For example, we might be magic XUL frames whose primary content is an HTML + // <select> + if (content && content->IsXULElement()) { + nsAutoString value; + nsresult error; + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::width, value); + if (!value.IsEmpty()) { + value.Trim("%"); + + aSize.width = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + aWidthSet = true; + } + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::height, value); + if (!value.IsEmpty()) { + value.Trim("%"); + + aSize.height = + nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + aHeightSet = true; + } + } + + return (aWidthSet && aHeightSet); +} + +bool nsIFrame::AddXULMinSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet, + bool& aHeightSet) { + aWidthSet = false; + aHeightSet = false; + + nsPresContext* pc = aBox->PresContext(); + + // See if a native theme wants to supply a minimum size. + const nsStyleDisplay* display = aBox->StyleDisplay(); + if (display->HasAppearance()) { + nsITheme* theme = pc->Theme(); + StyleAppearance appearance = display->EffectiveAppearance(); + if (theme->ThemeSupportsWidget(pc, aBox, appearance)) { + LayoutDeviceIntSize size = + theme->GetMinimumWidgetSize(pc, aBox, appearance); + if (size.width) { + aSize.width = pc->DevPixelsToAppUnits(size.width); + aWidthSet = true; + } + if (size.height) { + aSize.height = pc->DevPixelsToAppUnits(size.height); + aHeightSet = true; + } + } else { + switch (appearance) { + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: { + ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aBox); + auto sizes = theme->GetScrollbarSizes( + pc, style->StyleUIReset()->ScrollbarWidth(), + nsITheme::Overlay::No); + if (appearance == StyleAppearance::ScrollbarVertical) { + aSize.width = pc->DevPixelsToAppUnits(sizes.mVertical); + aWidthSet = true; + } else { + aSize.height = pc->DevPixelsToAppUnits(sizes.mHorizontal); + aHeightSet = true; + } + break; + } + default: + break; + } + } + } + + // add in the css min, max, pref + const nsStylePosition* position = aBox->StylePosition(); + const auto& minWidth = position->mMinWidth; + if (minWidth.ConvertsToLength()) { + nscoord min = minWidth.ToLength(); + if (!aWidthSet || min > aSize.width) { + aSize.width = min; + aWidthSet = true; + } + } else if (minWidth.ConvertsToPercentage()) { + NS_ASSERTION(minWidth.ToPercentage() == 0.0f, + "Non-zero percentage values not currently supported"); + aSize.width = 0; + aWidthSet = true; // FIXME: should we really do this for + // nonzero values? + } + // XXX Handle ExtremumLength? + // (Handling them requires GetXULPrefSize/GetXULMinSize methods that don't + // consider (min-/max-/)(width/height) properties. + // calc() with percentage is treated like '0' (unset) + + const auto& minHeight = position->mMinHeight; + if (minHeight.ConvertsToLength()) { + nscoord min = minHeight.ToLength(); + if (!aHeightSet || min > aSize.height) { + aSize.height = min; + aHeightSet = true; + } + } else if (minHeight.ConvertsToPercentage()) { + NS_ASSERTION(position->mMinHeight.ToPercentage() == 0.0f, + "Non-zero percentage values not currently supported"); + aSize.height = 0; + aHeightSet = true; // FIXME: should we really do this for + // nonzero values? + } + // calc() with percentage is treated like '0' (unset) + + nsIContent* content = aBox->GetContent(); + if (content && content->IsXULElement()) { + nsAutoString value; + nsresult error; + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::minwidth, + value); + if (!value.IsEmpty()) { + value.Trim("%"); + + nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + if (val > aSize.width) aSize.width = val; + aWidthSet = true; + } + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::minheight, + value); + if (!value.IsEmpty()) { + value.Trim("%"); + + nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + if (val > aSize.height) aSize.height = val; + + aHeightSet = true; + } + } + + return (aWidthSet && aHeightSet); +} + +bool nsIFrame::AddXULMaxSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet, + bool& aHeightSet) { + aWidthSet = false; + aHeightSet = false; + + // add in the css min, max, pref + const nsStylePosition* position = aBox->StylePosition(); + + // and max + // see if the width or height was specifically set + // XXX Handle eStyleUnit_Enumerated? + // (Handling the eStyleUnit_Enumerated types requires + // GetXULPrefSize/GetXULMinSize methods that don't consider + // (min-/max-/)(width/height) properties.) + const auto& maxWidth = position->mMaxWidth; + if (maxWidth.ConvertsToLength()) { + aSize.width = maxWidth.ToLength(); + aWidthSet = true; + } + // percentages and calc() with percentages are treated like 'none' + + const auto& maxHeight = position->mMaxHeight; + if (maxHeight.ConvertsToLength()) { + aSize.height = maxHeight.ToLength(); + aHeightSet = true; + } + // percentages and calc() with percentages are treated like 'none' + + nsIContent* content = aBox->GetContent(); + if (content && content->IsXULElement()) { + nsAutoString value; + nsresult error; + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::maxwidth, + value); + if (!value.IsEmpty()) { + value.Trim("%"); + + nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + aSize.width = val; + aWidthSet = true; + } + + content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::maxheight, + value); + if (!value.IsEmpty()) { + value.Trim("%"); + + nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + aSize.height = val; + + aHeightSet = true; + } + } + + return (aWidthSet || aHeightSet); +} + +void nsIFrame::AddXULBorderAndPadding(nsSize& aSize) { + AddXULBorderAndPadding(this, aSize); +} + +void nsIFrame::AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize) { + nsMargin borderPadding(0, 0, 0, 0); + aBox->GetXULBorderAndPadding(borderPadding); + AddXULMargin(aSize, borderPadding); +} + +void nsIFrame::AddXULMargin(nsIFrame* aChild, nsSize& aSize) { + nsMargin margin(0, 0, 0, 0); + aChild->GetXULMargin(margin); + AddXULMargin(aSize, margin); +} + +void nsIFrame::AddXULMargin(nsSize& aSize, const nsMargin& aMargin) { + if (aSize.width != NS_UNCONSTRAINEDSIZE) + aSize.width += aMargin.left + aMargin.right; + + if (aSize.height != NS_UNCONSTRAINEDSIZE) + aSize.height += aMargin.top + aMargin.bottom; +} + +nscoord nsIFrame::XULBoundsCheck(nscoord aMin, nscoord aPref, nscoord aMax) { + if (aPref > aMax) aPref = aMax; + + if (aPref < aMin) aPref = aMin; + + return aPref; +} + +nsSize nsIFrame::XULBoundsCheckMinMax(const nsSize& aMinSize, + const nsSize& aMaxSize) { + return nsSize(std::max(aMaxSize.width, aMinSize.width), + std::max(aMaxSize.height, aMinSize.height)); +} + +nsSize nsIFrame::XULBoundsCheck(const nsSize& aMinSize, const nsSize& aPrefSize, + const nsSize& aMaxSize) { + return nsSize( + XULBoundsCheck(aMinSize.width, aPrefSize.width, aMaxSize.width), + XULBoundsCheck(aMinSize.height, aPrefSize.height, aMaxSize.height)); +} + +/*static*/ +nsIFrame* nsIFrame::GetChildXULBox(const nsIFrame* aFrame) { + // box layout ends at box-wrapped frames, so don't allow these frames + // to report child boxes. + return aFrame->IsXULBoxFrame() ? aFrame->PrincipalChildList().FirstChild() + : nullptr; +} + +/*static*/ +nsIFrame* nsIFrame::GetNextXULBox(const nsIFrame* aFrame) { + return aFrame->GetParent() && aFrame->GetParent()->IsXULBoxFrame() + ? aFrame->GetNextSibling() + : nullptr; +} + +/*static*/ +nsIFrame* nsIFrame::GetParentXULBox(const nsIFrame* aFrame) { + return aFrame->GetParent() && aFrame->GetParent()->IsXULBoxFrame() + ? aFrame->GetParent() + : nullptr; +} diff --git a/layout/xul/nsBoxFrame.cpp b/layout/xul/nsBoxFrame.cpp new file mode 100644 index 0000000000..4a547dda98 --- /dev/null +++ b/layout/xul/nsBoxFrame.cpp @@ -0,0 +1,1035 @@ +/* -*- 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 +// + +// How boxes layout +// ---------------- +// Boxes layout a bit differently than html. html does a bottom up layout. Where +// boxes do a top down. +// +// 1) First thing a box does it goes out and askes each child for its min, max, +// and preferred sizes. +// +// 2) It then adds them up to determine its size. +// +// 3) If the box was asked to layout it self intrinically it will layout its +// children at their preferred size otherwise it will layout the child at +// the size it was told to. It will squeeze or stretch its children if +// Necessary. +// +// However there is a catch. Some html components like block frames can not +// determine their preferred size. this is their size if they were laid out +// intrinsically. So the box will flow the child to determine this can cache the +// value. + +// Boxes and Incremental Reflow +// ---------------------------- +// Boxes layout out top down by adding up their children's min, max, and +// preferred sizes. Only problem is if a incremental reflow occurs. The +// preferred size of a child deep in the hierarchy could change. And this could +// change any number of syblings around the box. Basically any children in the +// reflow chain must have their caches cleared so when asked for there current +// size they can relayout themselves. + +#include "nsBoxFrame.h" + +#include <algorithm> +#include <utility> + +#include "gfxUtils.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/CSSOrderAwareFrameIterator.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Touch.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/gfxVars.h" +#include "nsBoxLayout.h" +#include "nsBoxLayoutState.h" +#include "nsCOMPtr.h" +#include "nsCSSAnonBoxes.h" +#include "nsCSSRendering.h" +#include "nsContainerFrame.h" +#include "nsDisplayList.h" +#include "nsGkAtoms.h" +#include "nsHTMLParts.h" +#include "nsIContent.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsITheme.h" +#include "nsIWidget.h" +#include "nsLayoutUtils.h" +#include "nsNameSpaceManager.h" +#include "nsPlaceholderFrame.h" +#include "nsPresContext.h" +#include "nsSliderFrame.h" +#include "nsSprocketLayout.h" +#include "nsStyleConsts.h" +#include "nsTransform2D.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsWidgetsCID.h" + +// Needed for Print Preview + +#include "mozilla/TouchEvents.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::gfx; + +nsContainerFrame* NS_NewBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsBoxFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsBoxFrame) + +#ifdef DEBUG +NS_QUERYFRAME_HEAD(nsBoxFrame) + NS_QUERYFRAME_ENTRY(nsBoxFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) +#endif + +nsBoxFrame::nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + ClassID aID) + : nsContainerFrame(aStyle, aPresContext, aID), mAscent(0) { + AddStateBits(NS_STATE_IS_HORIZONTAL | NS_STATE_AUTO_STRETCH); + + mValign = vAlign_Top; + mHalign = hAlign_Left; + + // Use the static sprocket layout + nsCOMPtr<nsBoxLayout> layout; + NS_NewSprocketLayout(layout); + SetXULLayoutManager(layout); +} + +nsBoxFrame::~nsBoxFrame() = default; + +nsIFrame* nsBoxFrame::SlowOrdinalGroupAwareSibling(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::BoxOrdinalGroup); + + 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 nsBoxFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); + if (aListID == FrameChildListID::Principal) { + // initialize our list of infos. + nsBoxLayoutState state(PresContext()); + if (mLayoutManager) + mLayoutManager->ChildrenSet(this, state, mFrames.FirstChild()); + } +} + +/* virtual */ +void nsBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + nsContainerFrame::DidSetComputedStyle(aOldComputedStyle); + + // The values that CacheAttributes() computes depend on our style, + // so we need to recompute them here... + CacheAttributes(); +} + +/** + * Initialize us. This is a good time to get the alignment of the box + */ +void nsBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + + if (HasAnyStateBits(NS_FRAME_FONT_INFLATION_CONTAINER)) { + AddStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT); + } + + MarkIntrinsicISizesDirty(); + + CacheAttributes(); +} + +void nsBoxFrame::CacheAttributes() { + /* + printf("Caching: "); + XULDumpBox(stdout); + printf("\n"); + */ + + mValign = vAlign_Top; + mHalign = hAlign_Left; + + bool orient = false; + GetInitialOrientation(orient); + if (orient) + AddStateBits(NS_STATE_IS_HORIZONTAL); + else + RemoveStateBits(NS_STATE_IS_HORIZONTAL); + + bool normal = true; + GetInitialDirection(normal); + if (normal) + AddStateBits(NS_STATE_IS_DIRECTION_NORMAL); + else + RemoveStateBits(NS_STATE_IS_DIRECTION_NORMAL); + + GetInitialVAlignment(mValign); + GetInitialHAlignment(mHalign); + + bool autostretch = !!(mState & NS_STATE_AUTO_STRETCH); + GetInitialAutoStretch(autostretch); + if (autostretch) + AddStateBits(NS_STATE_AUTO_STRETCH); + else + RemoveStateBits(NS_STATE_AUTO_STRETCH); +} + +bool nsBoxFrame::GetInitialHAlignment(nsBoxFrame::Halignment& aHalign) { + if (!GetContent()) return false; + + // For horizontal boxes we're checking PACK. For vertical boxes we are + // checking ALIGN. + const nsStyleXUL* boxInfo = StyleXUL(); + if (IsXULHorizontal()) { + switch (boxInfo->mBoxPack) { + case StyleBoxPack::Start: + aHalign = nsBoxFrame::hAlign_Left; + return true; + case StyleBoxPack::Center: + aHalign = nsBoxFrame::hAlign_Center; + return true; + case StyleBoxPack::End: + aHalign = nsBoxFrame::hAlign_Right; + return true; + default: // Nonsensical value. Just bail. + return false; + } + } else { + switch (boxInfo->mBoxAlign) { + case StyleBoxAlign::Start: + aHalign = nsBoxFrame::hAlign_Left; + return true; + case StyleBoxAlign::Center: + aHalign = nsBoxFrame::hAlign_Center; + return true; + case StyleBoxAlign::End: + aHalign = nsBoxFrame::hAlign_Right; + return true; + default: // Nonsensical value. Just bail. + return false; + } + } +} + +bool nsBoxFrame::GetInitialVAlignment(nsBoxFrame::Valignment& aValign) { + if (!GetContent()) return false; + // For horizontal boxes we're checking ALIGN. For vertical boxes we are + // checking PACK. + const nsStyleXUL* boxInfo = StyleXUL(); + if (IsXULHorizontal()) { + switch (boxInfo->mBoxAlign) { + case StyleBoxAlign::Start: + aValign = nsBoxFrame::vAlign_Top; + return true; + case StyleBoxAlign::Center: + aValign = nsBoxFrame::vAlign_Middle; + return true; + case StyleBoxAlign::Baseline: + aValign = nsBoxFrame::vAlign_BaseLine; + return true; + case StyleBoxAlign::End: + aValign = nsBoxFrame::vAlign_Bottom; + return true; + default: // Nonsensical value. Just bail. + return false; + } + } else { + switch (boxInfo->mBoxPack) { + case StyleBoxPack::Start: + aValign = nsBoxFrame::vAlign_Top; + return true; + case StyleBoxPack::Center: + aValign = nsBoxFrame::vAlign_Middle; + return true; + case StyleBoxPack::End: + aValign = nsBoxFrame::vAlign_Bottom; + return true; + default: // Nonsensical value. Just bail. + return false; + } + } +} + +void nsBoxFrame::GetInitialOrientation(bool& aIsHorizontal) { + // see if we are a vertical or horizontal box. + if (!GetContent()) return; + + const nsStyleXUL* boxInfo = StyleXUL(); + if (boxInfo->mBoxOrient == StyleBoxOrient::Horizontal) { + aIsHorizontal = true; + } else { + aIsHorizontal = false; + } +} + +void nsBoxFrame::GetInitialDirection(bool& aIsNormal) { + if (!GetContent()) return; + + if (IsXULHorizontal()) { + // For horizontal boxes only, we initialize our value based off the CSS + // 'direction' property. This means that BiDI users will end up with + // horizontally inverted chrome. + // + // If text runs RTL then so do we. + aIsNormal = StyleVisibility()->mDirection == StyleDirection::Ltr; + if (GetContent()->IsElement()) { + Element* element = GetContent()->AsElement(); + + // Now see if we have an attribute. The attribute overrides + // the style system 'direction' property. + static Element::AttrValuesArray strings[] = {nsGkAtoms::ltr, + nsGkAtoms::rtl, nullptr}; + int32_t index = element->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters); + if (index >= 0) { + bool values[] = {true, false}; + aIsNormal = values[index]; + } + } + } else { + aIsNormal = true; // Assume a normal direction in the vertical case. + } + + // Now check the style system to see if we should invert aIsNormal. + const nsStyleXUL* boxInfo = StyleXUL(); + if (boxInfo->mBoxDirection == StyleBoxDirection::Reverse) { + aIsNormal = !aIsNormal; // Invert our direction. + } +} + +/* Returns true if it was set. + */ +bool nsBoxFrame::GetInitialAutoStretch(bool& aStretch) { + if (!GetContent()) return false; + + // Check the CSS box-align property. + const nsStyleXUL* boxInfo = StyleXUL(); + aStretch = (boxInfo->mBoxAlign == StyleBoxAlign::Stretch); + + return true; +} + +void nsBoxFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + nsFrameState preserveBits = + mState & (NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN); + nsIFrame::DidReflow(aPresContext, aReflowInput); + AddStateBits(preserveBits); + if (preserveBits & NS_FRAME_IS_DIRTY) { + this->MarkSubtreeDirty(); + } +} + +bool nsBoxFrame::HonorPrintBackgroundSettings() const { + return !mContent->IsInNativeAnonymousSubtree() && + nsContainerFrame::HonorPrintBackgroundSettings(); +} + +#ifdef DO_NOISY_REFLOW +static int myCounter = 0; +static void printSize(char* aDesc, nscoord aSize) { + printf(" %s: ", aDesc); + if (aSize == NS_UNCONSTRAINEDSIZE) { + printf("UC"); + } else { + printf("%d", aSize); + } +} +#endif + +/* virtual */ +nscoord nsBoxFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + nsBoxLayoutState state(PresContext(), aRenderingContext); + nsSize minSize = GetXULMinSize(state); + + // GetXULMinSize returns border-box width, and we want to return content + // width. Since Reflow uses the reflow input's border and padding, we + // actually just want to subtract what GetXULMinSize added, which is the + // result of GetXULBorderAndPadding. + nsMargin bp; + GetXULBorderAndPadding(bp); + + result = minSize.width - bp.LeftRight(); + result = std::max(result, 0); + + return result; +} + +/* virtual */ +nscoord nsBoxFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + nsBoxLayoutState state(PresContext(), aRenderingContext); + nsSize prefSize = GetXULPrefSize(state); + + // GetXULPrefSize returns border-box width, and we want to return content + // width. Since Reflow uses the reflow input's border and padding, we + // actually just want to subtract what GetXULPrefSize added, which is the + // result of GetXULBorderAndPadding. + nsMargin bp; + GetXULBorderAndPadding(bp); + + result = prefSize.width - bp.LeftRight(); + result = std::max(result, 0); + + return result; +} + +void nsBoxFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + // If you make changes to this method, please keep nsLeafBoxFrame::Reflow + // in sync, if the changes are applicable there. + + DO_GLOBAL_REFLOW_COUNT("nsBoxFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION( + aReflowInput.ComputedWidth() >= 0 && aReflowInput.ComputedHeight() >= 0, + "Computed Size < 0"); + +#ifdef DO_NOISY_REFLOW + printf( + "\n-------------Starting BoxFrame Reflow ----------------------------\n"); + printf("%p ** nsBF::Reflow %d ", this, myCounter++); + + printSize("AW", aReflowInput.AvailableWidth()); + printSize("AH", aReflowInput.AvailableHeight()); + printSize("CW", aReflowInput.ComputedWidth()); + printSize("CH", aReflowInput.ComputedHeight()); + + printf(" *\n"); + +#endif + + // create the layout state + nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext, + &aReflowInput, aReflowInput.mReflowDepth); + + WritingMode wm = aReflowInput.GetWritingMode(); + LogicalSize computedSize = aReflowInput.ComputedSize(); + + LogicalMargin m = aReflowInput.ComputedLogicalBorderPadding(wm); + // GetXULBorderAndPadding(m); + + LogicalSize prefSize(wm); + + // if we are told to layout intrinsic then get our preferred size. + NS_ASSERTION(computedSize.ISize(wm) != NS_UNCONSTRAINEDSIZE, + "computed inline size should always be computed"); + if (computedSize.BSize(wm) == NS_UNCONSTRAINEDSIZE) { + nsSize physicalPrefSize = GetXULPrefSize(state); + nsSize minSize = GetXULMinSize(state); + nsSize maxSize = GetXULMaxSize(state); + // XXXbz isn't GetXULPrefSize supposed to bounds-check for us? + physicalPrefSize = XULBoundsCheck(minSize, physicalPrefSize, maxSize); + prefSize = LogicalSize(wm, physicalPrefSize); + } + + // get our desiredSize + computedSize.ISize(wm) += m.IStart(wm) + m.IEnd(wm); + + if (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE) { + computedSize.BSize(wm) = prefSize.BSize(wm); + // prefSize is border-box but min/max constraints are content-box. + nscoord blockDirBorderPadding = + aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm); + nscoord contentBSize = computedSize.BSize(wm) - blockDirBorderPadding; + // Note: contentHeight might be negative, but that's OK because min-height + // is never negative. + computedSize.BSize(wm) = + aReflowInput.ApplyMinMaxHeight(contentBSize) + blockDirBorderPadding; + } else { + computedSize.BSize(wm) += m.BStart(wm) + m.BEnd(wm); + } + + nsSize physicalSize = computedSize.GetPhysicalSize(wm); + nsRect r(mRect.x, mRect.y, physicalSize.width, physicalSize.height); + + SetXULBounds(state, r); + + // layout our children + XULLayout(state); + + // ok our child could have gotten bigger. So lets get its bounds + + // get the ascent + LogicalSize boxSize = GetLogicalSize(wm); + nscoord ascent = boxSize.BSize(wm); + + // getting the ascent could be a lot of work. Don't get it if + // we are the root. The viewport doesn't care about it. + if (!Style()->IsRootElementStyle()) { + ascent = GetXULBoxAscent(state); + } + + aDesiredSize.SetSize(wm, boxSize); + aDesiredSize.SetBlockStartAscent(ascent); + + aDesiredSize.mOverflowAreas = GetOverflowAreas(); + +#ifdef DO_NOISY_REFLOW + { + printf("%p ** nsBF(done) W:%d H:%d ", this, aDesiredSize.Width(), + aDesiredSize.Height()); + + if (maxElementSize) { + printf("MW:%d\n", *maxElementWidth); + } else { + printf("MW:?\n"); + } + } +#endif + + ReflowAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus); +} + +nsSize nsBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize size(0, 0); + DISPLAY_PREF_SIZE(this, size); + if (!XULNeedsRecalc(mPrefSize)) { + size = mPrefSize; + return size; + } + + if (IsXULCollapsed()) return size; + + // if the size was not completely redefined in CSS then ask our children + bool widthSet, heightSet; + if (!nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet)) { + if (mLayoutManager) { + nsSize layoutSize = mLayoutManager->GetXULPrefSize(this, aBoxLayoutState); + if (!widthSet) size.width = layoutSize.width; + if (!heightSet) size.height = layoutSize.height; + } else { + size = nsIFrame::GetUncachedXULPrefSize(aBoxLayoutState); + } + } + + nsSize minSize = GetXULMinSize(aBoxLayoutState); + nsSize maxSize = GetXULMaxSize(aBoxLayoutState); + mPrefSize = XULBoundsCheck(minSize, size, maxSize); + + return mPrefSize; +} + +nscoord nsBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) { + if (!XULNeedsRecalc(mAscent)) { + return mAscent; + } + + if (IsXULCollapsed()) { + return 0; + } + + if (mLayoutManager) { + mAscent = mLayoutManager->GetAscent(this, aBoxLayoutState); + } else { + mAscent = GetXULPrefSize(aBoxLayoutState).height; + } + + return mAscent; +} + +nsSize nsBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize size(0, 0); + DISPLAY_MIN_SIZE(this, size); + if (!XULNeedsRecalc(mMinSize)) { + size = mMinSize; + return size; + } + + if (IsXULCollapsed()) return size; + + // if the size was not completely redefined in CSS then ask our children + bool widthSet, heightSet; + if (!nsIFrame::AddXULMinSize(this, size, widthSet, heightSet)) { + if (mLayoutManager) { + nsSize layoutSize = mLayoutManager->GetXULMinSize(this, aBoxLayoutState); + if (!widthSet) size.width = layoutSize.width; + if (!heightSet) size.height = layoutSize.height; + } else { + size = nsIFrame::GetUncachedXULMinSize(aBoxLayoutState); + } + } + + mMinSize = size; + + return size; +} + +nsSize nsBoxFrame::GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) { + NS_ASSERTION(aBoxLayoutState.GetRenderingContext(), + "must have rendering context"); + + nsSize size(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + DISPLAY_MAX_SIZE(this, size); + if (!XULNeedsRecalc(mMaxSize)) { + size = mMaxSize; + return size; + } + + if (IsXULCollapsed()) return size; + + // if the size was not completely redefined in CSS then ask our children + bool widthSet, heightSet; + if (!nsIFrame::AddXULMaxSize(this, size, widthSet, heightSet)) { + if (mLayoutManager) { + nsSize layoutSize = mLayoutManager->GetXULMaxSize(this, aBoxLayoutState); + if (!widthSet) size.width = layoutSize.width; + if (!heightSet) size.height = layoutSize.height; + } else { + size = nsIFrame::GetUncachedXULMaxSize(aBoxLayoutState); + } + } + + mMaxSize = size; + + return size; +} + +/** + * If subclassing please subclass this method not layout. + * layout will call this method. + */ +NS_IMETHODIMP +nsBoxFrame::DoXULLayout(nsBoxLayoutState& aState) { + ReflowChildFlags oldFlags = aState.LayoutFlags(); + aState.SetLayoutFlags(ReflowChildFlags::Default); + + nsresult rv = NS_OK; + if (mLayoutManager) { + XULCoordNeedsRecalc(mAscent); + rv = mLayoutManager->XULLayout(this, aState); + } + + aState.SetLayoutFlags(oldFlags); + + if (HasAbsolutelyPositionedChildren()) { + // Set up a |reflowInput| to pass into ReflowAbsoluteFrames + WritingMode wm = GetWritingMode(); + ReflowInput reflowInput( + aState.PresContext(), this, aState.GetRenderingContext(), + LogicalSize(wm, GetLogicalSize().ISize(wm), NS_UNCONSTRAINEDSIZE)); + + // Set up a |desiredSize| to pass into ReflowAbsoluteFrames + ReflowOutput desiredSize(reflowInput); + desiredSize.Width() = mRect.width; + desiredSize.Height() = mRect.height; + + // get the ascent (cribbed from ::Reflow) + nscoord ascent = mRect.height; + + // getting the ascent could be a lot of work. Don't get it if + // we are the root. The viewport doesn't care about it. + if (!Style()->IsRootElementStyle()) { + ascent = GetXULBoxAscent(aState); + } + desiredSize.SetBlockStartAscent(ascent); + desiredSize.mOverflowAreas = GetOverflowAreas(); + + AddStateBits(NS_FRAME_IN_REFLOW); + // Set up a |reflowStatus| to pass into ReflowAbsoluteFrames + // (just a dummy value; hopefully that's OK) + nsReflowStatus reflowStatus; + ReflowAbsoluteFrames(aState.PresContext(), desiredSize, reflowInput, + reflowStatus); + RemoveStateBits(NS_FRAME_IN_REFLOW); + } + + return rv; +} + +void nsBoxFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + // clean up the container box's layout manager and child boxes + SetXULLayoutManager(nullptr); + + nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +/* virtual */ +void nsBoxFrame::MarkIntrinsicISizesDirty() { + XULSizeNeedsRecalc(mPrefSize); + XULSizeNeedsRecalc(mMinSize); + XULSizeNeedsRecalc(mMaxSize); + XULCoordNeedsRecalc(mAscent); + + if (mLayoutManager) { + nsBoxLayoutState state(PresContext()); + mLayoutManager->IntrinsicISizesDirty(this, state); + } + + nsContainerFrame::MarkIntrinsicISizesDirty(); +} + +void nsBoxFrame::RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) { + MOZ_ASSERT(aListID == FrameChildListID::Principal, + "We don't support out-of-flow kids"); + + nsPresContext* presContext = PresContext(); + nsBoxLayoutState state(presContext); + + // remove the child frame + mFrames.RemoveFrame(aOldFrame); + + // notify the layout manager + if (mLayoutManager) mLayoutManager->ChildrenRemoved(this, state, aOldFrame); + + // destroy the child frame + aOldFrame->Destroy(); + + // mark us dirty and generate a reflow command + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_HAS_DIRTY_CHILDREN); +} + +void nsBoxFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this, + "inserting after sibling frame with different parent"); + NS_ASSERTION(!aPrevFrame || mFrames.ContainsFrame(aPrevFrame), + "inserting after sibling frame not in our child list"); + MOZ_ASSERT(aListID == FrameChildListID::Principal, + "We don't support out-of-flow kids"); + + nsBoxLayoutState state(PresContext()); + + // insert the child frames + const nsFrameList::Slice& newFrames = + mFrames.InsertFrames(this, aPrevFrame, std::move(aFrameList)); + + // notify the layout manager + if (mLayoutManager) + mLayoutManager->ChildrenInserted(this, state, aPrevFrame, newFrames); + + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_HAS_DIRTY_CHILDREN); +} + +void nsBoxFrame::AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) { + MOZ_ASSERT(aListID == FrameChildListID::Principal, + "We don't support out-of-flow kids"); + + nsBoxLayoutState state(PresContext()); + + // append the new frames + const nsFrameList::Slice& newFrames = + mFrames.AppendFrames(this, std::move(aFrameList)); + + // notify the layout manager + if (mLayoutManager) mLayoutManager->ChildrenAppended(this, state, newFrames); + + // XXXbz why is this NS_FRAME_FIRST_REFLOW check here? + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_HAS_DIRTY_CHILDREN); + } +} + +nsresult nsBoxFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = + nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); + + // Ignore 'width', 'height', 'screenX', 'screenY' and 'sizemode' on a + // <window>. + if (mContent->IsXULElement(nsGkAtoms::window) && + (nsGkAtoms::width == aAttribute || nsGkAtoms::height == aAttribute || + nsGkAtoms::screenX == aAttribute || nsGkAtoms::screenY == aAttribute || + nsGkAtoms::sizemode == aAttribute)) { + return rv; + } + + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::valign || + aAttribute == nsGkAtoms::minwidth || aAttribute == nsGkAtoms::maxwidth || + aAttribute == nsGkAtoms::minheight || + aAttribute == nsGkAtoms::maxheight || aAttribute == nsGkAtoms::orient || + aAttribute == nsGkAtoms::pack || aAttribute == nsGkAtoms::dir) { + if (aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::valign || + aAttribute == nsGkAtoms::orient || aAttribute == nsGkAtoms::pack || + aAttribute == nsGkAtoms::dir) { + mValign = nsBoxFrame::vAlign_Top; + mHalign = nsBoxFrame::hAlign_Left; + + bool orient = true; + GetInitialOrientation(orient); + if (orient) + AddStateBits(NS_STATE_IS_HORIZONTAL); + else + RemoveStateBits(NS_STATE_IS_HORIZONTAL); + + bool normal = true; + GetInitialDirection(normal); + if (normal) + AddStateBits(NS_STATE_IS_DIRECTION_NORMAL); + else + RemoveStateBits(NS_STATE_IS_DIRECTION_NORMAL); + + GetInitialVAlignment(mValign); + GetInitialHAlignment(mHalign); + + bool autostretch = !!(mState & NS_STATE_AUTO_STRETCH); + GetInitialAutoStretch(autostretch); + if (autostretch) + AddStateBits(NS_STATE_AUTO_STRETCH); + else + RemoveStateBits(NS_STATE_AUTO_STRETCH); + } + + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } else if (aAttribute == nsGkAtoms::rows && + mContent->IsXULElement(nsGkAtoms::tree)) { + // Reflow ourselves and all our children if "rows" changes, since + // nsTreeBodyFrame's layout reads this from its parent (this frame). + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } + + return rv; +} + +void nsBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + nsDisplayListCollection tempLists(aBuilder); + DisplayBorderBackgroundOutline(aBuilder, aLists); + + BuildDisplayListForChildren(aBuilder, aLists); + + // see if we have to draw a selection frame around this container + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +void nsBoxFrame::BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // Iterate over the children in CSS order. + auto iter = CSSOrderAwareFrameIterator( + this, FrameChildListID::Principal, + CSSOrderAwareFrameIterator::ChildFilter::IncludeAll, + CSSOrderAwareFrameIterator::OrderState::Unknown, + CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup); + // Put each child's background onto the BlockBorderBackgrounds list + // to emulate the existing two-layer XUL painting scheme. + nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds()); + for (; !iter.AtEnd(); iter.Next()) { + BuildDisplayListForChild(aBuilder, iter.get(), set); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsBoxFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"Box"_ns, aResult); +} +#endif + +nsresult nsBoxFrame::LayoutChildAt(nsBoxLayoutState& aState, nsIFrame* aBox, + const nsRect& aRect) { + // get the current rect + nsRect oldRect(aBox->GetRect()); + aBox->SetXULBounds(aState, aRect); + + bool layout = aBox->IsSubtreeDirty(); + + if (layout || + (oldRect.width != aRect.width || oldRect.height != aRect.height)) { + return aBox->XULLayout(aState); + } + + return NS_OK; +} + +namespace mozilla { + +/** + * This wrapper class lets us redirect mouse hits from descendant frames + * of a menu to the menu itself, if they didn't specify 'allowevents'. + * + * The wrapper simply turns a hit on a descendant element + * into a hit on the menu itself, unless there is an element between the target + * and the menu with the "allowevents" attribute. + * + * This is used by nsMenuFrame and nsTreeColFrame. + * + * Note that turning a hit on a descendant element into nullptr, so events + * could fall through to the menu background, might be an appealing + * simplification but it would mean slightly strange behaviour in some cases, + * because grabber wrappers can be created for many individual lists and items, + * so the exact fallthrough behaviour would be complex. E.g. an element with + * "allowevents" on top of the Content() list could receive the event even if it + * was covered by a PositionedDescenants() element without "allowevents". It is + * best to never convert a non-null hit into null. + */ +// REVIEW: This is roughly of what nsMenuFrame::GetFrameForPoint used to do. +// I've made 'allowevents' affect child elements because that seems the only +// reasonable thing to do. +class nsDisplayXULEventRedirector final : public nsDisplayWrapList { + public: + nsDisplayXULEventRedirector(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayItem* aItem, nsIFrame* aTargetFrame) + : nsDisplayWrapList(aBuilder, aFrame, aItem), + mTargetFrame(aTargetFrame) {} + nsDisplayXULEventRedirector(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayList* aList, nsIFrame* aTargetFrame) + : nsDisplayWrapList(aBuilder, aFrame, aList), + mTargetFrame(aTargetFrame) {} + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) override; + virtual bool ShouldFlattenAway(nsDisplayListBuilder* aBuilder) override { + return false; + } + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { + GetChildren()->Paint(aBuilder, aCtx, + mFrame->PresContext()->AppUnitsPerDevPixel()); + } + NS_DISPLAY_DECL_NAME("XULEventRedirector", TYPE_XUL_EVENT_REDIRECTOR) + private: + nsIFrame* mTargetFrame; +}; + +void nsDisplayXULEventRedirector::HitTest(nsDisplayListBuilder* aBuilder, + const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) { + nsTArray<nsIFrame*> outFrames; + mList.HitTest(aBuilder, aRect, aState, &outFrames); + + bool topMostAdded = false; + uint32_t localLength = outFrames.Length(); + + for (uint32_t i = 0; i < localLength; i++) { + for (nsIContent* content = outFrames.ElementAt(i)->GetContent(); + content && content != mTargetFrame->GetContent(); + content = content->GetParent()) { + if (!content->IsElement() || + !content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::allowevents, + nsGkAtoms::_true, eCaseMatters)) { + continue; + } + + // Events are allowed on 'frame', so let it go. + aOutFrames->AppendElement(outFrames.ElementAt(i)); + topMostAdded = true; + } + + // If there was no hit on the topmost frame or its ancestors, + // add the target frame itself as the first candidate (see bug 562554). + if (!topMostAdded) { + topMostAdded = true; + aOutFrames->AppendElement(mTargetFrame); + } + } +} + +} // namespace mozilla + +class nsXULEventRedirectorWrapper final : public nsDisplayItemWrapper { + public: + explicit nsXULEventRedirectorWrapper(nsIFrame* aTargetFrame) + : mTargetFrame(aTargetFrame) {} + virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, + nsDisplayList* aList) override { + return MakeDisplayItem<nsDisplayXULEventRedirector>(aBuilder, aFrame, aList, + mTargetFrame); + } + virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder, + nsDisplayItem* aItem) override { + return MakeDisplayItem<nsDisplayXULEventRedirector>( + aBuilder, aItem->Frame(), aItem, mTargetFrame); + } + + private: + nsIFrame* mTargetFrame; +}; + +void nsBoxFrame::WrapListsInRedirector(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aIn, + const nsDisplayListSet& aOut) { + nsXULEventRedirectorWrapper wrapper(this); + wrapper.WrapLists(aBuilder, this, aIn, aOut); +} + +bool nsBoxFrame::GetEventPoint(WidgetGUIEvent* aEvent, nsPoint& aPoint) { + LayoutDeviceIntPoint refPoint; + bool res = GetEventPoint(aEvent, refPoint); + aPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, refPoint, + RelativeTo{this}); + return res; +} + +bool nsBoxFrame::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; +} diff --git a/layout/xul/nsBoxFrame.h b/layout/xul/nsBoxFrame.h new file mode 100644 index 0000000000..c2d34bf3dd --- /dev/null +++ b/layout/xul/nsBoxFrame.h @@ -0,0 +1,185 @@ +/* -*- 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 + nsBoxFrame is a frame that can lay its children out either vertically or +horizontally. It lays them out according to a min max or preferred size. + +**/ + +#ifndef nsBoxFrame_h___ +#define nsBoxFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsCOMPtr.h" +#include "nsContainerFrame.h" +#include "nsBoxLayout.h" + +class nsBoxLayoutState; + +namespace mozilla { +class PresShell; +namespace gfx { +class DrawTarget; +} // namespace gfx +} // namespace mozilla + +nsContainerFrame* NS_NewBoxFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +class nsBoxFrame : public nsContainerFrame { + protected: + typedef mozilla::gfx::DrawTarget DrawTarget; + + public: + NS_DECL_FRAMEARENA_HELPERS(nsBoxFrame) +#ifdef DEBUG + NS_DECL_QUERYFRAME +#endif + + friend nsContainerFrame* NS_NewBoxFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + // gets the rect inside our border and debug border. If you wish to paint + // inside a box call this method to get the rect so you don't draw on the + // debug border or outer border. + + virtual void SetXULLayoutManager(nsBoxLayout* aLayout) override { + mLayoutManager = aLayout; + } + virtual nsBoxLayout* GetXULLayoutManager() override { return mLayoutManager; } + + virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override; + virtual Valignment GetXULVAlign() const override { return mValign; } + virtual Halignment GetXULHAlign() const override { return mHalign; } + NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override; + + virtual bool XULComputesOwnOverflowArea() override { return false; } + + // ----- child and sibling operations --- + + // ----- public methods ------- + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual void MarkIntrinsicISizesDirty() override; + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + 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; + virtual void RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) override; + + virtual void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override { + // record that children that are ignorable whitespace should be excluded + // (When content was loaded via the XUL content sink, it's already + // been excluded, but we need this for when the XUL namespace is used + // in other MIME types or when the XUL CSS display types are used with + // non-XUL elements.) + + // This is bogus, but it's what we've always done. + // (Given that we're replaced, we need to say we're a replaced element + // that contains a block so ReflowInput doesn't tell us to be + // NS_UNCONSTRAINEDSIZE wide.) + return nsContainerFrame::IsFrameOfType( + aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock | eXULBox)); + } + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + virtual void DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) override; + + virtual bool HonorPrintBackgroundSettings() const override; + + // virtual so nsButtonBoxFrame, nsSliderFrame and nsMenuFrame + // can override it + virtual void BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists); + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + static nsresult LayoutChildAt(nsBoxLayoutState& aState, nsIFrame* aBox, + const nsRect& aRect); + + /** + * Utility method to redirect events on descendants to this frame. + * Supports 'allowevents' attribute on descendant elements to allow those + * elements and their descendants to receive events. + */ + void WrapListsInRedirector(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aIn, + const nsDisplayListSet& aOut); + + // Gets a next / prev sibling accounting for ordinal group. Slow, please avoid + // usage if possible. + static nsIFrame* SlowOrdinalGroupAwareSibling(nsIFrame*, bool aNext); + + private: + explicit nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID) {} + + protected: + nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, ClassID aID); + virtual ~nsBoxFrame(); + + virtual void GetInitialOrientation(bool& aIsHorizontal); + virtual void GetInitialDirection(bool& aIsNormal); + virtual bool GetInitialHAlignment(Halignment& aHalign); + virtual bool GetInitialVAlignment(Valignment& aValign); + virtual bool GetInitialAutoStretch(bool& aStretch); + + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + nsSize mPrefSize; + nsSize mMinSize; + nsSize mMaxSize; + nscoord mAscent; + + nsCOMPtr<nsBoxLayout> mLayoutManager; + + // 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 offset associated with + // this frame. Return true if a single valid point was found. + bool GetEventPoint(mozilla::WidgetGUIEvent* aEvent, + mozilla::LayoutDeviceIntPoint& aPoint); + + private: + void CacheAttributes(); + + // instance variables. + Halignment mHalign; + Valignment mValign; + +}; // class nsBoxFrame + +#endif diff --git a/layout/xul/nsBoxLayout.cpp b/layout/xul/nsBoxLayout.cpp new file mode 100644 index 0000000000..81ee41103f --- /dev/null +++ b/layout/xul/nsBoxLayout.cpp @@ -0,0 +1,74 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsContainerFrame.h" +#include "nsBoxLayout.h" + +void nsBoxLayout::AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize) { + nsIFrame::AddXULBorderAndPadding(aBox, aSize); +} + +void nsBoxLayout::AddXULMargin(nsIFrame* aChild, nsSize& aSize) { + nsIFrame::AddXULMargin(aChild, aSize); +} + +void nsBoxLayout::AddXULMargin(nsSize& aSize, const nsMargin& aMargin) { + nsIFrame::AddXULMargin(aSize, aMargin); +} + +nsSize nsBoxLayout::GetXULPrefSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) { + nsSize pref(0, 0); + AddXULBorderAndPadding(aBox, pref); + + return pref; +} + +nsSize nsBoxLayout::GetXULMinSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) { + nsSize minSize(0, 0); + AddXULBorderAndPadding(aBox, minSize); + return minSize; +} + +nsSize nsBoxLayout::GetXULMaxSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) { + // AddXULBorderAndPadding () never changes maxSize (NS_UNCONSTRAINEDSIZE) + // AddXULBorderAndPadding(aBox, maxSize); + return nsSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); +} + +nscoord nsBoxLayout::GetAscent(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) { + return 0; +} + +NS_IMETHODIMP +nsBoxLayout::XULLayout(nsIFrame* aBox, nsBoxLayoutState& aBoxLayoutState) { + return NS_OK; +} + +void nsBoxLayout::AddLargestSize(nsSize& aSize, const nsSize& aSize2) { + if (aSize2.width > aSize.width) aSize.width = aSize2.width; + + if (aSize2.height > aSize.height) aSize.height = aSize2.height; +} + +void nsBoxLayout::AddSmallestSize(nsSize& aSize, const nsSize& aSize2) { + if (aSize2.width < aSize.width) aSize.width = aSize2.width; + + if (aSize2.height < aSize.height) aSize.height = aSize2.height; +} + +NS_IMPL_ISUPPORTS(nsBoxLayout, nsBoxLayout) diff --git a/layout/xul/nsBoxLayout.h b/layout/xul/nsBoxLayout.h new file mode 100644 index 0000000000..404d7e768d --- /dev/null +++ b/layout/xul/nsBoxLayout.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 nsBoxLayout_h___ +#define nsBoxLayout_h___ + +#include "nsISupports.h" +#include "nsCoord.h" +#include "nsFrameList.h" + +class nsIFrame; +class nsBoxLayoutState; +struct nsSize; +struct nsMargin; + +#define NS_BOX_LAYOUT_IID \ + { \ + 0x09d522a7, 0x304c, 0x4137, { \ + 0xaf, 0xc9, 0xe0, 0x80, 0x2e, 0x89, 0xb7, 0xe8 \ + } \ + } + +class nsBoxLayout : public nsISupports { + protected: + virtual ~nsBoxLayout() = default; + + public: + nsBoxLayout() = default; + + NS_DECL_ISUPPORTS + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_BOX_LAYOUT_IID) + + NS_IMETHOD XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState); + + virtual nsSize GetXULPrefSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState); + virtual nsSize GetXULMinSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState); + virtual nsSize GetXULMaxSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState); + virtual nscoord GetAscent(nsIFrame* aBox, nsBoxLayoutState& aBoxLayoutState); + virtual void ChildrenInserted(nsIFrame* aBox, nsBoxLayoutState& aState, + nsIFrame* aPrevBox, + const nsFrameList::Slice& aNewChildren) {} + virtual void ChildrenAppended(nsIFrame* aBox, nsBoxLayoutState& aState, + const nsFrameList::Slice& aNewChildren) {} + virtual void ChildrenRemoved(nsIFrame* aBox, nsBoxLayoutState& aState, + nsIFrame* aChildList) {} + virtual void ChildrenSet(nsIFrame* aBox, nsBoxLayoutState& aState, + nsIFrame* aChildList) {} + virtual void IntrinsicISizesDirty(nsIFrame* aBox, nsBoxLayoutState& aState) {} + + virtual void AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize); + virtual void AddXULMargin(nsIFrame* aChild, nsSize& aSize); + virtual void AddXULMargin(nsSize& aSize, const nsMargin& aMargin); + + static void AddLargestSize(nsSize& aSize, const nsSize& aToAdd); + static void AddSmallestSize(nsSize& aSize, const nsSize& aToAdd); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsBoxLayout, NS_BOX_LAYOUT_IID) + +#endif diff --git a/layout/xul/nsBoxLayoutState.cpp b/layout/xul/nsBoxLayoutState.cpp new file mode 100644 index 0000000000..80543036f7 --- /dev/null +++ b/layout/xul/nsBoxLayoutState.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/. */ + +// +// Eric Vaughan +// Netscape Communications +// +// See documentation in associated header file +// + +#include "nsBoxLayoutState.h" + +nsBoxLayoutState::nsBoxLayoutState(nsPresContext* aPresContext, + gfxContext* aRenderingContext, + const ReflowInput* aOuterReflowInput, + uint16_t aReflowDepth) + : mPresContext(aPresContext), + mRenderingContext(aRenderingContext), + mOuterReflowInput(aOuterReflowInput), + mLayoutFlags(nsIFrame::ReflowChildFlags::Default), + mReflowDepth(aReflowDepth), + mPaintingDisabled(false) { + NS_ASSERTION(mPresContext, "PresContext must be non-null"); +} + +nsBoxLayoutState::nsBoxLayoutState(const nsBoxLayoutState& aState) + : mPresContext(aState.mPresContext), + mRenderingContext(aState.mRenderingContext), + mOuterReflowInput(aState.mOuterReflowInput), + mLayoutFlags(aState.mLayoutFlags), + mReflowDepth(aState.mReflowDepth + 1), + mPaintingDisabled(aState.mPaintingDisabled) { + NS_ASSERTION(mPresContext, "PresContext must be non-null"); +} diff --git a/layout/xul/nsBoxLayoutState.h b/layout/xul/nsBoxLayoutState.h new file mode 100644 index 0000000000..7f7c9d7a3a --- /dev/null +++ b/layout/xul/nsBoxLayoutState.h @@ -0,0 +1,79 @@ +/* -*- 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/. */ + +/** + + Author: + Eric D Vaughan + +**/ + +#ifndef nsBoxLayoutState_h___ +#define nsBoxLayoutState_h___ + +#include "nsCOMPtr.h" +#include "nsPresContext.h" +#include "nsIFrame.h" + +class gfxContext; +namespace mozilla { +class PresShell; +struct ReflowInput; +} // namespace mozilla + +class MOZ_STACK_CLASS nsBoxLayoutState { + using ReflowInput = mozilla::ReflowInput; + + public: + explicit nsBoxLayoutState(nsPresContext* aPresContext, + gfxContext* aRenderingContext = nullptr, + // see OuterReflowInput() below + const ReflowInput* aOuterReflowInput = nullptr, + uint16_t aReflowDepth = 0); + nsBoxLayoutState(const nsBoxLayoutState& aState); + + nsPresContext* PresContext() const { return mPresContext; } + mozilla::PresShell* PresShell() const { return mPresContext->PresShell(); } + + nsIFrame::ReflowChildFlags LayoutFlags() const { return mLayoutFlags; } + void SetLayoutFlags(nsIFrame::ReflowChildFlags aFlags) { + mLayoutFlags = aFlags; + } + + // if true no one under us will paint during reflow. + void SetPaintingDisabled(bool aDisable) { mPaintingDisabled = aDisable; } + bool PaintingDisabled() const { return mPaintingDisabled; } + + // The rendering context may be null for specialized uses of + // nsBoxLayoutState and should be null-checked before it is used. + // However, passing a null rendering context to the constructor when + // doing box layout or intrinsic size calculation will cause bugs. + gfxContext* GetRenderingContext() const { return mRenderingContext; } + + struct AutoReflowDepth { + explicit AutoReflowDepth(nsBoxLayoutState& aState) : mState(aState) { + ++mState.mReflowDepth; + } + ~AutoReflowDepth() { --mState.mReflowDepth; } + nsBoxLayoutState& mState; + }; + + // The HTML reflow input that lives outside the box-block boundary. + // May not be set reliably yet. + const ReflowInput* OuterReflowInput() { return mOuterReflowInput; } + + uint16_t GetReflowDepth() { return mReflowDepth; } + + private: + RefPtr<nsPresContext> mPresContext; + gfxContext* mRenderingContext; + const ReflowInput* mOuterReflowInput; + nsIFrame::ReflowChildFlags mLayoutFlags; + uint16_t mReflowDepth; + bool mPaintingDisabled; +}; + +#endif 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..68f6f8b232 --- /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 ScrollFrameHelper::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 nsIFrame* 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/nsImageBoxFrame.cpp b/layout/xul/nsImageBoxFrame.cpp new file mode 100644 index 0000000000..9e10b58f43 --- /dev/null +++ b/layout/xul/nsImageBoxFrame.cpp @@ -0,0 +1,805 @@ +/* -*- 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 "gfxContext.h" +#include "nsImageBoxFrame.h" +#include "nsGkAtoms.h" +#include "mozilla/ComputedStyle.h" +#include "nsStyleConsts.h" +#include "nsStyleUtil.h" +#include "nsCOMPtr.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsBoxLayoutState.h" + +#include "nsHTMLParts.h" +#include "nsString.h" +#include "nsLeafFrame.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsImageMap.h" +#include "nsContainerFrame.h" +#include "nsCSSRendering.h" +#include "nsNameSpaceManager.h" +#include "nsTextFragment.h" +#include "nsTransform2D.h" +#include "nsITheme.h" + +#include "nsIURI.h" +#include "nsThreadUtils.h" +#include "nsDisplayList.h" +#include "ImageRegion.h" +#include "ImageContainer.h" +#include "nsIContent.h" + +#include "nsContentUtils.h" + +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Maybe.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/SVGImageContext.h" +#include "Units.h" +#include "mozilla/image/WebRenderImageProvider.h" +#include "mozilla/layers/RenderRootStateManager.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "mozilla/dom/ImageTracker.h" + +#if defined(XP_WIN) +// Undefine LoadImage to prevent naming conflict with Windows. +# undef LoadImage +#endif + +#define ONLOAD_CALLED_TOO_EARLY 1 + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::layers; + +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::ReferrerInfo; + +class nsImageBoxFrameEvent : public Runnable { + public: + nsImageBoxFrameEvent(nsIContent* content, EventMessage message) + : mozilla::Runnable("nsImageBoxFrameEvent"), + mContent(content), + mMessage(message) {} + + NS_IMETHOD Run() override; + + private: + const nsCOMPtr<nsIContent> mContent; + EventMessage mMessage; +}; + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsImageBoxFrameEvent::Run() { + RefPtr<nsPresContext> presContext = mContent->OwnerDoc()->GetPresContext(); + if (!presContext) { + return NS_OK; + } + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetEvent event(true, mMessage); + + event.mFlags.mBubbles = false; + EventDispatcher::Dispatch(mContent, presContext, &event, nullptr, &status); + return NS_OK; +} + +// Fire off an event that'll asynchronously call the image elements +// onload handler once handled. This is needed since the image library +// can't decide if it wants to call its observer methods +// synchronously or asynchronously. If an image is loaded from the +// cache the notifications come back synchronously, but if the image +// is loaded from the network the notifications come back +// asynchronously. +static void FireImageDOMEvent(nsIContent* aContent, EventMessage aMessage) { + NS_ASSERTION(aMessage == eLoad || aMessage == eLoadError, "invalid message"); + + nsCOMPtr<nsIRunnable> event = new nsImageBoxFrameEvent(aContent, aMessage); + nsresult rv = + aContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); + if (NS_FAILED(rv)) { + NS_WARNING("failed to dispatch image event"); + } +} + +// +// NS_NewImageBoxFrame +// +// Creates a new image frame and returns it +// +nsIFrame* NS_NewImageBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsImageBoxFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsImageBoxFrame) +NS_QUERYFRAME_HEAD(nsImageBoxFrame) + NS_QUERYFRAME_ENTRY(nsImageBoxFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame) + +nsresult nsImageBoxFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = + nsLeafBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); + + if (aAttribute == nsGkAtoms::src) { + UpdateImage(); + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } else if (aAttribute == nsGkAtoms::validate) + UpdateLoadFlags(); + + return rv; +} + +nsImageBoxFrame::nsImageBoxFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsLeafBoxFrame(aStyle, aPresContext, kClassID), + mIntrinsicSize(0, 0), + mLoadFlags(nsIRequest::LOAD_NORMAL), + mRequestRegistered(false), + mUseSrcAttr(false), + mSuppressStyleCheck(false) { + MarkIntrinsicISizesDirty(); +} + +nsImageBoxFrame::~nsImageBoxFrame() = default; + +/* virtual */ +void nsImageBoxFrame::MarkIntrinsicISizesDirty() { + XULSizeNeedsRecalc(mImageSize); + nsLeafBoxFrame::MarkIntrinsicISizesDirty(); +} + +void nsImageBoxFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + if (mImageRequest) { + nsLayoutUtils::DeregisterImageRequest(PresContext(), mImageRequest, + &mRequestRegistered); + + mImageRequest->UnlockImage(); + + if (mUseSrcAttr) { + PresContext()->Document()->ImageTracker()->Remove(mImageRequest); + } + + // Release image loader first so that it's refcnt can go to zero + mImageRequest->CancelAndForgetObserver(NS_ERROR_FAILURE); + } + + if (mListener) { + // set the frame to null so we don't send messages to a dead object. + reinterpret_cast<nsImageBoxListener*>(mListener.get())->ClearFrame(); + } + + nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +void nsImageBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + if (!mListener) { + RefPtr<nsImageBoxListener> listener = new nsImageBoxListener(this); + mListener = std::move(listener); + } + + mSuppressStyleCheck = true; + nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow); + mSuppressStyleCheck = false; + + UpdateLoadFlags(); + UpdateImage(); +} + +void nsImageBoxFrame::UpdateImage() { + nsPresContext* presContext = PresContext(); + Document* doc = presContext->Document(); + + RefPtr<imgRequestProxy> oldImageRequest = mImageRequest; + + if (mImageRequest) { + nsLayoutUtils::DeregisterImageRequest(presContext, mImageRequest, + &mRequestRegistered); + mImageRequest->CancelAndForgetObserver(NS_ERROR_FAILURE); + if (mUseSrcAttr) { + doc->ImageTracker()->Remove(mImageRequest); + } + mImageRequest = nullptr; + } + + // get the new image src + nsAutoString src; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src); + mUseSrcAttr = !src.IsEmpty(); + if (mUseSrcAttr) { + nsContentPolicyType contentPolicyType; + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + uint64_t requestContextID = 0; + nsContentUtils::GetContentPolicyTypeForUIImageLoading( + mContent, getter_AddRefs(triggeringPrincipal), contentPolicyType, + &requestContextID); + + nsCOMPtr<nsIURI> uri; + nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), src, doc, + mContent->GetBaseURI()); + if (uri) { + auto referrerInfo = MakeRefPtr<ReferrerInfo>(*mContent->AsElement()); + nsresult rv = nsContentUtils::LoadImage( + uri, mContent, doc, triggeringPrincipal, requestContextID, + referrerInfo, mListener, mLoadFlags, u""_ns, + getter_AddRefs(mImageRequest), contentPolicyType); + + if (NS_SUCCEEDED(rv) && mImageRequest) { + nsLayoutUtils::RegisterImageRequestIfAnimated( + presContext, mImageRequest, &mRequestRegistered); + + // Add to the ImageTracker so that we can find it when media + // feature values change (e.g. when the system theme changes) + // and invalidate the image. This allows favicons to respond + // to these changes. + doc->ImageTracker()->Add(mImageRequest); + } + } + } else if (auto* styleImage = GetImageFromStyle()) { + if (auto* styleRequest = styleImage->GetImageRequest()) { + styleRequest->SyncClone(mListener, mContent->GetComposedDoc(), + getter_AddRefs(mImageRequest)); + } + } + + if (!mImageRequest) { + // We have no image, so size to 0 + mIntrinsicSize.SizeTo(0, 0); + } else { + // We don't want discarding or decode-on-draw for xul images. + mImageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY); + mImageRequest->LockImage(); + } + + // Do this _after_ locking the new image in case they are the same image. + if (oldImageRequest) { + oldImageRequest->UnlockImage(); + } +} + +void nsImageBoxFrame::UpdateLoadFlags() { + static Element::AttrValuesArray strings[] = {nsGkAtoms::always, + nsGkAtoms::never, nullptr}; + switch (mContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::validate, strings, eCaseMatters)) { + case 0: + mLoadFlags = nsIRequest::VALIDATE_ALWAYS; + break; + case 1: + mLoadFlags = nsIRequest::VALIDATE_NEVER | nsIRequest::LOAD_FROM_CACHE; + break; + default: + mLoadFlags = nsIRequest::LOAD_NORMAL; + break; + } +} + +void nsImageBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists); + + if ((0 == mRect.width) || (0 == mRect.height)) { + // Do not render when given a zero area. This avoids some useless + // scaling work while we wait for our image dimensions to arrive + // asynchronously. + return; + } + + if (!IsVisibleForPainting()) return; + + uint32_t clipFlags = + nsStyleUtil::ObjectPropsMightCauseOverflow(StylePosition()) + ? 0 + : DisplayListClipState::ASSUME_DRAWING_RESTRICTED_TO_CONTENT_RECT; + + DisplayListClipState::AutoClipContainingBlockDescendantsToContentBox clip( + aBuilder, this, clipFlags); + + aLists.Content()->AppendNewToTop<nsDisplayXULImage>(aBuilder, this); +} + +already_AddRefed<imgIContainer> nsImageBoxFrame::GetImageContainerForPainting( + const nsPoint& aPt, ImgDrawResult& aDrawResult, + Maybe<nsPoint>& aAnchorPoint, nsRect& aDest) { + if (!mImageRequest) { + // This probably means we're drawn by a native theme. + aDrawResult = ImgDrawResult::SUCCESS; + return nullptr; + } + + // Don't draw if the image's size isn't available. + uint32_t imgStatus; + if (!NS_SUCCEEDED(mImageRequest->GetImageStatus(&imgStatus)) || + !(imgStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) { + aDrawResult = ImgDrawResult::NOT_READY; + return nullptr; + } + + nsCOMPtr<imgIContainer> imgCon; + mImageRequest->GetImage(getter_AddRefs(imgCon)); + + if (!imgCon) { + aDrawResult = ImgDrawResult::NOT_READY; + return nullptr; + } + + aDest = GetDestRect(aPt, aAnchorPoint); + aDrawResult = ImgDrawResult::SUCCESS; + return imgCon.forget(); +} + +ImgDrawResult nsImageBoxFrame::PaintImage(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + uint32_t aFlags) { + ImgDrawResult result; + Maybe<nsPoint> anchorPoint; + nsRect dest; + nsCOMPtr<imgIContainer> imgCon = + GetImageContainerForPainting(aPt, result, anchorPoint, dest); + if (!imgCon) { + return result; + } + + // don't draw if the image is not dirty + // XXX(seth): Can this actually happen anymore? + nsRect dirty; + if (!dirty.IntersectRect(aDirtyRect, dest)) { + return ImgDrawResult::TEMPORARY_ERROR; + } + + bool hasSubRect = !mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0); + + SVGImageContext svgContext; + SVGImageContext::MaybeStoreContextPaint(svgContext, this, imgCon); + return nsLayoutUtils::DrawSingleImage( + aRenderingContext, PresContext(), imgCon, + nsLayoutUtils::GetSamplingFilterForFrame(this), dest, dirty, svgContext, + aFlags, anchorPoint.ptrOr(nullptr), hasSubRect ? &mSubRect : nullptr); +} + +ImgDrawResult nsImageBoxFrame::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, + nsPoint aPt, uint32_t aFlags) { + ImgDrawResult result; + Maybe<nsPoint> anchorPoint; + nsRect dest; + nsCOMPtr<imgIContainer> imgCon = + GetImageContainerForPainting(aPt, result, anchorPoint, dest); + if (!imgCon) { + return result; + } + + if (StaticPrefs::image_svg_blob_image() && + imgCon->GetType() == imgIContainer::TYPE_VECTOR) { + aFlags |= imgIContainer::FLAG_RECORD_BLOB; + } + + const int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + LayoutDeviceRect fillRect = + LayoutDeviceRect::FromAppUnits(dest, appUnitsPerDevPixel); + + SVGImageContext svgContext; + Maybe<ImageIntRegion> region; + gfx::IntSize decodeSize = + nsLayoutUtils::ComputeImageContainerDrawingParameters( + imgCon, aItem->Frame(), fillRect, fillRect, aSc, aFlags, svgContext, + region); + + RefPtr<image::WebRenderImageProvider> provider; + result = + imgCon->GetImageProvider(aManager->LayerManager(), decodeSize, svgContext, + region, aFlags, getter_AddRefs(provider)); + + Maybe<wr::ImageKey> key = aManager->CommandBuilder().CreateImageProviderKey( + aItem, provider, result, aResources); + if (key.isNothing()) { + return result; + } + + auto rendering = wr::ToImageRendering(aItem->Frame()->UsedImageRendering()); + wr::LayoutRect fill = wr::ToLayoutRect(fillRect); + + aBuilder.PushImage(fill, fill, !BackfaceIsHidden(), false, rendering, + key.value()); + return result; +} + +nsRect nsImageBoxFrame::GetDestRect(const nsPoint& aOffset, + Maybe<nsPoint>& aAnchorPoint) { + nsCOMPtr<imgIContainer> imgCon; + mImageRequest->GetImage(getter_AddRefs(imgCon)); + MOZ_ASSERT(imgCon); + + nsRect clientRect; + GetXULClientRect(clientRect); + clientRect += aOffset; + nsRect dest; + if (!mUseSrcAttr) { + // Our image (if we have one) is coming from the CSS property + // 'list-style-image' (combined with '-moz-image-region'). For now, ignore + // 'object-fit' & 'object-position' in this case, and just fill our rect. + // XXXdholbert Should we even honor these properties in this case? They only + // apply to replaced elements, and I'm not sure we count as a replaced + // element when our image data is determined by CSS. + dest = clientRect; + } else { + // Determine dest rect based on intrinsic size & ratio, along with + // 'object-fit' & 'object-position' properties: + IntrinsicSize intrinsicSize; + AspectRatio intrinsicRatio; + if (mIntrinsicSize.width > 0 && mIntrinsicSize.height > 0) { + // Image has a valid size; use it as intrinsic size & ratio. + intrinsicSize = + IntrinsicSize(mIntrinsicSize.width, mIntrinsicSize.height); + intrinsicRatio = + AspectRatio::FromSize(mIntrinsicSize.width, mIntrinsicSize.height); + } else { + // Image doesn't have a (valid) intrinsic size. + // Try to look up intrinsic ratio and use that at least. + intrinsicRatio = imgCon->GetIntrinsicRatio().valueOr(AspectRatio()); + } + aAnchorPoint.emplace(); + dest = nsLayoutUtils::ComputeObjectDestRect(clientRect, intrinsicSize, + intrinsicRatio, StylePosition(), + aAnchorPoint.ptr()); + } + + return dest; +} + +void nsDisplayXULImage::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + // Even though we call StartDecoding when we get a new image we pass + // FLAG_SYNC_DECODE_IF_FAST here for the case where the size we draw at is not + // the intrinsic size of the image and we aren't likely to implement + // predictive decoding at the correct size for this class like nsImageFrame + // has. + uint32_t flags = imgIContainer::FLAG_SYNC_DECODE_IF_FAST; + if (aBuilder->ShouldSyncDecodeImages()) + flags |= imgIContainer::FLAG_SYNC_DECODE; + if (aBuilder->UseHighQualityScaling()) + flags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING; + + Unused << static_cast<nsImageBoxFrame*>(mFrame)->PaintImage( + *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), flags); +} + +bool nsDisplayXULImage::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + nsImageBoxFrame* imageFrame = static_cast<nsImageBoxFrame*>(mFrame); + if (!imageFrame->CanOptimizeToImageLayer()) { + return false; + } + + if (!imageFrame->mImageRequest) { + return true; + } + + uint32_t flags = imgIContainer::FLAG_SYNC_DECODE_IF_FAST | + imgIContainer::FLAG_ASYNC_NOTIFY; + if (aDisplayListBuilder->ShouldSyncDecodeImages()) { + flags |= imgIContainer::FLAG_SYNC_DECODE; + } + if (aDisplayListBuilder->IsPaintingToWindow()) { + flags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING; + } + + ImgDrawResult result = imageFrame->CreateWebRenderCommands( + aBuilder, aResources, aSc, aManager, this, ToReferenceFrame(), flags); + if (result == ImgDrawResult::NOT_SUPPORTED) { + return false; + } + + return true; +} + +bool nsImageBoxFrame::CanOptimizeToImageLayer() { + bool hasSubRect = !mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0); + if (hasSubRect) { + return false; + } + return true; +} + +const mozilla::StyleImage* nsImageBoxFrame::GetImageFromStyle( + const ComputedStyle& aStyle) const { + const nsStyleDisplay* disp = aStyle.StyleDisplay(); + if (disp->HasAppearance()) { + nsPresContext* pc = PresContext(); + if (pc->Theme()->ThemeSupportsWidget(pc, const_cast<nsImageBoxFrame*>(this), + disp->EffectiveAppearance())) { + return nullptr; + } + } + auto& image = aStyle.StyleList()->mListStyleImage; + if (!image.IsImageRequestType()) { + return nullptr; + } + return ℑ +} + +ImageResolution nsImageBoxFrame::GetImageResolution() const { + if (auto* image = GetImageFromStyle()) { + return image->GetResolution(); + } + if (!mImageRequest) { + return {}; + } + nsCOMPtr<imgIContainer> image; + mImageRequest->GetImage(getter_AddRefs(image)); + if (!image) { + return {}; + } + return image->GetResolution(); +} + +/* virtual */ +void nsImageBoxFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) { + nsLeafBoxFrame::DidSetComputedStyle(aOldStyle); + + // Fetch our subrect. + const nsStyleList* myList = StyleList(); + mSubRect = myList->GetImageRegion(); // before |mSuppressStyleCheck| test! + + if (mUseSrcAttr || mSuppressStyleCheck) { + return; // No more work required, since the image isn't specified by style. + } + + auto* oldImage = aOldStyle ? GetImageFromStyle(*aOldStyle) : nullptr; + auto* newImage = GetImageFromStyle(); + if (newImage == oldImage || + (newImage && oldImage && *oldImage == *newImage)) { + return; + } + UpdateImage(); +} + +void nsImageBoxFrame::GetImageSize() { + if (mIntrinsicSize.width > 0 && mIntrinsicSize.height > 0) { + mImageSize.width = mIntrinsicSize.width; + mImageSize.height = mIntrinsicSize.height; + } else { + mImageSize.width = 0; + mImageSize.height = 0; + } +} + +/** + * Ok return our dimensions + */ +nsSize nsImageBoxFrame::GetXULPrefSize(nsBoxLayoutState& aState) { + nsSize size(0, 0); + DISPLAY_PREF_SIZE(this, size); + if (XULNeedsRecalc(mImageSize)) { + GetImageSize(); + } + + if (!mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0)) { + size = mSubRect.Size(); + } else { + size = mImageSize; + } + + nsSize intrinsicSize = size; + + nsMargin borderPadding(0, 0, 0, 0); + GetXULBorderAndPadding(borderPadding); + size.width += borderPadding.LeftRight(); + size.height += borderPadding.TopBottom(); + + bool widthSet, heightSet; + nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet); + NS_ASSERTION( + size.width != NS_UNCONSTRAINEDSIZE && size.height != NS_UNCONSTRAINEDSIZE, + "non-intrinsic size expected"); + + nsSize minSize = GetXULMinSize(aState); + nsSize maxSize = GetXULMaxSize(aState); + + if (!widthSet && !heightSet) { + if (minSize.width != NS_UNCONSTRAINEDSIZE) + minSize.width -= borderPadding.LeftRight(); + if (minSize.height != NS_UNCONSTRAINEDSIZE) + minSize.height -= borderPadding.TopBottom(); + if (maxSize.width != NS_UNCONSTRAINEDSIZE) + maxSize.width -= borderPadding.LeftRight(); + if (maxSize.height != NS_UNCONSTRAINEDSIZE) + maxSize.height -= borderPadding.TopBottom(); + + size = nsLayoutUtils::ComputeAutoSizeWithIntrinsicDimensions( + minSize.width, minSize.height, maxSize.width, maxSize.height, + intrinsicSize.width, intrinsicSize.height); + NS_ASSERTION(size.width != NS_UNCONSTRAINEDSIZE && + size.height != NS_UNCONSTRAINEDSIZE, + "non-intrinsic size expected"); + size.width += borderPadding.LeftRight(); + size.height += borderPadding.TopBottom(); + return size; + } + + if (!widthSet) { + if (intrinsicSize.height > 0) { + // Subtract off the border and padding from the height because the + // content-box needs to be used to determine the ratio + nscoord height = size.height - borderPadding.TopBottom(); + size.width = nscoord(int64_t(height) * int64_t(intrinsicSize.width) / + int64_t(intrinsicSize.height)); + } else { + size.width = intrinsicSize.width; + } + + size.width += borderPadding.LeftRight(); + } else if (!heightSet) { + if (intrinsicSize.width > 0) { + nscoord width = size.width - borderPadding.LeftRight(); + size.height = nscoord(int64_t(width) * int64_t(intrinsicSize.height) / + int64_t(intrinsicSize.width)); + } else { + size.height = intrinsicSize.height; + } + + size.height += borderPadding.TopBottom(); + } + + return XULBoundsCheck(minSize, size, maxSize); +} + +nsSize nsImageBoxFrame::GetXULMinSize(nsBoxLayoutState& aState) { + // An image can always scale down to (0,0). + nsSize size(0, 0); + DISPLAY_MIN_SIZE(this, size); + AddXULBorderAndPadding(size); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(this, size, widthSet, heightSet); + return size; +} + +nscoord nsImageBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aState) { + return GetXULPrefSize(aState).height; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsImageBoxFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ImageBox"_ns, aResult); +} +#endif + +void nsImageBoxFrame::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + nsCOMPtr<imgIContainer> image; + aRequest->GetImage(getter_AddRefs(image)); + return OnSizeAvailable(aRequest, image); + } + + if (aType == imgINotificationObserver::DECODE_COMPLETE) { + return OnDecodeComplete(aRequest); + } + + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + uint32_t imgStatus; + aRequest->GetImageStatus(&imgStatus); + nsresult status = + imgStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK; + return OnLoadComplete(aRequest, status); + } + + if (aType == imgINotificationObserver::IS_ANIMATED) { + return OnImageIsAnimated(aRequest); + } + + if (aType == imgINotificationObserver::FRAME_UPDATE) { + return OnFrameUpdate(aRequest); + } +} + +void nsImageBoxFrame::OnSizeAvailable(imgIRequest* aRequest, + imgIContainer* aImage) { + if (NS_WARN_IF(!aImage)) { + return; + } + + // 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(); + + aImage->SetAnimationMode(PresContext()->ImageAnimationMode()); + + int32_t w = 0, h = 0; + aImage->GetWidth(&w); + aImage->GetHeight(&h); + + mIntrinsicSize.SizeTo(CSSPixel::ToAppUnits(w), CSSPixel::ToAppUnits(h)); + + GetImageResolution().ApplyTo(mIntrinsicSize.width, mIntrinsicSize.height); + + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } +} + +void nsImageBoxFrame::OnDecodeComplete(imgIRequest* aRequest) { + nsBoxLayoutState state(PresContext()); + this->XULRedraw(state); +} + +void nsImageBoxFrame::OnLoadComplete(imgIRequest* aRequest, nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + // Fire an onload DOM event. + FireImageDOMEvent(mContent, eLoad); + } else { + // Fire an onerror DOM event. + mIntrinsicSize.SizeTo(0, 0); + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + FireImageDOMEvent(mContent, eLoadError); + } +} + +void nsImageBoxFrame::OnImageIsAnimated(imgIRequest* aRequest) { + // Register with our refresh driver, if we're animated. + nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, + &mRequestRegistered); +} + +void nsImageBoxFrame::OnFrameUpdate(imgIRequest* aRequest) { + if ((0 == mRect.width) || (0 == mRect.height)) { + return; + } + + // Check if WebRender has interacted with this frame. If it has + // we need to let it know that things have changed. + const auto type = DisplayItemType::TYPE_XUL_IMAGE; + const auto providerId = aRequest->GetProviderId(); + if (WebRenderUserData::ProcessInvalidateForImage(this, type, providerId)) { + return; + } + + InvalidateLayer(type); +} + +NS_IMPL_ISUPPORTS(nsImageBoxListener, imgINotificationObserver) + +nsImageBoxListener::nsImageBoxListener(nsImageBoxFrame* frame) + : mFrame(frame) {} + +nsImageBoxListener::~nsImageBoxListener() = default; + +void nsImageBoxListener::Notify(imgIRequest* request, int32_t aType, + const nsIntRect* aData) { + if (!mFrame) { + return; + } + + return mFrame->Notify(request, aType, aData); +} diff --git a/layout/xul/nsImageBoxFrame.h b/layout/xul/nsImageBoxFrame.h new file mode 100644 index 0000000000..e233ab9d70 --- /dev/null +++ b/layout/xul/nsImageBoxFrame.h @@ -0,0 +1,184 @@ +/* -*- 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 nsImageBoxFrame_h___ +#define nsImageBoxFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsLeafBoxFrame.h" + +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "imgINotificationObserver.h" + +class imgRequestProxy; +class nsImageBoxFrame; + +namespace mozilla { +class nsDisplayXULImage; +class PresShell; +} // namespace mozilla + +class nsImageBoxListener final : public imgINotificationObserver { + public: + explicit nsImageBoxListener(nsImageBoxFrame* frame); + + NS_DECL_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + + void ClearFrame() { mFrame = nullptr; } + + private: + virtual ~nsImageBoxListener(); + + nsImageBoxFrame* mFrame; +}; + +class nsImageBoxFrame final : public nsLeafBoxFrame { + public: + typedef mozilla::image::ImgDrawResult ImgDrawResult; + typedef mozilla::layers::ImageContainer ImageContainer; + typedef mozilla::layers::LayerManager LayerManager; + + friend class mozilla::nsDisplayXULImage; + NS_DECL_FRAMEARENA_HELPERS(nsImageBoxFrame) + NS_DECL_QUERYFRAME + + virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override; + virtual void MarkIntrinsicISizesDirty() override; + + void Notify(imgIRequest* aRequest, int32_t aType, const nsIntRect* aData); + + friend nsIFrame* NS_NewImageBoxFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* asPrevInFlow) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual void DidSetComputedStyle(ComputedStyle* aOldStyle) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + /** + * Gets the image to be loaded from the current style. May be null if themed, + * or if not an url image. + * + * TODO(emilio): Maybe support list-style-image: linear-gradient() etc? + */ + const mozilla::StyleImage* GetImageFromStyle(const ComputedStyle&) const; + const mozilla::StyleImage* GetImageFromStyle() const { + return GetImageFromStyle(*Style()); + } + + mozilla::ImageResolution GetImageResolution() const; + + /** + * Update mUseSrcAttr from appropriate content attributes or from + * style, throw away the current image, and load the appropriate + * image. + * */ + void UpdateImage(); + + /** + * Update mLoadFlags from content attributes. Does not attempt to reload the + * image using the new load flags. + */ + void UpdateLoadFlags(); + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual ~nsImageBoxFrame(); + + already_AddRefed<imgIContainer> GetImageContainerForPainting( + const nsPoint& aPt, ImgDrawResult& aDrawResult, + Maybe<nsPoint>& aAnchorPoint, nsRect& aDest); + + ImgDrawResult PaintImage(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + uint32_t aFlags); + + ImgDrawResult CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, + nsPoint aPt, uint32_t aFlags); + + bool CanOptimizeToImageLayer(); + + nsRect GetDestRect(const nsPoint& aOffset, Maybe<nsPoint>& aAnchorPoint); + + protected: + explicit nsImageBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + virtual void GetImageSize(); + + private: + void OnSizeAvailable(imgIRequest* aRequest, imgIContainer* aImage); + void OnDecodeComplete(imgIRequest* aRequest); + void OnLoadComplete(imgIRequest* aRequest, nsresult aStatus); + void OnImageIsAnimated(imgIRequest* aRequest); + void OnFrameUpdate(imgIRequest* aRequest); + + nsRect mSubRect; ///< If set, indicates that only the portion of the image + ///< specified by the rect should be used. + nsSize mIntrinsicSize; + nsSize mImageSize; + + RefPtr<imgRequestProxy> mImageRequest; + nsCOMPtr<imgINotificationObserver> mListener; + + int32_t mLoadFlags; + + // Boolean variable to determine if the current image request has been + // registered with the refresh driver. + bool mRequestRegistered; + + bool mUseSrcAttr; ///< Whether or not the image src comes from an attribute. + bool mSuppressStyleCheck; +}; // class nsImageBoxFrame + +namespace mozilla { +class nsDisplayXULImage final : public nsPaintedDisplayItem { + public: + nsDisplayXULImage(nsDisplayListBuilder* aBuilder, nsImageBoxFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayXULImage); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULImage) + + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override { + *aSnap = true; + return nsRect(ToReferenceFrame(), Frame()->GetSize()); + } + // Doesn't handle HitTest because nsLeafBoxFrame already creates an + // event receiver for us + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + + NS_DISPLAY_DECL_NAME("XULImage", TYPE_XUL_IMAGE) +}; + +} // namespace mozilla + +#endif /* nsImageBoxFrame_h___ */ diff --git a/layout/xul/nsLeafBoxFrame.cpp b/layout/xul/nsLeafBoxFrame.cpp new file mode 100644 index 0000000000..c7e0a0b484 --- /dev/null +++ b/layout/xul/nsLeafBoxFrame.cpp @@ -0,0 +1,306 @@ +/* -*- 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 "mozilla/ComputedStyle.h" +#include "mozilla/PresShell.h" +#include "nsLeafBoxFrame.h" +#include "nsBoxFrame.h" +#include "nsCOMPtr.h" +#include "nsGkAtoms.h" +#include "nsPresContext.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsBoxLayoutState.h" +#include "nsWidgetsCID.h" +#include "nsViewManager.h" +#include "nsContainerFrame.h" +#include "nsDisplayList.h" +#include <algorithm> + +using namespace mozilla; + +// +// NS_NewLeafBoxFrame +// +// Creates a new Toolbar frame and returns it +// +nsIFrame* NS_NewLeafBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsLeafBoxFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsLeafBoxFrame) + +/** + * Initialize us. This is a good time to get the alignment of the box + */ +void nsLeafBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsLeafFrame::Init(aContent, aParent, aPrevInFlow); + + if (HasAnyStateBits(NS_FRAME_FONT_INFLATION_CONTAINER)) { + AddStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT); + } +} + +void nsLeafBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // REVIEW: GetFrameForPoint used to not report events for the background + // layer, whereas this code will put an event receiver for this frame in the + // BlockBorderBackground() list. But I don't see any need to preserve + // that anomalous behaviour. The important thing I'm preserving is that + // leaf boxes continue to receive events in the foreground layer. + DisplayBorderBackgroundOutline(aBuilder, aLists); + + if (!aBuilder->IsForEventDelivery() || !IsVisibleForPainting()) return; + + aLists.Content()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this); +} + +/* virtual */ +nscoord nsLeafBoxFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + nsBoxLayoutState state(PresContext(), aRenderingContext); + + WritingMode wm = GetWritingMode(); + LogicalSize minSize(wm, GetXULMinSize(state)); + + // GetXULMinSize returns border-box size, and we want to return content + // inline-size. Since Reflow uses the reflow input's border and padding, we + // actually just want to subtract what GetXULMinSize added, which is the + // result of GetXULBorderAndPadding. + nsMargin bp; + GetXULBorderAndPadding(bp); + + result = minSize.ISize(wm) - LogicalMargin(wm, bp).IStartEnd(wm); + + return result; +} + +/* virtual */ +nscoord nsLeafBoxFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + nsBoxLayoutState state(PresContext(), aRenderingContext); + + WritingMode wm = GetWritingMode(); + LogicalSize prefSize(wm, GetXULPrefSize(state)); + + // GetXULPrefSize returns border-box size, and we want to return content + // inline-size. Since Reflow uses the reflow input's border and padding, we + // actually just want to subtract what GetXULPrefSize added, which is the + // result of GetXULBorderAndPadding. + nsMargin bp; + GetXULBorderAndPadding(bp); + + result = prefSize.ISize(wm) - LogicalMargin(wm, bp).IStartEnd(wm); + + return result; +} + +nscoord nsLeafBoxFrame::GetIntrinsicISize() { + // No intrinsic width + return 0; +} + +LogicalSize nsLeafBoxFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + // Important: NOT calling our direct superclass here! + return nsIFrame::ComputeAutoSize(aRenderingContext, aWM, aCBSize, + aAvailableISize, aMargin, aBorderPadding, + aSizeOverrides, aFlags); +} + +void nsLeafBoxFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + // This is mostly a copy of nsBoxFrame::Reflow(). + // We aren't able to share an implementation because of the frame + // class hierarchy. If you make changes here, please keep + // nsBoxFrame::Reflow in sync. + + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsLeafBoxFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION( + aReflowInput.ComputedWidth() >= 0 && aReflowInput.ComputedHeight() >= 0, + "Computed Size < 0"); + +#ifdef DO_NOISY_REFLOW + printf( + "\n-------------Starting LeafBoxFrame Reflow " + "----------------------------\n"); + printf("%p ** nsLBF::Reflow %d R: ", this, myCounter++); + switch (aReflowInput.reason) { + case eReflowReason_Initial: + printf("Ini"); + break; + case eReflowReason_Incremental: + printf("Inc"); + break; + case eReflowReason_Resize: + printf("Rsz"); + break; + case eReflowReason_StyleChange: + printf("Sty"); + break; + case eReflowReason_Dirty: + printf("Drt "); + break; + default: + printf("<unknown>%d", aReflowInput.reason); + break; + } + + printSize("AW", aReflowInput.AvailableWidth()); + printSize("AH", aReflowInput.AvailableHeight()); + printSize("CW", aReflowInput.ComputedWidth()); + printSize("CH", aReflowInput.ComputedHeight()); + + printf(" *\n"); + +#endif + + // create the layout state + nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext); + + nsSize computedSize(aReflowInput.ComputedWidth(), + aReflowInput.ComputedHeight()); + + nsMargin m; + m = aReflowInput.ComputedPhysicalBorderPadding(); + + // GetXULBorderAndPadding(m); + + // this happens sometimes. So lets handle it gracefully. + if (aReflowInput.ComputedHeight() == 0) { + nsSize minSize = GetXULMinSize(state); + computedSize.height = minSize.height - m.top - m.bottom; + } + + nsSize prefSize(0, 0); + + // if we are told to layout intrinic then get our preferred size. + if (computedSize.width == NS_UNCONSTRAINEDSIZE || + computedSize.height == NS_UNCONSTRAINEDSIZE) { + prefSize = GetXULPrefSize(state); + nsSize minSize = GetXULMinSize(state); + nsSize maxSize = GetXULMaxSize(state); + prefSize = XULBoundsCheck(minSize, prefSize, maxSize); + } + + // get our desiredSize + if (aReflowInput.ComputedWidth() == NS_UNCONSTRAINEDSIZE) { + computedSize.width = prefSize.width; + } else { + computedSize.width += m.left + m.right; + } + + if (aReflowInput.ComputedHeight() == NS_UNCONSTRAINEDSIZE) { + computedSize.height = prefSize.height; + } else { + computedSize.height += m.top + m.bottom; + } + + // handle reflow input min and max sizes + // XXXbz the width handling here seems to be wrong, since + // mComputedMin/MaxWidth is a content-box size, whole + // computedSize.width is a border-box size... + if (computedSize.width > aReflowInput.ComputedMaxWidth()) + computedSize.width = aReflowInput.ComputedMaxWidth(); + + if (computedSize.width < aReflowInput.ComputedMinWidth()) + computedSize.width = aReflowInput.ComputedMinWidth(); + + // Now adjust computedSize.height for our min and max computed + // height. The only problem is that those are content-box sizes, + // while computedSize.height is a border-box size. So subtract off + // m.TopBottom() before adjusting, then readd it. + computedSize.height = std::max(0, computedSize.height - m.TopBottom()); + computedSize.height = + NS_CSS_MINMAX(computedSize.height, aReflowInput.ComputedMinHeight(), + aReflowInput.ComputedMaxHeight()); + computedSize.height += m.TopBottom(); + + nsRect r(mRect.x, mRect.y, computedSize.width, computedSize.height); + + SetXULBounds(state, r); + + // layout our children + XULLayout(state); + + // ok our child could have gotten bigger. So lets get its bounds + aDesiredSize.Width() = mRect.width; + aDesiredSize.Height() = mRect.height; + aDesiredSize.SetBlockStartAscent(GetXULBoxAscent(state)); + + // the overflow rect is set in SetXULBounds() above + aDesiredSize.mOverflowAreas = GetOverflowAreas(); + +#ifdef DO_NOISY_REFLOW + { + printf("%p ** nsLBF(done) W:%d H:%d ", this, aDesiredSize.Width(), + aDesiredSize.Height()); + + if (maxElementWidth) { + printf("MW:%d\n", *maxElementWidth); + } else { + printf("MW:?\n"); + } + } +#endif +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsLeafBoxFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"LeafBox"_ns, aResult); +} +#endif + +nsresult nsLeafBoxFrame::CharacterDataChanged( + const CharacterDataChangeInfo& aInfo) { + MarkIntrinsicISizesDirty(); + return nsLeafFrame::CharacterDataChanged(aInfo); +} + +/* virtual */ +nsSize nsLeafBoxFrame::GetXULPrefSize(nsBoxLayoutState& aState) { + return nsIFrame::GetUncachedXULPrefSize(aState); +} + +/* virtual */ +nsSize nsLeafBoxFrame::GetXULMinSize(nsBoxLayoutState& aState) { + return nsIFrame::GetUncachedXULMinSize(aState); +} + +/* virtual */ +nsSize nsLeafBoxFrame::GetXULMaxSize(nsBoxLayoutState& aState) { + return nsIFrame::GetUncachedXULMaxSize(aState); +} + +/* virtual */ +nscoord nsLeafBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aState) { + if (IsXULCollapsed()) { + return 0; + } + return GetXULPrefSize(aState).height; +} + +NS_IMETHODIMP +nsLeafBoxFrame::DoXULLayout(nsBoxLayoutState& aState) { return NS_OK; } diff --git a/layout/xul/nsLeafBoxFrame.h b/layout/xul/nsLeafBoxFrame.h new file mode 100644 index 0000000000..bf9df4c3cd --- /dev/null +++ b/layout/xul/nsLeafBoxFrame.h @@ -0,0 +1,80 @@ +/* -*- 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 nsLeafBoxFrame_h___ +#define nsLeafBoxFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsLeafFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +class nsLeafBoxFrame : public nsLeafFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsLeafBoxFrame) + + friend nsIFrame* NS_NewLeafBoxFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + virtual nsSize GetXULPrefSize(nsBoxLayoutState& aState) override; + virtual nsSize GetXULMinSize(nsBoxLayoutState& aState) override; + virtual nsSize GetXULMaxSize(nsBoxLayoutState& aState) override; + virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aState) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override { + // This is bogus, but it's what we've always done. + // Note that nsLeafFrame is also eReplacedContainsBlock. + return nsLeafFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock | + nsIFrame::eXULBox)); + } + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + // nsIHTMLReflow overrides + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + // Our auto size is that provided by nsFrame, not nsLeafFrame + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsresult CharacterDataChanged( + const CharacterDataChangeInfo&) override; + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* asPrevInFlow) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual bool XULComputesOwnOverflowArea() override { return false; } + + protected: + NS_IMETHOD DoXULLayout(nsBoxLayoutState& aState) override; + + virtual nscoord GetIntrinsicISize() override; + + explicit nsLeafBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + ClassID aID = kClassID) + : nsLeafFrame(aStyle, aPresContext, aID) {} + +}; // class nsLeafBoxFrame + +#endif /* nsLeafBoxFrame_h___ */ diff --git a/layout/xul/nsMenuBarFrame.cpp b/layout/xul/nsMenuBarFrame.cpp new file mode 100644 index 0000000000..914f12085e --- /dev/null +++ b/layout/xul/nsMenuBarFrame.cpp @@ -0,0 +1,136 @@ +/* -*- 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 "nsMenuBarFrame.h" +#include "mozilla/BasicEvents.h" +#include "nsIContent.h" +#include "nsAtom.h" +#include "nsPresContext.h" +#include "nsCSSRendering.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsMenuPopupFrame.h" +#include "nsUnicharUtils.h" +#include "nsPIDOMWindow.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsCSSFrameConstructor.h" +#ifdef XP_WIN +# include "nsISound.h" +# include "nsWidgetsCID.h" +#endif +#include "nsUTF8Utils.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/XULMenuParentElement.h" +#include "mozilla/dom/XULButtonElement.h" + +using namespace mozilla; + +// +// NS_NewMenuBarFrame +// +// Wrapper for creating a new menu Bar container +// +nsIFrame* NS_NewMenuBarFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsMenuBarFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsMenuBarFrame) + +NS_QUERYFRAME_HEAD(nsMenuBarFrame) + NS_QUERYFRAME_ENTRY(nsMenuBarFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame) + +// +// nsMenuBarFrame cntr +// +nsMenuBarFrame::nsMenuBarFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID) {} + +void nsMenuBarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBoxFrame::Init(aContent, aParent, aPrevInFlow); + + // Create the menu bar listener. + mMenuBarListener = new nsMenuBarListener(this, aContent); +} + +dom::XULMenuParentElement& nsMenuBarFrame::MenubarElement() const { + auto* content = dom::XULMenuParentElement::FromNode(GetContent()); + MOZ_DIAGNOSTIC_ASSERT(content); + return *content; +} + +MOZ_CAN_RUN_SCRIPT void nsMenuBarFrame::SetActive(bool aActiveFlag) { + // If the activity is not changed, there is nothing to do. + if (mIsActive == aActiveFlag) { + return; + } + + if (!aActiveFlag) { + // If there is a request to deactivate the menu bar, check to see whether + // there is a menu popup open for the menu bar. In this case, don't + // deactivate the menu bar. + if (auto* activeChild = MenubarElement().GetActiveMenuChild()) { + if (activeChild->IsMenuPopupOpen()) { + return; + } + } + } + + mIsActive = aActiveFlag; + if (mIsActive) { + InstallKeyboardNavigator(); + } else { + mActiveByKeyboard = false; + RemoveKeyboardNavigator(); + } + + RefPtr menubar = &MenubarElement(); + if (!aActiveFlag) { + menubar->SetActiveMenuChild(nullptr); + } + + constexpr auto active = u"DOMMenuBarActive"_ns; + constexpr auto inactive = u"DOMMenuBarInactive"_ns; + FireDOMEvent(aActiveFlag ? active : inactive, menubar); +} + +void nsMenuBarFrame::InstallKeyboardNavigator() { + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->SetActiveMenuBar(this, true); + } +} + +void nsMenuBarFrame::MenuClosed() { SetActive(false); } + +void nsMenuBarFrame::HandleEnterKeyPress(WidgetEvent& aEvent) { + if (RefPtr<dom::XULButtonElement> activeChild = + MenubarElement().GetActiveMenuChild()) { + activeChild->HandleEnterKeyPress(aEvent); + } +} + +void nsMenuBarFrame::RemoveKeyboardNavigator() { + if (!mIsActive) { + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->SetActiveMenuBar(this, false); + } + } +} + +void nsMenuBarFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) pm->SetActiveMenuBar(this, false); + + mMenuBarListener->OnDestroyMenuBarFrame(); + mMenuBarListener = nullptr; + + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} diff --git a/layout/xul/nsMenuBarFrame.h b/layout/xul/nsMenuBarFrame.h new file mode 100644 index 0000000000..9601f8be2e --- /dev/null +++ b/layout/xul/nsMenuBarFrame.h @@ -0,0 +1,83 @@ +/* -*- 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/. */ + +// +// nsMenuBarFrame +// + +#ifndef nsMenuBarFrame_h__ +#define nsMenuBarFrame_h__ + +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsBoxFrame.h" +#include "nsMenuBarListener.h" + +class nsIContent; + +namespace mozilla { +class PresShell; +namespace dom { +class KeyboardEvent; +class XULMenuParentElement; +} // namespace dom +} // namespace mozilla + +nsIFrame* NS_NewMenuBarFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +class nsMenuBarFrame final : public nsBoxFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsMenuBarFrame) + + explicit nsMenuBarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + void InstallKeyboardNavigator(); + void RemoveKeyboardNavigator(); + MOZ_CAN_RUN_SCRIPT void MenuClosed(); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + bool IsActiveByKeyboard() { return mActiveByKeyboard; } + void SetActiveByKeyboard() { mActiveByKeyboard = true; } + MOZ_CAN_RUN_SCRIPT void SetActive(bool aActive); + bool IsActive() const { return mIsActive; } + + mozilla::dom::XULMenuParentElement& MenubarElement() const; + + // Called when Enter is pressed while the menubar is focused. If the current + // menu is open, let the child handle the key. + MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(mozilla::WidgetEvent&); + + bool IsFrameOfType(uint32_t aFlags) const override { + // Override bogus IsFrameOfType in nsBoxFrame. + if (aFlags & (nsIFrame::eReplacedContainsBlock | nsIFrame::eReplaced)) + return false; + return nsBoxFrame::IsFrameOfType(aFlags); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"MenuBar"_ns, aResult); + } +#endif + + protected: + RefPtr<nsMenuBarListener> mMenuBarListener; // The listener that tells us + // about key and mouse events. + + bool mIsActive = false; // Whether or not the menu bar is active (a menu item + // is highlighted or shown). + // Whether the menubar was made active via the keyboard. + bool mActiveByKeyboard = false; +}; // class nsMenuBarFrame + +#endif diff --git a/layout/xul/nsMenuBarListener.cpp b/layout/xul/nsMenuBarListener.cpp new file mode 100644 index 0000000000..ed20499fa1 --- /dev/null +++ b/layout/xul/nsMenuBarListener.cpp @@ -0,0 +1,564 @@ +/* -*- 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 "nsMenuBarListener.h" +#include "XULButtonElement.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/XULButtonElement.h" +#include "nsMenuBarFrame.h" +#include "nsMenuPopupFrame.h" +#include "nsPIWindowRoot.h" +#include "nsISound.h" + +// Drag & Drop, Clipboard +#include "nsWidgetsCID.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" + +#include "nsContentUtils.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/dom/XULMenuParentElement.h" +#include "nsXULPopupManager.h" + +using namespace mozilla; +using mozilla::dom::Event; +using mozilla::dom::KeyboardEvent; + +/* + * nsMenuBarListener implementation + */ + +NS_IMPL_ISUPPORTS(nsMenuBarListener, nsIDOMEventListener) + +//////////////////////////////////////////////////////////////////////// + +int32_t nsMenuBarListener::mAccessKey = -1; +Modifiers nsMenuBarListener::mAccessKeyMask = 0; + +nsMenuBarListener::nsMenuBarListener(nsMenuBarFrame* aMenuBarFrame, + nsIContent* aMenuBarContent) + : mMenuBarFrame(aMenuBarFrame), + mContent(dom::XULMenuParentElement::FromNode(aMenuBarContent)), + mEventTarget(aMenuBarContent->GetComposedDoc()), + mTopWindowEventTarget(nullptr), + mAccessKeyDown(false), + mAccessKeyDownCanceled(false) { + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(mContent); + + // Hook up the menubar as a key listener on the whole document. This will + // see every keypress that occurs, but after everyone else does. + + // Also hook up the listener to the window listening for focus events. This + // is so we can keep proper state as the user alt-tabs through processes. + + mEventTarget->AddSystemEventListener(u"keypress"_ns, this, false); + mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false); + mEventTarget->AddSystemEventListener(u"keyup"_ns, this, false); + mEventTarget->AddSystemEventListener(u"mozaccesskeynotfound"_ns, this, false); + // Need a capturing event listener if the user has blocked pages from + // overriding system keys so that we can prevent menu accesskeys from being + // cancelled. + mEventTarget->AddEventListener(u"keydown"_ns, this, true); + + // mousedown event should be handled in all phase + mEventTarget->AddEventListener(u"mousedown"_ns, this, true); + mEventTarget->AddEventListener(u"mousedown"_ns, this, false); + mEventTarget->AddEventListener(u"blur"_ns, this, true); + + mEventTarget->AddEventListener(u"MozDOMFullscreen:Entered"_ns, this, false); + + // Needs to listen to the deactivate event of the window. + RefPtr<dom::EventTarget> topWindowEventTarget = + nsContentUtils::GetWindowRoot(aMenuBarContent->GetComposedDoc()); + mTopWindowEventTarget = topWindowEventTarget.get(); + + mTopWindowEventTarget->AddSystemEventListener(u"deactivate"_ns, this, true); +} + +//////////////////////////////////////////////////////////////////////// +nsMenuBarListener::~nsMenuBarListener() { + MOZ_ASSERT(!mEventTarget, + "OnDestroyMenuBarFrame() should've alreay been called"); +} + +void nsMenuBarListener::OnDestroyMenuBarFrame() { + mEventTarget->RemoveSystemEventListener(u"keypress"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"keyup"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"mozaccesskeynotfound"_ns, this, + false); + mEventTarget->RemoveEventListener(u"keydown"_ns, this, true); + + mEventTarget->RemoveEventListener(u"mousedown"_ns, this, true); + mEventTarget->RemoveEventListener(u"mousedown"_ns, this, false); + mEventTarget->RemoveEventListener(u"blur"_ns, this, true); + + mEventTarget->RemoveEventListener(u"MozDOMFullscreen:Entered"_ns, this, + false); + + mTopWindowEventTarget->RemoveSystemEventListener(u"deactivate"_ns, this, + true); + + mMenuBarFrame = nullptr; + mEventTarget = nullptr; + mTopWindowEventTarget = nullptr; +} + +int32_t nsMenuBarListener::GetMenuAccessKey() { + InitAccessKey(); + return mAccessKey; +} + +void nsMenuBarListener::InitAccessKey() { + if (mAccessKey >= 0) return; + + // Compiled-in defaults, in case we can't get LookAndFeel -- + // mac doesn't have menu shortcuts, other platforms use alt. +#ifdef XP_MACOSX + mAccessKey = 0; + mAccessKeyMask = 0; +#else + mAccessKey = dom::KeyboardEvent_Binding::DOM_VK_ALT; + mAccessKeyMask = MODIFIER_ALT; +#endif + + // Get the menu access key value from prefs, overriding the default: + mAccessKey = Preferences::GetInt("ui.key.menuAccessKey", mAccessKey); + switch (mAccessKey) { + case dom::KeyboardEvent_Binding::DOM_VK_SHIFT: + mAccessKeyMask = MODIFIER_SHIFT; + break; + case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: + mAccessKeyMask = MODIFIER_CONTROL; + break; + case dom::KeyboardEvent_Binding::DOM_VK_ALT: + mAccessKeyMask = MODIFIER_ALT; + break; + case dom::KeyboardEvent_Binding::DOM_VK_META: + mAccessKeyMask = MODIFIER_META; + break; + case dom::KeyboardEvent_Binding::DOM_VK_WIN: + mAccessKeyMask = MODIFIER_OS; + break; + default: + // Don't touch mAccessKeyMask. + break; + } +} + +void nsMenuBarListener::ToggleMenuActiveState() { + if (mMenuBarFrame->IsActive()) { + mMenuBarFrame->SetActive(false); + } else { + RefPtr content = mContent; + mMenuBarFrame->SetActive(true); + content->SelectFirstItem(); + } +} + +//////////////////////////////////////////////////////////////////////// +nsresult nsMenuBarListener::KeyUp(Event* aKeyEvent) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!nativeKeyEvent) { + return NS_OK; + } + + InitAccessKey(); + + // handlers shouldn't be triggered by non-trusted events. + if (!nativeKeyEvent->IsTrusted()) { + return NS_OK; + } + + if (!mAccessKey || !StaticPrefs::ui_key_menuAccessKeyFocuses()) { + return NS_OK; + } + + // On a press of the ALT key by itself, we toggle the menu's + // active/inactive state. + if (!nativeKeyEvent->DefaultPrevented() && mAccessKeyDown && + !mAccessKeyDownCanceled && + static_cast<int32_t>(nativeKeyEvent->mKeyCode) == mAccessKey) { + // The access key was down and is now up, and no other + // keys were pressed in between. + bool toggleMenuActiveState = true; + if (!mMenuBarFrame->IsActive()) { + // If the focused content is in a remote process, we should allow the + // focused web app to prevent to activate the menubar. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + // First, close all existing popups because other popups shouldn't + // handle key events when menubar is active and IME should be + // disabled. + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->Rollup(0, false, nullptr, nullptr); + } + // If menubar active state is changed or the menubar is destroyed + // during closing the popups, we should do nothing anymore. + toggleMenuActiveState = !Destroyed() && !mMenuBarFrame->IsActive(); + } + if (toggleMenuActiveState) { + if (!mMenuBarFrame->IsActive()) { + mMenuBarFrame->SetActiveByKeyboard(); + } + ToggleMenuActiveState(); + } + } + + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + + if (!Destroyed() && mMenuBarFrame->IsActive()) { + nativeKeyEvent->StopPropagation(); + nativeKeyEvent->PreventDefault(); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +nsresult nsMenuBarListener::KeyPress(Event* aKeyEvent) { + // if event has already been handled, bail + if (!aKeyEvent || aKeyEvent->DefaultPrevented()) { + return NS_OK; // don't consume event + } + + // handlers shouldn't be triggered by non-trusted events. + if (!aKeyEvent->IsTrusted()) { + return NS_OK; + } + + InitAccessKey(); + + if (mAccessKey) { + // If accesskey handling was forwarded to a child process, wait for + // the mozaccesskeynotfound event before handling accesskeys. + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!nativeKeyEvent) { + return NS_OK; + } + + RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent(); + uint32_t keyCode = keyEvent->KeyCode(); + + // Cancel the access key flag unless we are pressing the access key. + if (keyCode != (uint32_t)mAccessKey) { + mAccessKeyDownCanceled = true; + } + +#ifndef XP_MACOSX + // Need to handle F10 specially on Non-Mac platform. + if (nativeKeyEvent->mMessage == eKeyPress && keyCode == NS_VK_F10) { + if ((GetModifiersForAccessKey(*keyEvent) & ~MODIFIER_CONTROL) == 0) { + // If the keyboard event should activate the menubar and will be + // sent to a remote process, it should be executed with reply + // event from the focused remote process. Note that if the menubar + // is active, the event is already marked as "stop cross + // process dispatching". So, in that case, this won't wait + // reply from the remote content. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + // The F10 key just went down by itself or with ctrl pressed. + // In Windows, both of these activate the menu bar. + mMenuBarFrame->SetActiveByKeyboard(); + ToggleMenuActiveState(); + + if (mMenuBarFrame->IsActive()) { +# ifdef MOZ_WIDGET_GTK + RefPtr child = mContent->GetActiveMenuChild(); + // In GTK, this also opens the first menu. + child->OpenMenuPopup(false); +# endif + aKeyEvent->StopPropagation(); + aKeyEvent->PreventDefault(); + } + } + + return NS_OK; + } +#endif // !XP_MACOSX + + RefPtr menuForKey = GetMenuForKeyEvent(*keyEvent); + if (!menuForKey) { +#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 (mMenuBarFrame->IsActive()) { + if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) { + sound->Beep(); + } + mMenuBarFrame->SetActive(false); + } +#endif + return NS_OK; + } + + // If the keyboard event matches with a menu item's accesskey and + // will be sent to a remote process, it should be executed with + // reply event from the focused remote process. Note that if the + // menubar is active, the event is already marked as "stop cross + // process dispatching". So, in that case, this won't wait + // reply from the remote content. + if (nativeKeyEvent->WillBeSentToRemoteProcess()) { + nativeKeyEvent->StopImmediatePropagation(); + nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess(); + return NS_OK; + } + + mMenuBarFrame->SetActiveByKeyboard(); + mMenuBarFrame->SetActive(true); + menuForKey->OpenMenuPopup(true); + + // The opened menu will listen next keyup event. + // Therefore, we should clear the keydown flags here. + mAccessKeyDown = mAccessKeyDownCanceled = false; + + aKeyEvent->StopPropagation(); + aKeyEvent->PreventDefault(); + } + + return NS_OK; +} + +bool nsMenuBarListener::IsAccessKeyPressed(KeyboardEvent& aKeyEvent) { + InitAccessKey(); + // No other modifiers are allowed to be down except for Shift. + uint32_t modifiers = GetModifiersForAccessKey(aKeyEvent); + + return (mAccessKeyMask != MODIFIER_SHIFT && (modifiers & mAccessKeyMask) && + (modifiers & ~(mAccessKeyMask | MODIFIER_SHIFT)) == 0); +} + +Modifiers nsMenuBarListener::GetModifiersForAccessKey( + KeyboardEvent& aKeyEvent) { + WidgetInputEvent* inputEvent = aKeyEvent.WidgetEventPtr()->AsInputEvent(); + MOZ_ASSERT(inputEvent); + + static const Modifiers kPossibleModifiersForAccessKey = + (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META | + MODIFIER_OS); + return inputEvent->mModifiers & kPossibleModifiersForAccessKey; +} + +dom::XULButtonElement* nsMenuBarListener::GetMenuForKeyEvent( + KeyboardEvent& aKeyEvent) { + if (!IsAccessKeyPressed(aKeyEvent)) { + return nullptr; + } + + uint32_t charCode = aKeyEvent.CharCode(); + bool hasAccessKeyCandidates = charCode != 0; + if (!hasAccessKeyCandidates) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent.WidgetEventPtr()->AsKeyboardEvent(); + AutoTArray<uint32_t, 10> keys; + nativeKeyEvent->GetAccessKeyCandidates(keys); + hasAccessKeyCandidates = !keys.IsEmpty(); + } + + if (!hasAccessKeyCandidates) { + return nullptr; + } + // Do shortcut navigation. + // A letter was pressed. We want to see if a shortcut gets matched. If + // so, we'll know the menu got activated. + return mMenuBarFrame->MenubarElement().FindMenuWithShortcut(aKeyEvent); +} + +void nsMenuBarListener::ReserveKeyIfNeeded(Event* aKeyEvent) { + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent)) { + nativeKeyEvent->MarkAsReservedByChrome(); + } +} + +//////////////////////////////////////////////////////////////////////// +nsresult nsMenuBarListener::KeyDown(Event* aKeyEvent) { + InitAccessKey(); + + // handlers shouldn't be triggered by non-trusted events. + if (!aKeyEvent || !aKeyEvent->IsTrusted()) { + return NS_OK; + } + + RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent(); + if (!keyEvent) { + return NS_OK; + } + + uint32_t theChar = keyEvent->KeyCode(); + + uint16_t eventPhase = keyEvent->EventPhase(); + bool capturing = (eventPhase == dom::Event_Binding::CAPTURING_PHASE); + +#ifndef XP_MACOSX + if (capturing && !mAccessKeyDown && theChar == NS_VK_F10 && + (GetModifiersForAccessKey(*keyEvent) & ~MODIFIER_CONTROL) == 0) { + ReserveKeyIfNeeded(aKeyEvent); + } +#endif + + if (mAccessKey && StaticPrefs::ui_key_menuAccessKeyFocuses()) { + bool defaultPrevented = aKeyEvent->DefaultPrevented(); + + // No other modifiers can be down. + // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US + // enhanced 102-key keyboards if we don't check this. + bool isAccessKeyDownEvent = + ((theChar == (uint32_t)mAccessKey) && + (GetModifiersForAccessKey(*keyEvent) & ~mAccessKeyMask) == 0); + + if (!capturing && !mAccessKeyDown) { + // If accesskey isn't being pressed and the key isn't the accesskey, + // ignore the event. + if (!isAccessKeyDownEvent) { + return NS_OK; + } + + // Otherwise, accept the accesskey state. + mAccessKeyDown = true; + // If default is prevented already, cancel the access key down. + mAccessKeyDownCanceled = defaultPrevented; + return NS_OK; + } + + // If the pressed accesskey was canceled already or the event was + // consumed already, ignore the event. + if (mAccessKeyDownCanceled || defaultPrevented) { + return NS_OK; + } + + // Some key other than the access key just went down, + // so we won't activate the menu bar when the access key is released. + mAccessKeyDownCanceled = !isAccessKeyDownEvent; + } + + if (capturing && mAccessKey) { + if (GetMenuForKeyEvent(*keyEvent)) { + ReserveKeyIfNeeded(aKeyEvent); + } + } + + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult nsMenuBarListener::Blur(Event* aEvent) { + if (!IsMenuOpen() && mMenuBarFrame->IsActive()) { + ToggleMenuActiveState(); + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + } + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult nsMenuBarListener::OnWindowDeactivated(Event* aEvent) { + // Reset the accesskey state because we cannot receive the keyup event for + // the pressing accesskey. + mAccessKeyDown = false; + mAccessKeyDownCanceled = false; + return NS_OK; // means I am NOT consuming event +} + +bool nsMenuBarListener::IsMenuOpen() const { + auto* activeChild = mContent->GetActiveMenuChild(); + return activeChild && activeChild->IsMenuPopupOpen(); +} + +//////////////////////////////////////////////////////////////////////// +nsresult nsMenuBarListener::MouseDown(Event* aMouseEvent) { + // NOTE: MouseDown method listens all phases + + // Even if the mousedown event is canceled, it means the user don't want + // to activate the menu. Therefore, we need to record it at capturing (or + // target) phase. + if (mAccessKeyDown) { + mAccessKeyDownCanceled = true; + } + + // Don't do anything at capturing phase, any behavior should be cancelable. + if (aMouseEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) { + return NS_OK; + } + + if (!IsMenuOpen() && mMenuBarFrame->IsActive()) { + ToggleMenuActiveState(); + } + + return NS_OK; // means I am NOT consuming event +} + +//////////////////////////////////////////////////////////////////////// + +nsresult nsMenuBarListener::Fullscreen(Event* aEvent) { + if (mMenuBarFrame->IsActive()) { + ToggleMenuActiveState(); + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////// +MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult +nsMenuBarListener::HandleEvent(Event* aEvent) { + // If the menu bar is collapsed, don't do anything. + if (!mMenuBarFrame->StyleVisibility()->IsVisible()) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType.EqualsLiteral("keyup")) { + return KeyUp(aEvent); + } + if (eventType.EqualsLiteral("keydown")) { + return KeyDown(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("mozaccesskeynotfound")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("blur")) { + return Blur(aEvent); + } + if (eventType.EqualsLiteral("deactivate")) { + return OnWindowDeactivated(aEvent); + } + if (eventType.EqualsLiteral("mousedown")) { + return MouseDown(aEvent); + } + if (eventType.EqualsLiteral("MozDOMFullscreen:Entered")) { + return Fullscreen(aEvent); + } + + MOZ_ASSERT_UNREACHABLE("Unexpected eventType"); + return NS_OK; +} diff --git a/layout/xul/nsMenuBarListener.h b/layout/xul/nsMenuBarListener.h new file mode 100644 index 0000000000..f917e9fd36 --- /dev/null +++ b/layout/xul/nsMenuBarListener.h @@ -0,0 +1,121 @@ +/* -*- 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 nsMenuBarListener_h +#define nsMenuBarListener_h + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsIContent.h" +#include "nsIDOMEventListener.h" + +// X.h defines KeyPress +#ifdef KeyPress +# undef KeyPress +#endif + +class nsMenuFrame; +class nsMenuBarFrame; + +namespace mozilla { +namespace dom { +class EventTarget; +class KeyboardEvent; +class XULMenuParentElement; +class XULButtonElement; +} // namespace dom +} // namespace mozilla + +/** + * EventListener implementation for menubar. + */ +class nsMenuBarListener final : public nsIDOMEventListener { + public: + explicit nsMenuBarListener(nsMenuBarFrame* aMenuBarFrame, + nsIContent* aMenuBarContent); + + NS_DECL_ISUPPORTS + + /** + * nsIDOMEventListener interface method. + */ + NS_DECL_NSIDOMEVENTLISTENER + + /** + * When mMenuBarFrame is being destroyed, this should be called. + */ + void OnDestroyMenuBarFrame(); + + /** + * GetMenuAccessKey() returns keyCode value of a modifier key which is + * used for accesskey. Returns 0 if the platform doesn't support access key. + */ + static int32_t GetMenuAccessKey(); + + /** + * IsAccessKeyPressed() returns true if the modifier state of the event + * matches the modifier state of access key. + */ + static bool IsAccessKeyPressed(mozilla::dom::KeyboardEvent&); + + protected: + virtual ~nsMenuBarListener(); + + bool IsMenuOpen() const; + + MOZ_CAN_RUN_SCRIPT nsresult KeyUp(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult Blur(mozilla::dom::Event* aEvent); + MOZ_CAN_RUN_SCRIPT nsresult OnWindowDeactivated(mozilla::dom::Event* aEvent); + MOZ_CAN_RUN_SCRIPT nsresult MouseDown(mozilla::dom::Event* aMouseEvent); + MOZ_CAN_RUN_SCRIPT nsresult Fullscreen(mozilla::dom::Event* aEvent); + + static void InitAccessKey(); + + static mozilla::Modifiers GetModifiersForAccessKey( + mozilla::dom::KeyboardEvent& event); + + /** + * Given a key event for an Alt+shortcut combination, + * return the menu, if any, that would be opened. If aPeek + * is false, then play a beep and deactivate the menubar on Windows. + */ + mozilla::dom::XULButtonElement* GetMenuForKeyEvent( + mozilla::dom::KeyboardEvent& aKeyEvent); + + /** + * Call MarkAsReservedByChrome if the user's preferences indicate that + * the key should be chrome-only. + */ + void ReserveKeyIfNeeded(mozilla::dom::Event* aKeyEvent); + + // This should only be called by the nsMenuBarListener during event dispatch, + // thus ensuring that this doesn't get destroyed during the process. + MOZ_CAN_RUN_SCRIPT void ToggleMenuActiveState(); + + bool Destroyed() const { return !mMenuBarFrame; } + + // The menu bar object. + nsMenuBarFrame* mMenuBarFrame; + mozilla::dom::XULMenuParentElement* mContent; + // The event target to listen to the events. + // XXX Should this store this as strong reference? However, + // OnDestroyMenuBarFrame() should be called at destroying mMenuBarFrame. + // So, weak reference must be safe. + mozilla::dom::EventTarget* mEventTarget; + // The top window as EventTarget. + mozilla::dom::EventTarget* mTopWindowEventTarget; + // Whether or not the ALT key is currently down. + bool mAccessKeyDown; + // Whether or not the ALT key down is canceled by other action. + bool mAccessKeyDownCanceled; + // See KeyboardEvent for sample values (DOM_VK_* constants). + static int32_t mAccessKey; + // Modifier mask for the access key. + static mozilla::Modifiers mAccessKeyMask; +}; + +#endif // #ifndef nsMenuBarListener_h diff --git a/layout/xul/nsMenuPopupFrame.cpp b/layout/xul/nsMenuPopupFrame.cpp new file mode 100644 index 0000000000..3a465bfe07 --- /dev/null +++ b/layout/xul/nsMenuPopupFrame.cpp @@ -0,0 +1,2479 @@ +/* -*- 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 "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 "nsMenuBarFrame.h" +#include "nsPIDOMWindow.h" +#include "nsFrameManager.h" +#include "mozilla/dom/Document.h" +#include "nsRect.h" +#include "nsBoxLayoutState.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/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" + +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(nsBoxFrame) + +// +// nsMenuPopupFrame ctor +// +nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID), + mView(nullptr), + mPrefSize(-1, -1), + mXPos(0), + mYPos(0), + mAlignmentOffset(0), + mLastClientOffset(0, 0), + mPopupType(ePopupTypePanel), + mPopupState(ePopupClosed), + mPopupAlignment(POPUPALIGNMENT_NONE), + mPopupAnchor(POPUPALIGNMENT_NONE), + mPosition(POPUPPOSITION_UNKNOWN), + mFlip(FlipType_Default), + mIsOpenChanged(false), + mMenuCanOverlapOSBar(false), + mInContentShell(true), + mIsOffset(false), + mHFlip(false), + mVFlip(false), + mPositionedOffset(0), + mAnchorType(MenuPopupAnchorType_Node) { + // 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 != ePopupTypeMenu) { + // 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) { + nsBoxFrame::Init(aContent, aParent, aPrevInFlow); + + // lookup if we're allowed to overlap the OS bar (menubar/taskbar) from the + // look&feel object + mMenuCanOverlapOSBar = + LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) != 0; + + 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); + + mPopupType = ePopupTypePanel; + if (aContent->IsAnyOfXULElements(nsGkAtoms::menupopup, nsGkAtoms::popup)) { + mPopupType = ePopupTypeMenu; + } else if (aContent->IsXULElement(nsGkAtoms::tooltip)) { + mPopupType = ePopupTypeTooltip; + } + + if (PresContext()->IsChrome()) { + mInContentShell = false; + } + + // Support incontentshell=false attribute to allow popups to be displayed + // outside of the content shell. Chrome only. + if (aContent->NodePrincipal()->IsSystemPrincipal()) { + if (aContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::incontentshell, + nsGkAtoms::_true, eCaseMatters)) { + mInContentShell = true; + } else if (aContent->AsElement()->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 == ePopupTypePanel && + 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 == ePopupTypePanel && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::noautohide, + nsGkAtoms::_true, eIgnoreCase)); +} + +nsPopupLevel nsMenuPopupFrame::PopupLevel(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 != ePopupTypePanel) return ePopupLevelTop; + + // 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 ePopupLevelTop; + case 1: + return ePopupLevelParent; + case 2: + return ePopupLevelFloating; + } + + // Panels with titlebars most likely want to be floating popups. + if (mContent->AsElement()->HasAttr(nsGkAtoms::titlebar)) + return ePopupLevelFloating; + + // If this panel is a noautohide panel, the default is the parent level. + if (aIsNoAutoHide) return ePopupLevelParent; + + // Otherwise, the result depends on the platform. + return sDefaultLevelIsTop ? ePopupLevelTop : ePopupLevelParent; +} + +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); + } + if (nsIWidget* widget = GetWidget()) { + // This won't dynamically update the color scheme changes while the widget + // is shown, but it's good enough. + widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this))); + } +} + +nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) { + // Create a widget for ourselves. + nsWidgetInitData widgetData; + widgetData.mWindowType = eWindowType_popup; + widgetData.mBorderStyle = eBorderStyle_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 == ePopupTypePanel && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::drag, eIgnoreCase)) { + widgetData.mIsDragPopup = true; + } + } + + nsAutoString title; + if (widgetData.mNoAutoHide) { + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::titlebar, + nsGkAtoms::normal, eCaseMatters)) { + widgetData.mBorderStyle = eBorderStyle_title; + + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, + title); + + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::close, nsGkAtoms::_true, + eCaseMatters)) { + widgetData.mBorderStyle = static_cast<enum nsBorderStyle>( + widgetData.mBorderStyle | eBorderStyle_close); + } + } + } + + bool remote = HasRemoteContent(); + + nsTransparencyMode mode = nsLayoutUtils::GetFrameTransparency(this, this); + widgetData.mHasRemoteContent = remote; + widgetData.mSupportTranslucency = mode == eTransparencyTransparent; + widgetData.mPopupLevel = PopupLevel(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 != ePopupLevelTop) { + 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, true, true); + if (NS_FAILED(rv)) { + return rv; + } + + nsIWidget* widget = aView->GetWidget(); + widget->SetTransparencyMode(mode); + widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext())); + widget->SetWindowShadowStyle(GetShadowStyle()); + widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity); + widget->SetWindowTransform(ComputeWidgetTransform()); + + // 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; +} + +bool nsMenuPopupFrame::IsMouseTransparent() const { + return ::IsMouseTransparent(*Style()); +} + +StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() { + 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) { + nsBoxFrame::DidSetComputedStyle(aOldStyle); + + if (!aOldStyle) { + return; + } + + auto& newUI = *StyleUIReset(); + auto& oldUI = *aOldStyle->StyleUIReset(); + if (newUI.mWindowOpacity != oldUI.mWindowOpacity) { + if (nsIWidget* widget = GetWidget()) { + widget->SetWindowOpacity(newUI.mWindowOpacity); + } + } + + if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) { + if (nsIWidget* widget = GetWidget()) { + widget->SetWindowTransform(ComputeWidgetTransform()); + } + } + + auto oldRegion = ComputeInputRegion(*aOldStyle, *PresContext()); + auto newRegion = ComputeInputRegion(*Style(), *PresContext()); + if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent || + oldRegion.mMargin != newRegion.mMargin) { + if (nsIWidget* widget = GetWidget()) { + widget->SetInputRegion(newRegion); + } + } +} + +void nsMenuPopupFrame::ConstrainSizeForWayland(nsSize& aSize) const { +#ifdef MOZ_WAYLAND + if (!IS_WAYLAND_DISPLAY()) { + return; + } + + // If the size is not a whole number in CSS pixels we need round it up to + // avoid reflow of the tooltips/popups and putting the text on two lines + // (usually happens with 200% scale factor and font scale factor <> 1) because + // GTK throws away the decimals. + int32_t appPerCSS = AppUnitsPerCSSPixel(); + if (aSize.width % appPerCSS > 0) { + aSize.width += appPerCSS; + } + if (aSize.height % appPerCSS > 0) { + aSize.height += appPerCSS; + } + + nsIWidget* widget = GetWidget(); + if (!widget) { + return; + } + + // 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(), PresContext()->AppUnitsPerDevPixel()); + if (waylandSize.width > 0 && aSize.width > waylandSize.width) { + LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget, aSize.width, + waylandSize.width); + aSize.width = waylandSize.width; + } + if (waylandSize.height > 0 && aSize.height > waylandSize.height) { + LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget, + aSize.height, waylandSize.height); + aSize.height = waylandSize.height; + } +#endif +} + +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!"); + + nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext, + &aReflowInput, aReflowInput.mReflowDepth); + LayoutPopup(state); + + const auto wm = GetWritingMode(); + LogicalSize boxSize = GetLogicalSize(wm); + aDesiredSize.SetSize(wm, boxSize); + aDesiredSize.SetBlockStartAscent(boxSize.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(nsBoxLayoutState& aState) { + if (IsNativeMenu()) { + return; + } + + SchedulePaint(); + + bool shouldPosition = [&] { + if (!IsAnchored()) { + return true; + } + if (ShouldFollowAnchor()) { + return true; + } + // Don't reposition anchored popups that shouldn't follow the anchor and + // have already been positioned. + return mPopupState != ePopupShown || mUsedScreenRect.IsEmpty(); + }(); + + bool isOpen = IsOpen(); + if (!isOpen) { + shouldPosition = + mPopupState == ePopupShowing || mPopupState == ePopupPositioning; + + // 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 = shouldPosition || IsMenuList(); + if (!needsLayout) { + RemoveStateBits(NS_FRAME_FIRST_REFLOW); + return; + } + } + + // if the popup has just been opened, make sure the scrolled window is at 0,0 + // Don't scroll menulists as they will scroll to their selected item on their + // own. + if (mIsOpenChanged && !IsMenuList()) { + nsIScrollableFrame* scrollframe = + do_QueryFrame(nsIFrame::GetChildXULBox(this)); + if (scrollframe) { + AutoWeakFrame weakFrame(this); + scrollframe->ScrollTo(nsPoint(0, 0), ScrollMode::Instant); + if (!weakFrame.IsAlive()) { + return; + } + } + } + + // 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. + nsSize prefSize = GetXULPrefSize(aState); + nsSize minSize = GetXULMinSize(aState); + nsSize maxSize = GetXULMaxSize(aState); + if (ShouldExpandToInflowParentOrAnchor()) { + // 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(this)) { + prefSize.width += sf->GetDesiredScrollbarSizes(&aState).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(); + prefSize.width = std::max(prefSize.width, menuListOrAnchorWidth); + } + + prefSize = XULBoundsCheck(minSize, prefSize, maxSize); + + ConstrainSizeForWayland(prefSize); + + const bool sizeChanged = mPrefSize != prefSize; + // if the size changed then set the bounds to be the preferred size, and make + // sure we re-position the popup too (as that can shrink or resize us again). + if (sizeChanged) { + shouldPosition = true; + SetXULBounds(aState, nsRect(nsPoint(), prefSize), false); + mPrefSize = prefSize; + } + + bool needCallback = false; + if (shouldPosition) { + SetPopupPosition(false); + needCallback = true; + } + + // First do XUL layout on our contents. + const nsSize preLayoutSize = GetSize(); + XULLayout(aState); + + // If the width or height changed, readjust the popup position. This is a + // special case for tooltips where the preferred height doesn't include the + // real height for its inline element, but does once it is laid out. + // This is bug 228673 which doesn't have a simple fix. + // FIXME(emilio): Unclear if this is still an issue with modern flex + // emulation. Perhaps we should try to remove this. + bool rePosition = shouldPosition && (mPosition == POPUPPOSITION_SELECTION); + const nsSize postLayoutSize = GetSize(); + if (postLayoutSize.width > preLayoutSize.width || + postLayoutSize.height > preLayoutSize.height) { + // the size after layout was larger than the preferred size, so set the + // preferred size accordingly. + mPrefSize = postLayoutSize; + if (isOpen) { + rePosition = true; + needCallback = true; + } + } + + if (rePosition) { + SetPopupPosition(false); + } + + nsPresContext* pc = PresContext(); + nsView* view = GetView(); + + if (sizeChanged) { + // If the size of the popup changed, apply any size constraints. + nsIWidget* widget = view->GetWidget(); + if (widget) { + SetSizeConstraints(pc, widget, minSize, maxSize); + } + } + + if (isOpen) { + nsViewManager* viewManager = view->GetViewManager(); + nsRect rect = GetRect(); + rect.x = rect.y = 0; + rect.SizeTo(XULBoundsCheck(minSize, rect.Size(), maxSize)); + viewManager->ResizeView(view, rect); + + if (mPopupState == ePopupOpening) { + mPopupState = ePopupVisible; + } + + viewManager->SetViewVisibility(view, nsViewVisibility_kShow); + SyncFrameViewProperties(view); + } + + // 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, pc); + 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(), pc); + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); + } + + if (needCallback && !mReflowCallbackData.mPosted) { + pc->PresShell()->PostReflowCallback(this); + mReflowCallbackData.MarkPosted(openChanged); + } +} + +bool nsMenuPopupFrame::ReflowFinished() { + SetPopupPosition(false); + mReflowCallbackData.Clear(); + return false; +} + +void nsMenuPopupFrame::ReflowCallbackCanceled() { mReflowCallbackData.Clear(); } + +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 == ePopupTypeMenu) { + EventStateManager* activeESM = static_cast<EventStateManager*>( + EventStateManager::GetActiveEventStateManager()); + if (activeESM) { + 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 == ePopupTypeMenu) { + 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, nsViewVisibility_kHide); + + 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); +} + +nsIFrame::ReflowChildFlags nsMenuPopupFrame::GetXULLayoutFlags() { + return ReflowChildFlags::NoSizeView | ReflowChildFlags::NoMoveView; +} + +nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(nsRect& anchorRect, + FlipStyle& aHFlip, + FlipStyle& aVFlip) { + // 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(-mRect.width - margin.right, margin.top); + break; + case POPUPALIGNMENT_BOTTOMLEFT: + pnt.MoveBy(margin.left, -mRect.height - margin.bottom); + break; + case POPUPALIGNMENT_BOTTOMRIGHT: + pnt.MoveBy(-mRect.width - margin.right, -mRect.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 || mReflowCallbackData.mIsOpenChanged) { + if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) { + mPositionedOffset = + originalAnchorRect.height + selectedItemFrame->GetOffsetTo(this).y; + } + } + + 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() { + // 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) { + // 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) { + // 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) { + // 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; +} + +nsresult nsMenuPopupFrame::SetPopupPosition(bool aIsMove) { + // If this is due to a move, return early if the popup hasn't been laid out + // yet. On Windows, this can happen when using a drag popup before it opens. + if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) { + return NS_OK; + } + + nsPresContext* presContext = PresContext(); + nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); + NS_ASSERTION(rootFrame->GetView() && GetView() && + rootFrame->GetView() == GetView()->GetParent(), + "rootFrame's view is not our view's parent???"); + + // For anchored popups, the anchor rectangle. For non-anchored popups, the + // size will be 0. + nsRect anchorRect; + + bool anchored = IsAnchored(); + if (anchored) { + // In order to deal with transforms, we need the root prescontext: + nsPresContext* rootPresContext = presContext->GetRootPresContext(); + + // If we can't reach a root pres context, don't bother continuing: + if (!rootPresContext) { + return NS_OK; + } + + // If anchored to a rectangle, use that rectangle. Otherwise, determine the + // rectangle from the anchor. + if (mAnchorType == MenuPopupAnchorType_Rect) { + anchorRect = mScreenRect; + } else { + // 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) { + anchorFrame = rootFrame; + if (!anchorFrame) { + return NS_OK; + } + } + + anchorRect = ComputeAnchorRect(rootPresContext, anchorFrame); + } + } + + // 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. The preferred size should already be set by the parent frame. + { + NS_ASSERTION(mPrefSize.width >= 0 || mPrefSize.height >= 0, + "preferred size of popup not set"); + mRect.SizeTo(mPrefSize); + } + + // the screen position in app units where the popup should appear + nsPoint screenPoint; + + // 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. + nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits(); + + bool isNoAutoHide = IsNoAutoHide(); + nsPopupLevel popupLevel = PopupLevel(isNoAutoHide); + + if (anchored) { + // 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. + if (!mPositionedByMoveToRect) { + mUntransformedAnchorRect = anchorRect; + } + screenPoint = AdjustPositionForAnchorAlign(anchorRect, hFlip, vFlip); + } else { + // with no anchor, the popup is positioned relative to the root frame + anchorRect = rootScreenRect; + if (!mPositionedByMoveToRect) { + mUntransformedAnchorRect = anchorRect; + } + screenPoint = anchorRect.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. + nscoord anchorXOffset = CSSPixel::ToAppUnits(mXPos); + if (IsDirectionRTL()) { + screenPoint.x -= anchorXOffset; + anchorRect.x -= anchorXOffset; + } else { + screenPoint.x += anchorXOffset; + anchorRect.x += anchorXOffset; + } + nscoord anchorYOffset = CSSPixel::ToAppUnits(mYPos); + screenPoint.y += anchorYOffset; + anchorRect.y += anchorYOffset; + + // 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. + if (isNoAutoHide && (popupLevel != ePopupLevelParent || + mAnchorType == MenuPopupAnchorType_Rect)) { + // Account for the margin that will end up being added to the screen + // coordinate the next time SetPopupPosition is called. + mAnchorType = MenuPopupAnchorType_Point; + mScreenRect.x = screenPoint.x - margin.left; + mScreenRect.y = screenPoint.y - margin.top; + } + } else { + screenPoint = mScreenRect.TopLeft(); + anchorRect = nsRect(screenPoint, nsSize()); + if (!mPositionedByMoveToRect) { + mUntransformedAnchorRect = anchorRect; + } + + // Right-align RTL context menus, and apply margin and offsets as per the + // platform conventions. + if (mIsContextMenu && IsDirectionRTL()) { + screenPoint.x -= mRect.Width(); + screenPoint.MoveBy(-margin.right, margin.top); + } else { + screenPoint.MoveBy(margin.left, margin.top); + } + +#ifdef XP_MACOSX + // OSX tooltips follow standard flip rule but other popups flip horizontally + // not vertically + if (mPopupType == ePopupTypeTooltip) { + 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 + } + + nscoord oldAlignmentOffset = mAlignmentOffset; + + // If a panel is being moved or has flip="none", don't constrain or flip it, + // in order to avoid visual noise when moving windows between screens. + // However, if a panel is already constrained or flipped (mIsOffset), then we + // want to continue to calculate this. Also, always do this for content + // shells, so that the popup doesn't extend outside the containing frame. + if (!IS_WAYLAND_DISPLAY() && + (mInContentShell || + (mFlip != FlipType_None && + (!aIsMove || mIsOffset || mPopupType != ePopupTypePanel)))) { + const nsRect screenRect = [&] { + int32_t appPerDev = presContext->AppUnitsPerDevPixel(); + auto anchorRectDevPix = + LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRect, appPerDev); + auto rootScreenRectDevPix = + LayoutDeviceIntRect::FromAppUnitsToNearest(rootScreenRect, appPerDev); + auto screenRectDevPix = + GetConstraintRect(anchorRectDevPix, rootScreenRectDevPix, popupLevel); + nsRect sr = LayoutDeviceIntRect::ToAppUnits(screenRectDevPix, appPerDev); + + // Expand the allowable screen rect by the input margin (which can't be + // interacted with). + const nscoord inputMargin = + StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits(); + sr.Inflate(inputMargin); + return sr; + }(); + + // Ensure that anchorRect is on screen. + anchorRect = anchorRect.Intersect(screenRect); + + // Shrink the the popup down if it is larger than the screen size + if (mRect.width > screenRect.width) { + mRect.width = screenRect.width; + } + if (mRect.height > screenRect.height) { + mRect.height = screenRect.height; + } + + // At this point the anchor (anchorRect) is within the available screen + // area (screenRect) and the popup is known to be no larger than the + // screen. + + // 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. + const nsPoint preOffsetScreenPoint = screenPoint; + if (slideHorizontal) { + mRect.width = SlideOrResize(screenPoint.x, mRect.width, screenRect.x, + screenRect.XMost(), &mAlignmentOffset); + } else { + bool endAligned = IsDirectionRTL() + ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT + : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT; + mRect.width = + FlipOrResize(screenPoint.x, mRect.width, screenRect.x, + screenRect.XMost(), anchorRect.x, anchorRect.XMost(), + margin.left, margin.right, hFlip, endAligned, &mHFlip); + } + if (slideVertical) { + mRect.height = SlideOrResize(screenPoint.y, mRect.height, screenRect.y, + screenRect.YMost(), &mAlignmentOffset); + } else { + bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT; + mRect.height = + FlipOrResize(screenPoint.y, mRect.height, screenRect.y, + screenRect.YMost(), anchorRect.y, anchorRect.YMost(), + margin.top, margin.bottom, vFlip, endAligned, &mVFlip); + } + mIsOffset = preOffsetScreenPoint != screenPoint; + + NS_ASSERTION(screenPoint.x >= screenRect.x, "Popup is offscreen (x start)"); + NS_ASSERTION(screenPoint.y >= screenRect.y, "Popup is offscreen (y start)"); + NS_ASSERTION(screenPoint.x + mRect.width <= screenRect.XMost(), + "Popup is offscreen (x end)"); + NS_ASSERTION(screenPoint.y + mRect.height <= screenRect.YMost(), + "Popup is offscreen (y end)"); + } + + // snap the popup's position in screen coordinates to device pixels, + // see bug 622507, bug 961431 + screenPoint.x = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.x); + screenPoint.y = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.y); + + // determine the x and y position of the view by subtracting the desired + // screen position from the screen position of the root frame. + nsPoint viewPoint = screenPoint - rootScreenRect.TopLeft(); + + nsView* view = GetView(); + NS_ASSERTION(view, "popup with no view"); + + // 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. + nsIWidget* widget = view->GetWidget(); + if (mPopupType == ePopupTypePanel && widget) { + mLastClientOffset = widget->GetClientOffset(); + viewPoint.x += presContext->DevPixelsToAppUnits(mLastClientOffset.x); + viewPoint.y += presContext->DevPixelsToAppUnits(mLastClientOffset.y); + } + + presContext->GetPresShell()->GetViewManager()->MoveViewTo(view, viewPoint.x, + viewPoint.y); + + // Now that we've positioned the view, sync up the frame's origin. + nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame)); + + // 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. + nsIntRect newRect(screenPoint.x, screenPoint.y, mRect.width, mRect.height); + if (mPopupState == ePopupPositioning || + (mPopupState == ePopupShown && !newRect.IsEqualEdges(mUsedScreenRect)) || + (mPopupState == ePopupShown && oldAlignmentOffset != mAlignmentOffset)) { + mUsedScreenRect = newRect; + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) { + mPendingPositionedEvent = + nsXULPopupPositionedEvent::DispatchIfNeeded(mContent); + } + } + + // 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(); + + return NS_OK; +} + +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(); + } + } + } +} + +LayoutDeviceIntRect nsMenuPopupFrame::GetConstraintRect( + const LayoutDeviceIntRect& aAnchorRect, + const LayoutDeviceIntRect& aRootScreenRect, nsPopupLevel aPopupLevel) { + LayoutDeviceIntRect screenRectPixels; + + // GetConstraintRect() does not work on Wayland as we can't get absolute + // window position there. + MOZ_ASSERT(!IS_WAYLAND_DISPLAY(), + "GetConstraintRect does not work on Wayland"); + + // 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. + nsCOMPtr<nsIScreen> screen; + nsCOMPtr<nsIScreenManager> sm( + do_GetService("@mozilla.org/gfx/screenmanager;1")); + if (sm) { + // 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. + DesktopToLayoutDeviceScale scale = + PresContext()->DeviceContext()->GetDesktopToDeviceScale(); + DesktopRect rect = + (mInContentShell ? aRootScreenRect : aAnchorRect) / scale; + int32_t width = std::max(1, NSToIntRound(rect.width)); + int32_t height = std::max(1, NSToIntRound(rect.height)); + sm->ScreenForRect(rect.x, rect.y, width, height, getter_AddRefs(screen)); + if (screen) { + // Non-top-level popups (which will always be panels) + // should never overlap the OS bar: + bool dontOverlapOSBar = aPopupLevel != ePopupLevelTop; + // get the total screen area if the popup is allowed to overlap it. + if (!dontOverlapOSBar && mMenuCanOverlapOSBar && !mInContentShell) + screen->GetRect(&screenRectPixels.x, &screenRectPixels.y, + &screenRectPixels.width, &screenRectPixels.height); + else + screen->GetAvailRect(&screenRectPixels.x, &screenRectPixels.y, + &screenRectPixels.width, &screenRectPixels.height); + } + } + + if (mInContentShell) { + // for content shells, clip to the client area rather than the screen area + screenRectPixels.IntersectRect(screenRectPixels, aRootScreenRect); + } else if (!mOverrideConstraintRect.IsEmpty()) { + LayoutDeviceIntRect overrideConstrainRect = + LayoutDeviceIntRect::FromAppUnitsToNearest( + mOverrideConstraintRect, PresContext()->AppUnitsPerDevPixel()); + // 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. + screenRectPixels.IntersectRect(screenRectPixels, overrideConstrainRect); + screenRectPixels.x = overrideConstrainRect.x; + screenRectPixels.width = overrideConstrainRect.width; + } + + return screenRectPixels; +} + +void nsMenuPopupFrame::CanAdjustEdges(Side aHorizontalSide, Side aVerticalSide, + LayoutDeviceIntPoint& aChange) { + int8_t popupAlign(mPopupAlignment); + if (IsDirectionRTL()) { + popupAlign = -popupAlign; + } + + if (aHorizontalSide == (mHFlip ? eSideRight : eSideLeft)) { + if (popupAlign == POPUPALIGNMENT_TOPLEFT || + popupAlign == POPUPALIGNMENT_BOTTOMLEFT) { + aChange.x = 0; + } + } else if (aHorizontalSide == (mHFlip ? eSideLeft : eSideRight)) { + if (popupAlign == POPUPALIGNMENT_TOPRIGHT || + popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) { + aChange.x = 0; + } + } + + if (aVerticalSide == (mVFlip ? eSideBottom : eSideTop)) { + if (popupAlign == POPUPALIGNMENT_TOPLEFT || + popupAlign == POPUPALIGNMENT_TOPRIGHT) { + aChange.y = 0; + } + } else if (aVerticalSide == (mVFlip ? eSideTop : eSideBottom)) { + if (popupAlign == POPUPALIGNMENT_BOTTOMLEFT || + popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) { + aChange.y = 0; + } + } +} + +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; +} + +// 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(nsIFrame* aStart) { + if (!aStart) return nullptr; + + // try start frame and siblings + nsIFrame* currFrame = aStart; + do { + nsIScrollableFrame* sf = do_QueryFrame(currFrame); + if (sf) return sf; + currFrame = currFrame->GetNextSibling(); + } while (currFrame); + + // try children + currFrame = aStart; + do { + nsIFrame* childFrame = currFrame->PrincipalChildList().FirstChild(); + nsIScrollableFrame* sf = GetScrollFrame(childFrame); + if (sf) return sf; + currFrame = currFrame->GetNextSibling(); + } while (currFrame); + + return nullptr; +} + +void nsMenuPopupFrame::ChangeByPage(bool aIsUp) { + // Only scroll by page within menulists. + if (!IsMenuList()) { + return; + } + + nsIScrollableFrame* scrollframe = GetScrollFrame(this); + + 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(); + if (RefPtr menu = GetCurrentMenuItem()) { + // Give it to the child. + menu->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 = + nsBoxFrame::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) { + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + if (pm) { + 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::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + if (mReflowCallbackData.mPosted) { + PresShell()->CancelReflowCallback(this); + mReflowCallbackData.Clear(); + } + + // 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); + } + + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +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.x == appUnitsPos.x && mScreenRect.y == appUnitsPos.y) && + (!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; + } 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(); + nsViewVisibility visibility = nsViewVisibility_kHide; + + 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() { + 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 == ePopupTypePanel && + 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, false, true, true, false); + } + + 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..8f30dbe83b --- /dev/null +++ b/layout/xul/nsMenuPopupFrame.h @@ -0,0 +1,638 @@ +/* -*- 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 "nsIReflowCallback.h" +#include "nsXULPopupManager.h" + +#include "nsBoxFrame.h" + +#include "Units.h" + +class nsIWidget; + +namespace mozilla { +class PresShell; +namespace dom { +class KeyboardEvent; +class XULButtonElement; +class XULPopupElement; +} // namespace dom +} // 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 nsBoxFrame, public nsIReflowCallback { + 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() { 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; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nsIWidget* GetWidget() const; + + // Overridden methods + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual 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 DestroyFrom( + nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) 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; + + nsPopupLevel PopupLevel() const { return PopupLevel(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(); + + void DidSetComputedStyle(ComputedStyle* aOldStyle) override; + + // layout, position and display the popup as needed + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void LayoutPopup(nsBoxLayoutState& aState); + + // 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. + nsresult 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; + + nsPopupType PopupType() 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() { 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(nsIFrame* aStart); + + void SetOverrideConstraintRect(mozilla::LayoutDeviceIntRect aRect) { + mOverrideConstraintRect = ToAppUnits(aRect, mozilla::AppUnitsPerCSSPixel()); + } + + // For a popup that should appear anchored at the given rect, determine + // the screen area that it is constrained by. 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, given by + // aRootScreenRect. All coordinates are in app units. + // 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. + // This operates in device pixels. + mozilla::LayoutDeviceIntRect GetConstraintRect( + const mozilla::LayoutDeviceIntRect& aAnchorRect, + const mozilla::LayoutDeviceIntRect& aRootScreenRect, + nsPopupLevel aPopupLevel); + + // Determines whether the given edges of the popup may be moved, where + // aHorizontalSide and aVerticalSide are one of the enum Side constants. + // aChange is the distance to move on those sides. If will be reset to 0 + // if the side cannot be adjusted at all in that direction. For example, a + // popup cannot be moved if it is anchored on a particular side. + // + // Later, when bug 357725 is implemented, we can make this adjust aChange by + // the amount that the side can be resized, so that minimums and maximums + // can be taken into account. + void CanAdjustEdges(mozilla::Side aHorizontalSide, + mozilla::Side aVerticalSide, + mozilla::LayoutDeviceIntPoint& aChange); + + // 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; } + + // nsIReflowCallback + virtual bool ReflowFinished() override; + virtual void ReflowCallbackCanceled() override; + + protected: + // returns the popup's level. + nsPopupLevel PopupLevel(bool aIsNoAutoHide) const; + + void ConstrainSizeForWayland(nsSize&) const; + + // redefine to tell the box system not to move the views. + ReflowChildFlags GetXULLayoutFlags() override; + + 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& anchorRect, FlipStyle& aHFlip, + FlipStyle& aVFlip); + + // For popups that are going to align to their selected item, get the frame of + // the selected item. + nsIFrame* GetSelectedItemForAlignment(); + + // 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); + + // 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); + + // 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); + + // 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(); + + 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; } + + 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; + + RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher; + + // The popup's screen rectangle in app units. + nsIntRect 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; + + // 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; + int32_t mYPos; + 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; + + // 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; + + // 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; + + // 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; + + nsPopupType mPopupType; // type of popup + nsPopupState mPopupState; // open state of the popup + + // popup alignment relative to the anchor node + int8_t mPopupAlignment; + int8_t mPopupAnchor; + int8_t mPosition; + + FlipType mFlip; // Whether to flip + + struct ReflowCallbackData { + ReflowCallbackData() = default; + void MarkPosted(bool aIsOpenChanged) { + mPosted = true; + mIsOpenChanged = aIsOpenChanged; + } + void Clear() { + mPosted = false; + mIsOpenChanged = false; + } + bool mPosted = false; + bool mIsOpenChanged = false; + }; + ReflowCallbackData mReflowCallbackData; + + bool mIsOpenChanged; // true if the open state changed since the last layout + bool mIsContextMenu = false; // true for context menus and their submenus. + bool mIsTopLevelContextMenu = false; // true for the topmost context menu. + + bool mMenuCanOverlapOSBar; // can we appear over the taskbar/menubar? + bool mInContentShell; // True if the popup is in a content shell + + // True if this popup has been offset due to moving off / near the edge of the + // screen. (This is useful for ensuring that a move, which can't offset the + // popup, doesn't undo a previously set offset.) + bool mIsOffset; + + // the flip modes that were used when the popup was opened + bool mHFlip; + bool mVFlip; + + // 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. + nscoord mPositionedOffset; + + // How the popup is anchored. + MenuPopupAnchorType mAnchorType; + + 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..176c0ec09d --- /dev/null +++ b/layout/xul/nsScrollbarButtonFrame.cpp @@ -0,0 +1,274 @@ +/* -*- 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 nsBoxFrame::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::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + // 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(); + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} diff --git a/layout/xul/nsScrollbarButtonFrame.h b/layout/xul/nsScrollbarButtonFrame.h new file mode 100644 index 0000000000..2499c8c134 --- /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 "mozilla/Attributes.h" +#include "nsBoxFrame.h" +#include "nsRepeatService.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +class nsScrollbarButtonFrame final : public nsBoxFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsScrollbarButtonFrame) + + explicit nsScrollbarButtonFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID), mCursorOnThis(false) {} + + // Overrides + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) 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; +}; + +#endif diff --git a/layout/xul/nsScrollbarFrame.cpp b/layout/xul/nsScrollbarFrame.cpp new file mode 100644 index 0000000000..bc6db8a1af --- /dev/null +++ b/layout/xul/nsScrollbarFrame.cpp @@ -0,0 +1,546 @@ +/* -*- 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 "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(nsBoxFrame) + +void nsScrollbarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBoxFrame::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::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + aPostDestroyData.AddAnonymousContent(mUpTopButton.forget()); + aPostDestroyData.AddAnonymousContent(mDownTopButton.forget()); + aPostDestroyData.AddAnonymousContent(mSlider.forget()); + aPostDestroyData.AddAnonymousContent(mUpBottomButton.forget()); + aPostDestroyData.AddAnonymousContent(mDownBottomButton.forget()); + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +void nsScrollbarFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + nsBoxFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // nsGfxScrollFrame may have told us to shrink to nothing. If so, make sure + // our desired size agrees. + if (aReflowInput.AvailableWidth() == 0) { + aDesiredSize.Width() = 0; + } + if (aReflowInput.AvailableHeight() == 0) { + aDesiredSize.Height() = 0; + } +} + +nsresult nsScrollbarFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = + nsBoxFrame::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; +} + +nsresult nsScrollbarFrame::GetXULMargin(nsMargin& aMargin) { + aMargin.SizeTo(0, 0, 0, 0); + + const bool overlayScrollbars = PresContext()->UseOverlayScrollbars(); + + const bool horizontal = IsXULHorizontal(); + bool didSetMargin = false; + + if (overlayScrollbars) { + nsSize minSize; + bool widthSet = false; + bool heightSet = false; + AddXULMinSize(this, minSize, widthSet, heightSet); + if (horizontal) { + if (heightSet) { + aMargin.top = -minSize.height; + didSetMargin = true; + } + } else { + if (widthSet) { + aMargin.left = -minSize.width; + didSetMargin = true; + } + } + } + + if (!didSetMargin) { + DebugOnly<nsresult> rv = nsIFrame::GetXULMargin(aMargin); + // TODO(emilio): Should probably not be fallible, it's not like anybody + // cares about the return value anyway. + MOZ_ASSERT(NS_SUCCEEDED(rv), "nsIFrame::GetXULMargin can't really fail"); + } + + if (!horizontal) { + nsIScrollbarMediator* scrollFrame = GetScrollbarMediator(); + if (scrollFrame && !scrollFrame->IsScrollbarOnRight()) { + std::swap(aMargin.left, aMargin.right); + } + } + + return NS_OK; +} + +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(kNameSpaceID_None, 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(kNameSpaceID_None, aAttribute, value); + + if (!el->HasAttr(kNameSpaceID_None, 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 (mThumb && aAttribute == nsGkAtoms::disabled) { + mThumb->UnsetAttr(kNameSpaceID_None, nsGkAtoms::collapsed, 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); + } + // Set the value on "collapsed" attribute. + if (mThumb) { + mThumb->SetAttr(kNameSpaceID_None, nsGkAtoms::collapsed, 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..2539686f10 --- /dev/null +++ b/layout/xul/nsScrollbarFrame.h @@ -0,0 +1,159 @@ +/* -*- 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 "nsIAnonymousContentCreator.h" +#include "nsBoxFrame.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 nsBoxFrame, + public nsIAnonymousContentCreator { + using Element = mozilla::dom::Element; + + public: + explicit nsScrollbarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsBoxFrame(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 + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"ScrollbarFrame"_ns, aResult); + } +#endif + + // nsIFrame overrides + virtual 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; + + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + void SetScrollbarMediatorContent(nsIContent* aMediator); + nsIScrollbarMediator* GetScrollbarMediator(); + + // nsBox methods + + /** + * Treat scrollbars as clipping their children; overflowing children + * will not be allowed to set an overflow rect on this + * frame. This means that when the scroll code decides to hide a + * scrollframe by setting its height or width to zero, that will + * hide the children too. + */ + virtual bool DoesClipChildrenInBothAxes() override { return true; } + + virtual nsresult GetXULMargin(nsMargin& aMargin) override; + + /** + * 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 + virtual nsresult CreateAnonymousContent( + nsTArray<ContentInfo>& aElements) override; + virtual 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..42df839fb6 --- /dev/null +++ b/layout/xul/nsSliderFrame.cpp @@ -0,0 +1,1626 @@ +/* -*- 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 "nsBoxLayoutState.h" +#include "nsSprocketLayout.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 + +static already_AddRefed<nsIContent> GetContentOfBox(nsIFrame* aBox) { + nsCOMPtr<nsIContent> content = aBox->GetContent(); + return content.forget(); +} + +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(nsBoxFrame) + +nsSliderFrame::nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID), + mRatio(0.0f), + mDragStart(0), + mThumbStart(0), + mCurPos(0), + mChange(0), + mDragFinished(true), + mUserChanged(false), + mScrollingWithAPZ(false), + mSuppressionActive(false) {} + +// stop timer +nsSliderFrame::~nsSliderFrame() { + if (mSuppressionActive) { + if (mozilla::PresShell* presShell = PresShell()) { + presShell->SuppressDisplayport(false); + } + } +} + +void nsSliderFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBoxFrame::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(ChildListID aListID, nsIFrame* aOldFrame) { + nsBoxFrame::RemoveFrame(aListID, aOldFrame); + if (mFrames.IsEmpty()) RemoveListener(); +} + +void nsSliderFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + bool wasEmpty = mFrames.IsEmpty(); + nsBoxFrame::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(); + nsBoxFrame::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 = + nsBoxFrame::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. + + nsIFrame* scrollbarBox = GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox); + 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 + nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox); + if (scrollbarFrame) { + nsIScrollbarMediator* mediator = scrollbarFrame->GetScrollbarMediator(); + scrollbarFrame->SetIncrementToWhole(direction); + if (mediator) { + mediator->ScrollByWhole(scrollbarFrame, 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); + + virtual 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, + mozilla::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->GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox); + + 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; + bool isHorizontal = sliderFrame->IsXULHorizontal(); + 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, + mozilla::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; + } + + nsBoxFrame::BuildDisplayList(aBuilder, aLists); + + // If this is a vertical scrollbar for the root frame, draw any markers. + // Markers are not drawn for other scrollbars. + if (!aBuilder->IsForEventDelivery()) { + nsIFrame* scrollbarBox = GetScrollbar(); + if (scrollbarBox) { + if (nsIScrollableFrame* scrollFrame = + do_QueryFrame(scrollbarBox->GetParent())) { + if (scrollFrame->IsRootScrollFrameOfDocument()) { + Document* doc = mContent->GetUncomposedDoc(); + if (doc) { + nsGlobalWindowInner* window = + nsGlobalWindowInner::Cast(doc->GetInnerWindow()); + if (window && + window->GetScrollMarksOnHScrollbar() == IsXULHorizontal() && + 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::BuildDisplayListForChildren( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + // if we are too small to have a thumb don't paint it. + nsIFrame* thumb = nsIFrame::GetChildXULBox(this); + + if (thumb) { + nsRect thumbRect(thumb->GetRect()); + nsMargin m; + thumb->GetXULMargin(m); + thumbRect.Inflate(m); + + nsRect sliderTrack; + GetXULClientRect(sliderTrack); + + 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 nsBoxFrame::BuildDisplayList so + // that the event region that gets created for the thumb is included in + // the nsDisplayOwnLayer contents. + + const mozilla::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 float appUnitsPerCss = float(AppUnitsPerCSSPixel()); + const CSSCoord thumbLength = NSAppUnitsToFloatPixels( + isHorizontal ? thumbRect.width : thumbRect.height, appUnitsPerCss); + + nsIFrame* scrollbarBox = GetScrollbar(); + 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 + GetRect().TopLeft() + + scrollbarBox->GetPosition() - scrollPortOrigin; + const CSSCoord sliderTrackStart = NSAppUnitsToFloatPixels( + isHorizontal ? sliderTrack.x : sliderTrack.y, appUnitsPerCss); + const CSSCoord sliderTrackLength = NSAppUnitsToFloatPixels( + isHorizontal ? sliderTrack.width : sliderTrack.height, + appUnitsPerCss); + const CSSCoord thumbStart = NSAppUnitsToFloatPixels( + isHorizontal ? thumbRect.x : thumbRect.y, appUnitsPerCss); + + const nsRect overflow = thumb->InkOverflowRectRelativeToParent(); + nsSize refSize = aBuilder->RootReferenceFrame()->GetSize(); + nsRect dirty = aBuilder->GetVisibleRect().Intersect(thumbRect); + dirty = nsLayoutUtils::ComputePartialPrerenderArea( + thumb, 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); + nsBoxFrame::BuildDisplayListForChildren(aBuilder, 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, + isAsyncDraggable, sliderTrackStart, + sliderTrackLength, scrollTargetId), + true, false); + + return; + } + } + + nsBoxFrame::BuildDisplayListForChildren(aBuilder, aLists); +} + +NS_IMETHODIMP +nsSliderFrame::DoXULLayout(nsBoxLayoutState& aState) { + // get the thumb should be our only child + nsIFrame* thumbBox = nsIFrame::GetChildXULBox(this); + + if (!thumbBox) { + SyncXULLayout(aState); + return NS_OK; + } + + EnsureOrient(); + + // get the content area inside our borders + nsRect clientRect; + GetXULClientRect(clientRect); + + // get the scrollbar + nsIFrame* scrollbarBox = GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox); + + // get the thumb's pref size + nsSize thumbSize = thumbBox->GetXULPrefSize(aState); + + if (IsXULHorizontal()) + thumbSize.height = clientRect.height; + else + thumbSize.width = clientRect.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); + + nscoord& availableLength = + IsXULHorizontal() ? clientRect.width : clientRect.height; + nscoord& thumbLength = IsXULHorizontal() ? thumbSize.width : thumbSize.height; + + if ((pageIncrement + maxPos - minPos) > 0 && thumbBox->GetXULFlex() > 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. + nsRect thumbRect(clientRect.x, clientRect.y, thumbSize.width, + thumbSize.height); + int32_t& thumbPos = (IsXULHorizontal() ? thumbRect.x : thumbRect.y); + thumbPos += NSToCoordRound(pos * mRatio); + + nsRect oldThumbRect(thumbBox->GetRect()); + LayoutChildAt(aState, thumbBox, thumbRect); + + SyncXULLayout(aState); + + // Redraw only if thumb changed size. + if (!oldThumbRect.IsEqualInterior(thumbRect)) XULRedraw(aState); + + return NS_OK; +} + +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; + } + + nsIFrame* scrollbarBox = GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar; + scrollbar = GetContentOfBox(scrollbarBox); + bool isHorizontal = IsXULHorizontal(); + + 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() { + if (GetScrollbar() != this) { + return LookAndFeel::GetInt(LookAndFeel::IntID::ScrollToClick, false); + } + + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::movetoclick, + nsGkAtoms::_true, eCaseMatters)) { + return true; + } + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::movetoclick, + nsGkAtoms::_false, eCaseMatters)) { + return false; + } + +#ifdef XP_MACOSX + return true; +#else + return false; +#endif +} + +nsIFrame* nsSliderFrame::GetScrollbar() { + // if we are in a scrollbar then return the scrollbar's content node + // if we are not then return ours. + nsIFrame* scrollbar; + nsScrollbarButtonFrame::GetParentWithTag(nsGkAtoms::scrollbar, this, + scrollbar); + + if (scrollbar == nullptr) return this; + + return scrollbar->IsXULBoxFrame() ? scrollbar : this; +} + +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 = GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar; + scrollbar = GetContentOfBox(scrollbarBox); + + 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() { + nsIFrame* scrollbarBox = GetScrollbar(); + nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox); + + // 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; // The thumb may stream in asynchronously via XBL. + + nsRect thumbRect = thumbFrame->GetRect(); + + nsRect clientRect; + GetXULClientRect(clientRect); + + // figure out the new rect + nsRect newThumbRect(thumbRect); + + bool reverse = mContent->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::reverse, eCaseMatters); + nscoord pos = reverse ? (maxPos - curPos) : (curPos - minPos); + + if (IsXULHorizontal()) + newThumbRect.x = clientRect.x + NSToCoordRound(pos * mRatio); + else + newThumbRect.y = clientRect.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 (IsXULHorizontal()) { + newThumbRect.x = snappedThumbLocation.x; + } else { + newThumbRect.y = snappedThumbLocation.y; + } + + // set the rect + thumbFrame->SetRect(newThumbRect); + + // Request a repaint of the scrollbar + nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox); + nsIScrollbarMediator* mediator = + scrollbarFrame ? scrollbarFrame->GetScrollbarMediator() : nullptr; + 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) { + nsRect crect; + GetXULClientRect(crect); + nscoord offset = IsXULHorizontal() ? crect.x : crect.y; + int32_t newPos = NSToIntRound((aNewThumbPos - offset) / 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; + nsIFrame* scrollbarBox = GetScrollbar(); + AutoWeakFrame weakFrame(this); + + mUserChanged = true; + + nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox); + if (scrollbarFrame) { + // See if we have a mediator. + nsIScrollbarMediator* mediator = scrollbarFrame->GetScrollbarMediator(); + if (mediator) { + nscoord oldPos = + nsPresContext::CSSPixelsToAppUnits(GetCurrentPosition(scrollbar)); + nscoord newPos = nsPresContext::CSSPixelsToAppUnits(aNewPos); + mediator->ThumbMoved(scrollbarFrame, 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) { + nsBoxFrame::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() { + nsIFrame* scrollbarBox = GetScrollbar(); + if (!scrollbarBox) { + return nullptr; + } + + nsContainerFrame* scrollFrame = scrollbarBox->GetParent(); + if (!scrollFrame) { + return nullptr; + } + + nsIScrollableFrame* scrollFrameAsScrollable = do_QueryFrame(scrollFrame); + return scrollFrameAsScrollable; +} + +void nsSliderFrame::StartAPZDrag(WidgetGUIEvent* aEvent) { + if (!aEvent->mFlags.mHandledByAPZ) { + return; + } + + if (!gfxPlatform::GetPlatform()->SupportsApzDragInput()) { + return; + } + + nsIFrame* scrollbarBox = GetScrollbar(); + 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 = IsXULHorizontal(); + + mozilla::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; + } + + mozilla::PresShell* presShell = PresShell(); + uint64_t inputblockId = InputAPZContext::GetInputBlockId(); + uint32_t presShellId = presShell->GetPresShellId(); + AsyncDragMetrics dragMetrics( + scrollTargetId, presShellId, inputblockId, + NSAppUnitsToFloatPixels(mDragStart, float(AppUnitsPerCSSPixel())), + 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 = IsXULHorizontal(); + 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); + + nsIFrame* scrollbarBox = GetScrollbar(); + scrollbar = GetContentOfBox(scrollbarBox); + } + + 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; + } + + nsRect thumbRect = thumbFrame->GetRect(); +#if defined(MOZ_WIDGET_GTK) + /* Scrollbar track can have padding, so it's better to check that eventPoint + * is inside of actual thumb, not just its one axis. The part of the scrollbar + * track adjacent to thumb can actually receive events in GTK3 */ + return eventPoint.x >= thumbRect.x && eventPoint.x < thumbRect.XMost() && + eventPoint.y >= thumbRect.y && eventPoint.y < thumbRect.YMost(); +#else + bool isHorizontal = IsXULHorizontal(); + 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; +#endif +} + +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 (IsXULHorizontal() ? 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 + nsRect clientRect; + GetXULClientRect(clientRect); + + // Set the destination point to the very end of the scrollbar so that + // scrolling doesn't stop halfway through. + if (change > 0) { + mDestinationPoint = nsPoint(clientRect.width, clientRect.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(); + + nsIFrame* scrollbar = GetScrollbar(); + nsScrollbarFrame* sb = do_QueryFrame(scrollbar); + if (sb) { + nsIScrollbarMediator* m = sb->GetScrollbarMediator(); + if (m) { + m->ScrollbarReleased(sb); + } + } + return NS_OK; +} + +void nsSliderFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + // tell our mediator if we have one we are gone. + if (mMediator) { + mMediator->SetSlider(nullptr); + mMediator = nullptr; + } + StopRepeat(); + + // call base class Destroy() + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +nsSize nsSliderFrame::GetXULPrefSize(nsBoxLayoutState& aState) { + EnsureOrient(); + return nsBoxFrame::GetXULPrefSize(aState); +} + +nsSize nsSliderFrame::GetXULMinSize(nsBoxLayoutState& aState) { + EnsureOrient(); + + // our min size is just our borders and padding + return nsIFrame::GetUncachedXULMinSize(aState); +} + +nsSize nsSliderFrame::GetXULMaxSize(nsBoxLayoutState& aState) { + EnsureOrient(); + return nsBoxFrame::GetXULMaxSize(aState); +} + +void nsSliderFrame::EnsureOrient() { + nsIFrame* scrollbarBox = GetScrollbar(); + + bool isHorizontal = scrollbarBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL); + if (isHorizontal) + AddStateBits(NS_STATE_IS_HORIZONTAL); + else + RemoveStateBits(NS_STATE_IS_HORIZONTAL); +} + +void nsSliderFrame::Notify(void) { + bool stop = false; + + nsIFrame* thumbFrame = mFrames.FirstChild(); + if (!thumbFrame) { + StopRepeat(); + return; + } + nsRect thumbRect = thumbFrame->GetRect(); + + bool isHorizontal = IsXULHorizontal(); + + // 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; + } + nsIFrame* scrollbar = GetScrollbar(); + nsScrollbarFrame* sb = do_QueryFrame(scrollbar); + if (sb) { + nsIScrollbarMediator* m = sb->GetScrollbarMediator(); + sb->SetIncrementToPage(aChange); + if (m) { + 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 / mozilla::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(); +} + +NS_IMPL_ISUPPORTS(nsSliderMediator, nsIDOMEventListener) diff --git a/layout/xul/nsSliderFrame.h b/layout/xul/nsSliderFrame.h new file mode 100644 index 0000000000..f3323d6e46 --- /dev/null +++ b/layout/xul/nsSliderFrame.h @@ -0,0 +1,226 @@ +/* -*- 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 "nsRepeatService.h" +#include "nsBoxFrame.h" +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsITimer.h" +#include "nsIDOMEventListener.h" + +class nsITimer; +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; } + + virtual void SetSlider(nsSliderFrame* aSlider) { mSlider = aSlider; } + + NS_DECL_NSIDOMEVENTLISTENER + + protected: + virtual ~nsSliderMediator() = default; +}; + +class nsSliderFrame final : public nsBoxFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsSliderFrame) + NS_DECL_QUERYFRAME + + friend class nsSliderMediator; + friend class mozilla::nsDisplaySliderMarks; + + explicit nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsSliderFrame(); + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SliderFrame"_ns, aResult); + } +#endif + + virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) override; + NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override; + + // nsIFrame overrides + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + virtual void BuildDisplayListForChildren( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* asPrevInFlow) override; + + virtual 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; + virtual void RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) 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); + void EnsureOrient(); + + 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(); + nsIFrame* GetScrollbar(); + 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; + + 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..9874b8a800 --- /dev/null +++ b/layout/xul/nsSplitterFrame.cpp @@ -0,0 +1,956 @@ +/* -*- 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 "gfxContext.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 "nsBoxLayoutState.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(nsBoxLayoutState& aState, 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) + : nsBoxFrame(aStyle, aPresContext, kClassID) {} + +void nsSplitterFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + if (mInner) { + mInner->RemoveListener(); + mInner->Disconnect(); + mInner = nullptr; + } + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +nsresult nsSplitterFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = + nsBoxFrame::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); + + nsBoxFrame::Init(aContent, aParent, aPrevInFlow); + + mInner->AddListener(); + mInner->mParentBox = nullptr; +} + +static bool IsValidParentBox(nsIFrame* aFrame) { + return aFrame->IsXULBoxFrame() || aFrame->IsFlexContainerFrame(); +} + +static nsIFrame* GetValidParentBox(nsIFrame* aChild) { + return aChild->GetParent() && IsValidParentBox(aChild->GetParent()) + ? aChild->GetParent() + : nullptr; +} + +NS_IMETHODIMP +nsSplitterFrame::DoXULLayout(nsBoxLayoutState& aState) { + if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + mInner->mParentBox = GetValidParentBox(this); + mInner->UpdateState(); + } + + return nsBoxFrame::DoXULLayout(aState); +} + +static bool SplitterIsHorizontal(const nsIFrame* aParentBox) { + // If our parent is horizontal, the splitter is vertical and vice-versa. + if (aParentBox->IsXULBoxFrame()) { + return !aParentBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL); + } + MOZ_ASSERT(aParentBox->IsFlexContainerFrame()); + const FlexboxAxisInfo info(aParentBox); + return !info.mIsRowOriented; +} + +void nsSplitterFrame::GetInitialOrientation(bool& aIsHorizontal) { + if (nsIFrame* parent = GetValidParentBox(this)) { + aIsHorizontal = SplitterIsHorizontal(parent); + } else { + nsBoxFrame::GetInitialOrientation(aIsHorizontal); + } +} + +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) { + nsBoxFrame::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 nsBoxFrame::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->IsXULHorizontal(); + 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)}; +} + +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 + nsPresContext* outerPresContext = mOuter->PresContext(); + + RefPtr<gfxContext> rc = + outerPresContext->PresShell()->CreateReferenceRenderingContext(); + nsBoxLayoutState state(outerPresContext, rc); + + mDidDrag = false; + + EnsureOrient(); + const bool isHorizontal = !mOuter->IsXULHorizontal(); + + 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::BoxOrdinalGroup); + 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(); + const nscoord flex = childBox->GetXULFlex(); + 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 minSize; + nsSize prefSize; + nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + nsSize curSize = childBox->GetSize(); + if (childBox->IsXULBoxFrame()) { + minSize = childBox->GetXULMinSize(state); + maxSize = childBox->GetXULMaxSize(state); + prefSize = childBox->GetXULPrefSize(state); + } else { + const auto& pos = *childBox->StylePosition(); + minSize = ToLengthWithFallback(pos.mMinWidth, pos.mMinHeight); + maxSize = ToLengthWithFallback(pos.mMaxWidth, pos.mMaxHeight, + NS_UNCONSTRAINEDSIZE); + prefSize.width = ToLengthWithFallback(pos.mWidth, curSize.width); + prefSize.height = ToLengthWithFallback(pos.mHeight, curSize.height); + } + + maxSize = nsIFrame::XULBoundsCheckMinMax(minSize, maxSize); + prefSize = nsIFrame::XULBoundsCheck(minSize, prefSize, maxSize); + + nsSplitterFrame::AddXULMargin(childBox, minSize); + nsSplitterFrame::AddXULMargin(childBox, maxSize); + nsSplitterFrame::AddXULMargin(childBox, prefSize); + nsSplitterFrame::AddXULMargin(childBox, curSize); + + 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 = [&] { + if (mParentBox->IsXULBoxFrame()) { + return !mParentBox->IsXULNormalDirection(); + } + 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; +} + +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 = + nsBoxFrame::SlowOrdinalGroupAwareSibling(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() { + if (SplitterIsHorizontal(mParentBox)) + mOuter->AddStateBits(NS_STATE_IS_HORIZONTAL); + else + mOuter->RemoveStateBits(NS_STATE_IS_HORIZONTAL); +} + +void nsSplitterFrameInner::AdjustChildren(nsPresContext* aPresContext) { + EnsureOrient(); + const bool isHorizontal = !mOuter->IsXULHorizontal(); + + 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"); + + nsBoxLayoutState state(aPresContext); + + for (auto& info : aChildInfos) { + nscoord newPref = info.pref + (info.changed - info.current); + if (nsIFrame* childBox = + GetChildBoxForContent(mParentBox, info.childElem)) { + SetPreferredSize(state, childBox, aIsHorizontal, newPref); + } + } +} + +void nsSplitterFrameInner::SetPreferredSize(nsBoxLayoutState& aState, + nsIFrame* aChildBox, + bool aIsHorizontal, nscoord aSize) { + nsMargin margin(0, 0, 0, 0); + aChildBox->GetXULMargin(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..75af7cd83c --- /dev/null +++ b/layout/xul/nsSplitterFrame.h @@ -0,0 +1,83 @@ +/* -*- 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 "nsBoxFrame.h" + +class nsSplitterFrameInner; + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSplitterFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +class nsSplitterFrame final : public nsBoxFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsSplitterFrame) + + explicit nsSplitterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SplitterFrame"_ns, aResult); + } +#endif + + // nsIFrame overrides + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) 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; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual void GetInitialOrientation(bool& aIsHorizontal) override; + + private: + friend class nsSplitterFrameInner; + RefPtr<nsSplitterFrameInner> mInner; + +}; // class nsSplitterFrame + +#endif diff --git a/layout/xul/nsSprocketLayout.cpp b/layout/xul/nsSprocketLayout.cpp new file mode 100644 index 0000000000..e0f5e21d7e --- /dev/null +++ b/layout/xul/nsSprocketLayout.cpp @@ -0,0 +1,1372 @@ +/* -*- 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 "nsBoxLayoutState.h" +#include "nsSprocketLayout.h" +#include "nsPresContext.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsContainerFrame.h" +#include "nsBoxFrame.h" +#include "StackArena.h" +#include "mozilla/Likely.h" +#include "mozilla/CSSOrderAwareFrameIterator.h" +#include <algorithm> + +using mozilla::StyleDirection; +using namespace mozilla; + +nsBoxLayout* nsSprocketLayout::gInstance = nullptr; + +static Maybe<CSSOrderAwareFrameIterator> IterFor(nsIFrame* aBoxFrame) { + Maybe<CSSOrderAwareFrameIterator> ret; + if (aBoxFrame->IsXULBoxFrame()) { + ret.emplace(aBoxFrame, FrameChildListID::Principal, + CSSOrderAwareFrameIterator::ChildFilter::IncludeAll, + CSSOrderAwareFrameIterator::OrderState::Unknown, + CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup); + } + return ret; +} + +nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout) { + if (!nsSprocketLayout::gInstance) { + nsSprocketLayout::gInstance = new nsSprocketLayout(); + NS_IF_ADDREF(nsSprocketLayout::gInstance); + } + // we have not instance variables so just return our static one. + aNewLayout = nsSprocketLayout::gInstance; + return NS_OK; +} + +/*static*/ +void nsSprocketLayout::Shutdown() { NS_IF_RELEASE(gInstance); } + +nsSprocketLayout::nsSprocketLayout() = default; + +bool nsSprocketLayout::IsXULHorizontal(nsIFrame* aBox) { + return aBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL); +} + +void nsSprocketLayout::GetFrameState(nsIFrame* aBox, nsFrameState& aState) { + aState = aBox->GetStateBits(); +} + +static StyleDirection GetFrameDirection(nsIFrame* aBox) { + return aBox->StyleVisibility()->mDirection; +} + +static void HandleBoxPack(nsIFrame* aBox, const nsFrameState& aFrameState, + nscoord& aX, nscoord& aY, const nsRect& aOriginalRect, + const nsRect& aClientRect) { + // In the normal direction we lay out our kids in the positive direction + // (e.g., |x| will get bigger for a horizontal box, and |y| will get bigger + // for a vertical box). In the reverse direction, the opposite is true. We'll + // be laying out each child at a smaller |x| or |y|. + StyleDirection frameDirection = GetFrameDirection(aBox); + + if (aFrameState & NS_STATE_IS_HORIZONTAL) { + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) { + // The normal direction. |x| increases as we move through our children. + aX = aClientRect.x; + } else { + // The reverse direction. |x| decreases as we move through our children. + aX = aClientRect.x + aOriginalRect.width; + } + // |y| is always in the normal direction in horizontal boxes + aY = aClientRect.y; + } else { + // take direction property into account for |x| in vertical boxes + if (frameDirection == StyleDirection::Ltr) { + // The normal direction. |x| increases as we move through our children. + aX = aClientRect.x; + } else { + // The reverse direction. |x| decreases as we move through our children. + aX = aClientRect.x + aOriginalRect.width; + } + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) { + // The normal direction. |y| increases as we move through our children. + aY = aClientRect.y; + } else { + // The reverse direction. |y| decreases as we move through our children. + aY = aClientRect.y + aOriginalRect.height; + } + } + + // Get our pack/alignment information. + nsIFrame::Halignment halign = aBox->GetXULHAlign(); + nsIFrame::Valignment valign = aBox->GetXULVAlign(); + + // The following code handles box PACKING. Packing comes into play in the + // case where the computed size for all of our children (now stored in our + // client rect) is smaller than the size available for the box (stored in + // |aOriginalRect|). + // + // Here we adjust our |x| and |y| variables accordingly so that we start at + // the beginning, middle, or end of the box. + // + // XXXdwh JUSTIFY needs to be implemented! + if (aFrameState & NS_STATE_IS_HORIZONTAL) { + switch (halign) { + case nsBoxFrame::hAlign_Left: + break; // Nothing to do. The default initialized us properly. + + case nsBoxFrame::hAlign_Center: + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) + aX += (aOriginalRect.width - aClientRect.width) / 2; + else + aX -= (aOriginalRect.width - aClientRect.width) / 2; + break; + + case nsBoxFrame::hAlign_Right: + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) + aX += (aOriginalRect.width - aClientRect.width); + else + aX -= (aOriginalRect.width - aClientRect.width); + break; // Nothing to do for the reverse dir. The default initialized + // us properly. + } + } else { + switch (valign) { + case nsBoxFrame::vAlign_Top: + case nsBoxFrame::vAlign_BaseLine: // This value is technically impossible + // to specify for pack. + break; // Don't do anything. We were initialized correctly. + + case nsBoxFrame::vAlign_Middle: + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) + aY += (aOriginalRect.height - aClientRect.height) / 2; + else + aY -= (aOriginalRect.height - aClientRect.height) / 2; + break; + + case nsBoxFrame::vAlign_Bottom: + if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) + aY += (aOriginalRect.height - aClientRect.height); + else + aY -= (aOriginalRect.height - aClientRect.height); + break; + } + } +} + +NS_IMETHODIMP +nsSprocketLayout::XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState) { + // See if we are collapsed. If we are, then simply iterate over all our + // children and give them a rect of 0 width and height. + if (aBox->IsXULCollapsed()) { + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsBoxFrame::LayoutChildAt(aState, iter->get(), nsRect(0, 0, 0, 0)); + } + return NS_OK; + } + + nsBoxLayoutState::AutoReflowDepth depth(aState); + mozilla::AutoStackArena arena; + + // ----- figure out our size ---------- + const nsSize originalSize = aBox->GetSize(); + + // -- make sure we remove our border and padding ---- + nsRect clientRect; + aBox->GetXULClientRect(clientRect); + + // |originalClientRect| represents the rect of the entire box (excluding + // borders and padding). We store it here because we're going to use + // |clientRect| to hold the required size for all our kids. As an example, + // consider an hbox with a specified width of 300. If the kids total only 150 + // pixels of width, then we have 150 pixels left over. |clientRect| is going + // to hold a width of 150 and is going to be adjusted based off the value of + // the PACK property. If flexible objects are in the box, then the two rects + // will match. + nsRect originalClientRect(clientRect); + + // The frame state contains cached knowledge about our box, such as our + // orientation and direction. + nsFrameState frameState = nsFrameState(0); + GetFrameState(aBox, frameState); + + // Build a list of our children's desired sizes and computed sizes + nsBoxSize* boxSizes = nullptr; + nsComputedBoxSize* computedBoxSizes = nullptr; + + nscoord min = 0; + nscoord max = 0; + int32_t flexes = 0; + PopulateBoxSizes(aBox, aState, boxSizes, min, max, flexes); + + // The |size| variable will hold the total size of children along the axis of + // the box. Continuing with the example begun in the comment above, size + // would be 150 pixels. + nscoord size = clientRect.width; + if (!IsXULHorizontal(aBox)) size = clientRect.height; + ComputeChildSizes(aBox, aState, size, boxSizes, computedBoxSizes); + + // After the call to ComputeChildSizes, the |size| variable contains the + // total required size of all the children. We adjust our clientRect in the + // appropriate dimension to match this size. In our example, we now assign + // 150 pixels into the clientRect.width. + // + // The variables |min| and |max| hold the minimum required size box must be + // in the OPPOSITE orientation, e.g., for a horizontal box, |min| is the + // minimum height we require to enclose our children, and |max| is the maximum + // height required to enclose our children. + if (IsXULHorizontal(aBox)) { + clientRect.width = size; + if (clientRect.height < min) clientRect.height = min; + + if (frameState & NS_STATE_AUTO_STRETCH) { + if (clientRect.height > max) clientRect.height = max; + } + } else { + clientRect.height = size; + if (clientRect.width < min) clientRect.width = min; + + if (frameState & NS_STATE_AUTO_STRETCH) { + if (clientRect.width > max) clientRect.width = max; + } + } + + // With the sizes computed, now it's time to lay out our children. + bool finished; + nscoord passes = 0; + + // We flow children at their preferred locations (along with the appropriate + // computed flex). After we flow a child, it is possible that the child will + // change its size. If/when this happens, we have to do another pass. + // Typically only 2 passes are required, but the code is prepared to do as + // many passes as are necessary to achieve equilibrium. + nscoord x = 0; + nscoord y = 0; + nscoord origX = 0; + nscoord origY = 0; + + // |childResized| lets us know if a child changed its size after we attempted + // to lay it out at the specified size. If this happens, we usually have to + // do another pass. + bool childResized = false; + + // |passes| stores our number of passes. If for any reason we end up doing + // more than, say, 10 passes, we assert to indicate that something is + // seriously screwed up. + passes = 0; + do { + // Always assume that we're done. This will change if, for example, + // children don't stay the same size after being flowed. + finished = true; + + // Handle box packing. + HandleBoxPack(aBox, frameState, x, y, originalClientRect, clientRect); + + // Now that packing is taken care of we set up a few additional + // tracking variables. + origX = x; + origY = y; + + // Now we iterate over our box children and our box size lists in + // parallel. For each child, we look at its sizes and figure out + // where to place it. + nsComputedBoxSize* childComputedBoxSize = computedBoxSizes; + nsBoxSize* childBoxSize = boxSizes; + + auto iter = IterFor(aBox); + int32_t count = 0; + while ((iter && !iter->AtEnd()) || (childBoxSize && childBoxSize->bogus)) { + // If for some reason, our lists are not the same length, we guard + // by bailing out of the loop. + if (childBoxSize == nullptr) { + MOZ_ASSERT_UNREACHABLE("Lists not the same length."); + break; + } + + nscoord width = clientRect.width; + nscoord height = clientRect.height; + + if (!childBoxSize->bogus) { + nsIFrame* child = iter->get(); + + // We have a valid box size entry. This entry already contains + // information about our sizes along the axis of the box (e.g., widths + // in a horizontal box). If our default ALIGN is not stretch, however, + // then we also need to know the child's size along the opposite axis. + if (!(frameState & NS_STATE_AUTO_STRETCH)) { + nsSize prefSize = child->GetXULPrefSize(aState); + nsSize minSize = child->GetXULMinSize(aState); + nsSize maxSize = child->GetXULMaxSize(aState); + prefSize = nsIFrame::XULBoundsCheck(minSize, prefSize, maxSize); + + AddXULMargin(child, prefSize); + width = std::min(prefSize.width, originalClientRect.width); + height = std::min(prefSize.height, originalClientRect.height); + } + } + + // Obtain the computed size along the axis of the box for this child from + // the computedBoxSize entry. We store the result in |width| for + // horizontal boxes and |height| for vertical boxes. + if (frameState & NS_STATE_IS_HORIZONTAL) + width = childComputedBoxSize->size; + else + height = childComputedBoxSize->size; + + // Adjust our x/y for the left/right spacing. + if (frameState & NS_STATE_IS_HORIZONTAL) { + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + x += (childBoxSize->left); + else + x -= (childBoxSize->right); + } else { + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + y += (childBoxSize->left); + else + y -= (childBoxSize->right); + } + + // Now we build a child rect. + nscoord rectX = x; + nscoord rectY = y; + if (!(frameState & NS_STATE_IS_DIRECTION_NORMAL)) { + if (frameState & NS_STATE_IS_HORIZONTAL) + rectX -= width; + else + rectY -= height; + } + + // We now create an accurate child rect based off our computed size + // information. + nsRect childRect(rectX, rectY, width, height); + + // Sanity check against our clientRect. It is possible that a child + // specified a size that is too large to fit. If that happens, then we + // have to grow our client rect. Remember, clientRect is not the total + // rect of the enclosing box. It currently holds our perception of how + // big the children needed to be. + if (childRect.width > clientRect.width) + clientRect.width = childRect.width; + + if (childRect.height > clientRect.height) + clientRect.height = childRect.height; + + // Either |nextX| or |nextY| is updated by this function call, according + // to our axis. + nscoord nextX = x; + nscoord nextY = y; + + ComputeChildsNextPosition(aBox, x, y, nextX, nextY, childRect); + + // Now we further update our nextX/Y along our axis. + // We also set childRect.y/x along the opposite axis appropriately for a + // stretch alignment. (Non-stretch alignment is handled below.) + if (frameState & NS_STATE_IS_HORIZONTAL) { + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + nextX += (childBoxSize->right); + else + nextX -= (childBoxSize->left); + childRect.y = originalClientRect.y; + } else { + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + nextY += (childBoxSize->right); + else + nextY -= (childBoxSize->left); + if (GetFrameDirection(aBox) == StyleDirection::Ltr) { + childRect.x = originalClientRect.x; + } else { + // keep the right edge of the box the same + childRect.x = + clientRect.x + originalClientRect.width - childRect.width; + } + } + + // If we encounter a completely bogus box size, we just leave this child + // completely alone and continue through the loop to the next child. + if (childBoxSize->bogus) { + childComputedBoxSize = childComputedBoxSize->next; + childBoxSize = childBoxSize->next; + count++; + x = nextX; + y = nextY; + // FIXME(emilio): shouldn't this update `child` / `iter`? This looks + // broken. + continue; + } + + nsIFrame* child = iter->get(); + nsMargin margin(0, 0, 0, 0); + + bool layout = true; + + // Deflate the rect of our child by its margin. + child->GetXULMargin(margin); + childRect.Deflate(margin); + if (childRect.width < 0) childRect.width = 0; + if (childRect.height < 0) childRect.height = 0; + + // Now we're trying to figure out if we have to lay out this child, i.e., + // to call the child's XULLayout method. + if (passes > 0) { + layout = false; + } else { + // Always perform layout if we are dirty or have dirty children + if (!child->IsSubtreeDirty()) { + layout = false; + } + } + + nsRect oldRect(child->GetRect()); + + // Non-stretch alignment will be handled in AlignChildren(), so don't + // change child out-of-axis positions yet. + if (!(frameState & NS_STATE_AUTO_STRETCH)) { + if (frameState & NS_STATE_IS_HORIZONTAL) { + childRect.y = oldRect.y; + } else { + childRect.x = oldRect.x; + } + } + + // We computed a childRect. Now we want to set the bounds of the child to + // be that rect. If our old rect is different, then we know our size + // changed and we cache that fact in the |sizeChanged| variable. + + child->SetXULBounds(aState, childRect); + bool sizeChanged = (childRect.width != oldRect.width || + childRect.height != oldRect.height); + + if (sizeChanged) { + // Our size is different. Sanity check against our maximum allowed size + // to ensure we didn't exceed it. + nsSize minSize = child->GetXULMinSize(aState); + nsSize maxSize = child->GetXULMaxSize(aState); + maxSize = nsIFrame::XULBoundsCheckMinMax(minSize, maxSize); + + // make sure the size is in our max size. + if (childRect.width > maxSize.width) childRect.width = maxSize.width; + + if (childRect.height > maxSize.height) + childRect.height = maxSize.height; + + // set it again + child->SetXULBounds(aState, childRect); + } + + // If we already determined that layout was required or if our size has + // changed, then we make sure to call layout on the child, since its + // children may need to be shifted around as a result of the size change. + if (layout || sizeChanged) child->XULLayout(aState); + + // If the child was a block or inline (e.g., HTML) it may have changed its + // rect *during* layout. We have to check for this. + nsRect newChildRect(child->GetRect()); + + if (!newChildRect.IsEqualInterior(childRect)) { +#ifdef DEBUG_GROW + printf(" GREW from (%d,%d) -> (%d,%d)\n", childRect.width, + childRect.height, newChildRect.width, newChildRect.height); +#endif + newChildRect.Inflate(margin); + childRect.Inflate(margin); + + // The child changed size during layout. The ChildResized method + // handles this scenario. + ChildResized(aBox, aState, child, childBoxSize, childComputedBoxSize, + boxSizes, computedBoxSizes, childRect, newChildRect, + clientRect, flexes, finished); + + // We note that a child changed size, which means that another pass will + // be required. + childResized = true; + + // Now that a child resized, it's entirely possible that OUR rect is too + // small. Now we ensure that |originalClientRect| is grown to + // accommodate the size of |clientRect|. + if (clientRect.width > originalClientRect.width) + originalClientRect.width = clientRect.width; + + if (clientRect.height > originalClientRect.height) + originalClientRect.height = clientRect.height; + + if (!(frameState & NS_STATE_IS_DIRECTION_NORMAL)) { + // Our childRect had its XMost() or YMost() (depending on our layout + // direction), positioned at a certain point. Ensure that the + // newChildRect satisfies the same constraint. Note that this is + // just equivalent to adjusting the x/y by the difference in + // width/height between childRect and newChildRect. So we don't need + // to reaccount for the left and right of the box layout state again. + if (frameState & NS_STATE_IS_HORIZONTAL) + newChildRect.x = childRect.XMost() - newChildRect.width; + else + newChildRect.y = childRect.YMost() - newChildRect.height; + } + + if (!(frameState & NS_STATE_IS_HORIZONTAL)) { + if (GetFrameDirection(aBox) != StyleDirection::Ltr) { + // keep the right edge the same + newChildRect.x = childRect.XMost() - newChildRect.width; + } + } + + // If the child resized then recompute its position. + ComputeChildsNextPosition(aBox, x, y, nextX, nextY, newChildRect); + + if (newChildRect.width >= margin.left + margin.right && + newChildRect.height >= margin.top + margin.bottom) + newChildRect.Deflate(margin); + + if (childRect.width >= margin.left + margin.right && + childRect.height >= margin.top + margin.bottom) + childRect.Deflate(margin); + + child->SetXULBounds(aState, newChildRect); + + // If we are the first box that changed size, then we don't need to do a + // second pass + if (count == 0) finished = true; + } + + // Now update our x/y finally. + x = nextX; + y = nextY; + + // Move to the next child. + childComputedBoxSize = childComputedBoxSize->next; + childBoxSize = childBoxSize->next; + + iter->Next(); + count++; + } + + // Sanity-checking code to ensure we don't do an infinite # of passes. + passes++; + NS_ASSERTION(passes < 10, "A Box's child is constantly growing!!!!!"); + if (passes >= 10) break; + } while (false == finished); + + // Get rid of our size lists. + while (boxSizes) { + nsBoxSize* toDelete = boxSizes; + boxSizes = boxSizes->next; + delete toDelete; + } + + while (computedBoxSizes) { + nsComputedBoxSize* toDelete = computedBoxSizes; + computedBoxSizes = computedBoxSizes->next; + delete toDelete; + } + + if (childResized) { + // See if one of our children forced us to get bigger + nsRect tmpClientRect(originalClientRect); + nsMargin bp(0, 0, 0, 0); + aBox->GetXULBorderAndPadding(bp); + tmpClientRect.Inflate(bp); + + if (tmpClientRect.width > originalSize.width || + tmpClientRect.height > originalSize.height) { + // if it did reset our bounds. + nsRect bounds(aBox->GetRect()); + if (tmpClientRect.width > originalSize.width) + bounds.width = tmpClientRect.width; + + if (tmpClientRect.height > originalSize.height) + bounds.height = tmpClientRect.height; + + aBox->SetXULBounds(aState, bounds); + } + } + + // Because our size grew, we now have to readjust because of box packing. + // Repack in order to update our x and y to the correct values. + HandleBoxPack(aBox, frameState, x, y, originalClientRect, clientRect); + + // Compare against our original x and y and only worry about adjusting the + // children if we really did have to change the positions because of packing + // (typically for 'center' or 'end' pack values). + if (x != origX || y != origY) { + // reposition all our children + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + nsRect childRect(child->GetRect()); + childRect.x += (x - origX); + childRect.y += (y - origY); + child->SetXULBounds(aState, childRect); + } + } + + // Perform out-of-axis alignment for non-stretch alignments + if (!(frameState & NS_STATE_AUTO_STRETCH)) { + AlignChildren(aBox, aState); + } + + // That's it! If you made it this far without having a nervous breakdown, + // congratulations! Go get yourself a beer. + return NS_OK; +} + +void nsSprocketLayout::PopulateBoxSizes(nsIFrame* aBox, + nsBoxLayoutState& aState, + nsBoxSize*& aBoxSizes, + nscoord& aMinSize, nscoord& aMaxSize, + int32_t& aFlexes) { + aMinSize = 0; + aMaxSize = NS_UNCONSTRAINEDSIZE; + + bool isHorizontal; + + if (IsXULHorizontal(aBox)) + isHorizontal = true; + else + isHorizontal = false; + + // this is a nice little optimization + // it turns out that if we only have 1 flexable child + // then it does not matter what its preferred size is + // there is nothing to flex it relative. This is great + // because we can avoid asking for a preferred size in this + // case. Why is this good? Well you might have html inside it + // and asking html for its preferred size is rather expensive. + // so we can just optimize it out this way. + + // set flexes + aFlexes = 0; + nsBoxSize* currentBox = aBoxSizes; + nsBoxSize* last = nullptr; + + nscoord maxFlex = 0; + int32_t childCount = 0; + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + while (currentBox && currentBox->bogus) { + last = currentBox; + currentBox = currentBox->next; + } + ++childCount; + nsSize pref(0, 0); + nsSize minSize(0, 0); + nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + bool collapsed = child->IsXULCollapsed(); + + if (!collapsed) { + // only one flexible child? Cool we will just make its preferred size + // 0 then and not even have to ask for it. + // if (flexes != 1) { + + pref = child->GetXULPrefSize(aState); + minSize = child->GetXULMinSize(aState); + maxSize = + nsIFrame::XULBoundsCheckMinMax(minSize, child->GetXULMaxSize(aState)); + child->GetXULBoxAscent(aState); + //} + + pref = nsIFrame::XULBoundsCheck(minSize, pref, maxSize); + + AddXULMargin(child, pref); + AddXULMargin(child, minSize); + AddXULMargin(child, maxSize); + } + + if (!currentBox) { + // create one. + currentBox = new (aState) nsBoxSize(); + if (!aBoxSizes) { + aBoxSizes = currentBox; + last = aBoxSizes; + } else { + last->next = currentBox; + last = currentBox; + } + + nscoord minWidth; + nscoord maxWidth; + nscoord prefWidth; + + // get sizes from child + if (isHorizontal) { + minWidth = minSize.width; + maxWidth = maxSize.width; + prefWidth = pref.width; + } else { + minWidth = minSize.height; + maxWidth = maxSize.height; + prefWidth = pref.height; + } + + nscoord flex = child->GetXULFlex(); + + // set them if you collapsed you are not flexible. + if (collapsed) { + currentBox->flex = 0; + } else { + if (flex > maxFlex) { + maxFlex = flex; + } + currentBox->flex = flex; + } + + currentBox->pref = prefWidth; + currentBox->min = minWidth; + currentBox->max = maxWidth; + + NS_ASSERTION(minWidth <= prefWidth && prefWidth <= maxWidth, + "Bad min, pref, max widths!"); + } + + if (!isHorizontal) { + if (minSize.width > aMinSize) aMinSize = minSize.width; + + if (maxSize.width < aMaxSize) aMaxSize = maxSize.width; + + } else { + if (minSize.height > aMinSize) aMinSize = minSize.height; + + if (maxSize.height < aMaxSize) aMaxSize = maxSize.height; + } + + currentBox->collapsed = collapsed; + aFlexes += currentBox->flex; + + last = currentBox; + currentBox = currentBox->next; + } + + if (childCount > 0) { + nscoord maxAllowedFlex = nscoord_MAX / childCount; + + if (MOZ_UNLIKELY(maxFlex > maxAllowedFlex)) { + // clamp all the flexes + currentBox = aBoxSizes; + while (currentBox) { + currentBox->flex = std::min(currentBox->flex, maxAllowedFlex); + currentBox = currentBox->next; + } + } + } +#ifdef DEBUG + else { + NS_ASSERTION(maxFlex == 0, "How did that happen?"); + } +#endif +} + +void nsSprocketLayout::ComputeChildsNextPosition( + nsIFrame* aBox, const nscoord& aCurX, const nscoord& aCurY, nscoord& aNextX, + nscoord& aNextY, const nsRect& aCurrentChildSize) { + // Get the position along the box axis for the child. + // The out-of-axis position is not set. + nsFrameState frameState = nsFrameState(0); + GetFrameState(aBox, frameState); + + if (IsXULHorizontal(aBox)) { + // horizontal box's children. + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + aNextX = aCurX + aCurrentChildSize.width; + else + aNextX = aCurX - aCurrentChildSize.width; + + } else { + // vertical box's children. + if (frameState & NS_STATE_IS_DIRECTION_NORMAL) + aNextY = aCurY + aCurrentChildSize.height; + else + aNextY = aCurY - aCurrentChildSize.height; + } +} + +void nsSprocketLayout::AlignChildren(nsIFrame* aBox, nsBoxLayoutState& aState) { + nsFrameState frameState = nsFrameState(0); + GetFrameState(aBox, frameState); + bool isHorizontal = (frameState & NS_STATE_IS_HORIZONTAL) != 0; + nsRect clientRect; + aBox->GetXULClientRect(clientRect); + + MOZ_ASSERT(!(frameState & NS_STATE_AUTO_STRETCH), + "Only AlignChildren() with non-stretch alignment"); + + // These are only calculated if needed + nsIFrame::Halignment halign; + nsIFrame::Valignment valign; + nscoord maxAscent = 0; + bool isLTR; + + if (isHorizontal) { + valign = aBox->GetXULVAlign(); + if (valign == nsBoxFrame::vAlign_BaseLine) { + maxAscent = aBox->GetXULBoxAscent(aState); + } + } else { + isLTR = GetFrameDirection(aBox) == StyleDirection::Ltr; + halign = aBox->GetXULHAlign(); + } + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + nsMargin margin; + child->GetXULMargin(margin); + nsRect childRect = child->GetRect(); + + if (isHorizontal) { + const nscoord startAlign = clientRect.y + margin.top; + const nscoord endAlign = + clientRect.YMost() - margin.bottom - childRect.height; + + nscoord y = 0; + switch (valign) { + case nsBoxFrame::vAlign_Top: + y = startAlign; + break; + case nsBoxFrame::vAlign_Middle: + // Should this center the border box? + // This centers the margin box, the historical behavior. + y = (startAlign + endAlign) / 2; + break; + case nsBoxFrame::vAlign_Bottom: + y = endAlign; + break; + case nsBoxFrame::vAlign_BaseLine: + // Alignments don't force the box to grow (only sizes do), + // so keep the children within the box. + y = maxAscent - child->GetXULBoxAscent(aState); + y = std::max(startAlign, y); + y = std::min(y, endAlign); + break; + } + + childRect.y = y; + + } else { // vertical box + const nscoord leftAlign = clientRect.x + margin.left; + const nscoord rightAlign = + clientRect.XMost() - margin.right - childRect.width; + + nscoord x = 0; + switch (halign) { + case nsBoxFrame::hAlign_Left: // start + x = isLTR ? leftAlign : rightAlign; + break; + case nsBoxFrame::hAlign_Center: + x = (leftAlign + rightAlign) / 2; + break; + case nsBoxFrame::hAlign_Right: // end + x = isLTR ? rightAlign : leftAlign; + break; + } + + childRect.x = x; + } + + if (childRect.TopLeft() != child->GetPosition()) { + child->SetXULBounds(aState, childRect); + } + } +} + +void nsSprocketLayout::ChildResized( + nsIFrame* aBox, nsBoxLayoutState& aState, nsIFrame* aChild, + nsBoxSize* aChildBoxSize, nsComputedBoxSize* aChildComputedSize, + nsBoxSize* aBoxSizes, nsComputedBoxSize* aComputedBoxSizes, + const nsRect& aChildLayoutRect, nsRect& aChildActualRect, + nsRect& aContainingRect, int32_t aFlexes, bool& aFinished) + +{ + nsRect childCurrentRect(aChildLayoutRect); + + bool isHorizontal = IsXULHorizontal(aBox); + nscoord childLayoutWidth = GET_WIDTH(aChildLayoutRect, isHorizontal); + nscoord& childActualWidth = GET_WIDTH(aChildActualRect, isHorizontal); + nscoord& containingWidth = GET_WIDTH(aContainingRect, isHorizontal); + + // nscoord childLayoutHeight = GET_HEIGHT(aChildLayoutRect,isHorizontal); + nscoord& childActualHeight = GET_HEIGHT(aChildActualRect, isHorizontal); + nscoord& containingHeight = GET_HEIGHT(aContainingRect, isHorizontal); + + bool recompute = false; + + // if we are a horizontal box see if the child will fit inside us. + if (childActualHeight > containingHeight) { + // if we are a horizontal box and the child is bigger than our height + + // ok if the height changed then we need to reflow everyone but us at the + // new height so we will set the changed index to be us. And signal that we + // need a new pass. + + nsSize min = aChild->GetXULMinSize(aState); + nsSize max = + nsIFrame::XULBoundsCheckMinMax(min, aChild->GetXULMaxSize(aState)); + AddXULMargin(aChild, max); + + if (isHorizontal) + childActualHeight = + max.height < childActualHeight ? max.height : childActualHeight; + else + childActualHeight = + max.width < childActualHeight ? max.width : childActualHeight; + + // only set if it changes + if (childActualHeight > containingHeight) { + containingHeight = childActualHeight; + + // remember we do not need to clear the resized list because changing the + // height of a horizontal box will not affect the width of any of its + // children because block flow left to right, top to bottom. Just trust me + // on this one. + aFinished = false; + + // only recompute if there are flexes. + if (aFlexes > 0) { + // relayout everything + recompute = true; + InvalidateComputedSizes(aComputedBoxSizes); + nsComputedBoxSize* node = aComputedBoxSizes; + + while (node) { + node->resized = false; + node = node->next; + } + } + } + } + + if (childActualWidth > childLayoutWidth) { + nsSize min = aChild->GetXULMinSize(aState); + nsSize max = + nsIFrame::XULBoundsCheckMinMax(min, aChild->GetXULMaxSize(aState)); + + AddXULMargin(aChild, max); + + // our width now becomes the new size + + if (isHorizontal) + childActualWidth = + max.width < childActualWidth ? max.width : childActualWidth; + else + childActualWidth = + max.height < childActualWidth ? max.height : childActualWidth; + + if (childActualWidth > childLayoutWidth) { + aChildComputedSize->size = childActualWidth; + aChildBoxSize->min = childActualWidth; + if (aChildBoxSize->pref < childActualWidth) + aChildBoxSize->pref = childActualWidth; + if (aChildBoxSize->max < childActualWidth) + aChildBoxSize->max = childActualWidth; + + // if we have flexible elements with us then reflex things. Otherwise we + // can skip doing it. + if (aFlexes > 0) { + InvalidateComputedSizes(aComputedBoxSizes); + + nsComputedBoxSize* node = aComputedBoxSizes; + aChildComputedSize->resized = true; + + while (node) { + if (node->resized) node->valid = true; + + node = node->next; + } + + recompute = true; + aFinished = false; + } else { + containingWidth += aChildComputedSize->size - childLayoutWidth; + } + } + } + + if (recompute) + ComputeChildSizes(aBox, aState, containingWidth, aBoxSizes, + aComputedBoxSizes); + + if (!childCurrentRect.IsEqualInterior(aChildActualRect)) { + // the childRect includes the margin + // make sure we remove it before setting + // the bounds. + nsMargin margin(0, 0, 0, 0); + aChild->GetXULMargin(margin); + nsRect rect(aChildActualRect); + if (rect.width >= margin.left + margin.right && + rect.height >= margin.top + margin.bottom) + rect.Deflate(margin); + + aChild->SetXULBounds(aState, rect); + aChild->XULLayout(aState); + } +} + +void nsSprocketLayout::InvalidateComputedSizes( + nsComputedBoxSize* aComputedBoxSizes) { + while (aComputedBoxSizes) { + aComputedBoxSizes->valid = false; + aComputedBoxSizes = aComputedBoxSizes->next; + } +} + +void nsSprocketLayout::ComputeChildSizes( + nsIFrame* aBox, nsBoxLayoutState& aState, nscoord& aGivenSize, + nsBoxSize* aBoxSizes, nsComputedBoxSize*& aComputedBoxSizes) { + // nscoord onePixel = aState.PresContext()->IntScaledPixelsToTwips(1); + + int32_t sizeRemaining = aGivenSize; + int32_t spacerConstantsRemaining = 0; + + // ----- calculate the spacers constants and the size remaining ----- + + if (!aComputedBoxSizes) aComputedBoxSizes = new (aState) nsComputedBoxSize(); + + nsBoxSize* boxSizes = aBoxSizes; + nsComputedBoxSize* computedBoxSizes = aComputedBoxSizes; + int32_t count = 0; + int32_t validCount = 0; + + while (boxSizes) { + NS_ASSERTION( + (boxSizes->min <= boxSizes->pref && boxSizes->pref <= boxSizes->max), + "bad pref, min, max size"); + + // ignore collapsed children + // if (boxSizes->collapsed) + // { + // computedBoxSizes->valid = true; + // computedBoxSizes->size = boxSizes->pref; + // validCount++; + // boxSizes->flex = 0; + // }// else { + + if (computedBoxSizes->valid) { + sizeRemaining -= computedBoxSizes->size; + validCount++; + } else { + if (boxSizes->flex == 0) { + computedBoxSizes->valid = true; + computedBoxSizes->size = boxSizes->pref; + validCount++; + } + + spacerConstantsRemaining += boxSizes->flex; + sizeRemaining -= boxSizes->pref; + } + + sizeRemaining -= (boxSizes->left + boxSizes->right); + + //} + + boxSizes = boxSizes->next; + + if (boxSizes && !computedBoxSizes->next) + computedBoxSizes->next = new (aState) nsComputedBoxSize(); + + computedBoxSizes = computedBoxSizes->next; + count++; + } + + // everything accounted for? + if (validCount < count) { + // ----- Ok we are give a size to fit into so stretch or squeeze to fit + // ----- Make sure we look at our min and max size + bool limit = true; + while (limit) { + limit = false; + boxSizes = aBoxSizes; + computedBoxSizes = aComputedBoxSizes; + + while (boxSizes) { + // ignore collapsed spacers + + // if (!boxSizes->collapsed) { + + nscoord pref = 0; + nscoord max = NS_UNCONSTRAINEDSIZE; + nscoord min = 0; + nscoord flex = 0; + + pref = boxSizes->pref; + min = boxSizes->min; + max = boxSizes->max; + flex = boxSizes->flex; + + // ----- look at our min and max limits make sure we aren't too small or + // too big ----- + if (!computedBoxSizes->valid) { + int32_t newSize = pref + int32_t(int64_t(sizeRemaining) * flex / + spacerConstantsRemaining); + + if (newSize <= min) { + computedBoxSizes->size = min; + computedBoxSizes->valid = true; + spacerConstantsRemaining -= flex; + sizeRemaining += pref; + sizeRemaining -= min; + limit = true; + } else if (newSize >= max) { + computedBoxSizes->size = max; + computedBoxSizes->valid = true; + spacerConstantsRemaining -= flex; + sizeRemaining += pref; + sizeRemaining -= max; + limit = true; + } + } + // } + boxSizes = boxSizes->next; + computedBoxSizes = computedBoxSizes->next; + } + } + } + + // ---- once we have removed and min and max issues just stretch us out in the + // remaining space + // ---- or shrink us. Depends on the size remaining and the spacer constants + aGivenSize = 0; + boxSizes = aBoxSizes; + computedBoxSizes = aComputedBoxSizes; + + while (boxSizes) { + // ignore collapsed spacers + // if (!(boxSizes && boxSizes->collapsed)) { + + nscoord pref = 0; + nscoord flex = 0; + pref = boxSizes->pref; + flex = boxSizes->flex; + + if (!computedBoxSizes->valid) { + computedBoxSizes->size = pref + int32_t(int64_t(sizeRemaining) * flex / + spacerConstantsRemaining); + computedBoxSizes->valid = true; + } + + aGivenSize += (boxSizes->left + boxSizes->right); + aGivenSize += computedBoxSizes->size; + + // } + + boxSizes = boxSizes->next; + computedBoxSizes = computedBoxSizes->next; + } +} + +nsSize nsSprocketLayout::GetXULPrefSize(nsIFrame* aBox, + nsBoxLayoutState& aState) { + nsSize vpref(0, 0); + bool isHorizontal = IsXULHorizontal(aBox); + + // run through all the children and get their min, max, and preferred sizes + // return us the size of the box + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + // ignore collapsed children + if (child->IsXULCollapsed()) { + continue; + } + nsSize pref = child->GetXULPrefSize(aState); + AddXULMargin(child, pref); + AddLargestSize(vpref, pref, isHorizontal); + } + + // now add our border and padding + AddXULBorderAndPadding(aBox, vpref); + + return vpref; +} + +nsSize nsSprocketLayout::GetXULMinSize(nsIFrame* aBox, + nsBoxLayoutState& aState) { + nsSize minSize(0, 0); + bool isHorizontal = IsXULHorizontal(aBox); + + // run through all the children and get their min, max, and preferred sizes + // return us the size of the box + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + + // ignore collapsed children + if (child->IsXULCollapsed()) { + continue; + } + + nsSize min = child->GetXULMinSize(aState); + nsSize pref(0, 0); + + // if the child is not flexible then + // its min size is its pref size. + if (child->GetXULFlex() == 0) { + pref = child->GetXULPrefSize(aState); + if (isHorizontal) + min.width = pref.width; + else + min.height = pref.height; + } + + AddXULMargin(child, min); + AddLargestSize(minSize, min, isHorizontal); + } + + // now add our border and padding + AddXULBorderAndPadding(aBox, minSize); + + return minSize; +} + +nsSize nsSprocketLayout::GetXULMaxSize(nsIFrame* aBox, + nsBoxLayoutState& aState) { + bool isHorizontal = IsXULHorizontal(aBox); + + nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + + // run through all the children and get their min, max, and preferred sizes + // return us the size of the box + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + + // ignore collapsed children + if (child->IsXULCollapsed()) { + continue; + } + // if completely redefined don't even ask our child for its size. + nsSize min = child->GetXULMinSize(aState); + nsSize max = + nsIFrame::XULBoundsCheckMinMax(min, child->GetXULMaxSize(aState)); + + AddXULMargin(child, max); + AddSmallestSize(maxSize, max, isHorizontal); + } + + // now add our border and padding + AddXULBorderAndPadding(aBox, maxSize); + + return maxSize; +} + +nscoord nsSprocketLayout::GetAscent(nsIFrame* aBox, nsBoxLayoutState& aState) { + nscoord vAscent = 0; + + bool isHorizontal = IsXULHorizontal(aBox); + + // run through all the children and get their min, max, and preferred sizes + // return us the size of the box + + for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) { + nsIFrame* child = iter->get(); + + // ignore collapsed children + // if (!child->IsXULCollapsed()) + //{ + // if completely redefined don't even ask our child for its size. + nscoord ascent = child->GetXULBoxAscent(aState); + + nsMargin margin; + child->GetXULMargin(margin); + ascent += margin.top; + + if (isHorizontal) { + if (ascent > vAscent) vAscent = ascent; + } else { + if (vAscent == 0) vAscent = ascent; + } + //} + + child = nsIFrame::GetNextXULBox(child); + } + + nsMargin borderPadding; + aBox->GetXULBorderAndPadding(borderPadding); + + return vAscent + borderPadding.top; +} + +void nsSprocketLayout::SetLargestSize(nsSize& aSize1, const nsSize& aSize2, + bool aIsHorizontal) { + if (aIsHorizontal) { + if (aSize1.height < aSize2.height) aSize1.height = aSize2.height; + } else { + if (aSize1.width < aSize2.width) aSize1.width = aSize2.width; + } +} + +void nsSprocketLayout::SetSmallestSize(nsSize& aSize1, const nsSize& aSize2, + bool aIsHorizontal) { + if (aIsHorizontal) { + if (aSize1.height > aSize2.height) aSize1.height = aSize2.height; + } else { + if (aSize1.width > aSize2.width) aSize1.width = aSize2.width; + } +} + +void nsSprocketLayout::AddLargestSize(nsSize& aSize, const nsSize& aSizeToAdd, + bool aIsHorizontal) { + if (aIsHorizontal) + AddCoord(aSize.width, aSizeToAdd.width); + else + AddCoord(aSize.height, aSizeToAdd.height); + + SetLargestSize(aSize, aSizeToAdd, aIsHorizontal); +} + +void nsSprocketLayout::AddCoord(nscoord& aCoord, nscoord aCoordToAdd) { + if (aCoord != NS_UNCONSTRAINEDSIZE) { + if (aCoordToAdd == NS_UNCONSTRAINEDSIZE) + aCoord = aCoordToAdd; + else + aCoord += aCoordToAdd; + } +} +void nsSprocketLayout::AddSmallestSize(nsSize& aSize, const nsSize& aSizeToAdd, + bool aIsHorizontal) { + if (aIsHorizontal) + AddCoord(aSize.width, aSizeToAdd.width); + else + AddCoord(aSize.height, aSizeToAdd.height); + + SetSmallestSize(aSize, aSizeToAdd, aIsHorizontal); +} + +bool nsSprocketLayout::GetDefaultFlex(int32_t& aFlex) { + aFlex = 0; + return true; +} + +nsComputedBoxSize::nsComputedBoxSize() { + resized = false; + valid = false; + size = 0; + next = nullptr; +} + +nsBoxSize::nsBoxSize() { + pref = 0; + min = 0; + max = NS_UNCONSTRAINEDSIZE; + collapsed = false; + left = 0; + right = 0; + flex = 0; + next = nullptr; + bogus = false; +} + +void* nsBoxSize::operator new(size_t sz, + nsBoxLayoutState& aState) noexcept(true) { + return mozilla::AutoStackArena::Allocate(sz); +} + +void nsBoxSize::operator delete(void* aPtr, size_t sz) {} + +void* nsComputedBoxSize::operator new(size_t sz, + nsBoxLayoutState& aState) noexcept(true) { + return mozilla::AutoStackArena::Allocate(sz); +} + +void nsComputedBoxSize::operator delete(void* aPtr, size_t sz) {} diff --git a/layout/xul/nsSprocketLayout.h b/layout/xul/nsSprocketLayout.h new file mode 100644 index 0000000000..e1e7fe377e --- /dev/null +++ b/layout/xul/nsSprocketLayout.h @@ -0,0 +1,156 @@ +/* -*- 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 nsSprocketLayout_h___ +#define nsSprocketLayout_h___ + +#include "mozilla/Attributes.h" +#include "nsBoxLayout.h" +#include "nsCOMPtr.h" +#include "nsFrameState.h" + +class nsIFrame; +struct nsRect; + +class nsBoxSize { + public: + nsBoxSize(); + + nscoord pref; + nscoord min; + nscoord max; + nscoord flex; + nscoord left; + nscoord right; + bool collapsed; + bool bogus; + + nsBoxSize* next; + + void* operator new(size_t sz, nsBoxLayoutState& aState) noexcept(true); + void operator delete(void* aPtr, size_t sz); +}; + +class nsComputedBoxSize { + public: + nsComputedBoxSize(); + + nscoord size; + bool valid; + bool resized; + nsComputedBoxSize* next; + + void* operator new(size_t sz, nsBoxLayoutState& aState) noexcept(true); + void operator delete(void* aPtr, size_t sz); +}; + +#define GET_WIDTH(size, isHorizontal) (isHorizontal ? size.width : size.height) +#define GET_HEIGHT(size, isHorizontal) (isHorizontal ? size.height : size.width) +#define GET_X(size, isHorizontal) (isHorizontal ? size.x : size.y) +#define GET_Y(size, isHorizontal) (isHorizontal ? size.y : size.x) +#define GET_COORD(aX, aY, isHorizontal) (isHorizontal ? aX : aY) + +#define SET_WIDTH(size, coord, isHorizontal) \ + if (isHorizontal) { \ + (size).width = (coord); \ + } else { \ + (size).height = (coord); \ + } +#define SET_HEIGHT(size, coord, isHorizontal) \ + if (isHorizontal) { \ + (size).height = (coord); \ + } else { \ + (size).width = (coord); \ + } +#define SET_X(size, coord, isHorizontal) \ + if (isHorizontal) { \ + (size).x = (coord); \ + } else { \ + (size).y = (coord); \ + } +#define SET_Y(size, coord, isHorizontal) \ + if (isHorizontal) { \ + (size).y = (coord); \ + } else { \ + (size).x = (coord); \ + } + +#define SET_COORD(aX, aY, coord, isHorizontal) \ + if (isHorizontal) { \ + aX = (coord); \ + } else { \ + aY = (coord); \ + } + +nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout); + +class nsSprocketLayout : public nsBoxLayout { + public: + friend nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout); + static void Shutdown(); + + NS_IMETHOD XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState) override; + + virtual nsSize GetXULPrefSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMinSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMaxSize(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) override; + virtual nscoord GetAscent(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState) override; + + nsSprocketLayout(); + + static bool IsXULHorizontal(nsIFrame* aBox); + + static void SetLargestSize(nsSize& aSize1, const nsSize& aSize2, + bool aIsHorizontal); + static void SetSmallestSize(nsSize& aSize1, const nsSize& aSize2, + bool aIsHorizontal); + + static void AddLargestSize(nsSize& aSize, const nsSize& aSizeToAdd, + bool aIsHorizontal); + static void AddSmallestSize(nsSize& aSize, const nsSize& aSizeToAdd, + bool aIsHorizontal); + static void AddCoord(nscoord& aCoord, nscoord aCoordToAdd); + + protected: + void ComputeChildsNextPosition(nsIFrame* aBox, const nscoord& aCurX, + const nscoord& aCurY, nscoord& aNextX, + nscoord& aNextY, const nsRect& aChildSize); + + void ChildResized(nsIFrame* aBox, nsBoxLayoutState& aState, nsIFrame* aChild, + nsBoxSize* aChildBoxSize, + nsComputedBoxSize* aChildComputedBoxSize, + nsBoxSize* aBoxSizes, nsComputedBoxSize* aComputedBoxSizes, + const nsRect& aChildLayoutRect, nsRect& aChildActualRect, + nsRect& aContainingRect, int32_t aFlexes, bool& aFinished); + + void AlignChildren(nsIFrame* aBox, nsBoxLayoutState& aState); + + virtual void ComputeChildSizes(nsIFrame* aBox, nsBoxLayoutState& aState, + nscoord& aGivenSize, nsBoxSize* aBoxSizes, + nsComputedBoxSize*& aComputedBoxSizes); + + virtual void PopulateBoxSizes(nsIFrame* aBox, + nsBoxLayoutState& aBoxLayoutState, + nsBoxSize*& aBoxSizes, nscoord& aMinSize, + nscoord& aMaxSize, int32_t& aFlexes); + + virtual void InvalidateComputedSizes(nsComputedBoxSize* aComputedBoxSizes); + + virtual bool GetDefaultFlex(int32_t& aFlex); + + virtual void GetFrameState(nsIFrame* aBox, nsFrameState& aState); + + private: + // because the sprocket layout manager has no instance variables. We + // can make a static one and reuse it everywhere. + static nsBoxLayout* gInstance; +}; + +#endif diff --git a/layout/xul/nsTextBoxFrame.cpp b/layout/xul/nsTextBoxFrame.cpp new file mode 100644 index 0000000000..d31b8418da --- /dev/null +++ b/layout/xul/nsTextBoxFrame.cpp @@ -0,0 +1,1055 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cindent: */ +/* 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 "nsTextBoxFrame.h" + +#include "gfx2DGlue.h" +#include "gfxUtils.h" +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/Attributes.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/layers/RenderRootStateManager.h" +#include "mozilla/gfx/2D.h" +#include "nsFontMetrics.h" +#include "nsReadableUtils.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsGkAtoms.h" +#include "nsPresContext.h" +#include "gfxContext.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsBoxLayoutState.h" +#include "nsMenuBarListener.h" +#include "nsString.h" +#include "nsITheme.h" +#include "nsUnicharUtils.h" +#include "nsContentUtils.h" +#include "nsDisplayList.h" +#include "nsCSSRendering.h" +#include "nsIReflowCallback.h" +#include "nsBoxFrame.h" +#include "nsLayoutUtils.h" +#include "TextDrawTarget.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif + +#include "nsBidiUtils.h" +#include "nsBidiPresUtils.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +class nsAccessKeyInfo { + public: + int32_t mAccesskeyIndex; + nscoord mBeforeWidth, mAccessWidth, mAccessUnderlineSize, mAccessOffset; +}; + +bool nsTextBoxFrame::gAlwaysAppendAccessKey = false; +bool nsTextBoxFrame::gAccessKeyPrefInitialized = false; +bool nsTextBoxFrame::gInsertSeparatorBeforeAccessKey = false; +bool nsTextBoxFrame::gInsertSeparatorPrefInitialized = false; + +nsIFrame* NS_NewTextBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsTextBoxFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTextBoxFrame) + +NS_QUERYFRAME_HEAD(nsTextBoxFrame) + NS_QUERYFRAME_ENTRY(nsTextBoxFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame) + +nsresult nsTextBoxFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + bool aResize; + bool aRedraw; + + UpdateAttributes(aAttribute, aResize, aRedraw); + + if (aResize) { + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } else if (aRedraw) { + nsBoxLayoutState state(PresContext()); + XULRedraw(state); + } + + return NS_OK; +} + +nsTextBoxFrame::nsTextBoxFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsLeafBoxFrame(aStyle, aPresContext, kClassID), + mAccessKeyInfo(nullptr), + mCropType(CropRight), + mAscent(0), + mNeedsReflowCallback(false) { + MarkIntrinsicISizesDirty(); +} + +nsTextBoxFrame::~nsTextBoxFrame() { delete mAccessKeyInfo; } + +void nsTextBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow); + + bool aResize; + bool aRedraw; + UpdateAttributes(nullptr, aResize, aRedraw); /* update all */ +} + +bool nsTextBoxFrame::AlwaysAppendAccessKey() { + if (!gAccessKeyPrefInitialized) { + gAccessKeyPrefInitialized = true; + + const char* prefName = "intl.menuitems.alwaysappendaccesskeys"; + nsAutoString val; + Preferences::GetLocalizedString(prefName, val); + gAlwaysAppendAccessKey = val.EqualsLiteral("true"); + } + return gAlwaysAppendAccessKey; +} + +bool nsTextBoxFrame::InsertSeparatorBeforeAccessKey() { + if (!gInsertSeparatorPrefInitialized) { + gInsertSeparatorPrefInitialized = true; + + const char* prefName = "intl.menuitems.insertseparatorbeforeaccesskeys"; + nsAutoString val; + Preferences::GetLocalizedString(prefName, val); + gInsertSeparatorBeforeAccessKey = val.EqualsLiteral("true"); + } + return gInsertSeparatorBeforeAccessKey; +} + +class nsAsyncAccesskeyUpdate final : public nsIReflowCallback { + public: + explicit nsAsyncAccesskeyUpdate(nsIFrame* aFrame) : mWeakFrame(aFrame) {} + + virtual bool ReflowFinished() override { + bool shouldFlush = false; + nsTextBoxFrame* frame = static_cast<nsTextBoxFrame*>(mWeakFrame.GetFrame()); + if (frame) { + shouldFlush = frame->UpdateAccesskey(mWeakFrame); + } + delete this; + return shouldFlush; + } + + virtual void ReflowCallbackCanceled() override { delete this; } + + WeakFrame mWeakFrame; +}; + +bool nsTextBoxFrame::UpdateAccesskey(WeakFrame& aWeakThis) { + nsAutoString accesskey; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, + accesskey); + + if (!accesskey.Equals(mAccessKey)) { + // Need to get clean mTitle. + RecomputeTitle(); + mAccessKey = accesskey; + UpdateAccessTitle(); + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + return true; + } + return false; +} + +void nsTextBoxFrame::UpdateAttributes(nsAtom* aAttribute, bool& aResize, + bool& aRedraw) { + bool doUpdateTitle = false; + aResize = false; + aRedraw = false; + + if (aAttribute == nullptr || aAttribute == nsGkAtoms::crop) { + static dom::Element::AttrValuesArray strings[] = { + nsGkAtoms::left, nsGkAtoms::start, nsGkAtoms::center, + nsGkAtoms::right, nsGkAtoms::end, nsGkAtoms::none, + nullptr}; + CroppingStyle cropType; + switch (mContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::crop, strings, eCaseMatters)) { + case 0: + case 1: + cropType = CropLeft; + break; + case 2: + cropType = CropCenter; + break; + case 3: + case 4: + cropType = CropRight; + break; + case 5: + cropType = CropNone; + break; + default: + cropType = CropAuto; + break; + } + + if (cropType != mCropType) { + aResize = true; + mCropType = cropType; + } + } + + if (aAttribute == nullptr || aAttribute == nsGkAtoms::value) { + RecomputeTitle(); + doUpdateTitle = true; + } + + if (aAttribute == nullptr || aAttribute == nsGkAtoms::accesskey) { + mNeedsReflowCallback = true; + // Ensure that layout is refreshed and reflow callback called. + aResize = true; + } + + if (doUpdateTitle) { + UpdateAccessTitle(); + aResize = true; + } +} + +namespace mozilla { + +class nsDisplayXULTextBox final : public nsPaintedDisplayItem { + public: + nsDisplayXULTextBox(nsDisplayListBuilder* aBuilder, nsTextBoxFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayXULTextBox); + } +#ifdef NS_BUILD_REFCNT_LOGGING + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULTextBox) +#endif + + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + NS_DISPLAY_DECL_NAME("XULTextBox", TYPE_XUL_TEXT_BOX) + + virtual nsRect GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const override; + + void PaintTextToContext(gfxContext* aCtx, nsPoint aOffset, + const nscolor* aColor); + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; +}; + +static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset, + const nscolor& aShadowColor, void* aData) { + reinterpret_cast<nsDisplayXULTextBox*>(aData)->PaintTextToContext( + aCtx, aShadowOffset, &aShadowColor); +} + +void nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + // Paint the text shadow before doing any foreground stuff + nsRect drawRect = + static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect + ToReferenceFrame(); + nsLayoutUtils::PaintTextShadow(mFrame, aCtx, drawRect, + GetPaintRect(aBuilder, aCtx), + mFrame->StyleText()->mColor.ToColor(), + PaintTextShadowCallback, (void*)this); + + PaintTextToContext(aCtx, nsPoint(0, 0), nullptr); +} + +void nsDisplayXULTextBox::PaintTextToContext(gfxContext* aCtx, nsPoint aOffset, + const nscolor* aColor) { + static_cast<nsTextBoxFrame*>(mFrame)->PaintTitle( + *aCtx, mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(), + ToReferenceFrame() + aOffset, aColor); +} + +bool nsDisplayXULTextBox::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + bool snap = false; + auto bounds = GetBounds(aDisplayListBuilder, &snap); + + if (bounds.IsEmpty()) { + return true; + } + + auto appUnitsPerDevPixel = Frame()->PresContext()->AppUnitsPerDevPixel(); + gfx::Point deviceOffset = + LayoutDevicePoint::FromAppUnits(bounds.TopLeft(), appUnitsPerDevPixel) + .ToUnknownPoint(); + + RefPtr<mozilla::layout::TextDrawTarget> textDrawer = + new mozilla::layout::TextDrawTarget(aBuilder, aResources, aSc, aManager, + this, bounds); + RefPtr<gfxContext> captureCtx = + gfxContext::CreateOrNull(textDrawer, deviceOffset); + + Paint(aDisplayListBuilder, captureCtx); + textDrawer->TerminateShadows(); + + return textDrawer->Finish(); +} + +nsRect nsDisplayXULTextBox::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +nsRect nsDisplayXULTextBox::GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const { + return static_cast<nsTextBoxFrame*>(mFrame)->GetComponentAlphaBounds() + + ToReferenceFrame(); +} + +} // namespace mozilla + +void nsTextBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!IsVisibleForPainting()) return; + + nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists); + + aLists.Content()->AppendNewToTop<nsDisplayXULTextBox>(aBuilder, this); +} + +void nsTextBoxFrame::PaintTitle(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + const nscolor* aOverrideColor) { + if (mTitle.IsEmpty()) return; + + DrawText(aRenderingContext, aDirtyRect, mTextDrawRect + aPt, aOverrideColor); +} + +void nsTextBoxFrame::DrawText(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, const nsRect& aTextRect, + const nscolor* aOverrideColor) { + nsPresContext* presContext = PresContext(); + int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // paint the title + nscolor overColor = 0; + nscolor underColor = 0; + nscolor strikeColor = 0; + auto overStyle = StyleTextDecorationStyle::None; + auto underStyle = StyleTextDecorationStyle::None; + auto strikeStyle = StyleTextDecorationStyle::None; + + // Begin with no decorations + auto decorations = StyleTextDecorationLine::NONE; + // A mask of all possible line decorations. + auto decorMask = StyleTextDecorationLine::UNDERLINE | + StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::LINE_THROUGH; + + WritingMode wm = GetWritingMode(); + bool vertical = wm.IsVertical(); + + nsIFrame* f = this; + do { // find decoration colors + ComputedStyle* context = f->Style(); + if (!context->HasTextDecorationLines()) { + break; + } + const nsStyleTextReset* styleText = context->StyleTextReset(); + + // a decoration defined here + if (decorMask & styleText->mTextDecorationLine) { + nscolor color; + if (aOverrideColor) { + color = *aOverrideColor; + } else { + color = styleText->mTextDecorationColor.CalcColor(*context); + } + const auto style = styleText->mTextDecorationStyle; + + if (StyleTextDecorationLine::UNDERLINE & decorMask & + styleText->mTextDecorationLine) { + underColor = color; + underStyle = style; + decorMask &= ~StyleTextDecorationLine::UNDERLINE; + decorations |= StyleTextDecorationLine::UNDERLINE; + } + if (StyleTextDecorationLine::OVERLINE & decorMask & + styleText->mTextDecorationLine) { + overColor = color; + overStyle = style; + decorMask &= ~StyleTextDecorationLine::OVERLINE; + decorations |= StyleTextDecorationLine::OVERLINE; + } + if (StyleTextDecorationLine::LINE_THROUGH & decorMask & + styleText->mTextDecorationLine) { + strikeColor = color; + strikeStyle = style; + decorMask &= ~StyleTextDecorationLine::LINE_THROUGH; + decorations |= StyleTextDecorationLine::LINE_THROUGH; + } + } + } while (decorMask && (f = nsLayoutUtils::GetParentOrPlaceholderFor(f))); + + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + fontMet->SetVertical(wm.IsVertical()); + fontMet->SetTextOrientation(StyleVisibility()->mTextOrientation); + + nscoord offset; + nscoord size; + nscoord ascent = fontMet->MaxAscent(); + + nsPoint baselinePt; + if (wm.IsVertical()) { + baselinePt.x = presContext->RoundAppUnitsToNearestDevPixels( + aTextRect.x + (wm.IsVerticalRL() ? aTextRect.width - ascent : ascent)); + baselinePt.y = aTextRect.y; + } else { + baselinePt.x = aTextRect.x; + baselinePt.y = + presContext->RoundAppUnitsToNearestDevPixels(aTextRect.y + ascent); + } + + nsCSSRendering::PaintDecorationLineParams params; + params.dirtyRect = ToRect(presContext->AppUnitsToGfxUnits(aDirtyRect)); + params.pt = Point(presContext->AppUnitsToGfxUnits(aTextRect.x), + presContext->AppUnitsToGfxUnits(aTextRect.y)); + params.icoordInFrame = + Float(PresContext()->AppUnitsToGfxUnits(mTextDrawRect.x)); + params.lineSize = Size(presContext->AppUnitsToGfxUnits(aTextRect.width), 0); + params.ascent = presContext->AppUnitsToGfxUnits(ascent); + params.vertical = vertical; + + // XXX todo: vertical-mode support for decorations not tested yet, + // probably won't be positioned correctly + + // Underlines are drawn before overlines, and both before the text + // itself, per http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1. + // (We don't apply this rule to the access-key underline because we only + // find out where that is as a side effect of drawing the text, in the + // general case -- see below.) + if (decorations & (StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::UNDERLINE)) { + fontMet->GetUnderline(offset, size); + params.lineSize.height = presContext->AppUnitsToGfxUnits(size); + if ((decorations & StyleTextDecorationLine::UNDERLINE) && + underStyle != StyleTextDecorationStyle::None) { + params.color = underColor; + params.offset = presContext->AppUnitsToGfxUnits(offset); + params.decoration = StyleTextDecorationLine::UNDERLINE; + params.style = underStyle; + nsCSSRendering::PaintDecorationLine(this, *drawTarget, params); + } + if ((decorations & StyleTextDecorationLine::OVERLINE) && + overStyle != StyleTextDecorationStyle::None) { + params.color = overColor; + params.offset = params.ascent; + params.decoration = StyleTextDecorationLine::OVERLINE; + params.style = overStyle; + nsCSSRendering::PaintDecorationLine(this, *drawTarget, params); + } + } + + RefPtr<gfxContext> refContext = + PresShell()->CreateReferenceRenderingContext(); + DrawTarget* refDrawTarget = refContext->GetDrawTarget(); + + CalculateUnderline(refDrawTarget, *fontMet); + + DeviceColor color = ToDeviceColor( + aOverrideColor ? *aOverrideColor : StyleText()->mColor.ToColor()); + ColorPattern colorPattern(color); + aRenderingContext.SetDeviceColor(color); + + nsresult rv = NS_ERROR_FAILURE; + + if (mState & NS_FRAME_IS_BIDI) { + presContext->SetBidiEnabled(); + mozilla::intl::BidiEmbeddingLevel level = + nsBidiPresUtils::BidiLevelFromStyle(Style()); + if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) { + // We let the RenderText function calculate the mnemonic's + // underline position for us. + nsBidiPositionResolve posResolve; + posResolve.logicalIndex = mAccessKeyInfo->mAccesskeyIndex; + rv = nsBidiPresUtils::RenderText( + mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext, + aRenderingContext, refDrawTarget, *fontMet, baselinePt.x, + baselinePt.y, &posResolve, 1); + mAccessKeyInfo->mBeforeWidth = posResolve.visualLeftTwips; + mAccessKeyInfo->mAccessWidth = posResolve.visualWidth; + } else { + rv = nsBidiPresUtils::RenderText( + mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext, + aRenderingContext, refDrawTarget, *fontMet, baselinePt.x, + baselinePt.y); + } + } + if (NS_FAILED(rv)) { + fontMet->SetTextRunRTL(false); + + if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) { + // In the simple (non-BiDi) case, we calculate the mnemonic's + // underline position by getting the text metric. + // XXX are attribute values always two byte? + if (mAccessKeyInfo->mAccesskeyIndex > 0) + mAccessKeyInfo->mBeforeWidth = nsLayoutUtils::AppUnitWidthOfString( + mCroppedTitle.get(), mAccessKeyInfo->mAccesskeyIndex, *fontMet, + refDrawTarget); + else + mAccessKeyInfo->mBeforeWidth = 0; + } + + fontMet->DrawString(mCroppedTitle.get(), mCroppedTitle.Length(), + baselinePt.x, baselinePt.y, &aRenderingContext, + refDrawTarget); + } + + if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) { + nsRect r(aTextRect.x + mAccessKeyInfo->mBeforeWidth, + aTextRect.y + mAccessKeyInfo->mAccessOffset, + mAccessKeyInfo->mAccessWidth, + mAccessKeyInfo->mAccessUnderlineSize); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, colorPattern); + } + + // Strikeout is drawn on top of the text, per + // http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1. + if ((decorations & StyleTextDecorationLine::LINE_THROUGH) && + strikeStyle != StyleTextDecorationStyle::None) { + fontMet->GetStrikeout(offset, size); + params.color = strikeColor; + params.lineSize.height = presContext->AppUnitsToGfxUnits(size); + params.offset = presContext->AppUnitsToGfxUnits(offset); + params.decoration = StyleTextDecorationLine::LINE_THROUGH; + params.style = strikeStyle; + nsCSSRendering::PaintDecorationLine(this, *drawTarget, params); + } +} + +void nsTextBoxFrame::CalculateUnderline(DrawTarget* aDrawTarget, + nsFontMetrics& aFontMetrics) { + if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) { + // Calculate all fields of mAccessKeyInfo which + // are the same for both BiDi and non-BiDi frames. + const char16_t* titleString = mCroppedTitle.get(); + aFontMetrics.SetTextRunRTL(false); + mAccessKeyInfo->mAccessWidth = nsLayoutUtils::AppUnitWidthOfString( + titleString[mAccessKeyInfo->mAccesskeyIndex], aFontMetrics, + aDrawTarget); + + nscoord offset, baseline; + aFontMetrics.GetUnderline(offset, mAccessKeyInfo->mAccessUnderlineSize); + baseline = aFontMetrics.MaxAscent(); + mAccessKeyInfo->mAccessOffset = baseline - offset; + } +} + +void nsTextBoxFrame::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; + } +} + +nscoord nsTextBoxFrame::CalculateTitleForWidth(gfxContext& aRenderingContext, + nscoord aMaxWidth) { + if (mTitle.IsEmpty()) { + mCroppedTitle.Truncate(); + return 0; + } + + RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + // See if the text needs to be cropped to fit in the width given. + mCroppedTitle = mTitle; + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi( + mCroppedTitle, this, *fm, aRenderingContext); + if (width > aMaxWidth && mCropType != CropNone) { + CropStringForWidth(mCroppedTitle, aRenderingContext, *fm, aMaxWidth, + mCropType); + width = nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm, + aRenderingContext); + } + + if (StyleVisibility()->mDirection == StyleDirection::Rtl || + HasRTLChars(mCroppedTitle)) { + AddStateBits(NS_FRAME_IS_BIDI); + } + + return width; +} + +#define OLD_ELLIPSIS u"..."_ns + +// the following block is to append the accesskey to mTitle if there is an +// accesskey but the mTitle doesn't have the character +void nsTextBoxFrame::UpdateAccessTitle() { + /* + * Note that if you change appending access key label spec, + * you need to maintain same logic in following methods. See bug 324159. + * toolkit/components/prompts/src/CommonDialog.jsm (setLabelForNode) + * toolkit/content/widgets/text.js (formatAccessKey) + */ + int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey(); + if (!menuAccessKey || mAccessKey.IsEmpty()) return; + + if (!AlwaysAppendAccessKey() && + FindInReadable(mAccessKey, mTitle, nsCaseInsensitiveStringComparator)) + return; + + nsAutoString accessKeyLabel; + accessKeyLabel += '('; + accessKeyLabel += mAccessKey; + ToUpperCase(accessKeyLabel); + accessKeyLabel += ')'; + + if (mTitle.IsEmpty()) { + mTitle = accessKeyLabel; + return; + } + + if (StringEndsWith(mTitle, accessKeyLabel)) { + // Never append another "(X)" if the title already ends with "(X)". + return; + } + + const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); + uint32_t offset = mTitle.Length(); + if (StringEndsWith(mTitle, kEllipsis)) { + offset -= kEllipsis.Length(); + } else if (StringEndsWith(mTitle, OLD_ELLIPSIS)) { + // Try to check with our old ellipsis (for old addons) + offset -= OLD_ELLIPSIS.Length(); + } else { + // Try to check with + // our default ellipsis (for non-localized addons) or ':' + const char16_t kLastChar = mTitle.Last(); + if (kLastChar == char16_t(0x2026) || kLastChar == char16_t(':')) offset--; + } + + if (InsertSeparatorBeforeAccessKey() && offset > 0 && + !NS_IS_SPACE(mTitle[offset - 1])) { + mTitle.Insert(' ', offset); + offset++; + } + + mTitle.Insert(accessKeyLabel, offset); +} + +void nsTextBoxFrame::UpdateAccessIndex() { + int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey(); + if (menuAccessKey) { + if (mAccessKey.IsEmpty()) { + if (mAccessKeyInfo) { + delete mAccessKeyInfo; + mAccessKeyInfo = nullptr; + } + } else { + if (!mAccessKeyInfo) { + mAccessKeyInfo = new nsAccessKeyInfo(); + if (!mAccessKeyInfo) return; + } + + nsAString::const_iterator start, end; + + mCroppedTitle.BeginReading(start); + mCroppedTitle.EndReading(end); + + // remember the beginning of the string + nsAString::const_iterator originalStart = start; + + bool found; + if (!AlwaysAppendAccessKey()) { + // not appending access key - do case-sensitive search + // first + found = FindInReadable(mAccessKey, start, end); + if (!found) { + // didn't find it - perform a case-insensitive search + start = originalStart; + found = FindInReadable(mAccessKey, start, end, + nsCaseInsensitiveStringComparator); + } + } else { + found = RFindInReadable(mAccessKey, start, end, + nsCaseInsensitiveStringComparator); + } + + if (found) + mAccessKeyInfo->mAccesskeyIndex = Distance(originalStart, start); + else + mAccessKeyInfo->mAccesskeyIndex = kNotFound; + } + } +} + +void nsTextBoxFrame::RecomputeTitle() { + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, mTitle); + + // This doesn't handle language-specific uppercasing/lowercasing + // rules, unlike textruns. + StyleTextTransform textTransform = StyleText()->mTextTransform; + if (textTransform.case_ == StyleTextTransformCase::Uppercase) { + ToUpperCase(mTitle); + } else if (textTransform.case_ == StyleTextTransformCase::Lowercase) { + ToLowerCase(mTitle); + } + // We can't handle StyleTextTransformCase::Capitalize because we + // have no clue about word boundaries here. We also don't handle + // the full-width or full-size-kana transforms. +} + +void nsTextBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + nsLeafBoxFrame::DidSetComputedStyle(aOldComputedStyle); + + if (!aOldComputedStyle) { + // We're just being initialized + return; + } + + const nsStyleText* oldTextStyle = aOldComputedStyle->StyleText(); + if (oldTextStyle->mTextTransform != StyleText()->mTextTransform) { + RecomputeTitle(); + UpdateAccessTitle(); + } +} + +NS_IMETHODIMP +nsTextBoxFrame::DoXULLayout(nsBoxLayoutState& aBoxLayoutState) { + if (mNeedsReflowCallback) { + nsIReflowCallback* cb = new nsAsyncAccesskeyUpdate(this); + if (cb) { + PresShell()->PostReflowCallback(cb); + } + mNeedsReflowCallback = false; + } + + nsresult rv = nsLeafBoxFrame::DoXULLayout(aBoxLayoutState); + + CalcDrawRect(*aBoxLayoutState.GetRenderingContext()); + + const nsStyleText* textStyle = StyleText(); + + nsRect scrollBounds(nsPoint(0, 0), GetSize()); + nsRect textRect = mTextDrawRect; + + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + nsBoundingMetrics metrics = fontMet->GetInkBoundsForInkOverflow( + mCroppedTitle.get(), mCroppedTitle.Length(), + aBoxLayoutState.GetRenderingContext()->GetDrawTarget()); + + WritingMode wm = GetWritingMode(); + LogicalRect tr(wm, textRect, GetSize()); + + tr.IStart(wm) -= metrics.leftBearing; + tr.ISize(wm) = metrics.width; + // In DrawText() we always draw with the baseline at MaxAscent() (relative to + // mTextDrawRect), + tr.BStart(wm) += fontMet->MaxAscent() - metrics.ascent; + tr.BSize(wm) = metrics.ascent + metrics.descent; + + textRect = tr.GetPhysicalRect(wm, GetSize()); + + // Our scrollable overflow is our bounds; our ink overflow may + // extend beyond that. + nsRect visualBounds; + visualBounds.UnionRect(scrollBounds, textRect); + OverflowAreas overflow(visualBounds, scrollBounds); + + if (textStyle->HasTextShadow()) { + // text-shadow extends our visual but not scrollable bounds + nsRect& vis = overflow.InkOverflow(); + vis.UnionRect(vis, + nsLayoutUtils::GetTextShadowRectsUnion(mTextDrawRect, this)); + } + FinishAndStoreOverflow(overflow, GetSize()); + + return rv; +} + +nsRect nsTextBoxFrame::GetComponentAlphaBounds() const { + if (StyleText()->HasTextShadow()) { + return InkOverflowRectRelativeToSelf(); + } + return mTextDrawRect; +} + +bool nsTextBoxFrame::XULComputesOwnOverflowArea() { return true; } + +/* virtual */ +void nsTextBoxFrame::MarkIntrinsicISizesDirty() { + mNeedsRecalc = true; + nsLeafBoxFrame::MarkIntrinsicISizesDirty(); +} + +void nsTextBoxFrame::GetTextSize(gfxContext& aRenderingContext, + const nsString& aString, nsSize& aSize, + nscoord& aAscent) { + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + aSize.height = fontMet->MaxHeight(); + aSize.width = nsLayoutUtils::AppUnitWidthOfStringBidi(aString, this, *fontMet, + aRenderingContext); + aAscent = fontMet->MaxAscent(); +} + +void nsTextBoxFrame::CalcTextSize(nsBoxLayoutState& aBoxLayoutState) { + if (mNeedsRecalc) { + nsSize size; + gfxContext* rendContext = aBoxLayoutState.GetRenderingContext(); + if (rendContext) { + GetTextSize(*rendContext, mTitle, size, mAscent); + if (GetWritingMode().IsVertical()) { + std::swap(size.width, size.height); + } + mTextSize = size; + mNeedsRecalc = false; + } + } +} + +void nsTextBoxFrame::CalcDrawRect(gfxContext& aRenderingContext) { + WritingMode wm = GetWritingMode(); + + LogicalRect textRect(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm)); + nsMargin borderPadding; + GetXULBorderAndPadding(borderPadding); + textRect.Deflate(wm, LogicalMargin(wm, borderPadding)); + + // determine (cropped) title and underline position + // determine (cropped) title which fits in aRect, and its width + // (where "width" is the text measure along its baseline, i.e. actually + // a physical height in vertical writing modes) + nscoord titleWidth = + CalculateTitleForWidth(aRenderingContext, textRect.ISize(wm)); + +#ifdef ACCESSIBILITY + // Make sure to update the accessible tree in case when cropped title is + // changed. + nsAccessibilityService* accService = GetAccService(); + if (accService) { + accService->UpdateLabelValue(PresShell(), mContent, mCroppedTitle); + } +#endif + + // determine if and at which position to put the underline + UpdateAccessIndex(); + + // make the rect as small as our (cropped) text. + nscoord outerISize = textRect.ISize(wm); + textRect.ISize(wm) = titleWidth; + + // Align our text within the overall rect by checking our text-align property. + const nsStyleText* textStyle = StyleText(); + if (textStyle->mTextAlign == StyleTextAlign::Center) { + textRect.IStart(wm) += (outerISize - textRect.ISize(wm)) / 2; + } else if (textStyle->mTextAlign == StyleTextAlign::End || + (textStyle->mTextAlign == StyleTextAlign::Left && + wm.IsBidiRTL()) || + (textStyle->mTextAlign == StyleTextAlign::Right && + wm.IsBidiLTR())) { + textRect.IStart(wm) += (outerISize - textRect.ISize(wm)); + } + + mTextDrawRect = textRect.GetPhysicalRect(wm, GetSize()); +} + +/** + * Ok return our dimensions + */ +nsSize nsTextBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) { + CalcTextSize(aBoxLayoutState); + + nsSize size = mTextSize; + DISPLAY_PREF_SIZE(this, size); + + AddXULBorderAndPadding(size); + bool widthSet, heightSet; + nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet); + + return size; +} + +/** + * Ok return our dimensions + */ +nsSize nsTextBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) { + CalcTextSize(aBoxLayoutState); + + nsSize size = mTextSize; + DISPLAY_MIN_SIZE(this, size); + + // if there is cropping our min width becomes our border and padding + if (mCropType != CropNone && mCropType != CropAuto) { + if (GetWritingMode().IsVertical()) { + size.height = 0; + } else { + size.width = 0; + } + } + + AddXULBorderAndPadding(size); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(this, size, widthSet, heightSet); + + return size; +} + +nscoord nsTextBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) { + CalcTextSize(aBoxLayoutState); + + nscoord ascent = mAscent; + + nsMargin m(0, 0, 0, 0); + GetXULBorderAndPadding(m); + + WritingMode wm = GetWritingMode(); + ascent += LogicalMargin(wm, m).BStart(wm); + + return ascent; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsTextBoxFrame::GetFrameName(nsAString& aResult) const { + MakeFrameName(u"TextBox"_ns, aResult); + aResult += u"[value="_ns + mTitle + u"]"_ns; + return NS_OK; +} +#endif diff --git a/layout/xul/nsTextBoxFrame.h b/layout/xul/nsTextBoxFrame.h new file mode 100644 index 0000000000..14e45bc9bd --- /dev/null +++ b/layout/xul/nsTextBoxFrame.h @@ -0,0 +1,128 @@ +/* -*- 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 nsTextBoxFrame_h___ +#define nsTextBoxFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsLeafBoxFrame.h" + +class nsAccessKeyInfo; +class nsAsyncAccesskeyUpdate; +class nsFontMetrics; + +namespace mozilla { +class nsDisplayXULTextBox; +class PresShell; +} // namespace mozilla + +class nsTextBoxFrame final : public nsLeafBoxFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsTextBoxFrame) + + virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override; + NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override; + virtual void MarkIntrinsicISizesDirty() override; + + enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto }; + + friend nsIFrame* NS_NewTextBoxFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* asPrevInFlow) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + void UpdateAttributes(nsAtom* aAttribute, bool& aResize, bool& aRedraw); + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual ~nsTextBoxFrame(); + + void PaintTitle(gfxContext& aRenderingContext, const nsRect& aDirtyRect, + nsPoint aPt, const nscolor* aOverrideColor); + + nsRect GetComponentAlphaBounds() const; + + virtual bool XULComputesOwnOverflowArea() override; + + void GetCroppedTitle(nsString& aTitle) const { aTitle = mCroppedTitle; } + + virtual void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + // Helper for GetCroppedTitle, factored out so that we can also use it from + // nsTreeBodyFrame::AdjustForCellText. + static void CropStringForWidth(nsAString& aText, + gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, nscoord aWidth, + CroppingStyle aCropType); + + static bool AlwaysAppendAccessKey(); + static bool InsertSeparatorBeforeAccessKey(); + + protected: + friend class nsAsyncAccesskeyUpdate; + friend class mozilla::nsDisplayXULTextBox; + // Should be called only by nsAsyncAccesskeyUpdate. + // Returns true if accesskey was updated. + bool UpdateAccesskey(WeakFrame& aWeakThis); + void UpdateAccessTitle(); + void UpdateAccessIndex(); + + // Recompute our title, ignoring the access key but taking into + // account text-transform. + void RecomputeTitle(); + + // REVIEW: SORRY! Couldn't resist devirtualizing these + void LayoutTitle(nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aRect); + + void CalculateUnderline(DrawTarget* aDrawTarget, nsFontMetrics& aFontMetrics); + + void CalcTextSize(nsBoxLayoutState& aBoxLayoutState); + + void CalcDrawRect(gfxContext& aRenderingContext); + + explicit nsTextBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + nscoord CalculateTitleForWidth(gfxContext& aRenderingContext, nscoord aWidth); + + void GetTextSize(gfxContext& aRenderingContext, const nsString& aString, + nsSize& aSize, nscoord& aAscent); + + private: + void DrawText(gfxContext& aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aTextRect, const nscolor* aOverrideColor); + + nsString mTitle; + nsString mCroppedTitle; + nsString mAccessKey; + nsSize mTextSize; + nsRect mTextDrawRect; + nsAccessKeyInfo* mAccessKeyInfo; + + CroppingStyle mCropType; + nscoord mAscent; + bool mNeedsRecalc; + bool mNeedsReflowCallback; + + static bool gAlwaysAppendAccessKey; + static bool gAccessKeyPrefInitialized; + static bool gInsertSeparatorBeforeAccessKey; + static bool gInsertSeparatorPrefInitialized; + +}; // class nsTextBoxFrame + +#endif /* nsTextBoxFrame_h___ */ diff --git a/layout/xul/nsXULPopupManager.cpp b/layout/xul/nsXULPopupManager.cpp new file mode 100644 index 0000000000..27e4e18134 --- /dev/null +++ b/layout/xul/nsXULPopupManager.cpp @@ -0,0 +1,2800 @@ +/* -*- 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 "XULMenuParentElement.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 "nsMenuBarFrame.h" +#include "nsMenuBarListener.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/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"); + +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(nsIContent* 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; +} + +nsIContent* nsMenuChainItem::Content() { return mFrame->GetContent(); } + +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::RollupNativeMenu() { + if (mNativeMenu) { + RefPtr<NativeMenu> menu = mNativeMenu; + return menu->Close(); + } + return false; +} + +bool nsXULPopupManager::Rollup(uint32_t aCount, bool aFlush, + const LayoutDeviceIntPoint* pos, + 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; + } + + bool consume = false; + + if (nsMenuChainItem* item = GetTopVisibleMenu()) { + 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()->PopupType() != parent->Frame()->PopupType() || + first->IsContextMenu() != parent->IsContextMenu()) { + break; + } + first = parent; + } + + *aLastRolledUp = first->Content(); + } + + ConsumeOutsideClicksResult consumeResult = + item->Frame()->ConsumeOutsideClicks(); + 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 && pos && + 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) && + pos) { + nsMenuPopupFrame* popupFrame = item->Frame(); + CSSIntRect anchorRect = [&] { + if (popupFrame->IsAnchored()) { + // Check if the popup has a screen anchor rectangle. If not, get the + // rectangle from the anchor element. + auto r = popupFrame->GetScreenAnchorRect(); + if (r.x != -1 && r.y != -1) { + 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(*pos); + if (anchorRect.Contains(posCSSPixels)) { + if (consumeResult == ConsumeOutsideClicks_ParentOnly) { + consume = true; + } + + if (noRollupOnAnchor) { + rollup = false; + } + } + } + + if (rollup) { + // if a number of popups to close has been specified, determine the last + // popup to close + nsIContent* lastPopup = nullptr; + if (aCount != UINT32_MAX) { + nsMenuChainItem* last = item; + while (--aCount && last->GetParent()) { + last = last->GetParent(); + } + if (last) { + lastPopup = last->Content(); + } + } + + nsPresContext* presContext = item->Frame()->PresContext(); + RefPtr<nsViewManager> viewManager = + presContext->PresShell()->GetViewManager(); + + HidePopup(item->Content(), true, true, false, true, lastPopup); + + if (aFlush) { + // 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->PopupType() != ePopupTypePanel) 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()->PopupType() != parent->Frame()->PopupType() || + 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, nsIntPoint aPnt, + 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 curDevSize = view->CalcWidgetBounds(eWindowType_popup); + nsIWidget* widget = menuPopupFrame->GetWidget(); + if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y && + (!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. Otherwise, just update the popup to + // the specified screen coordinates. + if (menuPopupFrame->IsAnchored() && + menuPopupFrame->PopupLevel() == ePopupLevelParent) { + menuPopupFrame->SetPopupPosition(true); + } else { + CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt) / + menuPopupFrame->PresContext()->CSSToDevPixelScale(); + menuPopupFrame->MoveTo(cssPos, false, aByMoveToRect); + } +} + +void nsXULPopupManager::PopupResized(nsIFrame* aFrame, + LayoutDeviceIntSize aSize) { + nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame); + if (!menuPopupFrame) { + return; + } + + menuPopupFrame->WidgetPositionOrSizeDidChange(); + + nsView* view = menuPopupFrame->GetView(); + if (!view) { + return; + } + + LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup); + // If the size is what we think it is, we have nothing to do. + if (curDevSize.width == aSize.width && curDevSize.height == aSize.height) { + return; + } + + Element* popup = menuPopupFrame->GetContent()->AsElement(); + + // Only set the width and height if the popup already has these attributes. + if (!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::width) || + !popup->HasAttr(kNameSpaceID_None, 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); + 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::GetTopVisibleMenu() { + for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) { + if (!item->IsNoAutoHide() && + item->Frame()->PopupState() != ePopupInvisible) { + return item; + } + } + return nullptr; +} + +void nsXULPopupManager::SetActiveMenuBar(nsMenuBarFrame* 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->GetContent(), nullptr); + BeginShowingPopup(pendingPopup, parentIsContextMenu, aSelectFirstItem); +} + +void nsXULPopupManager::ShowPopup(nsIContent* aPopup, + nsIContent* aAnchorContent, + const nsAString& aPosition, int32_t aXPos, + int32_t aYPos, bool aIsContextMenu, + bool aAttributesOverride, + bool aSelectFirstItem, Event* aTriggerEvent) { + 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); +} + +static bool ShouldUseNativeContextMenus() { +#ifdef HAS_NATIVE_MENU_SUPPORT + return mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus(); +#else + return false; +#endif +} + +void nsXULPopupManager::ShowPopupAtScreen(nsIContent* 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(nsIContent* 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 + if (aPopup->IsElement()) { + menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu( + aPopup->AsElement()); + } +#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->AsElement()->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 (nsMenuPopupFrame* 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)); + + // 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->PopupType() == ePopupTypeMenu) { + // 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->Content(), true, false, false, false); + } +} + +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( + nsIContent* 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( + nsIContent* 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(nsIContent* aPopup, + nsMenuPopupFrame* aPopupFrame, + bool aIsContextMenu, + bool aSelectFirstItem) { + nsPopupType popupType = aPopupFrame->PopupType(); + const bool isMenu = popupType == ePopupTypeMenu; + + // 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 == ePopupTypeTooltip; + + 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; + if (aPopup->IsElement()) { + aPopup->AsElement()->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->Content(); + } + 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(nsIContent* aPopup) const { + for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) { + if (item->Frame()->GetContent() == aPopup) { + return item; + } + } + return nullptr; +} + +void nsXULPopupManager::HidePopup(nsIContent* aPopup, bool aHideChain, + bool aDeselectMenu, bool aAsynchronous, + bool aIsCancel, nsIContent* 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); + + bool deselectMenu = false; + nsCOMPtr<nsIContent> 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; + } 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(); + } + } + + deselectMenu = aDeselectMenu; + popupToHide = topMenu->Content(); + popupFrame = topMenu->Frame(); + + // 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 && (aHideChain || topMenu != foundPopup)) { + while (parent && parent->IsNoAutoHide()) { + parent = parent->GetParent(); + } + + if (parent) { + nextPopup = parent->Content(); + } + } + + lastPopup = aLastPopup ? aLastPopup : (aHideChain ? 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. + deselectMenu = aDeselectMenu; + popupToHide = aPopup; + } + + if (popupToHide) { + nsPopupState state = popupFrame->PopupState(); + // If the popup is already being hidden, don't attempt to hide it again + if (state == ePopupHiding) { + 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 (aAsynchronous) { + nsCOMPtr<nsIRunnable> event = new nsXULPopupHidingEvent( + popupToHide, nextPopup, lastPopup, popupFrame->PopupType(), + deselectMenu, aIsCancel); + aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); + } else { + RefPtr<nsPresContext> presContext = popupFrame->PresContext(); + FirePopupHidingEvent(popupToHide, nextPopup, lastPopup, presContext, + popupFrame->PopupType(), deselectMenu, aIsCancel); + } + } +} + +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, false, true, false, false); +} + +// 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<nsIContent> mContent; + + protected: + virtual ~TransitionEnder() = default; + + public: + bool mDeselectMenu; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder) + + TransitionEnder(nsIContent* aContent, bool aDeselectMenu) + : mContent(aContent), mDeselectMenu(aDeselectMenu) {} + + MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override { + mContent->RemoveSystemEventListener(u"transitionend"_ns, this, false); + + nsMenuPopupFrame* popupFrame = do_QueryFrame(mContent->GetPrimaryFrame()); + if (!popupFrame) { + 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(mContent, popupFrame, nullptr, nullptr, + popupFrame->PopupType(), mDeselectMenu); + } + + 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, mContent); +void nsXULPopupManager::HidePopupCallback( + nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup, + nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu) { + 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->Content() == aPopup) { + RemoveMenuChainItem(item); + SetCaptureState(aPopup); + break; + } + } + + AutoWeakFrame weakFrame(aPopupFrame); + aPopupFrame->HidePopup(aDeselectMenu, 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->PopupType())) { + nsCOMPtr<nsIContent> popupToHide = foundMenu->Content(); + nsMenuChainItem* parent = foundMenu->GetParent(); + + nsCOMPtr<nsIContent> nextPopup; + if (parent && popupToHide != aLastPopup) nextPopup = parent->Content(); + + 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->PopupType(), aDeselectMenu, false); + } + } +} + +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->Content()->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<nsIContent> 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); + + nsPopupType popupType = popupFrame->PopupType(); + + 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 == ePopupTypePanel && + !popup->AsElement()->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) { + // 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(); + } else { + // 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->AsElement()->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( + nsIContent* aPopup, nsIContent* aNextPopup, nsIContent* aLastPopup, + nsPresContext* aPresContext, nsPopupType aPopupType, bool aDeselectMenu, + bool aIsCancel) { + 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 == ePopupTypePanel && + (!aPopup->IsElement() || !aPopup->AsElement()->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) { + // 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); + } else { + // If the popup has an animate attribute and it is not set to false, check + // if it has a closing transition and wait for it to finish. The + // transition may still occur either way, but the view will be hidden and + // you won't be able to see it. 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. + if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) && + !aNextPopup && aPopup->IsElement() && + aPopup->AsElement()->HasAttr(nsGkAtoms::animate)) { + // If animate="false" then don't transition at all. If animate="cancel", + // only show the transition if cancelling the popup or rolling up. + // Otherwise, always show the transition. + nsAutoString animate; + aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::animate, + animate); + + if (!animate.EqualsLiteral("false") && + (!animate.EqualsLiteral("cancel") || aIsCancel)) { + presShell->FlushPendingNotifications(FlushType::Layout); + + // Get the frame again in case the flush caused it to go away + popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame()); + if (!popupFrame) return; + + if (AnimationUtils::HasCurrentTransitions( + aPopup->AsElement(), PseudoStyleType::NotPseudo)) { + RefPtr<TransitionEnder> ender = + new TransitionEnder(aPopup, aDeselectMenu); + aPopup->AddSystemEventListener(u"transitionend"_ns, ender, false, + false); + return; + } + } + } + + HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType, + aDeselectMenu); + } + } +} + +bool nsXULPopupManager::IsPopupOpen(nsIContent* 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(nsPopupType aType) { + for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) { + if (item->Frame()->IsVisible() && + (item->PopupType() == aType || aType == ePopupTypeAny)) { + 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->PopupType() == ePopupTypeTooltip) == aIsTooltip && + item->Content()->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->GetContent()), + "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->GetContent()) || 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->GetContent())) { + 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->GetContent()); + 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->Content(), false, false, true, false); + 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->Content()) 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; + nsMenuChainItem* item = GetTopVisibleMenu(); + if (item) { + if (item->IgnoreKeys() != eIgnoreKeys_True) { + newTarget = item->Content()->GetComposedDoc(); + } + isForMenu = item->PopupType() == ePopupTypeMenu; + } else if (mActiveMenuBar) { + newTarget = mActiveMenuBar->GetContent()->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(nsIContent* 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->GetContent(), false, false, true, false); + } + } + + 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; + } + + if (mActiveMenuBar) { + RefPtr menubar = &mActiveMenuBar->MenubarElement(); + 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(); + } + mActiveMenuBar->SetActive(false); +#endif + } + return false; +} + +bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) { + if (nsMenuChainItem* nextitem = GetTopVisibleMenu()) { + nextitem->Content()->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; + } 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 = XULMenuParentElement::FromNode(mActiveMenuBar->GetContent()); + 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->GetContent(), /* aHideChain = */ false, + /* aDeselectMenu = */ false, /* aAsynchronous = */ false, + /* aIsCancel = */ false); + } + 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->PopupType() != ePopupTypeMenu) { + if (keyCode == KeyboardEvent_Binding::DOM_VK_ESCAPE) { + HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true); + 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(0, false, nullptr, nullptr); + 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->Content(), false, false, false, true); + } else if (mActiveMenuBar) { + mActiveMenuBar->MenuClosed(); + } + 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(0, false, nullptr, nullptr); + break; + } else if (mActiveMenuBar) { + mActiveMenuBar->MenuClosed(); + 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) { + mActiveMenuBar->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( + mozilla::dom::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->PopupType() != ePopupTypeMenu) 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->PopupType() != ePopupTypeMenu)) + 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. + int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey(); + if (menuAccessKey) { + uint32_t theChar = aKeyEvent->KeyCode(); + + if (theChar == (uint32_t)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(0, false, nullptr, nullptr); + } else if (mActiveMenuBar) { + mActiveMenuBar->MenuClosed(); + } + + // 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->PopupType() != ePopupTypeMenu)) { + 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<nsIContent> popup = mPopup; + nsCOMPtr<nsIContent> nextPopup = mNextPopup; + nsCOMPtr<nsIContent> lastPopup = mLastPopup; + pm->FirePopupHidingEvent(popup, nextPopup, lastPopup, presContext, + mPopupType, mDeselectMenu, mIsRollup); + } + } + return NS_OK; +} + +bool nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent* aPopup) { + // The popuppositioned event only fires on arrow panels for now. + if (aPopup->IsElement() && + aPopup->AsElement()->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()) { + pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false, + false); + } + } + + return NS_OK; +} diff --git a/layout/xul/nsXULPopupManager.h b/layout/xul/nsXULPopupManager.h new file mode 100644 index 0000000000..9100ad7738 --- /dev/null +++ b/layout/xul/nsXULPopupManager.h @@ -0,0 +1,881 @@ +/* -*- 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 "nsPoint.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsIReflowCallback.h" +#include "nsThreadUtils.h" +#include "nsPresContext.h" +#include "nsStyleConsts.h" +#include "nsWidgetInitData.h" +#include "mozilla/Attributes.h" +#include "mozilla/widget/NativeMenu.h" +#include "Units.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 nsMenuPopupFrame; +class nsMenuBarFrame; +class nsIDocShellTreeItem; +class nsPIDOMWindowOuter; +class nsRefreshDriver; + +namespace mozilla { +class PresShell; +namespace dom { +class Event; +class KeyboardEvent; +class UIEvent; +class XULButtonElement; +} // 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, +}; + +#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"); + +/** + * 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 { + PendingPopup(nsIContent* aPopup, mozilla::dom::Event* aEvent); + + const RefPtr<nsIContent> mPopup; + const RefPtr<mozilla::dom::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 { + private: + nsMenuPopupFrame* mFrame; // the popup frame + nsPopupType 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, + nsPopupType 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) + + nsIContent* Content(); + nsMenuPopupFrame* Frame() { return mFrame; } + nsPopupType PopupType() { return mPopupType; } + bool IsNoAutoHide() { return mNoAutoHide; } + void SetNoAutoHide(bool aNoAutoHide) { mNoAutoHide = aNoAutoHide; } + bool IsMenu() { return mPopupType == ePopupTypeMenu; } + 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 { + public: + nsXULPopupHidingEvent(nsIContent* aPopup, nsIContent* aNextPopup, + nsIContent* aLastPopup, nsPopupType aPopupType, + bool aDeselectMenu, bool aIsCancel) + : mozilla::Runnable("nsXULPopupHidingEvent"), + mPopup(aPopup), + mNextPopup(aNextPopup), + mLastPopup(aLastPopup), + mPopupType(aPopupType), + mDeselectMenu(aDeselectMenu), + mIsRollup(aIsCancel) { + NS_ASSERTION(aPopup, + "null popup supplied to nsXULPopupHidingEvent constructor"); + // aNextPopup and aLastPopup may be null + } + + NS_IMETHOD Run() override; + + private: + nsCOMPtr<nsIContent> mPopup; + nsCOMPtr<nsIContent> mNextPopup; + nsCOMPtr<nsIContent> mLastPopup; + nsPopupType mPopupType; + bool mDeselectMenu; + bool mIsRollup; +}; + +// this class is used for dispatching popuppositioned events asynchronously. +class nsXULPopupPositionedEvent : public mozilla::Runnable { + public: + explicit nsXULPopupPositionedEvent(nsIContent* 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(nsIContent* aPopup); + + private: + const nsCOMPtr<nsIContent> mPopup; +}; + +// this class is used for dispatching menu command events asynchronously. +class nsXULMenuCommandEvent : public mozilla::Runnable { + public: + nsXULMenuCommandEvent(mozilla::dom::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<mozilla::dom::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; + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIDOMEVENTLISTENER + + // nsIRollupListener + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual bool Rollup(uint32_t aCount, bool aFlush, + const mozilla::LayoutDeviceIntPoint* pos, + nsIContent** aLastRolledUp) override; + virtual bool ShouldRollupOnMouseWheelEvent() override; + virtual bool ShouldConsumeOnMouseWheelEvent() override; + virtual bool ShouldRollupOnMouseActivate() override; + virtual uint32_t GetSubmenuWidgetChain( + nsTArray<nsIWidget*>* aWidgetChain) override; + virtual nsIWidget* GetRollupWidget() override; + virtual bool RollupNativeMenu() override; + + // 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(nsMenuBarFrame* 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(nsIContent* 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(nsIContent* 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(nsIContent* 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( + nsIContent* 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(nsIContent* 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. + * + * aHideChain - true if the entire chain of menus should be closed. If false, + * only this popup is closed. + * aDeselectMenu - true if the parent <menu> of the popup should be + * deselected. This will be false when the menu is closed by + * pressing the Escape key. + * aAsynchronous - true if the first popuphiding event should be sent + * asynchrously. This should be true if HidePopup is called + * from a frame. + * aIsCancel - true if this popup is hiding due to being cancelled. + * aLastPopup - optional popup to close last when hiding a chain of menus. + * If null, then all popups will be closed. + */ + void HidePopup(nsIContent* aPopup, bool aHideChain, bool aDeselectMenu, + bool aAsynchronous, bool aIsCancel, + nsIContent* 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(nsIContent* 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 ePopupTypeAny, a menu of any + * type is returned. + */ + nsIFrame* GetTopPopup(nsPopupType 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 screen coordinates. + */ + void PopupMoved(nsIFrame* aFrame, nsIntPoint aPoint, bool aByMoveToRect); + + /** + * Indicate that the popup associated with aView has been resized to the + * given device pixel size aSize. + */ + void PopupResized(nsIFrame* aFrame, 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(nsIContent* 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); + + // return the topmost menu, skipping over invisible popups + nsMenuChainItem* GetTopVisibleMenu(); + + // 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(nsIContent* aPopup, + nsMenuPopupFrame* aPopupFrame, + bool aIsContextMenu, + bool aSelectFirstItem); + MOZ_CAN_RUN_SCRIPT void HidePopupCallback( + nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup, + nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu); + + /** + * 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. + * aDeselectMenu - true to unhighlight the menu when hiding it + * aIsCancel - true if this popup is hiding due to being cancelled. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void FirePopupHidingEvent(nsIContent* aPopup, nsIContent* aNextPopup, + nsIContent* aLastPopup, nsPresContext* aPresContext, + nsPopupType aPopupType, bool aDeselectMenu, + bool aIsCancel); + + /** + * 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(nsIContent* 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 + nsMenuBarFrame* 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..4e9e62790d --- /dev/null +++ b/layout/xul/nsXULTooltipListener.cpp @@ -0,0 +1,678 @@ +/* -*- 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 "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/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.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) { + // FIXME(emilio): This can be faster, this should use static prefs. + // + // register the callback so we get notified of updates + Preferences::RegisterCallback(ToolbarTipsPrefChanged, + "browser.chrome.toolbar_tips"); + + // Call the pref callback to initialize our state. + ToolbarTipsPrefChanged("browser.chrome.toolbar_tips", nullptr); +} + +nsXULTooltipListener::~nsXULTooltipListener() { + MOZ_ASSERT(sInstance == this); + sInstance = nullptr; + + HideTooltip(); + + // Unregister our pref observer + Preferences::UnregisterCallback(ToolbarTipsPrefChanged, + "browser.chrome.toolbar_tips"); +} + +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 (!sShowTooltips) 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 (!keyEvent->IsModifierKeyEvent()) { + 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 + +// static +void nsXULTooltipListener::ToolbarTipsPrefChanged(const char* aPref, + void* aClosure) { + sShowTooltips = + Preferences::GetBool("browser.chrome.toolbar_tips", sShowTooltips); +} + +////////////////////////////////////////////////////////////////////////// +//// nsXULTooltipListener + +bool nsXULTooltipListener::sShowTooltips = false; + +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 + Document* doc = sourceNode->GetComposedDoc(); + if (doc) { + // 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); +#ifndef XP_WIN + // On Windows, key events don't close tooltips. + doc->AddSystemEventListener(u"keydown"_ns, this, true); +#endif + } + 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<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip)) { + if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) { + pm->HidePopup(currentTooltip, false, false, false, false); + } + } + + 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 + nsCOMPtr<Document> doc = currentTooltip->GetComposedDoc(); + if (doc) { + // 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); +#ifndef XP_WIN + doc->RemoveSystemEventListener(u"keydown"_ns, this, true); +#endif + } + + // 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..1810accf81 --- /dev/null +++ b/layout/xul/nsXULTooltipListener.h @@ -0,0 +1,98 @@ +/* -*- 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 "XULTreeElement.h" +#include "nsIWeakReferenceUtils.h" +#include "mozilla/Attributes.h" + +class nsIContent; +class nsTreeColumn; + +namespace mozilla { +namespace dom { +class Event; +class MouseEvent; +} // namespace dom +} // 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; + } + + protected: + nsXULTooltipListener(); + ~nsXULTooltipListener(); + + // pref callback for when the "show tooltips" pref changes + static bool sShowTooltips; + + 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; + static void ToolbarTipsPrefChanged(const char* aPref, void* aClosure); + + 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..595450fe47 --- /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; 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..f2ecbec30e --- /dev/null +++ b/layout/xul/reftest/image-size-ref.xhtml @@ -0,0 +1,115 @@ +<?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:div> + +<html:div><html:div + style="width: 20px; height: 15px;"/><html:div + style="width: 80px; height: 60px;"/><html:div + style="width: 40px; height: 30px;"/><html:div + style="width: 10px; height: 8px;"/><html:div + style="width: 10px; height: 8px;"/> +</html:div> + +<html:div><html:div style="width: 20px; height: 15px;"/></html:div> + +<html:div><html:div style="width: 20px; height: 15px;"/></html:div> + +<html:div><html:div style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/></html:div> + +<html:div><html:div style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/></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..3d372dc3cd --- /dev/null +++ b/layout/xul/reftest/image-size.xhtml @@ -0,0 +1,123 @@ +<?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" width="80" height="20"/> + <image src="image4x3.png" width="10" height="70"/> + <image src="image4x3.png" width="80"/> + <image src="image4x3.png" height="60"/> + <image src="image4x3.png" width="20"/> + <image src="image4x3.png" height="15"/> + <image src="image4x3.png" style="border: 8px solid green;"/> + <image src="image4x3.png" width="80" style="border: 8px solid yellow;"/> + <image src="image4x3.png" height="58" style="border: 8px solid green;"/> + <image src="image4x3.png" width="24" style="border: 8px solid yellow;"/> + <image src="image4x3.png" height="22" style="border: 8px solid green;"/> + <image src="image4x3.png" width="74" + style="border: 1px solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/> + <image src="image4x3.png" height="11" + style="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: 80px; height: 20px;" width="60" height="25"/> + <image src="image4x3.png" style="width: 10px; height: 70px;" width="20" height="75"/> + <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" maxwidth="20"/> +</hbox> + +<hbox align="end"> + <image src="image4x3.png" maxheight="15"/> +</hbox> + +<hbox align="end"> + <image src="image4x3.png" maxwidth="30" maxheight="25"/> +</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" maxwidth="24" style="border: 8px solid green;"/> +</hbox> +<hbox align="end"> + <image src="image4x3.png" maxheight="22" style="border: 8px solid green;"/> +</hbox> + +<hbox align="end"> + <image src="image4x3.png" minwidth="20"/> + <image src="image4x3.png" minheight="20"/> + <image src="image4x3.png" minwidth="20" minheight="25"/> + <image src="image4x3.png" minwidth="60" style="border: 8px solid green;"/> + <image src="image4x3.png" minheight="88" style="border: 8px solid yellow;"/> + <image src="image4x3.png" minwidth="90" minheight="76" style="border: 8px solid green;"/> + <image src="image4x3.png" minwidth="112" minheight="76" style="border: 8px solid yellow;"/> + <image src="image4x3.png" minwidth="106" + style="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;"/> +</hbox> + +<hbox align="start"> + <image style="width: auto; height: auto; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/> + <image width="80" style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/> + <image height="30" style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/> + <image style="width: 10px; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 21px, 5px);"/> + <image style="height: 8px; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 21px, 5px);"/> +</hbox> + +<hbox align="end"> + <image maxwidth="20" + style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/> +</hbox> + +<hbox align="end"> + <image maxheight="15" + style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/> +</hbox> + +<hbox align="end"> + <image maxwidth="24" + style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px); border: 8px solid green;"/> +</hbox> + +<hbox align="end"> + <image maxheight="22" + style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px); border: 8px solid green;"/> +</hbox> + +</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..ae29389e0f --- /dev/null +++ b/layout/xul/test/browser_bug1163304.js @@ -0,0 +1,83 @@ +const { CustomizableUITestUtils } = ChromeUtils.import( + "resource://testing-common/CustomizableUITestUtils.jsm" +); +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..1fad9ebe1a --- /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..8542fbaeb6 --- /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..c02b7aba08 --- /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..cb7e487a26 --- /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..b0ebb99b07 --- /dev/null +++ b/layout/xul/test/chrome.ini @@ -0,0 +1,39 @@ +[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_blockify_moz_box.html] +[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..599abe9b07 --- /dev/null +++ b/layout/xul/test/mochitest.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + file_bug386386.sjs +[test_bug386386.html] +allow_xul_xbl = true +[test_bug394800.xhtml] +allow_xul_xbl = true +[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_blockify_moz_box.html b/layout/xul/test/test_blockify_moz_box.html new file mode 100644 index 0000000000..3f4a4546b8 --- /dev/null +++ b/layout/xul/test/test_blockify_moz_box.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1580012 +--> +<head> + <title>Test for Bug 1580012</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <style> + /* Styling for parents that blockify their children: */ + .grid { display: grid; } + .flex { display: flex; } + + /* Styling that blockifies an element itself: */ + .float { float: left; } + .abs { position: absolute; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1580012">Mozilla Bug 1580012</a> +<p id="display"></p> +<div id="content"> + <!-- Boxes that have no reason to be blockified: --> + <div class="moz-box" id="regularMozBox"></div> + <div class="moz-inline-box" id="regularMozInlineBox"></div> + + <!-- A grid container with a -moz-box and a -moz-inline-box grid item (which + should both end up with display:-moz-box), and a -moz-inline-box + grandchild (which should preserve its -moz-inline-box display val): --> + <div class="grid"> + <div class="moz-box" id="gridItemMozBox"></div> + <div class="moz-inline-box" id="gridItemMozInlineBox"></div> + <div><div class="moz-inline-box" id="gridGrandchildMozInlineBox"></div></div> + </div> + + <!-- A flex container with a -moz-box and a -moz-inline-box flex item (which + should both end up with display:-moz-box), and a -moz-inline-box + grandchild (which should preserve its -moz-inline-box display val): --> + <div class="flex"> + <div class="moz-box" id="flexItemMozBox"></div> + <div class="moz-inline-box" id="flexItemMozInlineBox"></div> + <div><div class="moz-inline-box" id="flexGrandchildMozInlineBox"></div></div> + </div> + + <!-- Boxes that are directly blockified via other styling on them: --> + <!-- XXXdholbert commenting these out -- see notes below about assertion + failures for floated -moz-box. + <div class="float moz-box" id="floatMozBox"></div> + <div class="float moz-inline-box" id="floatMozInlineBox"></div> + --> + <!-- XXXdholbert commenting these out -- see notes below about assertion + failures for positioned -moz-box. + <div class="abs moz-box" id="absMozBox"></div> + <div class="abs moz-inline-box" id="absMozInlineBox"></div> + --> +</div> +<pre id="test"> +<script> + +/** Test for Bug 1580012 **/ + +function checkDisp(elemId, expectedDisplay) { + var elem = document.getElementById(elemId); + ok(elem, "should have a valid ID for an element"); + + is(getComputedStyle(elem).display, expectedDisplay, + "Element with ID " + elemId + " should have expected display value"); +} + +// Create CSS Style rules to add -moz-box / -moz-inline-box styling. +// Note that these style won't parse correctly until after we've flipped +// the prefs via pushPrefEnv(). That's why I'm creating these style rules +// here rather than just putting them inline in the <style> element. +var sheet = document.styleSheets[0]; +sheet.insertRule(".moz-box { display: -moz-box; }"); +sheet.insertRule(".moz-inline-box { display: -moz-inline-box; }"); + +// Check the computed 'display' of the various elements. +checkDisp("regularMozBox", "-moz-box"); +checkDisp("regularMozInlineBox", "-moz-inline-box"); + +checkDisp("gridItemMozBox", "-moz-box"); +checkDisp("gridItemMozInlineBox", "-moz-box"); +checkDisp("gridGrandchildMozInlineBox", "-moz-inline-box"); + +checkDisp("flexItemMozBox", "-moz-box"); +checkDisp("flexItemMozInlineBox", "-moz-box"); +checkDisp("flexGrandchildMozInlineBox", "-moz-inline-box"); + +// XXXdholbert The floated boxes trigger assertion failures where +// nsLineLayout thinks it somehow ended up with an inline-level (really, just +// a non-'block') floated thing. In practice this isn't really a concern +// since -moz-box display values are disabled in content and since XUL +// doesn't use 'float' for layout. So: I've added a fatal assertion in +// ReflowInput.cpp to validate that we never actually encounter a floated +// -moz-box/-moz-inline-box, and I'm commenting out these lines (which +// trigger that fatal assertion). +// +// checkDisp("floatMozBox", "-moz-box"); +// checkDisp("floatMozInlineBox", "-moz-box"); + + +// XXXdholbert These abspos boxes trigger a diagnostic assertion added in +// bug 1582819 which is intended to flush out XUL content that is positioned +// and hence was previously blockified to 'block' but will now be '-moz-box'. +// The diagnostic assertion doesn't need to stay around forever, but while +// it exists, we can't test this scenario without triggering it. +// +// checkDisp("absMozBox", "-moz-box"); +// checkDisp("absMozInlineBox", "-moz-box"); +</script> +</pre> +</body> +</html> diff --git a/layout/xul/test/test_bug1197913.xhtml b/layout/xul/test/test_bug1197913.xhtml new file mode 100644 index 0000000000..770c70113d --- /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, 0, 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, 0, 0, { type: "mousemove" }); + synthesizeMouse(menuitem, 1, 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..03dc5d5d1b --- /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)[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..c5c0fc0dce --- /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)) { + 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..8ee74177e9 --- /dev/null +++ b/layout/xul/test/test_splitter.xhtml @@ -0,0 +1,131 @@ +<?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"?> +<!-- +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"); + await runTests(false, "ltr-flex-splitter"); + await runTests(true, "rtl-flex-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"/> + <splitter id="ltr-splitter" width="5"/> + <vbox style="height: 300px;" flex="1"/> + </hbox> + + <hbox style="display: none; width: 200px; height: 300px; direction: rtl;"> + <vbox style="height: 300px;" flex="1"/> + <splitter id="rtl-splitter" width="5"/> + <vbox style="height: 300px;" flex="1"/> + </hbox> + + <hbox style="display: none; width: 200px; height: 300px; direction: ltr; -moz-box-layout: flex"> + <vbox style="height: 300px;" flex="1"/> + <splitter id="ltr-flex-splitter" width="5"/> + <vbox style="height: 300px;" flex="1"/> + </hbox> + + <hbox style="display: none; width: 200px; height: 300px; direction: rtl; -moz-box-layout: flex"> + <vbox style="height: 300px;" flex="1"/> + <splitter id="rtl-flex-splitter" width="5"/> + <vbox style="height: 300px;" flex="1"/> + </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..9e139309e7 --- /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" width="5" 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" width="5" 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" width="5" 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" width="5" 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..d502fd1248 --- /dev/null +++ b/layout/xul/test/test_windowminmaxsize.xhtml @@ -0,0 +1,191 @@ +<?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" flex="1" width="60" height="60" + style="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() +{ + // 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 { + 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 setattr(attr) { + if (attr in popupTests[gTestId]) + panel.setAttribute(attr, popupTests[gTestId][attr]); + else + panel.removeAttribute(attr); + } + setattr("minwidth"); + setattr("minheight"); + setattr("maxwidth"); + setattr("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..ab3e8fcf99 --- /dev/null +++ b/layout/xul/test/titledpanelwindow.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;'> +<panel style="background: white" noautohide='true' titlebar='normal' minwidth='120' minheight='140'/><label value='Test'/> +</window> diff --git a/layout/xul/test/windowminmaxsize1.xhtml b/layout/xul/test/windowminmaxsize1.xhtml new file mode 100644 index 0000000000..fff337da23 --- /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' width='150' height='150' style='-moz-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..bf20fd4ce2 --- /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;' minwidth='120' maxwidth='480' minheight='110' maxheight='470'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..96053b76f8 --- /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' width='150' height='150' style='-moz-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..2eca783041 --- /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' width='150' height='150' style='-moz-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..81b07ca5c0 --- /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;' minwidth='240' minheight='220'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..b4365e5388 --- /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;' width='190' height='220' minwidth='215' minheight='235'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..76a8b6b3f0 --- /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;' maxwidth='125' maxheight='95'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..9d037fd27d --- /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;' minwidth='210'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..52847e91af --- /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;' maxwidth='128'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..515fe1b243 --- /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;' maxwidth='195' width='230' height='120' minheight='180'> +<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-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..8ed749aca4 --- /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 +load 601427.html +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..04385a9d4f --- /dev/null +++ b/layout/xul/tree/moz.build @@ -0,0 +1,46 @@ +# -*- 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 += [ + "nsTreeColFrame.h", + "nsTreeColumns.h", + "nsTreeUtils.h", +] + +UNIFIED_SOURCES += [ + "nsTreeBodyFrame.cpp", + "nsTreeColFrame.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..f20d39b263 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.cpp @@ -0,0 +1,4348 @@ +/* -*- 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/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 "nsCSSRendering.h" +#include "nsString.h" +#include "nsContainerFrame.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsVariant.h" +#include "nsWidgetsCID.h" +#include "nsIFrameInlines.h" +#include "nsBoxFrame.h" +#include "nsBoxLayoutState.h" +#include "nsTextBoxFrame.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; + +// 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(nsLeafBoxFrame) + +// Constructor +nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsLeafBoxFrame(aStyle, aPresContext, kClassID), + mImageCache(), + mTopRowIndex(0), + mPageLength(0), + mHorzPosition(0), + mOriginalHorzWidth(-1), + mHorzWidth(0), + mAdjustWidth(0), + mRowHeight(0), + mIndentation(0), + mStringWidth(-1), + 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) { + nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow); + + mIndentation = GetIndentation(); + mRowHeight = GetRowHeight(); + + // Call GetBaseElement so that mTree is assigned. + GetBaseElement(); + + if (LookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0) { + mScrollbarActivity = + new ScrollbarActivity(static_cast<nsIScrollbarMediator*>(this)); + } +} + +nsSize nsTreeBodyFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) { + EnsureView(); + + RefPtr<XULTreeElement> tree(GetBaseElement()); + + nsSize min(0, 0); + int32_t desiredRows; + if (MOZ_UNLIKELY(!tree)) { + desiredRows = 0; + } else { + nsAutoString rows; + tree->GetAttr(kNameSpaceID_None, nsGkAtoms::rows, rows); + if (!rows.IsEmpty()) { + nsresult err; + desiredRows = rows.ToInteger(&err); + mPageLength = desiredRows; + } else { + desiredRows = 0; + } + } + + min.height = mRowHeight * desiredRows; + + AddXULBorderAndPadding(min); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(this, min, widthSet, heightSet); + + return min; +} + +nscoord nsTreeBodyFrame::CalcMaxRowWidth() { + if (mStringWidth != -1) return mStringWidth; + + if (!mView) { + return 0; + } + + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + nsMargin rowMargin(0, 0, 0, 0); + GetBorderPadding(rowContext, rowMargin); + + nscoord rowWidth; + nsTreeColumn* col; + + RefPtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext(); + + for (int32_t row = 0; row < mRowCount; ++row) { + rowWidth = 0; + + for (col = mColumns->GetFirstColumn(); col; col = col->GetNext()) { + nscoord desiredWidth, currentWidth; + nsresult rv = GetCellWidth(row, col, rc, desiredWidth, currentWidth); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE("invalid column"); + continue; + } + rowWidth += desiredWidth; + } + + if (rowWidth > mStringWidth) mStringWidth = rowWidth; + } + + mStringWidth += rowMargin.left + rowMargin.right; + return mStringWidth; +} + +void nsTreeBodyFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + 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); + + if (mTree) { + mTree->BodyDestroyed(mTopRowIndex); + } + + if (nsCOMPtr<nsITreeView> view = std::move(mView)) { + nsCOMPtr<nsITreeSelection> sel; + view->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + view->SetTree(nullptr); + } + + nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +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 nsRect& aRect, + nscoord aHorzWidth) { + if (!mReflowCallbackPosted && + (!aRect.IsEqualEdges(mRect) || mHorzWidth != aHorzWidth)) { + PresShell()->PostReflowCallback(this); + mReflowCallbackPosted = true; + mOriginalHorzWidth = mHorzWidth; + } else if (mReflowCallbackPosted && mHorzWidth != aHorzWidth && + mOriginalHorzWidth == aHorzWidth) { + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + mOriginalHorzWidth = -1; + } +} + +void nsTreeBodyFrame::SetXULBounds(nsBoxLayoutState& aBoxLayoutState, + const nsRect& aRect, + bool aRemoveOverflowArea) { + nscoord horzWidth = CalcHorzWidth(GetScrollParts()); + ManageReflowCallback(aRect, horzWidth); + mHorzWidth = horzWidth; + + nsLeafBoxFrame::SetXULBounds(aBoxLayoutState, aRect, aRemoveOverflowArea); +} + +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) { + // 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; + } + } + + nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame); + if (sf) { + if (!aCurrFrame->IsXULHorizontal()) { + 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(); + nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr; + if (treeFrame) { + // 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()); + nsIFrame* f = do_QueryFrame(result.mHScrollbar); + result.mHScrollbarContent = f->GetContent()->AsElement(); + } + if (result.mVScrollbar) { + result.mVScrollbar->SetScrollbarMediatorContent(GetContent()); + nsIFrame* f = do_QueryFrame(result.mVScrollbar); + result.mVScrollbarContent = f->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); + + RefPtr<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; + } + } + } + } + + using CroppingStyle = nsTextBoxFrame::CroppingStyle; + CroppingStyle cropType = CroppingStyle::CropRight; + if (aColumn->GetCropStyle() == 1) { + cropType = CroppingStyle::CropCenter; + } else if (aColumn->GetCropStyle() == 2) { + cropType = CroppingStyle::CropLeft; + } + nsTextBoxFrame::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(); + RefPtr<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; + + RefPtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext(); + + rv = GetCellWidth(aRow, aCol, rc, 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, + bool& aAllowImageRegions, + imgIContainer** aResult) { + *aResult = nullptr; + + nsAutoString imageSrc; + mView->GetImageSrc(aRowIndex, aCol, imageSrc); + RefPtr<imgRequestProxy> styleRequest; + if (!aUseContext && !imageSrc.IsEmpty()) { + aAllowImageRegions = false; + } else { + // Obtain the URL from the ComputedStyle. + aAllowImageRegions = true; + 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 (like nsImageBoxFrame does). 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. + bool useImageRegion = true; + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, useImageRegion, + getter_AddRefs(image)); + + const nsStylePosition* myPosition = aComputedStyle->StylePosition(); + const nsStyleList* myList = aComputedStyle->StyleList(); + nsRect imageRegion = myList->GetImageRegion(); + if (useImageRegion) { + r.x += imageRegion.x; + r.y += imageRegion.y; + } + + if (myPosition->mWidth.ConvertsToLength()) { + int32_t val = myPosition->mWidth.ToLength(); + r.width += val; + } else if (useImageRegion && imageRegion.width > 0) { + r.width += imageRegion.width; + } else { + needWidth = true; + } + + if (myPosition->mHeight.ConvertsToLength()) { + int32_t val = myPosition->mHeight.ToLength(); + r.height += val; + } else if (useImageRegion && imageRegion.height > 0) + r.height += imageRegion.height; + 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, + bool useImageRegion, + 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); + + const nsStyleList* myList = aComputedStyle->StyleList(); + nsRect imageRegion = myList->GetImageRegion(); + if (useImageRegion && imageRegion.width > 0) { + // CSS has specified an image region. + // Use the width of the image region. + imageSize.width = imageRegion.width; + } else if (image) { + nscoord width; + image->GetWidth(&width); + imageSize.width = nsPresContext::CSSPixelsToAppUnits(width); + } + + if (useImageRegion && imageRegion.height > 0) { + // CSS has specified an image region. + // Use the height of the image region. + imageSize.height = imageRegion.height; + } else if (image) { + 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, + bool useImageRegion, + imgIContainer* image) { + const nsStyleList* myList = aComputedStyle->StyleList(); + // CSS has specified an image region. + if (useImageRegion && myList->mImageRegion.IsRect()) { + return myList->GetImageRegion(); + } + + 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 nsLeafBoxFrame::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); + } + + virtual 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) + + virtual 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. + nsLeafBoxFrame::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); + if (!item) { + return; + } + 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::None, + 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; + bool useImageRegion = true; + GetImage(aRowIndex, aColumn, true, twistyContext, useImageRegion, + 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 opacity value for the image. + float opacity = imageContext->StyleEffects()->mOpacity; + + // 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. + bool useImageRegion = true; + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aColumn, false, imageContext, useImageRegion, + getter_AddRefs(image)); + + // Get the image destination size. + nsSize imageDestSize = GetImageDestSize(imageContext, useImageRegion, 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, useImageRegion, 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; + } + } + + if (opacity != 1.0f) { + aRenderingContext.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + opacity); + } + + 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); + + if (opacity != 1.0f) { + aRenderingContext.PopGroupAndBlend(); + } + } + + // 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 opacity value for the image. + float opacity = textContext->StyleEffects()->mOpacity; + + // 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()); + + if (opacity != 1.0f) { + aRenderingContext.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + opacity); + } + + aRenderingContext.SetColor( + sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor())); + nsLayoutUtils::DrawString( + this, *fontMet, &aRenderingContext, text.get(), text.Length(), + textRect.TopLeft() + nsPoint(0, baseline), cellContext); + + if (opacity != 1.0f) { + aRenderingContext.PopGroupAndBlend(); + } + + 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; + bool useImageRegion = true; + GetImage(aRowIndex, aColumn, true, checkboxContext, useImageRegion, + 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->IsXULHorizontal(); + + 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) { + nsLeafBoxFrame::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(); + mStringWidth = -1; +} + +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..e2ddacc539 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.h @@ -0,0 +1,614 @@ +/* -*- 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 "nsLeafBoxFrame.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 "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 nsLeafBoxFrame, + public nsIScrollbarMediator, + public nsIReflowCallback { + typedef mozilla::layout::ScrollbarActivity ScrollbarActivity; + typedef mozilla::image::ImgDrawResult ImgDrawResult; + + public: + explicit nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + ~nsTreeBodyFrame(); + + 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(const nsRect& aRect, nscoord aHorzWidth); + + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual void SetXULBounds(nsBoxLayoutState& aBoxLayoutState, + const nsRect& aRect, + bool aRemoveOverflowArea = false) override; + + // nsIReflowCallback + virtual bool ReflowFinished() override; + virtual void ReflowCallbackCanceled() override; + + // nsIScrollbarMediator + virtual void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + virtual void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + virtual void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + virtual void ScrollByUnit(nsScrollbarFrame* aScrollbar, + mozilla::ScrollMode aMode, int32_t aDirection, + mozilla::ScrollUnit aUnit, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + virtual void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) override; + virtual void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) override; + virtual void ScrollbarReleased(nsScrollbarFrame* aScrollbar) override {} + virtual void VisibilityChanged(bool aVisible) override { Invalidate(); } + virtual nsIFrame* GetScrollbarBox(bool aVertical) override { + ScrollParts parts = GetScrollParts(); + return aVertical ? parts.mVScrollbar : parts.mHScrollbar; + } + virtual void ScrollbarActivityStarted() const override; + virtual void ScrollbarActivityStopped() const override; + virtual bool IsScrollbarOnRight() const override { + return StyleVisibility()->mDirection == mozilla::StyleDirection::Ltr; + } + virtual bool ShouldSuppressScrollbarRepaints() const override { + return false; + } + + // Overridden from nsIFrame to cache our pres context. + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + mozilla::Maybe<Cursor> GetCursor(const nsPoint&) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual 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, bool& aAllowImageRegions, + 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* aComputedStyle, bool useImageRegion, + imgIContainer* image); + + // Returns the source rectangle of the image to be displayed. + nsRect GetImageSourceRect(ComputedStyle* aComputedStyle, bool useImageRegion, + imgIContainer* image); + + // 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); + nscoord CalcMaxRowWidth(); + + // 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; + + // Cached heights and indent info. + nsRect mInnerBox; // 4-byte aligned + int32_t mRowHeight; + int32_t mIndentation; + nscoord mStringWidth; + + 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/nsTreeColFrame.cpp b/layout/xul/tree/nsTreeColFrame.cpp new file mode 100644 index 0000000000..d075da8a72 --- /dev/null +++ b/layout/xul/tree/nsTreeColFrame.cpp @@ -0,0 +1,169 @@ +/* -*- 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 "nsTreeColFrame.h" + +#include "mozilla/ComputedStyle.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/XULTreeElement.h" +#include "mozilla/CSSOrderAwareFrameIterator.h" +#include "nsCOMPtr.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsTreeColumns.h" +#include "nsDisplayList.h" +#include "nsTreeBodyFrame.h" +#include "nsXULElement.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// +// NS_NewTreeColFrame +// +// Creates a new col frame +// +nsIFrame* NS_NewTreeColFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsTreeColFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTreeColFrame) + +// Destructor +nsTreeColFrame::~nsTreeColFrame() = default; + +void nsTreeColFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBoxFrame::Init(aContent, aParent, aPrevInFlow); + InvalidateColumns(); +} + +void nsTreeColFrame::DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) { + InvalidateColumns(false); + nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData); +} + +nsresult nsTreeColFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = + nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); + + if (aAttribute == nsGkAtoms::primary) { + InvalidateColumns(); + } + + return rv; +} + +void nsTreeColFrame::SetXULBounds(nsBoxLayoutState& aBoxLayoutState, + const nsRect& aRect, + bool aRemoveOverflowArea) { + nscoord oldWidth = mRect.width; + + nsBoxFrame::SetXULBounds(aBoxLayoutState, aRect, aRemoveOverflowArea); + if (mRect.width != oldWidth) { + RefPtr<XULTreeElement> tree = GetTree(); + if (tree) { + tree->Invalidate(); + } + } +} + +XULTreeElement* nsTreeColFrame::GetTree() { + nsIContent* parent = mContent->GetParent(); + return parent ? XULTreeElement::FromNodeOrNull(parent->GetParent()) : nullptr; +} + +void nsTreeColFrame::InvalidateColumns(bool aCanWalkFrameTree) { + RefPtr<XULTreeElement> tree = GetTree(); + if (!tree) { + return; + } + + nsTreeBodyFrame* body = aCanWalkFrameTree + ? tree->GetTreeBodyFrame(FlushType::None) + : tree->GetCachedTreeBodyFrame(); + + if (!body) { + return; + } + + RefPtr<nsTreeColumns> columns = body->Columns(); + if (!columns) { + return; + } + + columns->InvalidateColumns(); +} + +namespace mozilla { + +class nsDisplayXULTreeColSplitterTarget final : public nsDisplayItem { + public: + nsDisplayXULTreeColSplitterTarget(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame) + : nsDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayXULTreeColSplitterTarget); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULTreeColSplitterTarget) + + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) override; + NS_DISPLAY_DECL_NAME("XULTreeColSplitterTarget", + TYPE_XUL_TREE_COL_SPLITTER_TARGET) +}; + +void nsDisplayXULTreeColSplitterTarget::HitTest( + nsDisplayListBuilder* aBuilder, const nsRect& aRect, HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) { + nsRect rect = aRect - ToReferenceFrame(); + // If we are in either in the first 4 pixels or the last 4 pixels, we're going + // to do something really strange. Check for an adjacent splitter. + bool left = false; + bool right = false; + if (mFrame->GetSize().width - nsPresContext::CSSPixelsToAppUnits(4) <= + rect.XMost()) { + right = true; + } else if (nsPresContext::CSSPixelsToAppUnits(4) > rect.x) { + left = true; + } + + // Swap left and right for RTL trees in order to find the correct splitter + if (mFrame->StyleVisibility()->mDirection == StyleDirection::Rtl) { + std::swap(left, right); + } + + if (left || right) { + nsIFrame* child = nsBoxFrame::SlowOrdinalGroupAwareSibling(mFrame, right); + // We are a header. Look for the correct splitter. + if (child && child->GetContent()->IsXULElement(nsGkAtoms::splitter)) { + aOutFrames->AppendElement(child); + } + } +} + +} // namespace mozilla + +void nsTreeColFrame::BuildDisplayListForChildren( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + if (!aBuilder->IsForEventDelivery()) { + nsBoxFrame::BuildDisplayListForChildren(aBuilder, aLists); + return; + } + + nsDisplayListCollection set(aBuilder); + nsBoxFrame::BuildDisplayListForChildren(aBuilder, set); + + WrapListsInRedirector(aBuilder, set, aLists); + + aLists.Content()->AppendNewToTop<nsDisplayXULTreeColSplitterTarget>(aBuilder, + this); +} diff --git a/layout/xul/tree/nsTreeColFrame.h b/layout/xul/tree/nsTreeColFrame.h new file mode 100644 index 0000000000..f2525f7599 --- /dev/null +++ b/layout/xul/tree/nsTreeColFrame.h @@ -0,0 +1,60 @@ +/* -*- 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/Attributes.h" +#include "mozilla/ComputedStyle.h" +#include "nsBoxFrame.h" + +namespace mozilla { +class PresShell; +namespace dom { +class XULTreeElement; +} +} // namespace mozilla + +nsIFrame* NS_NewTreeColFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +class nsTreeColFrame final : public nsBoxFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsTreeColFrame) + + explicit nsTreeColFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsBoxFrame(aStyle, aPresContext, kClassID) {} + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot, + PostDestroyData& aPostDestroyData) override; + + virtual void BuildDisplayListForChildren( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual void SetXULBounds(nsBoxLayoutState& aBoxLayoutState, + const nsRect& aRect, + bool aRemoveOverflowArea = false) override; + + friend nsIFrame* NS_NewTreeColFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + virtual ~nsTreeColFrame(); + + /** + * @return the tree that this column belongs to, or nullptr. + */ + mozilla::dom::XULTreeElement* GetTree(); + + /** + * Helper method that gets the TreeColumns object this column belongs to + * and calls InvalidateColumns() on it. + */ + void InvalidateColumns(bool aCanWalkFrameTree = true); +}; diff --git a/layout/xul/tree/nsTreeColumns.cpp b/layout/xul/tree/nsTreeColumns.cpp new file mode 100644 index 0000000000..209a2582b0 --- /dev/null +++ b/layout/xul/tree/nsTreeColumns.cpp @@ -0,0 +1,464 @@ +/* -*- 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) return; + + nsTreeColumn* currCol = nullptr; + + // Enumerate the columns in visible order + CSSOrderAwareFrameIterator iter( + colFrame, FrameChildListID::Principal, + CSSOrderAwareFrameIterator::ChildFilter::IncludeAll, + CSSOrderAwareFrameIterator::OrderState::Unknown, + CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup); + 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..9f2d7989db --- /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 an nsTreeColFrame is destroyed. + * + * 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..7915b3feb2 --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.cpp @@ -0,0 +1,723 @@ +/* -*- 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; + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + asyncDispatcher->RunDOMEventWhenSafe(); + 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..108d808aab --- /dev/null +++ b/layout/xul/tree/nsTreeStyleCache.cpp @@ -0,0 +1,102 @@ +/* -*- 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/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__ |