diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /layout/xul/tree | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
38 files changed, 9287 insertions, 0 deletions
diff --git a/layout/xul/tree/crashtests/307298-1.xhtml b/layout/xul/tree/crashtests/307298-1.xhtml new file mode 100644 index 0000000000..6c04a01321 --- /dev/null +++ b/layout/xul/tree/crashtests/307298-1.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="var tree = document.getElementById('tree'), treeitem = document.getElementById('treeitem'); tree.parentNode.insertBefore(treeitem, tree);"> + +<tree flex="1" id="tree"> + <treecols> + <treecol id="name" label="Name" primary="true" flex="1"/> + </treecols> + + <treechildren> + <treeitem id="treeitem"> + <treerow> + <treecell label="Click the button below to crash"/> + </treerow> + </treeitem> + </treechildren> +</tree> + +</window> diff --git a/layout/xul/tree/crashtests/309732-1.xhtml b/layout/xul/tree/crashtests/309732-1.xhtml new file mode 100644 index 0000000000..a7e40b75b9 --- /dev/null +++ b/layout/xul/tree/crashtests/309732-1.xhtml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)"> + + + <script> + function boom() + { + document.documentElement.appendChild(document.getElementById("TC")); + document.documentElement.appendChild(document.getElementById("TI")); + + document.documentElement.removeAttribute("class"); + } + </script> + +<tree flex="1"> + <treecols> + <treecol label="Name"/> + </treecols> + <treechildren id="TC"> + <treeitem id="TI"> + <treerow> + <treecell label="First treecell"/> + </treerow> + </treeitem> + </treechildren> + </tree> +</window> diff --git a/layout/xul/tree/crashtests/309732-2.xhtml b/layout/xul/tree/crashtests/309732-2.xhtml new file mode 100644 index 0000000000..354c58dacf --- /dev/null +++ b/layout/xul/tree/crashtests/309732-2.xhtml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)"> + + <script> + function boom() + { + document.documentElement.appendChild(document.getElementById('TC')); + document.getElementById('TI').hidden = false; + + document.documentElement.removeAttribute("class"); + } + </script> + + + <tree flex="1"> + <treecols> + <treecol label="Name" flex="1"/> + </treecols> + <treechildren id="TC"> + <treeitem> + <treerow> + <treecell label="First treecell"/> + </treerow> + </treeitem> + </treechildren> + </tree> + <treeitem id="TI" hidden="true"/> +</window> diff --git a/layout/xul/tree/crashtests/366583-1.xhtml b/layout/xul/tree/crashtests/366583-1.xhtml new file mode 100644 index 0000000000..fd12709905 --- /dev/null +++ b/layout/xul/tree/crashtests/366583-1.xhtml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="boom1();" + class="reftest-wait"> + +<script> + +var tree; + +function boom1() +{ + tree = document.getElementById("tree"); + tree.style.position = "fixed"; + setTimeout(boom2, 30); +} + +function boom2() +{ + tree.style.overflow = "visible"; + document.documentElement.removeAttribute("class"); +} +</script> + +<tree rows="6" id="tree" style="display: list-item; overflow: auto; visibility: collapse;"> + <treecols> + <treecol id="firstname" label="First Name" primary="true" style="-moz-box-flex: 3"/> + <treecol id="lastname" label="Last Name" style="-moz-box-flex: 7"/> + </treecols> + + <treechildren> + <treeitem container="true" open="true"> + <treerow> + <treecell label="Foo"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + +</window> diff --git a/layout/xul/tree/crashtests/380217-1.xhtml b/layout/xul/tree/crashtests/380217-1.xhtml new file mode 100644 index 0000000000..251b3c450d --- /dev/null +++ b/layout/xul/tree/crashtests/380217-1.xhtml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="document.documentElement.style.content = '\'a\'';"> + +<html:style> +* { position: fixed; } +*:not(style) { + /* At the time this testcase was added, the above `float` styling would + have automatically forced "display:block" for these elements, so we + should preserve that styling to preserve the integrity of the crashtest + since blockification behavior for -moz-box is changing. */ + display: block; +} +</html:style> + +<tree rows="6"> + <treecols> + <treecol id="firstname" label="First Name" primary="true"/> + </treecols> + <treechildren> + <treeitem> + <treerow> + <treecell/> + </treerow> + </treeitem> + </treechildren> +</tree> + +</window> diff --git a/layout/xul/tree/crashtests/382444-1-inner.html b/layout/xul/tree/crashtests/382444-1-inner.html new file mode 100644 index 0000000000..01805e6b34 --- /dev/null +++ b/layout/xul/tree/crashtests/382444-1-inner.html @@ -0,0 +1,15 @@ +<html>
+<head>
+<title>Testcase bug - Crash [@ nsINodeInfo::Equals] with underflow event, tree stuff and removing window</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%3E%0A%3Ctree%20style%3D%22overflow%3A%20auto%3B%20display%3A%20-moz-inline-box%3B%22%3E%0A%3Ctreeitem%20style%3D%22overflow%3A%20scroll%3B%20display%3A%20table-cell%3B%22%3E%0A%3Ctreechildren%20style%3D%22%20display%3A%20table-row%3B%22%3E%0A%3Ctreeitem%20id%3D%22a%22%20style%3D%22display%3A%20table-cell%3B%22%3E%0A%3C/treeitem%3E%0A%3C/treechildren%3E%0A%3C/treeitem%3E%0A%0A%3C/tree%3E%0A%0A%3Cscript%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0Afunction%20doe%28%29%20%7B%0Adocument.getElementById%28%27a%27%29.parentNode.removeChild%28document.getElementById%28%27a%27%29%29%3B%0A%7D%0AsetTimeout%28doe%2C%20100%29%3B%0Adocument.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0Awindow.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/window%3E" id="content"></iframe>
+
+<script>
+function doe() {
+window.location.reload();
+}
+setTimeout(doe, 500);
+</script>
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/382444-1.html b/layout/xul/tree/crashtests/382444-1.html new file mode 100644 index 0000000000..8926cf16d7 --- /dev/null +++ b/layout/xul/tree/crashtests/382444-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 500); +</script> +<body> +<iframe src="382444-1-inner.html"></iframe> +</body> +</html> diff --git a/layout/xul/tree/crashtests/391178-1.xhtml b/layout/xul/tree/crashtests/391178-1.xhtml new file mode 100644 index 0000000000..0f4b16cd99 --- /dev/null +++ b/layout/xul/tree/crashtests/391178-1.xhtml @@ -0,0 +1,41 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script> + +var ccc; + +function boom() +{ + var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var hbox = document.createElementNS(XUL_NS, 'hbox'); + var tree = document.createElementNS(XUL_NS, 'tree'); + var treecol = document.createElementNS(XUL_NS, 'treecol'); + + ccc = document.getElementById("ccc"); + + ccc.style.position = "fixed"; + + hbox.appendChild(treecol); + tree.appendChild(hbox); + ccc.appendChild(tree); + + setTimeout(boom2, 200); +} + +function boom2() +{ + ccc.style.position = ""; + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body onload="boom();"> + +<div id="ccc"> +</div> + +</body> +</html> diff --git a/layout/xul/tree/crashtests/391178-2.xhtml b/layout/xul/tree/crashtests/391178-2.xhtml new file mode 100644 index 0000000000..423b5d1bfe --- /dev/null +++ b/layout/xul/tree/crashtests/391178-2.xhtml @@ -0,0 +1,20 @@ +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" class="reftest-wait"> + +<tree id="a" style="display: block; position: fixed;"> + <box style=" display: block; position: fixed;"> + <treecol style=" display: -moz-box;"/> + </box> + <box style="display: block; position: fixed;"> + <treechildren style="display: block; position: absolute;"/> + </box> +</tree> + +<script xmlns="http://www.w3.org/1999/xhtml"> +function removestyles(){ + document.getElementById('a').removeAttribute('style'); + document.documentElement.removeAttribute("class"); +} +setTimeout(removestyles, 100); +</script> +</window> diff --git a/layout/xul/tree/crashtests/393665-1.xhtml b/layout/xul/tree/crashtests/393665-1.xhtml new file mode 100644 index 0000000000..6fb5ec0c9e --- /dev/null +++ b/layout/xul/tree/crashtests/393665-1.xhtml @@ -0,0 +1,3 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <treechildren style="display: block" /> +</window> diff --git a/layout/xul/tree/crashtests/399227-1.xhtml b/layout/xul/tree/crashtests/399227-1.xhtml new file mode 100644 index 0000000000..3ae4dfa764 --- /dev/null +++ b/layout/xul/tree/crashtests/399227-1.xhtml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)"> + + + <script> + function boom() + { + var tree = document.getElementById("thetree"); + var selection = tree.view.selection; + + selection.select(0); + tree.parentNode.removeChild(tree); + + // This is expected to throw an error (it used to crash). + try { + selection.rangedSelect(1, 1, false); + } + catch (ex) {} + + document.documentElement.removeAttribute("class"); + } + </script> + +<tree flex="1" id="thetree"> + <treecols> + <treecol label="Name"/> + </treecols> + <treechildren id="TC"> + <treeitem id="TI1"> + <treerow> + <treecell label="First treecell"/> + </treerow> + </treeitem> + <treeitem id="TI2"> + <treerow> + <treecell label="Second treecell"/> + </treerow> + </treeitem> + </treechildren> + </tree> +</window> diff --git a/layout/xul/tree/crashtests/399692-1.xhtml b/layout/xul/tree/crashtests/399692-1.xhtml new file mode 100644 index 0000000000..97eec26742 --- /dev/null +++ b/layout/xul/tree/crashtests/399692-1.xhtml @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> +</head> +<body> + +<xul:treechildren style="display: inline;" /> + +</body> +</html> diff --git a/layout/xul/tree/crashtests/399715-1.xhtml b/layout/xul/tree/crashtests/399715-1.xhtml new file mode 100644 index 0000000000..ea0a20cfa2 --- /dev/null +++ b/layout/xul/tree/crashtests/399715-1.xhtml @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<body style="float: right;" onload="document.body.style.cssFloat = '';"> + +<xul:tree><xul:hbox><xul:treecol /></xul:hbox></xul:tree> + +</body> +</html> diff --git a/layout/xul/tree/crashtests/409807-1.xhtml b/layout/xul/tree/crashtests/409807-1.xhtml new file mode 100644 index 0000000000..a3af3da41b --- /dev/null +++ b/layout/xul/tree/crashtests/409807-1.xhtml @@ -0,0 +1,25 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="boom();"> + +<script type="text/javascript"> + +function boom() +{ + var tree = document.getElementById("tree"); + var tc = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "treechildren"); + + document.addEventListener("DOMAttrModified", m, false); + + tree.appendChild(tc); + + function m() + { + document.removeEventListener("DOMAttrModified", m, false); + tree.removeChild(tc); + } +} + +</script> + +<tree id="tree" /> + +</window> diff --git a/layout/xul/tree/crashtests/414170-1.xhtml b/layout/xul/tree/crashtests/414170-1.xhtml new file mode 100644 index 0000000000..82ea63bcfd --- /dev/null +++ b/layout/xul/tree/crashtests/414170-1.xhtml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="boom();"> + +<script type="text/javascript"> + +function boom() +{ + var option = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); + document.getElementById("tc").appendChild(option); +} + +</script> + +<tree><treechildren id="tc"><hbox/></treechildren></tree> + +</window> diff --git a/layout/xul/tree/crashtests/479931-1.xhtml b/layout/xul/tree/crashtests/479931-1.xhtml new file mode 100644 index 0000000000..458a192501 --- /dev/null +++ b/layout/xul/tree/crashtests/479931-1.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> +<script type="text/javascript"> + +function boom() +{ + var o = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); + var q = document.getElementById("q"); + q.appendChild(o); +} + +</script> +</head> +<body onload="boom();"> + +<xul:tree><xul:treechildren id="q"><div/></xul:treechildren></xul:tree> + +</body> +</html> diff --git a/layout/xul/tree/crashtests/585815-iframe.xhtml b/layout/xul/tree/crashtests/585815-iframe.xhtml new file mode 100644 index 0000000000..90c20fca80 --- /dev/null +++ b/layout/xul/tree/crashtests/585815-iframe.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="setInterval(run, 25)"> + +<tree style="-moz-box-flex: 1" rows="2"> + <treecols> + <treecol id="sender" label="Sender" style="-moz-box-flex: 1"/> + <treecol id="subject" label="Subject" style="-moz-box-flex: 2"/> + </treecols> + <treechildren> + <treeitem> + <treerow> + <treecell label="joe@somewhere.com"/> + <treecell label="Top secret plans"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="mel@whereever.com"/> + <treecell label="Let's do lunch"/> + </treerow> + </treeitem> + </treechildren> +</tree> + +<script type="text/javascript"><![CDATA[ +function run() { + var tree = document.getElementsByTagName("tree")[0]; + var sel = tree.view.selection; + sel.rangedSelect(0, 0, true); + sel.rangedSelect(1000, 1001, true); + sel.adjustSelection(1, 0x7fffffff); +} +]]></script> + +</window> diff --git a/layout/xul/tree/crashtests/585815.html b/layout/xul/tree/crashtests/585815.html new file mode 100644 index 0000000000..7c3b27f6aa --- /dev/null +++ b/layout/xul/tree/crashtests/585815.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"><head> + <meta charset="utf-8"> + <title>Testcase for bug 585815</title> +<script> +function done() +{ + document.documentElement.removeAttribute("class"); +} +</script> +</head> +<body onload="setTimeout(done,1000)"> + +<iframe src="585815-iframe.xhtml"></iframe> + + +</body> +</html> diff --git a/layout/xul/tree/crashtests/601427.html b/layout/xul/tree/crashtests/601427.html new file mode 100644 index 0000000000..2a2999052e --- /dev/null +++ b/layout/xul/tree/crashtests/601427.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<script> + +var onPaintFunctions = +[ + function() { document.documentElement.style.MozAppearance = "treeheadersortarrow"; }, + function() { document.documentElement.style.position = "fixed"; }, + function() { document.documentElement.removeAttribute("class"); } +]; + +var i = 0; + +function advance() +{ + var f = onPaintFunctions[i++]; + if (f) + f(); +} + +function start() +{ + window.addEventListener("MozAfterPaint", advance, true); + advance(); +} + +window.addEventListener("load", start); + +</script> +</html> diff --git a/layout/xul/tree/crashtests/730441-3.xhtml b/layout/xul/tree/crashtests/730441-3.xhtml new file mode 100644 index 0000000000..c3fe199a83 --- /dev/null +++ b/layout/xul/tree/crashtests/730441-3.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<!-- +###!!! ASSERTION: You can't dereference a NULL nsCOMPtr with operator->().: 'mRawPtr != 0', file ../../../../dist/include/nsCOMPtr.h, line 796 + +Program received signal SIGSEGV, Segmentation fault. +0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571 +571 boxObject->GetElement(getter_AddRefs(element)); +(gdb) bt 3 +#0 0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571 +#1 0xb736c76f in NS_InvokeByIndex_P () at xpcom/reflect/xptcall/md/unix/xptcinvoke_gcc_x86_unix.cpp:69 +#2 0xb6171901 in XPCWrappedNative::CallMethod (ccx=..., mode=XPCWrappedNative::CALL_METHOD) + at js/src/xpconnect/src/xpcwrappednative.cpp:2722 +(More stack frames follow...) +(gdb) list 566 +561 nsTreeContentView::SetTree(nsITreeBoxObject* aTree) +562 { +563 ClearRows(); +564 +565 mBoxObject = aTree; +566 +567 if (aTree && !mRoot) { +568 // Get our root element +569 nsCOMPtr<nsIBoxObject> boxObject = do_QueryInterface(mBoxObject); +570 nsCOMPtr<Element> element; +571 boxObject->GetElement(getter_AddRefs(element)); +(gdb) p boxObject +$16 = {mRawPtr = 0x0} + +|aTree| does not implement |nsIBoxObject|, so |do_QueryInterface(mBoxObject)| +returns null. Then we have |null->GetElement()|. +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="document.getElementById('tree').view.setTree({});"> +<tree id="tree"> + <treechildren/> +</tree> +</window> + diff --git a/layout/xul/tree/crashtests/crashtests.list b/layout/xul/tree/crashtests/crashtests.list new file mode 100644 index 0000000000..81f6ab7e6c --- /dev/null +++ b/layout/xul/tree/crashtests/crashtests.list @@ -0,0 +1,18 @@ +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/307298-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-2.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/366583-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/380217-1.xhtml +load 382444-1.html +load 391178-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/391178-2.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/393665-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/399227-1.xhtml +load 399692-1.xhtml +load 399715-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/409807-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/414170-1.xhtml +load 479931-1.xhtml +load 585815.html +pref(widget.windows.window_occlusion_tracking.enabled,false) load 601427.html # Bug 1819154 +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/730441-3.xhtml diff --git a/layout/xul/tree/moz.build b/layout/xul/tree/moz.build new file mode 100644 index 0000000000..f99b7bd41c --- /dev/null +++ b/layout/xul/tree/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +XPIDL_SOURCES += [ + "nsITreeSelection.idl", + "nsITreeView.idl", +] + +XPIDL_MODULE = "layout_xul_tree" + +EXPORTS += [ + "nsTreeColumns.h", + "nsTreeUtils.h", +] + +UNIFIED_SOURCES += [ + "nsTreeBodyFrame.cpp", + "nsTreeColumns.cpp", + "nsTreeContentView.cpp", + "nsTreeImageListener.cpp", + "nsTreeSelection.cpp", + "nsTreeStyleCache.cpp", + "nsTreeUtils.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "..", + "../../base", + "../../forms", + "../../generic", + "../../painting", + "../../style", + "/dom/base", + "/dom/xul", +] diff --git a/layout/xul/tree/nsITreeSelection.idl b/layout/xul/tree/nsITreeSelection.idl new file mode 100644 index 0000000000..d265b639ee --- /dev/null +++ b/layout/xul/tree/nsITreeSelection.idl @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl XULTreeElement; + +[scriptable, uuid(ab6fe746-300b-4ab4-abb9-1c0e3977874c)] +interface nsITreeSelection : nsISupports +{ + /** + * The tree widget for this selection. + */ + attribute XULTreeElement tree; + + /** + * This attribute is a boolean indicating single selection. + */ + readonly attribute boolean single; + + /** + * The number of rows currently selected in this tree. + */ + readonly attribute long count; + + /** + * Indicates whether or not the row at the specified index is + * part of the selection. + */ + boolean isSelected(in long index); + + /** + * Deselect all rows and select the row at the specified index. + */ + void select(in long index); + + /** + * Perform a timed select. + */ + void timedSelect(in long index, in long delay); + + /** + * Toggle the selection state of the row at the specified index. + */ + void toggleSelect(in long index); + + /** + * Select the range specified by the indices. If augment is true, + * then we add the range to the selection without clearing out anything + * else. If augment is false, everything is cleared except for the specified range. + */ + void rangedSelect(in long startIndex, in long endIndex, in boolean augment); + + /** + * Clears the range. + */ + void clearRange(in long startIndex, in long endIndex); + + /** + * Clears the selection. + */ + void clearSelection(); + + /** + * Selects all rows. + */ + void selectAll(); + + /** + * Iterate the selection using these methods. + */ + long getRangeCount(); + void getRangeAt(in long i, out long min, out long max); + + /** + * Can be used to invalidate the selection. + */ + void invalidateSelection(); + + /** + * Called when the row count changes to adjust selection indices. + */ + void adjustSelection(in long index, in long count); + + /** + * This attribute is a boolean indicating whether or not the + * "select" event should fire when the selection is changed using + * one of our methods. A view can use this to temporarily suppress + * the selection while manipulating all of the indices, e.g., on + * a sort. + * Note: setting this attribute to false will fire a select event. + */ + attribute boolean selectEventsSuppressed; + + /** + * The current item (the one that gets a focus rect in addition to being + * selected). + */ + attribute long currentIndex; + + /** + * The selection "pivot". This is the first item the user selected as + * part of a ranged select. + */ + readonly attribute long shiftSelectPivot; +}; + +/** + * The following interface is not scriptable and MUST NEVER BE MADE scriptable. + * Native treeselections implement it, and we use this to check whether a + * treeselection is native (and therefore suitable for use by untrusted content). + */ +[uuid(1bd59678-5cb3-4316-b246-31a91b19aabe)] +interface nsINativeTreeSelection : nsITreeSelection +{ + [noscript] void ensureNative(); +}; diff --git a/layout/xul/tree/nsITreeView.idl b/layout/xul/tree/nsITreeView.idl new file mode 100644 index 0000000000..7f2480ceaf --- /dev/null +++ b/layout/xul/tree/nsITreeView.idl @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsITreeSelection; + +webidl DataTransfer; +webidl TreeColumn; +webidl XULTreeElement; + +[scriptable, uuid(091116f0-0bdc-4b32-b9c8-c8d5a37cb088)] +interface nsITreeView : nsISupports +{ + /** + * The total number of rows in the tree (including the offscreen rows). + */ + readonly attribute long rowCount; + + /** + * The selection for this view. + */ + attribute nsITreeSelection selection; + + /** + * A whitespace delimited list of properties. For each property X the view + * gives back will cause the pseudoclasses ::-moz-tree-cell(x), + * ::-moz-tree-row(x), ::-moz-tree-twisty(x), ::-moz-tree-image(x), + * ::-moz-tree-cell-text(x). to be matched on the pseudoelement + * ::moz-tree-row. + */ + AString getRowProperties(in long index); + + /** + * A whitespace delimited list of properties for a given cell. Each + * property, x, that the view gives back will cause the pseudoclasses + * ::-moz-tree-cell(x), ::-moz-tree-row(x), ::-moz-tree-twisty(x), + * ::-moz-tree-image(x), ::-moz-tree-cell-text(x). to be matched on the + * cell. + */ + AString getCellProperties(in long row, in TreeColumn col); + + /** + * Called to get properties to paint a column background. For shading the sort + * column, etc. + */ + AString getColumnProperties(in TreeColumn col); + + /** + * Methods that can be used to test whether or not a twisty should be drawn, + * and if so, whether an open or closed twisty should be used. + */ + boolean isContainer(in long index); + boolean isContainerOpen(in long index); + boolean isContainerEmpty(in long index); + + /** + * isSeparator is used to determine if the row at index is a separator. + * A value of true will result in the tree drawing a horizontal separator. + * The tree uses the ::moz-tree-separator pseudoclass to draw the separator. + */ + boolean isSeparator(in long index); + + /** + * Specifies if there is currently a sort on any column. Used mostly by dragdrop + * to affect drop feedback. + */ + boolean isSorted(); + + const short DROP_BEFORE = -1; + const short DROP_ON = 0; + const short DROP_AFTER = 1; + /** + * Methods used by the drag feedback code to determine if a drag is allowable at + * the current location. To get the behavior where drops are only allowed on + * items, such as the mailNews folder pane, always return false when + * the orientation is not DROP_ON. + */ + boolean canDrop(in long index, in long orientation, in DataTransfer dataTransfer); + + /** + * Called when the user drops something on this view. The |orientation| param + * specifies before/on/after the given |row|. + */ + void drop(in long row, in long orientation, in DataTransfer dataTransfer); + + /** + * Methods used by the tree to draw thread lines in the tree. + * getParentIndex is used to obtain the index of a parent row. + * If there is no parent row, getParentIndex returns -1. + */ + long getParentIndex(in long rowIndex); + + /** + * hasNextSibling is used to determine if the row at rowIndex has a nextSibling + * that occurs *after* the index specified by afterIndex. Code that is forced + * to march down the view looking at levels can optimize the march by starting + * at afterIndex+1. + */ + boolean hasNextSibling(in long rowIndex, in long afterIndex); + + /** + * The level is an integer value that represents + * the level of indentation. It is multiplied by the width specified in the + * :moz-tree-indentation pseudoelement to compute the exact indendation. + */ + long getLevel(in long index); + + /** + * The image path for a given cell. For defining an icon for a cell. + * If the empty string is returned, the :moz-tree-image pseudoelement + * will be used. + */ + AString getImageSrc(in long row, in TreeColumn col); + + /** + * The value for a given cell. This method is only called for columns + * of type other than |text|. + */ + AString getCellValue(in long row, in TreeColumn col); + + /** + * The text for a given cell. If a column consists only of an image, then + * the empty string is returned. + */ + AString getCellText(in long row, in TreeColumn col); + + /** + * Called during initialization to link the view to the front end box object. + */ + void setTree(in XULTreeElement tree); + + /** + * Called on the view when an item is opened or closed. + */ + void toggleOpenState(in long index); + + /** + * Called on the view when a header is clicked. + */ + void cycleHeader(in TreeColumn col); + + /** + * Should be called from a XUL onselect handler whenever the selection changes. + */ + [binaryname(SelectionChangedXPCOM)] + void selectionChanged(); + + /** + * Called on the view when a cell in a non-selectable cycling column (e.g., unread/flag/etc.) is clicked. + */ + void cycleCell(in long row, in TreeColumn col); + + /** + * isEditable is called to ask the view if the cell contents are editable. + * A value of true will result in the tree popping up a text field when + * the user tries to inline edit the cell. + */ + boolean isEditable(in long row, in TreeColumn col); + + /** + * setCellValue is called when the value of the cell has been set by the user. + * This method is only called for columns of type other than |text|. + */ + void setCellValue(in long row, in TreeColumn col, in AString value); + + /** + * setCellText is called when the contents of the cell have been edited by the user. + */ + void setCellText(in long row, in TreeColumn col, in AString value); +}; diff --git a/layout/xul/tree/nsTreeBodyFrame.cpp b/layout/xul/tree/nsTreeBodyFrame.cpp new file mode 100644 index 0000000000..c7e8272aa0 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.cpp @@ -0,0 +1,4363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SimpleXULLeafFrame.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/Likely.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/intl/Segmenter.h" + +#include "gfxUtils.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsFontMetrics.h" +#include "nsITreeView.h" +#include "nsPresContext.h" +#include "nsNameSpaceManager.h" + +#include "nsTreeBodyFrame.h" +#include "nsTreeSelection.h" +#include "nsTreeImageListener.h" + +#include "nsGkAtoms.h" +#include "nsCSSAnonBoxes.h" + +#include "gfxContext.h" +#include "nsIContent.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/intl/Segmenter.h" +#include "nsCSSRendering.h" +#include "nsString.h" +#include "nsContainerFrame.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsVariant.h" +#include "nsWidgetsCID.h" +#include "nsIFrameInlines.h" +#include "nsTreeContentView.h" +#include "nsTreeUtils.h" +#include "nsStyleConsts.h" +#include "nsITheme.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsContentUtils.h" +#include "nsLayoutUtils.h" +#include "nsIScrollableFrame.h" +#include "nsDisplayList.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/TreeColumnBinding.h" +#include <algorithm> +#include "ScrollbarActivity.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +# include "nsIWritablePropertyBag2.h" +#endif +#include "nsBidiUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::layout; + +enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto }; + +// FIXME: Maybe unify with MiddleCroppingBlockFrame? +static void CropStringForWidth(nsAString& aText, gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, nscoord aWidth, + CroppingStyle aCropType) { + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // See if the width is even smaller than the ellipsis + // If so, clear the text completely. + const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); + aFontMetrics.SetTextRunRTL(false); + nscoord ellipsisWidth = + nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget); + + if (ellipsisWidth > aWidth) { + aText.Truncate(0); + return; + } + if (ellipsisWidth == aWidth) { + aText.Assign(kEllipsis); + return; + } + + // We will be drawing an ellipsis, thank you very much. + // Subtract out the required width of the ellipsis. + // This is the total remaining width we have to play with. + aWidth -= ellipsisWidth; + + using mozilla::intl::GraphemeClusterBreakIteratorUtf16; + using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16; + + // Now we crop. This is quite basic: it will not be really accurate in the + // presence of complex scripts with contextual shaping, etc., as it measures + // each grapheme cluster in isolation, not in its proper context. + switch (aCropType) { + case CropAuto: + case CropNone: + case CropRight: { + const Span text(aText); + GraphemeClusterBreakIteratorUtf16 iter(text); + uint32_t pos = 0; + nscoord totalWidth = 0; + + while (Maybe<uint32_t> nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(pos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos < aText.Length()) { + aText.Replace(pos, aText.Length() - pos, kEllipsis); + } + } break; + + case CropLeft: { + const Span text(aText); + GraphemeClusterBreakReverseIteratorUtf16 iter(text); + uint32_t pos = text.Length(); + nscoord totalWidth = 0; + + // nextPos is decreasing since we use a reverse iterator. + while (Maybe<uint32_t> nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, pos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos > 0) { + aText.Replace(0, pos, kEllipsis); + } + } break; + + case CropCenter: { + const Span text(aText); + nscoord totalWidth = 0; + GraphemeClusterBreakIteratorUtf16 leftIter(text); + GraphemeClusterBreakReverseIteratorUtf16 rightIter(text); + uint32_t leftPos = 0; + uint32_t rightPos = text.Length(); + + while (leftPos < rightPos) { + Maybe<uint32_t> nextPos = leftIter.Next(); + nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + leftPos = *nextPos; + totalWidth += charWidth; + + if (leftPos >= rightPos) { + break; + } + + nextPos = rightIter.Next(); + charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + rightPos = *nextPos; + totalWidth += charWidth; + } + + if (leftPos < rightPos) { + aText.Replace(leftPos, rightPos - leftPos, kEllipsis); + } + } break; + } +} + +// Function that cancels all the image requests in our cache. +void nsTreeBodyFrame::CancelImageRequests() { + for (nsTreeImageCacheEntry entry : mImageCache.Values()) { + // If our imgIRequest object was registered with the refresh driver + // then we need to deregister it. + nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, + nullptr); + entry.request->UnlockImage(); + entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); + } +} + +// +// NS_NewTreeFrame +// +// Creates a new tree frame +// +nsIFrame* NS_NewTreeBodyFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsTreeBodyFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTreeBodyFrame) + +NS_QUERYFRAME_HEAD(nsTreeBodyFrame) + NS_QUERYFRAME_ENTRY(nsIScrollbarMediator) + NS_QUERYFRAME_ENTRY(nsTreeBodyFrame) +NS_QUERYFRAME_TAIL_INHERITING(SimpleXULLeafFrame) + +// Constructor +nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SimpleXULLeafFrame(aStyle, aPresContext, kClassID), + mImageCache(), + mTopRowIndex(0), + mPageLength(0), + mHorzPosition(0), + mOriginalHorzWidth(-1), + mHorzWidth(0), + mAdjustWidth(0), + mRowHeight(0), + mIndentation(0), + mUpdateBatchNest(0), + mRowCount(0), + mMouseOverRow(-1), + mFocused(false), + mHasFixedRowCount(false), + mVerticalOverflow(false), + mHorizontalOverflow(false), + mReflowCallbackPosted(false), + mCheckingOverflow(false) { + mColumns = new nsTreeColumns(this); +} + +// Destructor +nsTreeBodyFrame::~nsTreeBodyFrame() { + CancelImageRequests(); + DetachImageListeners(); +} + +static void GetBorderPadding(ComputedStyle* aStyle, nsMargin& aMargin) { + aMargin.SizeTo(0, 0, 0, 0); + aStyle->StylePadding()->GetPadding(aMargin); + aMargin += aStyle->StyleBorder()->GetComputedBorder(); +} + +static void AdjustForBorderPadding(ComputedStyle* aStyle, nsRect& aRect) { + nsMargin borderPadding(0, 0, 0, 0); + GetBorderPadding(aStyle, borderPadding); + aRect.Deflate(borderPadding); +} + +void nsTreeBodyFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + SimpleXULLeafFrame::Init(aContent, aParent, aPrevInFlow); + + mIndentation = GetIndentation(); + mRowHeight = GetRowHeight(); + + // Call GetBaseElement so that mTree is assigned. + RefPtr<XULTreeElement> tree(GetBaseElement()); + if (MOZ_LIKELY(tree)) { + nsAutoString rows; + if (tree->GetAttr(nsGkAtoms::rows, rows)) { + nsresult err; + mPageLength = rows.ToInteger(&err); + mHasFixedRowCount = true; + } + } + + if (PresContext()->UseOverlayScrollbars()) { + mScrollbarActivity = + new ScrollbarActivity(static_cast<nsIScrollbarMediator*>(this)); + } +} + +void nsTreeBodyFrame::Destroy(DestroyContext& aContext) { + if (mScrollbarActivity) { + mScrollbarActivity->Destroy(); + mScrollbarActivity = nullptr; + } + + mScrollEvent.Revoke(); + // Make sure we cancel any posted callbacks. + if (mReflowCallbackPosted) { + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + } + + if (mColumns) mColumns->SetTree(nullptr); + + RefPtr tree = mTree; + + if (nsCOMPtr<nsITreeView> view = std::move(mView)) { + nsCOMPtr<nsITreeSelection> sel; + view->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + view->SetTree(nullptr); + } + + // Make this call now because view->SetTree can run js which can undo this + // call. + if (tree) { + tree->BodyDestroyed(mTopRowIndex); + } + if (mTree && mTree != tree) { + mTree->BodyDestroyed(mTopRowIndex); + } + + SimpleXULLeafFrame::Destroy(aContext); +} + +void nsTreeBodyFrame::EnsureView() { + if (mView) { + return; + } + + if (PresShell()->IsReflowLocked()) { + if (!mReflowCallbackPosted) { + mReflowCallbackPosted = true; + PresShell()->PostReflowCallback(this); + } + return; + } + + AutoWeakFrame weakFrame(this); + + RefPtr<XULTreeElement> tree = GetBaseElement(); + if (!tree) { + return; + } + nsCOMPtr<nsITreeView> treeView = tree->GetView(); + if (!treeView || !weakFrame.IsAlive()) { + return; + } + int32_t rowIndex = tree->GetCachedTopVisibleRow(); + + // Set our view. + SetView(treeView); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + // Scroll to the given row. + // XXX is this optimal if we haven't laid out yet? + ScrollToRow(rowIndex); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); +} + +void nsTreeBodyFrame::ManageReflowCallback() { + const nscoord horzWidth = CalcHorzWidth(GetScrollParts()); + if (!mReflowCallbackPosted) { + if (!mLastReflowRect || !mLastReflowRect->IsEqualEdges(mRect) || + mHorzWidth != horzWidth) { + PresShell()->PostReflowCallback(this); + mReflowCallbackPosted = true; + mOriginalHorzWidth = mHorzWidth; + } + } else if (mHorzWidth != horzWidth && mOriginalHorzWidth == horzWidth) { + // FIXME(emilio): This doesn't seem sound to me, if the rect changes in the + // block axis. + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + mOriginalHorzWidth = -1; + } + mLastReflowRect = Some(mRect); + mHorzWidth = horzWidth; +} + +nscoord nsTreeBodyFrame::GetIntrinsicBSize() { + return mHasFixedRowCount ? mRowHeight * mPageLength : 0; +} + +void nsTreeBodyFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + ManageReflowCallback(); + SimpleXULLeafFrame::DidReflow(aPresContext, aReflowInput); +} + +bool nsTreeBodyFrame::ReflowFinished() { + if (!mView) { + AutoWeakFrame weakFrame(this); + EnsureView(); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + } + if (mView) { + CalcInnerBox(); + ScrollParts parts = GetScrollParts(); + mHorzWidth = CalcHorzWidth(parts); + if (!mHasFixedRowCount) { + mPageLength = + (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; + } + + int32_t lastPageTopRow = std::max(0, mRowCount - mPageLength); + if (mTopRowIndex > lastPageTopRow) + ScrollToRowInternal(parts, lastPageTopRow); + + XULTreeElement* treeContent = GetBaseElement(); + if (treeContent && treeContent->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::keepcurrentinview, + nsGkAtoms::_true, eCaseMatters)) { + // make sure that the current selected item is still + // visible after the tree changes size. + if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) { + int32_t currentIndex; + sel->GetCurrentIndex(¤tIndex); + if (currentIndex != -1) { + EnsureRowIsVisibleInternal(parts, currentIndex); + } + } + } + + if (!FullScrollbarsUpdate(false)) { + return false; + } + } + + mReflowCallbackPosted = false; + return false; +} + +void nsTreeBodyFrame::ReflowCallbackCanceled() { + mReflowCallbackPosted = false; +} + +nsresult nsTreeBodyFrame::GetView(nsITreeView** aView) { + *aView = nullptr; + AutoWeakFrame weakFrame(this); + EnsureView(); + NS_ENSURE_STATE(weakFrame.IsAlive()); + NS_IF_ADDREF(*aView = mView); + return NS_OK; +} + +nsresult nsTreeBodyFrame::SetView(nsITreeView* aView) { + if (aView == mView) { + return NS_OK; + } + + // First clear out the old view. + nsCOMPtr<nsITreeView> oldView = std::move(mView); + if (oldView) { + AutoWeakFrame weakFrame(this); + + nsCOMPtr<nsITreeSelection> sel; + oldView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + oldView->SetTree(nullptr); + + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Only reset the top row index and delete the columns if we had an old + // non-null view. + mTopRowIndex = 0; + } + + // Tree, meet the view. + mView = aView; + + // Changing the view causes us to refetch our data. This will + // necessarily entail a full invalidation of the tree. + Invalidate(); + + RefPtr<XULTreeElement> treeContent = GetBaseElement(); + if (treeContent) { +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->TreeViewChanged(PresContext()->GetPresShell(), treeContent, + mView); + } +#endif // #ifdef ACCESSIBILITY + FireDOMEvent(u"TreeViewChanged"_ns, treeContent); + } + + if (aView) { + // Give the view a new empty selection object to play with, but only if it + // doesn't have one already. + nsCOMPtr<nsITreeSelection> sel; + aView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(treeContent); + } else { + NS_NewTreeSelection(treeContent, getter_AddRefs(sel)); + aView->SetSelection(sel); + } + + // View, meet the tree. + AutoWeakFrame weakFrame(this); + aView->SetTree(treeContent); + NS_ENSURE_STATE(weakFrame.IsAlive()); + aView->GetRowCount(&mRowCount); + + if (!PresShell()->IsReflowLocked()) { + // The scrollbar will need to be updated. + FullScrollbarsUpdate(false); + } else if (!mReflowCallbackPosted) { + mReflowCallbackPosted = true; + PresShell()->PostReflowCallback(this); + } + } + + return NS_OK; +} + +already_AddRefed<nsITreeSelection> nsTreeBodyFrame::GetSelection() const { + nsCOMPtr<nsITreeSelection> sel; + if (nsCOMPtr<nsITreeView> view = GetExistingView()) { + view->GetSelection(getter_AddRefs(sel)); + } + return sel.forget(); +} + +nsresult nsTreeBodyFrame::SetFocused(bool aFocused) { + if (mFocused != aFocused) { + mFocused = aFocused; + if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) { + sel->InvalidateSelection(); + } + } + return NS_OK; +} + +nsresult nsTreeBodyFrame::GetTreeBody(Element** aElement) { + // NS_ASSERTION(mContent, "no content, see bug #104878"); + if (!mContent) return NS_ERROR_NULL_POINTER; + + RefPtr<Element> element = mContent->AsElement(); + element.forget(aElement); + return NS_OK; +} + +int32_t nsTreeBodyFrame::RowHeight() const { + return nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); +} + +int32_t nsTreeBodyFrame::RowWidth() { + return nsPresContext::AppUnitsToIntCSSPixels(CalcHorzWidth(GetScrollParts())); +} + +int32_t nsTreeBodyFrame::GetHorizontalPosition() const { + return nsPresContext::AppUnitsToIntCSSPixels(mHorzPosition); +} + +Maybe<CSSIntRegion> nsTreeBodyFrame::GetSelectionRegion() { + if (!mView) { + return Nothing(); + } + + AutoWeakFrame wf(this); + nsCOMPtr<nsITreeSelection> selection = GetSelection(); + if (!selection || !wf.IsAlive()) { + return Nothing(); + } + + RefPtr<nsPresContext> presContext = PresContext(); + nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel()); + + nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); + nsPoint origin = GetOffsetTo(rootFrame); + + CSSIntRegion region; + + // iterate through the visible rows and add the selected ones to the + // drag region + int32_t x = nsPresContext::AppUnitsToIntCSSPixels(origin.x); + int32_t y = nsPresContext::AppUnitsToIntCSSPixels(origin.y); + int32_t top = y; + int32_t end = LastVisibleRow(); + int32_t rowHeight = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + for (int32_t i = mTopRowIndex; i <= end; i++) { + bool isSelected; + selection->IsSelected(i, &isSelected); + if (isSelected) { + region.OrWith(CSSIntRect(x, y, rect.width, rowHeight)); + } + y += rowHeight; + } + + // clip to the tree boundary in case one row extends past it + region.AndWith(CSSIntRect(x, top, rect.width, rect.height)); + + return Some(region); +} + +nsresult nsTreeBodyFrame::Invalidate() { + if (mUpdateBatchNest) return NS_OK; + + InvalidateFrame(); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateColumn(nsTreeColumn* aCol) { + if (mUpdateBatchNest) return NS_OK; + + if (!aCol) return NS_ERROR_INVALID_ARG; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(-1, -1, aCol, aCol); + } +#endif // #ifdef ACCESSIBILITY + + nsRect columnRect; + nsresult rv = aCol->GetRect(this, mInnerBox.y, mInnerBox.height, &columnRect); + NS_ENSURE_SUCCESS(rv, rv); + + // When false then column is out of view + if (OffsetForHorzScroll(columnRect, true)) + InvalidateFrameWithRect(columnRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateRow(int32_t aIndex) { + if (mUpdateBatchNest) return NS_OK; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(aIndex, aIndex, nullptr, nullptr); + } +#endif // #ifdef ACCESSIBILITY + + aIndex -= mTopRowIndex; + if (aIndex < 0 || aIndex > mPageLength) return NS_OK; + + nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * aIndex, + mInnerBox.width, mRowHeight); + InvalidateFrameWithRect(rowRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateCell(int32_t aIndex, nsTreeColumn* aCol) { + if (mUpdateBatchNest) return NS_OK; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(aIndex, aIndex, aCol, aCol); + } +#endif // #ifdef ACCESSIBILITY + + aIndex -= mTopRowIndex; + if (aIndex < 0 || aIndex > mPageLength) return NS_OK; + + if (!aCol) return NS_ERROR_INVALID_ARG; + + nsRect cellRect; + nsresult rv = aCol->GetRect(this, mInnerBox.y + mRowHeight * aIndex, + mRowHeight, &cellRect); + NS_ENSURE_SUCCESS(rv, rv); + + if (OffsetForHorzScroll(cellRect, true)) InvalidateFrameWithRect(cellRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateRange(int32_t aStart, int32_t aEnd) { + if (mUpdateBatchNest) return NS_OK; + + if (aStart == aEnd) return InvalidateRow(aStart); + + int32_t last = LastVisibleRow(); + if (aStart > aEnd || aEnd < mTopRowIndex || aStart > last) return NS_OK; + + if (aStart < mTopRowIndex) aStart = mTopRowIndex; + + if (aEnd > last) aEnd = last; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + int32_t end = + mRowCount > 0 ? ((mRowCount <= aEnd) ? mRowCount - 1 : aEnd) : 0; + FireInvalidateEvent(aStart, end, nullptr, nullptr); + } +#endif // #ifdef ACCESSIBILITY + + nsRect rangeRect(mInnerBox.x, + mInnerBox.y + mRowHeight * (aStart - mTopRowIndex), + mInnerBox.width, mRowHeight * (aEnd - aStart + 1)); + InvalidateFrameWithRect(rangeRect); + + return NS_OK; +} + +static void FindScrollParts(nsIFrame* aCurrFrame, + nsTreeBodyFrame::ScrollParts* aResult) { + if (!aResult->mColumnsScrollFrame) { + nsIScrollableFrame* f = do_QueryFrame(aCurrFrame); + if (f) { + aResult->mColumnsFrame = aCurrFrame; + aResult->mColumnsScrollFrame = f; + } + } + + if (nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame)) { + if (!sf->IsHorizontal()) { + if (!aResult->mVScrollbar) { + aResult->mVScrollbar = sf; + } + } else { + if (!aResult->mHScrollbar) { + aResult->mHScrollbar = sf; + } + } + // don't bother searching inside a scrollbar + return; + } + + nsIFrame* child = aCurrFrame->PrincipalChildList().FirstChild(); + while (child && !child->GetContent()->IsRootOfNativeAnonymousSubtree() && + (!aResult->mVScrollbar || !aResult->mHScrollbar || + !aResult->mColumnsScrollFrame)) { + FindScrollParts(child, aResult); + child = child->GetNextSibling(); + } +} + +nsTreeBodyFrame::ScrollParts nsTreeBodyFrame::GetScrollParts() { + ScrollParts result = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + XULTreeElement* tree = GetBaseElement(); + if (nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr) { + // The way we do this, searching through the entire frame subtree, is pretty + // dumb! We should know where these frames are. + FindScrollParts(treeFrame, &result); + if (result.mHScrollbar) { + result.mHScrollbar->SetScrollbarMediatorContent(GetContent()); + result.mHScrollbarContent = result.mHScrollbar->GetContent()->AsElement(); + } + if (result.mVScrollbar) { + result.mVScrollbar->SetScrollbarMediatorContent(GetContent()); + result.mVScrollbarContent = result.mVScrollbar->GetContent()->AsElement(); + } + } + return result; +} + +void nsTreeBodyFrame::UpdateScrollbars(const ScrollParts& aParts) { + nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + + AutoWeakFrame weakFrame(this); + + if (aParts.mVScrollbar) { + nsAutoString curPos; + curPos.AppendInt(mTopRowIndex * rowHeightAsPixels); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, + curPos, true); + // 'this' might be deleted here + } + + if (weakFrame.IsAlive() && aParts.mHScrollbar) { + nsAutoString curPos; + curPos.AppendInt(mHorzPosition); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, + curPos, true); + // 'this' might be deleted here + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } +} + +void nsTreeBodyFrame::CheckOverflow(const ScrollParts& aParts) { + bool verticalOverflowChanged = false; + bool horizontalOverflowChanged = false; + + if (!mVerticalOverflow && mRowCount > mPageLength) { + mVerticalOverflow = true; + verticalOverflowChanged = true; + } else if (mVerticalOverflow && mRowCount <= mPageLength) { + mVerticalOverflow = false; + verticalOverflowChanged = true; + } + + if (aParts.mColumnsFrame) { + nsRect bounds = aParts.mColumnsFrame->GetRect(); + if (bounds.width != 0) { + /* Ignore overflows that are less than half a pixel. Yes these happen + all over the place when flex boxes are compressed real small. + Probably a result of a rounding errors somewhere in the layout code. */ + bounds.width += nsPresContext::CSSPixelsToAppUnits(0.5f); + if (!mHorizontalOverflow && bounds.width < mHorzWidth) { + mHorizontalOverflow = true; + horizontalOverflowChanged = true; + } else if (mHorizontalOverflow && bounds.width >= mHorzWidth) { + mHorizontalOverflow = false; + horizontalOverflowChanged = true; + } + } + } + + if (!horizontalOverflowChanged && !verticalOverflowChanged) { + return; + } + + AutoWeakFrame weakFrame(this); + + RefPtr<nsPresContext> presContext = PresContext(); + RefPtr<mozilla::PresShell> presShell = presContext->GetPresShell(); + nsCOMPtr<nsIContent> content = mContent; + + if (verticalOverflowChanged) { + InternalScrollPortEvent event( + true, mVerticalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, + nullptr); + event.mOrient = InternalScrollPortEvent::eVertical; + EventDispatcher::Dispatch(content, presContext, &event); + } + + if (horizontalOverflowChanged) { + InternalScrollPortEvent event( + true, mHorizontalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, + nullptr); + event.mOrient = InternalScrollPortEvent::eHorizontal; + EventDispatcher::Dispatch(content, presContext, &event); + } + + // The synchronous event dispatch above can trigger reflow notifications. + // Flush those explicitly now, so that we can guard against potential infinite + // recursion. See bug 905909. + if (!weakFrame.IsAlive()) { + return; + } + NS_ASSERTION(!mCheckingOverflow, + "mCheckingOverflow should not already be set"); + // Don't use AutoRestore since we want to not touch mCheckingOverflow if we + // fail the weakFrame.IsAlive() check below + mCheckingOverflow = true; + presShell->FlushPendingNotifications(FlushType::Layout); + if (!weakFrame.IsAlive()) { + return; + } + mCheckingOverflow = false; +} + +void nsTreeBodyFrame::InvalidateScrollbars(const ScrollParts& aParts, + AutoWeakFrame& aWeakColumnsFrame) { + if (mUpdateBatchNest || !mView) return; + AutoWeakFrame weakFrame(this); + + if (aParts.mVScrollbar) { + // Do Vertical Scrollbar + nsAutoString maxposStr; + + nscoord rowHeightAsPixels = + nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + + int32_t size = rowHeightAsPixels * + (mRowCount > mPageLength ? mRowCount - mPageLength : 0); + maxposStr.AppendInt(size); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, + maxposStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + // Also set our page increment and decrement. + nscoord pageincrement = mPageLength * rowHeightAsPixels; + nsAutoString pageStr; + pageStr.AppendInt(pageincrement); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::pageincrement, pageStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + } + + if (aParts.mHScrollbar && aParts.mColumnsFrame && + aWeakColumnsFrame.IsAlive()) { + // And now Horizontal scrollbar + nsRect bounds = aParts.mColumnsFrame->GetRect(); + nsAutoString maxposStr; + + maxposStr.AppendInt(mHorzWidth > bounds.width ? mHorzWidth - bounds.width + : 0); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, + maxposStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + nsAutoString pageStr; + pageStr.AppendInt(bounds.width); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::pageincrement, pageStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + pageStr.Truncate(); + pageStr.AppendInt(nsPresContext::CSSPixelsToAppUnits(16)); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::increment, + pageStr, true); + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } +} + +// Takes client x/y in pixels, converts them to appunits, and converts into +// values relative to this nsTreeBodyFrame frame. +nsPoint nsTreeBodyFrame::AdjustClientCoordsToBoxCoordSpace(int32_t aX, + int32_t aY) { + nsPoint point(nsPresContext::CSSPixelsToAppUnits(aX), + nsPresContext::CSSPixelsToAppUnits(aY)); + + nsPresContext* presContext = PresContext(); + point -= GetOffsetTo(presContext->GetPresShell()->GetRootFrame()); + + // Adjust by the inner box coords, so that we're in the inner box's + // coordinate space. + point -= mInnerBox.TopLeft(); + return point; +} // AdjustClientCoordsToBoxCoordSpace + +int32_t nsTreeBodyFrame::GetRowAt(int32_t aX, int32_t aY) { + if (!mView) { + return 0; + } + + nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); + + // Check if the coordinates are above our visible space. + if (point.y < 0) { + return -1; + } + + return GetRowAtInternal(point.x, point.y); +} + +nsresult nsTreeBodyFrame::GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, + nsTreeColumn** aCol, + nsACString& aChildElt) { + if (!mView) return NS_OK; + + nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); + + // Check if the coordinates are above our visible space. + if (point.y < 0) { + *aRow = -1; + return NS_OK; + } + + nsTreeColumn* col; + nsCSSAnonBoxPseudoStaticAtom* child; + GetCellAt(point.x, point.y, aRow, &col, &child); + + if (col) { + NS_ADDREF(*aCol = col); + if (child == nsCSSAnonBoxes::mozTreeCell()) + aChildElt.AssignLiteral("cell"); + else if (child == nsCSSAnonBoxes::mozTreeTwisty()) + aChildElt.AssignLiteral("twisty"); + else if (child == nsCSSAnonBoxes::mozTreeImage()) + aChildElt.AssignLiteral("image"); + else if (child == nsCSSAnonBoxes::mozTreeCellText()) + aChildElt.AssignLiteral("text"); + } + + return NS_OK; +} + +// +// GetCoordsForCellItem +// +// Find the x/y location and width/height (all in PIXELS) of the given object +// in the given column. +// +// XXX IMPORTANT XXX: +// Hyatt says in the bug for this, that the following needs to be done: +// (1) You need to deal with overflow when computing cell rects. See other +// column iteration examples... if you don't deal with this, you'll mistakenly +// extend the cell into the scrollbar's rect. +// +// (2) You are adjusting the cell rect by the *row" border padding. That's +// wrong. You need to first adjust a row rect by its border/padding, and then +// the cell rect fits inside the adjusted row rect. It also can have +// border/padding as well as margins. The vertical direction isn't that +// important, but you need to get the horizontal direction right. +// +// (3) GetImageSize() does not include margins (but it does include +// border/padding). You need to make sure to add in the image's margins as well. +// +nsresult nsTreeBodyFrame::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsACString& aElement, + int32_t* aX, int32_t* aY, + int32_t* aWidth, + int32_t* aHeight) { + *aX = 0; + *aY = 0; + *aWidth = 0; + *aHeight = 0; + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord currX = mInnerBox.x - mHorzPosition; + + // The Rect for the requested item. + nsRect theRect; + + nsPresContext* presContext = PresContext(); + + nsCOMPtr<nsITreeView> view = GetExistingView(); + + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + // The Rect for the current cell. + nscoord colWidth; +#ifdef DEBUG + nsresult rv = +#endif + currCol->GetWidthInTwips(this, &colWidth); + NS_ASSERTION(NS_SUCCEEDED(rv), "invalid column"); + + nsRect cellRect(currX, mInnerBox.y + mRowHeight * (aRow - mTopRowIndex), + colWidth, mRowHeight); + + // Check the ID of the current column to see if it matches. If it doesn't + // increment the current X value and continue to the next column. + if (currCol != aCol) { + currX += cellRect.width; + continue; + } + // Now obtain the properties for our cell. + PrefillPropertyArray(aRow, currCol); + + nsAutoString properties; + view->GetCellProperties(aRow, currCol, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + + // We don't want to consider any of the decorations that may be present + // on the current row, so we have to deflate the rect by the border and + // padding and offset its left and top coordinates appropriately. + AdjustForBorderPadding(rowContext, cellRect); + + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + constexpr auto cell = "cell"_ns; + if (currCol->IsCycler() || cell.Equals(aElement)) { + // If the current Column is a Cycler, then the Rect is just the cell - the + // margins. Similarly, if we're just being asked for the cell rect, + // provide it. + + theRect = cellRect; + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + theRect.Deflate(cellMargin); + break; + } + + // Since we're not looking for the cell, and since the cell isn't a cycler, + // we're looking for some subcomponent, and now we need to subtract the + // borders and padding of the cell from cellRect so this does not + // interfere with our computations. + AdjustForBorderPadding(cellContext, cellRect); + + UniquePtr<gfxContext> rc = + presContext->PresShell()->CreateReferenceRenderingContext(); + + // Now we'll start making our way across the cell, starting at the edge of + // the cell and proceeding until we hit the right edge. |cellX| is the + // working X value that we will increment as we crawl from left to right. + nscoord cellX = cellRect.x; + nscoord remainWidth = cellRect.width; + + if (currCol->IsPrimary()) { + // If the current Column is a Primary, then we need to take into account + // the indentation and possibly a twisty. + + // The amount of indentation is the indentation width (|mIndentation|) by + // the level. + int32_t level; + view->GetLevel(aRow, &level); + if (!isRTL) cellX += mIndentation * level; + remainWidth -= mIndentation * level; + + // Find the twisty rect by computing its size. + nsRect imageRect; + nsRect twistyRect(cellRect); + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + GetTwistyRect(aRow, currCol, imageRect, twistyRect, presContext, + twistyContext); + + if ("twisty"_ns.Equals(aElement)) { + // If we're looking for the twisty Rect, just return the size + theRect = twistyRect; + break; + } + + // Now we need to add in the margins of the twisty element, so that we + // can find the offset of the next element in the cell. + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + // Adjust our working X value with the twisty width (image size, margins, + // borders, padding. + if (!isRTL) cellX += twistyRect.width; + } + + // Cell Image + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + nsRect imageSize = GetImageSize(aRow, currCol, false, imageContext); + if ("image"_ns.Equals(aElement)) { + theRect = imageSize; + theRect.x = cellX; + theRect.y = cellRect.y; + break; + } + + // Add in the margins of the cell image. + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageSize.Inflate(imageMargin); + + // Increment cellX by the image width + if (!isRTL) cellX += imageSize.width; + + // Cell Text + nsAutoString cellText; + view->GetCellText(aRow, currCol, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + // Create a scratch rect to represent the text rectangle, with the current + // X and Y coords, and a guess at the width and height. The width is the + // remaining width we have left to traverse in the cell, which will be the + // widest possible value for the text rect, and the row height. + nsRect textRect(cellX, cellRect.y, remainWidth, cellRect.height); + + // Measure the width of the text. If the width of the text is greater than + // the remaining width available, then we just assume that the text has + // been cropped and use the remaining rect as the text Rect. Otherwise, + // we add in borders and padding to the text dimension and give that back. + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); + nscoord height = fm->MaxHeight(); + + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + // Center the text. XXX Obey vertical-align style prop? + if (height < textRect.height) { + textRect.y += (textRect.height - height) / 2; + textRect.height = height; + } + + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(textContext, bp); + textRect.height += bp.top + bp.bottom; + + AdjustForCellText(cellText, aRow, currCol, *rc, *fm, textRect); + + theRect = textRect; + } + + if (isRTL) theRect.x = mInnerBox.width - theRect.x - theRect.width; + + *aX = nsPresContext::AppUnitsToIntCSSPixels(theRect.x); + *aY = nsPresContext::AppUnitsToIntCSSPixels(theRect.y); + *aWidth = nsPresContext::AppUnitsToIntCSSPixels(theRect.width); + *aHeight = nsPresContext::AppUnitsToIntCSSPixels(theRect.height); + + return NS_OK; +} + +int32_t nsTreeBodyFrame::GetRowAtInternal(nscoord aX, nscoord aY) { + if (mRowHeight <= 0) return -1; + + // Now just mod by our total inner box height and add to our top row index. + int32_t row = (aY / mRowHeight) + mTopRowIndex; + + // Check if the coordinates are below our visible space (or within our visible + // space but below any row). + if (row > mTopRowIndex + mPageLength || row >= mRowCount) return -1; + + return row; +} + +void nsTreeBodyFrame::CheckTextForBidi(nsAutoString& aText) { + // We could check to see whether the prescontext already has bidi enabled, + // but usually it won't, so it's probably faster to avoid the call to + // GetPresContext() when it's not needed. + if (HasRTLChars(aText)) { + PresContext()->SetBidiEnabled(); + } +} + +void nsTreeBodyFrame::AdjustForCellText(nsAutoString& aText, int32_t aRowIndex, + nsTreeColumn* aColumn, + gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, + nsRect& aTextRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + nscoord maxWidth = aTextRect.width; + bool widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( + aText, aFontMetrics, drawTarget, maxWidth); + + nsCOMPtr<nsITreeView> view = GetExistingView(); + if (aColumn->Overflow()) { + DebugOnly<nsresult> rv; + nsTreeColumn* nextColumn = aColumn->GetNext(); + while (nextColumn && widthIsGreater) { + while (nextColumn) { + nscoord width; + rv = nextColumn->GetWidthInTwips(this, &width); + NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); + + if (width != 0) { + break; + } + + nextColumn = nextColumn->GetNext(); + } + + if (nextColumn) { + nsAutoString nextText; + view->GetCellText(aRowIndex, nextColumn, nextText); + // We don't measure or draw this text so no need to check it for + // bidi-ness + + if (nextText.Length() == 0) { + nscoord width; + rv = nextColumn->GetWidthInTwips(this, &width); + NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); + + maxWidth += width; + widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( + aText, aFontMetrics, drawTarget, maxWidth); + + nextColumn = nextColumn->GetNext(); + } else { + nextColumn = nullptr; + } + } + } + } + + CroppingStyle cropType = CroppingStyle::CropRight; + if (aColumn->GetCropStyle() == 1) { + cropType = CroppingStyle::CropCenter; + } else if (aColumn->GetCropStyle() == 2) { + cropType = CroppingStyle::CropLeft; + } + CropStringForWidth(aText, aRenderingContext, aFontMetrics, maxWidth, + cropType); + + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi( + aText, this, aFontMetrics, aRenderingContext); + + switch (aColumn->GetTextAlignment()) { + case mozilla::StyleTextAlign::Right: + aTextRect.x += aTextRect.width - width; + break; + case mozilla::StyleTextAlign::Center: + aTextRect.x += (aTextRect.width - width) / 2; + break; + default: + break; + } + + aTextRect.width = width; +} + +nsCSSAnonBoxPseudoStaticAtom* nsTreeBodyFrame::GetItemWithinCellAt( + nscoord aX, const nsRect& aCellRect, int32_t aRowIndex, + nsTreeColumn* aColumn) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Obtain the properties for our cell. + PrefillPropertyArray(aRowIndex, aColumn); + nsAutoString properties; + nsCOMPtr<nsITreeView> view = GetExistingView(); + view->GetCellProperties(aRowIndex, aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the cell. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect cellRect(aCellRect); + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + cellRect.Deflate(cellMargin); + + // Adjust the rect for its border and padding. + AdjustForBorderPadding(cellContext, cellRect); + + if (aX < cellRect.x || aX >= cellRect.x + cellRect.width) { + // The user clicked within the cell's margins/borders/padding. This + // constitutes a click on the cell. + return nsCSSAnonBoxes::mozTreeCell(); + } + + nscoord currX = cellRect.x; + nscoord remainingWidth = cellRect.width; + + // Handle right alignment hit testing. + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + nsPresContext* presContext = PresContext(); + UniquePtr<gfxContext> rc = + presContext->PresShell()->CreateReferenceRenderingContext(); + + if (aColumn->IsPrimary()) { + // If we're the primary column, we have indentation and a twisty. + int32_t level; + view->GetLevel(aRowIndex, &level); + + if (!isRTL) currX += mIndentation * level; + remainingWidth -= mIndentation * level; + + if ((isRTL && aX > currX + remainingWidth) || (!isRTL && aX < currX)) { + // The user clicked within the indentation. + return nsCSSAnonBoxes::mozTreeCell(); + } + + // Always leave space for the twisty. + nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); + bool hasTwisty = false; + bool isContainer = false; + view->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + bool isContainerEmpty = false; + view->IsContainerEmpty(aRowIndex, &isContainerEmpty); + if (!isContainerEmpty) hasTwisty = true; + } + + // Resolve style for the twisty. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, presContext, + twistyContext); + + // We will treat a click as hitting the twisty if it happens on the margins, + // borders, padding, or content of the twisty object. By allowing a "slop" + // into the margin, we make it a little bit easier for a user to hit the + // twisty. (We don't want to be too picky here.) + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + if (isRTL) twistyRect.x = currX + remainingWidth - twistyRect.width; + + // Now we test to see if aX is actually within the twistyRect. If it is, + // and if the item should have a twisty, then we return "twisty". If it is + // within the rect but we shouldn't have a twisty, then we return "cell". + if (aX >= twistyRect.x && aX < twistyRect.x + twistyRect.width) { + if (hasTwisty) + return nsCSSAnonBoxes::mozTreeTwisty(); + else + return nsCSSAnonBoxes::mozTreeCell(); + } + + if (!isRTL) currX += twistyRect.width; + remainingWidth -= twistyRect.width; + } + + // Now test to see if the user hit the icon for the cell. + nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); + + // Resolve style for the image. + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + nsRect iconSize = GetImageSize(aRowIndex, aColumn, false, imageContext); + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + iconSize.Inflate(imageMargin); + iconRect.width = iconSize.width; + if (isRTL) iconRect.x = currX + remainingWidth - iconRect.width; + + if (aX >= iconRect.x && aX < iconRect.x + iconRect.width) { + // The user clicked on the image. + return nsCSSAnonBoxes::mozTreeImage(); + } + + if (!isRTL) currX += iconRect.width; + remainingWidth -= iconRect.width; + + nsAutoString cellText; + view->GetCellText(aRowIndex, aColumn, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + nsRect textRect(currX, cellRect.y, remainingWidth, cellRect.height); + + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + AdjustForBorderPadding(textContext, textRect); + + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); + AdjustForCellText(cellText, aRowIndex, aColumn, *rc, *fm, textRect); + + if (aX >= textRect.x && aX < textRect.x + textRect.width) + return nsCSSAnonBoxes::mozTreeCellText(); + else + return nsCSSAnonBoxes::mozTreeCell(); +} + +void nsTreeBodyFrame::GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, + nsTreeColumn** aCol, + nsCSSAnonBoxPseudoStaticAtom** aChildElt) { + *aCol = nullptr; + *aChildElt = nullptr; + + *aRow = GetRowAtInternal(aX, aY); + if (*aRow < 0) return; + + // Determine the column hit. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect cellRect; + nsresult rv = currCol->GetRect( + this, mInnerBox.y + mRowHeight * (*aRow - mTopRowIndex), mRowHeight, + &cellRect); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE("column has no frame"); + continue; + } + + if (!OffsetForHorzScroll(cellRect, false)) continue; + + if (aX >= cellRect.x && aX < cellRect.x + cellRect.width) { + // We know the column hit now. + *aCol = currCol; + + if (currCol->IsCycler()) + // Cyclers contain only images. Fill this in immediately and return. + *aChildElt = nsCSSAnonBoxes::mozTreeImage(); + else + *aChildElt = GetItemWithinCellAt(aX, cellRect, *aRow, currCol); + break; + } + } +} + +nsresult nsTreeBodyFrame::GetCellWidth(int32_t aRow, nsTreeColumn* aCol, + gfxContext* aRenderingContext, + nscoord& aDesiredSize, + nscoord& aCurrentSize) { + MOZ_ASSERT(aCol, "aCol must not be null"); + MOZ_ASSERT(aRenderingContext, "aRenderingContext must not be null"); + + // The rect for the current cell. + nscoord colWidth; + nsresult rv = aCol->GetWidthInTwips(this, &colWidth); + NS_ENSURE_SUCCESS(rv, rv); + + nsRect cellRect(0, 0, colWidth, mRowHeight); + + int32_t overflow = + cellRect.x + cellRect.width - (mInnerBox.x + mInnerBox.width); + if (overflow > 0) cellRect.width -= overflow; + + // Adjust borders and padding for the cell. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(cellContext, bp); + + aCurrentSize = cellRect.width; + aDesiredSize = bp.left + bp.right; + nsCOMPtr<nsITreeView> view = GetExistingView(); + + if (aCol->IsPrimary()) { + // If the current Column is a Primary, then we need to take into account + // the indentation and possibly a twisty. + + // The amount of indentation is the indentation width (|mIndentation|) by + // the level. + int32_t level; + view->GetLevel(aRow, &level); + aDesiredSize += mIndentation * level; + + // Find the twisty rect by computing its size. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + nsRect twistyRect(cellRect); + GetTwistyRect(aRow, aCol, imageSize, twistyRect, PresContext(), + twistyContext); + + // Add in the margins of the twisty element. + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + aDesiredSize += twistyRect.width; + } + + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + // Account for the width of the cell image. + nsRect imageSize = GetImageSize(aRow, aCol, false, imageContext); + // Add in the margins of the cell image. + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageSize.Inflate(imageMargin); + + aDesiredSize += imageSize.width; + + // Get the cell text. + nsAutoString cellText; + view->GetCellText(aRow, aCol, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + // Get the borders and padding for the text. + GetBorderPadding(textContext, bp); + + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); + // Get the width of the text itself + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(cellText, this, *fm, + *aRenderingContext); + nscoord totalTextWidth = width + bp.left + bp.right; + aDesiredSize += totalTextWidth; + return NS_OK; +} + +nsresult nsTreeBodyFrame::IsCellCropped(int32_t aRow, nsTreeColumn* aCol, + bool* _retval) { + nscoord currentSize, desiredSize; + nsresult rv; + + if (!aCol) return NS_ERROR_INVALID_ARG; + + UniquePtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext(); + + rv = GetCellWidth(aRow, aCol, rc.get(), desiredSize, currentSize); + NS_ENSURE_SUCCESS(rv, rv); + + *_retval = desiredSize > currentSize; + + return NS_OK; +} + +nsresult nsTreeBodyFrame::CreateTimer(const LookAndFeel::IntID aID, + nsTimerCallbackFunc aFunc, int32_t aType, + nsITimer** aTimer, const char* aName) { + // Get the delay from the look and feel service. + int32_t delay = LookAndFeel::GetInt(aID, 0); + + nsCOMPtr<nsITimer> timer; + + // Create a new timer only if the delay is greater than zero. + // Zero value means that this feature is completely disabled. + if (delay > 0) { + MOZ_TRY_VAR(timer, + NS_NewTimerWithFuncCallback( + aFunc, this, delay, aType, aName, + mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other))); + } + + timer.forget(aTimer); + return NS_OK; +} + +nsresult nsTreeBodyFrame::RowCountChanged(int32_t aIndex, int32_t aCount) { + if (aCount == 0 || !mView) { + return NS_OK; // Nothing to do. + } + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireRowCountChangedEvent(aIndex, aCount); + } +#endif // #ifdef ACCESSIBILITY + + AutoWeakFrame weakFrame(this); + + // Adjust our selection. + if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) { + sel->AdjustSelection(aIndex, aCount); + } + + NS_ENSURE_STATE(weakFrame.IsAlive()); + + if (mUpdateBatchNest) return NS_OK; + + mRowCount += aCount; +#ifdef DEBUG + int32_t rowCount = mRowCount; + mView->GetRowCount(&rowCount); + NS_ASSERTION( + rowCount == mRowCount, + "row count did not change by the amount suggested, check caller"); +#endif + + int32_t count = Abs(aCount); + int32_t last = LastVisibleRow(); + if (aIndex >= mTopRowIndex && aIndex <= last) InvalidateRange(aIndex, last); + + ScrollParts parts = GetScrollParts(); + + if (mTopRowIndex == 0) { + // Just update the scrollbar and return. + FullScrollbarsUpdate(false); + return NS_OK; + } + + bool needsInvalidation = false; + // Adjust our top row index. + if (aCount > 0) { + if (mTopRowIndex > aIndex) { + // Rows came in above us. Augment the top row index. + mTopRowIndex += aCount; + } + } else if (aCount < 0) { + if (mTopRowIndex > aIndex + count - 1) { + // No need to invalidate. The remove happened + // completely above us (offscreen). + mTopRowIndex -= count; + } else if (mTopRowIndex >= aIndex) { + // This is a full-blown invalidate. + if (mTopRowIndex + mPageLength > mRowCount - 1) { + mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); + } + needsInvalidation = true; + } + } + + FullScrollbarsUpdate(needsInvalidation); + return NS_OK; +} + +nsresult nsTreeBodyFrame::BeginUpdateBatch() { + ++mUpdateBatchNest; + + return NS_OK; +} + +nsresult nsTreeBodyFrame::EndUpdateBatch() { + NS_ASSERTION(mUpdateBatchNest > 0, "badly nested update batch"); + + if (--mUpdateBatchNest != 0) { + return NS_OK; + } + + nsCOMPtr<nsITreeView> view = GetExistingView(); + if (!view) { + return NS_OK; + } + + Invalidate(); + int32_t countBeforeUpdate = mRowCount; + view->GetRowCount(&mRowCount); + if (countBeforeUpdate != mRowCount) { + if (mTopRowIndex + mPageLength > mRowCount - 1) { + mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); + } + FullScrollbarsUpdate(false); + } + + return NS_OK; +} + +void nsTreeBodyFrame::PrefillPropertyArray(int32_t aRowIndex, + nsTreeColumn* aCol) { + MOZ_ASSERT(!aCol || aCol->GetFrame(), "invalid column passed"); + mScratchArray.Clear(); + + // focus + if (mFocused) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::focus); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::blur); + + // sort + bool sorted = false; + mView->IsSorted(&sorted); + if (sorted) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::sorted); + + // drag session + if (mSlots && mSlots->mIsDragging) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dragSession); + + if (aRowIndex != -1) { + if (aRowIndex == mMouseOverRow) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::hover); + + nsCOMPtr<nsITreeSelection> selection = GetSelection(); + if (selection) { + // selected + bool isSelected; + selection->IsSelected(aRowIndex, &isSelected); + if (isSelected) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::selected); + + // current + int32_t currentIndex; + selection->GetCurrentIndex(¤tIndex); + if (aRowIndex == currentIndex) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::current); + } + + // container or leaf + bool isContainer = false; + mView->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::container); + + // open or closed + bool isOpen = false; + mView->IsContainerOpen(aRowIndex, &isOpen); + if (isOpen) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::open); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::closed); + } else { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::leaf); + } + + // drop orientation + if (mSlots && mSlots->mDropAllowed && mSlots->mDropRow == aRowIndex) { + if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropBefore); + else if (mSlots->mDropOrient == nsITreeView::DROP_ON) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropOn); + else if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropAfter); + } + + // odd or even + if (aRowIndex % 2) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::odd); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::even); + + XULTreeElement* tree = GetBaseElement(); + if (tree && tree->HasAttr(kNameSpaceID_None, nsGkAtoms::editing)) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::editing); + } + + // multiple columns + if (mColumns->GetColumnAt(1)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::multicol); + } + + if (aCol) { + mScratchArray.AppendElement(aCol->GetAtom()); + + if (aCol->IsPrimary()) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::primary); + + if (aCol->GetType() == TreeColumn_Binding::TYPE_CHECKBOX) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checkbox); + + if (aRowIndex != -1) { + nsAutoString value; + mView->GetCellValue(aRowIndex, aCol, value); + if (value.EqualsLiteral("true")) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checked); + } + } + + // Read special properties from attributes on the column content node + if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertbefore, + nsGkAtoms::_true, eCaseMatters)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertbefore); + if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertafter, + nsGkAtoms::_true, eCaseMatters)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertafter); + } +} + +nsITheme* nsTreeBodyFrame::GetTwistyRect(int32_t aRowIndex, + nsTreeColumn* aColumn, + nsRect& aImageRect, + nsRect& aTwistyRect, + nsPresContext* aPresContext, + ComputedStyle* aTwistyContext) { + // The twisty rect extends all the way to the end of the cell. This is + // incorrect. We need to determine the twisty rect's true width. This is + // done by examining the ComputedStyle for a width first. If it has one, we + // use that. If it doesn't, we use the image's natural width. If the image + // hasn't loaded and if no width is specified, then we just bail. If there is + // a -moz-appearance involved, adjust the rect by the minimum widget size + // provided by the theme implementation. + aImageRect = GetImageSize(aRowIndex, aColumn, true, aTwistyContext); + if (aImageRect.height > aTwistyRect.height) + aImageRect.height = aTwistyRect.height; + if (aImageRect.width > aTwistyRect.width) + aImageRect.width = aTwistyRect.width; + else + aTwistyRect.width = aImageRect.width; + + bool useTheme = false; + nsITheme* theme = nullptr; + StyleAppearance appearance = + aTwistyContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) + useTheme = true; + } + + if (useTheme) { + LayoutDeviceIntSize minTwistySizePx = + theme->GetMinimumWidgetSize(aPresContext, this, appearance); + + // GMWS() returns size in pixels, we need to convert it back to app units + nsSize minTwistySize; + minTwistySize.width = + aPresContext->DevPixelsToAppUnits(minTwistySizePx.width); + minTwistySize.height = + aPresContext->DevPixelsToAppUnits(minTwistySizePx.height); + + if (aTwistyRect.width < minTwistySize.width) { + aTwistyRect.width = minTwistySize.width; + } + } + + return useTheme ? theme : nullptr; +} + +nsresult nsTreeBodyFrame::GetImage(int32_t aRowIndex, nsTreeColumn* aCol, + bool aUseContext, + ComputedStyle* aComputedStyle, + imgIContainer** aResult) { + *aResult = nullptr; + + nsAutoString imageSrc; + mView->GetImageSrc(aRowIndex, aCol, imageSrc); + RefPtr<imgRequestProxy> styleRequest; + if (aUseContext || imageSrc.IsEmpty()) { + // Obtain the URL from the ComputedStyle. + styleRequest = + aComputedStyle->StyleList()->mListStyleImage.GetImageRequest(); + if (!styleRequest) return NS_OK; + nsCOMPtr<nsIURI> uri; + styleRequest->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(spec, imageSrc); + } + + // Look the image up in our cache. + nsTreeImageCacheEntry entry; + if (mImageCache.Get(imageSrc, &entry)) { + // Find out if the image has loaded. + uint32_t status; + imgIRequest* imgReq = entry.request; + imgReq->GetImageStatus(&status); + imgReq->GetImage(aResult); // We hand back the image here. The GetImage + // call addrefs *aResult. + bool animated = true; // Assuming animated is the safe option + + // We can only call GetAnimated if we're decoded + if (*aResult && (status & imgIRequest::STATUS_DECODE_COMPLETE)) + (*aResult)->GetAnimated(&animated); + + if ((!(status & imgIRequest::STATUS_LOAD_COMPLETE)) || animated) { + // We either aren't done loading, or we're animating. Add our row as a + // listener for invalidations. + nsCOMPtr<imgINotificationObserver> obs; + imgReq->GetNotificationObserver(getter_AddRefs(obs)); + + if (obs) { + static_cast<nsTreeImageListener*>(obs.get())->AddCell(aRowIndex, aCol); + } + + return NS_OK; + } + } + + if (!*aResult) { + // Create a new nsTreeImageListener object and pass it our row and column + // information. + nsTreeImageListener* listener = new nsTreeImageListener(this); + if (!listener) return NS_ERROR_OUT_OF_MEMORY; + + mCreatedListeners.Insert(listener); + + listener->AddCell(aRowIndex, aCol); + nsCOMPtr<imgINotificationObserver> imgNotificationObserver = listener; + + Document* doc = mContent->GetComposedDoc(); + if (!doc) + // The page is currently being torn down. Why bother. + return NS_ERROR_FAILURE; + + RefPtr<imgRequestProxy> imageRequest; + if (styleRequest) { + styleRequest->SyncClone(imgNotificationObserver, doc, + getter_AddRefs(imageRequest)); + } else { + nsCOMPtr<nsIURI> srcURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI()); + if (!srcURI) return NS_ERROR_FAILURE; + + auto referrerInfo = MakeRefPtr<mozilla::dom::ReferrerInfo>(*doc); + + // XXXbz what's the origin principal for this stuff that comes from our + // view? I guess we should assume that it's the node's principal... + nsresult rv = nsContentUtils::LoadImage( + srcURI, mContent, doc, mContent->NodePrincipal(), 0, referrerInfo, + imgNotificationObserver, nsIRequest::LOAD_NORMAL, u""_ns, + getter_AddRefs(imageRequest)); + NS_ENSURE_SUCCESS(rv, rv); + + // NOTE(heycam): If it's an SVG image, and we need to want the image to + // able to respond to media query changes, it needs to be added to the + // document's ImageTracker. For now, assume we don't need this. + } + listener->UnsuppressInvalidation(); + + if (!imageRequest) return NS_ERROR_FAILURE; + + // We don't want discarding/decode-on-draw for xul images + imageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY); + imageRequest->LockImage(); + + // In a case it was already cached. + imageRequest->GetImage(aResult); + nsTreeImageCacheEntry cacheEntry(imageRequest, imgNotificationObserver); + mImageCache.InsertOrUpdate(imageSrc, cacheEntry); + } + return NS_OK; +} + +nsRect nsTreeBodyFrame::GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, + bool aUseContext, + ComputedStyle* aComputedStyle) { + // XXX We should respond to visibility rules for collapsed vs. hidden. + + // This method returns the width of the twisty INCLUDING borders and padding. + // It first checks the ComputedStyle for a width. If none is found, it tries + // to use the default image width for the twisty. If no image is found, it + // defaults to border+padding. + nsRect r(0, 0, 0, 0); + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(aComputedStyle, bp); + r.Inflate(bp); + + // Now r contains our border+padding info. We now need to get our width and + // height. + bool needWidth = false; + bool needHeight = false; + + // We have to load image even though we already have a size. + // Don't change this, otherwise things start to go awry. + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, getter_AddRefs(image)); + + const nsStylePosition* myPosition = aComputedStyle->StylePosition(); + if (myPosition->mWidth.ConvertsToLength()) { + int32_t val = myPosition->mWidth.ToLength(); + r.width += val; + } else { + needWidth = true; + } + + if (myPosition->mHeight.ConvertsToLength()) { + int32_t val = myPosition->mHeight.ToLength(); + r.height += val; + } else { + needHeight = true; + } + + if (image) { + if (needWidth || needHeight) { + // Get the natural image size. + + if (needWidth) { + // Get the size from the image. + nscoord width; + image->GetWidth(&width); + r.width += nsPresContext::CSSPixelsToAppUnits(width); + } + + if (needHeight) { + nscoord height; + image->GetHeight(&height); + r.height += nsPresContext::CSSPixelsToAppUnits(height); + } + } + } + + return r; +} + +// GetImageDestSize returns the destination size of the image. +// The width and height do not include borders and padding. +// The width and height have not been adjusted to fit in the row height +// or cell width. +// The width and height reflect the destination size specified in CSS, +// or the image region specified in CSS, or the natural size of the +// image. +// If only the destination width has been specified in CSS, the height is +// calculated to maintain the aspect ratio of the image. +// If only the destination height has been specified in CSS, the width is +// calculated to maintain the aspect ratio of the image. +nsSize nsTreeBodyFrame::GetImageDestSize(ComputedStyle* aComputedStyle, + imgIContainer* image) { + nsSize size(0, 0); + + // We need to get the width and height. + bool needWidth = false; + bool needHeight = false; + + // Get the style position to see if the CSS has specified the + // destination width/height. + const nsStylePosition* myPosition = aComputedStyle->StylePosition(); + + if (myPosition->mWidth.ConvertsToLength()) { + // CSS has specified the destination width. + size.width = myPosition->mWidth.ToLength(); + } else { + // We'll need to get the width of the image/region. + needWidth = true; + } + + if (myPosition->mHeight.ConvertsToLength()) { + // CSS has specified the destination height. + size.height = myPosition->mHeight.ToLength(); + } else { + // We'll need to get the height of the image/region. + needHeight = true; + } + + if (needWidth || needHeight) { + // We need to get the size of the image/region. + nsSize imageSize(0, 0); + if (image) { + nscoord width; + image->GetWidth(&width); + imageSize.width = nsPresContext::CSSPixelsToAppUnits(width); + nscoord height; + image->GetHeight(&height); + imageSize.height = nsPresContext::CSSPixelsToAppUnits(height); + } + + if (needWidth) { + if (!needHeight && imageSize.height != 0) { + // The CSS specified the destination height, but not the destination + // width. We need to calculate the width so that we maintain the + // image's aspect ratio. + size.width = imageSize.width * size.height / imageSize.height; + } else { + size.width = imageSize.width; + } + } + + if (needHeight) { + if (!needWidth && imageSize.width != 0) { + // The CSS specified the destination width, but not the destination + // height. We need to calculate the height so that we maintain the + // image's aspect ratio. + size.height = imageSize.height * size.width / imageSize.width; + } else { + size.height = imageSize.height; + } + } + } + + return size; +} + +// GetImageSourceRect returns the source rectangle of the image to be +// displayed. +// The width and height reflect the image region specified in CSS, or +// the natural size of the image. +// The width and height do not include borders and padding. +// The width and height do not reflect the destination size specified +// in CSS. +nsRect nsTreeBodyFrame::GetImageSourceRect(ComputedStyle* aComputedStyle, + imgIContainer* image) { + if (!image) { + return nsRect(); + } + + nsRect r; + // Use the actual image size. + nscoord coord; + if (NS_SUCCEEDED(image->GetWidth(&coord))) { + r.width = nsPresContext::CSSPixelsToAppUnits(coord); + } + if (NS_SUCCEEDED(image->GetHeight(&coord))) { + r.height = nsPresContext::CSSPixelsToAppUnits(coord); + } + return r; +} + +int32_t nsTreeBodyFrame::GetRowHeight() { + // Look up the correct height. It is equal to the specified height + // + the specified margins. + mScratchArray.Clear(); + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + if (rowContext) { + const nsStylePosition* myPosition = rowContext->StylePosition(); + + nscoord minHeight = 0; + if (myPosition->mMinHeight.ConvertsToLength()) { + minHeight = myPosition->mMinHeight.ToLength(); + } + + nscoord height = 0; + if (myPosition->mHeight.ConvertsToLength()) { + height = myPosition->mHeight.ToLength(); + } + + if (height < minHeight) height = minHeight; + + if (height > 0) { + height = nsPresContext::AppUnitsToIntCSSPixels(height); + height += height % 2; + height = nsPresContext::CSSPixelsToAppUnits(height); + + // XXX Check box-sizing to determine if border/padding should augment the + // height Inflate the height by our margins. + nsRect rowRect(0, 0, 0, height); + nsMargin rowMargin; + rowContext->StyleMargin()->GetMargin(rowMargin); + rowRect.Inflate(rowMargin); + height = rowRect.height; + return height; + } + } + + return nsPresContext::CSSPixelsToAppUnits(18); // As good a default as any. +} + +int32_t nsTreeBodyFrame::GetIndentation() { + // Look up the correct indentation. It is equal to the specified indentation + // width. + mScratchArray.Clear(); + ComputedStyle* indentContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeIndentation()); + if (indentContext) { + const nsStylePosition* myPosition = indentContext->StylePosition(); + if (myPosition->mWidth.ConvertsToLength()) { + return myPosition->mWidth.ToLength(); + } + } + + return nsPresContext::CSSPixelsToAppUnits(16); // As good a default as any. +} + +void nsTreeBodyFrame::CalcInnerBox() { + mInnerBox.SetRect(0, 0, mRect.width, mRect.height); + AdjustForBorderPadding(mComputedStyle, mInnerBox); +} + +nscoord nsTreeBodyFrame::CalcHorzWidth(const ScrollParts& aParts) { + // Compute the adjustment to the last column. This varies depending on the + // visibility of the columnpicker and the scrollbar. + if (aParts.mColumnsFrame) + mAdjustWidth = mRect.width - aParts.mColumnsFrame->GetRect().width; + else + mAdjustWidth = 0; + + nscoord width = 0; + + // We calculate this from the scrollable frame, so that it + // properly covers all contingencies of what could be + // scrollable (columns, body, etc...) + + if (aParts.mColumnsScrollFrame) { + width = aParts.mColumnsScrollFrame->GetScrollRange().width + + aParts.mColumnsScrollFrame->GetScrollPortRect().width; + } + + // If no horz scrolling periphery is present, then just return our width + if (width == 0) width = mRect.width; + + return width; +} + +Maybe<nsIFrame::Cursor> nsTreeBodyFrame::GetCursor(const nsPoint& aPoint) { + // Check the GetScriptHandlingObject so we don't end up running code when + // the document is a zombie. + bool dummy; + if (mView && GetContent()->GetComposedDoc()->GetScriptHandlingObject(dummy)) { + int32_t row; + nsTreeColumn* col; + nsCSSAnonBoxPseudoStaticAtom* child; + GetCellAt(aPoint.x, aPoint.y, &row, &col, &child); + + if (child) { + // Our scratch array is already prefilled. + RefPtr<ComputedStyle> childContext = GetPseudoComputedStyle(child); + StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword; + if (kind == StyleCursorKind::Auto) { + kind = StyleCursorKind::Default; + } + return Some( + Cursor{kind, AllowCustomCursorImage::Yes, std::move(childContext)}); + } + } + return SimpleXULLeafFrame::GetCursor(aPoint); +} + +static uint32_t GetDropEffect(WidgetGUIEvent* aEvent) { + NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + nsContentUtils::SetDataTransferInEvent(dragEvent); + + uint32_t action = 0; + if (dragEvent->mDataTransfer) { + action = dragEvent->mDataTransfer->DropEffectInt(); + } + return action; +} + +nsresult nsTreeBodyFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + if (aEvent->mMessage == eMouseOver || aEvent->mMessage == eMouseMove) { + nsPoint pt = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); + int32_t xTwips = pt.x - mInnerBox.x; + int32_t yTwips = pt.y - mInnerBox.y; + int32_t newrow = GetRowAtInternal(xTwips, yTwips); + if (mMouseOverRow != newrow) { + // redraw the old and the new row + if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); + mMouseOverRow = newrow; + if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); + } + } else if (aEvent->mMessage == eMouseOut) { + if (mMouseOverRow != -1) { + InvalidateRow(mMouseOverRow); + mMouseOverRow = -1; + } + } else if (aEvent->mMessage == eDragEnter) { + if (!mSlots) { + mSlots = MakeUnique<Slots>(); + } + + // Cache several things we'll need throughout the course of our work. These + // will all get released on a drag exit. + + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + // Cache the drag session. + mSlots->mIsDragging = true; + mSlots->mDropRow = -1; + mSlots->mDropOrient = -1; + mSlots->mDragAction = GetDropEffect(aEvent); + } else if (aEvent->mMessage == eDragOver) { + // The mouse is hovering over this tree. If we determine things are + // different from the last time, invalidate the drop feedback at the old + // position, query the view to see if the current location is droppable, + // and then invalidate the drop feedback at the new location if it is. + // The mouse may or may not have changed position from the last time + // we were called, so optimize out a lot of the extra notifications by + // checking if anything changed first. For drop feedback we use drop, + // dropBefore and dropAfter property. + if (!mView || !mSlots) { + return NS_OK; + } + + // Save last values, we will need them. + int32_t lastDropRow = mSlots->mDropRow; + int16_t lastDropOrient = mSlots->mDropOrient; +#ifndef XP_MACOSX + int16_t lastScrollLines = mSlots->mScrollLines; +#endif + + // Find out the current drag action + uint32_t lastDragAction = mSlots->mDragAction; + mSlots->mDragAction = GetDropEffect(aEvent); + + // Compute the row mouse is over and the above/below/on state. + // Below we'll use this to see if anything changed. + // Also check if we want to auto-scroll. + ComputeDropPosition(aEvent, &mSlots->mDropRow, &mSlots->mDropOrient, + &mSlots->mScrollLines); + + // While we're here, handle tracking of scrolling during a drag. + if (mSlots->mScrollLines) { + if (mSlots->mDropAllowed) { + // Invalidate primary cell at old location. + mSlots->mDropAllowed = false; + InvalidateDropFeedback(lastDropRow, lastDropOrient); + } +#ifdef XP_MACOSX + ScrollByLines(mSlots->mScrollLines); +#else + if (!lastScrollLines) { + // Cancel any previously initialized timer. + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + // Set a timer to trigger the tree scrolling. + CreateTimer(LookAndFeel::IntID::TreeLazyScrollDelay, LazyScrollCallback, + nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::LazyScrollCallback"); + } +#endif + // Bail out to prevent spring loaded timer and feedback line settings. + return NS_OK; + } + + // If changed from last time, invalidate primary cell at the old location + // and if allowed, invalidate primary cell at the new location. If nothing + // changed, just bail. + if (mSlots->mDropRow != lastDropRow || + mSlots->mDropOrient != lastDropOrient || + mSlots->mDragAction != lastDragAction) { + // Invalidate row at the old location. + if (mSlots->mDropAllowed) { + mSlots->mDropAllowed = false; + InvalidateDropFeedback(lastDropRow, lastDropOrient); + } + + if (mSlots->mTimer) { + // Timer is active but for a different row than the current one, kill + // it. + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + if (mSlots->mDropRow >= 0) { + if (!mSlots->mTimer && mSlots->mDropOrient == nsITreeView::DROP_ON) { + // Either there wasn't a timer running or it was just killed above. + // If over a folder, start up a timer to open the folder. + bool isContainer = false; + mView->IsContainer(mSlots->mDropRow, &isContainer); + if (isContainer) { + bool isOpen = false; + mView->IsContainerOpen(mSlots->mDropRow, &isOpen); + if (!isOpen) { + // This node isn't expanded, set a timer to expand it. + CreateTimer(LookAndFeel::IntID::TreeOpenDelay, OpenCallback, + nsITimer::TYPE_ONE_SHOT, + getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::OpenCallback"); + } + } + } + + // The dataTransfer was initialized by the call to GetDropEffect above. + bool canDropAtNewLocation = false; + mView->CanDrop(mSlots->mDropRow, mSlots->mDropOrient, + aEvent->AsDragEvent()->mDataTransfer, + &canDropAtNewLocation); + + if (canDropAtNewLocation) { + // Invalidate row at the new location. + mSlots->mDropAllowed = canDropAtNewLocation; + InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); + } + } + } + + // Indicate that the drop is allowed by preventing the default behaviour. + if (mSlots->mDropAllowed) *aEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (aEvent->mMessage == eDrop) { + // this event was meant for another frame, so ignore it + if (!mSlots) return NS_OK; + + // Tell the view where the drop happened. + + // Remove the drop folder and all its parents from the array. + int32_t parentIndex; + nsresult rv = mView->GetParentIndex(mSlots->mDropRow, &parentIndex); + while (NS_SUCCEEDED(rv) && parentIndex >= 0) { + mSlots->mArray.RemoveElement(parentIndex); + rv = mView->GetParentIndex(parentIndex, &parentIndex); + } + + NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + nsContentUtils::SetDataTransferInEvent(dragEvent); + + mView->Drop(mSlots->mDropRow, mSlots->mDropOrient, + dragEvent->mDataTransfer); + mSlots->mDropRow = -1; + mSlots->mDropOrient = -1; + mSlots->mIsDragging = false; + *aEventStatus = + nsEventStatus_eConsumeNoDefault; // already handled the drop + } else if (aEvent->mMessage == eDragExit) { + // this event was meant for another frame, so ignore it + if (!mSlots) return NS_OK; + + // Clear out all our tracking vars. + + if (mSlots->mDropAllowed) { + mSlots->mDropAllowed = false; + InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); + } else + mSlots->mDropAllowed = false; + mSlots->mIsDragging = false; + mSlots->mScrollLines = 0; + // If a drop is occuring, the exit event will fire just before the drop + // event, so don't reset mDropRow or mDropOrient as these fields are used + // by the drop event. + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + if (!mSlots->mArray.IsEmpty()) { + // Close all spring loaded folders except the drop folder. + CreateTimer(LookAndFeel::IntID::TreeCloseDelay, CloseCallback, + nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::CloseCallback"); + } + } + + return NS_OK; +} + +namespace mozilla { + +class nsDisplayTreeBody final : public nsPaintedDisplayItem { + public: + nsDisplayTreeBody(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayTreeBody); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTreeBody) + + nsDisplayItemGeometry* AllocateGeometry( + nsDisplayListBuilder* aBuilder) override { + return new nsDisplayTreeBodyGeometry(this, aBuilder, IsWindowActive()); + } + + void Destroy(nsDisplayListBuilder* aBuilder) override { + aBuilder->UnregisterThemeGeometry(this); + nsPaintedDisplayItem::Destroy(aBuilder); + } + + bool IsWindowActive() const { + DocumentState docState = + mFrame->PresContext()->Document()->GetDocumentState(); + return !docState.HasState(DocumentState::WINDOW_INACTIVE); + } + + void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion* aInvalidRegion) const override { + auto geometry = static_cast<const nsDisplayTreeBodyGeometry*>(aGeometry); + + if (IsWindowActive() != geometry->mWindowIsActive) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsPaintedDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, + aInvalidRegion); + } + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { + MOZ_ASSERT(aBuilder); + Unused << static_cast<nsTreeBodyFrame*>(mFrame)->PaintTreeBody( + *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), aBuilder); + } + + NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY) + + nsRect GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const override { + bool snap; + return GetBounds(aBuilder, &snap); + } +}; + +} // namespace mozilla + +#ifdef XP_MACOSX +static bool IsInSourceList(nsIFrame* aFrame) { + for (nsIFrame* frame = aFrame; frame; + frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) { + if (frame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::MozMacSourceList) { + return true; + } + } + return false; +} +#endif + +// Painting routines +void nsTreeBodyFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // REVIEW: why did we paint if we were collapsed? that makes no sense! + if (!IsVisibleForPainting()) return; // We're invisible. Don't paint. + + // Handles painting our background, border, and outline. + SimpleXULLeafFrame::BuildDisplayList(aBuilder, aLists); + + // Bail out now if there's no view or we can't run script because the + // document is a zombie + if (!mView || !GetContent()->GetComposedDoc()->GetWindow()) return; + + nsDisplayItem* item = MakeDisplayItem<nsDisplayTreeBody>(aBuilder, this); + aLists.Content()->AppendToTop(item); + +#ifdef XP_MACOSX + XULTreeElement* tree = GetBaseElement(); + nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr; + nsCOMPtr<nsITreeView> view = GetExistingView(); + nsCOMPtr<nsITreeSelection> selection = GetSelection(); + nsITheme* theme = PresContext()->Theme(); + // On Mac, we support native theming of selected rows. On 10.10 and higher, + // this means applying vibrancy which require us to register the theme + // geometrics for the row. In order to make the vibrancy effect to work + // properly, we also need an ancestor frame to be themed as a source list. + if (selection && theme && IsInSourceList(treeFrame)) { + // Loop through our onscreen rows. If the row is selected and a + // -moz-appearance is provided, RegisterThemeGeometry might be necessary. + const auto end = std::min(mRowCount, LastVisibleRow() + 1); + for (auto i = FirstVisibleRow(); i < end; i++) { + bool isSelected; + selection->IsSelected(i, &isSelected); + if (isSelected) { + PrefillPropertyArray(i, nullptr); + nsAutoString properties; + view->GetRowProperties(i, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + if (theme->ThemeSupportsWidget(PresContext(), this, appearance)) { + nsITheme::ThemeGeometryType type = + theme->ThemeGeometryTypeForWidget(this, appearance); + if (type != nsITheme::eThemeGeometryTypeUnknown) { + nsRect rowRect(mInnerBox.x, + mInnerBox.y + mRowHeight * (i - FirstVisibleRow()), + mInnerBox.width, mRowHeight); + aBuilder->RegisterThemeGeometry( + type, item, + LayoutDeviceIntRect::FromUnknownRect( + (rowRect + aBuilder->ToReferenceFrame(this)) + .ToNearestPixels( + PresContext()->AppUnitsPerDevPixel()))); + } + } + } + } + } + } +#endif +} + +ImgDrawResult nsTreeBodyFrame::PaintTreeBody(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + // Update our available height and our page count. + CalcInnerBox(); + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + aRenderingContext.Save(); + aRenderingContext.Clip(NSRectToSnappedRect( + mInnerBox + aPt, PresContext()->AppUnitsPerDevPixel(), *drawTarget)); + int32_t oldPageCount = mPageLength; + if (!mHasFixedRowCount) { + mPageLength = + (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; + } + + if (oldPageCount != mPageLength || + mHorzWidth != CalcHorzWidth(GetScrollParts())) { + // Schedule a ResizeReflow that will update our info properly. + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + } +#ifdef DEBUG + int32_t rowCount = mRowCount; + mView->GetRowCount(&rowCount); + NS_WARNING_ASSERTION(mRowCount == rowCount, "row count changed unexpectedly"); +#endif + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Loop through our columns and paint them (e.g., for sorting). This is only + // relevant when painting backgrounds, since columns contain no content. + // Content is contained in the rows. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect colRect; + nsresult rv = + currCol->GetRect(this, mInnerBox.y, mInnerBox.height, &colRect); + // Don't paint hidden columns. + if (NS_FAILED(rv) || colRect.width == 0) continue; + + if (OffsetForHorzScroll(colRect, false)) { + nsRect dirtyRect; + colRect += aPt; + if (dirtyRect.IntersectRect(aDirtyRect, colRect)) { + result &= PaintColumn(currCol, colRect, PresContext(), + aRenderingContext, aDirtyRect); + } + } + } + // Loop through our on-screen rows. + for (int32_t i = mTopRowIndex; + i < mRowCount && i <= mTopRowIndex + mPageLength; i++) { + nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * (i - mTopRowIndex), + mInnerBox.width, mRowHeight); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, rowRect + aPt) && + rowRect.y < (mInnerBox.y + mInnerBox.height)) { + result &= PaintRow(i, rowRect + aPt, PresContext(), aRenderingContext, + aDirtyRect, aPt, aBuilder); + } + } + + if (mSlots && mSlots->mDropAllowed && + (mSlots->mDropOrient == nsITreeView::DROP_BEFORE || + mSlots->mDropOrient == nsITreeView::DROP_AFTER)) { + nscoord yPos = mInnerBox.y + + mRowHeight * (mSlots->mDropRow - mTopRowIndex) - + mRowHeight / 2; + nsRect feedbackRect(mInnerBox.x, yPos, mInnerBox.width, mRowHeight); + if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) + feedbackRect.y += mRowHeight; + + nsRect dirtyRect; + feedbackRect += aPt; + if (dirtyRect.IntersectRect(aDirtyRect, feedbackRect)) { + result &= PaintDropFeedback(feedbackRect, PresContext(), + aRenderingContext, aDirtyRect, aPt); + } + } + aRenderingContext.Restore(); + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintColumn(nsTreeColumn* aColumn, + const nsRect& aColumnRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Now obtain the properties for our cell. + PrefillPropertyArray(-1, aColumn); + nsAutoString properties; + + nsCOMPtr<nsITreeView> view = GetExistingView(); + view->GetColumnProperties(aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the column. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* colContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeColumn()); + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect colRect(aColumnRect); + nsMargin colMargin; + colContext->StyleMargin()->GetMargin(colMargin); + colRect.Deflate(colMargin); + + return PaintBackgroundLayer(colContext, aPresContext, aRenderingContext, + colRect, aDirtyRect); +} + +ImgDrawResult nsTreeBodyFrame::PaintRow(int32_t aRowIndex, + const nsRect& aRowRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + // We have been given a rect for our row. We treat this row like a full-blown + // frame, meaning that it can have borders, margins, padding, and a + // background. + + // Without a view, we have no data. Check for this up front. + nsCOMPtr<nsITreeView> view = GetExistingView(); + if (!view) { + return ImgDrawResult::SUCCESS; + } + + nsresult rv; + + // Now obtain the properties for our row. + // XXX Automatically fill in the following props: open, closed, container, + // leaf, selected, focused + PrefillPropertyArray(aRowIndex, nullptr); + + nsAutoString properties; + view->GetRowProperties(aRowIndex, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the row. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + + // Obtain the margins for the row and then deflate our rect by that + // amount. The row is assumed to be contained within the deflated rect. + nsRect rowRect(aRowRect); + nsMargin rowMargin; + rowContext->StyleMargin()->GetMargin(rowMargin); + rowRect.Deflate(rowMargin); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Paint our borders and background for our row rect. + nsITheme* theme = nullptr; + auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + } + + if (theme && theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) { + nsRect dirty; + dirty.IntersectRect(rowRect, aDirtyRect); + theme->DrawWidgetBackground(&aRenderingContext, this, appearance, rowRect, + dirty); + } else { + result &= PaintBackgroundLayer(rowContext, aPresContext, aRenderingContext, + rowRect, aDirtyRect); + } + + // Adjust the rect for its border and padding. + nsRect originalRowRect = rowRect; + AdjustForBorderPadding(rowContext, rowRect); + + bool isSeparator = false; + view->IsSeparator(aRowIndex, &isSeparator); + if (isSeparator) { + // The row is a separator. + + nscoord primaryX = rowRect.x; + nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); + if (primaryCol) { + // Paint the primary cell. + nsRect cellRect; + rv = primaryCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE("primary column is invalid"); + return result; + } + + if (OffsetForHorzScroll(cellRect, false)) { + cellRect.x += aPt.x; + nsRect dirtyRect; + nsRect checkRect(cellRect.x, originalRowRect.y, cellRect.width, + originalRowRect.height); + if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) { + result &= + PaintCell(aRowIndex, primaryCol, cellRect, aPresContext, + aRenderingContext, aDirtyRect, primaryX, aPt, aBuilder); + } + } + + // Paint the left side of the separator. + nscoord currX; + nsTreeColumn* previousCol = primaryCol->GetPrevious(); + if (previousCol) { + nsRect prevColRect; + rv = previousCol->GetRect(this, 0, 0, &prevColRect); + if (NS_SUCCEEDED(rv)) { + currX = (prevColRect.x - mHorzPosition) + prevColRect.width + aPt.x; + } else { + MOZ_ASSERT_UNREACHABLE( + "The column before the primary column is " + "invalid"); + currX = rowRect.x; + } + } else { + currX = rowRect.x; + } + + int32_t level; + view->GetLevel(aRowIndex, &level); + if (level == 0) currX += mIndentation; + + if (currX > rowRect.x) { + nsRect separatorRect(rowRect); + separatorRect.width -= rowRect.x + rowRect.width - currX; + result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, + aRenderingContext, aDirtyRect); + } + } + + // Paint the right side (whole) separator. + nsRect separatorRect(rowRect); + if (primaryX > rowRect.x) { + separatorRect.width -= primaryX - rowRect.x; + separatorRect.x += primaryX - rowRect.x; + } + result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, + aRenderingContext, aDirtyRect); + } else { + // Now loop over our cells. Only paint a cell if it intersects with our + // dirty rect. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect cellRect; + rv = currCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); + // Don't paint cells in hidden columns. + if (NS_FAILED(rv) || cellRect.width == 0) continue; + + if (OffsetForHorzScroll(cellRect, false)) { + cellRect.x += aPt.x; + + // for primary columns, use the row's vertical size so that the + // lines get drawn properly + nsRect checkRect = cellRect; + if (currCol->IsPrimary()) + checkRect = nsRect(cellRect.x, originalRowRect.y, cellRect.width, + originalRowRect.height); + + nsRect dirtyRect; + nscoord dummy; + if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) + result &= + PaintCell(aRowIndex, currCol, cellRect, aPresContext, + aRenderingContext, aDirtyRect, dummy, aPt, aBuilder); + } + } + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintSeparator(int32_t aRowIndex, + const nsRect& aSeparatorRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + // Resolve style for the separator. + ComputedStyle* separatorContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeSeparator()); + bool useTheme = false; + nsITheme* theme = nullptr; + StyleAppearance appearance = + separatorContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) + useTheme = true; + } + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // use -moz-appearance if provided. + if (useTheme) { + nsRect dirty; + dirty.IntersectRect(aSeparatorRect, aDirtyRect); + theme->DrawWidgetBackground(&aRenderingContext, this, appearance, + aSeparatorRect, dirty); + } else { + const nsStylePosition* stylePosition = separatorContext->StylePosition(); + + // Obtain the height for the separator or use the default value. + nscoord height; + if (stylePosition->mHeight.ConvertsToLength()) { + height = stylePosition->mHeight.ToLength(); + } else { + // Use default height 2px. + height = nsPresContext::CSSPixelsToAppUnits(2); + } + + // Obtain the margins for the separator and then deflate our rect by that + // amount. The separator is assumed to be contained within the deflated + // rect. + nsRect separatorRect(aSeparatorRect.x, aSeparatorRect.y, + aSeparatorRect.width, height); + nsMargin separatorMargin; + separatorContext->StyleMargin()->GetMargin(separatorMargin); + separatorRect.Deflate(separatorMargin); + + // Center the separator. + separatorRect.y += (aSeparatorRect.height - height) / 2; + + result &= + PaintBackgroundLayer(separatorContext, aPresContext, aRenderingContext, + separatorRect, aDirtyRect); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintCell( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCellRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX, nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Now obtain the properties for our cell. + // XXX Automatically fill in the following props: open, closed, container, + // leaf, selected, focused, and the col ID. + PrefillPropertyArray(aRowIndex, aColumn); + nsAutoString properties; + nsCOMPtr<nsITreeView> view = GetExistingView(); + view->GetCellProperties(aRowIndex, aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the cell. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect cellRect(aCellRect); + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + cellRect.Deflate(cellMargin); + + // Paint our borders and background for our row rect. + ImgDrawResult result = PaintBackgroundLayer( + cellContext, aPresContext, aRenderingContext, cellRect, aDirtyRect); + + // Adjust the rect for its border and padding. + AdjustForBorderPadding(cellContext, cellRect); + + nscoord currX = cellRect.x; + nscoord remainingWidth = cellRect.width; + + // Now we paint the contents of the cells. + // Directionality of the tree determines the order in which we paint. + // StyleDirection::Ltr means paint from left to right. + // StyleDirection::Rtl means paint from right to left. + + if (aColumn->IsPrimary()) { + // If we're the primary column, we need to indent and paint the twisty and + // any connecting lines between siblings. + + int32_t level; + view->GetLevel(aRowIndex, &level); + + if (!isRTL) currX += mIndentation * level; + remainingWidth -= mIndentation * level; + + // Resolve the style to use for the connecting lines. + ComputedStyle* lineContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeLine()); + + if (mIndentation && level && + lineContext->StyleVisibility()->IsVisibleOrCollapsed()) { + // Paint the thread lines. + + // Get the size of the twisty. We don't want to paint the twisty + // before painting of connecting lines since it would paint lines over + // the twisty. But we need to leave a place for it. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + nsRect twistyRect(aCellRect); + GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext, + twistyContext); + + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + const nsStyleBorder* borderStyle = lineContext->StyleBorder(); + // Resolve currentcolor values against the treeline context + nscolor color = borderStyle->mBorderLeftColor.CalcColor(*lineContext); + ColorPattern colorPatt(ToDeviceColor(color)); + + StyleBorderStyle style = borderStyle->GetBorderStyle(eSideLeft); + StrokeOptions strokeOptions; + nsLayoutUtils::InitDashPattern(strokeOptions, style); + + nscoord srcX = currX + twistyRect.width - mIndentation / 2; + nscoord lineY = (aRowIndex - mTopRowIndex) * mRowHeight + aPt.y; + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + nsPresContext* pc = PresContext(); + + // Don't paint off our cell. + if (srcX <= cellRect.x + cellRect.width) { + nscoord destX = currX + twistyRect.width; + if (destX > cellRect.x + cellRect.width) + destX = cellRect.x + cellRect.width; + if (isRTL) { + srcX = currX + remainingWidth - (srcX - cellRect.x); + destX = currX + remainingWidth - (destX - cellRect.x); + } + Point p1(pc->AppUnitsToGfxUnits(srcX), + pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); + Point p2(pc->AppUnitsToGfxUnits(destX), + pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); + SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, + strokeOptions.mLineWidth); + drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); + } + + int32_t currentParent = aRowIndex; + for (int32_t i = level; i > 0; i--) { + if (srcX <= cellRect.x + cellRect.width) { + // Paint full vertical line only if we have next sibling. + bool hasNextSibling; + view->HasNextSibling(currentParent, aRowIndex, &hasNextSibling); + if (hasNextSibling || i == level) { + Point p1(pc->AppUnitsToGfxUnits(srcX), + pc->AppUnitsToGfxUnits(lineY)); + Point p2; + p2.x = pc->AppUnitsToGfxUnits(srcX); + + if (hasNextSibling) + p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight); + else if (i == level) + p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2); + + SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, + strokeOptions.mLineWidth); + drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); + } + } + + int32_t parent; + if (NS_FAILED(view->GetParentIndex(currentParent, &parent)) || + parent < 0) + break; + currentParent = parent; + srcX -= mIndentation; + } + } + + // Always leave space for the twisty. + nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); + result &= PaintTwisty(aRowIndex, aColumn, twistyRect, aPresContext, + aRenderingContext, aDirtyRect, remainingWidth, currX); + } + + // Now paint the icon for our cell. + nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, iconRect)) { + result &= PaintImage(aRowIndex, aColumn, iconRect, aPresContext, + aRenderingContext, aDirtyRect, remainingWidth, currX, + aBuilder); + } + + // Now paint our element, but only if we aren't a cycler column. + // XXX until we have the ability to load images, allow the view to + // insert text into cycler columns... + if (!aColumn->IsCycler()) { + nsRect elementRect(currX, cellRect.y, remainingWidth, cellRect.height); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, elementRect)) { + switch (aColumn->GetType()) { + case TreeColumn_Binding::TYPE_TEXT: + result &= PaintText(aRowIndex, aColumn, elementRect, aPresContext, + aRenderingContext, aDirtyRect, currX); + break; + case TreeColumn_Binding::TYPE_CHECKBOX: + result &= PaintCheckbox(aRowIndex, aColumn, elementRect, aPresContext, + aRenderingContext, aDirtyRect); + break; + } + } + } + + aCurrX = currX; + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintTwisty( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTwistyRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord rightEdge = aCurrX + aRemainingWidth; + // Paint the twisty, but only if we are a non-empty container. + bool shouldPaint = false; + bool isContainer = false; + nsCOMPtr<nsITreeView> view = GetExistingView(); + view->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + bool isContainerEmpty = false; + view->IsContainerEmpty(aRowIndex, &isContainerEmpty); + if (!isContainerEmpty) shouldPaint = true; + } + + // Resolve style for the twisty. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + // Obtain the margins for the twisty and then deflate our rect by that + // amount. The twisty is assumed to be contained within the deflated rect. + nsRect twistyRect(aTwistyRect); + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Deflate(twistyMargin); + + nsRect imageSize; + nsITheme* theme = GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, + aPresContext, twistyContext); + + // Subtract out the remaining width. This is done even when we don't actually + // paint a twisty in this cell, so that cells in different rows still line up. + nsRect copyRect(twistyRect); + copyRect.Inflate(twistyMargin); + aRemainingWidth -= copyRect.width; + if (!isRTL) aCurrX += copyRect.width; + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (shouldPaint) { + // Paint our borders and background for our image rect. + result &= PaintBackgroundLayer(twistyContext, aPresContext, + aRenderingContext, twistyRect, aDirtyRect); + + if (theme) { + if (isRTL) twistyRect.x = rightEdge - twistyRect.width; + // yeah, I know it says we're drawing a background, but a twisty is really + // a fg object since it doesn't have anything that gecko would want to + // draw over it. Besides, we have to prevent imagelib from drawing it. + nsRect dirty; + dirty.IntersectRect(twistyRect, aDirtyRect); + theme->DrawWidgetBackground( + &aRenderingContext, this, + twistyContext->StyleDisplay()->EffectiveAppearance(), twistyRect, + dirty); + } else { + // Time to paint the twisty. + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(twistyContext, bp); + twistyRect.Deflate(bp); + if (isRTL) twistyRect.x = rightEdge - twistyRect.width; + imageSize.Deflate(bp); + + // Get the image for drawing. + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aColumn, true, twistyContext, getter_AddRefs(image)); + if (image) { + nsPoint anchorPoint = twistyRect.TopLeft(); + + // Center the image. XXX Obey vertical-align style prop? + if (imageSize.height < twistyRect.height) { + anchorPoint.y += (twistyRect.height - imageSize.height) / 2; + } + + // Apply context paint if applicable + SVGImageContext svgContext; + SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, + *twistyContext, image); + + // Paint the image. + result &= nsLayoutUtils::DrawSingleUnscaledImage( + aRenderingContext, aPresContext, image, SamplingFilter::POINT, + anchorPoint, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, + &imageSize); + } + } + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintImage( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aImageRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX, + nsDisplayListBuilder* aBuilder) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord rightEdge = aCurrX + aRemainingWidth; + // Resolve style for the image. + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + // Obtain the margins for the image and then deflate our rect by that + // amount. The image is assumed to be contained within the deflated rect. + nsRect imageRect(aImageRect); + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageRect.Deflate(imageMargin); + + // Get the image. + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aColumn, false, imageContext, getter_AddRefs(image)); + + // Get the image destination size. + nsSize imageDestSize = GetImageDestSize(imageContext, image); + if (!imageDestSize.width || !imageDestSize.height) { + return ImgDrawResult::SUCCESS; + } + + // Get the borders and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(imageContext, bp); + + // destRect will be passed as the aDestRect argument in the DrawImage method. + // Start with the imageDestSize width and height. + nsRect destRect(0, 0, imageDestSize.width, imageDestSize.height); + // Inflate destRect for borders and padding so that we can compare/adjust + // with respect to imageRect. + destRect.Inflate(bp); + + // The destRect width and height have not been adjusted to fit within the + // cell width and height. + // We must adjust the width even if image is null, because the width is used + // to update the aRemainingWidth and aCurrX values. + // Since the height isn't used unless the image is not null, we will adjust + // the height inside the if (image) block below. + + if (destRect.width > imageRect.width) { + // The destRect is too wide to fit within the cell width. + // Adjust destRect width to fit within the cell width. + destRect.width = imageRect.width; + } else { + // The cell is wider than the destRect. + // In a cycler column, the image is centered horizontally. + if (!aColumn->IsCycler()) { + // If this column is not a cycler, we won't center the image horizontally. + // We adjust the imageRect width so that the image is placed at the start + // of the cell. + imageRect.width = destRect.width; + } + } + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (image) { + if (isRTL) imageRect.x = rightEdge - imageRect.width; + // Paint our borders and background for our image rect + result &= PaintBackgroundLayer(imageContext, aPresContext, + aRenderingContext, imageRect, aDirtyRect); + + // The destRect x and y have not been set yet. Let's do that now. + // Initially, we use the imageRect x and y. + destRect.x = imageRect.x; + destRect.y = imageRect.y; + + if (destRect.width < imageRect.width) { + // The destRect width is smaller than the cell width. + // Center the image horizontally in the cell. + // Adjust the destRect x accordingly. + destRect.x += (imageRect.width - destRect.width) / 2; + } + + // Now it's time to adjust the destRect height to fit within the cell + // height. + if (destRect.height > imageRect.height) { + // The destRect height is larger than the cell height. + // Adjust destRect height to fit within the cell height. + destRect.height = imageRect.height; + } else if (destRect.height < imageRect.height) { + // The destRect height is smaller than the cell height. + // Center the image vertically in the cell. + // Adjust the destRect y accordingly. + destRect.y += (imageRect.height - destRect.height) / 2; + } + + // It's almost time to paint the image. + // Deflate destRect for the border and padding. + destRect.Deflate(bp); + + // Compute the area where our whole image would be mapped, to get the + // desired subregion onto our actual destRect: + nsRect wholeImageDest; + CSSIntSize rawImageCSSIntSize; + if (NS_SUCCEEDED(image->GetWidth(&rawImageCSSIntSize.width)) && + NS_SUCCEEDED(image->GetHeight(&rawImageCSSIntSize.height))) { + // Get the image source rectangle - the rectangle containing the part of + // the image that we are going to display. sourceRect will be passed as + // the aSrcRect argument in the DrawImage method. + nsRect sourceRect = GetImageSourceRect(imageContext, image); + + // Let's say that the image is 100 pixels tall and that the CSS has + // specified that the destination height should be 50 pixels tall. Let's + // say that the cell height is only 20 pixels. So, in those 20 visible + // pixels, we want to see the top 20/50ths of the image. So, the + // sourceRect.height should be 100 * 20 / 50, which is 40 pixels. + // Essentially, we are scaling the image as dictated by the CSS + // destination height and width, and we are then clipping the scaled + // image by the cell width and height. + nsSize rawImageSize(CSSPixel::ToAppUnits(rawImageCSSIntSize)); + wholeImageDest = nsLayoutUtils::GetWholeImageDestination( + rawImageSize, sourceRect, nsRect(destRect.TopLeft(), imageDestSize)); + } else { + // GetWidth/GetHeight failed, so we can't easily map a subregion of the + // source image onto the destination area. + // * If this happens with a RasterImage, it probably means the image is + // in an error state, and we shouldn't draw anything. Hence, we leave + // wholeImageDest as an empty rect (its initial state). + // * If this happens with a VectorImage, it probably means the image has + // no explicit width or height attribute -- but we can still proceed and + // just treat the destination area as our whole SVG image area. Hence, we + // set wholeImageDest to the full destRect. + if (image->GetType() == imgIContainer::TYPE_VECTOR) { + wholeImageDest = destRect; + } + } + + const auto* styleEffects = imageContext->StyleEffects(); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); + if (!styleEffects->IsOpaque()) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + styleEffects->mOpacity); + } + + uint32_t drawFlags = aBuilder && aBuilder->UseHighQualityScaling() + ? imgIContainer::FLAG_HIGH_QUALITY_SCALING + : imgIContainer::FLAG_NONE; + result &= nsLayoutUtils::DrawImage( + aRenderingContext, imageContext, aPresContext, image, + nsLayoutUtils::GetSamplingFilterForFrame(this), wholeImageDest, + destRect, destRect.TopLeft(), aDirtyRect, drawFlags); + } + + // Update the aRemainingWidth and aCurrX values. + imageRect.Inflate(imageMargin); + aRemainingWidth -= imageRect.width; + if (!isRTL) { + aCurrX += imageRect.width; + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintText( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTextRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + // Now obtain the text for our cell. + nsAutoString text; + nsCOMPtr<nsITreeView> view = GetExistingView(); + view->GetCellText(aRowIndex, aColumn, text); + + // We're going to paint this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(text); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (text.Length() == 0) { + // Don't paint an empty string. XXX What about background/borders? Still + // paint? + return result; + } + + int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // Resolve style for the text. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + // Obtain the margins for the text and then deflate our rect by that + // amount. The text is assumed to be contained within the deflated rect. + nsRect textRect(aTextRect); + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(textContext, bp); + textRect.Deflate(bp); + + // Compute our text size. + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); + + nscoord height = fontMet->MaxHeight(); + nscoord baseline = fontMet->MaxAscent(); + + // Center the text. XXX Obey vertical-align style prop? + if (height < textRect.height) { + textRect.y += (textRect.height - height) / 2; + textRect.height = height; + } + + // Set our font. + AdjustForCellText(text, aRowIndex, aColumn, aRenderingContext, *fontMet, + textRect); + textRect.Inflate(bp); + + // Subtract out the remaining width. + if (!isRTL) aCurrX += textRect.width + textMargin.LeftRight(); + + result &= PaintBackgroundLayer(textContext, aPresContext, aRenderingContext, + textRect, aDirtyRect); + + // Time to paint our text. + textRect.Deflate(bp); + + // Set our color. + ColorPattern color(ToDeviceColor(textContext->StyleText()->mColor)); + + // Draw decorations. + StyleTextDecorationLine decorations = + textContext->StyleTextReset()->mTextDecorationLine; + + nscoord offset; + nscoord size; + if (decorations & (StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::UNDERLINE)) { + fontMet->GetUnderline(offset, size); + if (decorations & StyleTextDecorationLine::OVERLINE) { + nsRect r(textRect.x, textRect.y, textRect.width, size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + if (decorations & StyleTextDecorationLine::UNDERLINE) { + nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, + size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + } + if (decorations & StyleTextDecorationLine::LINE_THROUGH) { + fontMet->GetStrikeout(offset, size); + nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + const auto* styleEffects = textContext->StyleEffects(); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); + if (!styleEffects->IsOpaque()) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + styleEffects->mOpacity); + } + + aRenderingContext.SetColor( + sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor())); + nsLayoutUtils::DrawString( + this, *fontMet, &aRenderingContext, text.get(), text.Length(), + textRect.TopLeft() + nsPoint(0, baseline), cellContext); + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintCheckbox(int32_t aRowIndex, + nsTreeColumn* aColumn, + const nsRect& aCheckboxRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Resolve style for the checkbox. + ComputedStyle* checkboxContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCheckbox()); + + nscoord rightEdge = aCheckboxRect.XMost(); + + // Obtain the margins for the checkbox and then deflate our rect by that + // amount. The checkbox is assumed to be contained within the deflated rect. + nsRect checkboxRect(aCheckboxRect); + nsMargin checkboxMargin; + checkboxContext->StyleMargin()->GetMargin(checkboxMargin); + checkboxRect.Deflate(checkboxMargin); + + nsRect imageSize = GetImageSize(aRowIndex, aColumn, true, checkboxContext); + + if (imageSize.height > checkboxRect.height) { + imageSize.height = checkboxRect.height; + } + if (imageSize.width > checkboxRect.width) { + imageSize.width = checkboxRect.width; + } + + if (StyleVisibility()->mDirection == StyleDirection::Rtl) { + checkboxRect.x = rightEdge - checkboxRect.width; + } + + // Paint our borders and background for our image rect. + ImgDrawResult result = + PaintBackgroundLayer(checkboxContext, aPresContext, aRenderingContext, + checkboxRect, aDirtyRect); + + // Time to paint the checkbox. + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(checkboxContext, bp); + checkboxRect.Deflate(bp); + + // Get the image for drawing. + nsCOMPtr<imgIContainer> image; + GetImage(aRowIndex, aColumn, true, checkboxContext, getter_AddRefs(image)); + if (image) { + nsPoint pt = checkboxRect.TopLeft(); + + if (imageSize.height < checkboxRect.height) { + pt.y += (checkboxRect.height - imageSize.height) / 2; + } + + if (imageSize.width < checkboxRect.width) { + pt.x += (checkboxRect.width - imageSize.width) / 2; + } + + // Apply context paint if applicable + SVGImageContext svgContext; + SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, + *checkboxContext, image); + // Paint the image. + result &= nsLayoutUtils::DrawSingleUnscaledImage( + aRenderingContext, aPresContext, image, SamplingFilter::POINT, pt, + &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintDropFeedback( + const nsRect& aDropFeedbackRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt) { + // Paint the drop feedback in between rows. + + nscoord currX; + + // Adjust for the primary cell. + nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); + + if (primaryCol) { +#ifdef DEBUG + nsresult rv = +#endif + primaryCol->GetXInTwips(this, &currX); + NS_ASSERTION(NS_SUCCEEDED(rv), "primary column is invalid?"); + + currX += aPt.x - mHorzPosition; + } else { + currX = aDropFeedbackRect.x; + } + + PrefillPropertyArray(mSlots->mDropRow, primaryCol); + + // Resolve the style to use for the drop feedback. + ComputedStyle* feedbackContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeDropFeedback()); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Paint only if it is visible. + nsCOMPtr<nsITreeView> view = GetExistingView(); + if (feedbackContext->StyleVisibility()->IsVisibleOrCollapsed()) { + int32_t level; + view->GetLevel(mSlots->mDropRow, &level); + + // If our previous or next row has greater level use that for + // correct visual indentation. + if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) { + if (mSlots->mDropRow > 0) { + int32_t previousLevel; + view->GetLevel(mSlots->mDropRow - 1, &previousLevel); + if (previousLevel > level) level = previousLevel; + } + } else { + if (mSlots->mDropRow < mRowCount - 1) { + int32_t nextLevel; + view->GetLevel(mSlots->mDropRow + 1, &nextLevel); + if (nextLevel > level) level = nextLevel; + } + } + + currX += mIndentation * level; + + if (primaryCol) { + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + nsRect imageSize; + nsRect twistyRect; + GetTwistyRect(mSlots->mDropRow, primaryCol, imageSize, twistyRect, + aPresContext, twistyContext); + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + currX += twistyRect.width; + } + + const nsStylePosition* stylePosition = feedbackContext->StylePosition(); + + // Obtain the width for the drop feedback or use default value. + nscoord width; + if (stylePosition->mWidth.ConvertsToLength()) { + width = stylePosition->mWidth.ToLength(); + } else { + // Use default width 50px. + width = nsPresContext::CSSPixelsToAppUnits(50); + } + + // Obtain the height for the drop feedback or use default value. + nscoord height; + if (stylePosition->mHeight.ConvertsToLength()) { + height = stylePosition->mHeight.ToLength(); + } else { + // Use default height 2px. + height = nsPresContext::CSSPixelsToAppUnits(2); + } + + // Obtain the margins for the drop feedback and then deflate our rect + // by that amount. + nsRect feedbackRect(currX, aDropFeedbackRect.y, width, height); + nsMargin margin; + feedbackContext->StyleMargin()->GetMargin(margin); + feedbackRect.Deflate(margin); + + feedbackRect.y += (aDropFeedbackRect.height - height) / 2; + + // Finally paint the drop feedback. + result &= PaintBackgroundLayer(feedbackContext, aPresContext, + aRenderingContext, feedbackRect, aDirtyRect); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintBackgroundLayer( + ComputedStyle* aComputedStyle, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aRect, + const nsRect& aDirtyRect) { + const nsStyleBorder* myBorder = aComputedStyle->StyleBorder(); + nsCSSRendering::PaintBGParams params = + nsCSSRendering::PaintBGParams::ForAllLayers( + *aPresContext, aDirtyRect, aRect, this, + nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES); + ImgDrawResult result = nsCSSRendering::PaintStyleImageLayerWithSC( + params, aRenderingContext, aComputedStyle, *myBorder); + + result &= nsCSSRendering::PaintBorderWithStyleBorder( + aPresContext, aRenderingContext, this, aDirtyRect, aRect, *myBorder, + mComputedStyle, PaintBorderFlags::SyncDecodeImages); + + nsCSSRendering::PaintNonThemedOutline(aPresContext, aRenderingContext, this, + aDirtyRect, aRect, aComputedStyle); + + return result; +} + +// Scrolling +nsresult nsTreeBodyFrame::EnsureRowIsVisible(int32_t aRow) { + ScrollParts parts = GetScrollParts(); + nsresult rv = EnsureRowIsVisibleInternal(parts, aRow); + NS_ENSURE_SUCCESS(rv, rv); + UpdateScrollbars(parts); + return rv; +} + +nsresult nsTreeBodyFrame::EnsureRowIsVisibleInternal(const ScrollParts& aParts, + int32_t aRow) { + if (!mView || !mPageLength) { + return NS_OK; + } + + if (mTopRowIndex <= aRow && mTopRowIndex + mPageLength > aRow) return NS_OK; + + if (aRow < mTopRowIndex) + ScrollToRowInternal(aParts, aRow); + else { + // Bring it just on-screen. + int32_t distance = aRow - (mTopRowIndex + mPageLength) + 1; + ScrollToRowInternal(aParts, mTopRowIndex + distance); + } + + return NS_OK; +} + +nsresult nsTreeBodyFrame::EnsureCellIsVisible(int32_t aRow, + nsTreeColumn* aCol) { + if (!aCol) return NS_ERROR_INVALID_ARG; + + ScrollParts parts = GetScrollParts(); + + nscoord result = -1; + nsresult rv; + + nscoord columnPos; + rv = aCol->GetXInTwips(this, &columnPos); + if (NS_FAILED(rv)) return rv; + + nscoord columnWidth; + rv = aCol->GetWidthInTwips(this, &columnWidth); + if (NS_FAILED(rv)) return rv; + + // If the start of the column is before the + // start of the horizontal view, then scroll + if (columnPos < mHorzPosition) result = columnPos; + // If the end of the column is past the end of + // the horizontal view, then scroll + else if ((columnPos + columnWidth) > (mHorzPosition + mInnerBox.width)) + result = ((columnPos + columnWidth) - (mHorzPosition + mInnerBox.width)) + + mHorzPosition; + + if (result != -1) { + rv = ScrollHorzInternal(parts, result); + if (NS_FAILED(rv)) return rv; + } + + rv = EnsureRowIsVisibleInternal(parts, aRow); + NS_ENSURE_SUCCESS(rv, rv); + UpdateScrollbars(parts); + return rv; +} + +void nsTreeBodyFrame::ScrollToRow(int32_t aRow) { + ScrollParts parts = GetScrollParts(); + ScrollToRowInternal(parts, aRow); + UpdateScrollbars(parts); +} + +nsresult nsTreeBodyFrame::ScrollToRowInternal(const ScrollParts& aParts, + int32_t aRow) { + ScrollInternal(aParts, aRow); + + return NS_OK; +} + +void nsTreeBodyFrame::ScrollByLines(int32_t aNumLines) { + if (!mView) { + return; + } + int32_t newIndex = mTopRowIndex + aNumLines; + ScrollToRow(newIndex); +} + +void nsTreeBodyFrame::ScrollByPages(int32_t aNumPages) { + if (!mView) { + return; + } + int32_t newIndex = mTopRowIndex + aNumPages * mPageLength; + ScrollToRow(newIndex); +} + +nsresult nsTreeBodyFrame::ScrollInternal(const ScrollParts& aParts, + int32_t aRow) { + if (!mView) { + return NS_OK; + } + + // Note that we may be "over scrolled" at this point; that is the + // current mTopRowIndex may be larger than mRowCount - mPageLength. + // This can happen when items are removed for example. (bug 1085050) + + int32_t maxTopRowIndex = std::max(0, mRowCount - mPageLength); + aRow = mozilla::clamped(aRow, 0, maxTopRowIndex); + if (aRow == mTopRowIndex) { + return NS_OK; + } + mTopRowIndex = aRow; + Invalidate(); + PostScrollEvent(); + return NS_OK; +} + +nsresult nsTreeBodyFrame::ScrollHorzInternal(const ScrollParts& aParts, + int32_t aPosition) { + if (!mView || !aParts.mColumnsScrollFrame || !aParts.mHScrollbar) + return NS_OK; + + if (aPosition == mHorzPosition) return NS_OK; + + if (aPosition < 0 || aPosition > mHorzWidth) return NS_OK; + + nsRect bounds = aParts.mColumnsFrame->GetRect(); + if (aPosition > (mHorzWidth - bounds.width)) + aPosition = mHorzWidth - bounds.width; + + mHorzPosition = aPosition; + + Invalidate(); + + // Update the column scroll view + AutoWeakFrame weakFrame(this); + aParts.mColumnsScrollFrame->ScrollTo(nsPoint(mHorzPosition, 0), + ScrollMode::Instant); + if (!weakFrame.IsAlive()) { + return NS_ERROR_FAILURE; + } + // And fire off an event about it all + PostScrollEvent(); + return NS_OK; +} + +void nsTreeBodyFrame::ScrollByPage(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + ScrollByPages(aDirection); +} + +void nsTreeBodyFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + int32_t newIndex = aDirection < 0 ? 0 : mTopRowIndex; + ScrollToRow(newIndex); +} + +void nsTreeBodyFrame::ScrollByLine(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + ScrollByLines(aDirection); +} + +void nsTreeBodyFrame::ScrollByUnit( + nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection, + ScrollUnit aUnit, ScrollSnapFlags aSnapFlags /* = Disabled */) { + MOZ_ASSERT_UNREACHABLE("Can't get here, we pass false to MoveToNewPosition"); +} + +void nsTreeBodyFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) { + ScrollParts parts = GetScrollParts(); + int32_t increment = aScrollbar->GetIncrement(); + int32_t direction = 0; + if (increment < 0) { + direction = -1; + } else if (increment > 0) { + direction = 1; + } + bool isHorizontal = aScrollbar->IsHorizontal(); + + AutoWeakFrame weakFrame(this); + if (isHorizontal) { + int32_t curpos = aScrollbar->MoveToNewPosition( + nsScrollbarFrame::ImplementsScrollByUnit::No); + if (weakFrame.IsAlive()) { + ScrollHorzInternal(parts, curpos); + } + } else { + ScrollToRowInternal(parts, mTopRowIndex + direction); + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } + if (weakFrame.IsAlive()) { + UpdateScrollbars(parts); + } +} + +void nsTreeBodyFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) { + ScrollParts parts = GetScrollParts(); + + if (aOldPos == aNewPos) return; + + AutoWeakFrame weakFrame(this); + + // Vertical Scrollbar + if (parts.mVScrollbar == aScrollbar) { + nscoord rh = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + nscoord newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); + nscoord newrow = (rh > 0) ? (newIndex / rh) : 0; + ScrollInternal(parts, newrow); + // Horizontal Scrollbar + } else if (parts.mHScrollbar == aScrollbar) { + int32_t newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); + ScrollHorzInternal(parts, newIndex); + } + if (weakFrame.IsAlive()) { + UpdateScrollbars(parts); + } +} + +// The style cache. +ComputedStyle* nsTreeBodyFrame::GetPseudoComputedStyle( + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement) { + return mStyleCache.GetComputedStyle(PresContext(), mContent, mComputedStyle, + aPseudoElement, mScratchArray); +} + +XULTreeElement* nsTreeBodyFrame::GetBaseElement() { + if (!mTree) { + nsIFrame* parent = GetParent(); + while (parent) { + nsIContent* content = parent->GetContent(); + if (content && content->IsXULElement(nsGkAtoms::tree)) { + mTree = XULTreeElement::FromNodeOrNull(content->AsElement()); + break; + } + + parent = parent->GetInFlowParent(); + } + } + + return mTree; +} + +nsresult nsTreeBodyFrame::ClearStyleAndImageCaches() { + mStyleCache.Clear(); + CancelImageRequests(); + mImageCache.Clear(); + return NS_OK; +} + +void nsTreeBodyFrame::RemoveImageCacheEntry(int32_t aRowIndex, + nsTreeColumn* aCol) { + nsAutoString imageSrc; + nsCOMPtr<nsITreeView> view = GetExistingView(); + if (NS_FAILED(view->GetImageSrc(aRowIndex, aCol, imageSrc))) { + return; + } + nsTreeImageCacheEntry entry; + if (!mImageCache.Get(imageSrc, &entry)) { + return; + } + nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr); + entry.request->UnlockImage(); + entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); + mImageCache.Remove(imageSrc); +} + +/* virtual */ +void nsTreeBodyFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + SimpleXULLeafFrame::DidSetComputedStyle(aOldComputedStyle); + + // Clear the style cache; the pointers are no longer even valid + mStyleCache.Clear(); + // XXX The following is hacky, but it's not incorrect, + // and appears to fix a few bugs with style changes, like text zoom and + // dpi changes + mIndentation = GetIndentation(); + mRowHeight = GetRowHeight(); +} + +bool nsTreeBodyFrame::OffsetForHorzScroll(nsRect& rect, bool clip) { + rect.x -= mHorzPosition; + + // Scrolled out before + if (rect.XMost() <= mInnerBox.x) return false; + + // Scrolled out after + if (rect.x > mInnerBox.XMost()) return false; + + if (clip) { + nscoord leftEdge = std::max(rect.x, mInnerBox.x); + nscoord rightEdge = std::min(rect.XMost(), mInnerBox.XMost()); + rect.x = leftEdge; + rect.width = rightEdge - leftEdge; + + // Should have returned false above + NS_ASSERTION(rect.width >= 0, "horz scroll code out of sync"); + } + + return true; +} + +bool nsTreeBodyFrame::CanAutoScroll(int32_t aRowIndex) { + // Check first for partially visible last row. + if (aRowIndex == mRowCount - 1) { + nscoord y = mInnerBox.y + (aRowIndex - mTopRowIndex) * mRowHeight; + if (y < mInnerBox.height && y + mRowHeight > mInnerBox.height) return true; + } + + if (aRowIndex > 0 && aRowIndex < mRowCount - 1) return true; + + return false; +} + +// Given a dom event, figure out which row in the tree the mouse is over, +// if we should drop before/after/on that row or we should auto-scroll. +// Doesn't query the content about if the drag is allowable, that's done +// elsewhere. +// +// For containers, we break up the vertical space of the row as follows: if in +// the topmost 25%, the drop is _before_ the row the mouse is over; if in the +// last 25%, _after_; in the middle 50%, we consider it a drop _on_ the +// container. +// +// For non-containers, if the mouse is in the top 50% of the row, the drop is +// _before_ and the bottom 50% _after_ +void nsTreeBodyFrame::ComputeDropPosition(WidgetGUIEvent* aEvent, int32_t* aRow, + int16_t* aOrient, + int16_t* aScrollLines) { + *aOrient = -1; + *aScrollLines = 0; + + // Convert the event's point to our coordinates. We want it in + // the coordinates of our inner box's coordinates. + nsPoint pt = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); + int32_t xTwips = pt.x - mInnerBox.x; + int32_t yTwips = pt.y - mInnerBox.y; + + nsCOMPtr<nsITreeView> view = GetExistingView(); + *aRow = GetRowAtInternal(xTwips, yTwips); + if (*aRow >= 0) { + // Compute the top/bottom of the row in question. + int32_t yOffset = yTwips - mRowHeight * (*aRow - mTopRowIndex); + + bool isContainer = false; + view->IsContainer(*aRow, &isContainer); + if (isContainer) { + // for a container, use a 25%/50%/25% breakdown + if (yOffset < mRowHeight / 4) + *aOrient = nsITreeView::DROP_BEFORE; + else if (yOffset > mRowHeight - (mRowHeight / 4)) + *aOrient = nsITreeView::DROP_AFTER; + else + *aOrient = nsITreeView::DROP_ON; + } else { + // for a non-container use a 50%/50% breakdown + if (yOffset < mRowHeight / 2) + *aOrient = nsITreeView::DROP_BEFORE; + else + *aOrient = nsITreeView::DROP_AFTER; + } + } + + if (CanAutoScroll(*aRow)) { + // Get the max value from the look and feel service. + int32_t scrollLinesMax = + LookAndFeel::GetInt(LookAndFeel::IntID::TreeScrollLinesMax, 0); + scrollLinesMax--; + if (scrollLinesMax < 0) scrollLinesMax = 0; + + // Determine if we're w/in a margin of the top/bottom of the tree during a + // drag. This will ultimately cause us to scroll, but that's done elsewhere. + nscoord height = (3 * mRowHeight) / 4; + if (yTwips < height) { + // scroll up + *aScrollLines = + NSToIntRound(-scrollLinesMax * (1 - (float)yTwips / height) - 1); + } else if (yTwips > mRect.height - height) { + // scroll down + *aScrollLines = NSToIntRound( + scrollLinesMax * (1 - (float)(mRect.height - yTwips) / height) + 1); + } + } +} // ComputeDropPosition + +void nsTreeBodyFrame::OpenCallback(nsITimer* aTimer, void* aClosure) { + auto* self = static_cast<nsTreeBodyFrame*>(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr<nsITreeView> view = self->GetExistingView(); + if (self->mSlots->mDropRow >= 0) { + self->mSlots->mArray.AppendElement(self->mSlots->mDropRow); + view->ToggleOpenState(self->mSlots->mDropRow); + } +} + +void nsTreeBodyFrame::CloseCallback(nsITimer* aTimer, void* aClosure) { + auto* self = static_cast<nsTreeBodyFrame*>(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr<nsITreeView> view = self->GetExistingView(); + auto array = std::move(self->mSlots->mArray); + if (!view) { + return; + } + for (auto elem : Reversed(array)) { + view->ToggleOpenState(elem); + } +} + +void nsTreeBodyFrame::LazyScrollCallback(nsITimer* aTimer, void* aClosure) { + nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure); + if (self) { + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + if (self->mView) { + // Set a new timer to scroll the tree repeatedly. + self->CreateTimer(LookAndFeel::IntID::TreeScrollDelay, ScrollCallback, + nsITimer::TYPE_REPEATING_SLACK, + getter_AddRefs(self->mSlots->mTimer), + "nsTreeBodyFrame::ScrollCallback"); + self->ScrollByLines(self->mSlots->mScrollLines); + // ScrollByLines may have deleted |self|. + } + } +} + +void nsTreeBodyFrame::ScrollCallback(nsITimer* aTimer, void* aClosure) { + nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure); + if (self) { + // Don't scroll if we are already at the top or bottom of the view. + if (self->mView && self->CanAutoScroll(self->mSlots->mDropRow)) { + self->ScrollByLines(self->mSlots->mScrollLines); + } else { + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + } + } +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsTreeBodyFrame::ScrollEvent::Run() { + if (mInner) { + mInner->FireScrollEvent(); + } + return NS_OK; +} + +void nsTreeBodyFrame::FireScrollEvent() { + mScrollEvent.Forget(); + WidgetGUIEvent event(true, eScroll, nullptr); + // scroll events fired at elements don't bubble + event.mFlags.mBubbles = false; + RefPtr<nsIContent> content = GetContent(); + RefPtr<nsPresContext> presContext = PresContext(); + EventDispatcher::Dispatch(content, presContext, &event); +} + +void nsTreeBodyFrame::PostScrollEvent() { + if (mScrollEvent.IsPending()) return; + + RefPtr<ScrollEvent> event = new ScrollEvent(this); + nsresult rv = + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, do_AddRef(event)); + if (NS_FAILED(rv)) { + NS_WARNING("failed to dispatch ScrollEvent"); + } else { + mScrollEvent = std::move(event); + } +} + +void nsTreeBodyFrame::ScrollbarActivityStarted() const { + if (mScrollbarActivity) { + mScrollbarActivity->ActivityStarted(); + } +} + +void nsTreeBodyFrame::ScrollbarActivityStopped() const { + if (mScrollbarActivity) { + mScrollbarActivity->ActivityStopped(); + } +} + +void nsTreeBodyFrame::DetachImageListeners() { mCreatedListeners.Clear(); } + +void nsTreeBodyFrame::RemoveTreeImageListener(nsTreeImageListener* aListener) { + if (aListener) { + mCreatedListeners.Remove(aListener); + } +} + +#ifdef ACCESSIBILITY +static void InitCustomEvent(CustomEvent* aEvent, const nsAString& aType, + nsIWritablePropertyBag2* aDetail) { + AutoJSAPI jsapi; + if (!jsapi.Init(aEvent->GetParentObject())) { + return; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> detail(cx); + if (!ToJSValue(cx, aDetail, &detail)) { + jsapi.ClearException(); + return; + } + + aEvent->InitCustomEvent(cx, aType, /* aCanBubble = */ true, + /* aCancelable = */ false, detail); +} + +void nsTreeBodyFrame::FireRowCountChangedEvent(int32_t aIndex, int32_t aCount) { + RefPtr<XULTreeElement> tree(GetBaseElement()); + if (!tree) return; + + RefPtr<Document> doc = tree->OwnerDoc(); + MOZ_ASSERT(doc); + + RefPtr<Event> event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr<nsIWritablePropertyBag2> propBag( + do_CreateInstance("@mozilla.org/hash-property-bag;1")); + if (!propBag) { + return; + } + + // Set 'index' data - the row index rows are changed from. + propBag->SetPropertyAsInt32(u"index"_ns, aIndex); + + // Set 'count' data - the number of changed rows. + propBag->SetPropertyAsInt32(u"count"_ns, aCount); + + InitCustomEvent(treeEvent, u"TreeRowCountChanged"_ns, propBag); + + event->SetTrusted(true); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(tree, event); + asyncDispatcher->PostDOMEvent(); +} + +void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx, + int32_t aEndRowIdx, + nsTreeColumn* aStartCol, + nsTreeColumn* aEndCol) { + RefPtr<XULTreeElement> tree(GetBaseElement()); + if (!tree) return; + + RefPtr<Document> doc = tree->OwnerDoc(); + + RefPtr<Event> event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr<nsIWritablePropertyBag2> propBag( + do_CreateInstance("@mozilla.org/hash-property-bag;1")); + if (!propBag) { + return; + } + + if (aStartRowIdx != -1 && aEndRowIdx != -1) { + // Set 'startrow' data - the start index of invalidated rows. + propBag->SetPropertyAsInt32(u"startrow"_ns, aStartRowIdx); + + // Set 'endrow' data - the end index of invalidated rows. + propBag->SetPropertyAsInt32(u"endrow"_ns, aEndRowIdx); + } + + if (aStartCol && aEndCol) { + // Set 'startcolumn' data - the start index of invalidated rows. + int32_t startColIdx = aStartCol->GetIndex(); + + propBag->SetPropertyAsInt32(u"startcolumn"_ns, startColIdx); + + // Set 'endcolumn' data - the start index of invalidated rows. + int32_t endColIdx = aEndCol->GetIndex(); + propBag->SetPropertyAsInt32(u"endcolumn"_ns, endColIdx); + } + + InitCustomEvent(treeEvent, u"TreeInvalidated"_ns, propBag); + + event->SetTrusted(true); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(tree, event); + asyncDispatcher->PostDOMEvent(); +} +#endif + +class nsOverflowChecker : public Runnable { + public: + explicit nsOverflowChecker(nsTreeBodyFrame* aFrame) + : mozilla::Runnable("nsOverflowChecker"), mFrame(aFrame) {} + NS_IMETHOD Run() override { + if (mFrame.IsAlive()) { + nsTreeBodyFrame* tree = static_cast<nsTreeBodyFrame*>(mFrame.GetFrame()); + nsTreeBodyFrame::ScrollParts parts = tree->GetScrollParts(); + tree->CheckOverflow(parts); + } + return NS_OK; + } + + private: + WeakFrame mFrame; +}; + +bool nsTreeBodyFrame::FullScrollbarsUpdate(bool aNeedsFullInvalidation) { + ScrollParts parts = GetScrollParts(); + AutoWeakFrame weakFrame(this); + AutoWeakFrame weakColumnsFrame(parts.mColumnsFrame); + UpdateScrollbars(parts); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + if (aNeedsFullInvalidation) { + Invalidate(); + } + InvalidateScrollbars(parts, weakColumnsFrame); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + + // Overflow checking dispatches synchronous events, which can cause infinite + // recursion during reflow. Do the first overflow check synchronously, but + // force any nested checks to round-trip through the event loop. See bug + // 905909. + RefPtr<nsOverflowChecker> checker = new nsOverflowChecker(this); + if (!mCheckingOverflow) { + nsContentUtils::AddScriptRunner(checker); + } else { + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, checker.forget()); + } + return weakFrame.IsAlive(); +} + +void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) { + nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr); +} diff --git a/layout/xul/tree/nsTreeBodyFrame.h b/layout/xul/tree/nsTreeBodyFrame.h new file mode 100644 index 0000000000..35fa7eb1f0 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.h @@ -0,0 +1,607 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeBodyFrame_h +#define nsTreeBodyFrame_h + +#include "mozilla/AtomArray.h" +#include "mozilla/Attributes.h" + +#include "nsITreeView.h" +#include "nsIScrollbarMediator.h" +#include "nsITimer.h" +#include "nsIReflowCallback.h" +#include "nsTArray.h" +#include "nsTreeStyleCache.h" +#include "nsTreeColumns.h" +#include "nsTHashMap.h" +#include "nsTHashSet.h" +#include "imgIRequest.h" +#include "imgINotificationObserver.h" +#include "nsScrollbarFrame.h" +#include "nsThreadUtils.h" +#include "SimpleXULLeafFrame.h" +#include "mozilla/LookAndFeel.h" + +class nsFontMetrics; +class nsOverflowChecker; +class nsTreeImageListener; + +namespace mozilla { +class PresShell; +namespace layout { +class ScrollbarActivity; +} // namespace layout +} // namespace mozilla + +// An entry in the tree's image cache +struct nsTreeImageCacheEntry { + nsTreeImageCacheEntry() = default; + nsTreeImageCacheEntry(imgIRequest* aRequest, + imgINotificationObserver* aListener) + : request(aRequest), listener(aListener) {} + + nsCOMPtr<imgIRequest> request; + nsCOMPtr<imgINotificationObserver> listener; +}; + +// The actual frame that paints the cells and rows. +class nsTreeBodyFrame final : public mozilla::SimpleXULLeafFrame, + public nsIScrollbarMediator, + public nsIReflowCallback { + typedef mozilla::layout::ScrollbarActivity ScrollbarActivity; + typedef mozilla::image::ImgDrawResult ImgDrawResult; + + public: + explicit nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + ~nsTreeBodyFrame(); + + nscoord GetIntrinsicBSize() override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsTreeBodyFrame) + + // Callback handler methods for refresh driver based animations. + // Calls to these functions are forwarded from nsTreeImageListener. These + // mirror how nsImageFrame works. + void OnImageIsAnimated(imgIRequest* aRequest); + + // non-virtual signatures like nsITreeBodyFrame + already_AddRefed<nsTreeColumns> Columns() const { + RefPtr<nsTreeColumns> cols = mColumns; + return cols.forget(); + } + already_AddRefed<nsITreeView> GetExistingView() const { + nsCOMPtr<nsITreeView> view = mView; + return view.forget(); + } + already_AddRefed<nsITreeSelection> GetSelection() const; + nsresult GetView(nsITreeView** aView); + nsresult SetView(nsITreeView* aView); + bool GetFocused() const { return mFocused; } + nsresult SetFocused(bool aFocused); + nsresult GetTreeBody(mozilla::dom::Element** aElement); + int32_t RowHeight() const; + int32_t RowWidth(); + int32_t GetHorizontalPosition() const; + mozilla::Maybe<mozilla::CSSIntRegion> GetSelectionRegion(); + int32_t FirstVisibleRow() const { return mTopRowIndex; } + int32_t LastVisibleRow() const { return mTopRowIndex + mPageLength; } + int32_t PageLength() const { return mPageLength; } + nsresult EnsureRowIsVisible(int32_t aRow); + nsresult EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol); + void ScrollToRow(int32_t aRow); + void ScrollByLines(int32_t aNumLines); + void ScrollByPages(int32_t aNumPages); + nsresult Invalidate(); + nsresult InvalidateColumn(nsTreeColumn* aCol); + nsresult InvalidateRow(int32_t aRow); + nsresult InvalidateCell(int32_t aRow, nsTreeColumn* aCol); + nsresult InvalidateRange(int32_t aStart, int32_t aEnd); + int32_t GetRowAt(int32_t aX, int32_t aY); + nsresult GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, nsTreeColumn** aCol, + nsACString& aChildElt); + nsresult GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsACString& aElt, int32_t* aX, + int32_t* aY, int32_t* aWidth, int32_t* aHeight); + nsresult IsCellCropped(int32_t aRow, nsTreeColumn* aCol, bool* aResult); + nsresult RowCountChanged(int32_t aIndex, int32_t aCount); + nsresult BeginUpdateBatch(); + nsresult EndUpdateBatch(); + nsresult ClearStyleAndImageCaches(); + void RemoveImageCacheEntry(int32_t aRowIndex, nsTreeColumn* aCol); + + void CancelImageRequests(); + + void ManageReflowCallback(); + + void DidReflow(nsPresContext*, const ReflowInput*) override; + + // nsIReflowCallback + bool ReflowFinished() override; + void ReflowCallbackCanceled() override; + + // nsIScrollbarMediator + void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByUnit(nsScrollbarFrame* aScrollbar, mozilla::ScrollMode aMode, + int32_t aDirection, mozilla::ScrollUnit aUnit, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) override; + void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) override; + void ScrollbarReleased(nsScrollbarFrame* aScrollbar) override {} + void VisibilityChanged(bool aVisible) override { Invalidate(); } + nsScrollbarFrame* GetScrollbarBox(bool aVertical) override { + ScrollParts parts = GetScrollParts(); + return aVertical ? parts.mVScrollbar : parts.mHScrollbar; + } + void ScrollbarActivityStarted() const override; + void ScrollbarActivityStopped() const override; + bool IsScrollbarOnRight() const override { + return StyleVisibility()->mDirection == mozilla::StyleDirection::Ltr; + } + bool ShouldSuppressScrollbarRepaints() const override { return false; } + + // Overridden from nsIFrame to cache our pres context. + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + void Destroy(DestroyContext&) override; + + mozilla::Maybe<Cursor> GetCursor(const nsPoint&) override; + + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + friend nsIFrame* NS_NewTreeBodyFrame(mozilla::PresShell* aPresShell); + friend class nsTreeColumn; + + struct ScrollParts { + nsScrollbarFrame* mVScrollbar; + RefPtr<mozilla::dom::Element> mVScrollbarContent; + nsScrollbarFrame* mHScrollbar; + RefPtr<mozilla::dom::Element> mHScrollbarContent; + nsIFrame* mColumnsFrame; + nsIScrollableFrame* mColumnsScrollFrame; + }; + + ImgDrawResult PaintTreeBody(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder); + + // Get the base element, <tree> + mozilla::dom::XULTreeElement* GetBaseElement(); + + bool GetVerticalOverflow() const { return mVerticalOverflow; } + bool GetHorizontalOverflow() const { return mHorizontalOverflow; } + + // This returns the property array where atoms are stored for style during + // draw, whether the row currently being drawn is selected, hovered, etc. + const mozilla::AtomArray& GetPropertyArrayForCurrentDrawingItem() { + return mScratchArray; + } + + protected: + friend class nsOverflowChecker; + + // This method paints a specific column background of the tree. + ImgDrawResult PaintColumn(nsTreeColumn* aColumn, const nsRect& aColumnRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a single row in the tree. + ImgDrawResult PaintRow(int32_t aRowIndex, const nsRect& aRowRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder); + + // This method paints a single separator in the tree. + ImgDrawResult PaintSeparator(int32_t aRowIndex, const nsRect& aSeparatorRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a specific cell in a given row of the tree. + ImgDrawResult PaintCell(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aCellRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX, + nsPoint aPt, nsDisplayListBuilder* aBuilder); + + // This method paints the twisty inside a cell in the primary column of an + // tree. + ImgDrawResult PaintTwisty(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aTwistyRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, + nscoord& aCurrX); + + // This method paints the image inside the cell of an tree. + ImgDrawResult PaintImage(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aImageRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, + nscoord& aCurrX, nsDisplayListBuilder* aBuilder); + + // This method paints the text string inside a particular cell of the tree. + ImgDrawResult PaintText(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aTextRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX); + + // This method paints the checkbox inside a particular cell of the tree. + ImgDrawResult PaintCheckbox(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aCheckboxRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a drop feedback of the tree. + ImgDrawResult PaintDropFeedback(const nsRect& aDropFeedbackRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt); + + // This method is called with a specific ComputedStyle and rect to + // paint the background rect as if it were a full-blown frame. + ImgDrawResult PaintBackgroundLayer(ComputedStyle* aComputedStyle, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aRect, + const nsRect& aDirtyRect); + + // An internal hit test. aX and aY are expected to be in twips in the + // coordinate system of this frame. + int32_t GetRowAtInternal(nscoord aX, nscoord aY); + + // Check for bidi characters in the text, and if there are any, ensure + // that the prescontext is in bidi mode. + void CheckTextForBidi(nsAutoString& aText); + + void AdjustForCellText(nsAutoString& aText, int32_t aRowIndex, + nsTreeColumn* aColumn, gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, nsRect& aTextRect); + + // A helper used when hit testing. + nsCSSAnonBoxPseudoStaticAtom* GetItemWithinCellAt(nscoord aX, + const nsRect& aCellRect, + int32_t aRowIndex, + nsTreeColumn* aColumn); + + // An internal hit test. aX and aY are expected to be in twips in the + // coordinate system of this frame. + void GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, nsTreeColumn** aCol, + nsCSSAnonBoxPseudoStaticAtom** aChildElt); + + // Retrieve the area for the twisty for a cell. + nsITheme* GetTwistyRect(int32_t aRowIndex, nsTreeColumn* aColumn, + nsRect& aImageRect, nsRect& aTwistyRect, + nsPresContext* aPresContext, + ComputedStyle* aTwistyContext); + + // Fetch an image from the image cache. + nsresult GetImage(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, + ComputedStyle* aComputedStyle, imgIContainer** aResult); + + // Returns the size of a given image. This size *includes* border and + // padding. It does not include margins. + nsRect GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, + ComputedStyle* aComputedStyle); + + // Returns the destination size of the image, not including borders and + // padding. + nsSize GetImageDestSize(ComputedStyle*, imgIContainer*); + + // Returns the source rectangle of the image to be displayed. + nsRect GetImageSourceRect(ComputedStyle*, imgIContainer*); + + // Returns the height of rows in the tree. + int32_t GetRowHeight(); + + // Returns our indentation width. + int32_t GetIndentation(); + + // Calculates our width/height once border and padding have been removed. + void CalcInnerBox(); + + // Calculate the total width of our scrollable portion + nscoord CalcHorzWidth(const ScrollParts& aParts); + + // Looks up a ComputedStyle in the style cache. On a cache miss we resolve + // the pseudo-styles passed in and place them into the cache. + ComputedStyle* GetPseudoComputedStyle( + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement); + + // Retrieves the scrollbars and scrollview relevant to this treebody. We + // traverse the frame tree under our base element, in frame order, looking + // for the first relevant vertical scrollbar, horizontal scrollbar, and + // scrollable frame (with associated content and scrollable view). These + // are all volatile and should not be retained. + ScrollParts GetScrollParts(); + + // Update the curpos of the scrollbar. + void UpdateScrollbars(const ScrollParts& aParts); + + // Update the maxpos of the scrollbar. + void InvalidateScrollbars(const ScrollParts& aParts, + AutoWeakFrame& aWeakColumnsFrame); + + // Check overflow and generate events. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void CheckOverflow(const ScrollParts& aParts); + + // Calls UpdateScrollbars, Invalidate aNeedsFullInvalidation if true, + // InvalidateScrollbars and finally CheckOverflow. + // returns true if the frame is still alive after the method call. + bool FullScrollbarsUpdate(bool aNeedsFullInvalidation); + + // Use to auto-fill some of the common properties without the view having to + // do it. Examples include container, open, selected, and focus. + void PrefillPropertyArray(int32_t aRowIndex, nsTreeColumn* aCol); + + // Our internal scroll method, used by all the public scroll methods. + nsresult ScrollInternal(const ScrollParts& aParts, int32_t aRow); + nsresult ScrollToRowInternal(const ScrollParts& aParts, int32_t aRow); + nsresult ScrollHorzInternal(const ScrollParts& aParts, int32_t aPosition); + nsresult EnsureRowIsVisibleInternal(const ScrollParts& aParts, int32_t aRow); + + // Convert client pixels into appunits in our coordinate space. + nsPoint AdjustClientCoordsToBoxCoordSpace(int32_t aX, int32_t aY); + + void EnsureView(); + + nsresult GetCellWidth(int32_t aRow, nsTreeColumn* aCol, + gfxContext* aRenderingContext, nscoord& aDesiredSize, + nscoord& aCurrentSize); + + // Translate the given rect horizontally from tree coordinates into the + // coordinate system of our nsTreeBodyFrame. If clip is true, then clip the + // rect to its intersection with mInnerBox in the horizontal direction. + // Return whether the result has a nonempty intersection with mInnerBox + // after projecting both onto the horizontal coordinate axis. + bool OffsetForHorzScroll(nsRect& rect, bool clip); + + bool CanAutoScroll(int32_t aRowIndex); + + // Calc the row and above/below/on status given where the mouse currently is + // hovering. Also calc if we're in the region in which we want to auto-scroll + // the tree. A positive value of |aScrollLines| means scroll down, a negative + // value means scroll up, a zero value means that we aren't in drag scroll + // region. + void ComputeDropPosition(mozilla::WidgetGUIEvent* aEvent, int32_t* aRow, + int16_t* aOrient, int16_t* aScrollLines); + + void InvalidateDropFeedback(int32_t aRow, int16_t aOrientation) { + InvalidateRow(aRow); + if (aOrientation != nsITreeView::DROP_ON) + InvalidateRow(aRow + aOrientation); + } + + public: + /** + * Remove an nsITreeImageListener from being tracked by this frame. Only tree + * image listeners that are created by this frame are tracked. + * + * @param aListener A pointer to an nsTreeImageListener to no longer + * track. + */ + void RemoveTreeImageListener(nsTreeImageListener* aListener); + + protected: + // Create a new timer. This method is used to delay various actions like + // opening/closing folders or tree scrolling. + // aID is type of the action, aFunc is the function to be called when + // the timer fires and aType is type of timer - one shot or repeating. + nsresult CreateTimer(const mozilla::LookAndFeel::IntID aID, + nsTimerCallbackFunc aFunc, int32_t aType, + nsITimer** aTimer, const char* aName); + + static void OpenCallback(nsITimer* aTimer, void* aClosure); + + static void CloseCallback(nsITimer* aTimer, void* aClosure); + + static void LazyScrollCallback(nsITimer* aTimer, void* aClosure); + + static void ScrollCallback(nsITimer* aTimer, void* aClosure); + + class ScrollEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit ScrollEvent(nsTreeBodyFrame* aInner) + : mozilla::Runnable("nsTreeBodyFrame::ScrollEvent"), mInner(aInner) {} + void Revoke() { mInner = nullptr; } + + private: + nsTreeBodyFrame* mInner; + }; + + void PostScrollEvent(); + MOZ_CAN_RUN_SCRIPT void FireScrollEvent(); + + /** + * Clear the pointer to this frame for all nsTreeImageListeners that were + * created by this frame. + */ + void DetachImageListeners(); + +#ifdef ACCESSIBILITY + /** + * Fires 'treeRowCountChanged' event asynchronously. The event is a + * CustomEvent that is used to expose the following information structures + * via a property bag. + * + * @param aIndex the row index rows are added/removed from + * @param aCount the number of added/removed rows (the sign points to + * an operation, plus - addition, minus - removing) + */ + void FireRowCountChangedEvent(int32_t aIndex, int32_t aCount); + + /** + * Fires 'treeInvalidated' event asynchronously. The event is a CustomEvent + * that is used to expose the information structures described by method + * arguments via a property bag. + * + * @param aStartRow the start index of invalidated rows, -1 means that + * columns have been invalidated only + * @param aEndRow the end index of invalidated rows, -1 means that columns + * have been invalidated only + * @param aStartCol the start invalidated column, nullptr means that only + * rows have been invalidated + * @param aEndCol the end invalidated column, nullptr means that rows have + * been invalidated only + */ + void FireInvalidateEvent(int32_t aStartRow, int32_t aEndRow, + nsTreeColumn* aStartCol, nsTreeColumn* aEndCol); +#endif + + protected: // Data Members + class Slots { + public: + Slots() = default; + + ~Slots() { + if (mTimer) { + mTimer->Cancel(); + } + } + + friend class nsTreeBodyFrame; + + protected: + // If the drop is actually allowed here or not. + bool mDropAllowed = false; + + // True while dragging over the tree. + bool mIsDragging = false; + + // The row the mouse is hovering over during a drop. + int32_t mDropRow = -1; + + // Where we want to draw feedback (above/on this row/below) if allowed. + int16_t mDropOrient = -1; + + // Number of lines to be scrolled. + int16_t mScrollLines = 0; + + // The drag action that was received for this slot + uint32_t mDragAction = 0; + + // Timer for opening/closing spring loaded folders or scrolling the tree. + nsCOMPtr<nsITimer> mTimer; + + // An array used to keep track of all spring loaded folders. + nsTArray<int32_t> mArray; + }; + + mozilla::UniquePtr<Slots> mSlots; + + nsRevocableEventPtr<ScrollEvent> mScrollEvent; + + RefPtr<ScrollbarActivity> mScrollbarActivity; + + // The <tree> element containing this treebody. + RefPtr<mozilla::dom::XULTreeElement> mTree; + + // Cached column information. + RefPtr<nsTreeColumns> mColumns; + + // The current view for this tree widget. We get all of our row and cell data + // from the view. + nsCOMPtr<nsITreeView> mView; + + // A cache of all the ComputedStyles we have seen for rows and cells of the + // tree. This is a mapping from a list of atoms to a corresponding + // ComputedStyle. This cache stores every combination that occurs in the + // tree, so for n distinct properties, this cache could have 2 to the n + // entries (the power set of all row properties). + nsTreeStyleCache mStyleCache; + + // A hashtable that maps from URLs to image request/listener pairs. The URL + // is provided by the view or by the ComputedStyle. The ComputedStyle + // represents a resolved :-moz-tree-cell-image (or twisty) pseudo-element. + // It maps directly to an imgIRequest. + nsTHashMap<nsStringHashKey, nsTreeImageCacheEntry> mImageCache; + + // A scratch array used when looking up cached ComputedStyles. + mozilla::AtomArray mScratchArray; + + // The index of the first visible row and the # of rows visible onscreen. + // The tree only examines onscreen rows, starting from + // this index and going up to index+pageLength. + int32_t mTopRowIndex; + int32_t mPageLength; + + // The horizontal scroll position + nscoord mHorzPosition; + + // The original desired horizontal width before changing it and posting a + // reflow callback. In some cases, the desired horizontal width can first be + // different from the current desired horizontal width, only to return to + // the same value later during the same reflow. In this case, we can cancel + // the posted reflow callback and prevent an unnecessary reflow. + nscoord mOriginalHorzWidth; + // Our desired horizontal width (the width for which we actually have tree + // columns). + nscoord mHorzWidth; + // The amount by which to adjust the width of the last cell. + // This depends on whether or not the columnpicker and scrollbars are present. + nscoord mAdjustWidth; + + // Our last reflowed rect, used for invalidation, see ManageReflowCallback(). + Maybe<nsRect> mLastReflowRect; + + // Cached heights and indent info. + nsRect mInnerBox; // 4-byte aligned + int32_t mRowHeight; + int32_t mIndentation; + + int32_t mUpdateBatchNest; + + // Cached row count. + int32_t mRowCount; + + // The row the mouse is hovering over. + int32_t mMouseOverRow; + + // Whether or not we're currently focused. + bool mFocused; + + // Do we have a fixed number of onscreen rows? + bool mHasFixedRowCount; + + bool mVerticalOverflow; + bool mHorizontalOverflow; + + bool mReflowCallbackPosted; + + // Set while we flush layout to take account of effects of + // overflow/underflow event handlers + bool mCheckingOverflow; + + // Hash set to keep track of which listeners we created and thus + // have pointers to us. + nsTHashSet<nsTreeImageListener*> mCreatedListeners; + +}; // class nsTreeBodyFrame + +#endif diff --git a/layout/xul/tree/nsTreeColumns.cpp b/layout/xul/tree/nsTreeColumns.cpp new file mode 100644 index 0000000000..62a8299c6f --- /dev/null +++ b/layout/xul/tree/nsTreeColumns.cpp @@ -0,0 +1,462 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsTreeColumns.h" +#include "nsTreeUtils.h" +#include "mozilla/ComputedStyle.h" +#include "nsContentUtils.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/dom/Element.h" +#include "mozilla/CSSOrderAwareFrameIterator.h" +#include "mozilla/dom/TreeColumnBinding.h" +#include "mozilla/dom/TreeColumnsBinding.h" +#include "mozilla/dom/XULTreeElement.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// Column class that caches all the info about our column. +nsTreeColumn::nsTreeColumn(nsTreeColumns* aColumns, dom::Element* aElement) + : mContent(aElement), mColumns(aColumns), mIndex(0), mPrevious(nullptr) { + NS_ASSERTION(aElement && aElement->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL), + "nsTreeColumn's content must be a <xul:treecol>"); + + Invalidate(IgnoreErrors()); +} + +nsTreeColumn::~nsTreeColumn() { + if (mNext) { + mNext->SetPrevious(nullptr); + } +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTreeColumn) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContent) + if (tmp->mNext) { + tmp->mNext->SetPrevious(nullptr); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNext) + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTreeColumn) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNext) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumn) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumn) + +// QueryInterface implementation for nsTreeColumn +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumn) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsTreeColumn) +NS_INTERFACE_MAP_END + +nsIFrame* nsTreeColumn::GetFrame() { return mContent->GetPrimaryFrame(); } + +bool nsTreeColumn::IsLastVisible(nsTreeBodyFrame* aBodyFrame) { + NS_ASSERTION(GetFrame(), "should have checked for this already"); + + // cyclers are fixed width, don't adjust them + if (IsCycler()) return false; + + // we're certainly not the last visible if we're not visible + if (GetFrame()->GetRect().width == 0) return false; + + // try to find a visible successor + for (nsTreeColumn* next = GetNext(); next; next = next->GetNext()) { + nsIFrame* frame = next->GetFrame(); + if (frame && frame->GetRect().width > 0) return false; + } + return true; +} + +nsresult nsTreeColumn::GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, + nscoord aHeight, nsRect* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = nsRect(); + return NS_ERROR_FAILURE; + } + + const bool isRTL = + aBodyFrame->StyleVisibility()->mDirection == StyleDirection::Rtl; + *aResult = frame->GetRect(); + if (frame->StyleVisibility()->IsCollapse()) { + aResult->SizeTo(nsSize()); + } + aResult->y = aY; + aResult->height = aHeight; + if (isRTL) + aResult->x += aBodyFrame->mAdjustWidth; + else if (IsLastVisible(aBodyFrame)) + aResult->width += aBodyFrame->mAdjustWidth; + return NS_OK; +} + +nsresult nsTreeColumn::GetXInTwips(nsTreeBodyFrame* aBodyFrame, + nscoord* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = 0; + return NS_ERROR_FAILURE; + } + *aResult = frame->GetRect().x; + return NS_OK; +} + +nsresult nsTreeColumn::GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, + nscoord* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = 0; + return NS_ERROR_FAILURE; + } + *aResult = frame->GetRect().width; + if (IsLastVisible(aBodyFrame)) *aResult += aBodyFrame->mAdjustWidth; + return NS_OK; +} + +void nsTreeColumn::GetId(nsAString& aId) const { aId = GetId(); } + +void nsTreeColumn::Invalidate(ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Fetch the Id. + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, mId); + + // If we have an Id, cache the Id as an atom. + if (!mId.IsEmpty()) { + mAtom = NS_Atomize(mId); + } + + // Cache our index. + nsTreeUtils::GetColumnIndex(mContent, &mIndex); + + const nsStyleVisibility* vis = frame->StyleVisibility(); + + // Cache our text alignment policy. + const nsStyleText* textStyle = frame->StyleText(); + + mTextAlignment = textStyle->mTextAlign; + // START or END alignment sometimes means RIGHT + if ((mTextAlignment == StyleTextAlign::Start && + vis->mDirection == StyleDirection::Rtl) || + (mTextAlignment == StyleTextAlign::End && + vis->mDirection == StyleDirection::Ltr)) { + mTextAlignment = StyleTextAlign::Right; + } else if (mTextAlignment == StyleTextAlign::Start || + mTextAlignment == StyleTextAlign::End) { + mTextAlignment = StyleTextAlign::Left; + } + + // Figure out if we're the primary column (that has to have indentation + // and twisties drawn. + mIsPrimary = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::primary, + nsGkAtoms::_true, eCaseMatters); + + // Figure out if we're a cycling column (one that doesn't cause a selection + // to happen). + mIsCycler = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::cycler, + nsGkAtoms::_true, eCaseMatters); + + mIsEditable = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable, + nsGkAtoms::_true, eCaseMatters); + + mOverflow = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::overflow, + nsGkAtoms::_true, eCaseMatters); + + // Figure out our column type. Default type is text. + mType = TreeColumn_Binding::TYPE_TEXT; + static Element::AttrValuesArray typestrings[] = {nsGkAtoms::checkbox, + nullptr}; + switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + typestrings, eCaseMatters)) { + case 0: + mType = TreeColumn_Binding::TYPE_CHECKBOX; + break; + } + + // Fetch the crop style. + mCropStyle = 0; + static Element::AttrValuesArray cropstrings[] = { + nsGkAtoms::center, nsGkAtoms::left, nsGkAtoms::start, nullptr}; + switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::crop, + cropstrings, eCaseMatters)) { + case 0: + mCropStyle = 1; + break; + case 1: + case 2: + mCropStyle = 2; + break; + } +} + +nsIContent* nsTreeColumn::GetParentObject() const { return mContent; } + +/* virtual */ +JSObject* nsTreeColumn::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::TreeColumn_Binding::Wrap(aCx, this, aGivenProto); +} + +Element* nsTreeColumn::Element() { return mContent; } + +int32_t nsTreeColumn::GetX(mozilla::ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().x); +} + +int32_t nsTreeColumn::GetWidth(mozilla::ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().width); +} + +already_AddRefed<nsTreeColumn> nsTreeColumn::GetPreviousColumn() { + return do_AddRef(mPrevious); +} + +nsTreeColumns::nsTreeColumns(nsTreeBodyFrame* aTree) : mTree(aTree) {} + +nsTreeColumns::~nsTreeColumns() { nsTreeColumns::InvalidateColumns(); } + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(nsTreeColumns) + +// QueryInterface implementation for nsTreeColumns +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumns) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumns) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumns) + +nsIContent* nsTreeColumns::GetParentObject() const { + return mTree ? mTree->GetBaseElement() : nullptr; +} + +/* virtual */ +JSObject* nsTreeColumns::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::TreeColumns_Binding::Wrap(aCx, this, aGivenProto); +} + +XULTreeElement* nsTreeColumns::GetTree() const { + if (!mTree) { + return nullptr; + } + + return XULTreeElement::FromNodeOrNull(mTree->GetBaseElement()); +} + +uint32_t nsTreeColumns::Count() { + EnsureColumns(); + uint32_t count = 0; + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + ++count; + } + return count; +} + +nsTreeColumn* nsTreeColumns::GetLastColumn() { + EnsureColumns(); + nsTreeColumn* currCol = mFirstColumn; + while (currCol) { + nsTreeColumn* next = currCol->GetNext(); + if (!next) { + return currCol; + } + currCol = next; + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetSortedColumn() { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None, + nsGkAtoms::sortDirection)) { + return currCol; + } + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetKeyColumn() { + EnsureColumns(); + + nsTreeColumn* first = nullptr; + nsTreeColumn* primary = nullptr; + nsTreeColumn* sorted = nullptr; + + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + // Skip hidden columns. + if (currCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + continue; + + // Skip non-text column + if (currCol->GetType() != TreeColumn_Binding::TYPE_TEXT) continue; + + if (!first) first = currCol; + + if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None, + nsGkAtoms::sortDirection)) { + // Use sorted column as the key. + sorted = currCol; + break; + } + + if (currCol->IsPrimary()) + if (!primary) primary = currCol; + } + + if (sorted) return sorted; + if (primary) return primary; + return first; +} + +nsTreeColumn* nsTreeColumns::GetColumnFor(dom::Element* aElement) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->mContent == aElement) { + return currCol; + } + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::NamedGetter(const nsAString& aId, bool& aFound) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->GetId().Equals(aId)) { + aFound = true; + return currCol; + } + } + aFound = false; + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetNamedColumn(const nsAString& aId) { + bool dummy; + return NamedGetter(aId, dummy); +} + +void nsTreeColumns::GetSupportedNames(nsTArray<nsString>& aNames) { + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + aNames.AppendElement(currCol->GetId()); + } +} + +nsTreeColumn* nsTreeColumns::IndexedGetter(uint32_t aIndex, bool& aFound) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->GetIndex() == static_cast<int32_t>(aIndex)) { + aFound = true; + return currCol; + } + } + aFound = false; + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetColumnAt(uint32_t aIndex) { + bool dummy; + return IndexedGetter(aIndex, dummy); +} + +void nsTreeColumns::InvalidateColumns() { + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + currCol->SetColumns(nullptr); + } + mFirstColumn = nullptr; +} + +nsTreeColumn* nsTreeColumns::GetPrimaryColumn() { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->IsPrimary()) { + return currCol; + } + } + return nullptr; +} + +void nsTreeColumns::EnsureColumns() { + if (mTree && !mFirstColumn) { + nsIContent* treeContent = mTree->GetBaseElement(); + if (!treeContent) return; + + nsIContent* colsContent = + nsTreeUtils::GetDescendantChild(treeContent, nsGkAtoms::treecols); + if (!colsContent) return; + + nsIContent* colContent = + nsTreeUtils::GetDescendantChild(colsContent, nsGkAtoms::treecol); + if (!colContent) return; + + nsIFrame* colFrame = colContent->GetPrimaryFrame(); + if (!colFrame) return; + + colFrame = colFrame->GetParent(); + if (!colFrame || !colFrame->GetContent()) return; + + nsTreeColumn* currCol = nullptr; + + // Enumerate the columns in visible order + CSSOrderAwareFrameIterator iter( + colFrame, FrameChildListID::Principal, + CSSOrderAwareFrameIterator::ChildFilter::IncludeAll); + for (; !iter.AtEnd(); iter.Next()) { + nsIFrame* colFrame = iter.get(); + nsIContent* colContent = colFrame->GetContent(); + if (!colContent->IsXULElement(nsGkAtoms::treecol)) { + continue; + } + // Create a new column structure. + nsTreeColumn* col = new nsTreeColumn(this, colContent->AsElement()); + + if (currCol) { + currCol->SetNext(col); + col->SetPrevious(currCol); + } else { + mFirstColumn = col; + } + currCol = col; + } + } +} diff --git a/layout/xul/tree/nsTreeColumns.h b/layout/xul/tree/nsTreeColumns.h new file mode 100644 index 0000000000..677206a89d --- /dev/null +++ b/layout/xul/tree/nsTreeColumns.h @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeColumns_h__ +#define nsTreeColumns_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsCoord.h" +#include "nsCycleCollectionParticipant.h" +#include "nsQueryObject.h" +#include "nsWrapperCache.h" +#include "nsString.h" + +class nsAtom; +class nsTreeBodyFrame; +class nsTreeColumns; +class nsIFrame; +class nsIContent; +struct nsRect; + +namespace mozilla { +enum class StyleTextAlignKeyword : uint8_t; +using StyleTextAlign = StyleTextAlignKeyword; +class ErrorResult; +namespace dom { +class Element; +class XULTreeElement; +} // namespace dom +} // namespace mozilla + +#define NS_TREECOLUMN_IMPL_CID \ + { /* 02cd1963-4b5d-4a6c-9223-814d3ade93a3 */ \ + 0x02cd1963, 0x4b5d, 0x4a6c, { \ + 0x92, 0x23, 0x81, 0x4d, 0x3a, 0xde, 0x93, 0xa3 \ + } \ + } + +// This class is our column info. We use it to iterate our columns and to +// obtain information about each column. +class nsTreeColumn final : public nsISupports, public nsWrapperCache { + public: + nsTreeColumn(nsTreeColumns* aColumns, mozilla::dom::Element* aElement); + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_TREECOLUMN_IMPL_CID) + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn) + + // WebIDL + nsIContent* GetParentObject() const; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + mozilla::dom::Element* Element(); + + nsTreeColumns* GetColumns() const { return mColumns; } + + int32_t GetX(mozilla::ErrorResult& aRv); + int32_t GetWidth(mozilla::ErrorResult& aRv); + + void GetId(nsAString& aId) const; + int32_t Index() const { return mIndex; } + + bool Primary() const { return mIsPrimary; } + bool Cycler() const { return mIsCycler; } + bool Editable() const { return mIsEditable; } + int16_t Type() const { return mType; } + + nsTreeColumn* GetNext() const { return mNext; } + nsTreeColumn* GetPrevious() const { return mPrevious; } + + already_AddRefed<nsTreeColumn> GetPreviousColumn(); + + void Invalidate(mozilla::ErrorResult& aRv); + + friend class nsTreeBodyFrame; + friend class nsTreeColumns; + + protected: + ~nsTreeColumn(); + nsIFrame* GetFrame(); + nsIFrame* GetFrame(nsTreeBodyFrame* aBodyFrame); + // Don't call this if GetWidthInTwips or GetRect fails + bool IsLastVisible(nsTreeBodyFrame* aBodyFrame); + + /** + * Returns a rect with x and width taken from the frame's rect and specified + * y and height. May fail in case there's no frame for the column. + */ + nsresult GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, nscoord aHeight, + nsRect* aResult); + + nsresult GetXInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult); + nsresult GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult); + + void SetColumns(nsTreeColumns* aColumns) { mColumns = aColumns; } + + public: + const nsAString& GetId() const { return mId; } + nsAtom* GetAtom() { return mAtom; } + int32_t GetIndex() { return mIndex; } + + protected: + bool IsPrimary() { return mIsPrimary; } + bool IsCycler() { return mIsCycler; } + bool IsEditable() { return mIsEditable; } + bool Overflow() { return mOverflow; } + + int16_t GetType() { return mType; } + + int8_t GetCropStyle() { return mCropStyle; } + mozilla::StyleTextAlign GetTextAlignment() { return mTextAlignment; } + + void SetNext(nsTreeColumn* aNext) { + NS_ASSERTION(!mNext, "already have a next sibling"); + mNext = aNext; + } + void SetPrevious(nsTreeColumn* aPrevious) { mPrevious = aPrevious; } + + private: + /** + * Non-null nsIContent for the associated <treecol> element. + */ + RefPtr<mozilla::dom::Element> mContent; + + nsTreeColumns* mColumns; + + nsString mId; + RefPtr<nsAtom> mAtom; + + int32_t mIndex; + + bool mIsPrimary; + bool mIsCycler; + bool mIsEditable; + bool mOverflow; + + int16_t mType; + + int8_t mCropStyle; + mozilla::StyleTextAlign mTextAlignment; + + RefPtr<nsTreeColumn> mNext; + nsTreeColumn* mPrevious; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsTreeColumn, NS_TREECOLUMN_IMPL_CID) + +class nsTreeColumns final : public nsISupports, public nsWrapperCache { + private: + ~nsTreeColumns(); + + public: + explicit nsTreeColumns(nsTreeBodyFrame* aTree); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumns) + + nsIContent* GetParentObject() const; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL + mozilla::dom::XULTreeElement* GetTree() const; + uint32_t Count(); + uint32_t Length() { return Count(); } + + nsTreeColumn* GetFirstColumn() { + EnsureColumns(); + return mFirstColumn; + } + nsTreeColumn* GetLastColumn(); + + nsTreeColumn* GetPrimaryColumn(); + nsTreeColumn* GetSortedColumn(); + nsTreeColumn* GetKeyColumn(); + + nsTreeColumn* GetColumnFor(mozilla::dom::Element* aElement); + + nsTreeColumn* IndexedGetter(uint32_t aIndex, bool& aFound); + nsTreeColumn* GetColumnAt(uint32_t aIndex); + nsTreeColumn* NamedGetter(const nsAString& aId, bool& aFound); + nsTreeColumn* GetNamedColumn(const nsAString& aId); + void GetSupportedNames(nsTArray<nsString>& aNames); + + void InvalidateColumns(); + + friend class nsTreeBodyFrame; + + protected: + void SetTree(nsTreeBodyFrame* aTree) { mTree = aTree; } + + // Builds our cache of column info. + void EnsureColumns(); + + private: + nsTreeBodyFrame* mTree; + + /** + * The first column in the list of columns. All of the columns are supposed + * to be "alive", i.e. have a frame. This is achieved by clearing the columns + * list each time a treecol changes size. + * + * XXX this means that new nsTreeColumn objects are unnecessarily created + * for untouched columns. + */ + RefPtr<nsTreeColumn> mFirstColumn; +}; + +#endif // nsTreeColumns_h__ diff --git a/layout/xul/tree/nsTreeContentView.cpp b/layout/xul/tree/nsTreeContentView.cpp new file mode 100644 index 0000000000..edb5368e2a --- /dev/null +++ b/layout/xul/tree/nsTreeContentView.cpp @@ -0,0 +1,1269 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsTreeUtils.h" +#include "nsTreeContentView.h" +#include "ChildIterator.h" +#include "nsError.h" +#include "nsXULSortService.h" +#include "nsTreeBodyFrame.h" +#include "nsTreeColumns.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/TreeContentViewBinding.h" +#include "mozilla/dom/XULTreeElement.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Document.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// A content model view implementation for the tree. + +#define ROW_FLAG_CONTAINER 0x01 +#define ROW_FLAG_OPEN 0x02 +#define ROW_FLAG_EMPTY 0x04 +#define ROW_FLAG_SEPARATOR 0x08 + +class Row { + public: + Row(Element* aContent, int32_t aParentIndex) + : mContent(aContent), + mParentIndex(aParentIndex), + mSubtreeSize(0), + mFlags(0) {} + + ~Row() = default; + + void SetContainer(bool aContainer) { + aContainer ? mFlags |= ROW_FLAG_CONTAINER : mFlags &= ~ROW_FLAG_CONTAINER; + } + bool IsContainer() { return mFlags & ROW_FLAG_CONTAINER; } + + void SetOpen(bool aOpen) { + aOpen ? mFlags |= ROW_FLAG_OPEN : mFlags &= ~ROW_FLAG_OPEN; + } + bool IsOpen() { return !!(mFlags & ROW_FLAG_OPEN); } + + void SetEmpty(bool aEmpty) { + aEmpty ? mFlags |= ROW_FLAG_EMPTY : mFlags &= ~ROW_FLAG_EMPTY; + } + bool IsEmpty() { return !!(mFlags & ROW_FLAG_EMPTY); } + + void SetSeparator(bool aSeparator) { + aSeparator ? mFlags |= ROW_FLAG_SEPARATOR : mFlags &= ~ROW_FLAG_SEPARATOR; + } + bool IsSeparator() { return !!(mFlags & ROW_FLAG_SEPARATOR); } + + // Weak reference to a content item. + Element* mContent; + + // The parent index of the item, set to -1 for the top level items. + int32_t mParentIndex; + + // Subtree size for this item. + int32_t mSubtreeSize; + + private: + // State flags + int8_t mFlags; +}; + +// We don't reference count the reference to the document +// If the document goes away first, we'll be informed and we +// can drop our reference. +// If we go away first, we'll get rid of ourselves from the +// document's observer list. + +nsTreeContentView::nsTreeContentView(void) + : mTree(nullptr), mSelection(nullptr), mDocument(nullptr) {} + +nsTreeContentView::~nsTreeContentView(void) { + // Remove ourselves from mDocument's observers. + if (mDocument) mDocument->RemoveObserver(this); +} + +nsresult NS_NewTreeContentView(nsITreeView** aResult) { + *aResult = new nsTreeContentView; + if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsTreeContentView, mTree, mSelection, + mBody) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeContentView) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeContentView) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeContentView) + NS_INTERFACE_MAP_ENTRY(nsITreeView) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITreeView) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END + +JSObject* nsTreeContentView::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TreeContentView_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* nsTreeContentView::GetParentObject() { return mTree; } + +NS_IMETHODIMP +nsTreeContentView::GetRowCount(int32_t* aRowCount) { + *aRowCount = mRows.Length(); + + return NS_OK; +} + +NS_IMETHODIMP +nsTreeContentView::GetSelection(nsITreeSelection** aSelection) { + NS_IF_ADDREF(*aSelection = GetSelection()); + + return NS_OK; +} + +bool nsTreeContentView::CanTrustTreeSelection(nsISupports* aValue) { + // Untrusted content is only allowed to specify known-good views + if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) return true; + nsCOMPtr<nsINativeTreeSelection> nativeTreeSel = do_QueryInterface(aValue); + return nativeTreeSel && NS_SUCCEEDED(nativeTreeSel->EnsureNative()); +} + +NS_IMETHODIMP +nsTreeContentView::SetSelection(nsITreeSelection* aSelection) { + ErrorResult rv; + SetSelection(aSelection, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetSelection(nsITreeSelection* aSelection, + ErrorResult& aError) { + if (aSelection && !CanTrustTreeSelection(aSelection)) { + aError.ThrowSecurityError("Not allowed to set tree selection"); + return; + } + + mSelection = aSelection; +} + +void nsTreeContentView::GetRowProperties(int32_t aRow, nsAString& aProperties, + ErrorResult& aError) { + aProperties.Truncate(); + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + nsIContent* realRow; + if (row->IsSeparator()) + realRow = row->mContent; + else + realRow = nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + + if (realRow && realRow->IsElement()) { + realRow->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, + aProperties); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetRowProperties(int32_t aIndex, nsAString& aProps) { + ErrorResult rv; + GetRowProperties(aIndex, aProps, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aProperties, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) { + cell->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, aProperties); + } + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol, + nsAString& aProps) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellProperties(aRow, *aCol, aProps, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetColumnProperties(nsTreeColumn& aColumn, + nsAString& aProperties) { + RefPtr<Element> element = aColumn.Element(); + + if (element) { + element->GetAttr(nsGkAtoms::properties, aProperties); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetColumnProperties(nsTreeColumn* aCol, nsAString& aProps) { + NS_ENSURE_ARG(aCol); + + GetColumnProperties(*aCol, aProps); + return NS_OK; +} + +bool nsTreeContentView::IsContainer(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsContainer(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainer(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainer(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsContainerOpen(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsOpen(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainerOpen(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainerOpen(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsContainerEmpty(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsEmpty(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainerEmpty(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainerEmpty(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsSeparator(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsSeparator(); +} + +NS_IMETHODIMP +nsTreeContentView::IsSeparator(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsSeparator(aIndex, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +nsTreeContentView::IsSorted(bool* _retval) { + *_retval = IsSorted(); + + return NS_OK; +} + +bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + } + return false; +} + +bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer, + ErrorResult& aError) { + return CanDrop(aRow, aOrientation, aError); +} + +NS_IMETHODIMP +nsTreeContentView::CanDrop(int32_t aIndex, int32_t aOrientation, + DataTransfer* aDataTransfer, bool* _retval) { + ErrorResult rv; + *_retval = CanDrop(aIndex, aOrientation, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + } +} + +void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer, ErrorResult& aError) { + Drop(aRow, aOrientation, aError); +} + +NS_IMETHODIMP +nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer) { + ErrorResult rv; + Drop(aRow, aOrientation, rv); + return rv.StealNSResult(); +} + +int32_t nsTreeContentView::GetParentIndex(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return 0; + } + + return mRows[aRow]->mParentIndex; +} + +NS_IMETHODIMP +nsTreeContentView::GetParentIndex(int32_t aRowIndex, int32_t* _retval) { + ErrorResult rv; + *_retval = GetParentIndex(aRowIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::HasNextSibling(int32_t aRow, int32_t aAfterIndex, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + // We have a next sibling if the row is not the last in the subtree. + int32_t parentIndex = mRows[aRow]->mParentIndex; + if (parentIndex < 0) { + return uint32_t(aRow) < mRows.Length() - 1; + } + + // Compute the last index in this subtree. + int32_t lastIndex = parentIndex + (mRows[parentIndex])->mSubtreeSize; + Row* row = mRows[lastIndex].get(); + while (row->mParentIndex != parentIndex) { + lastIndex = row->mParentIndex; + row = mRows[lastIndex].get(); + } + + return aRow < lastIndex; +} + +NS_IMETHODIMP +nsTreeContentView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex, + bool* _retval) { + ErrorResult rv; + *_retval = HasNextSibling(aRowIndex, aAfterIndex, rv); + return rv.StealNSResult(); +} + +int32_t nsTreeContentView::GetLevel(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return 0; + } + + int32_t level = 0; + Row* row = mRows[aRow].get(); + while (row->mParentIndex >= 0) { + level++; + row = mRows[row->mParentIndex].get(); + } + return level; +} + +NS_IMETHODIMP +nsTreeContentView::GetLevel(int32_t aIndex, int32_t* _retval) { + ErrorResult rv; + *_retval = GetLevel(aIndex, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aSrc, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::src, aSrc); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetImageSrc(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aValue, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellValue(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aText, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + // Check for a "label" attribute - this is valid on an <treeitem> + // with a single implied column. + if (row->mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText) && + !aText.IsEmpty()) { + return; + } + + if (row->mContent->IsXULElement(nsGkAtoms::treeitem)) { + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText); + } + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellText(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetTree(XULTreeElement* aTree, ErrorResult& aError) { + aError = SetTree(aTree); +} + +NS_IMETHODIMP +nsTreeContentView::SetTree(XULTreeElement* aTree) { + ClearRows(); + + mTree = aTree; + + if (aTree) { + // Add ourselves to document's observers. + Document* document = mTree->GetComposedDoc(); + if (document) { + document->AddObserver(this); + mDocument = document; + } + + RefPtr<dom::Element> bodyElement = mTree->GetTreeBody(); + if (bodyElement) { + mBody = std::move(bodyElement); + int32_t index = 0; + Serialize(mBody, -1, &index, mRows); + } + } + + return NS_OK; +} + +void nsTreeContentView::ToggleOpenState(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // We don't serialize content right here, since content might be generated + // lazily. + Row* row = mRows[aRow].get(); + + if (row->IsOpen()) + row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"false"_ns, + true); + else + row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, + true); +} + +NS_IMETHODIMP +nsTreeContentView::ToggleOpenState(int32_t aIndex) { + ErrorResult rv; + ToggleOpenState(aIndex, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::CycleHeader(nsTreeColumn& aColumn, + ErrorResult& aError) { + if (!mTree) return; + + RefPtr<Element> column = aColumn.Element(); + nsAutoString sort; + column->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, sort); + if (!sort.IsEmpty()) { + nsAutoString sortdirection; + static Element::AttrValuesArray strings[] = { + nsGkAtoms::ascending, nsGkAtoms::descending, nullptr}; + switch (column->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sortDirection, + strings, eCaseMatters)) { + case 0: + sortdirection.AssignLiteral("descending"); + break; + case 1: + sortdirection.AssignLiteral("natural"); + break; + default: + sortdirection.AssignLiteral("ascending"); + break; + } + + nsAutoString hints; + column->GetAttr(kNameSpaceID_None, nsGkAtoms::sorthints, hints); + sortdirection.Append(' '); + sortdirection += hints; + + XULWidgetSort(mTree, sort, sortdirection); + } +} + +NS_IMETHODIMP +nsTreeContentView::CycleHeader(nsTreeColumn* aCol) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + CycleHeader(*aCol, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +nsTreeContentView::SelectionChangedXPCOM() { return NS_OK; } + +NS_IMETHODIMP +nsTreeContentView::CycleCell(int32_t aRow, nsTreeColumn* aCol) { return NS_OK; } + +bool nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn& aColumn, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell && cell->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable, + nsGkAtoms::_false, eCaseMatters)) { + return false; + } + } + + return true; +} + +NS_IMETHODIMP +nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn* aCol, bool* _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + *_retval = IsEditable(aRow, *aCol, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true); + } +} + +NS_IMETHODIMP +nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aValue) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + SetCellValue(aRow, *aCol, aValue, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::label, aValue, true); + } +} + +NS_IMETHODIMP +nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aValue) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + SetCellText(aRow, *aCol, aValue, rv); + return rv.StealNSResult(); +} + +Element* nsTreeContentView::GetItemAtIndex(int32_t aIndex, + ErrorResult& aError) { + if (!IsValidRowIndex(aIndex)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return nullptr; + } + + return mRows[aIndex]->mContent; +} + +int32_t nsTreeContentView::GetIndexOfItem(Element* aItem) { + return FindContent(aItem); +} + +void nsTreeContentView::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + if (aElement == mTree || aElement == mBody) { + mTree->ClearStyleAndImageCaches(); + mTree->Invalidate(); + } + + // We don't consider non-XUL nodes. + nsIContent* parent = nullptr; + if (!aElement->IsXULElement() || + ((parent = aElement->GetParent()) && !parent->IsXULElement())) { + return; + } + if (!aElement->IsAnyOfXULElements(nsGkAtoms::treecol, nsGkAtoms::treeitem, + nsGkAtoms::treeseparator, + nsGkAtoms::treerow, nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = aElement; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Handle changes of the hidden attribute. + if (aAttribute == nsGkAtoms::hidden && + aElement->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + bool hidden = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters); + + int32_t index = FindContent(aElement); + if (hidden && index >= 0) { + // Hide this row along with its children. + int32_t count = RemoveRow(index); + if (mTree) mTree->RowCountChanged(index, -count); + } else if (!hidden && index < 0) { + // Show this row along with its children. + nsCOMPtr<nsIContent> parent = aElement->GetParent(); + if (parent) { + InsertRowFor(parent, aElement); + } + } + + return; + } + + if (aElement->IsXULElement(nsGkAtoms::treecol)) { + if (aAttribute == nsGkAtoms::properties) { + if (mTree) { + RefPtr<nsTreeColumns> cols = mTree->GetColumns(); + if (cols) { + RefPtr<nsTreeColumn> col = cols->GetColumnFor(aElement); + mTree->InvalidateColumn(col); + } + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treeitem)) { + int32_t index = FindContent(aElement); + if (index >= 0) { + Row* row = mRows[index].get(); + if (aAttribute == nsGkAtoms::container) { + bool isContainer = + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters); + row->SetContainer(isContainer); + if (mTree) mTree->InvalidateRow(index); + } else if (aAttribute == nsGkAtoms::open) { + bool isOpen = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open, + nsGkAtoms::_true, eCaseMatters); + bool wasOpen = row->IsOpen(); + if (!isOpen && wasOpen) + CloseContainer(index); + else if (isOpen && !wasOpen) + OpenContainer(index); + } else if (aAttribute == nsGkAtoms::empty) { + bool isEmpty = + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty, + nsGkAtoms::_true, eCaseMatters); + row->SetEmpty(isEmpty); + if (mTree) mTree->InvalidateRow(index); + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treeseparator)) { + int32_t index = FindContent(aElement); + if (index >= 0) { + if (aAttribute == nsGkAtoms::properties && mTree) { + mTree->InvalidateRow(index); + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treerow)) { + if (aAttribute == nsGkAtoms::properties) { + nsCOMPtr<nsIContent> parent = aElement->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) { + mTree->InvalidateRow(index); + } + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treecell)) { + if (aAttribute == nsGkAtoms::properties || aAttribute == nsGkAtoms::mode || + aAttribute == nsGkAtoms::src || aAttribute == nsGkAtoms::value || + aAttribute == nsGkAtoms::label) { + nsIContent* parent = aElement->GetParent(); + if (parent) { + nsCOMPtr<nsIContent> grandParent = parent->GetParent(); + if (grandParent && grandParent->IsXULElement()) { + int32_t index = FindContent(grandParent); + if (index >= 0 && mTree) { + // XXX Should we make an effort to invalidate only cell ? + mTree->InvalidateRow(index); + } + } + } + } + } +} + +void nsTreeContentView::ContentAppended(nsIContent* aFirstNewContent) { + for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) { + // Our contentinserted doesn't use the index + ContentInserted(cur); + } +} + +void nsTreeContentView::ContentInserted(nsIContent* aChild) { + NS_ASSERTION(aChild, "null ptr"); + nsIContent* container = aChild->GetParent(); + + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + + // Don't allow non-XUL nodes. + if (!aChild->IsXULElement() || !container->IsXULElement()) return; + + if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator, + nsGkAtoms::treechildren, nsGkAtoms::treerow, + nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = container; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + + if (aChild->IsXULElement(nsGkAtoms::treechildren)) { + int32_t index = FindContent(container); + if (index >= 0) { + Row* row = mRows[index].get(); + row->SetEmpty(false); + if (mTree) mTree->InvalidateRow(index); + if (row->IsContainer() && row->IsOpen()) { + int32_t count = EnsureSubtree(index); + if (mTree) mTree->RowCountChanged(index + 1, count); + } + } + } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + InsertRowFor(container, aChild); + } else if (aChild->IsXULElement(nsGkAtoms::treerow)) { + int32_t index = FindContent(container); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } else if (aChild->IsXULElement(nsGkAtoms::treecell)) { + nsCOMPtr<nsIContent> parent = container->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } + } +} + +void nsTreeContentView::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + NS_ASSERTION(aChild, "null ptr"); + + nsIContent* container = aChild->GetParent(); + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + + // We don't consider non-XUL nodes. + if (!aChild->IsXULElement() || !container->IsXULElement()) return; + + if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator, + nsGkAtoms::treechildren, nsGkAtoms::treerow, + nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = container; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + + if (aChild->IsXULElement(nsGkAtoms::treechildren)) { + int32_t index = FindContent(container); + if (index >= 0) { + Row* row = mRows[index].get(); + row->SetEmpty(true); + int32_t count = RemoveSubtree(index); + // Invalidate also the row to update twisty. + if (mTree) { + mTree->InvalidateRow(index); + mTree->RowCountChanged(index + 1, -count); + } + } + } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + int32_t index = FindContent(aChild); + if (index >= 0) { + int32_t count = RemoveRow(index); + if (mTree) mTree->RowCountChanged(index, -count); + } + } else if (aChild->IsXULElement(nsGkAtoms::treerow)) { + int32_t index = FindContent(container); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } else if (aChild->IsXULElement(nsGkAtoms::treecell)) { + nsCOMPtr<nsIContent> parent = container->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } + } +} + +void nsTreeContentView::NodeWillBeDestroyed(nsINode* aNode) { + // XXXbz do we need this strong ref? Do we drop refs to self in ClearRows? + nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this); + ClearRows(); +} + +// Recursively serialize content, starting with aContent. +void nsTreeContentView::Serialize(nsIContent* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray<UniquePtr<Row>>& aRows) { + // Don't allow non-XUL nodes. + if (!aContent->IsXULElement()) return; + + dom::FlattenedChildIterator iter(aContent); + for (nsIContent* content = iter.GetNextChild(); content; + content = iter.GetNextChild()) { + int32_t count = aRows.Length(); + + if (content->IsXULElement(nsGkAtoms::treeitem)) { + SerializeItem(content->AsElement(), aParentIndex, aIndex, aRows); + } else if (content->IsXULElement(nsGkAtoms::treeseparator)) { + SerializeSeparator(content->AsElement(), aParentIndex, aIndex, aRows); + } + + *aIndex += aRows.Length() - count; + } +} + +void nsTreeContentView::SerializeItem(Element* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray<UniquePtr<Row>>& aRows) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + return; + + aRows.AppendElement(MakeUnique<Row>(aContent, aParentIndex)); + Row* row = aRows.LastElement().get(); + + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters)) { + row->SetContainer(true); + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open, + nsGkAtoms::_true, eCaseMatters)) { + row->SetOpen(true); + nsIContent* child = + nsTreeUtils::GetImmediateChild(aContent, nsGkAtoms::treechildren); + if (child && child->IsXULElement()) { + // Now, recursively serialize our child. + int32_t count = aRows.Length(); + int32_t index = 0; + Serialize(child, aParentIndex + *aIndex + 1, &index, aRows); + row->mSubtreeSize += aRows.Length() - count; + } else + row->SetEmpty(true); + } else if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty, + nsGkAtoms::_true, eCaseMatters)) { + row->SetEmpty(true); + } + } +} + +void nsTreeContentView::SerializeSeparator(Element* aContent, + int32_t aParentIndex, + int32_t* aIndex, + nsTArray<UniquePtr<Row>>& aRows) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + return; + + auto row = MakeUnique<Row>(aContent, aParentIndex); + row->SetSeparator(true); + aRows.AppendElement(std::move(row)); +} + +void nsTreeContentView::GetIndexInSubtree(nsIContent* aContainer, + nsIContent* aContent, + int32_t* aIndex) { + if (!aContainer->IsXULElement()) return; + + for (nsIContent* content = aContainer->GetFirstChild(); content; + content = content->GetNextSibling()) { + if (content == aContent) break; + + if (content->IsXULElement(nsGkAtoms::treeitem)) { + if (!content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) { + (*aIndex)++; + if (content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters) && + content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::open, nsGkAtoms::_true, + eCaseMatters)) { + nsIContent* child = + nsTreeUtils::GetImmediateChild(content, nsGkAtoms::treechildren); + if (child && child->IsXULElement()) + GetIndexInSubtree(child, aContent, aIndex); + } + } + } else if (content->IsXULElement(nsGkAtoms::treeseparator)) { + if (!content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + (*aIndex)++; + } + } +} + +int32_t nsTreeContentView::EnsureSubtree(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + + nsIContent* child; + child = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treechildren); + if (!child || !child->IsXULElement()) { + return 0; + } + + AutoTArray<UniquePtr<Row>, 8> rows; + int32_t index = 0; + Serialize(child, aIndex, &index, rows); + // Insert |rows| into |mRows| at position |aIndex|, by first creating empty + // UniquePtr entries and then Move'ing |rows|'s entries into them. (Note + // that we can't simply use InsertElementsAt with an array argument, since + // the destination can't steal ownership from its const source argument.) + UniquePtr<Row>* newRows = mRows.InsertElementsAt(aIndex + 1, rows.Length()); + for (nsTArray<Row>::index_type i = 0; i < rows.Length(); i++) { + newRows[i] = std::move(rows[i]); + } + int32_t count = rows.Length(); + + row->mSubtreeSize += count; + UpdateSubtreeSizes(row->mParentIndex, count); + + // Update parent indexes, but skip newly added rows. + // They already have correct values. + UpdateParentIndexes(aIndex, count + 1, count); + + return count; +} + +int32_t nsTreeContentView::RemoveSubtree(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + int32_t count = row->mSubtreeSize; + + mRows.RemoveElementsAt(aIndex + 1, count); + + row->mSubtreeSize -= count; + UpdateSubtreeSizes(row->mParentIndex, -count); + + UpdateParentIndexes(aIndex, 0, -count); + + return count; +} + +void nsTreeContentView::InsertRowFor(nsIContent* aParent, nsIContent* aChild) { + int32_t grandParentIndex = -1; + bool insertRow = false; + + nsCOMPtr<nsIContent> grandParent = aParent->GetParent(); + + if (grandParent->IsXULElement(nsGkAtoms::tree)) { + // Allow insertion to the outermost container. + insertRow = true; + } else { + // Test insertion to an inner container. + + // First try to find this parent in our array of rows, if we find one + // we can be sure that all other parents are open too. + grandParentIndex = FindContent(grandParent); + if (grandParentIndex >= 0) { + // Got it, now test if it is open. + if (mRows[grandParentIndex]->IsOpen()) insertRow = true; + } + } + + if (insertRow) { + int32_t index = 0; + GetIndexInSubtree(aParent, aChild, &index); + + int32_t count = InsertRow(grandParentIndex, index, aChild); + if (mTree) mTree->RowCountChanged(grandParentIndex + index + 1, count); + } +} + +int32_t nsTreeContentView::InsertRow(int32_t aParentIndex, int32_t aIndex, + nsIContent* aContent) { + AutoTArray<UniquePtr<Row>, 8> rows; + if (aContent->IsXULElement(nsGkAtoms::treeitem)) { + SerializeItem(aContent->AsElement(), aParentIndex, &aIndex, rows); + } else if (aContent->IsXULElement(nsGkAtoms::treeseparator)) { + SerializeSeparator(aContent->AsElement(), aParentIndex, &aIndex, rows); + } + + // We can't use InsertElementsAt since the destination can't steal + // ownership from its const source argument. + int32_t count = rows.Length(); + for (nsTArray<Row>::index_type i = 0; i < size_t(count); i++) { + mRows.InsertElementAt(aParentIndex + aIndex + i + 1, std::move(rows[i])); + } + + UpdateSubtreeSizes(aParentIndex, count); + + // Update parent indexes, but skip added rows. + // They already have correct values. + UpdateParentIndexes(aParentIndex + aIndex, count + 1, count); + + return count; +} + +int32_t nsTreeContentView::RemoveRow(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + int32_t count = row->mSubtreeSize + 1; + int32_t parentIndex = row->mParentIndex; + + mRows.RemoveElementsAt(aIndex, count); + + UpdateSubtreeSizes(parentIndex, -count); + + UpdateParentIndexes(aIndex, 0, -count); + + return count; +} + +void nsTreeContentView::ClearRows() { + mRows.Clear(); + mBody = nullptr; + // Remove ourselves from mDocument's observers. + if (mDocument) { + mDocument->RemoveObserver(this); + mDocument = nullptr; + } +} + +void nsTreeContentView::OpenContainer(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + row->SetOpen(true); + + int32_t count = EnsureSubtree(aIndex); + if (mTree) { + mTree->InvalidateRow(aIndex); + mTree->RowCountChanged(aIndex + 1, count); + } +} + +void nsTreeContentView::CloseContainer(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + row->SetOpen(false); + + int32_t count = RemoveSubtree(aIndex); + if (mTree) { + mTree->InvalidateRow(aIndex); + mTree->RowCountChanged(aIndex + 1, -count); + } +} + +int32_t nsTreeContentView::FindContent(nsIContent* aContent) { + for (uint32_t i = 0; i < mRows.Length(); i++) { + if (mRows[i]->mContent == aContent) { + return i; + } + } + + return -1; +} + +void nsTreeContentView::UpdateSubtreeSizes(int32_t aParentIndex, + int32_t count) { + while (aParentIndex >= 0) { + Row* row = mRows[aParentIndex].get(); + row->mSubtreeSize += count; + aParentIndex = row->mParentIndex; + } +} + +void nsTreeContentView::UpdateParentIndexes(int32_t aIndex, int32_t aSkip, + int32_t aCount) { + int32_t count = mRows.Length(); + for (int32_t i = aIndex + aSkip; i < count; i++) { + Row* row = mRows[i].get(); + if (row->mParentIndex > aIndex) { + row->mParentIndex += aCount; + } + } +} + +Element* nsTreeContentView::GetCell(nsIContent* aContainer, + nsTreeColumn& aCol) { + int32_t colIndex(aCol.GetIndex()); + + // Traverse through cells, try to find the cell by index in a row. + Element* result = nullptr; + int32_t j = 0; + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* cell = iter.GetNextChild(); cell; + cell = iter.GetNextChild()) { + if (cell->IsXULElement(nsGkAtoms::treecell)) { + if (j == colIndex) { + result = cell->AsElement(); + break; + } + j++; + } + } + + return result; +} + +bool nsTreeContentView::IsValidRowIndex(int32_t aRowIndex) { + return aRowIndex >= 0 && aRowIndex < int32_t(mRows.Length()); +} diff --git a/layout/xul/tree/nsTreeContentView.h b/layout/xul/tree/nsTreeContentView.h new file mode 100644 index 0000000000..8138ab44fc --- /dev/null +++ b/layout/xul/tree/nsTreeContentView.h @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeContentView_h__ +#define nsTreeContentView_h__ + +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsStubDocumentObserver.h" +#include "nsITreeView.h" +#include "nsITreeSelection.h" +#include "nsWrapperCache.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" + +class nsSelection; +class nsTreeColumn; +class Row; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class DataTransfer; +class Document; +class Element; +class XULTreeElement; +} // namespace dom +} // namespace mozilla + +nsresult NS_NewTreeContentView(nsITreeView** aResult); + +class nsTreeContentView final : public nsITreeView, + public nsStubDocumentObserver, + public nsWrapperCache { + typedef mozilla::dom::Element Element; + + public: + nsTreeContentView(void); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(nsTreeContentView, + nsITreeView) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject(); + + int32_t RowCount() { return mRows.Length(); } + nsITreeSelection* GetSelection() { return mSelection; } + void SetSelection(nsITreeSelection* aSelection, mozilla::ErrorResult& aError); + void GetRowProperties(int32_t aRow, nsAString& aProperties, + mozilla::ErrorResult& aError); + void GetCellProperties(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aProperies, mozilla::ErrorResult& aError); + void GetColumnProperties(nsTreeColumn& aColumn, nsAString& aProperies); + bool IsContainer(int32_t aRow, mozilla::ErrorResult& aError); + bool IsContainerOpen(int32_t aRow, mozilla::ErrorResult& aError); + bool IsContainerEmpty(int32_t aRow, mozilla::ErrorResult& aError); + bool IsSeparator(int32_t aRow, mozilla::ErrorResult& aError); + bool IsSorted() { return false; } + bool CanDrop(int32_t aRow, int32_t aOrientation, + mozilla::dom::DataTransfer* aDataTransfer, + mozilla::ErrorResult& aError); + void Drop(int32_t aRow, int32_t aOrientation, + mozilla::dom::DataTransfer* aDataTransfer, + mozilla::ErrorResult& aError); + int32_t GetParentIndex(int32_t aRow, mozilla::ErrorResult& aError); + bool HasNextSibling(int32_t aRow, int32_t aAfterIndex, + mozilla::ErrorResult& aError); + int32_t GetLevel(int32_t aRow, mozilla::ErrorResult& aError); + void GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, nsAString& aSrc, + mozilla::ErrorResult& aError); + void GetCellValue(int32_t aRow, nsTreeColumn& aColumn, nsAString& aValue, + mozilla::ErrorResult& aError); + void GetCellText(int32_t aRow, nsTreeColumn& aColumn, nsAString& aText, + mozilla::ErrorResult& aError); + void SetTree(mozilla::dom::XULTreeElement* aTree, + mozilla::ErrorResult& aError); + void ToggleOpenState(int32_t aRow, mozilla::ErrorResult& aError); + void CycleHeader(nsTreeColumn& aColumn, mozilla::ErrorResult& aError); + void SelectionChanged() {} + void CycleCell(int32_t aRow, nsTreeColumn& aColumn) {} + bool IsEditable(int32_t aRow, nsTreeColumn& aColumn, + mozilla::ErrorResult& aError); + void SetCellValue(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, mozilla::ErrorResult& aError); + void SetCellText(int32_t aRow, nsTreeColumn& aColumn, const nsAString& aText, + mozilla::ErrorResult& aError); + Element* GetItemAtIndex(int32_t aRow, mozilla::ErrorResult& aError); + int32_t GetIndexOfItem(Element* aItem); + + NS_DECL_NSITREEVIEW + + // nsIDocumentObserver + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + + static bool CanTrustTreeSelection(nsISupports* aValue); + + protected: + ~nsTreeContentView(void); + + // Recursive methods which deal with serializing of nested content. + void Serialize(nsIContent* aContent, int32_t aParentIndex, int32_t* aIndex, + nsTArray<mozilla::UniquePtr<Row>>& aRows); + + void SerializeItem(Element* aContent, int32_t aParentIndex, int32_t* aIndex, + nsTArray<mozilla::UniquePtr<Row>>& aRows); + + void SerializeSeparator(Element* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray<mozilla::UniquePtr<Row>>& aRows); + + void GetIndexInSubtree(nsIContent* aContainer, nsIContent* aContent, + int32_t* aResult); + + // Helper methods which we use to manage our plain array of rows. + int32_t EnsureSubtree(int32_t aIndex); + + int32_t RemoveSubtree(int32_t aIndex); + + int32_t InsertRow(int32_t aParentIndex, int32_t aIndex, nsIContent* aContent); + + void InsertRowFor(nsIContent* aParent, nsIContent* aChild); + + int32_t RemoveRow(int32_t aIndex); + + void ClearRows(); + + void OpenContainer(int32_t aIndex); + + void CloseContainer(int32_t aIndex); + + int32_t FindContent(nsIContent* aContent); + + void UpdateSubtreeSizes(int32_t aIndex, int32_t aCount); + + void UpdateParentIndexes(int32_t aIndex, int32_t aSkip, int32_t aCount); + + bool CanDrop(int32_t aRow, int32_t aOrientation, + mozilla::ErrorResult& aError); + void Drop(int32_t aRow, int32_t aOrientation, mozilla::ErrorResult& aError); + + // Content helpers. + Element* GetCell(nsIContent* aContainer, nsTreeColumn& aCol); + + private: + bool IsValidRowIndex(int32_t aRowIndex); + + RefPtr<mozilla::dom::XULTreeElement> mTree; + nsCOMPtr<nsITreeSelection> mSelection; + nsCOMPtr<nsIContent> mBody; + mozilla::dom::Document* mDocument; // WEAK + nsTArray<mozilla::UniquePtr<Row>> mRows; +}; + +#endif // nsTreeContentView_h__ diff --git a/layout/xul/tree/nsTreeImageListener.cpp b/layout/xul/tree/nsTreeImageListener.cpp new file mode 100644 index 0000000000..a560ada948 --- /dev/null +++ b/layout/xul/tree/nsTreeImageListener.cpp @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsTreeImageListener.h" +#include "XULTreeElement.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "nsIContent.h" +#include "nsTreeColumns.h" + +using mozilla::dom::XULTreeElement; + +NS_IMPL_ISUPPORTS(nsTreeImageListener, imgINotificationObserver) + +nsTreeImageListener::nsTreeImageListener(nsTreeBodyFrame* aTreeFrame) + : mTreeFrame(aTreeFrame), + mInvalidationSuppressed(true), + mInvalidationArea(nullptr) {} + +nsTreeImageListener::~nsTreeImageListener() { delete mInvalidationArea; } + +void nsTreeImageListener::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (aType == imgINotificationObserver::IS_ANIMATED) { + if (mTreeFrame) { + mTreeFrame->OnImageIsAnimated(aRequest); + } + return; + } + + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + // Ensure the animation (if any) is started. Note: There is no + // corresponding call to Decrement for this. This Increment will be + // 'cleaned up' by the Request when it is destroyed, but only then. + aRequest->IncrementAnimationConsumers(); + + if (mTreeFrame) { + nsCOMPtr<imgIContainer> image; + aRequest->GetImage(getter_AddRefs(image)); + if (image) { + nsPresContext* presContext = mTreeFrame->PresContext(); + image->SetAnimationMode(presContext->ImageAnimationMode()); + } + } + } + + if (aType == imgINotificationObserver::FRAME_UPDATE) { + Invalidate(); + } +} + +void nsTreeImageListener::AddCell(int32_t aIndex, nsTreeColumn* aCol) { + if (!mInvalidationArea) { + mInvalidationArea = new InvalidationArea(aCol); + mInvalidationArea->AddRow(aIndex); + } else { + InvalidationArea* currArea; + for (currArea = mInvalidationArea; currArea; + currArea = currArea->GetNext()) { + if (currArea->GetCol() == aCol) { + currArea->AddRow(aIndex); + break; + } + } + if (!currArea) { + currArea = new InvalidationArea(aCol); + currArea->SetNext(mInvalidationArea); + mInvalidationArea = currArea; + mInvalidationArea->AddRow(aIndex); + } + } +} + +void nsTreeImageListener::Invalidate() { + if (!mInvalidationSuppressed) { + for (InvalidationArea* currArea = mInvalidationArea; currArea; + currArea = currArea->GetNext()) { + // Loop from min to max, invalidating each cell that was listening for + // this image. + for (int32_t i = currArea->GetMin(); i <= currArea->GetMax(); ++i) { + if (mTreeFrame) { + RefPtr<XULTreeElement> tree = + XULTreeElement::FromNodeOrNull(mTreeFrame->GetBaseElement()); + if (tree) { + tree->InvalidateCell(i, currArea->GetCol()); + } + } + } + } + } +} + +nsTreeImageListener::InvalidationArea::InvalidationArea(nsTreeColumn* aCol) + : mCol(aCol), + mMin(-1), // min should start out "undefined" + mMax(0), + mNext(nullptr) {} + +void nsTreeImageListener::InvalidationArea::AddRow(int32_t aIndex) { + if (mMin == -1) + mMin = mMax = aIndex; + else if (aIndex < mMin) + mMin = aIndex; + else if (aIndex > mMax) + mMax = aIndex; +} + +NS_IMETHODIMP +nsTreeImageListener::ClearFrame() { + mTreeFrame = nullptr; + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeImageListener.h b/layout/xul/tree/nsTreeImageListener.h new file mode 100644 index 0000000000..f5e6e70512 --- /dev/null +++ b/layout/xul/tree/nsTreeImageListener.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeImageListener_h__ +#define nsTreeImageListener_h__ + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/Attributes.h" + +class nsTreeColumn; + +// This class handles image load observation. +class nsTreeImageListener final : public imgINotificationObserver { + public: + explicit nsTreeImageListener(nsTreeBodyFrame* aTreeFrame); + + NS_DECL_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + + NS_IMETHOD ClearFrame(); + + friend class nsTreeBodyFrame; + + protected: + ~nsTreeImageListener(); + + void UnsuppressInvalidation() { mInvalidationSuppressed = false; } + void Invalidate(); + void AddCell(int32_t aIndex, nsTreeColumn* aCol); + + private: + nsTreeBodyFrame* mTreeFrame; + + // A guard that prevents us from recursive painting. + bool mInvalidationSuppressed; + + class InvalidationArea { + public: + explicit InvalidationArea(nsTreeColumn* aCol); + ~InvalidationArea() { delete mNext; } + + friend class nsTreeImageListener; + + protected: + void AddRow(int32_t aIndex); + nsTreeColumn* GetCol() { return mCol.get(); } + int32_t GetMin() { return mMin; } + int32_t GetMax() { return mMax; } + InvalidationArea* GetNext() { return mNext; } + void SetNext(InvalidationArea* aNext) { mNext = aNext; } + + private: + RefPtr<nsTreeColumn> mCol; + int32_t mMin; + int32_t mMax; + InvalidationArea* mNext; + }; + + InvalidationArea* mInvalidationArea; +}; + +#endif // nsTreeImageListener_h__ diff --git a/layout/xul/tree/nsTreeSelection.cpp b/layout/xul/tree/nsTreeSelection.cpp new file mode 100644 index 0000000000..952a9bf376 --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.cpp @@ -0,0 +1,724 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsTreeSelection.h" +#include "XULTreeElement.h" +#include "nsITreeView.h" +#include "nsString.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsComponentManagerUtils.h" +#include "nsTreeColumns.h" + +using namespace mozilla; +using dom::XULTreeElement; + +// A helper class for managing our ranges of selection. +struct nsTreeRange { + nsTreeSelection* mSelection; + + nsTreeRange* mPrev; + nsTreeRange* mNext; + + int32_t mMin; + int32_t mMax; + + nsTreeRange(nsTreeSelection* aSel, int32_t aSingleVal) + : mSelection(aSel), + mPrev(nullptr), + mNext(nullptr), + mMin(aSingleVal), + mMax(aSingleVal) {} + nsTreeRange(nsTreeSelection* aSel, int32_t aMin, int32_t aMax) + : mSelection(aSel), + mPrev(nullptr), + mNext(nullptr), + mMin(aMin), + mMax(aMax) {} + + ~nsTreeRange() { delete mNext; } + + void Connect(nsTreeRange* aPrev = nullptr, nsTreeRange* aNext = nullptr) { + if (aPrev) + aPrev->mNext = this; + else + mSelection->mFirstRange = this; + + if (aNext) aNext->mPrev = this; + + mPrev = aPrev; + mNext = aNext; + } + + nsresult RemoveRange(int32_t aStart, int32_t aEnd) { + // This should so be a loop... sigh... + // We start past the range to remove, so no more to remove + if (aEnd < mMin) return NS_OK; + // We are the last range to be affected + if (aEnd < mMax) { + if (aStart <= mMin) { + // Just chop the start of the range off + mMin = aEnd + 1; + } else { + // We need to split the range + nsTreeRange* range = new nsTreeRange(mSelection, aEnd + 1, mMax); + if (!range) return NS_ERROR_OUT_OF_MEMORY; + + mMax = aStart - 1; + range->Connect(this, mNext); + } + return NS_OK; + } + nsTreeRange* next = mNext; + if (aStart <= mMin) { + // The remove includes us, remove ourselves from the list + if (mPrev) + mPrev->mNext = next; + else + mSelection->mFirstRange = next; + + if (next) next->mPrev = mPrev; + mPrev = mNext = nullptr; + delete this; + } else if (aStart <= mMax) { + // Just chop the end of the range off + mMax = aStart - 1; + } + return next ? next->RemoveRange(aStart, aEnd) : NS_OK; + } + + nsresult Remove(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) { + // We have found the range that contains us. + if (mMin == mMax) { + // Delete the whole range. + if (mPrev) mPrev->mNext = mNext; + if (mNext) mNext->mPrev = mPrev; + nsTreeRange* first = mSelection->mFirstRange; + if (first == this) mSelection->mFirstRange = mNext; + mNext = mPrev = nullptr; + delete this; + } else if (aIndex == mMin) + mMin++; + else if (aIndex == mMax) + mMax--; + else { + // We have to break this range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex + 1, mMax); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(this, mNext); + mMax = aIndex - 1; + } + } else if (mNext) + return mNext->Remove(aIndex); + + return NS_OK; + } + + nsresult Add(int32_t aIndex) { + if (aIndex < mMin) { + // We have found a spot to insert. + if (aIndex + 1 == mMin) + mMin = aIndex; + else if (mPrev && mPrev->mMax + 1 == aIndex) + mPrev->mMax = aIndex; + else { + // We have to create a new range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(mPrev, this); + } + } else if (mNext) + mNext->Add(aIndex); + else { + // Insert on to the end. + if (mMax + 1 == aIndex) + mMax = aIndex; + else { + // We have to create a new range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(this, nullptr); + } + } + return NS_OK; + } + + bool Contains(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) return true; + + if (mNext) return mNext->Contains(aIndex); + + return false; + } + + int32_t Count() { + int32_t total = mMax - mMin + 1; + if (mNext) total += mNext->Count(); + return total; + } + + static void CollectRanges(nsTreeRange* aRange, nsTArray<int32_t>& aRanges) { + nsTreeRange* cur = aRange; + while (cur) { + aRanges.AppendElement(cur->mMin); + aRanges.AppendElement(cur->mMax); + cur = cur->mNext; + } + } + + static void InvalidateRanges(XULTreeElement* aTree, + nsTArray<int32_t>& aRanges) { + if (aTree) { + RefPtr<nsXULElement> tree = aTree; + for (uint32_t i = 0; i < aRanges.Length(); i += 2) { + aTree->InvalidateRange(aRanges[i], aRanges[i + 1]); + } + } + } + + void Invalidate() { + nsTArray<int32_t> ranges; + CollectRanges(this, ranges); + InvalidateRanges(mSelection->mTree, ranges); + } + + void RemoveAllBut(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) { + // Invalidate everything in this list. + nsTArray<int32_t> ranges; + CollectRanges(mSelection->mFirstRange, ranges); + + mMin = aIndex; + mMax = aIndex; + + nsTreeRange* first = mSelection->mFirstRange; + if (mPrev) mPrev->mNext = mNext; + if (mNext) mNext->mPrev = mPrev; + mNext = mPrev = nullptr; + + if (first != this) { + delete mSelection->mFirstRange; + mSelection->mFirstRange = this; + } + InvalidateRanges(mSelection->mTree, ranges); + } else if (mNext) + mNext->RemoveAllBut(aIndex); + } + + void Insert(nsTreeRange* aRange) { + if (mMin >= aRange->mMax) + aRange->Connect(mPrev, this); + else if (mNext) + mNext->Insert(aRange); + else + aRange->Connect(this, nullptr); + } +}; + +nsTreeSelection::nsTreeSelection(XULTreeElement* aTree) + : mTree(aTree), + mSuppressed(false), + mCurrentIndex(-1), + mShiftSelectPivot(-1), + mFirstRange(nullptr) {} + +nsTreeSelection::~nsTreeSelection() { + delete mFirstRange; + if (mSelectTimer) mSelectTimer->Cancel(); +} + +NS_IMPL_CYCLE_COLLECTION(nsTreeSelection, mTree) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeSelection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeSelection) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeSelection) + NS_INTERFACE_MAP_ENTRY(nsITreeSelection) + NS_INTERFACE_MAP_ENTRY(nsINativeTreeSelection) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsTreeSelection::GetTree(XULTreeElement** aTree) { + NS_IF_ADDREF(*aTree = mTree); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetTree(XULTreeElement* aTree) { + if (mSelectTimer) { + mSelectTimer->Cancel(); + mSelectTimer = nullptr; + } + + mTree = aTree; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetSingle(bool* aSingle) { + if (!mTree) { + return NS_ERROR_NULL_POINTER; + } + + *aSingle = mTree->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype, + u"single"_ns, eCaseMatters); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::IsSelected(int32_t aIndex, bool* aResult) { + if (mFirstRange) + *aResult = mFirstRange->Contains(aIndex); + else + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::TimedSelect(int32_t aIndex, int32_t aMsec) { + bool suppressSelect = mSuppressed; + + if (aMsec != -1) mSuppressed = true; + + nsresult rv = Select(aIndex); + if (NS_FAILED(rv)) return rv; + + if (aMsec != -1) { + mSuppressed = suppressSelect; + if (!mSuppressed) { + if (mSelectTimer) mSelectTimer->Cancel(); + + if (!mTree) { + return NS_ERROR_UNEXPECTED; + } + nsIEventTarget* target = + mTree->OwnerDoc()->EventTargetFor(TaskCategory::Other); + NS_NewTimerWithFuncCallback(getter_AddRefs(mSelectTimer), SelectCallback, + this, aMsec, nsITimer::TYPE_ONE_SHOT, + "nsTreeSelection::SelectCallback", target); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::Select(int32_t aIndex) { + mShiftSelectPivot = -1; + + nsresult rv = SetCurrentIndex(aIndex); + if (NS_FAILED(rv)) return rv; + + if (mFirstRange) { + bool alreadySelected = mFirstRange->Contains(aIndex); + + if (alreadySelected) { + int32_t count = mFirstRange->Count(); + if (count > 1) { + // We need to deselect everything but our item. + mFirstRange->RemoveAllBut(aIndex); + FireOnSelectHandler(); + } + return NS_OK; + } else { + // Clear out our selection. + mFirstRange->Invalidate(); + delete mFirstRange; + } + } + + // Create our new selection. + mFirstRange = new nsTreeRange(this, aIndex); + if (!mFirstRange) return NS_ERROR_OUT_OF_MEMORY; + + mFirstRange->Invalidate(); + + // Fire the select event + FireOnSelectHandler(); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ToggleSelect(int32_t aIndex) { + // There are six cases that can occur on a ToggleSelect with our + // range code. + // (1) A new range should be made for a selection. + // (2) A single range is removed from the selection. + // (3) The item is added to an existing range. + // (4) The item is removed from an existing range. + // (5) The addition of the item causes two ranges to be merged. + // (6) The removal of the item causes two ranges to be split. + mShiftSelectPivot = -1; + nsresult rv = SetCurrentIndex(aIndex); + if (NS_FAILED(rv)) return rv; + + if (!mFirstRange) + Select(aIndex); + else { + if (!mFirstRange->Contains(aIndex)) { + bool single; + rv = GetSingle(&single); + if (NS_SUCCEEDED(rv) && !single) rv = mFirstRange->Add(aIndex); + } else + rv = mFirstRange->Remove(aIndex); + if (NS_SUCCEEDED(rv)) { + if (mTree) mTree->InvalidateRow(aIndex); + + FireOnSelectHandler(); + } + } + + return rv; +} + +NS_IMETHODIMP nsTreeSelection::RangedSelect(int32_t aStartIndex, + int32_t aEndIndex, bool aAugment) { + bool single; + nsresult rv = GetSingle(&single); + if (NS_FAILED(rv)) return rv; + + if ((mFirstRange || (aStartIndex != aEndIndex)) && single) return NS_OK; + + if (!aAugment) { + // Clear our selection. + if (mFirstRange) { + mFirstRange->Invalidate(); + delete mFirstRange; + mFirstRange = nullptr; + } + } + + if (aStartIndex == -1) { + if (mShiftSelectPivot != -1) + aStartIndex = mShiftSelectPivot; + else if (mCurrentIndex != -1) + aStartIndex = mCurrentIndex; + else + aStartIndex = aEndIndex; + } + + mShiftSelectPivot = aStartIndex; + rv = SetCurrentIndex(aEndIndex); + if (NS_FAILED(rv)) return rv; + + int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; + int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; + + if (aAugment && mFirstRange) { + // We need to remove all the items within our selected range from the + // selection, and then we insert our new range into the list. + nsresult rv = mFirstRange->RemoveRange(start, end); + if (NS_FAILED(rv)) return rv; + } + + nsTreeRange* range = new nsTreeRange(this, start, end); + if (!range) return NS_ERROR_OUT_OF_MEMORY; + + range->Invalidate(); + + if (aAugment && mFirstRange) + mFirstRange->Insert(range); + else + mFirstRange = range; + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ClearRange(int32_t aStartIndex, + int32_t aEndIndex) { + nsresult rv = SetCurrentIndex(aEndIndex); + if (NS_FAILED(rv)) return rv; + + if (mFirstRange) { + int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; + int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; + + mFirstRange->RemoveRange(start, end); + + if (mTree) mTree->InvalidateRange(start, end); + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ClearSelection() { + if (mFirstRange) { + mFirstRange->Invalidate(); + delete mFirstRange; + mFirstRange = nullptr; + } + mShiftSelectPivot = -1; + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SelectAll() { + if (!mTree) return NS_OK; + + nsCOMPtr<nsITreeView> view = mTree->GetView(); + if (!view) return NS_OK; + + int32_t rowCount; + view->GetRowCount(&rowCount); + bool single; + nsresult rv = GetSingle(&single); + if (NS_FAILED(rv)) return rv; + + if (rowCount == 0 || (rowCount > 1 && single)) return NS_OK; + + mShiftSelectPivot = -1; + + // Invalidate not necessary when clearing selection, since + // we're going to invalidate the world on the SelectAll. + delete mFirstRange; + + mFirstRange = new nsTreeRange(this, 0, rowCount - 1); + mFirstRange->Invalidate(); + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetRangeCount(int32_t* aResult) { + int32_t count = 0; + nsTreeRange* curr = mFirstRange; + while (curr) { + count++; + curr = curr->mNext; + } + + *aResult = count; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetRangeAt(int32_t aIndex, int32_t* aMin, + int32_t* aMax) { + *aMin = *aMax = -1; + int32_t i = -1; + nsTreeRange* curr = mFirstRange; + while (curr) { + i++; + if (i == aIndex) { + *aMin = curr->mMin; + *aMax = curr->mMax; + break; + } + curr = curr->mNext; + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetCount(int32_t* count) { + if (mFirstRange) + *count = mFirstRange->Count(); + else // No range available, so there's no selected row. + *count = 0; + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetSelectEventsSuppressed( + bool* aSelectEventsSuppressed) { + *aSelectEventsSuppressed = mSuppressed; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetSelectEventsSuppressed( + bool aSelectEventsSuppressed) { + mSuppressed = aSelectEventsSuppressed; + if (!mSuppressed) FireOnSelectHandler(); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetCurrentIndex(int32_t* aCurrentIndex) { + *aCurrentIndex = mCurrentIndex; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetCurrentIndex(int32_t aIndex) { + if (!mTree) { + return NS_ERROR_UNEXPECTED; + } + if (mCurrentIndex == aIndex) { + return NS_OK; + } + if (mCurrentIndex != -1 && mTree) mTree->InvalidateRow(mCurrentIndex); + + mCurrentIndex = aIndex; + if (!mTree) return NS_OK; + + if (aIndex != -1) mTree->InvalidateRow(aIndex); + + // Fire DOMMenuItemActive or DOMMenuItemInactive event for tree. + NS_ENSURE_STATE(mTree); + + constexpr auto DOMMenuItemActive = u"DOMMenuItemActive"_ns; + constexpr auto DOMMenuItemInactive = u"DOMMenuItemInactive"_ns; + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + mTree, (aIndex != -1 ? DOMMenuItemActive : DOMMenuItemInactive), + CanBubble::eYes, ChromeOnlyDispatch::eNo); + return asyncDispatcher->PostDOMEvent(); +} + +#define ADD_NEW_RANGE(macro_range, macro_selection, macro_start, macro_end) \ + { \ + int32_t start = macro_start; \ + int32_t end = macro_end; \ + if (start > end) { \ + end = start; \ + } \ + nsTreeRange* macro_new_range = \ + new nsTreeRange(macro_selection, start, end); \ + if (macro_range) \ + macro_range->Insert(macro_new_range); \ + else \ + macro_range = macro_new_range; \ + } + +NS_IMETHODIMP +nsTreeSelection::AdjustSelection(int32_t aIndex, int32_t aCount) { + NS_ASSERTION(aCount != 0, "adjusting by zero"); + if (!aCount) return NS_OK; + + // adjust mShiftSelectPivot, if necessary + if ((mShiftSelectPivot != 1) && (aIndex <= mShiftSelectPivot)) { + // if we are deleting and the delete includes the shift select pivot, reset + // it + if (aCount < 0 && (mShiftSelectPivot <= (aIndex - aCount - 1))) { + mShiftSelectPivot = -1; + } else { + mShiftSelectPivot += aCount; + } + } + + // adjust mCurrentIndex, if necessary + if ((mCurrentIndex != -1) && (aIndex <= mCurrentIndex)) { + // if we are deleting and the delete includes the current index, reset it + if (aCount < 0 && (mCurrentIndex <= (aIndex - aCount - 1))) { + mCurrentIndex = -1; + } else { + mCurrentIndex += aCount; + } + } + + // no selection, so nothing to do. + if (!mFirstRange) return NS_OK; + + bool selChanged = false; + nsTreeRange* oldFirstRange = mFirstRange; + nsTreeRange* curr = mFirstRange; + mFirstRange = nullptr; + while (curr) { + if (aCount > 0) { + // inserting + if (aIndex > curr->mMax) { + // adjustment happens after the range, so no change + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); + } else if (aIndex <= curr->mMin) { + // adjustment happens before the start of the range, so shift down + ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, + curr->mMax + aCount); + selChanged = true; + } else { + // adjustment happen inside the range. + // break apart the range and create two ranges + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1); + ADD_NEW_RANGE(mFirstRange, this, aIndex + aCount, curr->mMax + aCount); + selChanged = true; + } + } else { + // deleting + if (aIndex > curr->mMax) { + // adjustment happens after the range, so no change + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); + } else { + // remember, aCount is negative + selChanged = true; + int32_t lastIndexOfAdjustment = aIndex - aCount - 1; + if (aIndex <= curr->mMin) { + if (lastIndexOfAdjustment < curr->mMin) { + // adjustment happens before the start of the range, so shift up + ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, + curr->mMax + aCount); + } else if (lastIndexOfAdjustment >= curr->mMax) { + // adjustment contains the range. remove the range by not adding it + // to the newRange + } else { + // adjustment starts before the range, and ends in the middle of it, + // so trim the range + ADD_NEW_RANGE(mFirstRange, this, aIndex, curr->mMax + aCount) + } + } else if (lastIndexOfAdjustment >= curr->mMax) { + // adjustment starts in the middle of the current range, and contains + // the end of the range, so trim the range + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1) + } else { + // range contains the adjustment, so shorten the range + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax + aCount) + } + } + } + curr = curr->mNext; + } + + delete oldFirstRange; + + // Fire the select event + if (selChanged) FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP +nsTreeSelection::InvalidateSelection() { + if (mFirstRange) mFirstRange->Invalidate(); + return NS_OK; +} + +NS_IMETHODIMP +nsTreeSelection::GetShiftSelectPivot(int32_t* aIndex) { + *aIndex = mShiftSelectPivot; + return NS_OK; +} + +nsresult nsTreeSelection::FireOnSelectHandler() { + if (mSuppressed || !mTree) { + return NS_OK; + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + return NS_OK; +} + +void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) { + RefPtr<nsTreeSelection> self = static_cast<nsTreeSelection*>(aClosure); + if (self) { + self->FireOnSelectHandler(); + aTimer->Cancel(); + self->mSelectTimer = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////// + +nsresult NS_NewTreeSelection(XULTreeElement* aTree, + nsITreeSelection** aResult) { + *aResult = new nsTreeSelection(aTree); + if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(*aResult); + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeSelection.h b/layout/xul/tree/nsTreeSelection.h new file mode 100644 index 0000000000..3b0eb6b21e --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeSelection_h__ +#define nsTreeSelection_h__ + +#include "nsITreeSelection.h" +#include "nsITimer.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "XULTreeElement.h" + +class nsTreeColumn; +struct nsTreeRange; + +class nsTreeSelection final : public nsINativeTreeSelection { + public: + explicit nsTreeSelection(mozilla::dom::XULTreeElement* aTree); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(nsTreeSelection) + NS_DECL_NSITREESELECTION + + // nsINativeTreeSelection: Untrusted code can use us + NS_IMETHOD EnsureNative() override { return NS_OK; } + + friend struct nsTreeRange; + + protected: + ~nsTreeSelection(); + + nsresult FireOnSelectHandler(); + static void SelectCallback(nsITimer* aTimer, void* aClosure); + + protected: + // The tree will hold on to us through the view and let go when it dies. + RefPtr<mozilla::dom::XULTreeElement> mTree; + + bool mSuppressed; // Whether or not we should be firing onselect events. + int32_t mCurrentIndex; // The item to draw the rect around. The last one + // clicked, etc. + int32_t mShiftSelectPivot; // Used when multiple SHIFT+selects are performed + // to pivot on. + + nsTreeRange* mFirstRange; // Our list of ranges. + + nsCOMPtr<nsITimer> mSelectTimer; +}; + +nsresult NS_NewTreeSelection(mozilla::dom::XULTreeElement* aTree, + nsITreeSelection** aResult); + +#endif diff --git a/layout/xul/tree/nsTreeStyleCache.cpp b/layout/xul/tree/nsTreeStyleCache.cpp new file mode 100644 index 0000000000..d5823ac38e --- /dev/null +++ b/layout/xul/tree/nsTreeStyleCache.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsTreeStyleCache.h" +#include "mozilla/ComputedStyleInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ServoStyleSet.h" +#include "nsPresContextInlines.h" + +using namespace mozilla; + +nsTreeStyleCache::Transition::Transition(DFAState aState, nsAtom* aSymbol) + : mState(aState), mInputSymbol(aSymbol) {} + +bool nsTreeStyleCache::Transition::operator==(const Transition& aOther) const { + return aOther.mState == mState && aOther.mInputSymbol == mInputSymbol; +} + +uint32_t nsTreeStyleCache::Transition::Hash() const { + // Make a 32-bit integer that combines the low-order 16 bits of the state and + // the input symbol. + uint32_t hb = mState << 16; + uint32_t lb = (NS_PTR_TO_UINT32(mInputSymbol.get()) << 16) >> 16; + return hb + lb; +} + +// The ComputedStyle cache impl +ComputedStyle* nsTreeStyleCache::GetComputedStyle( + nsPresContext* aPresContext, nsIContent* aContent, ComputedStyle* aStyle, + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, const AtomArray& aInputWord) { + MOZ_ASSERT(nsCSSAnonBoxes::IsTreePseudoElement(aPseudoElement)); + + uint32_t count = aInputWord.Length(); + + // Go ahead and init the transition table. + if (!mTransitionTable) { + // Automatic miss. Build the table + mTransitionTable = MakeUnique<TransitionTable>(); + } + + // The first transition is always made off the supplied pseudo-element. + Transition transition(0, aPseudoElement); + DFAState currState = mTransitionTable->Get(transition); + + if (!currState) { + // We had a miss. Make a new state and add it to our hash. + currState = mNextState; + mNextState++; + mTransitionTable->InsertOrUpdate(transition, currState); + } + + for (uint32_t i = 0; i < count; i++) { + Transition transition(currState, aInputWord[i]); + currState = mTransitionTable->Get(transition); + + if (!currState) { + // We had a miss. Make a new state and add it to our hash. + currState = mNextState; + mNextState++; + mTransitionTable->InsertOrUpdate(transition, currState); + } + } + + // We're in a final state. + // Look up our ComputedStyle for this state. + ComputedStyle* result = nullptr; + if (mCache) { + result = mCache->GetWeak(currState); + } + if (!result) { + // We missed the cache. Resolve this pseudo-style. + RefPtr<ComputedStyle> newResult = + aPresContext->StyleSet()->ResolveXULTreePseudoStyle( + aContent->AsElement(), aPseudoElement, aStyle, aInputWord); + + // Normally we rely on nsIFrame::Init / RestyleManager to call this, but + // these are weird and don't use a frame, yet ::-moz-tree-twisty definitely + // pokes at list-style-image. + newResult->StartImageLoads(*aPresContext->Document()); + + // Even though xul-tree pseudos are defined in nsCSSAnonBoxList, nothing has + // ever treated them as an anon box, and they don't ever get boxes anyway. + // + // This is really weird, and probably nothing really relies on the result of + // these assert, but it's here just to avoid changing them accidentally. + MOZ_ASSERT(newResult->GetPseudoType() == PseudoStyleType::XULTree); + MOZ_ASSERT(!newResult->IsAnonBox()); + MOZ_ASSERT(!newResult->IsPseudoElement()); + + // Put the ComputedStyle in our table, transferring the owning reference to + // the table. + if (!mCache) { + mCache = MakeUnique<ComputedStyleCache>(); + } + result = newResult.get(); + mCache->InsertOrUpdate(currState, std::move(newResult)); + } + + return result; +} diff --git a/layout/xul/tree/nsTreeStyleCache.h b/layout/xul/tree/nsTreeStyleCache.h new file mode 100644 index 0000000000..b0c0e4d9dc --- /dev/null +++ b/layout/xul/tree/nsTreeStyleCache.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeStyleCache_h__ +#define nsTreeStyleCache_h__ + +#include "mozilla/AtomArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMArray.h" +#include "nsTHashMap.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/ComputedStyle.h" + +class nsIContent; + +class nsTreeStyleCache { + public: + nsTreeStyleCache() : mNextState(0) {} + + ~nsTreeStyleCache() { Clear(); } + + void Clear() { + mTransitionTable = nullptr; + mCache = nullptr; + mNextState = 0; + } + + mozilla::ComputedStyle* GetComputedStyle( + nsPresContext* aPresContext, nsIContent* aContent, + mozilla::ComputedStyle* aStyle, + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, + const mozilla::AtomArray& aInputWord); + + protected: + typedef uint32_t DFAState; + + class Transition final { + public: + Transition(DFAState aState, nsAtom* aSymbol); + bool operator==(const Transition& aOther) const; + uint32_t Hash() const; + + private: + DFAState mState; + RefPtr<nsAtom> mInputSymbol; + }; + + typedef nsTHashMap<nsGenericHashKey<Transition>, DFAState> TransitionTable; + + // A transition table for a deterministic finite automaton. The DFA + // takes as its input a single pseudoelement and an ordered set of properties. + // It transitions on an input word that is the concatenation of the + // pseudoelement supplied with the properties in the array. + // + // It transitions from state to state by looking up entries in the transition + // table (which is a mapping from (S,i)->S', where S is the current state, i + // is the next property in the input word, and S' is the state to transition + // to. + // + // If S' is not found, it is constructed and entered into the hashtable + // under the key (S,i). + // + // Once the entire word has been consumed, the final state is used + // to reference the cache table to locate the ComputedStyle. + mozilla::UniquePtr<TransitionTable> mTransitionTable; + + // The cache of all active ComputedStyles. This is a hash from + // a final state in the DFA, Sf, to the resultant ComputedStyle. + typedef nsRefPtrHashtable<nsUint32HashKey, mozilla::ComputedStyle> + ComputedStyleCache; + mozilla::UniquePtr<ComputedStyleCache> mCache; + + // An integer counter that is used when we need to make new states in the + // DFA. + DFAState mNextState; +}; + +#endif // nsTreeStyleCache_h__ diff --git a/layout/xul/tree/nsTreeUtils.cpp b/layout/xul/tree/nsTreeUtils.cpp new file mode 100644 index 0000000000..10767e22d2 --- /dev/null +++ b/layout/xul/tree/nsTreeUtils.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsReadableUtils.h" +#include "nsTreeUtils.h" +#include "ChildIterator.h" +#include "nsCRT.h" +#include "nsAtom.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" + +using namespace mozilla; + +nsresult nsTreeUtils::TokenizeProperties(const nsAString& aProperties, + AtomArray& aPropertiesArray) { + nsAString::const_iterator end; + aProperties.EndReading(end); + + nsAString::const_iterator iter; + aProperties.BeginReading(iter); + + do { + // Skip whitespace + while (iter != end && nsCRT::IsAsciiSpace(*iter)) ++iter; + + // If only whitespace, we're done + if (iter == end) break; + + // Note the first non-whitespace character + nsAString::const_iterator first = iter; + + // Advance to the next whitespace character + while (iter != end && !nsCRT::IsAsciiSpace(*iter)) ++iter; + + // XXX this would be nonsensical + NS_ASSERTION(iter != first, "eh? something's wrong here"); + if (iter == first) break; + + RefPtr<nsAtom> atom = NS_Atomize(Substring(first, iter)); + aPropertiesArray.AppendElement(atom); + } while (iter != end); + + return NS_OK; +} + +nsIContent* nsTreeUtils::GetImmediateChild(nsIContent* aContainer, + nsAtom* aTag) { + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (child->IsXULElement(aTag)) { + return child; + } + // <slot> is in the flattened tree, but <tree> code is used to work with + // <xbl:children> which is not, so recurse in <slot> here. + if (child->IsHTMLElement(nsGkAtoms::slot)) { + if (nsIContent* c = GetImmediateChild(child, aTag)) { + return c; + } + } + } + + return nullptr; +} + +nsIContent* nsTreeUtils::GetDescendantChild(nsIContent* aContainer, + nsAtom* aTag) { + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (child->IsXULElement(aTag)) { + return child; + } + + child = GetDescendantChild(child, aTag); + if (child) { + return child; + } + } + + return nullptr; +} + +nsresult nsTreeUtils::UpdateSortIndicators(dom::Element* aColumn, + const nsAString& aDirection) { + aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, aDirection, + true); + aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, u"true"_ns, true); + + // Unset sort attribute(s) on the other columns + nsCOMPtr<nsIContent> parentContent = aColumn->GetParent(); + if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols, + kNameSpaceID_XUL)) { + for (nsINode* childContent = parentContent->GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + if (childContent != aColumn && + childContent->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL)) { + childContent->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::sortDirection, true); + childContent->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::sortActive, true); + } + } + } + + return NS_OK; +} + +nsresult nsTreeUtils::GetColumnIndex(dom::Element* aColumn, int32_t* aResult) { + nsIContent* parentContent = aColumn->GetParent(); + if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols, + kNameSpaceID_XUL)) { + int32_t colIndex = 0; + + for (nsINode* childContent = parentContent->GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + if (childContent->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL)) { + if (childContent == aColumn) { + *aResult = colIndex; + return NS_OK; + } + ++colIndex; + } + } + } + + *aResult = -1; + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeUtils.h b/layout/xul/tree/nsTreeUtils.h new file mode 100644 index 0000000000..d0588f1273 --- /dev/null +++ b/layout/xul/tree/nsTreeUtils.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeUtils_h__ +#define nsTreeUtils_h__ + +#include "mozilla/AtomArray.h" +#include "nsError.h" +#include "nsString.h" +#include "nsTreeStyleCache.h" + +class nsAtom; +class nsIContent; +namespace mozilla { +namespace dom { +class Element; +} +} // namespace mozilla + +class nsTreeUtils { + public: + /** + * Parse a whitespace separated list of properties into an array + * of atoms. + */ + static nsresult TokenizeProperties(const nsAString& aProperties, + mozilla::AtomArray& aPropertiesArray); + + static nsIContent* GetImmediateChild(nsIContent* aContainer, nsAtom* aTag); + + static nsIContent* GetDescendantChild(nsIContent* aContainer, nsAtom* aTag); + + static nsresult UpdateSortIndicators(mozilla::dom::Element* aColumn, + const nsAString& aDirection); + + static nsresult GetColumnIndex(mozilla::dom::Element* aColumn, + int32_t* aResult); +}; + +#endif // nsTreeUtils_h__ |