diff options
Diffstat (limited to 'layout/forms')
170 files changed, 20419 insertions, 0 deletions
diff --git a/layout/forms/crashtests/1102791.html b/layout/forms/crashtests/1102791.html new file mode 100644 index 000000000..5bcdd80a6 --- /dev/null +++ b/layout/forms/crashtests/1102791.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html class="reftest-print"><head> + <meta charset="utf-8"> + <title>Testcase for bug 1102791</title> + <style type="text/css"> + +html,body { + color:black; background-color:white; font-size:16px; padding:0; margin:0; +} + +button { + position: absolute; + -moz-appearance: none; + background: transparent; + padding: 0; + border-style:none; +} +button::before { + position: absolute; + content: "::before"; + width: 10px; + height: 200em; + border: 1px solid black; +} + + </style> +</head> +<body> + +<button></button> + +</body> +</html> diff --git a/layout/forms/crashtests/1140216.html b/layout/forms/crashtests/1140216.html new file mode 100644 index 000000000..72612b8b3 --- /dev/null +++ b/layout/forms/crashtests/1140216.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<style> + +input { -moz-appearance: textfield; } + +</style> +<script> + +function boom() +{ + window.getComputedStyle(x, "::-moz-number-spin-down").getPropertyValue("color"); +} + +</script> +<body onload="boom();"> +<input type="number" id="x"> +</body> +</html> diff --git a/layout/forms/crashtests/1182414.html b/layout/forms/crashtests/1182414.html new file mode 100644 index 000000000..5d15f3905 --- /dev/null +++ b/layout/forms/crashtests/1182414.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-print"> +<head> +<meta charset="UTF-8"> +<style type="text/css"> +#menu { position: fixed; left: 0px; top: 0px; } +</style> +</head> +<body> + <svg id="canvas" width="2427" height="2295.5" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg> + +<div id="menu"> + <input id="chooseSize" type="range"> +</div> +</body> +</html> + diff --git a/layout/forms/crashtests/1212688.html b/layout/forms/crashtests/1212688.html new file mode 100644 index 000000000..68262d907 --- /dev/null +++ b/layout/forms/crashtests/1212688.html @@ -0,0 +1,27 @@ +<style type="text/css">
+ optgroup {overflow-x: hidden;}
+</style>
+<select>
+ <optgroup label="optgroup">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ <option>6</option>
+ <option>7</option>
+ <option>8</option>
+ <option>9</option>
+ <option>10</option>
+ <option>11</option>
+ <option>12</option>
+ <option>13</option>
+ <option>14</option>
+ <option>15</option>
+ <option>16</option>
+ <option>17</option>
+ <option>18</option>
+ <option>19</option>
+ <option>20</option>
+</optgroup>
+</select>
diff --git a/layout/forms/crashtests/1228670.xhtml b/layout/forms/crashtests/1228670.xhtml new file mode 100644 index 000000000..cc8d309c0 --- /dev/null +++ b/layout/forms/crashtests/1228670.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <select> + <optgroup style="display: list-item; list-style: inside;"></optgroup> + </select> + </body> +</html> diff --git a/layout/forms/crashtests/1279354.html b/layout/forms/crashtests/1279354.html new file mode 100644 index 000000000..1f4f40942 --- /dev/null +++ b/layout/forms/crashtests/1279354.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html class="reftest-print"><head> + <meta charset="utf-8"> + <title>Testcase for bug 1279354</title> +</head> +<body> + +<div style="position:fixed"><progress></progress></div> +1 +<br style="page-break-after: always"> +2 +<br style="page-break-after: always"> +3 +<br style="page-break-after: always"> + +</body> +</html> diff --git a/layout/forms/crashtests/166750-1.html b/layout/forms/crashtests/166750-1.html new file mode 100644 index 000000000..d873be1b2 --- /dev/null +++ b/layout/forms/crashtests/166750-1.html @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head><title>Killer</title></head>
+<body>
+ <form style="overflow: auto;">
+ <select style="position: fixed;">
+ <option>First</option>
+ <option>Second</option>
+ <option>Third</option>
+ </select>
+ </form>
+</body>
+</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/200347-1.html b/layout/forms/crashtests/200347-1.html new file mode 100644 index 000000000..b41ec8365 --- /dev/null +++ b/layout/forms/crashtests/200347-1.html @@ -0,0 +1,8 @@ +<html>
+ <body>
+ <fieldset style="width: 300px; position: fixed">
+ <legend>Crash test</legend>
+ <div style="float: right; background-color: orange; border: 1px solid black">Hello, my name is Inigo Montoya.</div> hello world content is the best content around, I love hello world content to death, especially when it wraps; that just gives me the chills. Anything less than hello world content is uncivilized.
+ </fieldset>
+ </body>
+</html>
diff --git a/layout/forms/crashtests/203041-1.html b/layout/forms/crashtests/203041-1.html new file mode 100644 index 000000000..32d95b40e --- /dev/null +++ b/layout/forms/crashtests/203041-1.html @@ -0,0 +1,24 @@ +<html>
+
+<head></head>
+
+<body>
+<form method="post" action="#" enctype="multipart/form-data" name="content">
+
+ <div id="sshot" style="position:absolute; left:0; top:70; width:600;
+visibility:visible">
+ <input type="file" name="sshot-1" size="20">
+ </div>
+
+ <div id="comment" style="position:absolute; left:0; top:70; width:600;
+visibility:hidden"></div>
+
+ <script language="JavaScript">
+ <!--
+ document.documentElement.offsetHeight;
+ document.getElementById('sshot').style.display = 'none';
+ document.getElementById('comment').style.display = 'none';
+ // -->
+ </script>
+</form>
+</body></html>
\ No newline at end of file diff --git a/layout/forms/crashtests/213390-1.html b/layout/forms/crashtests/213390-1.html new file mode 100644 index 000000000..0ff94ec54 --- /dev/null +++ b/layout/forms/crashtests/213390-1.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
+
+<HTML>
+ <BODY>
+
+ <EMBED>
+
+ <TABLE>
+ <TR>
+ <TD STYLE="FONT: 10px Arial;">
+ <A STYLE="FONT: 11px Arial;">1</A>
+ </TD>
+ </TR>
+ </TABLE>
+
+ <DIV STYLE="POSITION: absolute;">
+ <FORM>
+ <SELECT STYLE="FONT: 11px Arial;">
+ </FORM>
+ </DIV>
+
+ </BODY>
+</HTML>
diff --git a/layout/forms/crashtests/258101-1.html b/layout/forms/crashtests/258101-1.html new file mode 100644 index 000000000..245917cbf --- /dev/null +++ b/layout/forms/crashtests/258101-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta content="text/html; charset=ISO-8859-1"
+ http-equiv="content-type">
+ <title>Crash Testcase</title>
+ <script type="text/javascript">
+function crash()
+{
+ var inp = document.getElementById("theinp");
+ inp.type = "file";
+}
+ </script>
+</head>
+<body onload="crash();">
+<input id="theinp" type="text">
+</body>
+</html>
diff --git a/layout/forms/crashtests/266225-1.html b/layout/forms/crashtests/266225-1.html new file mode 100644 index 000000000..c714125dd --- /dev/null +++ b/layout/forms/crashtests/266225-1.html @@ -0,0 +1,7 @@ +<HTML>
+<HEAD>
+</HEAD>
+<BODY>
+<FIELDSET STYLE="float:right; text-indent:999px;">Test</FIELDSET>
+</BODY>
+</HTML>
diff --git a/layout/forms/crashtests/310426-1.xhtml b/layout/forms/crashtests/310426-1.xhtml new file mode 100644 index 000000000..683dba679 --- /dev/null +++ b/layout/forms/crashtests/310426-1.xhtml @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+
+<body>
+
+<select><span style="position: absolute;" /></select>
+
+</body>
+
+</html>
diff --git a/layout/forms/crashtests/310520-1.xhtml b/layout/forms/crashtests/310520-1.xhtml new file mode 100644 index 000000000..5a76a6de1 --- /dev/null +++ b/layout/forms/crashtests/310520-1.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script><![CDATA[
+
+function init()
+{
+ var select = document.getElementsByTagName("select")[0];
+ select.parentNode.removeChild(select);
+}
+
+window.addEventListener("load", init, 0);
+
+]]></script>
+
+
+</head>
+<body><select><input/></select></body>
+</html>
diff --git a/layout/forms/crashtests/315752-1.xhtml b/layout/forms/crashtests/315752-1.xhtml new file mode 100644 index 000000000..66d3d793d --- /dev/null +++ b/layout/forms/crashtests/315752-1.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace">
+<head>
+<title>Settings - Unclassified NewsBoard</title>
+
+
+</head>
+<body>
+
+<select name="Language">
+ <option value="" style="clear: right;">
+ Automatic </option>
+ <option value="de" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> Deutsch</span> <small>(de)</small> </option>
+ <option value="en" selected="selected" style="clear: right;">
+ <span alt="" style="float: right; margin-top: 1px;"> English</span> <small>(en)</small> </option>
+</select>
+
+</body>
+</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/317502-1.xhtml b/layout/forms/crashtests/317502-1.xhtml new file mode 100644 index 000000000..7ef5f0b4c --- /dev/null +++ b/layout/forms/crashtests/317502-1.xhtml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> + <title>Testcase, bug 317502</title> +</head> +<body onload="document.getElementsByTagName('input')[0].style.width='200px'"> + +<input type="file" style="position: fixed" /> + +</body> +</html> diff --git a/layout/forms/crashtests/321894.html b/layout/forms/crashtests/321894.html new file mode 100644 index 000000000..0a82645ba --- /dev/null +++ b/layout/forms/crashtests/321894.html @@ -0,0 +1,17 @@ +<html><head>
+<title>
+Testcase bug 321894 - ASSERTION: RemovedAsPrimaryFrame called after PreDestroy: 'PR_FALSE'
+</title>
+</head>
+<body>
+This shouldn't assert in a debug build
+<span>
+ <script>var x=document.body.offsetHeight;</script>
+ <div>
+ <span style="float:left;">
+ <input type="text">
+ </span>
+ </div>
+</span>
+</body>
+</html>
diff --git a/layout/forms/crashtests/323499-1.html b/layout/forms/crashtests/323499-1.html new file mode 100644 index 000000000..bc015aa76 --- /dev/null +++ b/layout/forms/crashtests/323499-1.html @@ -0,0 +1,21 @@ +<html> +<head> + +<style> +select { + visibility: collapse; +} + +body { + display: -moz-grid-group; +} + +</style> +</head> + +<body> + +<select><option>a</option></select> + +</body> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/343510-1.html b/layout/forms/crashtests/343510-1.html new file mode 100644 index 000000000..00cb94f21 --- /dev/null +++ b/layout/forms/crashtests/343510-1.html @@ -0,0 +1,4 @@ +<select style="position: absolute;"> +<meta> +<option style="position: absolute;"> +<body style="display: block;"> diff --git a/layout/forms/crashtests/363696-1.xul b/layout/forms/crashtests/363696-1.xul new file mode 100644 index 000000000..19f938deb --- /dev/null +++ b/layout/forms/crashtests/363696-1.xul @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<body> + +<xul:hbox> + <input type="file" /> +</xul:hbox> + +</body> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/363696-2.html b/layout/forms/crashtests/363696-2.html new file mode 100644 index 000000000..40883b304 --- /dev/null +++ b/layout/forms/crashtests/363696-2.html @@ -0,0 +1,2 @@ +<div style="display: -moz-grid-line;"> +<input type="file">
\ No newline at end of file diff --git a/layout/forms/crashtests/363696-3.html b/layout/forms/crashtests/363696-3.html new file mode 100644 index 000000000..e1d38076e --- /dev/null +++ b/layout/forms/crashtests/363696-3.html @@ -0,0 +1,5 @@ +<html><head></head><body>
+
+<span style="display: -moz-box;"><input type="file"><textarea></textarea></span>
+
+</body></html>
\ No newline at end of file diff --git a/layout/forms/crashtests/366205-1.html b/layout/forms/crashtests/366205-1.html new file mode 100644 index 000000000..2a3786afe --- /dev/null +++ b/layout/forms/crashtests/366205-1.html @@ -0,0 +1,11 @@ +<html> +<head> +</head> +<body onload='var yar=document.getElementById("yar"); yar.parentNode.removeChild(yar); document.getElementById("foo").removeAttribute("multiple");'> + +<div id="yar">Clicking the button below should not trigger an assertion</div> + +<select id="foo" multiple><option>A</option></select> + +</body> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/366537-1.xhtml b/layout/forms/crashtests/366537-1.xhtml new file mode 100644 index 000000000..b799cd2ca --- /dev/null +++ b/layout/forms/crashtests/366537-1.xhtml @@ -0,0 +1,32 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> + +<head> +<script> + +function boom() +{ + var fieldset = document.getElementById("fieldset"); + var h3 = document.getElementById("h3"); + var legend = document.getElementById("legend"); + + fieldset.appendChild(legend); + fieldset.appendChild(h3); + + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + + +<body onload="setTimeout(boom, 30);"> + +<legend id="legend">legend</legend> + +<h3 id="h3">H3</h3> + +<select><option>option<fieldset id="fieldset"></fieldset></option></select> + +</body> + +</html> diff --git a/layout/forms/crashtests/367587-1.html b/layout/forms/crashtests/367587-1.html new file mode 100644 index 000000000..8f55ec458 --- /dev/null +++ b/layout/forms/crashtests/367587-1.html @@ -0,0 +1,37 @@ +<html class="reftest-wait"> +<head> + +<style> + +#opt1 { display: table-footer-group; } +#opt1 { visibility: collapse; } +#opt1 { overflow: -moz-scrollbars-vertical; } + +#opt2 { display: table-cell; } +#opt2 { clear: left; } + +select { width: 1px; } + +</style> + +<script> + +function boom() +{ + document.getElementById("opt2").style.clear = "none"; + document.documentElement.removeAttribute("class"); +} + +</script> + +</head> + +<body onload="setTimeout(boom, 30);"> + +<select multiple> +<option id="opt1">A</option> +<option id="opt2">B</option> +</select> + +</body> +</html> diff --git a/layout/forms/crashtests/370703-1.html b/layout/forms/crashtests/370703-1.html new file mode 100644 index 000000000..b447b180b --- /dev/null +++ b/layout/forms/crashtests/370703-1.html @@ -0,0 +1,30 @@ +<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ document.getElementById("M").style.width = "6em";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+<body onload="setTimeout(boom, 30);">
+
+<div style="position:absolute; top: 50px; left: 50px; border: 2px solid orange;">
+
+ <select>
+ <option id="M">M</option>
+ </select>
+
+ <br>
+
+ <select style="width: 200%">
+ <option style="visibility: collapse; overflow: auto; display: table-footer-group;">X</option>
+ </select>
+
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/370940-1.html b/layout/forms/crashtests/370940-1.html new file mode 100644 index 000000000..ccaeb9643 --- /dev/null +++ b/layout/forms/crashtests/370940-1.html @@ -0,0 +1,28 @@ +<html class="reftest-wait"> +<head> +<script> + +var HTML_NS = "http://www.w3.org/1999/xhtml"; + +function boom1() +{ + var newSpan = document.createElementNS(HTML_NS, "span"); + document.body.appendChild(newSpan); + + setTimeout(boom2, 30); +} + +function boom2() +{ + var newDiv = document.createElementNS(HTML_NS, "div"); + document.getElementById("s").appendChild(newDiv); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="setTimeout(boom1, 30);"> + <p>العربي</p> + <span id="s"></span><input><select></select><isindex><span></span> +</body> +</html> diff --git a/layout/forms/crashtests/370967.html b/layout/forms/crashtests/370967.html new file mode 100644 index 000000000..be7854b41 --- /dev/null +++ b/layout/forms/crashtests/370967.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<html>
+<head>
+<title>Untitled</title>
+</head>
+<body>
+Focus the input of the isindex, then do a reload, Mozilla should not crash
+<isindex autofocus>
+</label>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/373586-1.xhtml b/layout/forms/crashtests/373586-1.xhtml new file mode 100644 index 000000000..070315d15 --- /dev/null +++ b/layout/forms/crashtests/373586-1.xhtml @@ -0,0 +1,40 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+
+<bindings xmlns="http://www.mozilla.org/xbl">
+ <binding id="foo">
+ <content>
+ <children xmlns="http://www.mozilla.org/xbl" />
+ </content>
+ </binding>
+</bindings>
+
+<script>
+function boom()
+{
+ document.getElementById("div").style.MozBinding = "url('#foo')";
+
+ var opt1 = document.getElementById("opt1");
+ opt1.removeChild(opt1.firstChild);
+
+ document.getElementById("textarea").value += " ";
+
+ document.documentElement.removeAttribute("class")
+}
+</script>
+
+</head>
+
+<body onload="setTimeout(boom, 30);">
+
+<div id="div">
+ <textarea rows="3" cols="5" id="textarea"></textarea>
+</div>
+
+<select>
+ <option id="opt1">opt1</option>
+</select>
+
+</body>
+</html>
+
diff --git a/layout/forms/crashtests/375299-binding.xml b/layout/forms/crashtests/375299-binding.xml new file mode 100644 index 000000000..ca76a3389 --- /dev/null +++ b/layout/forms/crashtests/375299-binding.xml @@ -0,0 +1,4 @@ +<bindings xmlns="http://www.mozilla.org/xbl"> +<binding id="a"> +<content><children/></content> +</binding></bindings>
\ No newline at end of file diff --git a/layout/forms/crashtests/375299.html b/layout/forms/crashtests/375299.html new file mode 100644 index 000000000..fd43d1c63 --- /dev/null +++ b/layout/forms/crashtests/375299.html @@ -0,0 +1,17 @@ +<html><head>
+<title>Testcase bug - Crash [@ nsFileControlFrame::CreateAnonymousContent] when removing stylesheet with binding and removing file input</title>
+<style>
+body {-moz-binding:url("375299-binding.xml#a"); } +</style>
+</head><body onload="doe()">
+<input type="file">
+
+<script>
+function doe() {
+var x=document.getElementsByTagName('input')[0];
+x.parentNode.removeChild(x);
+var y=document.getElementsByTagName('style')[0];
+y.parentNode.removeChild(y);
+}
+</script>
+</body></html> diff --git a/layout/forms/crashtests/378369.html b/layout/forms/crashtests/378369.html new file mode 100644 index 000000000..90327735e --- /dev/null +++ b/layout/forms/crashtests/378369.html @@ -0,0 +1,19 @@ +<html>
+<head>
+<title>Testcase bug - Crash [@ nsEventListenerManager::FixContextMenuEvent] when firing contextmenu event in display: none iframe</title>
+</head>
+<body>
+This page should not crash Mozilla
+<iframe style="display: none;"></iframe>
+
+<script>
+function docontextmenu(i){
+var doc = window.frames[0].document;
+ var ev = doc.createEvent ('MouseEvents');
+ ev.initMouseEvent('contextmenu', true,true, window, 3,5, 5, 400, 400, 0, 0, 0,0,0,null);
+ doc.dispatchEvent(ev);
+}
+setTimeout(docontextmenu,0);
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/378413-1.xhtml b/layout/forms/crashtests/378413-1.xhtml new file mode 100644 index 000000000..c9ca6990a --- /dev/null +++ b/layout/forms/crashtests/378413-1.xhtml @@ -0,0 +1,16 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body style="direction: rtl;"> + +<form style="display: -moz-deck; width: 1em;"> +<fieldset> + <legend>Your name</legend> + <input type="text" name="name" size="20"/> +</fieldset> +<fieldset> + <legend>Your E-mail address</legend> + <input type="text" name="email" size="25"/> +</fieldset> +</form> + +</body> +</html> diff --git a/layout/forms/crashtests/380116-1.xhtml b/layout/forms/crashtests/380116-1.xhtml new file mode 100644 index 000000000..cb37ff079 --- /dev/null +++ b/layout/forms/crashtests/380116-1.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body onload="document.getElementById('t').style.display = 'block';"> + +<table border="1" id="t"> + <tr> + <td><select><option>foopy</option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/382212-1.xhtml b/layout/forms/crashtests/382212-1.xhtml new file mode 100644 index 000000000..46c957989 --- /dev/null +++ b/layout/forms/crashtests/382212-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <body> + <div> + <xul:textbox><select><option>foo</option></select></xul:textbox> + </div> + </body> +</html> diff --git a/layout/forms/crashtests/382610-1.html b/layout/forms/crashtests/382610-1.html new file mode 100644 index 000000000..9fe9c5b5c --- /dev/null +++ b/layout/forms/crashtests/382610-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<body> + +<div style="position: absolute"> + <input type="file" style="position: absolute; -moz-appearance: menulist;"> +</div> + +</body> +</html> + diff --git a/layout/forms/crashtests/383887-1.html b/layout/forms/crashtests/383887-1.html new file mode 100644 index 000000000..65d4751f7 --- /dev/null +++ b/layout/forms/crashtests/383887-1.html @@ -0,0 +1,20 @@ +<html> +<body> + +<table> + <tr> + <td> + <select> + <option style="padding: 200%;"> + </select> + </td> + </tr> + <tr> + <td> + <div style="padding: 200%"><span style="float: left; border: 1px solid red;"></span></div> + </td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/386554-1.html b/layout/forms/crashtests/386554-1.html new file mode 100644 index 000000000..585f1e5b5 --- /dev/null +++ b/layout/forms/crashtests/386554-1.html @@ -0,0 +1,14 @@ +<html> +<head></head> + +<body> + +<div style="position: absolute"><input id="input" type="file" style="top: 100%"></div> + +<script> +document.body.offsetHeight; +document.getElementById("input").style.position = "absolute"; +</script> + +</body> +</html> diff --git a/layout/forms/crashtests/388374-1.xhtml b/layout/forms/crashtests/388374-1.xhtml new file mode 100644 index 000000000..2a871f68b --- /dev/null +++ b/layout/forms/crashtests/388374-1.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var newText = document.createTextNode('x'); + document.getElementById("a").parentNode.appendChild(newText); +} +</script> +</head> + +<body onload="boom()"> + +<table border="1"> + <tr> + <td>1</td> + <td id="a" style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%;">pppp</option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/388374-2.html b/layout/forms/crashtests/388374-2.html new file mode 100644 index 000000000..81918c37c --- /dev/null +++ b/layout/forms/crashtests/388374-2.html @@ -0,0 +1,25 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var newDiv = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + newDiv.style.width = "100px"; + newDiv.style.height = "8px"; + newDiv.style.background = "lightgreen"; + document.getElementById("tr").appendChild(newDiv); +} +</script> +</head> + +<body onload="boom()"> + +<table border="1"> + <tr id="tr"> + <td></td> + <td style="padding-left: 10%"><select style="padding-left: 20%; bottom: 10%;"><option style="padding: 0 0 10% 20%; width: 25px;"></option></select></td> + </tr> +</table> + +</body> +</html> diff --git a/layout/forms/crashtests/393656-1.xhtml b/layout/forms/crashtests/393656-1.xhtml new file mode 100644 index 000000000..22a9326c5 --- /dev/null +++ b/layout/forms/crashtests/393656-1.xhtml @@ -0,0 +1,13 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +</head> + +<body> + <select style="float: right; clear: right;"> + <option><a style="float: right;"></a></option> + <option style="margin: 100%;"></option> + <option><div style="clear: both"></div></option> + </select> +</body> + +</html> diff --git a/layout/forms/crashtests/393656-2.xhtml b/layout/forms/crashtests/393656-2.xhtml new file mode 100644 index 000000000..7d9adaf20 --- /dev/null +++ b/layout/forms/crashtests/393656-2.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +</head> + +<body> + <div style="overflow: hidden;"> + <div style="float: right;"><span>XXX</span></div> + <select style="float: right; clear: right;"> + <option><a style="float: right;"></a></option> + <option style="margin: 100%;"></option> + <option><div style="clear: both"></div></option> + </select> + <dl style="float: left;"></dl> + <div> + <div style="float: left;"></div> + <div style="display: block; clear: both"></div> + <dl style="float: left;"></dl> + </div> + </div> +</body> + +</html> diff --git a/layout/forms/crashtests/399262.html b/layout/forms/crashtests/399262.html new file mode 100644 index 000000000..77bc0a16d --- /dev/null +++ b/layout/forms/crashtests/399262.html @@ -0,0 +1,50 @@ +<style> +select::first-letter, option::first-letter { color:green; } +.block select, .block option{ display:block; } +.scroll select, .scroll option{ display:block; overflow:hidden; } +</style> + +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> + +<div class="block"> +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> +</div> + +<div class="scroll"> +<select></select> +<select>F</select> +<select>Ff</select> +<select><option>F</select> +<select><option>Ff</select> +</div> + +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> + +<div class="block"> +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> +</div> + +<div class="scroll"> +<select size=2></select> +<select size=2>F</select> +<select size=2>Ff</select> +<select size=2><option>F</select> +<select size=2><option>Ff</select> +</div> + diff --git a/layout/forms/crashtests/402852-1.html b/layout/forms/crashtests/402852-1.html new file mode 100644 index 000000000..e5ba9adb1 --- /dev/null +++ b/layout/forms/crashtests/402852-1.html @@ -0,0 +1,2 @@ +<body style="text-indent: 99999999999999999999px;"> +<fieldset> diff --git a/layout/forms/crashtests/403148-1.html b/layout/forms/crashtests/403148-1.html new file mode 100644 index 000000000..cfb38fdb1 --- /dev/null +++ b/layout/forms/crashtests/403148-1.html @@ -0,0 +1,22 @@ +<html> +<head> +<script> + +function boom() +{ + document.getElementById("ce").contentEditable = true; + document.documentElement.offsetHeight; + document.getElementById("finput").setAttribute("type", ""); +} + +</script> +</head> + +<body onload="boom();"> + +<input id="finput" type="file" disabled> + +<span id="ce"></span> + +</body> +</html> diff --git a/layout/forms/crashtests/404118-1.html b/layout/forms/crashtests/404118-1.html new file mode 100644 index 000000000..40a2ac72b --- /dev/null +++ b/layout/forms/crashtests/404118-1.html @@ -0,0 +1,5 @@ +<html> +<body> +<input type="file" style="position: fixed; height: 0; width: 10px; white-space: normal;"> +</body> +</html> diff --git a/layout/forms/crashtests/404123-1.html b/layout/forms/crashtests/404123-1.html new file mode 100644 index 000000000..73fbaa363 --- /dev/null +++ b/layout/forms/crashtests/404123-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body> + +<fieldset> +<legend style="padding: 0pt 100%;"></legend> +</fieldset> + +</body> +</html> diff --git a/layout/forms/crashtests/407066.html b/layout/forms/crashtests/407066.html new file mode 100644 index 000000000..64a3739ea --- /dev/null +++ b/layout/forms/crashtests/407066.html @@ -0,0 +1 @@ +<div style="display: inline-block; -moz-column-count: 2;">text<input type="reset" style="float: right;"> diff --git a/layout/forms/crashtests/451316.html b/layout/forms/crashtests/451316.html new file mode 100644 index 000000000..77f5d5517 --- /dev/null +++ b/layout/forms/crashtests/451316.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<body> +<div style="-moz-column-width: 1px;"><select style="height: 12em; display: block;"></select></div> +<div style="-moz-column-width: 1px;"><select multiple style="height: 12em; display: block;"></select></div> +</body> +</html> diff --git a/layout/forms/crashtests/455451-1.html b/layout/forms/crashtests/455451-1.html new file mode 100644 index 000000000..09ee84aad --- /dev/null +++ b/layout/forms/crashtests/455451-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMCharacterDataModified", function(){}, false); + document.body.appendChild(document.createElement("isindex")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/457537-1.html b/layout/forms/crashtests/457537-1.html new file mode 100644 index 000000000..f6fa2fb05 --- /dev/null +++ b/layout/forms/crashtests/457537-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMAttrModified", function(){}, false); + document.body.appendChild(document.createElement("input")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/457537-2.html b/layout/forms/crashtests/457537-2.html new file mode 100644 index 000000000..48bf1e73e --- /dev/null +++ b/layout/forms/crashtests/457537-2.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function boom() +{ + window.addEventListener("DOMAttrModified", function(){}, false); + document.body.appendChild(document.createElement("isindex")); +} + +</script> +</head> + +<body onload="boom();"></body> + +</html> diff --git a/layout/forms/crashtests/478219-1.xhtml b/layout/forms/crashtests/478219-1.xhtml new file mode 100644 index 000000000..47144e649 --- /dev/null +++ b/layout/forms/crashtests/478219-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:mathml="http://www.w3.org/1998/Math/MathML" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<div> +<xul:textbox onDOMAttrModified="this.parentNode.removeChild(this)"> +<mathml:mathml/> +</xul:textbox> +</div> +</html> diff --git a/layout/forms/crashtests/498698-1.html b/layout/forms/crashtests/498698-1.html new file mode 100644 index 000000000..fa8e2e17a --- /dev/null +++ b/layout/forms/crashtests/498698-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<select style="height: 3401091640591pc"></select> +</body> +</html> diff --git a/layout/forms/crashtests/513113-1.html b/layout/forms/crashtests/513113-1.html new file mode 100644 index 000000000..36f3974a1 --- /dev/null +++ b/layout/forms/crashtests/513113-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html><body onload="document.body.style.display = 'table';" style="-moz-column-count: 1; white-space: pre-line; -moz-appearance: resizer; height: 23698324514pc;"> + + + +<input></body></html> diff --git a/layout/forms/crashtests/538062-1.xhtml b/layout/forms/crashtests/538062-1.xhtml new file mode 100644 index 000000000..431f7ab84 --- /dev/null +++ b/layout/forms/crashtests/538062-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + var option = document.getElementsByTagName("option")[0]; + var b = option.firstChild; + option.appendChild(document.createTextNode("\u064A")); + document.documentElement.offsetHeight; + option.removeChild(b); +} + +]]> +</script> +</head> + +<body onload="boom();"><select><option>B </option></select></body> +</html> diff --git a/layout/forms/crashtests/570624-1.html b/layout/forms/crashtests/570624-1.html new file mode 100644 index 000000000..47a277728 --- /dev/null +++ b/layout/forms/crashtests/570624-1.html @@ -0,0 +1,15 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +function boom() +{ + var xt = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", 'textbox'); + document.body.appendChild(xt); + xt.setAttribute('disabled', "true"); + xt.setAttribute('value', "foo"); +} +</script> +</head> +<body onload="boom();"> +</body> +</html> diff --git a/layout/forms/crashtests/578604-1.html b/layout/forms/crashtests/578604-1.html new file mode 100644 index 000000000..d934c454d --- /dev/null +++ b/layout/forms/crashtests/578604-1.html @@ -0,0 +1,17 @@ +<html class="reftest-print">
+<head>
+<style>
+ h2 { page-break-before: always; }
+ #reviewer { position: fixed;}
+</style>
+</head>
+
+<body>
+
+<h2>Crash Test Case</h2>
+<div>
+<input id='reviewer'>
+</div>
+
+</body>
+</html>
diff --git a/layout/forms/crashtests/590302-1.xhtml b/layout/forms/crashtests/590302-1.xhtml new file mode 100644 index 000000000..339a424f7 --- /dev/null +++ b/layout/forms/crashtests/590302-1.xhtml @@ -0,0 +1,4 @@ +<html class="reftest-print" xmlns="http://www.w3.org/1999/xhtml" style=" float: left; white-space: pre; "> +<div style="page-break-before: always;"></div> +<textarea style="position: fixed;"></textarea> +</html> diff --git a/layout/forms/crashtests/626014.xhtml b/layout/forms/crashtests/626014.xhtml new file mode 100644 index 000000000..05a2361d3 --- /dev/null +++ b/layout/forms/crashtests/626014.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function boom() +{ + document.getElementById("i").selectionEnd; + document.getElementById("a").appendChild(document.getElementById("b")); +} + +]]> +</script> +</head> +<body onload="boom();"> + +<mrow xmlns="http://www.w3.org/1998/Math/MathML" id="a"><mrow id="b"/><input xmlns="http://www.w3.org/1999/xhtml" id="i" /></mrow> + +</body> +</html> diff --git a/layout/forms/crashtests/639733.xhtml b/layout/forms/crashtests/639733.xhtml new file mode 100644 index 000000000..51c46df6c --- /dev/null +++ b/layout/forms/crashtests/639733.xhtml @@ -0,0 +1,26 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +<![CDATA[ + +function remove(n) +{ + n.parentNode.removeChild(n); +} + +function boom() +{ + + document.documentElement.offsetHeight; + remove(document.getElementById("s")); + remove(document.getElementById("e")); + document.documentElement.offsetHeight; +} + +]]> +</script> +</head> +<body onload="boom();"> +<embed id="e" src="data:text/html,A"></embed><span id="s"><div></div></span><isindex/> +</body> +</html> diff --git a/layout/forms/crashtests/669767.html b/layout/forms/crashtests/669767.html new file mode 100644 index 000000000..f41a9125c --- /dev/null +++ b/layout/forms/crashtests/669767.html @@ -0,0 +1,14 @@ +<html>
+<head>
+<title>Untitled</title>
+
+
+</head>
+<body>
+<iframe src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%3Cbody%3E%0A%3Ctextarea%3E%3C/textarea%3E%0A%0A%0A%3Cstyle%3E%0A@font-face%20%7B%0A%20%20%20%20%20%20font-family%3A%20%22cutabovetherest%22%3B%0A%20%20%20%20%20%20src%3A%20url%28%22http%3A//www.webpagepublicity.com/free-fonts/a/A%2520Cut%2520Above%2520The%2520Rest.ttf%22%29%3B%0A%7D%20%20%20%20%0A%0A%3C/style%3E%0A%0A%3Coptgroup%20contenteditable%3D%22true%22%20style%3D%22display%3A%20inline%3B%22%3E%3C/optgroup%3E%0A%0A%3C/body%3E%3C/html%3E"></iframe>
+<script>
+
+
+</script>
+</body>
+</html>
diff --git a/layout/forms/crashtests/682684-binding.xml b/layout/forms/crashtests/682684-binding.xml new file mode 100644 index 000000000..910eec33c --- /dev/null +++ b/layout/forms/crashtests/682684-binding.xml @@ -0,0 +1,4 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<style>html::before { content:"b"; }</style> +<input contenteditable="true" spellcheck="true"/> +</html>
\ No newline at end of file diff --git a/layout/forms/crashtests/682684.xhtml b/layout/forms/crashtests/682684.xhtml new file mode 100644 index 000000000..2b7b2c83f --- /dev/null +++ b/layout/forms/crashtests/682684.xhtml @@ -0,0 +1,3 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<span style="mask: url(682684-binding.xml#a);"/> +</html> diff --git a/layout/forms/crashtests/865602.html b/layout/forms/crashtests/865602.html new file mode 100644 index 000000000..b7048d955 --- /dev/null +++ b/layout/forms/crashtests/865602.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html><body> + +<div style="-moz-column-count: 2;"><fieldset style="transform-style: preserve-3d;"><fieldset style="white-space: pre-line; position: fixed;"><div style="position: fixed;"> + + +</div></fieldset></fieldset></div> + +</body></html> diff --git a/layout/forms/crashtests/893332-1.html b/layout/forms/crashtests/893332-1.html new file mode 100644 index 000000000..1cd17c0b1 --- /dev/null +++ b/layout/forms/crashtests/893332-1.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + </head> + <body> + <input min="-10" max="10" step="0.1" type="range" value="-5"/> + <input max="50" min="10" step="2" type="range" value="7"/> + </body> +</html> diff --git a/layout/forms/crashtests/944198.html b/layout/forms/crashtests/944198.html new file mode 100644 index 000000000..0da7bc54d --- /dev/null +++ b/layout/forms/crashtests/944198.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +</head> +<body onload="c.removeAttribute('type'); c.removeAttribute('value');"> +<input id="c" type="color"> +</body> +</html> diff --git a/layout/forms/crashtests/949891.xhtml b/layout/forms/crashtests/949891.xhtml new file mode 100644 index 000000000..8fbfe1e42 --- /dev/null +++ b/layout/forms/crashtests/949891.xhtml @@ -0,0 +1,5 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body> +<input type="number"> </input> +</body> +</html> diff --git a/layout/forms/crashtests/959311.html b/layout/forms/crashtests/959311.html new file mode 100644 index 000000000..c646b84f7 --- /dev/null +++ b/layout/forms/crashtests/959311.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-print"> +<head> +<meta charset="utf-8"> +<style type="text/css"> +@page { size:5in 3in; margin:0in; } +div { height: 2.5in; } +select { height: 0.5in; display:block; padding:20px; page-break-inside:initial; } +</style> +</head> +<body> + <div></div> + <select> + <option>Text</option> + </select> + </body> +</html> diff --git a/layout/forms/crashtests/960277-2.html b/layout/forms/crashtests/960277-2.html new file mode 100644 index 000000000..ea771ae79 --- /dev/null +++ b/layout/forms/crashtests/960277-2.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<style> +#d { overflow:scroll; width:200px; height:200px; background:yellow; } +#d2 { right:0; width:100px; height:100px; background:purple; } +</style> +<fieldset id="d"> + <div id="d2"></div> +</fieldset> +<script> +d.getBoundingClientRect(); +d.style.transform = "translateX(100px)"; +d.getBoundingClientRect(); +d2.style.position = "absolute"; +</script> diff --git a/layout/forms/crashtests/997709-1.html b/layout/forms/crashtests/997709-1.html new file mode 100644 index 000000000..31d048407 --- /dev/null +++ b/layout/forms/crashtests/997709-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE HTML> +<html class="reftest-print"><body><div style="position: fixed;"> +<div style="page-break-after: always"></div> +<select style="display:flex; position: fixed;"><option>A</select> +</div></body></html> diff --git a/layout/forms/crashtests/crashtests.list b/layout/forms/crashtests/crashtests.list new file mode 100644 index 000000000..462ff35cf --- /dev/null +++ b/layout/forms/crashtests/crashtests.list @@ -0,0 +1,69 @@ +load 166750-1.html +load 200347-1.html +load 203041-1.html +load 213390-1.html +load 258101-1.html +load 266225-1.html +load 310426-1.xhtml +load 310520-1.xhtml +load 315752-1.xhtml +load 317502-1.xhtml +load 321894.html +load 323499-1.html +load 343510-1.html +load 363696-1.xul +load 363696-2.html +load 363696-3.html +load 366205-1.html +load 366537-1.xhtml +load 367587-1.html +load 370703-1.html +load 370940-1.html +load 370967.html +load 373586-1.xhtml +load 375299.html +load 378369.html +asserts(4-10) load 378413-1.xhtml # bug 424225, bug 402850? +load 380116-1.xhtml +load 382212-1.xhtml +load 382610-1.html +load 383887-1.html +load 386554-1.html +load 388374-1.xhtml +load 388374-2.html +load 393656-1.xhtml +load 393656-2.xhtml +load 399262.html +load 402852-1.html +load 403148-1.html +load 404118-1.html +load 404123-1.html +load 407066.html +load 451316.html +load 455451-1.html +load 457537-1.html +load 457537-2.html +load 478219-1.xhtml +load 498698-1.html +load 513113-1.html +load 538062-1.xhtml +load 570624-1.html +asserts(1) load 578604-1.html # bug 584564 +asserts(4-7) load 590302-1.xhtml # bug 584564 +load 626014.xhtml +load 639733.xhtml +load 669767.html +load 682684.xhtml +load 865602.html +load 893332-1.html +load 944198.html +load 949891.xhtml +load 959311.html +load 960277-2.html +load 997709-1.html +load 1102791.html +load 1140216.html +load 1182414.html +load 1212688.html +load 1228670.xhtml +load 1279354.html diff --git a/layout/forms/moz.build b/layout/forms/moz.build new file mode 100644 index 000000000..4fecbad38 --- /dev/null +++ b/layout/forms/moz.build @@ -0,0 +1,57 @@ +# -*- 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', 'Layout: Form Controls') + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] +MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] + +EXPORTS += [ + 'nsIComboboxControlFrame.h', + 'nsIFormControlFrame.h', + 'nsIListControlFrame.h', + 'nsISelectControlFrame.h', + 'nsITextControlFrame.h', +] + +UNIFIED_SOURCES += [ + 'nsButtonFrameRenderer.cpp', + 'nsColorControlFrame.cpp', + 'nsComboboxControlFrame.cpp', + 'nsDateTimeControlFrame.cpp', + 'nsFieldSetFrame.cpp', + 'nsFileControlFrame.cpp', + 'nsFormControlFrame.cpp', + 'nsGfxButtonControlFrame.cpp', + 'nsGfxCheckboxControlFrame.cpp', + 'nsGfxRadioControlFrame.cpp', + 'nsHTMLButtonControlFrame.cpp', + 'nsImageControlFrame.cpp', + 'nsLegendFrame.cpp', + 'nsListControlFrame.cpp', + 'nsMeterFrame.cpp', + 'nsNumberControlFrame.cpp', + 'nsProgressFrame.cpp', + 'nsRangeFrame.cpp', + 'nsSelectsAreaFrame.cpp', + 'nsTextControlFrame.cpp', +] + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '../../editor/txmgr', + '../base', + '../generic', + '../style', + '../xul', + '/dom/base', + '/dom/html', +] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/layout/forms/nsButtonFrameRenderer.cpp b/layout/forms/nsButtonFrameRenderer.cpp new file mode 100644 index 000000000..096031385 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.cpp @@ -0,0 +1,516 @@ +/* -*- 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 "nsButtonFrameRenderer.h" +#include "nsCSSRendering.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsCSSPseudoElements.h" +#include "nsNameSpaceManager.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsFrame.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/Element.h" + +#define ACTIVE "active" +#define HOVER "hover" +#define FOCUS "focus" + +using namespace mozilla; +using namespace mozilla::image; + +nsButtonFrameRenderer::nsButtonFrameRenderer() +{ + MOZ_COUNT_CTOR(nsButtonFrameRenderer); +} + +nsButtonFrameRenderer::~nsButtonFrameRenderer() +{ + MOZ_COUNT_DTOR(nsButtonFrameRenderer); + +#ifdef DEBUG + if (mInnerFocusStyle) { + mInnerFocusStyle->FrameRelease(); + } + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameRelease(); + } +#endif +} + +void +nsButtonFrameRenderer::SetFrame(nsFrame* aFrame, nsPresContext* aPresContext) +{ + mFrame = aFrame; + ReResolveStyles(aPresContext); +} + +nsIFrame* +nsButtonFrameRenderer::GetFrame() +{ + return mFrame; +} + +void +nsButtonFrameRenderer::SetDisabled(bool aDisabled, bool notify) +{ + if (aDisabled) + mFrame->GetContent()->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, EmptyString(), + notify); + else + mFrame->GetContent()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, notify); +} + +bool +nsButtonFrameRenderer::isDisabled() +{ + return mFrame->GetContent()->AsElement()-> + State().HasState(NS_EVENT_STATE_DISABLED); +} + +class nsDisplayButtonBoxShadowOuter : public nsDisplayItem { +public: + nsDisplayButtonBoxShadowOuter(nsDisplayListBuilder* aBuilder, + nsButtonFrameRenderer* aRenderer) + : nsDisplayItem(aBuilder, aRenderer->GetFrame()), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonBoxShadowOuter); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayButtonBoxShadowOuter() { + MOZ_COUNT_DTOR(nsDisplayButtonBoxShadowOuter); + } +#endif + + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) override; + NS_DISPLAY_DECL_NAME("ButtonBoxShadowOuter", TYPE_BUTTON_BOX_SHADOW_OUTER) +private: + nsButtonFrameRenderer* mBFR; +}; + +nsRect +nsDisplayButtonBoxShadowOuter::GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) { + *aSnap = false; + return mFrame->GetVisualOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +void +nsDisplayButtonBoxShadowOuter::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) { + nsRect frameRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + nsRect buttonRect; + mBFR->GetButtonRect(frameRect, buttonRect); + + nsCSSRendering::PaintBoxShadowOuter(mFrame->PresContext(), *aCtx, mFrame, + buttonRect, mVisibleRect); +} + +class nsDisplayButtonBorder : public nsDisplayItem { +public: + nsDisplayButtonBorder(nsDisplayListBuilder* aBuilder, + nsButtonFrameRenderer* aRenderer) + : nsDisplayItem(aBuilder, aRenderer->GetFrame()), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonBorder); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayButtonBorder() { + MOZ_COUNT_DTOR(nsDisplayButtonBorder); + } +#endif + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*> *aOutFrames) override { + aOutFrames->AppendElement(mFrame); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) override; + virtual nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override; + virtual void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) override; + NS_DISPLAY_DECL_NAME("ButtonBorderBackground", TYPE_BUTTON_BORDER_BACKGROUND) +private: + nsButtonFrameRenderer* mBFR; +}; + +nsDisplayItemGeometry* +nsDisplayButtonBorder::AllocateGeometry(nsDisplayListBuilder* aBuilder) +{ + return new nsDisplayItemGenericImageGeometry(this, aBuilder); +} + +void +nsDisplayButtonBorder::ComputeInvalidationRegion( + nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) +{ + auto geometry = + static_cast<const nsDisplayItemGenericImageGeometry*>(aGeometry); + + if (aBuilder->ShouldSyncDecodeImages() && + geometry->ShouldInvalidateToSyncDecodeImages()) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, aInvalidRegion); +} + +void nsDisplayButtonBorder::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) +{ + NS_ASSERTION(mFrame, "No frame?"); + nsPresContext* pc = mFrame->PresContext(); + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // draw the border and background inside the focus and outline borders + DrawResult result = + mBFR->PaintBorder(aBuilder, pc, *aCtx, mVisibleRect, r); + + nsDisplayItemGenericImageGeometry::UpdateDrawResult(this, result); +} + +nsRect +nsDisplayButtonBorder::GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) { + *aSnap = false; + return aBuilder->IsForEventDelivery() ? nsRect(ToReferenceFrame(), mFrame->GetSize()) + : mFrame->GetVisualOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +class nsDisplayButtonForeground : public nsDisplayItem { +public: + nsDisplayButtonForeground(nsDisplayListBuilder* aBuilder, + nsButtonFrameRenderer* aRenderer) + : nsDisplayItem(aBuilder, aRenderer->GetFrame()), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonForeground); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayButtonForeground() { + MOZ_COUNT_DTOR(nsDisplayButtonForeground); + } +#endif + + nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override; + void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) override; + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override; + NS_DISPLAY_DECL_NAME("ButtonForeground", TYPE_BUTTON_FOREGROUND) +private: + nsButtonFrameRenderer* mBFR; +}; + +nsDisplayItemGeometry* +nsDisplayButtonForeground::AllocateGeometry(nsDisplayListBuilder* aBuilder) +{ + return new nsDisplayItemGenericImageGeometry(this, aBuilder); +} + +void +nsDisplayButtonForeground::ComputeInvalidationRegion( + nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion* aInvalidRegion) +{ + auto geometry = + static_cast<const nsDisplayItemGenericImageGeometry*>(aGeometry); + + if (aBuilder->ShouldSyncDecodeImages() && + geometry->ShouldInvalidateToSyncDecodeImages()) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, aInvalidRegion); +} + +void nsDisplayButtonForeground::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) +{ + nsPresContext *presContext = mFrame->PresContext(); + const nsStyleDisplay *disp = mFrame->StyleDisplay(); + if (!mFrame->IsThemed(disp) || + !presContext->GetTheme()->ThemeDrawsFocusForWidget(disp->mAppearance)) { + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // Draw the focus and outline borders. + DrawResult result = + mBFR->PaintOutlineAndFocusBorders(aBuilder, presContext, *aCtx, + mVisibleRect, r); + + nsDisplayItemGenericImageGeometry::UpdateDrawResult(this, result); + } +} + +nsresult +nsButtonFrameRenderer::DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, + nsDisplayList* aForeground) +{ + if (mFrame->StyleEffects()->mBoxShadow) { + aBackground->AppendNewToTop(new (aBuilder) + nsDisplayButtonBoxShadowOuter(aBuilder, this)); + } + + nsRect buttonRect; + GetButtonRect(mFrame->GetRectRelativeToSelf(), buttonRect); + + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, mFrame, buttonRect, aBackground); + + aBackground->AppendNewToTop(new (aBuilder) + nsDisplayButtonBorder(aBuilder, this)); + + // Only display focus rings if we actually have them. Since at most one + // button would normally display a focus ring, most buttons won't have them. + if ((mOuterFocusStyle && mOuterFocusStyle->StyleBorder()->HasBorder()) || + (mInnerFocusStyle && mInnerFocusStyle->StyleBorder()->HasBorder())) { + aForeground->AppendNewToTop(new (aBuilder) + nsDisplayButtonForeground(aBuilder, this)); + } + return NS_OK; +} + +DrawResult +nsButtonFrameRenderer::PaintOutlineAndFocusBorders( + nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + nsRenderingContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect) +{ + // once we have all that we'll draw the focus if we have it. We will + // need to draw 2 focuses, the inner and the outer. This is so we + // can do any kind of look and feel. Some buttons have focus on the + // outside like mac and motif. While others like windows have it + // inside (dotted line). Usually only one will be specifed. But I + // guess you could have both if you wanted to. + + nsRect rect; + + PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SYNC_DECODE_IMAGES + : PaintBorderFlags(); + + DrawResult result = DrawResult::SUCCESS; + + if (mOuterFocusStyle) { + // ---------- paint the outer focus border ------------- + + GetButtonOuterFocusRect(aRect, rect); + + result &= + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, rect, mOuterFocusStyle, flags); + } + + if (mInnerFocusStyle) { + // ---------- paint the inner focus border ------------- + + GetButtonInnerFocusRect(aRect, rect); + + result &= + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, rect, mInnerFocusStyle, flags); + } + + return result; +} + +DrawResult +nsButtonFrameRenderer::PaintBorder( + nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + nsRenderingContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect) +{ + // get the button rect this is inside the focus and outline rects + nsRect buttonRect; + GetButtonRect(aRect, buttonRect); + + nsStyleContext* context = mFrame->StyleContext(); + + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SYNC_DECODE_IMAGES + : PaintBorderFlags(); + + nsCSSRendering::PaintBoxShadowInner(aPresContext, aRenderingContext, + mFrame, buttonRect); + + DrawResult result = + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, buttonRect, context, borderFlags); + + return result; +} + + +void +nsButtonFrameRenderer::GetButtonOuterFocusRect(const nsRect& aRect, nsRect& focusRect) +{ + focusRect = aRect; +} + +void +nsButtonFrameRenderer::GetButtonRect(const nsRect& aRect, nsRect& r) +{ + r = aRect; + r.Deflate(GetButtonOuterFocusBorderAndPadding()); +} + + +void +nsButtonFrameRenderer::GetButtonInnerFocusRect(const nsRect& aRect, nsRect& focusRect) +{ + GetButtonRect(aRect, focusRect); + focusRect.Deflate(GetButtonBorderAndPadding()); + focusRect.Deflate(GetButtonInnerFocusMargin()); +} + + +nsMargin +nsButtonFrameRenderer::GetButtonOuterFocusBorderAndPadding() +{ + nsMargin result(0,0,0,0); + + if (mOuterFocusStyle) { + mOuterFocusStyle->StylePadding()->GetPadding(result); + result += mOuterFocusStyle->StyleBorder()->GetComputedBorder(); + } + + return result; +} + +nsMargin +nsButtonFrameRenderer::GetButtonBorderAndPadding() +{ + return mFrame->GetUsedBorderAndPadding(); +} + +/** + * Gets the size of the buttons border this is the union of the normal and disabled borders. + */ +nsMargin +nsButtonFrameRenderer::GetButtonInnerFocusMargin() +{ + nsMargin innerFocusMargin(0,0,0,0); + + if (mInnerFocusStyle) { + const nsStyleMargin* margin = mInnerFocusStyle->StyleMargin(); + margin->GetMargin(innerFocusMargin); + } + + return innerFocusMargin; +} + +nsMargin +nsButtonFrameRenderer::GetButtonInnerFocusBorderAndPadding() +{ + nsMargin result(0,0,0,0); + + if (mInnerFocusStyle) { + mInnerFocusStyle->StylePadding()->GetPadding(result); + result += mInnerFocusStyle->StyleBorder()->GetComputedBorder(); + } + + return result; +} + +// gets all the focus borders and padding that will be added to the regular border +nsMargin +nsButtonFrameRenderer::GetAddedButtonBorderAndPadding() +{ + return GetButtonOuterFocusBorderAndPadding() + GetButtonInnerFocusMargin() + GetButtonInnerFocusBorderAndPadding(); +} + +/** + * Call this when styles change + */ +void +nsButtonFrameRenderer::ReResolveStyles(nsPresContext* aPresContext) +{ + // get all the styles + nsStyleContext* context = mFrame->StyleContext(); + StyleSetHandle styleSet = aPresContext->StyleSet(); + +#ifdef DEBUG + if (mInnerFocusStyle) { + mInnerFocusStyle->FrameRelease(); + } + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameRelease(); + } +#endif + + // style for the inner such as a dotted line (Windows) + mInnerFocusStyle = + styleSet->ProbePseudoElementStyle(mFrame->GetContent()->AsElement(), + CSSPseudoElementType::mozFocusInner, + context); + + // style for outer focus like a ridged border (MAC). + mOuterFocusStyle = + styleSet->ProbePseudoElementStyle(mFrame->GetContent()->AsElement(), + CSSPseudoElementType::mozFocusOuter, + context); + +#ifdef DEBUG + if (mInnerFocusStyle) { + mInnerFocusStyle->FrameAddRef(); + } + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameAddRef(); + } +#endif +} + +nsStyleContext* +nsButtonFrameRenderer::GetStyleContext(int32_t aIndex) const +{ + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: + return mInnerFocusStyle; + case NS_BUTTON_RENDERER_FOCUS_OUTER_CONTEXT_INDEX: + return mOuterFocusStyle; + default: + return nullptr; + } +} + +void +nsButtonFrameRenderer::SetStyleContext(int32_t aIndex, nsStyleContext* aStyleContext) +{ + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: +#ifdef DEBUG + if (mInnerFocusStyle) { + mInnerFocusStyle->FrameRelease(); + } +#endif + mInnerFocusStyle = aStyleContext; + break; + case NS_BUTTON_RENDERER_FOCUS_OUTER_CONTEXT_INDEX: +#ifdef DEBUG + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameRelease(); + } +#endif + mOuterFocusStyle = aStyleContext; + break; + } +#ifdef DEBUG + aStyleContext->FrameAddRef(); +#endif +} diff --git a/layout/forms/nsButtonFrameRenderer.h b/layout/forms/nsButtonFrameRenderer.h new file mode 100644 index 000000000..8662c52e0 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +#ifndef nsButtonFrameRenderer_h___ +#define nsButtonFrameRenderer_h___ + +#include "imgIContainer.h" +#include "nsMargin.h" + +class nsIFrame; +class nsFrame; +class nsDisplayList; +class nsDisplayListBuilder; +class nsPresContext; +class nsRenderingContext; +struct nsRect; +class nsStyleContext; + + +#define NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX 0 +#define NS_BUTTON_RENDERER_FOCUS_OUTER_CONTEXT_INDEX 1 +#define NS_BUTTON_RENDERER_LAST_CONTEXT_INDEX NS_BUTTON_RENDERER_FOCUS_OUTER_CONTEXT_INDEX + +class nsButtonFrameRenderer { + typedef mozilla::image::DrawResult DrawResult; + +public: + + nsButtonFrameRenderer(); + ~nsButtonFrameRenderer(); + + /** + * Create display list items for the button + */ + nsresult DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, nsDisplayList* aForeground); + + + DrawResult PaintOutlineAndFocusBorders(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + nsRenderingContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect); + + DrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + nsRenderingContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect); + + void SetFrame(nsFrame* aFrame, nsPresContext* aPresContext); + + void SetDisabled(bool aDisabled, bool notify); + + bool isActive(); + bool isDisabled(); + + void GetButtonOuterFocusRect(const nsRect& aRect, nsRect& aResult); + void GetButtonRect(const nsRect& aRect, nsRect& aResult); + void GetButtonInnerFocusRect(const nsRect& aRect, nsRect& aResult); + nsMargin GetButtonOuterFocusBorderAndPadding(); + nsMargin GetButtonBorderAndPadding(); + nsMargin GetButtonInnerFocusMargin(); + nsMargin GetButtonInnerFocusBorderAndPadding(); + nsMargin GetAddedButtonBorderAndPadding(); + + nsStyleContext* GetStyleContext(int32_t aIndex) const; + void SetStyleContext(int32_t aIndex, nsStyleContext* aStyleContext); + void ReResolveStyles(nsPresContext* aPresContext); + + nsIFrame* GetFrame(); + +protected: + +private: + + // cached styles for focus and outline. + RefPtr<nsStyleContext> mInnerFocusStyle; + RefPtr<nsStyleContext> mOuterFocusStyle; + + nsFrame* mFrame; +}; + + +#endif + diff --git a/layout/forms/nsColorControlFrame.cpp b/layout/forms/nsColorControlFrame.cpp new file mode 100644 index 000000000..e0bae43a9 --- /dev/null +++ b/layout/forms/nsColorControlFrame.cpp @@ -0,0 +1,147 @@ +/* -*- 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 "nsColorControlFrame.h" + +#include "nsContentCreatorFunctions.h" +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsCSSPseudoElements.h" +#include "nsFormControlFrame.h" +#include "nsGkAtoms.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMNode.h" +#include "nsIFormControl.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsIDocument.h" + +using mozilla::dom::Element; + +nsColorControlFrame::nsColorControlFrame(nsStyleContext* aContext) + : nsHTMLButtonControlFrame(aContext) +{ +} + +nsIFrame* +NS_NewColorControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsColorControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsColorControlFrame) + +NS_QUERYFRAME_HEAD(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + + +void nsColorControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContentUtils::DestroyAnonymousContent(&mColorContent); + nsHTMLButtonControlFrame::DestroyFrom(aDestructRoot); +} + +nsIAtom* +nsColorControlFrame::GetType() const +{ + return nsGkAtoms::colorControlFrame; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsColorControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("ColorControl"), aResult); +} +#endif + +// Create the color area for the button. +// The frame will be generated by the frame constructor. +nsresult +nsColorControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + mColorContent = doc->CreateHTMLElement(nsGkAtoms::div); + + // Mark the element to be native anonymous before setting any attributes. + mColorContent->SetIsNativeAnonymousRoot(); + + nsresult rv = UpdateColor(); + NS_ENSURE_SUCCESS(rv, rv); + + CSSPseudoElementType pseudoType = CSSPseudoElementType::mozColorSwatch; + RefPtr<nsStyleContext> newStyleContext = PresContext()->StyleSet()-> + ResolvePseudoElementStyle(mContent->AsElement(), pseudoType, + StyleContext(), mColorContent->AsElement()); + if (!aElements.AppendElement(ContentInfo(mColorContent, newStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void +nsColorControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mColorContent) { + aElements.AppendElement(mColorContent); + } +} + +nsresult +nsColorControlFrame::UpdateColor() +{ + // Get the color from the "value" property of our content; it will return the + // default color (through the sanitization algorithm) if there is none. + nsAutoString color; + nsCOMPtr<nsIDOMHTMLInputElement> elt = do_QueryInterface(mContent); + elt->GetValue(color); + MOZ_ASSERT(!color.IsEmpty(), + "Content node's GetValue() should return a valid color string " + "(the default color, in case no valid color is set)"); + + // Set the background-color style property of the swatch element to this color + return mColorContent->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + NS_LITERAL_STRING("background-color:") + color, true); +} + +nsresult +nsColorControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + NS_ASSERTION(mColorContent, "The color div must exist"); + + // If the value attribute is set, update the color box, but only if we're + // still a color control, which might not be the case if the type attribute + // was removed/changed. + nsCOMPtr<nsIFormControl> fctrl = do_QueryInterface(GetContent()); + if (fctrl->GetType() == NS_FORM_INPUT_COLOR && + aNameSpaceID == kNameSpaceID_None && nsGkAtoms::value == aAttribute) { + UpdateColor(); + } + return nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +nsContainerFrame* +nsColorControlFrame::GetContentInsertionFrame() +{ + return this; +} + +Element* +nsColorControlFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::mozColorSwatch) { + return mColorContent; + } + + return nsContainerFrame::GetPseudoElement(aType); +} diff --git a/layout/forms/nsColorControlFrame.h b/layout/forms/nsColorControlFrame.h new file mode 100644 index 000000000..238347ce3 --- /dev/null +++ b/layout/forms/nsColorControlFrame.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef nsColorControlFrame_h___ +#define nsColorControlFrame_h___ + +#include "nsCOMPtr.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsIAnonymousContentCreator.h" + +namespace mozilla { +enum class CSSPseudoElementType : uint8_t; +} // namespace mozilla + +// Class which implements the input type=color + +class nsColorControlFrame final : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator +{ + typedef mozilla::CSSPseudoElementType CSSPseudoElementType; + typedef mozilla::dom::Element Element; + +public: + friend nsIFrame* NS_NewColorControlFrame(nsIPresShell* aPresShell, + nsStyleContext* aContext); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + NS_DECL_QUERYFRAME_TARGET(nsColorControlFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + // nsIFrame + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + virtual bool IsLeaf() const override { return true; } + virtual nsContainerFrame* GetContentInsertionFrame() override; + + virtual Element* GetPseudoElement(CSSPseudoElementType aType) override; + + // Refresh the color swatch, using associated input's value + nsresult UpdateColor(); + +private: + explicit nsColorControlFrame(nsStyleContext* aContext); + + nsCOMPtr<Element> mColorContent; +}; + + +#endif // nsColorControlFrame_h___ diff --git a/layout/forms/nsComboboxControlFrame.cpp b/layout/forms/nsComboboxControlFrame.cpp new file mode 100644 index 000000000..f69198cc7 --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.cpp @@ -0,0 +1,1715 @@ +/* -*- 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 "nsComboboxControlFrame.h" + +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "nsCOMPtr.h" +#include "nsFocusManager.h" +#include "nsFormControlFrame.h" +#include "nsGkAtoms.h" +#include "nsCSSAnonBoxes.h" +#include "nsHTMLParts.h" +#include "nsIFormControl.h" +#include "nsNameSpaceManager.h" +#include "nsIListControlFrame.h" +#include "nsPIDOMWindow.h" +#include "nsIPresShell.h" +#include "nsPresState.h" +#include "nsContentList.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsIDOMEventListener.h" +#include "nsIDOMNode.h" +#include "nsISelectControlFrame.h" +#include "nsContentUtils.h" +#include "nsIDocument.h" +#include "nsIScrollableFrame.h" +#include "nsListControlFrame.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsThemeConstants.h" +#include "nsRenderingContext.h" +#include "mozilla/Likely.h" +#include <algorithm> +#include "nsTextNode.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/EventStates.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Unused.h" +#include "gfx2DGlue.h" + +#ifdef XP_WIN +#define COMBOBOX_ROLLUP_CONSUME_EVENT 0 +#else +#define COMBOBOX_ROLLUP_CONSUME_EVENT 1 +#endif + +using namespace mozilla; +using namespace mozilla::gfx; + +NS_IMETHODIMP +nsComboboxControlFrame::RedisplayTextEvent::Run() +{ + if (mControlFrame) + mControlFrame->HandleRedisplayTextEvent(); + return NS_OK; +} + +class nsPresState; + +#define FIX_FOR_BUG_53259 + +// Drop down list event management. +// The combo box uses the following strategy for managing the drop-down list. +// If the combo box or its arrow button is clicked on the drop-down list is displayed +// If mouse exits the combo box with the drop-down list displayed the drop-down list +// is asked to capture events +// The drop-down list will capture all events including mouse down and up and will always +// return with ListWasSelected method call regardless of whether an item in the list was +// actually selected. +// The ListWasSelected code will turn off mouse-capture for the drop-down list. +// The drop-down list does not explicitly set capture when it is in the drop-down mode. + + +/** + * Helper class that listens to the combo boxes button. If the button is pressed the + * combo box is toggled to open or close. this is used by Accessibility which presses + * that button Programmatically. + */ +class nsComboButtonListener : public nsIDOMEventListener +{ +private: + virtual ~nsComboButtonListener() {} + +public: + NS_DECL_ISUPPORTS + + NS_IMETHOD HandleEvent(nsIDOMEvent*) override + { + mComboBox->ShowDropDown(!mComboBox->IsDroppedDown()); + return NS_OK; + } + + explicit nsComboButtonListener(nsComboboxControlFrame* aCombobox) + { + mComboBox = aCombobox; + } + + nsComboboxControlFrame* mComboBox; +}; + +NS_IMPL_ISUPPORTS(nsComboButtonListener, + nsIDOMEventListener) + +// static class data member for Bug 32920 +nsComboboxControlFrame* nsComboboxControlFrame::sFocused = nullptr; + +nsContainerFrame* +NS_NewComboboxControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext, nsFrameState aStateFlags) +{ + nsComboboxControlFrame* it = new (aPresShell) nsComboboxControlFrame(aContext); + + if (it) { + // set the state flags (if any are provided) + it->AddStateBits(aStateFlags); + } + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame) + +//----------------------------------------------------------- +// Reflow Debugging Macros +// These let us "see" how many reflow counts are happening +//----------------------------------------------------------- +#ifdef DO_REFLOW_COUNTER + +#define MAX_REFLOW_CNT 1024 +static int32_t gTotalReqs = 0;; +static int32_t gTotalReflows = 0;; +static int32_t gReflowControlCntRQ[MAX_REFLOW_CNT]; +static int32_t gReflowControlCnt[MAX_REFLOW_CNT]; +static int32_t gReflowInx = -1; + +#define REFLOW_COUNTER() \ + if (mReflowId > -1) \ + gReflowControlCnt[mReflowId]++; + +#define REFLOW_COUNTER_REQUEST() \ + if (mReflowId > -1) \ + gReflowControlCntRQ[mReflowId]++; + +#define REFLOW_COUNTER_DUMP(__desc) \ + if (mReflowId > -1) {\ + gTotalReqs += gReflowControlCntRQ[mReflowId];\ + gTotalReflows += gReflowControlCnt[mReflowId];\ + printf("** Id:%5d %s RF: %d RQ: %d %d/%d %5.2f\n", \ + mReflowId, (__desc), \ + gReflowControlCnt[mReflowId], \ + gReflowControlCntRQ[mReflowId],\ + gTotalReflows, gTotalReqs, float(gTotalReflows)/float(gTotalReqs)*100.0f);\ + } + +#define REFLOW_COUNTER_INIT() \ + if (gReflowInx < MAX_REFLOW_CNT) { \ + gReflowInx++; \ + mReflowId = gReflowInx; \ + gReflowControlCnt[mReflowId] = 0; \ + gReflowControlCntRQ[mReflowId] = 0; \ + } else { \ + mReflowId = -1; \ + } + +// reflow messages +#define REFLOW_DEBUG_MSG(_msg1) printf((_msg1)) +#define REFLOW_DEBUG_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +#define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) printf((_msg1), (_msg2), (_msg3)) +#define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) printf((_msg1), (_msg2), (_msg3), (_msg4)) + +#else //------------- + +#define REFLOW_COUNTER_REQUEST() +#define REFLOW_COUNTER() +#define REFLOW_COUNTER_DUMP(__desc) +#define REFLOW_COUNTER_INIT() + +#define REFLOW_DEBUG_MSG(_msg) +#define REFLOW_DEBUG_MSG2(_msg1, _msg2) +#define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) +#define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) + + +#endif + +//------------------------------------------ +// This is for being VERY noisy +//------------------------------------------ +#ifdef DO_VERY_NOISY +#define REFLOW_NOISY_MSG(_msg1) printf((_msg1)) +#define REFLOW_NOISY_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +#define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) printf((_msg1), (_msg2), (_msg3)) +#define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) printf((_msg1), (_msg2), (_msg3), (_msg4)) +#else +#define REFLOW_NOISY_MSG(_msg) +#define REFLOW_NOISY_MSG2(_msg1, _msg2) +#define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) +#define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) +#endif + +//------------------------------------------ +// Displays value in pixels or twips +//------------------------------------------ +#ifdef DO_PIXELS +#define PX(__v) __v / 15 +#else +#define PX(__v) __v +#endif + +//------------------------------------------------------ +//-- Done with macros +//------------------------------------------------------ + +nsComboboxControlFrame::nsComboboxControlFrame(nsStyleContext* aContext) + : nsBlockFrame(aContext) + , mDisplayFrame(nullptr) + , mButtonFrame(nullptr) + , mDropdownFrame(nullptr) + , mListControlFrame(nullptr) + , mDisplayISize(0) + , mRecentSelectedIndex(NS_SKIP_NOTIFY_INDEX) + , mDisplayedIndex(-1) + , mLastDropDownBeforeScreenBCoord(nscoord_MIN) + , mLastDropDownAfterScreenBCoord(nscoord_MIN) + , mDroppedDown(false) + , mInRedisplayText(false) + , mDelayedShowDropDown(false) + , mIsOpenInParentProcess(false) +{ + REFLOW_COUNTER_INIT() +} + +//-------------------------------------------------------------- +nsComboboxControlFrame::~nsComboboxControlFrame() +{ + REFLOW_COUNTER_DUMP("nsCCF"); +} + +//-------------------------------------------------------------- + +NS_QUERYFRAME_HEAD(nsComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsIComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +#ifdef ACCESSIBILITY +a11y::AccType +nsComboboxControlFrame::AccessibleType() +{ + return a11y::eHTMLComboboxType; +} +#endif + +void +nsComboboxControlFrame::SetFocus(bool aOn, bool aRepaint) +{ + nsWeakFrame weakFrame(this); + if (aOn) { + nsListControlFrame::ComboboxFocusSet(); + sFocused = this; + if (mDelayedShowDropDown) { + ShowDropDown(true); // might destroy us + if (!weakFrame.IsAlive()) { + return; + } + } + } else { + sFocused = nullptr; + mDelayedShowDropDown = false; + if (mDroppedDown) { + mListControlFrame->ComboboxFinish(mDisplayedIndex); // might destroy us + if (!weakFrame.IsAlive()) { + return; + } + } + // May delete |this|. + mListControlFrame->FireOnInputAndOnChange(); + } + + if (!weakFrame.IsAlive()) { + return; + } + + // This is needed on a temporary basis. It causes the focus + // rect to be drawn. This is much faster than ReResolvingStyle + // Bug 32920 + InvalidateFrame(); +} + +void +nsComboboxControlFrame::ShowPopup(bool aShowPopup) +{ + nsView* view = mDropdownFrame->GetView(); + nsViewManager* viewManager = view->GetViewManager(); + + if (aShowPopup) { + nsRect rect = mDropdownFrame->GetRect(); + rect.x = rect.y = 0; + viewManager->ResizeView(view, rect); + viewManager->SetViewVisibility(view, nsViewVisibility_kShow); + } else { + viewManager->SetViewVisibility(view, nsViewVisibility_kHide); + nsRect emptyRect(0, 0, 0, 0); + viewManager->ResizeView(view, emptyRect); + } + + // fire a popup dom event if it is safe to do so + nsCOMPtr<nsIPresShell> shell = PresContext()->GetPresShell(); + if (shell && nsContentUtils::IsSafeToRunScript()) { + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, aShowPopup ? eXULPopupShowing : eXULPopupHiding, + nullptr, WidgetMouseEvent::eReal); + + shell->HandleDOMEventWithTarget(mContent, &event, &status); + } +} + +bool +nsComboboxControlFrame::ShowList(bool aShowList) +{ + nsView* view = mDropdownFrame->GetView(); + if (aShowList) { + NS_ASSERTION(!view->HasWidget(), + "We shouldn't have a widget before we need to display the popup"); + + // Create the widget for the drop-down list + view->GetViewManager()->SetViewFloating(view, true); + + nsWidgetInitData widgetData; + widgetData.mWindowType = eWindowType_popup; + widgetData.mBorderStyle = eBorderStyle_default; + view->CreateWidgetForPopup(&widgetData); + } else { + nsIWidget* widget = view->GetWidget(); + if (widget) { + // We must do this before ShowPopup in case it destroys us (bug 813442). + widget->CaptureRollupEvents(this, false); + } + } + + nsWeakFrame weakFrame(this); + ShowPopup(aShowList); // might destroy us + if (!weakFrame.IsAlive()) { + return false; + } + + mDroppedDown = aShowList; + nsIWidget* widget = view->GetWidget(); + if (mDroppedDown) { + // The listcontrol frame will call back to the nsComboboxControlFrame's + // ListWasSelected which will stop the capture. + mListControlFrame->AboutToDropDown(); + mListControlFrame->CaptureMouseEvents(true); + if (widget) { + widget->CaptureRollupEvents(this, true); + } + } else { + if (widget) { + view->DestroyWidget(); + } + } + + return weakFrame.IsAlive(); +} + +class nsResizeDropdownAtFinalPosition final + : public nsIReflowCallback, public Runnable +{ +public: + explicit nsResizeDropdownAtFinalPosition(nsComboboxControlFrame* aFrame) + : mFrame(aFrame) + { + MOZ_COUNT_CTOR(nsResizeDropdownAtFinalPosition); + } + +protected: + ~nsResizeDropdownAtFinalPosition() + { + MOZ_COUNT_DTOR(nsResizeDropdownAtFinalPosition); + } + +public: + virtual bool ReflowFinished() override + { + Run(); + NS_RELEASE_THIS(); + return false; + } + + virtual void ReflowCallbackCanceled() override + { + NS_RELEASE_THIS(); + } + + NS_IMETHOD Run() override + { + if (mFrame.IsAlive()) { + static_cast<nsComboboxControlFrame*>(mFrame.GetFrame())-> + AbsolutelyPositionDropDown(); + } + return NS_OK; + } + + nsWeakFrame mFrame; +}; + +void +nsComboboxControlFrame::ReflowDropdown(nsPresContext* aPresContext, + const ReflowInput& aReflowInput) +{ + // All we want out of it later on, really, is the block size of a row, so we + // don't even need to cache mDropdownFrame's ascent or anything. If we don't + // need to reflow it, just bail out here. + if (!aReflowInput.ShouldReflowAllKids() && + !NS_SUBTREE_DIRTY(mDropdownFrame)) { + return; + } + + // XXXbz this will, for small-block-size dropdowns, have extra space + // on the appropriate edge for the scrollbar we don't show... but + // that's the best we can do here for now. + WritingMode wm = mDropdownFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.AvailableSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput kidReflowInput(aPresContext, aReflowInput, mDropdownFrame, + availSize); + + // If the dropdown's intrinsic inline size is narrower than our + // specified inline size, then expand it out. We want our border-box + // inline size to end up the same as the dropdown's so account for + // both sets of mComputedBorderPadding. + nscoord forcedISize = aReflowInput.ComputedISize() + + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(wm) - + kidReflowInput.ComputedLogicalBorderPadding().IStartEnd(wm); + kidReflowInput.SetComputedISize(std::max(kidReflowInput.ComputedISize(), + forcedISize)); + + // ensure we start off hidden + if (!mDroppedDown && GetStateBits() & NS_FRAME_FIRST_REFLOW) { + nsView* view = mDropdownFrame->GetView(); + nsViewManager* viewManager = view->GetViewManager(); + viewManager->SetViewVisibility(view, nsViewVisibility_kHide); + nsRect emptyRect(0, 0, 0, 0); + viewManager->ResizeView(view, emptyRect); + } + + // Allow the child to move/size/change-visibility its view if it's currently + // dropped down + int32_t flags = mDroppedDown ? 0 + : NS_FRAME_NO_MOVE_FRAME | + NS_FRAME_NO_VISIBILITY | + NS_FRAME_NO_SIZE_VIEW; + + //XXX Can this be different from the dropdown's writing mode? + // That would be odd! + // Note that we don't need to pass the true frame position or container size + // to ReflowChild or FinishReflowChild here; it will be positioned as needed + // by AbsolutelyPositionDropDown(). + WritingMode outerWM = GetWritingMode(); + const nsSize dummyContainerSize; + ReflowOutput desiredSize(aReflowInput); + nsReflowStatus ignoredStatus; + ReflowChild(mDropdownFrame, aPresContext, desiredSize, + kidReflowInput, outerWM, LogicalPoint(outerWM), + dummyContainerSize, flags, ignoredStatus); + + // Set the child's width and height to its desired size + FinishReflowChild(mDropdownFrame, aPresContext, desiredSize, &kidReflowInput, + outerWM, LogicalPoint(outerWM), dummyContainerSize, flags); +} + +nsPoint +nsComboboxControlFrame::GetCSSTransformTranslation() +{ + nsIFrame* frame = this; + bool is3DTransform = false; + Matrix transform; + while (frame) { + nsIFrame* parent; + Matrix4x4 ctm = frame->GetTransformMatrix(nullptr, &parent); + Matrix matrix; + if (ctm.Is2D(&matrix)) { + transform = transform * matrix; + } else { + is3DTransform = true; + break; + } + frame = parent; + } + nsPoint translation; + if (!is3DTransform && !transform.HasNonTranslation()) { + nsPresContext* pc = PresContext(); + // To get the translation introduced only by transforms we subtract the + // regular non-transform translation. + nsRootPresContext* rootPC = pc->GetRootPresContext(); + if (rootPC) { + int32_t apd = pc->AppUnitsPerDevPixel(); + translation.x = NSFloatPixelsToAppUnits(transform._31, apd); + translation.y = NSFloatPixelsToAppUnits(transform._32, apd); + translation -= GetOffsetToCrossDoc(rootPC->PresShell()->GetRootFrame()); + } + } + return translation; +} + +class nsAsyncRollup : public Runnable +{ +public: + explicit nsAsyncRollup(nsComboboxControlFrame* aFrame) : mFrame(aFrame) {} + NS_IMETHOD Run() override + { + if (mFrame.IsAlive()) { + static_cast<nsComboboxControlFrame*>(mFrame.GetFrame()) + ->RollupFromList(); + } + return NS_OK; + } + nsWeakFrame mFrame; +}; + +class nsAsyncResize : public Runnable +{ +public: + explicit nsAsyncResize(nsComboboxControlFrame* aFrame) : mFrame(aFrame) {} + NS_IMETHOD Run() override + { + if (mFrame.IsAlive()) { + nsComboboxControlFrame* combo = + static_cast<nsComboboxControlFrame*>(mFrame.GetFrame()); + static_cast<nsListControlFrame*>(combo->mDropdownFrame)-> + SetSuppressScrollbarUpdate(true); + nsCOMPtr<nsIPresShell> shell = mFrame->PresContext()->PresShell(); + shell->FrameNeedsReflow(combo->mDropdownFrame, nsIPresShell::eResize, + NS_FRAME_IS_DIRTY); + shell->FlushPendingNotifications(Flush_Layout); + if (mFrame.IsAlive()) { + combo = static_cast<nsComboboxControlFrame*>(mFrame.GetFrame()); + static_cast<nsListControlFrame*>(combo->mDropdownFrame)-> + SetSuppressScrollbarUpdate(false); + if (combo->mDelayedShowDropDown) { + combo->ShowDropDown(true); + } + } + } + return NS_OK; + } + nsWeakFrame mFrame; +}; + +void +nsComboboxControlFrame::GetAvailableDropdownSpace(WritingMode aWM, + nscoord* aBefore, + nscoord* aAfter, + LogicalPoint* aTranslation) +{ + MOZ_ASSERT(!XRE_IsContentProcess()); + // Note: At first glance, it appears that you could simply get the + // absolute bounding box for the dropdown list by first getting its + // view, then getting the view's nsIWidget, then asking the nsIWidget + // for its AbsoluteBounds. + // The problem with this approach, is that the dropdown list's bcoord + // location can change based on whether the dropdown is placed after + // or before the display frame. The approach taken here is to get the + // absolute position of the display frame and use its location to + // determine if the dropdown will go offscreen. + + // Normal frame geometry (eg GetOffsetTo, mRect) doesn't include transforms. + // In the special case that our transform is only a 2D translation we + // introduce this hack so that the dropdown will show up in the right place. + // Use null container size when converting a vector from logical to physical. + const nsSize nullContainerSize; + *aTranslation = LogicalPoint(aWM, GetCSSTransformTranslation(), + nullContainerSize); + *aBefore = 0; + *aAfter = 0; + + nsRect screen = nsFormControlFrame::GetUsableScreenRect(PresContext()); + nsSize containerSize = screen.Size(); + LogicalRect logicalScreen(aWM, screen, containerSize); + if (mLastDropDownAfterScreenBCoord == nscoord_MIN) { + LogicalRect thisScreenRect(aWM, GetScreenRectInAppUnits(), + containerSize); + mLastDropDownAfterScreenBCoord = thisScreenRect.BEnd(aWM) + + aTranslation->B(aWM); + mLastDropDownBeforeScreenBCoord = thisScreenRect.BStart(aWM) + + aTranslation->B(aWM); + } + + nscoord minBCoord; + nsPresContext* pc = PresContext()->GetToplevelContentDocumentPresContext(); + nsIFrame* root = pc ? pc->PresShell()->GetRootFrame() : nullptr; + if (root) { + minBCoord = LogicalRect(aWM, + root->GetScreenRectInAppUnits(), + containerSize).BStart(aWM); + if (mLastDropDownAfterScreenBCoord < minBCoord) { + // Don't allow the drop-down to be placed before the content area. + return; + } + } else { + minBCoord = logicalScreen.BStart(aWM); + } + + nscoord after = logicalScreen.BEnd(aWM) - mLastDropDownAfterScreenBCoord; + nscoord before = mLastDropDownBeforeScreenBCoord - minBCoord; + + // If the difference between the space before and after is less + // than a row-block-size, then we favor the space after. + if (before >= after) { + nsListControlFrame* lcf = static_cast<nsListControlFrame*>(mDropdownFrame); + nscoord rowBSize = lcf->GetBSizeOfARow(); + if (before < after + rowBSize) { + before -= rowBSize; + } + } + + *aAfter = after; + *aBefore = before; +} + +nsComboboxControlFrame::DropDownPositionState +nsComboboxControlFrame::AbsolutelyPositionDropDown() +{ + if (XRE_IsContentProcess()) { + return eDropDownPositionSuppressed; + } + + WritingMode wm = GetWritingMode(); + LogicalPoint translation(wm); + nscoord before, after; + mLastDropDownAfterScreenBCoord = nscoord_MIN; + GetAvailableDropdownSpace(wm, &before, &after, &translation); + if (before <= 0 && after <= 0) { + if (IsDroppedDown()) { + // Hide the view immediately to minimize flicker. + nsView* view = mDropdownFrame->GetView(); + view->GetViewManager()->SetViewVisibility(view, nsViewVisibility_kHide); + NS_DispatchToCurrentThread(new nsAsyncRollup(this)); + } + return eDropDownPositionSuppressed; + } + + LogicalSize dropdownSize = mDropdownFrame->GetLogicalSize(wm); + nscoord bSize = std::max(before, after); + nsListControlFrame* lcf = static_cast<nsListControlFrame*>(mDropdownFrame); + if (bSize < dropdownSize.BSize(wm)) { + if (lcf->GetNumDisplayRows() > 1) { + // The drop-down doesn't fit and currently shows more than 1 row - + // schedule a resize to show fewer rows. + NS_DispatchToCurrentThread(new nsAsyncResize(this)); + return eDropDownPositionPendingResize; + } + } else if (bSize > (dropdownSize.BSize(wm) + lcf->GetBSizeOfARow() * 1.5) && + lcf->GetDropdownCanGrow()) { + // The drop-down fits but there is room for at least 1.5 more rows - + // schedule a resize to show more rows if it has more rows to show. + // (1.5 rows for good measure to avoid any rounding issues that would + // lead to a loop of reflow requests) + NS_DispatchToCurrentThread(new nsAsyncResize(this)); + return eDropDownPositionPendingResize; + } + + // Position the drop-down after if there is room, otherwise place it before + // if there is room. If there is no room for it on either side then place + // it after (to avoid overlapping UI like the URL bar). + bool b = dropdownSize.BSize(wm)<= after || dropdownSize.BSize(wm) > before; + LogicalPoint dropdownPosition(wm, 0, b ? BSize(wm) : -dropdownSize.BSize(wm)); + + // Don't position the view unless the position changed since it might cause + // a call to NotifyGeometryChange() and an infinite loop here. + nsSize containerSize = GetSize(); + const LogicalPoint currentPos = + mDropdownFrame->GetLogicalPosition(containerSize); + const LogicalPoint newPos = dropdownPosition + translation; + if (currentPos != newPos) { + mDropdownFrame->SetPosition(wm, newPos, containerSize); + nsContainerFrame::PositionFrameView(mDropdownFrame); + } + return eDropDownPositionFinal; +} + +void +nsComboboxControlFrame::NotifyGeometryChange() +{ + if (XRE_IsContentProcess()) { + return; + } + + // We don't need to resize if we're not dropped down since ShowDropDown + // does that, or if we're dirty then the reflow callback does it, + // or if we have a delayed ShowDropDown pending. + if (IsDroppedDown() && + !(GetStateBits() & NS_FRAME_IS_DIRTY) && + !mDelayedShowDropDown) { + // Async because we're likely in a middle of a scroll here so + // frame/view positions are in flux. + RefPtr<nsResizeDropdownAtFinalPosition> resize = + new nsResizeDropdownAtFinalPosition(this); + NS_DispatchToCurrentThread(resize); + } +} + +//---------------------------------------------------------- +// +//---------------------------------------------------------- +#ifdef DO_REFLOW_DEBUG +static int myCounter = 0; + +static void printSize(char * aDesc, nscoord aSize) +{ + printf(" %s: ", aDesc); + if (aSize == NS_UNCONSTRAINEDSIZE) { + printf("UC"); + } else { + printf("%d", PX(aSize)); + } +} +#endif + +//------------------------------------------------------------------- +//-- Main Reflow for the Combobox +//------------------------------------------------------------------- + +nscoord +nsComboboxControlFrame::GetIntrinsicISize(nsRenderingContext* aRenderingContext, + nsLayoutUtils::IntrinsicISizeType aType) +{ + // get the scrollbar width, we'll use this later + nscoord scrollbarWidth = 0; + nsPresContext* presContext = PresContext(); + if (mListControlFrame) { + nsIScrollableFrame* scrollable = do_QueryFrame(mListControlFrame); + NS_ASSERTION(scrollable, "List must be a scrollable frame"); + scrollbarWidth = scrollable->GetNondisappearingScrollbarWidth( + presContext, aRenderingContext, GetWritingMode()); + } + + nscoord displayISize = 0; + if (MOZ_LIKELY(mDisplayFrame)) { + displayISize = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + mDisplayFrame, + aType); + } + + if (mDropdownFrame) { + nscoord dropdownContentISize; + bool isUsingOverlayScrollbars = + LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0; + if (aType == nsLayoutUtils::MIN_ISIZE) { + dropdownContentISize = mDropdownFrame->GetMinISize(aRenderingContext); + if (isUsingOverlayScrollbars) { + dropdownContentISize += scrollbarWidth; + } + } else { + NS_ASSERTION(aType == nsLayoutUtils::PREF_ISIZE, "Unexpected type"); + dropdownContentISize = mDropdownFrame->GetPrefISize(aRenderingContext); + if (isUsingOverlayScrollbars) { + dropdownContentISize += scrollbarWidth; + } + } + dropdownContentISize = NSCoordSaturatingSubtract(dropdownContentISize, + scrollbarWidth, + nscoord_MAX); + + displayISize = std::max(dropdownContentISize, displayISize); + } + + // add room for the dropmarker button if there is one + if ((!IsThemed() || + presContext->GetTheme()->ThemeNeedsComboboxDropmarker()) && + StyleDisplay()->mAppearance != NS_THEME_NONE) { + displayISize += scrollbarWidth; + } + + return displayISize; + +} + +nscoord +nsComboboxControlFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord minISize; + DISPLAY_MIN_WIDTH(this, minISize); + minISize = GetIntrinsicISize(aRenderingContext, nsLayoutUtils::MIN_ISIZE); + return minISize; +} + +nscoord +nsComboboxControlFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + nscoord prefISize; + DISPLAY_PREF_WIDTH(this, prefISize); + prefISize = GetIntrinsicISize(aRenderingContext, nsLayoutUtils::PREF_ISIZE); + return prefISize; +} + +void +nsComboboxControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + // Constraints we try to satisfy: + + // 1) Default inline size of button is the vertical scrollbar size + // 2) If the inline size of button is bigger than our inline size, set + // inline size of button to 0. + // 3) Default block size of button is block size of display area + // 4) Inline size of display area is whatever is left over from our + // inline size after allocating inline size for the button. + // 5) Block Size of display area is GetBSizeOfARow() on the + // mListControlFrame. + + if (!mDisplayFrame || !mButtonFrame || !mDropdownFrame) { + NS_ERROR("Why did the frame constructor allow this to happen? Fix it!!"); + return; + } + + // Make sure the displayed text is the same as the selected option, bug 297389. + int32_t selectedIndex; + nsAutoString selectedOptionText; + if (!mDroppedDown) { + selectedIndex = mListControlFrame->GetSelectedIndex(); + } + else { + // In dropped down mode the "selected index" is the hovered menu item, + // we want the last selected item which is |mDisplayedIndex| in this case. + selectedIndex = mDisplayedIndex; + } + if (selectedIndex != -1) { + mListControlFrame->GetOptionText(selectedIndex, selectedOptionText); + } + if (mDisplayedOptionText != selectedOptionText) { + RedisplayText(selectedIndex); + } + + // First reflow our dropdown so that we know how tall we should be. + ReflowDropdown(aPresContext, aReflowInput); + RefPtr<nsResizeDropdownAtFinalPosition> resize = + new nsResizeDropdownAtFinalPosition(this); + if (NS_SUCCEEDED(aPresContext->PresShell()->PostReflowCallback(resize))) { + // The reflow callback queue doesn't AddRef so we keep it alive until + // it's released in its ReflowFinished / ReflowCallbackCanceled. + Unused << resize.forget(); + } + + // Get the width of the vertical scrollbar. That will be the inline + // size of the dropdown button. + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord buttonISize; + const nsStyleDisplay *disp = StyleDisplay(); + if ((IsThemed(disp) && !aPresContext->GetTheme()->ThemeNeedsComboboxDropmarker()) || + StyleDisplay()->mAppearance == NS_THEME_NONE) { + buttonISize = 0; + } + else { + nsIScrollableFrame* scrollable = do_QueryFrame(mListControlFrame); + NS_ASSERTION(scrollable, "List must be a scrollable frame"); + buttonISize = scrollable->GetNondisappearingScrollbarWidth( + PresContext(), aReflowInput.mRenderingContext, wm); + if (buttonISize > aReflowInput.ComputedISize()) { + buttonISize = 0; + } + } + + mDisplayISize = aReflowInput.ComputedISize() - buttonISize; + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // The button should occupy the same space as a scrollbar + nsSize containerSize = aDesiredSize.PhysicalSize(); + LogicalRect buttonRect = mButtonFrame->GetLogicalRect(containerSize); + + buttonRect.IStart(wm) = + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(wm) + + mDisplayISize - + (aReflowInput.ComputedLogicalBorderPadding().IEnd(wm) - + aReflowInput.ComputedLogicalPadding().IEnd(wm)); + buttonRect.ISize(wm) = buttonISize; + + buttonRect.BStart(wm) = this->GetLogicalUsedBorder(wm).BStart(wm); + buttonRect.BSize(wm) = mDisplayFrame->BSize(wm) + + this->GetLogicalUsedPadding(wm).BStartEnd(wm); + + mButtonFrame->SetRect(buttonRect, containerSize); + + if (!NS_INLINE_IS_BREAK_BEFORE(aStatus) && + !NS_FRAME_IS_FULLY_COMPLETE(aStatus)) { + // This frame didn't fit inside a fragmentation container. Splitting + // a nsComboboxControlFrame makes no sense, so we override the status here. + aStatus = NS_FRAME_COMPLETE; + } +} + +//-------------------------------------------------------------- + +nsIAtom* +nsComboboxControlFrame::GetType() const +{ + return nsGkAtoms::comboboxControlFrame; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsComboboxControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("ComboboxControl"), aResult); +} +#endif + + +//---------------------------------------------------------------------- +// nsIComboboxControlFrame +//---------------------------------------------------------------------- +void +nsComboboxControlFrame::ShowDropDown(bool aDoDropDown) +{ + MOZ_ASSERT(!XRE_IsContentProcess()); + mDelayedShowDropDown = false; + EventStates eventStates = mContent->AsElement()->State(); + if (aDoDropDown && eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return; + } + + if (!mDroppedDown && aDoDropDown) { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm || fm->GetFocusedContent() == GetContent()) { + DropDownPositionState state = AbsolutelyPositionDropDown(); + if (state == eDropDownPositionFinal) { + ShowList(aDoDropDown); // might destroy us + } else if (state == eDropDownPositionPendingResize) { + // Delay until after the resize reflow, see nsAsyncResize. + mDelayedShowDropDown = true; + } + } else { + // Delay until we get focus, see SetFocus(). + mDelayedShowDropDown = true; + } + } else if (mDroppedDown && !aDoDropDown) { + ShowList(aDoDropDown); // might destroy us + } +} + +void +nsComboboxControlFrame::SetDropDown(nsIFrame* aDropDownFrame) +{ + mDropdownFrame = aDropDownFrame; + mListControlFrame = do_QueryFrame(mDropdownFrame); + if (!sFocused && nsContentUtils::IsFocusedContent(GetContent())) { + sFocused = this; + nsListControlFrame::ComboboxFocusSet(); + } +} + +nsIFrame* +nsComboboxControlFrame::GetDropDown() +{ + return mDropdownFrame; +} + +/////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsComboboxControlFrame::RedisplaySelectedText() +{ + nsAutoScriptBlocker scriptBlocker; + return RedisplayText(mListControlFrame->GetSelectedIndex()); +} + +nsresult +nsComboboxControlFrame::RedisplayText(int32_t aIndex) +{ + // Get the text to display + if (aIndex != -1) { + mListControlFrame->GetOptionText(aIndex, mDisplayedOptionText); + } else { + mDisplayedOptionText.Truncate(); + } + mDisplayedIndex = aIndex; + + REFLOW_DEBUG_MSG2("RedisplayText \"%s\"\n", + NS_LossyConvertUTF16toASCII(mDisplayedOptionText).get()); + + // Send reflow command because the new text maybe larger + nsresult rv = NS_OK; + if (mDisplayContent) { + // Don't call ActuallyDisplayText(true) directly here since that + // could cause recursive frame construction. See bug 283117 and the comment in + // HandleRedisplayTextEvent() below. + + // Revoke outstanding events to avoid out-of-order events which could mean + // displaying the wrong text. + mRedisplayTextEvent.Revoke(); + + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "If we happen to run our redisplay event now, we might kill " + "ourselves!"); + + RefPtr<RedisplayTextEvent> event = new RedisplayTextEvent(this); + mRedisplayTextEvent = event; + nsContentUtils::AddScriptRunner(event); + } + return rv; +} + +void +nsComboboxControlFrame::HandleRedisplayTextEvent() +{ + // First, make sure that the content model is up to date and we've + // constructed the frames for all our content in the right places. + // Otherwise they'll end up under the wrong insertion frame when we + // ActuallyDisplayText, since that flushes out the content sink by + // calling SetText on a DOM node with aNotify set to true. See bug + // 289730. + nsWeakFrame weakThis(this); + PresContext()->Document()-> + FlushPendingNotifications(Flush_ContentAndNotify); + if (!weakThis.IsAlive()) + return; + + // Redirect frame insertions during this method (see GetContentInsertionFrame()) + // so that any reframing that the frame constructor forces upon us is inserted + // into the correct parent (mDisplayFrame). See bug 282607. + NS_PRECONDITION(!mInRedisplayText, "Nested RedisplayText"); + mInRedisplayText = true; + mRedisplayTextEvent.Forget(); + + ActuallyDisplayText(true); + // XXXbz This should perhaps be eResize. Check. + PresContext()->PresShell()->FrameNeedsReflow(mDisplayFrame, + nsIPresShell::eStyleChange, + NS_FRAME_IS_DIRTY); + + mInRedisplayText = false; +} + +void +nsComboboxControlFrame::ActuallyDisplayText(bool aNotify) +{ + if (mDisplayedOptionText.IsEmpty()) { + // Have to use a non-breaking space for line-block-size calculations + // to be right + static const char16_t space = 0xA0; + mDisplayContent->SetText(&space, 1, aNotify); + } else { + mDisplayContent->SetText(mDisplayedOptionText, aNotify); + } +} + +int32_t +nsComboboxControlFrame::GetIndexOfDisplayArea() +{ + return mDisplayedIndex; +} + +//---------------------------------------------------------------------- +// nsISelectControlFrame +//---------------------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) +{ + nsISelectControlFrame* listFrame = do_QueryFrame(mDropdownFrame); + if (!listFrame) + return NS_ERROR_FAILURE; + + return listFrame->DoneAddingChildren(aIsDone); +} + +NS_IMETHODIMP +nsComboboxControlFrame::AddOption(int32_t aIndex) +{ + if (aIndex <= mDisplayedIndex) { + ++mDisplayedIndex; + } + + nsListControlFrame* lcf = static_cast<nsListControlFrame*>(mDropdownFrame); + return lcf->AddOption(aIndex); +} + + +NS_IMETHODIMP +nsComboboxControlFrame::RemoveOption(int32_t aIndex) +{ + nsWeakFrame weakThis(this); + if (mListControlFrame->GetNumberOfOptions() > 0) { + if (aIndex < mDisplayedIndex) { + --mDisplayedIndex; + } else if (aIndex == mDisplayedIndex) { + mDisplayedIndex = 0; // IE6 compat + RedisplayText(mDisplayedIndex); + } + } + else { + // If we removed the last option, we need to blank things out + RedisplayText(-1); + } + + if (!weakThis.IsAlive()) + return NS_OK; + + nsListControlFrame* lcf = static_cast<nsListControlFrame*>(mDropdownFrame); + return lcf->RemoveOption(aIndex); +} + +NS_IMETHODIMP +nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) +{ + nsAutoScriptBlocker scriptBlocker; + RedisplayText(aNewIndex); + NS_ASSERTION(mDropdownFrame, "No dropdown frame!"); + + nsISelectControlFrame* listFrame = do_QueryFrame(mDropdownFrame); + NS_ASSERTION(listFrame, "No list frame!"); + + return listFrame->OnSetSelectedIndex(aOldIndex, aNewIndex); +} + +// End nsISelectControlFrame +//---------------------------------------------------------------------- + +nsresult +nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + NS_ENSURE_ARG_POINTER(aEventStatus); + + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return NS_OK; + } + +#if COMBOBOX_ROLLUP_CONSUME_EVENT == 0 + if (aEvent->mMessage == eMouseDown) { + nsIWidget* widget = GetNearestWidget(); + if (widget && GetContent() == widget->GetLastRollup()) { + // This event did a Rollup on this control - prevent it from opening + // the dropdown again! + *aEventStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + } +#endif + + // If we have style that affects how we are selected, feed event down to + // nsFrame::HandleEvent so that selection takes place when appropriate. + const nsStyleUserInterface* uiStyle = StyleUserInterface(); + if (uiStyle->mUserInput == StyleUserInput::None || + uiStyle->mUserInput == StyleUserInput::Disabled) { + return nsBlockFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + + +nsresult +nsComboboxControlFrame::SetFormProperty(nsIAtom* aName, const nsAString& aValue) +{ + nsIFormControlFrame* fcFrame = do_QueryFrame(mDropdownFrame); + if (!fcFrame) { + return NS_NOINTERFACE; + } + + return fcFrame->SetFormProperty(aName, aValue); +} + +nsContainerFrame* +nsComboboxControlFrame::GetContentInsertionFrame() { + return mInRedisplayText ? mDisplayFrame : mDropdownFrame->GetContentInsertionFrame(); +} + +nsresult +nsComboboxControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + // The frames used to display the combo box and the button used to popup the dropdown list + // are created through anonymous content. The dropdown list is not created through anonymous + // content because its frame is initialized specifically for the drop-down case and it is placed + // a special list referenced through NS_COMBO_FRAME_POPUP_LIST_INDEX to keep separate from the + // layout of the display and button. + // + // Note: The value attribute of the display content is set when an item is selected in the dropdown list. + // If the content specified below does not honor the value attribute than nothing will be displayed. + + // For now the content that is created corresponds to two input buttons. It would be better to create the + // tag as something other than input, but then there isn't any way to create a button frame since it + // isn't possible to set the display type in CSS2 to create a button frame. + + // create content used for display + //nsIAtom* tag = NS_Atomize("mozcombodisplay"); + + // Add a child text content node for the label + + nsNodeInfoManager *nimgr = mContent->NodeInfo()->NodeInfoManager(); + + mDisplayContent = new nsTextNode(nimgr); + + // set the value of the text node + mDisplayedIndex = mListControlFrame->GetSelectedIndex(); + if (mDisplayedIndex != -1) { + mListControlFrame->GetOptionText(mDisplayedIndex, mDisplayedOptionText); + } + ActuallyDisplayText(false); + + if (!aElements.AppendElement(mDisplayContent)) + return NS_ERROR_OUT_OF_MEMORY; + + mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button); + if (!mButtonContent) + return NS_ERROR_OUT_OF_MEMORY; + + // make someone to listen to the button. If its pressed by someone like Accessibility + // then open or close the combo box. + mButtonListener = new nsComboButtonListener(this); + mButtonContent->AddEventListener(NS_LITERAL_STRING("click"), mButtonListener, + false, false); + + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("button"), false); + // Set tabindex="-1" so that the button is not tabbable + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, + NS_LITERAL_STRING("-1"), false); + + WritingMode wm = GetWritingMode(); + if (wm.IsVertical()) { + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::orientation, + wm.IsVerticalRL() ? NS_LITERAL_STRING("left") + : NS_LITERAL_STRING("right"), + false); + } + + if (!aElements.AppendElement(mButtonContent)) + return NS_ERROR_OUT_OF_MEMORY; + + return NS_OK; +} + +void +nsComboboxControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mDisplayContent) { + aElements.AppendElement(mDisplayContent); + } + + if (mButtonContent) { + aElements.AppendElement(mButtonContent); + } +} + +// XXXbz this is a for-now hack. Now that display:inline-block works, +// need to revisit this. +class nsComboboxDisplayFrame : public nsBlockFrame { +public: + NS_DECL_FRAMEARENA_HELPERS + + nsComboboxDisplayFrame (nsStyleContext* aContext, + nsComboboxControlFrame* aComboBox) + : nsBlockFrame(aContext), + mComboBox(aComboBox) + {} + + // Need this so that line layout knows that this block's inline size + // depends on the available inline size. + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsBlockFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplacedContainsBlock)); + } + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + +protected: + nsComboboxControlFrame* mComboBox; +}; + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame) + +nsIAtom* +nsComboboxDisplayFrame::GetType() const +{ + return nsGkAtoms::comboboxDisplayFrame; +} + +void +nsComboboxDisplayFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + ReflowInput state(aReflowInput); + if (state.ComputedBSize() == NS_INTRINSICSIZE) { + // Note that the only way we can have a computed block size here is + // if the combobox had a specified block size. If it didn't, size + // based on what our rows look like, for lack of anything better. + state.SetComputedBSize(mComboBox->mListControlFrame->GetBSizeOfARow()); + } + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord computedISize = mComboBox->mDisplayISize - + state.ComputedLogicalBorderPadding().IStartEnd(wm); + if (computedISize < 0) { + computedISize = 0; + } + state.SetComputedISize(computedISize); + nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + aStatus = NS_FRAME_COMPLETE; // this type of frame can't be split +} + +void +nsComboboxDisplayFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + nsDisplayListCollection set; + nsBlockFrame::BuildDisplayList(aBuilder, aDirtyRect, set); + + // remove background items if parent frame is themed + if (mComboBox->IsThemed()) { + set.BorderBackground()->DeleteAll(); + } + + set.MoveTo(aLists); +} + +nsIFrame* +nsComboboxControlFrame::CreateFrameFor(nsIContent* aContent) +{ + NS_PRECONDITION(nullptr != aContent, "null ptr"); + + NS_ASSERTION(mDisplayContent, "mDisplayContent can't be null!"); + + if (mDisplayContent != aContent) { + // We only handle the frames for mDisplayContent here + return nullptr; + } + + // Get PresShell + nsIPresShell *shell = PresContext()->PresShell(); + StyleSetHandle styleSet = shell->StyleSet(); + + // create the style contexts for the anonymous block frame and text frame + RefPtr<nsStyleContext> styleContext; + styleContext = styleSet-> + ResolveAnonymousBoxStyle(nsCSSAnonBoxes::mozDisplayComboboxControlFrame, + mStyleContext, + nsStyleSet::eSkipParentDisplayBasedStyleFixup); + + RefPtr<nsStyleContext> textStyleContext; + textStyleContext = + styleSet->ResolveStyleForText(mDisplayContent, mStyleContext); + + // Start by creating our anonymous block frame + mDisplayFrame = new (shell) nsComboboxDisplayFrame(styleContext, this); + mDisplayFrame->Init(mContent, this, nullptr); + + // Create a text frame and put it inside the block frame + nsIFrame* textFrame = NS_NewTextFrame(shell, textStyleContext); + + // initialize the text frame + textFrame->Init(aContent, mDisplayFrame, nullptr); + mDisplayContent->SetPrimaryFrame(textFrame); + + nsFrameList textList(textFrame, textFrame); + mDisplayFrame->SetInitialChildList(kPrincipalList, textList); + return mDisplayFrame; +} + +void +nsComboboxControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + if (sFocused == this) { + sFocused = nullptr; + } + + // Revoke any pending RedisplayTextEvent + mRedisplayTextEvent.Revoke(); + + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + + if (mDroppedDown) { + MOZ_ASSERT(mDropdownFrame, "mDroppedDown without frame"); + nsView* view = mDropdownFrame->GetView(); + MOZ_ASSERT(view); + nsIWidget* widget = view->GetWidget(); + if (widget) { + widget->CaptureRollupEvents(this, false); + } + } + + // Cleanup frames in popup child list + mPopupFrames.DestroyFramesFrom(aDestructRoot); + nsContentUtils::DestroyAnonymousContent(&mDisplayContent); + nsContentUtils::DestroyAnonymousContent(&mButtonContent); + nsBlockFrame::DestroyFrom(aDestructRoot); +} + +const nsFrameList& +nsComboboxControlFrame::GetChildList(ChildListID aListID) const +{ + if (kSelectPopupList == aListID) { + return mPopupFrames; + } + return nsBlockFrame::GetChildList(aListID); +} + +void +nsComboboxControlFrame::GetChildLists(nsTArray<ChildList>* aLists) const +{ + nsBlockFrame::GetChildLists(aLists); + mPopupFrames.AppendIfNonempty(aLists, kSelectPopupList); +} + +void +nsComboboxControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) +{ + if (kSelectPopupList == aListID) { + mPopupFrames.SetFrames(aChildList); + } else { + for (nsFrameList::Enumerator e(aChildList); !e.AtEnd(); e.Next()) { + nsCOMPtr<nsIFormControl> formControl = + do_QueryInterface(e.get()->GetContent()); + if (formControl && formControl->GetType() == NS_FORM_BUTTON_BUTTON) { + mButtonFrame = e.get(); + break; + } + } + NS_ASSERTION(mButtonFrame, "missing button frame in initial child list"); + nsBlockFrame::SetInitialChildList(aListID, aChildList); + } +} + +//---------------------------------------------------------------------- + //nsIRollupListener +//---------------------------------------------------------------------- +bool +nsComboboxControlFrame::Rollup(uint32_t aCount, bool aFlush, + const nsIntPoint* pos, nsIContent** aLastRolledUp) +{ + if (!mDroppedDown) { + return false; + } + + bool consume = !!COMBOBOX_ROLLUP_CONSUME_EVENT; + nsWeakFrame weakFrame(this); + mListControlFrame->AboutToRollup(); // might destroy us + if (!weakFrame.IsAlive()) { + return consume; + } + ShowDropDown(false); // might destroy us + if (weakFrame.IsAlive()) { + mListControlFrame->CaptureMouseEvents(false); + } + + if (aFlush && weakFrame.IsAlive()) { + // The popup's visibility doesn't update until the minimize animation has + // finished, so call UpdateWidgetGeometry to update it right away. + nsViewManager* viewManager = mDropdownFrame->GetView()->GetViewManager(); + viewManager->UpdateWidgetGeometry(); // might destroy us + } + + if (!weakFrame.IsAlive()) { + return consume; + } + + if (aLastRolledUp) { + *aLastRolledUp = GetContent(); + } + return consume; +} + +nsIWidget* +nsComboboxControlFrame::GetRollupWidget() +{ + nsView* view = mDropdownFrame->GetView(); + MOZ_ASSERT(view); + return view->GetWidget(); +} + +void +nsComboboxControlFrame::RollupFromList() +{ + if (ShowList(false)) + mListControlFrame->CaptureMouseEvents(false); +} + +int32_t +nsComboboxControlFrame::UpdateRecentIndex(int32_t aIndex) +{ + int32_t index = mRecentSelectedIndex; + if (mRecentSelectedIndex == NS_SKIP_NOTIFY_INDEX || aIndex == NS_SKIP_NOTIFY_INDEX) + mRecentSelectedIndex = aIndex; + return index; +} + +class nsDisplayComboboxFocus : public nsDisplayItem { +public: + nsDisplayComboboxFocus(nsDisplayListBuilder* aBuilder, + nsComboboxControlFrame* aFrame) + : nsDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayComboboxFocus); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayComboboxFocus() { + MOZ_COUNT_DTOR(nsDisplayComboboxFocus); + } +#endif + + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override; + NS_DISPLAY_DECL_NAME("ComboboxFocus", TYPE_COMBOBOX_FOCUS) +}; + +void nsDisplayComboboxFocus::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) +{ + static_cast<nsComboboxControlFrame*>(mFrame) + ->PaintFocus(*aCtx->GetDrawTarget(), ToReferenceFrame()); +} + +void +nsComboboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ +#ifdef NOISY + printf("%p paint at (%d, %d, %d, %d)\n", this, + aDirtyRect.x, aDirtyRect.y, aDirtyRect.width, aDirtyRect.height); +#endif + + if (aBuilder->IsForEventDelivery()) { + // Don't allow children to receive events. + // REVIEW: following old GetFrameForPoint + DisplayBorderBackgroundOutline(aBuilder, aLists); + } else { + // REVIEW: Our in-flow child frames are inline-level so they will paint in our + // content list, so we don't need to mess with layers. + nsBlockFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists); + } + + // draw a focus indicator only when focus rings should be drawn + nsIDocument* doc = mContent->GetComposedDoc(); + if (doc) { + nsPIDOMWindowOuter* window = doc->GetWindow(); + if (window && window->ShouldShowFocusRing()) { + nsPresContext *presContext = PresContext(); + const nsStyleDisplay *disp = StyleDisplay(); + if ((!IsThemed(disp) || + !presContext->GetTheme()->ThemeDrawsFocusForWidget(disp->mAppearance)) && + mDisplayFrame && IsVisibleForPainting(aBuilder)) { + aLists.Content()->AppendNewToTop( + new (aBuilder) nsDisplayComboboxFocus(aBuilder, this)); + } + } + } + + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +void nsComboboxControlFrame::PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt) +{ + /* Do we need to do anything? */ + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED) || sFocused != this) + return; + + int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + + nsRect clipRect = mDisplayFrame->GetRect() + aPt; + aDrawTarget.PushClipRect(NSRectToSnappedRect(clipRect, + appUnitsPerDevPixel, + aDrawTarget)); + + // REVIEW: Why does the old code paint mDisplayFrame again? We've + // already painted it in the children above. So clipping it here won't do + // us much good. + + ///////////////////// + // draw focus + + StrokeOptions strokeOptions; + nsLayoutUtils::InitDashPattern(strokeOptions, NS_STYLE_BORDER_STYLE_DOTTED); + ColorPattern color(ToDeviceColor(StyleColor()->mColor)); + nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1); + clipRect.width -= onePixel; + clipRect.height -= onePixel; + Rect r = ToRect(nsLayoutUtils::RectToGfxRect(clipRect, appUnitsPerDevPixel)); + StrokeSnappedEdgesOfRect(r, aDrawTarget, color, strokeOptions); + + aDrawTarget.PopClip(); +} + +//--------------------------------------------------------- +// gets the content (an option) by index and then set it as +// being selected or not selected +//--------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) +{ + if (mDroppedDown) { + nsISelectControlFrame *selectFrame = do_QueryFrame(mListControlFrame); + if (selectFrame) { + selectFrame->OnOptionSelected(aIndex, aSelected); + } + } else { + if (aSelected) { + nsAutoScriptBlocker blocker; + RedisplayText(aIndex); + } else { + nsWeakFrame weakFrame(this); + RedisplaySelectedText(); + if (weakFrame.IsAlive()) { + FireValueChangeEvent(); // Fire after old option is unselected + } + } + } + + return NS_OK; +} + +void nsComboboxControlFrame::FireValueChangeEvent() +{ + // Fire ValueChange event to indicate data value of combo box has changed + nsContentUtils::AddScriptRunner( + new AsyncEventDispatcher(mContent, NS_LITERAL_STRING("ValueChange"), true, + false)); +} + +void +nsComboboxControlFrame::OnContentReset() +{ + if (mListControlFrame) { + mListControlFrame->OnContentReset(); + } +} + + +//-------------------------------------------------------- +// nsIStatefulFrame +//-------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::SaveState(nsPresState** aState) +{ + MOZ_ASSERT(!(*aState)); + (*aState) = new nsPresState(); + (*aState)->SetDroppedDown(mDroppedDown); + return NS_OK; +} + +NS_IMETHODIMP +nsComboboxControlFrame::RestoreState(nsPresState* aState) +{ + if (!aState) { + return NS_ERROR_FAILURE; + } + ShowList(aState->GetDroppedDown()); // might destroy us + return NS_OK; +} + +// Append a suffix so that the state key for the combobox is different +// from the state key the list control uses to sometimes save the scroll +// position for the same Element +NS_IMETHODIMP +nsComboboxControlFrame::GenerateStateKey(nsIContent* aContent, + nsIDocument* aDocument, + nsACString& aKey) +{ + nsresult rv = nsContentUtils::GenerateStateKey(aContent, aDocument, aKey); + if (NS_FAILED(rv) || aKey.IsEmpty()) { + return rv; + } + aKey.Append("CCF"); + return NS_OK; +} + +// Fennec uses a custom combobox built-in widget. +// + +/* static */ +bool +nsComboboxControlFrame::ToolkitHasNativePopup() +{ +#ifdef MOZ_USE_NATIVE_POPUP_WINDOWS + return true; +#else + return false; +#endif /* MOZ_USE_NATIVE_POPUP_WINDOWS */ +} + diff --git a/layout/forms/nsComboboxControlFrame.h b/layout/forms/nsComboboxControlFrame.h new file mode 100644 index 000000000..22849e8d1 --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.h @@ -0,0 +1,328 @@ +/* -*- 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/. */ + +#ifndef nsComboboxControlFrame_h___ +#define nsComboboxControlFrame_h___ + +#ifdef DEBUG_evaughan +//#define DEBUG_rods +#endif + +#ifdef DEBUG_rods +//#define DO_REFLOW_DEBUG +//#define DO_REFLOW_COUNTER +//#define DO_UNCONSTRAINED_CHECK +//#define DO_PIXELS +//#define DO_NEW_REFLOW +#endif + +//Mark used to indicate when onchange has been fired for current combobox item +#define NS_SKIP_NOTIFY_INDEX -2 + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIComboboxControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsISelectControlFrame.h" +#include "nsIRollupListener.h" +#include "nsIStatefulFrame.h" +#include "nsThreadUtils.h" + +class nsStyleContext; +class nsIListControlFrame; +class nsComboboxDisplayFrame; +class nsIDOMEventListener; +class nsIScrollableFrame; + +namespace mozilla { +namespace gfx { +class DrawTarget; +} // namespace gfx +} // namespace mozilla + +class nsComboboxControlFrame final : public nsBlockFrame, + public nsIFormControlFrame, + public nsIComboboxControlFrame, + public nsIAnonymousContentCreator, + public nsISelectControlFrame, + public nsIRollupListener, + public nsIStatefulFrame +{ + typedef mozilla::gfx::DrawTarget DrawTarget; + +public: + friend nsContainerFrame* NS_NewComboboxControlFrame(nsIPresShell* aPresShell, + nsStyleContext* aContext, + nsFrameState aFlags); + friend class nsComboboxDisplayFrame; + + explicit nsComboboxControlFrame(nsStyleContext* aContext); + ~nsComboboxControlFrame(); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + virtual nsIFrame* CreateFrameFor(nsIContent* aContent) override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + void PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt); + + // XXXbz this is only needed to prevent the quirk percent height stuff from + // leaking out of the combobox. We may be able to get rid of this as more + // things move to IsFrameOfType. + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsBlockFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + virtual nsIScrollableFrame* GetScrollTargetFrame() override { + return do_QueryFrame(mDropdownFrame); + } + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + virtual void SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) override; + virtual const nsFrameList& GetChildList(ChildListID aListID) const override; + virtual void GetChildLists(nsTArray<ChildList>* aLists) const override; + + virtual nsContainerFrame* GetContentInsertionFrame() override; + + // nsIFormControlFrame + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + /** + * Inform the control that it got (or lost) focus. + * If it lost focus, the dropdown menu will be rolled up if needed, + * and FireOnChange() will be called. + * @param aOn true if got focus, false if lost focus. + * @param aRepaint if true then force repaint (NOTE: we always force repaint currently) + * @note This method might destroy |this|. + */ + virtual void SetFocus(bool aOn, bool aRepaint) override; + + //nsIComboboxControlFrame + virtual bool IsDroppedDown() override { return mDroppedDown; } + /** + * @note This method might destroy |this|. + */ + virtual void ShowDropDown(bool aDoDropDown) override; + virtual nsIFrame* GetDropDown() override; + virtual void SetDropDown(nsIFrame* aDropDownFrame) override; + /** + * @note This method might destroy |this|. + */ + virtual void RollupFromList() override; + + /** + * Return the available space before and after this frame for + * placing the drop-down list, and the current 2D translation. + * Note that either or both can be less than or equal to zero, + * if both are then the drop-down should be closed. + */ + void GetAvailableDropdownSpace(mozilla::WritingMode aWM, + nscoord* aBefore, + nscoord* aAfter, + mozilla::LogicalPoint* aTranslation); + virtual int32_t GetIndexOfDisplayArea() override; + /** + * @note This method might destroy |this|. + */ + NS_IMETHOD RedisplaySelectedText() override; + virtual int32_t UpdateRecentIndex(int32_t aIndex) override; + virtual void OnContentReset() override; + + + bool IsOpenInParentProcess() override + { + return mIsOpenInParentProcess; + } + + void SetOpenInParentProcess(bool aVal) override + { + mIsOpenInParentProcess = aVal; + } + + // nsISelectControlFrame + NS_IMETHOD AddOption(int32_t index) override; + NS_IMETHOD RemoveOption(int32_t index) override; + NS_IMETHOD DoneAddingChildren(bool aIsDone) override; + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) override; + NS_IMETHOD OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) override; + + //nsIRollupListener + /** + * Hide the dropdown menu and stop capturing mouse events. + * @note This method might destroy |this|. + */ + virtual bool Rollup(uint32_t aCount, bool aFlush, + const nsIntPoint* pos, nsIContent** aLastRolledUp) override; + virtual void NotifyGeometryChange() override; + + /** + * A combobox should roll up if a mousewheel event happens outside of + * the popup area. + */ + virtual bool ShouldRollupOnMouseWheelEvent() override + { return true; } + + virtual bool ShouldConsumeOnMouseWheelEvent() override + { return false; } + + /** + * A combobox should not roll up if activated by a mouse activate message + * (eg. X-mouse). + */ + virtual bool ShouldRollupOnMouseActivate() override + { return false; } + + virtual uint32_t GetSubmenuWidgetChain(nsTArray<nsIWidget*> *aWidgetChain) override + { return 0; } + + virtual nsIWidget* GetRollupWidget() override; + + //nsIStatefulFrame + NS_IMETHOD SaveState(nsPresState** aState) override; + NS_IMETHOD RestoreState(nsPresState* aState) override; + NS_IMETHOD GenerateStateKey(nsIContent* aContent, + nsIDocument* aDocument, + nsACString& aKey) override; + + static bool ToolkitHasNativePopup(); + +protected: + friend class RedisplayTextEvent; + friend class nsAsyncResize; + friend class nsResizeDropdownAtFinalPosition; + + // Utilities + void ReflowDropdown(nsPresContext* aPresContext, + const ReflowInput& aReflowInput); + + enum DropDownPositionState { + // can't show the dropdown at its current position + eDropDownPositionSuppressed, + // a resize reflow is pending, don't show it yet + eDropDownPositionPendingResize, + // the dropdown has its final size and position and can be displayed here + eDropDownPositionFinal + }; + DropDownPositionState AbsolutelyPositionDropDown(); + + // Helper for GetMinISize/GetPrefISize + nscoord GetIntrinsicISize(nsRenderingContext* aRenderingContext, + nsLayoutUtils::IntrinsicISizeType aType); + + class RedisplayTextEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit RedisplayTextEvent(nsComboboxControlFrame *c) : mControlFrame(c) {} + void Revoke() { mControlFrame = nullptr; } + private: + nsComboboxControlFrame *mControlFrame; + }; + + /** + * Show or hide the dropdown list. + * @note This method might destroy |this|. + */ + void ShowPopup(bool aShowPopup); + + /** + * Show or hide the dropdown list. + * @param aShowList true to show, false to hide the dropdown. + * @note This method might destroy |this|. + * @return false if this frame is destroyed, true if still alive. + */ + bool ShowList(bool aShowList); + void CheckFireOnChange(); + void FireValueChangeEvent(); + nsresult RedisplayText(int32_t aIndex); + void HandleRedisplayTextEvent(); + void ActuallyDisplayText(bool aNotify); + +private: + // If our total transform to the root frame of the root document is only a 2d + // translation then return that translation, otherwise returns (0,0). + nsPoint GetCSSTransformTranslation(); + +protected: + nsFrameList mPopupFrames; // additional named child list + nsCOMPtr<nsIContent> mDisplayContent; // Anonymous content used to display the current selection + nsCOMPtr<nsIContent> mButtonContent; // Anonymous content for the button + nsContainerFrame* mDisplayFrame; // frame to display selection + nsIFrame* mButtonFrame; // button frame + nsIFrame* mDropdownFrame; // dropdown list frame + nsIListControlFrame * mListControlFrame; // ListControl Interface for the dropdown frame + + // The inline size of our display area. Used by that frame's reflow + // to size to the full inline size except the drop-marker. + nscoord mDisplayISize; + + nsRevocableEventPtr<RedisplayTextEvent> mRedisplayTextEvent; + + int32_t mRecentSelectedIndex; + int32_t mDisplayedIndex; + nsString mDisplayedOptionText; + + // make someone to listen to the button. If its programmatically pressed by someone like Accessibility + // then open or close the combo box. + nsCOMPtr<nsIDOMEventListener> mButtonListener; + + // The last y-positions used for estimating available space before and + // after for the dropdown list in GetAvailableDropdownSpace. These are + // reset to nscoord_MIN in AbsolutelyPositionDropDown when placing the + // dropdown at its actual position. The GetAvailableDropdownSpace call + // from nsListControlFrame::ReflowAsDropdown use the last position. + nscoord mLastDropDownBeforeScreenBCoord; + nscoord mLastDropDownAfterScreenBCoord; + // Current state of the dropdown list, true is dropped down. + bool mDroppedDown; + // See comment in HandleRedisplayTextEvent(). + bool mInRedisplayText; + // Acting on ShowDropDown(true) is delayed until we're focused. + bool mDelayedShowDropDown; + + bool mIsOpenInParentProcess; + + // static class data member for Bug 32920 + // only one control can be focused at a time + static nsComboboxControlFrame* sFocused; + +#ifdef DO_REFLOW_COUNTER + int32_t mReflowId; +#endif +}; + +#endif diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp new file mode 100644 index 000000000..df2e43986 --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.cpp @@ -0,0 +1,414 @@ +/* -*- 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/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + */ + +#include "nsDateTimeControlFrame.h" + +#include "nsContentUtils.h" +#include "nsFormControlFrame.h" +#include "nsGkAtoms.h" +#include "nsContentUtils.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentList.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsNodeInfoManager.h" +#include "nsIDateTimeInputArea.h" +#include "nsIObserverService.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMMutationEvent.h" +#include "jsapi.h" +#include "nsJSUtils.h" +#include "nsThreadUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* +NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsDateTimeControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame) + +NS_QUERYFRAME_HEAD(nsDateTimeControlFrame) + NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsDateTimeControlFrame::nsDateTimeControlFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) +{ +} + +void +nsDateTimeControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + nsContentUtils::DestroyAnonymousContent(&mInputAreaContent); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +void +nsDateTimeControlFrame::UpdateInputBoxValue() +{ + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + inputAreaContent->NotifyInputElementValueChanged(); + } +} + +void +nsDateTimeControlFrame::SetValueFromPicker(const DateTimeValue& aValue) +{ + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + AutoJSAPI api; + if (!api.Init(mContent->OwnerDoc()->GetScopeObject())) { + return; + } + + JSObject* wrapper = mContent->GetWrapper(); + if (!wrapper) { + return; + } + + JSObject* scope = xpc::GetXBLScope(api.cx(), wrapper); + AutoJSAPI jsapi; + if (!scope || !jsapi.Init(scope)) { + return; + } + + JS::Rooted<JS::Value> jsValue(jsapi.cx()); + if (!ToJSValue(jsapi.cx(), aValue, &jsValue)) { + return; + } + + inputAreaContent->SetValueFromPicker(jsValue); + } +} + +void +nsDateTimeControlFrame::SetPickerState(bool aOpen) +{ + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + inputAreaContent->SetPickerState(aOpen); + } +} + +void +nsDateTimeControlFrame::HandleFocusEvent() +{ + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + inputAreaContent->FocusInnerTextBox(); + } +} + +void +nsDateTimeControlFrame::HandleBlurEvent() +{ + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + inputAreaContent->BlurInnerTextBox(); + } +} + +nscoord +nsDateTimeControlFrame::GetMinISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::MIN_ISIZE); + } else { + result = 0; + } + + return result; +} + +nscoord +nsDateTimeControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_PREF_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::PREF_ISIZE); + } else { + result = 0; + } + + return result; +} + +void +nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + + DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d", + aReflowInput.AvailableWidth(), + aReflowInput.AvailableHeight())); + + NS_ASSERTION(mInputAreaContent, "The input area content must exist!"); + + const WritingMode myWM = aReflowInput.GetWritingMode(); + + // The ISize of our content box, which is the available ISize + // for our anonymous content: + const nscoord contentBoxISize = aReflowInput.ComputedISize(); + nscoord contentBoxBSize = aReflowInput.ComputedBSize(); + + // Figure out our border-box sizes as well (by adding borderPadding to + // content-box sizes): + const nscoord borderBoxISize = contentBoxISize + + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(myWM); + + nscoord borderBoxBSize; + if (contentBoxBSize != NS_INTRINSICSIZE) { + borderBoxBSize = contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize. + + nsIFrame* inputAreaFrame = mFrames.FirstChild(); + if (!inputAreaFrame) { // display:none? + if (contentBoxBSize == NS_INTRINSICSIZE) { + contentBoxBSize = 0; + borderBoxBSize = + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } + } else { + NS_ASSERTION(inputAreaFrame->GetContent() == mInputAreaContent, + "What is this child doing here?"); + + ReflowOutput childDesiredSize(aReflowInput); + + WritingMode wm = inputAreaFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + ReflowInput childReflowOuput(aPresContext, aReflowInput, + inputAreaFrame, availSize); + + // Convert input area margin into my own writing-mode (in case it differs): + LogicalMargin childMargin = + childReflowOuput.ComputedLogicalMargin().ConvertTo(myWM, wm); + + // offsets of input area frame within this frame: + LogicalPoint + childOffset(myWM, + aReflowInput.ComputedLogicalBorderPadding().IStart(myWM) + + childMargin.IStart(myWM), + aReflowInput.ComputedLogicalBorderPadding().BStart(myWM) + + childMargin.BStart(myWM)); + + nsReflowStatus childStatus; + // We initially reflow the child with a dummy containerSize; positioning + // will be fixed later. + const nsSize dummyContainerSize; + ReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + childReflowOuput, myWM, childOffset, dummyContainerSize, 0, + childStatus); + MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(childStatus), + "We gave our child unconstrained available block-size, " + "so it should be complete"); + + nscoord childMarginBoxBSize = + childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM); + + if (contentBoxBSize == NS_INTRINSICSIZE) { + // We are intrinsically sized -- we should shrinkwrap the input area's + // block-size: + contentBoxBSize = childMarginBoxBSize; + + // Make sure we obey min/max-bsize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since ComputedMaxBSize and + // ComputedMinBSize are content heights. + contentBoxBSize = + NS_CSS_MINMAX(contentBoxBSize, + aReflowInput.ComputedMinBSize(), + aReflowInput.ComputedMaxBSize()); + + borderBoxBSize = contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } + + // Center child in block axis + nscoord extraSpace = contentBoxBSize - childMarginBoxBSize; + childOffset.B(myWM) += std::max(0, extraSpace / 2); + + // Needed in FinishReflowChild, for logical-to-physical conversion: + nsSize borderBoxSize = LogicalSize(myWM, borderBoxISize, borderBoxBSize). + GetPhysicalSize(myWM); + + // Place the child + FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + &childReflowOuput, myWM, childOffset, borderBoxSize, 0); + + nsSize contentBoxSize = + LogicalSize(myWM, contentBoxISize, contentBoxBSize). + GetPhysicalSize(myWM); + aDesiredSize.SetBlockStartAscent( + childDesiredSize.BlockStartAscent() + + inputAreaFrame->BStart(aReflowInput.GetWritingMode(), + contentBoxSize)); + } + + LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize); + aDesiredSize.SetSize(myWM, logicalDesiredSize); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (inputAreaFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsDateTimeControlFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +nsresult +nsDateTimeControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + // Set up "datetimebox" XUL element which will be XBL-bound to the + // actual controls. + nsNodeInfoManager* nodeInfoManager = + mContent->GetComposedDoc()->NodeInfoManager(); + RefPtr<NodeInfo> nodeInfo = + nodeInfoManager->GetNodeInfo(nsGkAtoms::datetimebox, nullptr, + kNameSpaceID_XUL, nsIDOMNode::ELEMENT_NODE); + NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY); + + NS_TrustedNewXULElement(getter_AddRefs(mInputAreaContent), nodeInfo.forget()); + aElements.AppendElement(mInputAreaContent); + + // Propogate our tabindex. + nsAutoString tabIndexStr; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, tabIndexStr)) { + mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, + tabIndexStr, false); + } + + // Propagate our readonly state. + nsAutoString readonly; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly)) { + mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly, + false); + } + + SyncDisabledState(); + + return NS_OK; +} + +void +nsDateTimeControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mInputAreaContent) { + aElements.AppendElement(mInputAreaContent); + } +} + +void +nsDateTimeControlFrame::SyncDisabledState() +{ + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, + EmptyString(), true); + } else { + mInputAreaContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +nsresult +nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + NS_ASSERTION(mInputAreaContent, "The input area content must exist!"); + + // nsGkAtoms::disabled is handled by SyncDisabledState + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::value || + aAttribute == nsGkAtoms::readonly || + aAttribute == nsGkAtoms::tabindex) { + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + auto contentAsInputElem = static_cast<dom::HTMLInputElement*>(mContent); + // If script changed the <input>'s type before setting these attributes + // then we don't need to do anything since we are going to be reframed. + if (contentAsInputElem->GetType() == NS_FORM_INPUT_TIME) { + if (aAttribute == nsGkAtoms::value) { + nsCOMPtr<nsIDateTimeInputArea> inputAreaContent = + do_QueryInterface(mInputAreaContent); + if (inputAreaContent) { + nsContentUtils::AddScriptRunner(NewRunnableMethod(inputAreaContent, + &nsIDateTimeInputArea::NotifyInputElementValueChanged)); + } + } else { + if (aModType == nsIDOMMutationEvent::REMOVAL) { + mInputAreaContent->UnsetAttr(aNameSpaceID, aAttribute, true); + } else { + MOZ_ASSERT(aModType == nsIDOMMutationEvent::ADDITION || + aModType == nsIDOMMutationEvent::MODIFICATION); + nsAutoString value; + mContent->GetAttr(aNameSpaceID, aAttribute, value); + mInputAreaContent->SetAttr(aNameSpaceID, aAttribute, value, true); + } + } + } + } + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +void +nsDateTimeControlFrame::ContentStatesChanged(EventStates aStates) +{ + if (aStates.HasState(NS_EVENT_STATE_DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +nsIAtom* +nsDateTimeControlFrame::GetType() const +{ + return nsGkAtoms::dateTimeControlFrame; +} diff --git a/layout/forms/nsDateTimeControlFrame.h b/layout/forms/nsDateTimeControlFrame.h new file mode 100644 index 000000000..5ca599f6f --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.h @@ -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/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + * + * NOTE: some of the above-mentioned input types are still to-be-implemented. + * See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date), + * bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local). + */ + +#ifndef nsDateTimeControlFrame_h__ +#define nsDateTimeControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +namespace dom { +struct DateTimeValue; +} // namespace dom +} // namespace mozilla + +class nsDateTimeControlFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator +{ + typedef mozilla::dom::DateTimeValue DateTimeValue; + + explicit nsDateTimeControlFrame(nsStyleContext* aContext); + +public: + friend nsIFrame* NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, + nsStyleContext* aContext); + + void ContentStatesChanged(mozilla::EventStates aStates) override; + void DestroyFrom(nsIFrame* aDestructRoot) override; + + NS_DECL_QUERYFRAME_TARGET(nsDateTimeControlFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("DateTimeControl"), aResult); + } +#endif + + nsIAtom* GetType() const override; + + bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // Reflow + nscoord GetMinISize(nsRenderingContext* aRenderingContext) override; + + nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override; + + void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowState, + nsReflowStatus& aStatus) override; + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + nsresult AttributeChanged(int32_t aNameSpaceID, nsIAtom* aAttribute, + int32_t aModType) override; + + void UpdateInputBoxValue(); + void SetValueFromPicker(const DateTimeValue& aValue); + void HandleFocusEvent(); + void HandleBlurEvent(); + void SetPickerState(bool aOpen); + +private: + class SyncDisabledStateEvent; + friend class SyncDisabledStateEvent; + class SyncDisabledStateEvent : public mozilla::Runnable + { + public: + explicit SyncDisabledStateEvent(nsDateTimeControlFrame* aFrame) + : mFrame(aFrame) + {} + + NS_IMETHOD Run() override + { + nsDateTimeControlFrame* frame = + static_cast<nsDateTimeControlFrame*>(mFrame.GetFrame()); + NS_ENSURE_STATE(frame); + + frame->SyncDisabledState(); + return NS_OK; + } + + private: + nsWeakFrame mFrame; + }; + + /** + * Sync the disabled state of the anonymous children up with our content's. + */ + void SyncDisabledState(); + + // Anonymous child which is bound via XBL to an element that wraps the input + // area and reset button. + nsCOMPtr<nsIContent> mInputAreaContent; +}; + +#endif // nsDateTimeControlFrame_h__ diff --git a/layout/forms/nsFieldSetFrame.cpp b/layout/forms/nsFieldSetFrame.cpp new file mode 100644 index 000000000..befd41ee2 --- /dev/null +++ b/layout/forms/nsFieldSetFrame.cpp @@ -0,0 +1,703 @@ +/* -*- 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 "nsFieldSetFrame.h" + +#include "mozilla/gfx/2D.h" +#include "nsCSSAnonBoxes.h" +#include "nsLayoutUtils.h" +#include "nsLegendFrame.h" +#include "nsCSSRendering.h" +#include <algorithm> +#include "nsIFrame.h" +#include "nsPresContext.h" +#include "mozilla/RestyleManager.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsDisplayList.h" +#include "nsRenderingContext.h" +#include "nsIScrollableFrame.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::layout; + +nsContainerFrame* +NS_NewFieldSetFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsFieldSetFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFieldSetFrame) + +nsFieldSetFrame::nsFieldSetFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) + , mLegendRect(GetWritingMode()) +{ + mLegendSpace = 0; +} + +nsIAtom* +nsFieldSetFrame::GetType() const +{ + return nsGkAtoms::fieldSetFrame; +} + +nsRect +nsFieldSetFrame::VisualBorderRectRelativeToSelf() const +{ + WritingMode wm = GetWritingMode(); + css::Side legendSide = wm.PhysicalSide(eLogicalSideBStart); + nscoord legendBorder = StyleBorder()->GetComputedBorderWidth(legendSide); + LogicalRect r(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm)); + nsSize containerSize = r.Size(wm).GetPhysicalSize(wm); + if (legendBorder < mLegendRect.BSize(wm)) { + nscoord off = (mLegendRect.BSize(wm) - legendBorder) / 2; + r.BStart(wm) += off; + r.BSize(wm) -= off; + } + return r.GetPhysicalRect(wm, containerSize); +} + +nsIFrame* +nsFieldSetFrame::GetInner() const +{ + nsIFrame* last = mFrames.LastChild(); + if (last && + last->StyleContext()->GetPseudo() == nsCSSAnonBoxes::fieldsetContent) { + return last; + } + MOZ_ASSERT(mFrames.LastChild() == mFrames.FirstChild()); + return nullptr; +} + +nsIFrame* +nsFieldSetFrame::GetLegend() const +{ + if (mFrames.FirstChild() == GetInner()) { + MOZ_ASSERT(mFrames.LastChild() == mFrames.FirstChild()); + return nullptr; + } + MOZ_ASSERT(mFrames.FirstChild() && + mFrames.FirstChild()->GetContentInsertionFrame()->GetType() == + nsGkAtoms::legendFrame); + return mFrames.FirstChild(); +} + +class nsDisplayFieldSetBorderBackground : public nsDisplayItem { +public: + nsDisplayFieldSetBorderBackground(nsDisplayListBuilder* aBuilder, + nsFieldSetFrame* aFrame) + : nsDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayFieldSetBorderBackground); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayFieldSetBorderBackground() { + MOZ_COUNT_DTOR(nsDisplayFieldSetBorderBackground); + } +#endif + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray<nsIFrame*> *aOutFrames) override; + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override; + virtual nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override; + virtual void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) override; + NS_DISPLAY_DECL_NAME("FieldSetBorderBackground", TYPE_FIELDSET_BORDER_BACKGROUND) +}; + +void nsDisplayFieldSetBorderBackground::HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, nsTArray<nsIFrame*> *aOutFrames) +{ + // aPt is guaranteed to be in this item's bounds. We do the hit test based on the + // frame bounds even though our background doesn't cover the whole frame. + // It's not clear whether this is correct. + aOutFrames->AppendElement(mFrame); +} + +void +nsDisplayFieldSetBorderBackground::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) +{ + DrawResult result = static_cast<nsFieldSetFrame*>(mFrame)-> + PaintBorder(aBuilder, *aCtx, ToReferenceFrame(), mVisibleRect); + + nsDisplayItemGenericImageGeometry::UpdateDrawResult(this, result); +} + +nsDisplayItemGeometry* +nsDisplayFieldSetBorderBackground::AllocateGeometry(nsDisplayListBuilder* aBuilder) +{ + return new nsDisplayItemGenericImageGeometry(this, aBuilder); +} + +void +nsDisplayFieldSetBorderBackground::ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) +{ + auto geometry = + static_cast<const nsDisplayItemGenericImageGeometry*>(aGeometry); + + if (aBuilder->ShouldSyncDecodeImages() && + geometry->ShouldInvalidateToSyncDecodeImages()) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, aInvalidRegion); +} + +void +nsFieldSetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) { + // Paint our background and border in a special way. + // REVIEW: We don't really need to check frame emptiness here; if it's empty, + // the background/border display item won't do anything, and if it isn't empty, + // we need to paint the outline + if (!(GetStateBits() & NS_FRAME_IS_OVERFLOW_CONTAINER) && + IsVisibleForPainting(aBuilder)) { + if (StyleEffects()->mBoxShadow) { + aLists.BorderBackground()->AppendNewToTop(new (aBuilder) + nsDisplayBoxShadowOuter(aBuilder, this)); + } + + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, this, VisualBorderRectRelativeToSelf(), + aLists.BorderBackground(), + /* aAllowWillPaintBorderOptimization = */ false); + + aLists.BorderBackground()->AppendNewToTop(new (aBuilder) + nsDisplayFieldSetBorderBackground(aBuilder, this)); + + DisplayOutlineUnconditional(aBuilder, aLists); + + DO_GLOBAL_REFLOW_COUNT_DSP("nsFieldSetFrame"); + } + + if (GetPrevInFlow()) { + DisplayOverflowContainers(aBuilder, aDirtyRect, aLists); + } + + nsDisplayListCollection contentDisplayItems; + if (nsIFrame* inner = GetInner()) { + // Collect the inner frame's display items into their own collection. + // We need to be calling BuildDisplayList on it before the legend in + // case it contains out-of-flow frames whose placeholders are in the + // legend. However, we want the inner frame's display items to be + // after the legend's display items in z-order, so we need to save them + // and append them later. + BuildDisplayListForChild(aBuilder, inner, aDirtyRect, contentDisplayItems); + } + if (nsIFrame* legend = GetLegend()) { + // The legend's background goes on our BlockBorderBackgrounds list because + // it's a block child. + nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds()); + BuildDisplayListForChild(aBuilder, legend, aDirtyRect, set); + } + // Put the inner frame's display items on the master list. Note that this + // moves its border/background display items to our BorderBackground() list, + // which isn't really correct, but it's OK because the inner frame is + // anonymous and can't have its own border and background. + contentDisplayItems.MoveTo(aLists); +} + +DrawResult +nsFieldSetFrame::PaintBorder( + nsDisplayListBuilder* aBuilder, + nsRenderingContext& aRenderingContext, + nsPoint aPt, + const nsRect& aDirtyRect) +{ + // if the border is smaller than the legend. Move the border down + // to be centered on the legend. + // FIXME: This means border-radius clamping is incorrect; we should + // override nsIFrame::GetBorderRadii. + WritingMode wm = GetWritingMode(); + nsRect rect = VisualBorderRectRelativeToSelf(); + nscoord off = wm.IsVertical() ? rect.x : rect.y; + rect += aPt; + nsPresContext* presContext = PresContext(); + + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SYNC_DECODE_IMAGES + : PaintBorderFlags(); + + DrawResult result = DrawResult::SUCCESS; + + nsCSSRendering::PaintBoxShadowInner(presContext, aRenderingContext, + this, rect); + + if (nsIFrame* legend = GetLegend()) { + css::Side legendSide = wm.PhysicalSide(eLogicalSideBStart); + nscoord legendBorderWidth = + StyleBorder()->GetComputedBorderWidth(legendSide); + + // Use the rect of the legend frame, not mLegendRect, so we draw our + // border under the legend's inline-start and -end margins. + LogicalRect legendRect(wm, legend->GetRect() + aPt, rect.Size()); + + // Compute clipRect using logical coordinates, so that the legend space + // will be clipped out of the appropriate physical side depending on mode. + LogicalRect clipRect = LogicalRect(wm, rect, rect.Size()); + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + gfxContext* gfx = aRenderingContext.ThebesContext(); + int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); + + // draw inline-start portion of the block-start side of the border + clipRect.ISize(wm) = legendRect.IStart(wm) - clipRect.IStart(wm); + clipRect.BSize(wm) = legendBorderWidth; + + gfx->Save(); + gfx->Clip(NSRectToSnappedRect(clipRect.GetPhysicalRect(wm, rect.Size()), + appUnitsPerDevPixel, *drawTarget)); + result &= + nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, rect, mStyleContext, borderFlags); + gfx->Restore(); + + // draw inline-end portion of the block-start side of the border + clipRect = LogicalRect(wm, rect, rect.Size()); + clipRect.ISize(wm) = clipRect.IEnd(wm) - legendRect.IEnd(wm); + clipRect.IStart(wm) = legendRect.IEnd(wm); + clipRect.BSize(wm) = legendBorderWidth; + + gfx->Save(); + gfx->Clip(NSRectToSnappedRect(clipRect.GetPhysicalRect(wm, rect.Size()), + appUnitsPerDevPixel, *drawTarget)); + result &= + nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, rect, mStyleContext, borderFlags); + gfx->Restore(); + + // draw remainder of the border (omitting the block-start side) + clipRect = LogicalRect(wm, rect, rect.Size()); + clipRect.BStart(wm) += legendBorderWidth; + clipRect.BSize(wm) = BSize(wm) - (off + legendBorderWidth); + + gfx->Save(); + gfx->Clip(NSRectToSnappedRect(clipRect.GetPhysicalRect(wm, rect.Size()), + appUnitsPerDevPixel, *drawTarget)); + result &= + nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, rect, mStyleContext, borderFlags); + gfx->Restore(); + } else { + result &= + nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, nsRect(aPt, mRect.Size()), + mStyleContext, borderFlags); + } + + return result; +} + +nscoord +nsFieldSetFrame::GetIntrinsicISize(nsRenderingContext* aRenderingContext, + nsLayoutUtils::IntrinsicISizeType aType) +{ + nscoord legendWidth = 0; + nscoord contentWidth = 0; + if (nsIFrame* legend = GetLegend()) { + legendWidth = + nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType); + } + + if (nsIFrame* inner = GetInner()) { + // Ignore padding on the inner, since the padding will be applied to the + // outer instead, and the padding computed for the inner is wrong + // for percentage padding. + contentWidth = + nsLayoutUtils::IntrinsicForContainer(aRenderingContext, inner, aType, + nsLayoutUtils::IGNORE_PADDING); + } + + return std::max(legendWidth, contentWidth); +} + + +nscoord +nsFieldSetFrame::GetMinISize(nsRenderingContext* aRenderingContext) +{ + nscoord result = 0; + DISPLAY_MIN_WIDTH(this, result); + + result = GetIntrinsicISize(aRenderingContext, nsLayoutUtils::MIN_ISIZE); + return result; +} + +nscoord +nsFieldSetFrame::GetPrefISize(nsRenderingContext* aRenderingContext) +{ + nscoord result = 0; + DISPLAY_PREF_WIDTH(this, result); + + result = GetIntrinsicISize(aRenderingContext, nsLayoutUtils::PREF_ISIZE); + return result; +} + +/* virtual */ +LogicalSize +nsFieldSetFrame::ComputeSize(nsRenderingContext *aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + LogicalSize result = + nsContainerFrame::ComputeSize(aRenderingContext, aWM, + aCBSize, aAvailableISize, + aMargin, aBorder, aPadding, aFlags); + + // XXX The code below doesn't make sense if the caller's writing mode + // is orthogonal to this frame's. Not sure yet what should happen then; + // for now, just bail out. + if (aWM.IsVertical() != GetWritingMode().IsVertical()) { + return result; + } + + // Fieldsets never shrink below their min width. + + // If we're a container for font size inflation, then shrink + // wrapping inside of us should not apply font size inflation. + AutoMaybeDisableFontInflation an(this); + + nscoord minISize = GetMinISize(aRenderingContext); + if (minISize > result.ISize(aWM)) { + result.ISize(aWM) = minISize; + } + + return result; +} + +void +nsFieldSetFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsFieldSetFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_PRECONDITION(aReflowInput.ComputedISize() != NS_INTRINSICSIZE, + "Should have a precomputed inline-size!"); + + // Initialize OUT parameter + aStatus = NS_FRAME_COMPLETE; + + nsOverflowAreas ocBounds; + nsReflowStatus ocStatus = NS_FRAME_COMPLETE; + if (GetPrevInFlow()) { + ReflowOverflowContainerChildren(aPresContext, aReflowInput, ocBounds, 0, + ocStatus); + } + + //------------ Handle Incremental Reflow ----------------- + bool reflowInner; + bool reflowLegend; + nsIFrame* legend = GetLegend(); + nsIFrame* inner = GetInner(); + if (aReflowInput.ShouldReflowAllKids()) { + reflowInner = inner != nullptr; + reflowLegend = legend != nullptr; + } else { + reflowInner = inner && NS_SUBTREE_DIRTY(inner); + reflowLegend = legend && NS_SUBTREE_DIRTY(legend); + } + + // We don't allow fieldsets to break vertically. If we did, we'd + // need logic here to push and pull overflow frames. + // Since we're not applying our padding in this frame, we need to add it here + // to compute the available width for our children. + WritingMode wm = GetWritingMode(); + WritingMode innerWM = inner ? inner->GetWritingMode() : wm; + WritingMode legendWM = legend ? legend->GetWritingMode() : wm; + LogicalSize innerAvailSize = aReflowInput.ComputedSizeWithPadding(innerWM); + LogicalSize legendAvailSize = aReflowInput.ComputedSizeWithPadding(legendWM); + innerAvailSize.BSize(innerWM) = legendAvailSize.BSize(legendWM) = + NS_UNCONSTRAINEDSIZE; + NS_ASSERTION(!inner || + nsLayoutUtils::IntrinsicForContainer(aReflowInput.mRenderingContext, + inner, + nsLayoutUtils::MIN_ISIZE) <= + innerAvailSize.ISize(innerWM), + "Bogus availSize.ISize; should be bigger"); + NS_ASSERTION(!legend || + nsLayoutUtils::IntrinsicForContainer(aReflowInput.mRenderingContext, + legend, + nsLayoutUtils::MIN_ISIZE) <= + legendAvailSize.ISize(legendWM), + "Bogus availSize.ISize; should be bigger"); + + // get our border and padding + LogicalMargin border = aReflowInput.ComputedLogicalBorderPadding() - + aReflowInput.ComputedLogicalPadding(); + + // Figure out how big the legend is if there is one. + // get the legend's margin + LogicalMargin legendMargin(wm); + // reflow the legend only if needed + Maybe<ReflowInput> legendReflowInput; + if (legend) { + legendReflowInput.emplace(aPresContext, aReflowInput, legend, + legendAvailSize); + } + if (reflowLegend) { + ReflowOutput legendDesiredSize(aReflowInput); + + // We'll move the legend to its proper place later, so the position + // and containerSize passed here are unimportant. + const nsSize dummyContainerSize; + ReflowChild(legend, aPresContext, legendDesiredSize, *legendReflowInput, + wm, LogicalPoint(wm), dummyContainerSize, + NS_FRAME_NO_MOVE_FRAME, aStatus); +#ifdef NOISY_REFLOW + printf(" returned (%d, %d)\n", + legendDesiredSize.Width(), legendDesiredSize.Height()); +#endif + // figure out the legend's rectangle + legendMargin = legend->GetLogicalUsedMargin(wm); + mLegendRect = + LogicalRect(wm, 0, 0, + legendDesiredSize.ISize(wm) + legendMargin.IStartEnd(wm), + legendDesiredSize.BSize(wm) + legendMargin.BStartEnd(wm)); + nscoord oldSpace = mLegendSpace; + mLegendSpace = 0; + if (mLegendRect.BSize(wm) > border.BStart(wm)) { + // center the border on the legend + mLegendSpace = mLegendRect.BSize(wm) - border.BStart(wm); + } else { + mLegendRect.BStart(wm) = + (border.BStart(wm) - mLegendRect.BSize(wm)) / 2; + } + + // if the legend space changes then we need to reflow the + // content area as well. + if (mLegendSpace != oldSpace && inner) { + reflowInner = true; + } + + FinishReflowChild(legend, aPresContext, legendDesiredSize, + legendReflowInput.ptr(), wm, LogicalPoint(wm), + dummyContainerSize, NS_FRAME_NO_MOVE_FRAME); + } else if (!legend) { + mLegendRect.SetEmpty(); + mLegendSpace = 0; + } else { + // mLegendSpace and mLegendRect haven't changed, but we need + // the used margin when placing the legend. + legendMargin = legend->GetLogicalUsedMargin(wm); + } + + // This containerSize is incomplete as yet: it does not include the size + // of the |inner| frame itself. + nsSize containerSize = (LogicalSize(wm, 0, mLegendSpace) + + border.Size(wm)).GetPhysicalSize(wm); + // reflow the content frame only if needed + if (reflowInner) { + ReflowInput kidReflowInput(aPresContext, aReflowInput, inner, + innerAvailSize, nullptr, + ReflowInput::CALLER_WILL_INIT); + // Override computed padding, in case it's percentage padding + kidReflowInput.Init(aPresContext, nullptr, nullptr, + &aReflowInput.ComputedPhysicalPadding()); + // Our child is "height:100%" but we actually want its height to be reduced + // by the amount of content-height the legend is eating up, unless our + // height is unconstrained (in which case the child's will be too). + if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.SetComputedBSize( + std::max(0, aReflowInput.ComputedBSize() - mLegendSpace)); + } + + if (aReflowInput.ComputedMinBSize() > 0) { + kidReflowInput.ComputedMinBSize() = + std::max(0, aReflowInput.ComputedMinBSize() - mLegendSpace); + } + + if (aReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.ComputedMaxBSize() = + std::max(0, aReflowInput.ComputedMaxBSize() - mLegendSpace); + } + + ReflowOutput kidDesiredSize(kidReflowInput, + aDesiredSize.mFlags); + // Reflow the frame + NS_ASSERTION(kidReflowInput.ComputedPhysicalMargin() == nsMargin(0,0,0,0), + "Margins on anonymous fieldset child not supported!"); + LogicalPoint pt(wm, border.IStart(wm), border.BStart(wm) + mLegendSpace); + + // We don't know the correct containerSize until we have reflowed |inner|, + // so we use a dummy value for now; FinishReflowChild will fix the position + // if necessary. + const nsSize dummyContainerSize; + ReflowChild(inner, aPresContext, kidDesiredSize, kidReflowInput, + wm, pt, dummyContainerSize, 0, aStatus); + + // Update containerSize to account for size of the inner frame, so that + // FinishReflowChild can position it correctly. + containerSize += kidDesiredSize.PhysicalSize(); + FinishReflowChild(inner, aPresContext, kidDesiredSize, + &kidReflowInput, wm, pt, containerSize, 0); + NS_FRAME_TRACE_REFLOW_OUT("FieldSet::Reflow", aStatus); + } else if (inner) { + // |inner| didn't need to be reflowed but we do need to include its size + // in containerSize. + containerSize += inner->GetSize(); + } + + LogicalRect contentRect(wm); + if (inner) { + // We don't support margins on inner, so our content rect is just the + // inner's border-box. (We don't really care about container size at this + // point, as we'll figure out the actual positioning later.) + contentRect = inner->GetLogicalRect(wm, containerSize); + } + + // Our content rect must fill up the available width + LogicalSize availSize = aReflowInput.ComputedSizeWithPadding(wm); + if (availSize.ISize(wm) > contentRect.ISize(wm)) { + contentRect.ISize(wm) = innerAvailSize.ISize(wm); + } + + if (legend) { + // The legend is positioned inline-wards within the inner's content rect + // (so that padding on the fieldset affects the legend position). + LogicalRect innerContentRect = contentRect; + innerContentRect.Deflate(wm, aReflowInput.ComputedLogicalPadding()); + // If the inner content rect is larger than the legend, we can align the + // legend. + if (innerContentRect.ISize(wm) > mLegendRect.ISize(wm)) { + // NOTE legend @align values are: left/right/center/top/bottom. + // GetLogicalAlign converts left/right to start/end for the given WM. + // @see HTMLLegendElement::ParseAttribute, nsLegendFrame::GetLogicalAlign + int32_t align = static_cast<nsLegendFrame*> + (legend->GetContentInsertionFrame())->GetLogicalAlign(wm); + switch (align) { + case NS_STYLE_TEXT_ALIGN_END: + mLegendRect.IStart(wm) = + innerContentRect.IEnd(wm) - mLegendRect.ISize(wm); + break; + case NS_STYLE_TEXT_ALIGN_CENTER: + // Note: rounding removed; there doesn't seem to be any need + mLegendRect.IStart(wm) = innerContentRect.IStart(wm) + + (innerContentRect.ISize(wm) - mLegendRect.ISize(wm)) / 2; + break; + case NS_STYLE_TEXT_ALIGN_START: + case NS_STYLE_VERTICAL_ALIGN_TOP: + case NS_STYLE_VERTICAL_ALIGN_BOTTOM: + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected GetLogicalAlign value"); + } + } else { + // otherwise make place for the legend + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + innerContentRect.ISize(wm) = mLegendRect.ISize(wm); + contentRect.ISize(wm) = mLegendRect.ISize(wm) + + aReflowInput.ComputedLogicalPadding().IStartEnd(wm); + } + + // place the legend + LogicalRect actualLegendRect = mLegendRect; + actualLegendRect.Deflate(wm, legendMargin); + LogicalPoint actualLegendPos(actualLegendRect.Origin(wm)); + + // Note that legend's writing mode may be different from the fieldset's, + // so we need to convert offsets before applying them to it (bug 1134534). + LogicalMargin offsets = + legendReflowInput->ComputedLogicalOffsets(). + ConvertTo(wm, legendReflowInput->GetWritingMode()); + ReflowInput::ApplyRelativePositioning(legend, wm, offsets, + &actualLegendPos, + containerSize); + + legend->SetPosition(wm, actualLegendPos, containerSize); + nsContainerFrame::PositionFrameView(legend); + nsContainerFrame::PositionChildViews(legend); + } + + // Return our size and our result. + LogicalSize finalSize(wm, contentRect.ISize(wm) + border.IStartEnd(wm), + mLegendSpace + border.BStartEnd(wm) + + (inner ? inner->BSize(wm) : 0)); + aDesiredSize.SetSize(wm, finalSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (legend) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, legend); + } + if (inner) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inner); + } + + // Merge overflow container bounds and status. + aDesiredSize.mOverflowAreas.UnionWith(ocBounds); + NS_MergeReflowStatusInto(&aStatus, ocStatus); + + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus); + + InvalidateFrame(); + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +#ifdef DEBUG +void +nsFieldSetFrame::SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) +{ + nsContainerFrame::SetInitialChildList(aListID, aChildList); + MOZ_ASSERT(aListID != kPrincipalList || GetInner(), + "Setting principal child list should populate our inner frame"); +} +void +nsFieldSetFrame::AppendFrames(ChildListID aListID, + nsFrameList& aFrameList) +{ + MOZ_CRASH("nsFieldSetFrame::AppendFrames not supported"); +} + +void +nsFieldSetFrame::InsertFrames(ChildListID aListID, + nsIFrame* aPrevFrame, + nsFrameList& aFrameList) +{ + MOZ_CRASH("nsFieldSetFrame::InsertFrames not supported"); +} + +void +nsFieldSetFrame::RemoveFrame(ChildListID aListID, + nsIFrame* aOldFrame) +{ + MOZ_CRASH("nsFieldSetFrame::RemoveFrame not supported"); +} +#endif + +#ifdef ACCESSIBILITY +a11y::AccType +nsFieldSetFrame::AccessibleType() +{ + return a11y::eHTMLGroupboxType; +} +#endif + +nscoord +nsFieldSetFrame::GetLogicalBaseline(WritingMode aWritingMode) const +{ + nsIFrame* inner = GetInner(); + return inner->BStart(aWritingMode, GetParent()->GetSize()) + + inner->GetLogicalBaseline(aWritingMode); +} diff --git a/layout/forms/nsFieldSetFrame.h b/layout/forms/nsFieldSetFrame.h new file mode 100644 index 000000000..54eaf678f --- /dev/null +++ b/layout/forms/nsFieldSetFrame.h @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +#ifndef nsFieldSetFrame_h___ +#define nsFieldSetFrame_h___ + +#include "mozilla/Attributes.h" +#include "imgIContainer.h" +#include "nsContainerFrame.h" + +class nsFieldSetFrame final : public nsContainerFrame +{ + typedef mozilla::image::DrawResult DrawResult; + +public: + NS_DECL_FRAMEARENA_HELPERS + + explicit nsFieldSetFrame(nsStyleContext* aContext); + + nscoord + GetIntrinsicISize(nsRenderingContext* aRenderingContext, + nsLayoutUtils::IntrinsicISizeType); + virtual nscoord GetMinISize(nsRenderingContext* aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override; + virtual mozilla::LogicalSize + ComputeSize(nsRenderingContext *aRenderingContext, + mozilla::WritingMode aWritingMode, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + virtual nscoord GetLogicalBaseline(mozilla::WritingMode aWritingMode) const override; + + /** + * The area to paint box-shadows around. It's the border rect except + * when there's a <legend> we offset the y-position to the center of it. + */ + virtual nsRect VisualBorderRectRelativeToSelf() const override; + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + DrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + nsRenderingContext& aRenderingContext, + nsPoint aPt, const nsRect& aDirtyRect); + +#ifdef DEBUG + virtual void SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) override; + virtual void AppendFrames(ChildListID aListID, + nsFrameList& aFrameList) override; + virtual void InsertFrames(ChildListID aListID, + nsIFrame* aPrevFrame, + nsFrameList& aFrameList) override; + virtual void RemoveFrame(ChildListID aListID, + nsIFrame* aOldFrame) override; +#endif + + virtual nsIAtom* GetType() const override; + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~nsIFrame::eCanContainOverflowContainers); + } + virtual nsIScrollableFrame* GetScrollTargetFrame() override + { + return do_QueryFrame(GetInner()); + } + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("FieldSet"), aResult); + } +#endif + + /** + * Return the anonymous frame that contains all descendants except + * the legend frame. This is currently always a block frame with + * pseudo nsCSSAnonBoxes::fieldsetContent -- this may change in the + * future when we add support for CSS overflow for <fieldset>. + */ + nsIFrame* GetInner() const; + + /** + * Return the frame that represents the legend if any. This may be + * a nsLegendFrame or a nsHTMLScrollFrame with the nsLegendFrame as the + * scrolled frame (aka content insertion frame). + */ + nsIFrame* GetLegend() const; + +protected: + mozilla::LogicalRect mLegendRect; + nscoord mLegendSpace; +}; + +#endif // nsFieldSetFrame_h___ diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp new file mode 100644 index 000000000..659371615 --- /dev/null +++ b/layout/forms/nsFileControlFrame.cpp @@ -0,0 +1,506 @@ +/* -*- 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 "nsFileControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "nsIDocument.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/Preferences.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "mozilla/EventStates.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileList.h" +#include "nsIDOMDragEvent.h" +#include "nsIDOMFileList.h" +#include "nsContentList.h" +#include "nsIDOMMutationEvent.h" +#include "nsTextNode.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* +NS_NewFileControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsFileControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) + +nsFileControlFrame::nsFileControlFrame(nsStyleContext* aContext) + : nsBlockFrame(aContext) +{ + AddStateBits(NS_BLOCK_FLOAT_MGR); +} + + +void +nsFileControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mMouseListener = new DnDListener(this); +} + +void +nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + ENSURE_TRUE(mContent); + + // Remove the events. + if (mContent) { + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("drop"), + mMouseListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("dragover"), + mMouseListener, false); + } + + nsContentUtils::DestroyAnonymousContent(&mTextContent); + nsContentUtils::DestroyAnonymousContent(&mBrowseFilesOrDirs); + + mMouseListener->ForgetFrame(); + nsBlockFrame::DestroyFrom(aDestructRoot); +} + +static already_AddRefed<Element> +MakeAnonButton(nsIDocument* aDoc, const char* labelKey, + HTMLInputElement* aInputElement, + const nsAString& aAccessKey) +{ + RefPtr<Element> button = aDoc->CreateHTMLElement(nsGkAtoms::button); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + button->SetIsNativeAnonymousRoot(); + button->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("button"), false); + + // Set the file picking button text depending on the current locale. + nsXPIDLString buttonTxt; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + labelKey, buttonTxt); + + // Set the browse button text. It's a bit of a pain to do because we want to + // make sure we are not notifying. + RefPtr<nsTextNode> textContent = + new nsTextNode(button->NodeInfo()->NodeInfoManager()); + + textContent->SetText(buttonTxt, false); + + nsresult rv = button->AppendChildTo(textContent, false); + if (NS_FAILED(rv)) { + return nullptr; + } + + // Make sure access key and tab order for the element actually redirect to the + // file picking button. + RefPtr<HTMLButtonElement> buttonElement = + HTMLButtonElement::FromContentOrNull(button); + + if (!aAccessKey.IsEmpty()) { + buttonElement->SetAccessKey(aAccessKey); + } + + // Both elements are given the same tab index so that the user can tab + // to the file control at the correct index, and then between the two + // buttons. + int32_t tabIndex; + aInputElement->GetTabIndex(&tabIndex); + buttonElement->SetTabIndex(tabIndex); + + return button.forget(); +} + +nsresult +nsFileControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + + RefPtr<HTMLInputElement> fileContent = HTMLInputElement::FromContentOrNull(mContent); + + // The access key is transferred to the "Choose files..." button only. In + // effect that access key allows access to the control via that button, then + // the user can tab between the two buttons. + nsAutoString accessKey; + fileContent->GetAccessKey(accessKey); + + mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent, accessKey); + if (!mBrowseFilesOrDirs || !aElements.AppendElement(mBrowseFilesOrDirs)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Create and setup the text showing the selected files. + RefPtr<NodeInfo> nodeInfo; + nodeInfo = doc->NodeInfoManager()->GetNodeInfo(nsGkAtoms::label, nullptr, + kNameSpaceID_XUL, + nsIDOMNode::ELEMENT_NODE); + NS_TrustedNewXULElement(getter_AddRefs(mTextContent), nodeInfo.forget()); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + mTextContent->SetIsNativeAnonymousRoot(); + mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::crop, + NS_LITERAL_STRING("center"), false); + + // Update the displayed text to reflect the current element's value. + nsAutoString value; + HTMLInputElement::FromContent(mContent)->GetDisplayFileName(value); + UpdateDisplayedValue(value, false); + + if (!aElements.AppendElement(mTextContent)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We should be able to interact with the element by doing drag and drop. + mContent->AddSystemEventListener(NS_LITERAL_STRING("drop"), + mMouseListener, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("dragover"), + mMouseListener, false); + + SyncDisabledState(); + + return NS_OK; +} + +void +nsFileControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mBrowseFilesOrDirs) { + aElements.AppendElement(mBrowseFilesOrDirs); + } + + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +void +nsFileControlFrame::SetFocus(bool aOn, bool aRepaint) +{ +} + +static void +AppendBlobImplAsDirectory(nsTArray<OwningFileOrDirectory>& aArray, + BlobImpl* aBlobImpl, + nsIContent* aContent) +{ + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aBlobImpl->IsDirectory()); + + nsAutoString fullpath; + ErrorResult err; + aBlobImpl->GetMozFullPath(fullpath, err); + if (err.Failed()) { + err.SuppressException(); + return; + } + + nsCOMPtr<nsIFile> file; + NS_ConvertUTF16toUTF8 path(fullpath); + nsresult rv = NS_NewNativeLocalFile(path, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow(); + if (!inner || !inner->IsCurrentInnerWindow()) { + return; + } + + RefPtr<Directory> directory = + Directory::Create(inner, file); + MOZ_ASSERT(directory); + + OwningFileOrDirectory* element = aArray.AppendElement(); + element->SetAsDirectory() = directory; +} + +/** + * This is called when we receive a drop or a dragover. + */ +NS_IMETHODIMP +nsFileControlFrame::DnDListener::HandleEvent(nsIDOMEvent* aEvent) +{ + NS_ASSERTION(mFrame, "We should have been unregistered"); + + bool defaultPrevented = false; + aEvent->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + + nsCOMPtr<nsIDOMDragEvent> dragEvent = do_QueryInterface(aEvent); + if (!dragEvent) { + return NS_OK; + } + + nsCOMPtr<nsIDOMDataTransfer> dataTransfer; + dragEvent->GetDataTransfer(getter_AddRefs(dataTransfer)); + if (!IsValidDropData(dataTransfer)) { + return NS_OK; + } + + + nsCOMPtr<nsIContent> content = mFrame->GetContent(); + bool supportsMultiple = content && content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple); + if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { + dataTransfer->SetDropEffect(NS_LITERAL_STRING("none")); + aEvent->StopPropagation(); + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("dragover")) { + // Prevent default if we can accept this drag data + aEvent->PreventDefault(); + return NS_OK; + } + + if (eventType.EqualsLiteral("drop")) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + + NS_ASSERTION(content, "The frame has no content???"); + + HTMLInputElement* inputElement = HTMLInputElement::FromContent(content); + NS_ASSERTION(inputElement, "No input element for this file upload control frame!"); + + nsCOMPtr<nsIDOMFileList> fileList; + dataTransfer->GetFiles(getter_AddRefs(fileList)); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsTArray<OwningFileOrDirectory> array; + if (webkitDir) { + AppendBlobImplAsDirectory(array, webkitDir, content); + inputElement->MozSetDndFilesAndDirectories(array); + } else { + bool blinkFileSystemEnabled = + Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false); + if (blinkFileSystemEnabled) { + FileList* files = static_cast<FileList*>(fileList.get()); + if (files) { + for (uint32_t i = 0; i < files->Length(); ++i) { + File* file = files->Item(i); + if (file) { + if (file->Impl() && file->Impl()->IsDirectory()) { + AppendBlobImplAsDirectory(array, file->Impl(), content); + } else { + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsFile() = file; + } + } + } + } + } + + // This is rather ugly. Pass the directories as Files using SetFiles, + // but then if blink filesystem API is enabled, it wants + // FileOrDirectory array. + inputElement->SetFiles(fileList, true); + if (blinkFileSystemEnabled) { + inputElement->UpdateEntries(array); + } + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("input"), true, + false); + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("change"), true, + false); + } + } + + return NS_OK; +} + +nsresult +nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory(nsIDOMFileList* aFileList, + BlobImpl** aBlobImpl) +{ + *aBlobImpl = nullptr; + + HTMLInputElement* inputElement = + HTMLInputElement::FromContent(mFrame->GetContent()); + bool webkitDirPicker = + Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) && + inputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory); + if (!webkitDirPicker) { + return NS_OK; + } + + if (!aFileList) { + return NS_ERROR_FAILURE; + } + + FileList* files = static_cast<FileList*>(aFileList); + // webkitdirectory doesn't care about the length of the file list but + // only about the first item on it. + uint32_t len = files->Length(); + if (len) { + File* file = files->Item(0); + if (file) { + BlobImpl* impl = file->Impl(); + if (impl && impl->IsDirectory()) { + RefPtr<BlobImpl> retVal = impl; + retVal.swap(*aBlobImpl); + return NS_OK; + } + } + } + + return NS_ERROR_FAILURE; +} + +bool +nsFileControlFrame::DnDListener::IsValidDropData(nsIDOMDataTransfer* aDOMDataTransfer) +{ + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer); + NS_ENSURE_TRUE(dataTransfer, false); + + // We only support dropping files onto a file upload control + nsTArray<nsString> types; + dataTransfer->GetTypes(types, *nsContentUtils::GetSystemPrincipal()); + + return types.Contains(NS_LITERAL_STRING("Files")); +} + +bool +nsFileControlFrame::DnDListener::CanDropTheseFiles(nsIDOMDataTransfer* aDOMDataTransfer, + bool aSupportsMultiple) +{ + nsCOMPtr<DataTransfer> dataTransfer = do_QueryInterface(aDOMDataTransfer); + NS_ENSURE_TRUE(dataTransfer, false); + + nsCOMPtr<nsIDOMFileList> fileList; + dataTransfer->GetFiles(getter_AddRefs(fileList)); + + RefPtr<BlobImpl> webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + // Just check if either there isn't webkitdirectory attribute, or + // fileList has a directory which can be dropped to the element. + // No need to use webkitDir for anything here. + NS_ENSURE_SUCCESS(rv, false); + + uint32_t listLength = 0; + if (fileList) { + fileList->GetLength(&listLength); + } + return listLength <= 1 || aSupportsMultiple; +} + +nscoord +nsFileControlFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + // Our min width is our pref width + result = GetPrefISize(aRenderingContext); + return result; +} + +void +nsFileControlFrame::SyncDisabledState() +{ + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, + EmptyString(), true); + } else { + mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +nsresult +nsFileControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::tabindex) { + if (aModType == nsIDOMMutationEvent::REMOVAL) { + mBrowseFilesOrDirs->UnsetAttr(aNameSpaceID, aAttribute, true); + } else { + nsAutoString value; + mContent->GetAttr(aNameSpaceID, aAttribute, value); + mBrowseFilesOrDirs->SetAttr(aNameSpaceID, aAttribute, value, true); + } + } + + return nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +void +nsFileControlFrame::ContentStatesChanged(EventStates aStates) +{ + if (aStates.HasState(NS_EVENT_STATE_DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsFileControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("FileControl"), aResult); +} +#endif + +void +nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, bool aNotify) +{ + mTextContent->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, aNotify); +} + +nsresult +nsFileControlFrame::SetFormProperty(nsIAtom* aName, + const nsAString& aValue) +{ + if (nsGkAtoms::value == aName) { + UpdateDisplayedValue(aValue, true); + } + return NS_OK; +} + +void +nsFileControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + BuildDisplayListForInline(aBuilder, aDirtyRect, aLists); +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsFileControlFrame::AccessibleType() +{ + return a11y::eHTMLFileInputType; +} +#endif + +//////////////////////////////////////////////////////////// +// Mouse listener implementation + +NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, + nsIDOMEventListener) diff --git a/layout/forms/nsFileControlFrame.h b/layout/forms/nsFileControlFrame.h new file mode 100644 index 000000000..55c51d426 --- /dev/null +++ b/layout/forms/nsFileControlFrame.h @@ -0,0 +1,167 @@ +/* -*- 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/. */ + +#ifndef nsFileControlFrame_h___ +#define nsFileControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIDOMEventListener.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +class nsIDOMDataTransfer; +class nsIDOMFileList; +namespace mozilla { +namespace dom { +class BlobImpl; +} // namespace dom +} // namespace mozilla + +class nsFileControlFrame : public nsBlockFrame, + public nsIFormControlFrame, + public nsIAnonymousContentCreator +{ +public: + explicit nsFileControlFrame(nsStyleContext* aContext); + + virtual void Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + // nsIFormControlFrame + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + virtual void SetFocus(bool aOn, bool aRepaint) override; + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + virtual void ContentStatesChanged(mozilla::EventStates aStates) override; + virtual bool IsLeaf() const override + { + return true; + } + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + typedef bool (*AcceptAttrCallback)(const nsAString&, void*); + +protected: + + class MouseListener; + friend class MouseListener; + class MouseListener : public nsIDOMEventListener { + public: + NS_DECL_ISUPPORTS + + explicit MouseListener(nsFileControlFrame* aFrame) + : mFrame(aFrame) + {} + + void ForgetFrame() { + mFrame = nullptr; + } + + protected: + virtual ~MouseListener() {} + + nsFileControlFrame* mFrame; + }; + + class SyncDisabledStateEvent; + friend class SyncDisabledStateEvent; + class SyncDisabledStateEvent : public mozilla::Runnable + { + public: + explicit SyncDisabledStateEvent(nsFileControlFrame* aFrame) + : mFrame(aFrame) + {} + + NS_IMETHOD Run() override { + nsFileControlFrame* frame = static_cast<nsFileControlFrame*>(mFrame.GetFrame()); + NS_ENSURE_STATE(frame); + + frame->SyncDisabledState(); + return NS_OK; + } + + private: + nsWeakFrame mFrame; + }; + + class DnDListener: public MouseListener { + public: + explicit DnDListener(nsFileControlFrame* aFrame) + : MouseListener(aFrame) + {} + + NS_DECL_NSIDOMEVENTLISTENER + + nsresult GetBlobImplForWebkitDirectory(nsIDOMFileList* aFileList, + mozilla::dom::BlobImpl** aBlobImpl); + + bool IsValidDropData(nsIDOMDataTransfer* aDOMDataTransfer); + bool CanDropTheseFiles(nsIDOMDataTransfer* aDOMDataTransfer, bool aSupportsMultiple); + }; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsBlockFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * The text box input. + * @see nsFileControlFrame::CreateAnonymousContent + */ + nsCOMPtr<nsIContent> mTextContent; + /** + * The button to open a file or directory picker. + * @see nsFileControlFrame::CreateAnonymousContent + */ + nsCOMPtr<nsIContent> mBrowseFilesOrDirs; + + /** + * Drag and drop mouse listener. + * This makes sure we don't get used after destruction. + */ + RefPtr<DnDListener> mMouseListener; + +protected: + /** + * Sync the disabled state of the content with anonymous children. + */ + void SyncDisabledState(); + + /** + * Updates the displayed value by using aValue. + */ + void UpdateDisplayedValue(const nsAString& aValue, bool aNotify); +}; + +#endif // nsFileControlFrame_h___ diff --git a/layout/forms/nsFormControlFrame.cpp b/layout/forms/nsFormControlFrame.cpp new file mode 100644 index 000000000..4ee62acbf --- /dev/null +++ b/layout/forms/nsFormControlFrame.cpp @@ -0,0 +1,225 @@ +/* -*- 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 "nsFormControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" +#include "nsIDOMHTMLInputElement.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/LookAndFeel.h" +#include "nsDeviceContext.h" +#include "nsIContent.h" + +using namespace mozilla; + +//#define FCF_NOISY + +nsFormControlFrame::nsFormControlFrame(nsStyleContext* aContext) + : nsAtomicContainerFrame(aContext) +{ +} + +nsFormControlFrame::~nsFormControlFrame() +{ +} + +nsIAtom* +nsFormControlFrame::GetType() const +{ + return nsGkAtoms::formControlFrame; +} + +void +nsFormControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + // Unregister the access key registered in reflow + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsAtomicContainerFrame::DestroyFrom(aDestructRoot); +} + +NS_QUERYFRAME_HEAD(nsFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsAtomicContainerFrame) + +/* virtual */ nscoord +nsFormControlFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + result = GetIntrinsicISize(); + return result; +} + +/* virtual */ nscoord +nsFormControlFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_PREF_WIDTH(this, result); + result = GetIntrinsicISize(); + return result; +} + +/* virtual */ +LogicalSize +nsFormControlFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + const WritingMode wm = GetWritingMode(); + LogicalSize result(wm, GetIntrinsicISize(), GetIntrinsicBSize()); + return result.ConvertTo(aWM, wm); +} + +nscoord +nsFormControlFrame::GetIntrinsicISize() +{ + // Provide a reasonable default for sites that use an "auto" height. + // Note that if you change this, you should change the values in forms.css + // as well. This is the 13px default width minus the 2px default border. + return nsPresContext::CSSPixelsToAppUnits(13 - 2 * 2); +} + +nscoord +nsFormControlFrame::GetIntrinsicBSize() +{ + // Provide a reasonable default for sites that use an "auto" height. + // Note that if you change this, you should change the values in forms.css + // as well. This is the 13px default width minus the 2px default border. + return nsPresContext::CSSPixelsToAppUnits(13 - 2 * 2); +} + +nscoord +nsFormControlFrame::GetLogicalBaseline(WritingMode aWritingMode) const +{ + NS_ASSERTION(!NS_SUBTREE_DIRTY(this), + "frame must not be dirty"); + // Treat radio buttons and checkboxes as having an intrinsic baseline + // at the block-end of the control (use the block-end content edge rather + // than the margin edge). + // For "inverted" lines (typically in writing-mode:vertical-lr), use the + // block-start end instead. + return aWritingMode.IsLineInverted() + ? GetLogicalUsedBorderAndPadding(aWritingMode).BStart(aWritingMode) + : BSize(aWritingMode) - + GetLogicalUsedBorderAndPadding(aWritingMode).BEnd(aWritingMode); +} + +void +nsFormControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsFormControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("enter nsFormControlFrame::Reflow: aMaxSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + if (mState & NS_FRAME_FIRST_REFLOW) { + RegUnRegAccessKey(static_cast<nsIFrame*>(this), true); + } + + aStatus = NS_FRAME_COMPLETE; + aDesiredSize.SetSize(aReflowInput.GetWritingMode(), + aReflowInput.ComputedSizeWithBorderPadding()); + + if (nsLayoutUtils::FontSizeInflationEnabled(aPresContext)) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + aDesiredSize.Width() *= inflation; + aDesiredSize.Height() *= inflation; + } + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsFormControlFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + FinishAndStoreOverflow(&aDesiredSize); +} + +nsresult +nsFormControlFrame::RegUnRegAccessKey(nsIFrame * aFrame, bool aDoReg) +{ + NS_ENSURE_ARG_POINTER(aFrame); + + nsPresContext* presContext = aFrame->PresContext(); + + NS_ASSERTION(presContext, "aPresContext is NULL in RegUnRegAccessKey!"); + + nsAutoString accessKey; + + nsIContent* content = aFrame->GetContent(); + content->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, accessKey); + if (!accessKey.IsEmpty()) { + EventStateManager* stateManager = presContext->EventStateManager(); + if (aDoReg) { + stateManager->RegisterAccessKey(content, (uint32_t)accessKey.First()); + } else { + stateManager->UnregisterAccessKey(content, (uint32_t)accessKey.First()); + } + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +void +nsFormControlFrame::SetFocus(bool aOn, bool aRepaint) +{ +} + +nsresult +nsFormControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + // Check for user-input:none style + const nsStyleUserInterface* uiStyle = StyleUserInterface(); + if (uiStyle->mUserInput == StyleUserInput::None || + uiStyle->mUserInput == StyleUserInput::Disabled) { + return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + +void +nsFormControlFrame::GetCurrentCheckState(bool *aState) +{ + nsCOMPtr<nsIDOMHTMLInputElement> inputElement = do_QueryInterface(mContent); + if (inputElement) { + inputElement->GetChecked(aState); + } +} + +nsresult +nsFormControlFrame::SetFormProperty(nsIAtom* aName, const nsAString& aValue) +{ + return NS_OK; +} + +// static +nsRect +nsFormControlFrame::GetUsableScreenRect(nsPresContext* aPresContext) +{ + nsRect screen; + + nsDeviceContext *context = aPresContext->DeviceContext(); + int32_t dropdownCanOverlapOSBar = + LookAndFeel::GetInt(LookAndFeel::eIntID_MenusCanOverlapOSBar, 0); + if ( dropdownCanOverlapOSBar ) + context->GetRect(screen); + else + context->GetClientRect(screen); + + return screen; +} diff --git a/layout/forms/nsFormControlFrame.h b/layout/forms/nsFormControlFrame.h new file mode 100644 index 000000000..fd3e95d93 --- /dev/null +++ b/layout/forms/nsFormControlFrame.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +#ifndef nsFormControlFrame_h___ +#define nsFormControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsIFormControlFrame.h" +#include "nsAtomicContainerFrame.h" +#include "nsDisplayList.h" + +/** + * nsFormControlFrame is the base class for radio buttons and + * checkboxes. It also has two static methods (RegUnRegAccessKey and + * GetScreenHeight) that are used by other form controls. + */ +class nsFormControlFrame : public nsAtomicContainerFrame, + public nsIFormControlFrame +{ +public: + /** + * Main constructor + * @param aContent the content representing this frame + * @param aParentFrame the parent frame + */ + explicit nsFormControlFrame(nsStyleContext*); + + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsAtomicContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + NS_DECL_QUERYFRAME + NS_DECL_ABSTRACT_FRAME(nsFormControlFrame) + + // nsIFrame replacements + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override { + DO_GLOBAL_REFLOW_COUNT_DSP("nsFormControlFrame"); + DisplayBorderBackgroundOutline(aBuilder, aLists); + } + + /** + * Both GetMinISize and GetPrefISize will return whatever GetIntrinsicISize + * returns. + */ + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + /** + * Our auto size is just intrinsic width and intrinsic height. + */ + virtual mozilla::LogicalSize + ComputeAutoSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + + /** + * Respond to a gui event + * @see nsIFrame::HandleEvent + */ + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual nscoord GetLogicalBaseline(mozilla::WritingMode aWritingMode) + const override; + + /** + * Respond to the request to resize and/or reflow + * @see nsIFrame::Reflow + */ + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + // new behavior + + virtual void SetFocus(bool aOn = true, bool aRepaint = false) override; + + // nsIFormControlFrame + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + + // AccessKey Helper function + static nsresult RegUnRegAccessKey(nsIFrame * aFrame, bool aDoReg); + + /** + * Returns the usable screen rect in app units, eg the rect where we can + * draw dropdowns. + */ + static nsRect GetUsableScreenRect(nsPresContext* aPresContext); + +protected: + + virtual ~nsFormControlFrame(); + + nscoord GetIntrinsicISize(); + nscoord GetIntrinsicBSize(); + +// +//------------------------------------------------------------------------------------- +// Utility methods for managing checkboxes and radiobuttons +//------------------------------------------------------------------------------------- +// + /** + * Get the state of the checked attribute. + * @param aState set to true if the checked attribute is set, + * false if the checked attribute has been removed + */ + + void GetCurrentCheckState(bool* aState); +}; + +#endif + diff --git a/layout/forms/nsGfxButtonControlFrame.cpp b/layout/forms/nsGfxButtonControlFrame.cpp new file mode 100644 index 000000000..90da437f7 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.cpp @@ -0,0 +1,237 @@ +/* -*- 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 "nsGfxButtonControlFrame.h" +#include "nsIFormControl.h" +#include "nsGkAtoms.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsContentUtils.h" +// MouseEvent suppression in PP +#include "nsContentList.h" + +#include "nsIDOMHTMLInputElement.h" +#include "nsTextNode.h" + +using namespace mozilla; + +nsGfxButtonControlFrame::nsGfxButtonControlFrame(nsStyleContext* aContext): + nsHTMLButtonControlFrame(aContext) +{ +} + +nsContainerFrame* +NS_NewGfxButtonControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsGfxButtonControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame) + +void nsGfxButtonControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + nsContentUtils::DestroyAnonymousContent(&mTextContent); + nsHTMLButtonControlFrame::DestroyFrom(aDestructRoot); +} + +nsIAtom* +nsGfxButtonControlFrame::GetType() const +{ + return nsGkAtoms::gfxButtonControlFrame; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsGfxButtonControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("ButtonControl"), aResult); +} +#endif + +// Create the text content used as label for the button. +// The frame will be generated by the frame constructor. +nsresult +nsGfxButtonControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsXPIDLString label; + GetLabel(label); + + // Add a child text content node for the label + mTextContent = new nsTextNode(mContent->NodeInfo()->NodeInfoManager()); + + // set the value of the text node and add it to the child list + mTextContent->SetText(label, false); + aElements.AppendElement(mTextContent); + + return NS_OK; +} + +void +nsGfxButtonControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +// Create the text content used as label for the button. +// The frame will be generated by the frame constructor. +nsIFrame* +nsGfxButtonControlFrame::CreateFrameFor(nsIContent* aContent) +{ + nsIFrame * newFrame = nullptr; + + if (aContent == mTextContent) { + nsContainerFrame* parentFrame = do_QueryFrame(mFrames.FirstChild()); + + nsPresContext* presContext = PresContext(); + RefPtr<nsStyleContext> textStyleContext; + textStyleContext = presContext->StyleSet()-> + ResolveStyleForText(mTextContent, mStyleContext); + + newFrame = NS_NewTextFrame(presContext->PresShell(), textStyleContext); + // initialize the text frame + newFrame->Init(mTextContent, parentFrame, nullptr); + mTextContent->SetPrimaryFrame(newFrame); + } + + return newFrame; +} + +NS_QUERYFRAME_HEAD(nsGfxButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + +// Initially we hardcoded the default strings here. +// Next, we used html.css to store the default label for various types +// of buttons. (nsGfxButtonControlFrame::DoNavQuirksReflow rev 1.20) +// However, since html.css is not internationalized, we now grab the default +// label from a string bundle as is done for all other UI strings. +// See bug 16999 for further details. +nsresult +nsGfxButtonControlFrame::GetDefaultLabel(nsXPIDLString& aString) const +{ + nsCOMPtr<nsIFormControl> form = do_QueryInterface(mContent); + NS_ENSURE_TRUE(form, NS_ERROR_UNEXPECTED); + + int32_t type = form->GetType(); + const char *prop; + if (type == NS_FORM_INPUT_RESET) { + prop = "Reset"; + } + else if (type == NS_FORM_INPUT_SUBMIT) { + prop = "Submit"; + } + else { + aString.Truncate(); + return NS_OK; + } + + return nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + prop, aString); +} + +nsresult +nsGfxButtonControlFrame::GetLabel(nsXPIDLString& aLabel) +{ + // Get the text from the "value" property on our content if there is + // one; otherwise set it to a default value (localized). + nsresult rv; + nsCOMPtr<nsIDOMHTMLInputElement> elt = do_QueryInterface(mContent); + if (mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::value) && elt) { + rv = elt->GetValue(aLabel); + } else { + // Generate localized label. + // We can't make any assumption as to what the default would be + // because the value is localized for non-english platforms, thus + // it might not be the string "Reset", "Submit Query", or "Browse..." + rv = GetDefaultLabel(aLabel); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // Compress whitespace out of label if needed. + if (!StyleText()->WhiteSpaceIsSignificant()) { + aLabel.CompressWhitespace(); + } else if (aLabel.Length() > 2 && aLabel.First() == ' ' && + aLabel.CharAt(aLabel.Length() - 1) == ' ') { + // This is a bit of a hack. The reason this is here is as follows: we now + // have default padding on our buttons to make them non-ugly. + // Unfortunately, IE-windows does not have such padding, so people will + // stick values like " ok " (with the spaces) in the buttons in an attempt + // to make them look decent. Unfortunately, if they do this the button + // looks way too big in Mozilla. Worse yet, if they do this _and_ set a + // fixed width for the button we run into trouble because our focus-rect + // border/padding and outer border take up 10px of the horizontal button + // space or so; the result is that the text is misaligned, even with the + // recentering we do in nsHTMLButtonControlFrame::Reflow. So to solve + // this, even if the whitespace is significant, single leading and trailing + // _spaces_ (and not other whitespace) are removed. The proper solution, + // of course, is to not have the focus rect painting taking up 6px of + // horizontal space. We should do that instead (via XBL form controls or + // changing the renderer) and remove this. + aLabel.Cut(0, 1); + aLabel.Truncate(aLabel.Length() - 1); + } + + return NS_OK; +} + +nsresult +nsGfxButtonControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + nsresult rv = NS_OK; + + // If the value attribute is set, update the text of the label + if (nsGkAtoms::value == aAttribute) { + if (mTextContent && mContent) { + nsXPIDLString label; + rv = GetLabel(label); + NS_ENSURE_SUCCESS(rv, rv); + + mTextContent->SetText(label, true); + } else { + rv = NS_ERROR_UNEXPECTED; + } + + // defer to HTMLButtonControlFrame + } else { + rv = nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); + } + return rv; +} + +bool +nsGfxButtonControlFrame::IsLeaf() const +{ + return true; +} + +nsContainerFrame* +nsGfxButtonControlFrame::GetContentInsertionFrame() +{ + return this; +} + +nsresult +nsGfxButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + // Override the HandleEvent to prevent the nsFrame::HandleEvent + // from being called. The nsFrame::HandleEvent causes the button label + // to be selected (Drawn with an XOR rectangle over the label) + + // do we have user-input style? + const nsStyleUserInterface* uiStyle = StyleUserInterface(); + if (uiStyle->mUserInput == StyleUserInput::None || + uiStyle->mUserInput == StyleUserInput::Disabled) { + return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} diff --git a/layout/forms/nsGfxButtonControlFrame.h b/layout/forms/nsGfxButtonControlFrame.h new file mode 100644 index 000000000..d91fe3695 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.h @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +#ifndef nsGfxButtonControlFrame_h___ +#define nsGfxButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsCOMPtr.h" +#include "nsIAnonymousContentCreator.h" + +// Class which implements the input[type=button, reset, submit] and +// browse button for input[type=file]. +// The label for button is specified through generated content +// in the ua.css file. + +class nsGfxButtonControlFrame : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator +{ +public: + NS_DECL_FRAMEARENA_HELPERS + + explicit nsGfxButtonControlFrame(nsStyleContext* aContext); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + NS_DECL_QUERYFRAME + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + virtual nsIFrame* CreateFrameFor(nsIContent* aContent) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + virtual bool IsLeaf() const override; + + virtual nsContainerFrame* GetContentInsertionFrame() override; + +protected: + nsresult GetDefaultLabel(nsXPIDLString& aLabel) const; + + nsresult GetLabel(nsXPIDLString& aLabel); + + virtual bool IsInput() override { return true; } +private: + nsCOMPtr<nsIContent> mTextContent; +}; + + +#endif + diff --git a/layout/forms/nsGfxCheckboxControlFrame.cpp b/layout/forms/nsGfxCheckboxControlFrame.cpp new file mode 100644 index 000000000..061c92349 --- /dev/null +++ b/layout/forms/nsGfxCheckboxControlFrame.cpp @@ -0,0 +1,147 @@ +/* -*- 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 "nsGfxCheckboxControlFrame.h" + +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "nsIContent.h" +#include "nsCOMPtr.h" +#include "nsLayoutUtils.h" +#include "nsRenderingContext.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsDisplayList.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::gfx; + +static void +PaintCheckMark(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const nsRect& aDirtyRect, + nsPoint aPt) +{ + nsRect rect(aPt, aFrame->GetSize()); + rect.Deflate(aFrame->GetUsedBorderAndPadding()); + + // Points come from the coordinates on a 7X7 unit box centered at 0,0 + const int32_t checkPolygonX[] = { -3, -1, 3, 3, -1, -3 }; + const int32_t checkPolygonY[] = { -1, 1, -3, -1, 3, 1 }; + const int32_t checkNumPoints = sizeof(checkPolygonX) / sizeof(int32_t); + const int32_t checkSize = 9; // 2 units of padding on either side + // of the 7x7 unit checkmark + + // Scale the checkmark based on the smallest dimension + nscoord paintScale = std::min(rect.width, rect.height) / checkSize; + nsPoint paintCenter(rect.x + rect.width / 2, + rect.y + rect.height / 2); + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + nsPoint p = paintCenter + nsPoint(checkPolygonX[0] * paintScale, + checkPolygonY[0] * paintScale); + + int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); + builder->MoveTo(NSPointToPoint(p, appUnitsPerDevPixel)); + for (int32_t polyIndex = 1; polyIndex < checkNumPoints; polyIndex++) { + p = paintCenter + nsPoint(checkPolygonX[polyIndex] * paintScale, + checkPolygonY[polyIndex] * paintScale); + builder->LineTo(NSPointToPoint(p, appUnitsPerDevPixel)); + } + RefPtr<Path> path = builder->Finish(); + aDrawTarget->Fill(path, + ColorPattern(ToDeviceColor(aFrame->StyleColor()->mColor))); +} + +static void +PaintIndeterminateMark(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const nsRect& aDirtyRect, + nsPoint aPt) +{ + int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); + + nsRect rect(aPt, aFrame->GetSize()); + rect.Deflate(aFrame->GetUsedBorderAndPadding()); + rect.y += (rect.height - rect.height/4) / 2; + rect.height /= 4; + + Rect devPxRect = NSRectToSnappedRect(rect, appUnitsPerDevPixel, *aDrawTarget); + + aDrawTarget->FillRect( + devPxRect, ColorPattern(ToDeviceColor(aFrame->StyleColor()->mColor))); +} + +//------------------------------------------------------------ +nsIFrame* +NS_NewGfxCheckboxControlFrame(nsIPresShell* aPresShell, + nsStyleContext* aContext) +{ + return new (aPresShell) nsGfxCheckboxControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsGfxCheckboxControlFrame) + + +//------------------------------------------------------------ +// Initialize GFX-rendered state +nsGfxCheckboxControlFrame::nsGfxCheckboxControlFrame(nsStyleContext* aContext) +: nsFormControlFrame(aContext) +{ +} + +nsGfxCheckboxControlFrame::~nsGfxCheckboxControlFrame() +{ +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsGfxCheckboxControlFrame::AccessibleType() +{ + return a11y::eHTMLCheckboxType; +} +#endif + +//------------------------------------------------------------ +void +nsGfxCheckboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + nsFormControlFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists); + + // Get current checked state through content model. + if ((!IsChecked() && !IsIndeterminate()) || !IsVisibleForPainting(aBuilder)) + return; // we're not checked or not visible, nothing to paint. + + if (IsThemed()) + return; // No need to paint the checkmark. The theme will do it. + + aLists.Content()->AppendNewToTop(new (aBuilder) + nsDisplayGeneric(aBuilder, this, + IsIndeterminate() + ? PaintIndeterminateMark : PaintCheckMark, + "CheckedCheckbox", + nsDisplayItem::TYPE_CHECKED_CHECKBOX)); +} + +//------------------------------------------------------------ +bool +nsGfxCheckboxControlFrame::IsChecked() +{ + nsCOMPtr<nsIDOMHTMLInputElement> elem(do_QueryInterface(mContent)); + bool retval = false; + elem->GetChecked(&retval); + return retval; +} + +bool +nsGfxCheckboxControlFrame::IsIndeterminate() +{ + nsCOMPtr<nsIDOMHTMLInputElement> elem(do_QueryInterface(mContent)); + bool retval = false; + elem->GetIndeterminate(&retval); + return retval; +} diff --git a/layout/forms/nsGfxCheckboxControlFrame.h b/layout/forms/nsGfxCheckboxControlFrame.h new file mode 100644 index 000000000..70b8d8d6a --- /dev/null +++ b/layout/forms/nsGfxCheckboxControlFrame.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ +#ifndef nsGfxCheckboxControlFrame_h___ +#define nsGfxCheckboxControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsFormControlFrame.h" + +class nsGfxCheckboxControlFrame : public nsFormControlFrame +{ +public: + NS_DECL_FRAMEARENA_HELPERS + + explicit nsGfxCheckboxControlFrame(nsStyleContext* aContext); + virtual ~nsGfxCheckboxControlFrame(); + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("CheckboxControl"), aResult); + } +#endif + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +protected: + + bool IsChecked(); + bool IsIndeterminate(); +}; + +#endif + diff --git a/layout/forms/nsGfxRadioControlFrame.cpp b/layout/forms/nsGfxRadioControlFrame.cpp new file mode 100644 index 000000000..e4a35a998 --- /dev/null +++ b/layout/forms/nsGfxRadioControlFrame.cpp @@ -0,0 +1,93 @@ +/* -*- 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 "nsGfxRadioControlFrame.h" + +#include "gfx2DGlue.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "nsLayoutUtils.h" +#include "nsRenderingContext.h" +#include "nsDisplayList.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +nsIFrame* +NS_NewGfxRadioControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsGfxRadioControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsGfxRadioControlFrame) + +nsGfxRadioControlFrame::nsGfxRadioControlFrame(nsStyleContext* aContext): + nsFormControlFrame(aContext) +{ +} + +nsGfxRadioControlFrame::~nsGfxRadioControlFrame() +{ +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsGfxRadioControlFrame::AccessibleType() +{ + return a11y::eHTMLRadioButtonType; +} +#endif + +//-------------------------------------------------------------- +// Draw the dot for a non-native radio button in the checked state. +static void +PaintCheckedRadioButton(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const nsRect& aDirtyRect, + nsPoint aPt) +{ + // The dot is an ellipse 2px on all sides smaller than the content-box, + // drawn in the foreground color. + nsRect rect(aPt, aFrame->GetSize()); + rect.Deflate(aFrame->GetUsedBorderAndPadding()); + rect.Deflate(nsPresContext::CSSPixelsToAppUnits(2), + nsPresContext::CSSPixelsToAppUnits(2)); + + Rect devPxRect = + ToRect(nsLayoutUtils::RectToGfxRect(rect, + aFrame->PresContext()->AppUnitsPerDevPixel())); + + ColorPattern color(ToDeviceColor(aFrame->StyleColor()->mColor)); + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size()); + RefPtr<Path> ellipse = builder->Finish(); + aDrawTarget->Fill(ellipse, color); +} + +void +nsGfxRadioControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + nsFormControlFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists); + + if (!IsVisibleForPainting(aBuilder)) + return; + + if (IsThemed()) + return; // The theme will paint the check, if any. + + bool checked = true; + GetCurrentCheckState(&checked); // Get check state from the content model + if (!checked) + return; + + aLists.Content()->AppendNewToTop(new (aBuilder) + nsDisplayGeneric(aBuilder, this, PaintCheckedRadioButton, + "CheckedRadioButton", + nsDisplayItem::TYPE_CHECKED_RADIOBUTTON)); +} diff --git a/layout/forms/nsGfxRadioControlFrame.h b/layout/forms/nsGfxRadioControlFrame.h new file mode 100644 index 000000000..f91e6b94c --- /dev/null +++ b/layout/forms/nsGfxRadioControlFrame.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef nsGfxRadioControlFrame_h___ +#define nsGfxRadioControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsFormControlFrame.h" + + +// nsGfxRadioControlFrame + +class nsGfxRadioControlFrame : public nsFormControlFrame +{ +public: + explicit nsGfxRadioControlFrame(nsStyleContext* aContext); + ~nsGfxRadioControlFrame(); + + NS_DECL_FRAMEARENA_HELPERS + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; +}; + +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.cpp b/layout/forms/nsHTMLButtonControlFrame.cpp new file mode 100644 index 000000000..ef9459ef2 --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.cpp @@ -0,0 +1,465 @@ +/* -*- 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 "nsHTMLButtonControlFrame.h" + +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsButtonFrameRenderer.h" +#include "nsCSSAnonBoxes.h" +#include "nsFormControlFrame.h" +#include "nsNameSpaceManager.h" +#include "nsDisplayList.h" +#include <algorithm> + +using namespace mozilla; + +nsContainerFrame* +NS_NewHTMLButtonControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsHTMLButtonControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame) + +nsHTMLButtonControlFrame::nsHTMLButtonControlFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) +{ +} + +nsHTMLButtonControlFrame::~nsHTMLButtonControlFrame() +{ +} + +void +nsHTMLButtonControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +void +nsHTMLButtonControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + mRenderer.SetFrame(this, PresContext()); +} + +NS_QUERYFRAME_HEAD(nsHTMLButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType +nsHTMLButtonControlFrame::AccessibleType() +{ + return a11y::eHTMLButtonType; +} +#endif + +nsIAtom* +nsHTMLButtonControlFrame::GetType() const +{ + return nsGkAtoms::HTMLButtonControlFrame; +} + +void +nsHTMLButtonControlFrame::SetFocus(bool aOn, bool aRepaint) +{ +} + +nsresult +nsHTMLButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + // if disabled do nothing + if (mRenderer.isDisabled()) { + return NS_OK; + } + + // mouse clicks are handled by content + // we don't want our children to get any events. So just pass it to frame. + return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +bool +nsHTMLButtonControlFrame::ShouldClipPaintingToBorderBox() +{ + return IsInput() || StyleDisplay()->mOverflowX != NS_STYLE_OVERFLOW_VISIBLE; +} + +void +nsHTMLButtonControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + // Clip to our border area for event hit testing. + Maybe<DisplayListClipState::AutoSaveRestore> eventClipState; + const bool isForEventDelivery = aBuilder->IsForEventDelivery(); + if (isForEventDelivery) { + eventClipState.emplace(aBuilder); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + nscoord radii[8]; + bool hasRadii = GetBorderRadii(radii); + eventClipState->ClipContainingBlockDescendants(rect, hasRadii ? radii : nullptr); + } + + nsDisplayList onTop; + if (IsVisibleForPainting(aBuilder)) { + mRenderer.DisplayButton(aBuilder, aLists.BorderBackground(), &onTop); + } + + nsDisplayListCollection set; + + // Do not allow the child subtree to receive events. + if (!isForEventDelivery) { + DisplayListClipState::AutoSaveRestore clipState(aBuilder); + + if (ShouldClipPaintingToBorderBox()) { + nsMargin border = StyleBorder()->GetComputedBorder(); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + rect.Deflate(border); + nscoord radii[8]; + bool hasRadii = GetPaddingBoxBorderRadii(radii); + clipState.ClipContainingBlockDescendants(rect, hasRadii ? radii : nullptr); + } + + BuildDisplayListForChild(aBuilder, mFrames.FirstChild(), aDirtyRect, set, + DISPLAY_CHILD_FORCE_PSEUDO_STACKING_CONTEXT); + // That should put the display items in set.Content() + } + + // Put the foreground outline and focus rects on top of the children + set.Content()->AppendToTop(&onTop); + set.MoveTo(aLists); + + DisplayOutline(aBuilder, aLists); + + // to draw border when selected in editor + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +nscoord +nsHTMLButtonControlFrame::GetMinISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::MIN_ISIZE); + + result += GetWritingMode().IsVertical() + ? mRenderer.GetAddedButtonBorderAndPadding().TopBottom() + : mRenderer.GetAddedButtonBorderAndPadding().LeftRight(); + + return result; +} + +nscoord +nsHTMLButtonControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_PREF_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::PREF_ISIZE); + + result += GetWritingMode().IsVertical() + ? mRenderer.GetAddedButtonBorderAndPadding().TopBottom() + : mRenderer.GetAddedButtonBorderAndPadding().LeftRight(); + + return result; +} + +void +nsHTMLButtonControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsHTMLButtonControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_PRECONDITION(aReflowInput.ComputedISize() != NS_INTRINSICSIZE, + "Should have real computed inline-size by now"); + + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), true); + } + + // Reflow the child + nsIFrame* firstKid = mFrames.FirstChild(); + + MOZ_ASSERT(firstKid, "Button should have a child frame for its contents"); + MOZ_ASSERT(!firstKid->GetNextSibling(), + "Button should have exactly one child frame"); + MOZ_ASSERT(firstKid->StyleContext()->GetPseudo() == + nsCSSAnonBoxes::buttonContent, + "Button's child frame has unexpected pseudo type!"); + + // XXXbz Eventually we may want to check-and-bail if + // !aReflowInput.ShouldReflowAllKids() && + // !NS_SUBTREE_DIRTY(firstKid). + // We'd need to cache our ascent for that, of course. + + // Reflow the contents of the button. + // (This populates our aDesiredSize, too.) + ReflowButtonContents(aPresContext, aDesiredSize, + aReflowInput, firstKid); + + if (!ShouldClipPaintingToBorderBox()) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, firstKid); + } + // else, we ignore child overflow -- anything that overflows beyond our + // own border-box will get clipped when painting. + + aStatus = NS_FRAME_COMPLETE; + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, + aReflowInput, aStatus); + + // We're always complete and we don't support overflow containers + // so we shouldn't have a next-in-flow ever. + aStatus = NS_FRAME_COMPLETE; + MOZ_ASSERT(!GetNextInFlow()); + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +// Helper-function that lets us clone the button's reflow state, but with its +// ComputedWidth and ComputedHeight reduced by the amount of renderer-specific +// focus border and padding that we're using. (This lets us provide a more +// appropriate content-box size for descendents' percent sizes to resolve +// against.) +static ReflowInput +CloneReflowInputWithReducedContentBox( + const ReflowInput& aButtonReflowInput, + const nsMargin& aFocusPadding) +{ + nscoord adjustedWidth = + aButtonReflowInput.ComputedWidth() - aFocusPadding.LeftRight(); + adjustedWidth = std::max(0, adjustedWidth); + + // (Only adjust height if it's an actual length.) + nscoord adjustedHeight = aButtonReflowInput.ComputedHeight(); + if (adjustedHeight != NS_INTRINSICSIZE) { + adjustedHeight -= aFocusPadding.TopBottom(); + adjustedHeight = std::max(0, adjustedHeight); + } + + ReflowInput clone(aButtonReflowInput); + clone.SetComputedWidth(adjustedWidth); + clone.SetComputedHeight(adjustedHeight); + + return clone; +} + +void +nsHTMLButtonControlFrame::ReflowButtonContents(nsPresContext* aPresContext, + ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, + nsIFrame* aFirstKid) +{ + WritingMode wm = GetWritingMode(); + LogicalSize availSize = aButtonReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_INTRINSICSIZE; + + // Buttons have some bonus renderer-determined border/padding, + // which occupies part of the button's content-box area: + LogicalMargin focusPadding = + LogicalMargin(wm, mRenderer.GetAddedButtonBorderAndPadding()); + + // See whether out availSize's inline-size is big enough. If it's + // smaller than our intrinsic min iSize, that means that the kid + // wouldn't really fit. In that case, we overflow into our internal + // focuspadding (which other browsers don't have) so that there's a + // little more space for it. + // Note that GetMinISize includes the focusPadding. + nscoord IOverflow = GetMinISize(aButtonReflowInput.mRenderingContext) - + aButtonReflowInput.ComputedISize(); + nscoord IFocusPadding = focusPadding.IStartEnd(wm); + nscoord focusPaddingReduction = std::min(IFocusPadding, + std::max(IOverflow, 0)); + if (focusPaddingReduction > 0) { + nscoord startReduction = focusPadding.IStart(wm); + if (focusPaddingReduction != IFocusPadding) { + startReduction = NSToCoordRound(startReduction * + (float(focusPaddingReduction) / + float(IFocusPadding))); + } + focusPadding.IStart(wm) -= startReduction; + focusPadding.IEnd(wm) -= focusPaddingReduction - startReduction; + } + + // shorthand for a value we need to use in a bunch of places + const LogicalMargin& clbp = aButtonReflowInput.ComputedLogicalBorderPadding(); + + // Indent the child inside us by the focus border. We must do this separate + // from the regular border. + availSize.ISize(wm) -= focusPadding.IStartEnd(wm); + + LogicalPoint childPos(wm); + childPos.I(wm) = focusPadding.IStart(wm) + clbp.IStart(wm); + availSize.ISize(wm) = std::max(availSize.ISize(wm), 0); + + // Give child a clone of the button's reflow state, with height/width reduced + // by focusPadding, so that descendants with height:100% don't protrude. + ReflowInput adjustedButtonReflowInput = + CloneReflowInputWithReducedContentBox(aButtonReflowInput, + focusPadding.GetPhysicalMargin(wm)); + + ReflowInput contentsReflowInput(aPresContext, + adjustedButtonReflowInput, + aFirstKid, availSize); + + nsReflowStatus contentsReflowStatus; + ReflowOutput contentsDesiredSize(aButtonReflowInput); + childPos.B(wm) = 0; // This will be set properly later, after reflowing the + // child to determine its size. + + // We just pass a dummy containerSize here, as the child will be + // repositioned later by FinishReflowChild. + nsSize dummyContainerSize; + ReflowChild(aFirstKid, aPresContext, + contentsDesiredSize, contentsReflowInput, + wm, childPos, dummyContainerSize, 0, contentsReflowStatus); + MOZ_ASSERT(NS_FRAME_IS_COMPLETE(contentsReflowStatus), + "We gave button-contents frame unconstrained available height, " + "so it should be complete"); + + // Compute the button's content-box size: + LogicalSize buttonContentBox(wm); + if (aButtonReflowInput.ComputedBSize() != NS_INTRINSICSIZE) { + // Button has a fixed block-size -- that's its content-box bSize. + buttonContentBox.BSize(wm) = aButtonReflowInput.ComputedBSize(); + } else { + // Button is intrinsically sized -- it should shrinkwrap the + // button-contents' bSize, plus any focus-padding space: + buttonContentBox.BSize(wm) = + contentsDesiredSize.BSize(wm) + focusPadding.BStartEnd(wm); + + // Make sure we obey min/max-bSize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aButtonReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since mComputedMaxBSize and + // mComputedMinBSize are content bSizes. + buttonContentBox.BSize(wm) = + NS_CSS_MINMAX(buttonContentBox.BSize(wm), + aButtonReflowInput.ComputedMinBSize(), + aButtonReflowInput.ComputedMaxBSize()); + } + if (aButtonReflowInput.ComputedISize() != NS_INTRINSICSIZE) { + buttonContentBox.ISize(wm) = aButtonReflowInput.ComputedISize(); + } else { + buttonContentBox.ISize(wm) = + contentsDesiredSize.ISize(wm) + focusPadding.IStartEnd(wm); + buttonContentBox.ISize(wm) = + NS_CSS_MINMAX(buttonContentBox.ISize(wm), + aButtonReflowInput.ComputedMinISize(), + aButtonReflowInput.ComputedMaxISize()); + } + + // Center child in the block-direction in the button + // (technically, inside of the button's focus-padding area) + nscoord extraSpace = + buttonContentBox.BSize(wm) - focusPadding.BStartEnd(wm) - + contentsDesiredSize.BSize(wm); + + childPos.B(wm) = std::max(0, extraSpace / 2); + + // Adjust childPos.B() to be in terms of the button's frame-rect, instead of + // its focus-padding rect: + childPos.B(wm) += focusPadding.BStart(wm) + clbp.BStart(wm); + + nsSize containerSize = + (buttonContentBox + clbp.Size(wm)).GetPhysicalSize(wm); + + // Place the child + FinishReflowChild(aFirstKid, aPresContext, + contentsDesiredSize, &contentsReflowInput, + wm, childPos, containerSize, 0); + + // Make sure we have a useful 'ascent' value for the child + if (contentsDesiredSize.BlockStartAscent() == + ReflowOutput::ASK_FOR_BASELINE) { + WritingMode wm = aButtonReflowInput.GetWritingMode(); + contentsDesiredSize.SetBlockStartAscent(aFirstKid->GetLogicalBaseline(wm)); + } + + // OK, we're done with the child frame. + // Use what we learned to populate the button frame's reflow metrics. + // * Button's height & width are content-box size + border-box contribution: + aButtonDesiredSize.SetSize(wm, + LogicalSize(wm, aButtonReflowInput.ComputedISize() + clbp.IStartEnd(wm), + buttonContentBox.BSize(wm) + clbp.BStartEnd(wm))); + + // * Button's ascent is its child's ascent, plus the child's block-offset + // within our frame... unless it's orthogonal, in which case we'll use the + // contents inline-size as an approximation for now. + // XXX is there a better strategy? should we include border-padding? + if (aButtonDesiredSize.GetWritingMode().IsOrthogonalTo(wm)) { + aButtonDesiredSize.SetBlockStartAscent(contentsDesiredSize.ISize(wm)); + } else { + aButtonDesiredSize.SetBlockStartAscent(contentsDesiredSize.BlockStartAscent() + + childPos.B(wm)); + } + + aButtonDesiredSize.SetOverflowAreasToDesiredBounds(); +} + +nsresult nsHTMLButtonControlFrame::SetFormProperty(nsIAtom* aName, const nsAString& aValue) +{ + if (nsGkAtoms::value == aName) { + return mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::value, + aValue, true); + } + return NS_OK; +} + +nsStyleContext* +nsHTMLButtonControlFrame::GetAdditionalStyleContext(int32_t aIndex) const +{ + return mRenderer.GetStyleContext(aIndex); +} + +void +nsHTMLButtonControlFrame::SetAdditionalStyleContext(int32_t aIndex, + nsStyleContext* aStyleContext) +{ + mRenderer.SetStyleContext(aIndex, aStyleContext); +} + +#ifdef DEBUG +void +nsHTMLButtonControlFrame::AppendFrames(ChildListID aListID, + nsFrameList& aFrameList) +{ + MOZ_CRASH("unsupported operation"); +} + +void +nsHTMLButtonControlFrame::InsertFrames(ChildListID aListID, + nsIFrame* aPrevFrame, + nsFrameList& aFrameList) +{ + MOZ_CRASH("unsupported operation"); +} + +void +nsHTMLButtonControlFrame::RemoveFrame(ChildListID aListID, + nsIFrame* aOldFrame) +{ + MOZ_CRASH("unsupported operation"); +} +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.h b/layout/forms/nsHTMLButtonControlFrame.h new file mode 100644 index 000000000..96ad0f366 --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.h @@ -0,0 +1,115 @@ +/* -*- 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/. */ + +#ifndef nsHTMLButtonControlFrame_h___ +#define nsHTMLButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsButtonFrameRenderer.h" + +class nsRenderingContext; +class nsPresContext; + +class nsHTMLButtonControlFrame : public nsContainerFrame, + public nsIFormControlFrame +{ +public: + explicit nsHTMLButtonControlFrame(nsStyleContext* aContext); + ~nsHTMLButtonControlFrame(); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual void Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual nsStyleContext* GetAdditionalStyleContext(int32_t aIndex) const override; + virtual void SetAdditionalStyleContext(int32_t aIndex, + nsStyleContext* aStyleContext) override; + +#ifdef DEBUG + virtual void AppendFrames(ChildListID aListID, + nsFrameList& aFrameList) override; + virtual void InsertFrames(ChildListID aListID, + nsIFrame* aPrevFrame, + nsFrameList& aFrameList) override; + virtual void RemoveFrame(ChildListID aListID, + nsIFrame* aOldFrame) override; +#endif + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("HTMLButtonControl"), aResult); + } +#endif + + virtual bool HonorPrintBackgroundSettings() override { return false; } + + // nsIFormControlFrame + void SetFocus(bool aOn, bool aRepaint) override; + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + + // Inserted child content gets its frames parented by our child block + virtual nsContainerFrame* GetContentInsertionFrame() override { + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + +protected: + virtual bool IsInput() { return false; } + + // Indicates whether we should clip our children's painting to our + // border-box (either because of "overflow" or because of legacy reasons + // about how <input>-flavored buttons work). + bool ShouldClipPaintingToBorderBox(); + + // Reflows the button's sole child frame, and computes the desired size + // of the button itself from the results. + void ReflowButtonContents(nsPresContext* aPresContext, + ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, + nsIFrame* aFirstKid); + + nsButtonFrameRenderer mRenderer; +}; + +#endif + + + + diff --git a/layout/forms/nsIComboboxControlFrame.h b/layout/forms/nsIComboboxControlFrame.h new file mode 100644 index 000000000..9ac430ed1 --- /dev/null +++ b/layout/forms/nsIComboboxControlFrame.h @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +#ifndef nsIComboboxControlFrame_h___ +#define nsIComboboxControlFrame_h___ + +#include "nsQueryFrame.h" + +/** + * nsIComboboxControlFrame is the interface for comboboxes. + */ +class nsIComboboxControlFrame : public nsQueryFrame +{ +public: + NS_DECL_QUERYFRAME_TARGET(nsIComboboxControlFrame) + + /** + * Indicates whether the list is dropped down + */ + virtual bool IsDroppedDown() = 0; + + virtual bool IsOpenInParentProcess() = 0; + virtual void SetOpenInParentProcess(bool aVal) = 0; + + bool IsDroppedDownOrHasParentPopup() { return IsDroppedDown() || IsOpenInParentProcess(); } + + /** + * Shows or hides the drop down + */ + virtual void ShowDropDown(bool aDoDropDown) = 0; + + /** + * Gets the Drop Down List + */ + virtual nsIFrame* GetDropDown() = 0; + + /** + * Sets the Drop Down List + */ + virtual void SetDropDown(nsIFrame* aDropDownFrame) = 0; + + /** + * Tells the combobox to roll up + */ + virtual void RollupFromList() = 0; + + /** + * Redisplay the selected text (will do nothing if text has not changed). + * This method might destroy this frame or any others that happen to be + * around. It might even run script. + */ + NS_IMETHOD RedisplaySelectedText() = 0; + + /** + * Method for the listbox to set and get the recent index + */ + virtual int32_t UpdateRecentIndex(int32_t aIndex) = 0; + + /** + * Notification that the content has been reset + */ + virtual void OnContentReset() = 0; + + /** + * This returns the index of the item that is currently being displayed + * in the display area. It may differ from what the currently Selected index + * is in in the dropdown. + * + * Detailed explanation: + * When the dropdown is dropped down via a mouse click and the user moves the mouse + * up and down without clicking, the currently selected item is being tracking inside + * the dropdown, but the combobox is not being updated. When the user selects items + * with the arrow keys, the combobox is being updated. So when the user clicks outside + * the dropdown and it needs to roll up it has to decide whether to keep the current + * selection or not. This method is used to get the current index in the combobox to + * compare it to the current index in the dropdown to see if the combox has been updated + * and that way it knows whether to "cancel" the current selection residing in the + * dropdown. Or whether to leave the selection alone. + */ + virtual int32_t GetIndexOfDisplayArea() = 0; +}; + +#endif + diff --git a/layout/forms/nsIFormControlFrame.h b/layout/forms/nsIFormControlFrame.h new file mode 100644 index 000000000..9c4baa8fc --- /dev/null +++ b/layout/forms/nsIFormControlFrame.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef nsIFormControlFrame_h___ +#define nsIFormControlFrame_h___ + +#include "nsQueryFrame.h" + +class nsAString; +class nsIAtom; + +/** + * nsIFormControlFrame is the common interface for frames of form controls. It + * provides a uniform way of creating widgets, resizing, and painting. + * @see nsLeafFrame and its base classes for more info + */ +class nsIFormControlFrame : public nsQueryFrame +{ +public: + NS_DECL_QUERYFRAME_TARGET(nsIFormControlFrame) + + /** + * + * @param aOn + * @param aRepaint + */ + virtual void SetFocus(bool aOn = true, bool aRepaint = false) = 0; + + /** + * Set a property on the form control frame. + * + * @param aName name of the property to set + * @param aValue value of the property + * @returns NS_OK if the property name is valid, otherwise an error code + */ + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) = 0; +}; + +#endif + diff --git a/layout/forms/nsIListControlFrame.h b/layout/forms/nsIListControlFrame.h new file mode 100644 index 000000000..03c6ce8b3 --- /dev/null +++ b/layout/forms/nsIListControlFrame.h @@ -0,0 +1,98 @@ +/* -*- 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/. */ + +#ifndef nsIListControlFrame_h___ +#define nsIListControlFrame_h___ + +#include "nsQueryFrame.h" + +class nsAString; + +namespace mozilla { +namespace dom { +class HTMLOptionElement; +} // namespace dom +} // namespace mozilla + +/** + * nsIListControlFrame is the interface for frame-based listboxes. + */ +class nsIListControlFrame : public nsQueryFrame +{ +public: + NS_DECL_QUERYFRAME_TARGET(nsIListControlFrame) + + /** + * Sets the ComboBoxFrame + * + */ + virtual void SetComboboxFrame(nsIFrame* aComboboxFrame) = 0; + + /** + * Get the display string for an item + */ + virtual void GetOptionText(uint32_t aIndex, nsAString& aStr) = 0; + + /** + * Get the Selected Item's index + * + */ + virtual int32_t GetSelectedIndex() = 0; + + /** + * Return current option. The current option is the option displaying + * the focus ring when the listbox is focused. + */ + virtual mozilla::dom::HTMLOptionElement* GetCurrentOption() = 0; + + /** + * Initiates mouse capture for the listbox + * + */ + virtual void CaptureMouseEvents(bool aGrabMouseEvents) = 0; + + /** + * Returns the block size of a single row in the list. This is the + * maximum of the block sizes of all the options/optgroups. + */ + virtual nscoord GetBSizeOfARow() = 0; + + /** + * Returns the number of options in the listbox + */ + + virtual uint32_t GetNumberOfOptions() = 0; + + /** + * Called by combobox when it's about to drop down + */ + virtual void AboutToDropDown() = 0; + + /** + * Called by combobox when it's about to roll up + */ + virtual void AboutToRollup() = 0; + + /** + * Fire on input and on change (used by combobox) + */ + virtual void FireOnInputAndOnChange() = 0; + + /** + * Tell the selected list to roll up and ensure that the proper index is + * selected, possibly firing onChange if the index has changed + * + * @param aIndex the index to actually select + */ + virtual void ComboboxFinish(int32_t aIndex) = 0; + + /** + * Notification that the content has been reset + */ + virtual void OnContentReset() = 0; +}; + +#endif + diff --git a/layout/forms/nsISelectControlFrame.h b/layout/forms/nsISelectControlFrame.h new file mode 100644 index 000000000..3e82d7f5b --- /dev/null +++ b/layout/forms/nsISelectControlFrame.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef nsISelectControlFrame_h___ +#define nsISelectControlFrame_h___ + +#include "nsQueryFrame.h" + +/** + * nsISelectControlFrame is the interface for combo boxes and listboxes + */ +class nsISelectControlFrame : public nsQueryFrame +{ +public: + NS_DECL_QUERYFRAME_TARGET(nsISelectControlFrame) + + /** + * Adds an option to the list at index + */ + + NS_IMETHOD AddOption(int32_t index) = 0; + + /** + * Removes the option at index. The caller must have a live script + * blocker while calling this method. + */ + NS_IMETHOD RemoveOption(int32_t index) = 0; + + /** + * Sets whether the parser is done adding children + * @param aIsDone whether the parser is done adding children + */ + NS_IMETHOD DoneAddingChildren(bool aIsDone) = 0; + + /** + * Notify the frame when an option is selected + */ + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) = 0; + + /** + * Notify the frame when selectedIndex was changed. This might + * destroy the frame. + */ + NS_IMETHOD OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) = 0; + +}; + +#endif diff --git a/layout/forms/nsITextControlFrame.h b/layout/forms/nsITextControlFrame.h new file mode 100644 index 000000000..ce46b66ff --- /dev/null +++ b/layout/forms/nsITextControlFrame.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef nsITextControlFrame_h___ +#define nsITextControlFrame_h___ + +#include "nsIFormControlFrame.h" + +class nsIEditor; +class nsISelectionController; +class nsFrameSelection; + +class nsITextControlFrame : public nsIFormControlFrame +{ +public: + NS_DECL_QUERYFRAME_TARGET(nsITextControlFrame) + + enum SelectionDirection { + eNone, + eForward, + eBackward + }; + + NS_IMETHOD GetEditor(nsIEditor **aEditor) = 0; + + NS_IMETHOD SetSelectionStart(int32_t aSelectionStart) = 0; + NS_IMETHOD SetSelectionEnd(int32_t aSelectionEnd) = 0; + + NS_IMETHOD SetSelectionRange(int32_t aSelectionStart, + int32_t aSelectionEnd, + SelectionDirection aDirection = eNone) = 0; + NS_IMETHOD GetSelectionRange(int32_t* aSelectionStart, + int32_t* aSelectionEnd, + SelectionDirection* aDirection = nullptr) = 0; + + NS_IMETHOD GetOwnedSelectionController(nsISelectionController** aSelCon) = 0; + virtual nsFrameSelection* GetOwnedFrameSelection() = 0; + + virtual nsresult GetPhonetic(nsAString& aPhonetic) = 0; + + /** + * Ensure editor is initialized with the proper flags and the default value. + * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created + * @throws various and sundry other things + */ + virtual nsresult EnsureEditorInitialized() = 0; + + virtual nsresult ScrollSelectionIntoView() = 0; +}; + +#endif diff --git a/layout/forms/nsImageControlFrame.cpp b/layout/forms/nsImageControlFrame.cpp new file mode 100644 index 000000000..212fa9356 --- /dev/null +++ b/layout/forms/nsImageControlFrame.cpp @@ -0,0 +1,206 @@ +/* -*- 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 "nsImageFrame.h" +#include "nsIFormControlFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsFormControlFrame.h" +#include "nsLayoutUtils.h" +#include "mozilla/MouseEvents.h" +#include "nsIContent.h" + +using namespace mozilla; + +class nsImageControlFrame : public nsImageFrame, + public nsIFormControlFrame +{ +public: + explicit nsImageControlFrame(nsStyleContext* aContext); + ~nsImageControlFrame(); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + virtual void Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual nsIAtom* GetType() const override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("ImageControl"), aResult); + } +#endif + + virtual nsresult GetCursor(const nsPoint& aPoint, + nsIFrame::Cursor& aCursor) override; + // nsIFormContromFrame + virtual void SetFocus(bool aOn, bool aRepaint) override; + virtual nsresult SetFormProperty(nsIAtom* aName, + const nsAString& aValue) override; +}; + + +nsImageControlFrame::nsImageControlFrame(nsStyleContext* aContext) + : nsImageFrame(aContext) +{ +} + +nsImageControlFrame::~nsImageControlFrame() +{ +} + +void +nsImageControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + if (!GetPrevInFlow()) { + nsFormControlFrame::RegUnRegAccessKey(this, false); + } + nsImageFrame::DestroyFrom(aDestructRoot); +} + +nsIFrame* +NS_NewImageControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsImageControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsImageControlFrame) + +void +nsImageControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + nsImageFrame::Init(aContent, aParent, aPrevInFlow); + + if (aPrevInFlow) { + return; + } + + mContent->SetProperty(nsGkAtoms::imageClickedPoint, + new nsIntPoint(0, 0), + nsINode::DeleteProperty<nsIntPoint>); +} + +NS_QUERYFRAME_HEAD(nsImageControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsImageFrame) + +#ifdef ACCESSIBILITY +a11y::AccType +nsImageControlFrame::AccessibleType() +{ + if (mContent->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input)) { + return a11y::eHTMLButtonType; + } + + return a11y::eNoType; +} +#endif + +nsIAtom* +nsImageControlFrame::GetType() const +{ + return nsGkAtoms::imageControlFrame; +} + +void +nsImageControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + DO_GLOBAL_REFLOW_COUNT("nsImageControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + if (!GetPrevInFlow() && (mState & NS_FRAME_FIRST_REFLOW)) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + return nsImageFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); +} + +nsresult +nsImageControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + NS_ENSURE_ARG_POINTER(aEventStatus); + + // Don't do anything if the event has already been handled by someone + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + // do we have user-input style? + const nsStyleUserInterface* uiStyle = StyleUserInterface(); + if (uiStyle->mUserInput == StyleUserInput::None || + uiStyle->mUserInput == StyleUserInput::Disabled) { + return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + if (mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) { // XXX cache disabled + return NS_OK; + } + + *aEventStatus = nsEventStatus_eIgnore; + + if (aEvent->mMessage == eMouseUp && + aEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton) { + // Store click point for HTMLInputElement::SubmitNamesValues + // Do this on MouseUp because the specs don't say and that's what IE does + nsIntPoint* lastClickPoint = + static_cast<nsIntPoint*> + (mContent->GetProperty(nsGkAtoms::imageClickedPoint)); + if (lastClickPoint) { + // normally lastClickedPoint is not null, as it's allocated in Init() + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, this); + TranslateEventCoords(pt, *lastClickPoint); + } + } + return nsImageFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +void +nsImageControlFrame::SetFocus(bool aOn, bool aRepaint) +{ +} + +nsresult +nsImageControlFrame::GetCursor(const nsPoint& aPoint, + nsIFrame::Cursor& aCursor) +{ + // Use style defined cursor if one is provided, otherwise when + // the cursor style is "auto" we use the pointer cursor. + FillCursorInformationFromStyle(StyleUserInterface(), aCursor); + + if (NS_STYLE_CURSOR_AUTO == aCursor.mCursor) { + aCursor.mCursor = NS_STYLE_CURSOR_POINTER; + } + + return NS_OK; +} + +nsresult +nsImageControlFrame::SetFormProperty(nsIAtom* aName, + const nsAString& aValue) +{ + return NS_OK; +} diff --git a/layout/forms/nsLegendFrame.cpp b/layout/forms/nsLegendFrame.cpp new file mode 100644 index 000000000..60056f15d --- /dev/null +++ b/layout/forms/nsLegendFrame.cpp @@ -0,0 +1,94 @@ +/* -*- 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 "nsLegendFrame.h" +#include "nsIContent.h" +#include "nsGenericHTMLElement.h" +#include "nsAttrValueInlines.h" +#include "nsHTMLParts.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsFormControlFrame.h" + +nsIFrame* +NS_NewLegendFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ +#ifdef DEBUG + const nsStyleDisplay* disp = aContext->StyleDisplay(); + NS_ASSERTION(!disp->IsAbsolutelyPositionedStyle() && !disp->IsFloatingStyle(), + "Legends should not be positioned and should not float"); +#endif + + nsIFrame* f = new (aPresShell) nsLegendFrame(aContext); + if (f) { + f->AddStateBits(NS_BLOCK_FLOAT_MGR | NS_BLOCK_MARGIN_ROOT); + } + return f; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsLegendFrame) + +nsIAtom* +nsLegendFrame::GetType() const +{ + return nsGkAtoms::legendFrame; +} + +void +nsLegendFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsBlockFrame::DestroyFrom(aDestructRoot); +} + +NS_QUERYFRAME_HEAD(nsLegendFrame) + NS_QUERYFRAME_ENTRY(nsLegendFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +void +nsLegendFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + DO_GLOBAL_REFLOW_COUNT("nsLegendFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), true); + } + return nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); +} + +int32_t +nsLegendFrame::GetLogicalAlign(WritingMode aCBWM) +{ + int32_t intValue = NS_STYLE_TEXT_ALIGN_START; + nsGenericHTMLElement* content = nsGenericHTMLElement::FromContent(mContent); + if (content) { + const nsAttrValue* attr = content->GetParsedAttr(nsGkAtoms::align); + if (attr && attr->Type() == nsAttrValue::eEnum) { + intValue = attr->GetEnumValue(); + switch (intValue) { + case NS_STYLE_TEXT_ALIGN_LEFT: + intValue = aCBWM.IsBidiLTR() ? NS_STYLE_TEXT_ALIGN_START + : NS_STYLE_TEXT_ALIGN_END; + break; + case NS_STYLE_TEXT_ALIGN_RIGHT: + intValue = aCBWM.IsBidiLTR() ? NS_STYLE_TEXT_ALIGN_END + : NS_STYLE_TEXT_ALIGN_START; + break; + } + } + } + return intValue; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsLegendFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("Legend"), aResult); +} +#endif diff --git a/layout/forms/nsLegendFrame.h b/layout/forms/nsLegendFrame.h new file mode 100644 index 000000000..5f5e1e03e --- /dev/null +++ b/layout/forms/nsLegendFrame.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef nsLegendFrame_h___ +#define nsLegendFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" + +class nsLegendFrame : public nsBlockFrame { +public: + NS_DECL_QUERYFRAME_TARGET(nsLegendFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + explicit nsLegendFrame(nsStyleContext* aContext) : nsBlockFrame(aContext) {} + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + int32_t GetLogicalAlign(WritingMode aCBWM); +}; + + +#endif // guard diff --git a/layout/forms/nsListControlFrame.cpp b/layout/forms/nsListControlFrame.cpp new file mode 100644 index 000000000..cc5f37f9a --- /dev/null +++ b/layout/forms/nsListControlFrame.cpp @@ -0,0 +1,2537 @@ +/* -*- 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 "nscore.h" +#include "nsCOMPtr.h" +#include "nsUnicharUtils.h" +#include "nsListControlFrame.h" +#include "nsFormControlFrame.h" // for COMPARE macro +#include "nsGkAtoms.h" +#include "nsIDOMHTMLSelectElement.h" +#include "nsIDOMHTMLOptionElement.h" +#include "nsComboboxControlFrame.h" +#include "nsIDOMHTMLOptGroupElement.h" +#include "nsIPresShell.h" +#include "nsIDOMMouseEvent.h" +#include "nsIXULRuntime.h" +#include "nsFontMetrics.h" +#include "nsIScrollableFrame.h" +#include "nsCSSRendering.h" +#include "nsIDOMEventListener.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsContentUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/HTMLOptionsCollection.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/EventStates.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" +#include <algorithm> + +using namespace mozilla; + +// Constants +const uint32_t kMaxDropDownRows = 20; // This matches the setting for 4.x browsers +const int32_t kNothingSelected = -1; + +// Static members +nsListControlFrame * nsListControlFrame::mFocused = nullptr; +nsString * nsListControlFrame::sIncrementalString = nullptr; + +// Using for incremental typing navigation +#define INCREMENTAL_SEARCH_KEYPRESS_TIME 1000 +// XXX, kyle.yuan@sun.com, there are 4 definitions for the same purpose: +// nsMenuPopupFrame.h, nsListControlFrame.cpp, listbox.xml, tree.xml +// need to find a good place to put them together. +// if someone changes one, please also change the other. + +DOMTimeStamp nsListControlFrame::gLastKeyTime = 0; + +/****************************************************************************** + * nsListEventListener + * This class is responsible for propagating events to the nsListControlFrame. + * Frames are not refcounted so they can't be used as event listeners. + *****************************************************************************/ + +class nsListEventListener final : public nsIDOMEventListener +{ +public: + explicit nsListEventListener(nsListControlFrame *aFrame) + : mFrame(aFrame) { } + + void SetFrame(nsListControlFrame *aFrame) { mFrame = aFrame; } + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + +private: + ~nsListEventListener() {} + + nsListControlFrame *mFrame; +}; + +//--------------------------------------------------------- +nsContainerFrame* +NS_NewListControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + nsListControlFrame* it = + new (aPresShell) nsListControlFrame(aContext); + + it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame) + +//--------------------------------------------------------- +nsListControlFrame::nsListControlFrame(nsStyleContext* aContext) + : nsHTMLScrollFrame(aContext, false), + mMightNeedSecondPass(false), + mHasPendingInterruptAtStartOfReflow(false), + mDropdownCanGrow(false), + mForceSelection(false), + mLastDropdownComputedBSize(NS_UNCONSTRAINEDSIZE) +{ + mComboboxFrame = nullptr; + mChangesSinceDragStart = false; + mButtonDown = false; + + mIsAllContentHere = false; + mIsAllFramesHere = false; + mHasBeenInitialized = false; + mNeedToReset = true; + mPostChildrenLoadedReset = false; + + mControlSelectMode = false; +} + +//--------------------------------------------------------- +nsListControlFrame::~nsListControlFrame() +{ + mComboboxFrame = nullptr; +} + +// for Bug 47302 (remove this comment later) +void +nsListControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + // get the receiver interface from the browser button's content node + ENSURE_TRUE(mContent); + + // Clear the frame pointer on our event listener, just in case the + // event listener can outlive the frame. + + mEventListener->SetFrame(nullptr); + + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"), + mEventListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keypress"), + mEventListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"), + mEventListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"), + mEventListener, false); + mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousemove"), + mEventListener, false); + + if (XRE_IsContentProcess() && + Preferences::GetBool("browser.tabs.remote.desktopbehavior", false)) { + nsContentUtils::AddScriptRunner( + new AsyncEventDispatcher(mContent, + NS_LITERAL_STRING("mozhidedropdown"), true, + true)); + } + + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsHTMLScrollFrame::DestroyFrom(aDestructRoot); +} + +void +nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + // We allow visibility:hidden <select>s to contain visible options. + + // Don't allow painting of list controls when painting is suppressed. + // XXX why do we need this here? we should never reach this. Maybe + // because these can have widgets? Hmm + if (aBuilder->IsBackgroundOnly()) + return; + + DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame"); + + if (IsInDropDownMode()) { + NS_ASSERTION(NS_GET_A(mLastDropdownBackstopColor) == 255, + "need an opaque backstop color"); + // XXX Because we have an opaque widget and we get called to paint with + // this frame as the root of a stacking context we need make sure to draw + // some opaque color over the whole widget. (Bug 511323) + aLists.BorderBackground()->AppendNewToBottom( + new (aBuilder) nsDisplaySolidColor(aBuilder, + this, nsRect(aBuilder->ToReferenceFrame(this), GetSize()), + mLastDropdownBackstopColor)); + } + + nsHTMLScrollFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists); +} + +/** + * This is called by the SelectsAreaFrame, which is the same + * as the frame returned by GetOptionsContainer. It's the frame which is + * scrolled by us. + * @param aPt the offset of this frame, relative to the rendering reference + * frame + */ +void nsListControlFrame::PaintFocus(DrawTarget* aDrawTarget, nsPoint aPt) +{ + if (mFocused != this) return; + + nsPresContext* presContext = PresContext(); + + nsIFrame* containerFrame = GetOptionsContainer(); + if (!containerFrame) return; + + nsIFrame* childframe = nullptr; + nsCOMPtr<nsIContent> focusedContent = GetCurrentOption(); + if (focusedContent) { + childframe = focusedContent->GetPrimaryFrame(); + } + + nsRect fRect; + if (childframe) { + // get the child rect + fRect = childframe->GetRect(); + // get it into our coordinates + fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this)); + } else { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + fRect.x = fRect.y = 0; + if (GetWritingMode().IsVertical()) { + fRect.width = GetScrollPortRect().width; + fRect.height = CalcFallbackRowBSize(inflation); + } else { + fRect.width = CalcFallbackRowBSize(inflation); + fRect.height = GetScrollPortRect().height; + } + fRect.MoveBy(containerFrame->GetOffsetTo(this)); + } + fRect += aPt; + + bool lastItemIsSelected = false; + if (focusedContent) { + nsCOMPtr<nsIDOMHTMLOptionElement> domOpt = + do_QueryInterface(focusedContent); + if (domOpt) { + domOpt->GetSelected(&lastItemIsSelected); + } + } + + // set up back stop colors and then ask L&F service for the real colors + nscolor color = + LookAndFeel::GetColor(lastItemIsSelected ? + LookAndFeel::eColorID_WidgetSelectForeground : + LookAndFeel::eColorID_WidgetSelectBackground); + + nsCSSRendering::PaintFocus(presContext, aDrawTarget, fRect, color); +} + +void +nsListControlFrame::InvalidateFocus() +{ + if (mFocused != this) + return; + + nsIFrame* containerFrame = GetOptionsContainer(); + if (containerFrame) { + containerFrame->InvalidateFrame(); + } +} + +NS_QUERYFRAME_HEAD(nsListControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIListControlFrame) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame) + +#ifdef ACCESSIBILITY +a11y::AccType +nsListControlFrame::AccessibleType() +{ + return a11y::eHTMLSelectListType; +} +#endif + +static nscoord +GetMaxOptionBSize(nsIFrame* aContainer, WritingMode aWM) +{ + nscoord result = 0; + for (nsIFrame* option : aContainer->PrincipalChildList()) { + nscoord optionBSize; + if (nsCOMPtr<nsIDOMHTMLOptGroupElement> + (do_QueryInterface(option->GetContent()))) { + // An optgroup; drill through any scroll frame and recurse. |frame| might + // be null here though if |option| is an anonymous leaf frame of some sort. + auto frame = option->GetContentInsertionFrame(); + optionBSize = frame ? GetMaxOptionBSize(frame, aWM) : 0; + } else { + // an option + optionBSize = option->BSize(aWM); + } + if (result < optionBSize) + result = optionBSize; + } + return result; +} + +//----------------------------------------------------------------- +// Main Reflow for ListBox/Dropdown +//----------------------------------------------------------------- + +nscoord +nsListControlFrame::CalcBSizeOfARow() +{ + // Calculate the block size in our writing mode of a single row in the + // listbox or dropdown list by using the tallest thing in the subtree, + // since there may be option groups in addition to option elements, + // either of which may be visible or invisible, may use different + // fonts, etc. + int32_t blockSizeOfARow = GetMaxOptionBSize(GetOptionsContainer(), + GetWritingMode()); + + // Check to see if we have zero items (and optimize by checking + // blockSizeOfARow first) + if (blockSizeOfARow == 0 && GetNumberOfOptions() == 0) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + blockSizeOfARow = CalcFallbackRowBSize(inflation); + } + + return blockSizeOfARow; +} + +nscoord +nsListControlFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_PREF_WIDTH(this, result); + + // Always add scrollbar inline sizes to the pref-inline-size of the + // scrolled content. Combobox frames depend on this happening in the + // dropdown, and standalone listboxes are overflow:scroll so they need + // it too. + WritingMode wm = GetWritingMode(); + result = GetScrolledFrame()->GetPrefISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes(PresContext(), + aRenderingContext)); + result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm)); + return result; +} + +nscoord +nsListControlFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + // Always add scrollbar inline sizes to the min-inline-size of the + // scrolled content. Combobox frames depend on this happening in the + // dropdown, and standalone listboxes are overflow:scroll so they need + // it too. + WritingMode wm = GetWritingMode(); + result = GetScrolledFrame()->GetMinISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes(PresContext(), + aRenderingContext)); + result += scrollbarSize.IStartEnd(wm); + + return result; +} + +void +nsListControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + NS_PRECONDITION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, + "Must have a computed inline size"); + + SchedulePaint(); + + mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt(); + + // If all the content and frames are here + // then initialize it before reflow + if (mIsAllContentHere && !mHasBeenInitialized) { + if (false == mIsAllFramesHere) { + CheckIfAllFramesHere(); + } + if (mIsAllFramesHere && !mHasBeenInitialized) { + mHasBeenInitialized = true; + } + } + + if (GetStateBits() & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + if (IsInDropDownMode()) { + ReflowAsDropdown(aPresContext, aDesiredSize, aReflowInput, aStatus); + return; + } + + MarkInReflow(); + /* + * Due to the fact that our intrinsic block size depends on the block + * sizes of our kids, we end up having to do two-pass reflow, in + * general -- the first pass to find the intrinsic block size and a + * second pass to reflow the scrollframe at that block size (which + * will size the scrollbars correctly, etc). + * + * Naturally, we want to avoid doing the second reflow as much as + * possible. + * We can skip it in the following cases (in all of which the first + * reflow is already happening at the right block size): + * + * - We're reflowing with a constrained computed block size -- just + * use that block size. + * - We're not dirty and have no dirty kids and shouldn't be reflowing + * all kids. In this case, our cached max block size of a child is + * not going to change. + * - We do our first reflow using our cached max block size of a + * child, then compute the new max block size and it's the same as + * the old one. + */ + + bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE); + + mMightNeedSecondPass = autoBSize && + (NS_SUBTREE_DIRTY(this) || aReflowInput.ShouldReflowAllKids()); + + ReflowInput state(aReflowInput); + int32_t length = GetNumberOfRows(); + + nscoord oldBSizeOfARow = BSizeOfARow(); + + if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW) && autoBSize) { + // When not doing an initial reflow, and when the block size is + // auto, start off with our computed block size set to what we'd + // expect our block size to be. + nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length); + computedBSize = state.ApplyMinMaxBSize(computedBSize); + state.SetComputedBSize(computedBSize); + } + + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + + if (!mMightNeedSecondPass) { + NS_ASSERTION(!autoBSize || BSizeOfARow() == oldBSizeOfARow, + "How did our BSize of a row change if nothing was dirty?"); + NS_ASSERTION(!autoBSize || + !(GetStateBits() & NS_FRAME_FIRST_REFLOW), + "How do we not need a second pass during initial reflow at " + "auto BSize?"); + NS_ASSERTION(!IsScrollbarUpdateSuppressed(), + "Shouldn't be suppressing if we don't need a second pass!"); + if (!autoBSize) { + // Update our mNumDisplayRows based on our new row block size now + // that we know it. Note that if autoBSize and we landed in this + // code then we already set mNumDisplayRows in CalcIntrinsicBSize. + // Also note that we can't use BSizeOfARow() here because that + // just uses a cached value that we didn't compute. + nscoord rowBSize = CalcBSizeOfARow(); + if (rowBSize == 0) { + // Just pick something + mNumDisplayRows = 1; + } else { + mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize); + } + } + + return; + } + + mMightNeedSecondPass = false; + + // Now see whether we need a second pass. If we do, our + // nsSelectsAreaFrame will have suppressed the scrollbar update. + if (!IsScrollbarUpdateSuppressed()) { + // All done. No need to do more reflow. + NS_ASSERTION(!IsScrollbarUpdateSuppressed(), + "Shouldn't be suppressing if the block size of a row has not " + "changed!"); + return; + } + + SetSuppressScrollbarUpdate(false); + + // Gotta reflow again. + // XXXbz We're just changing the block size here; do we need to dirty + // ourselves or anything like that? We might need to, per the letter + // of the reflow protocol, but things seem to work fine without it... + // Is that just an implementation detail of nsHTMLScrollFrame that + // we're depending on? + nsHTMLScrollFrame::DidReflow(aPresContext, &state, + nsDidReflowStatus::FINISHED); + + // Now compute the block size we want to have + nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length); + computedBSize = state.ApplyMinMaxBSize(computedBSize); + state.SetComputedBSize(computedBSize); + + // XXXbz to make the ascent really correct, we should add our + // mComputedPadding.top to it (and subtract it from descent). Need that + // because nsGfxScrollFrame just adds in the border.... + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); +} + +void +nsListControlFrame::ReflowAsDropdown(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + NS_PRECONDITION(aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE, + "We should not have a computed block size here!"); + + mMightNeedSecondPass = NS_SUBTREE_DIRTY(this) || + aReflowInput.ShouldReflowAllKids(); + + WritingMode wm = aReflowInput.GetWritingMode(); +#ifdef DEBUG + nscoord oldBSizeOfARow = BSizeOfARow(); + nscoord oldVisibleBSize = (GetStateBits() & NS_FRAME_FIRST_REFLOW) ? + NS_UNCONSTRAINEDSIZE : GetScrolledFrame()->BSize(wm); +#endif + + ReflowInput state(aReflowInput); + + if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW)) { + // When not doing an initial reflow, and when the block size is + // auto, start off with our computed block size set to what we'd + // expect our block size to be. + // Note: At this point, mLastDropdownComputedBSize can be + // NS_UNCONSTRAINEDSIZE in cases when last time we didn't have to + // constrain the block size. That's fine; just do the same thing as + // last time. + state.SetComputedBSize(mLastDropdownComputedBSize); + } + + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + + if (!mMightNeedSecondPass) { + NS_ASSERTION(oldVisibleBSize == GetScrolledFrame()->BSize(wm), + "How did our kid's BSize change if nothing was dirty?"); + NS_ASSERTION(BSizeOfARow() == oldBSizeOfARow, + "How did our BSize of a row change if nothing was dirty?"); + NS_ASSERTION(!IsScrollbarUpdateSuppressed(), + "Shouldn't be suppressing if we don't need a second pass!"); + NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW), + "How can we avoid a second pass during first reflow?"); + return; + } + + mMightNeedSecondPass = false; + + // Now see whether we need a second pass. If we do, our nsSelectsAreaFrame + // will have suppressed the scrollbar update. + if (!IsScrollbarUpdateSuppressed()) { + // All done. No need to do more reflow. + NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW), + "How can we avoid a second pass during first reflow?"); + return; + } + + SetSuppressScrollbarUpdate(false); + + nscoord visibleBSize = GetScrolledFrame()->BSize(wm); + nscoord blockSizeOfARow = BSizeOfARow(); + + // Gotta reflow again. + // XXXbz We're just changing the block size here; do we need to dirty + // ourselves or anything like that? We might need to, per the letter + // of the reflow protocol, but things seem to work fine without it... + // Is that just an implementation detail of nsHTMLScrollFrame that + // we're depending on? + nsHTMLScrollFrame::DidReflow(aPresContext, &state, + nsDidReflowStatus::FINISHED); + + // Now compute the block size we want to have. + // Note: no need to apply min/max constraints, since we have no such + // rules applied to the combobox dropdown. + + mDropdownCanGrow = false; + if (visibleBSize <= 0 || blockSizeOfARow <= 0 || XRE_IsContentProcess()) { + // Looks like we have no options. Just size us to a single row + // block size. + state.SetComputedBSize(blockSizeOfARow); + mNumDisplayRows = 1; + } else { + nsComboboxControlFrame* combobox = + static_cast<nsComboboxControlFrame*>(mComboboxFrame); + LogicalPoint translation(wm); + nscoord before, after; + combobox->GetAvailableDropdownSpace(wm, &before, &after, &translation); + if (before <= 0 && after <= 0) { + state.SetComputedBSize(blockSizeOfARow); + mNumDisplayRows = 1; + mDropdownCanGrow = GetNumberOfRows() > 1; + } else { + nscoord bp = aReflowInput.ComputedLogicalBorderPadding().BStartEnd(wm); + nscoord availableBSize = std::max(before, after) - bp; + nscoord newBSize; + uint32_t rows; + if (visibleBSize <= availableBSize) { + // The dropdown fits in the available block size. + rows = GetNumberOfRows(); + mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows); + if (mNumDisplayRows == rows) { + newBSize = visibleBSize; // use the exact block size + } else { + newBSize = mNumDisplayRows * blockSizeOfARow; // approximate + // The approximation here might actually be too big (bug 1208978); + // don't let it exceed the actual block-size of the list. + newBSize = std::min(newBSize, visibleBSize); + } + } else { + rows = availableBSize / blockSizeOfARow; + mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows); + newBSize = mNumDisplayRows * blockSizeOfARow; // approximate + } + state.SetComputedBSize(newBSize); + mDropdownCanGrow = visibleBSize - newBSize >= blockSizeOfARow && + mNumDisplayRows != kMaxDropDownRows; + } + } + + mLastDropdownComputedBSize = state.ComputedBSize(); + + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); +} + +ScrollbarStyles +nsListControlFrame::GetScrollbarStyles() const +{ + // We can't express this in the style system yet; when we can, this can go away + // and GetScrollbarStyles can be devirtualized + int32_t style = IsInDropDownMode() ? NS_STYLE_OVERFLOW_AUTO + : NS_STYLE_OVERFLOW_SCROLL; + if (GetWritingMode().IsVertical()) { + return ScrollbarStyles(style, NS_STYLE_OVERFLOW_HIDDEN); + } else { + return ScrollbarStyles(NS_STYLE_OVERFLOW_HIDDEN, style); + } +} + +bool +nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const +{ + return !IsInDropDownMode(); +} + +//--------------------------------------------------------- +nsContainerFrame* +nsListControlFrame::GetContentInsertionFrame() { + return GetOptionsContainer()->GetContentInsertionFrame(); +} + +//--------------------------------------------------------- +bool +nsListControlFrame::ExtendedSelection(int32_t aStartIndex, + int32_t aEndIndex, + bool aClearAll) +{ + return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, + true, aClearAll); +} + +//--------------------------------------------------------- +bool +nsListControlFrame::SingleSelection(int32_t aClickedIndex, bool aDoToggle) +{ + if (mComboboxFrame) { + mComboboxFrame->UpdateRecentIndex(GetSelectedIndex()); + } + + bool wasChanged = false; + // Get Current selection + if (aDoToggle) { + wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex); + } else { + wasChanged = SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, + true, true); + } + nsWeakFrame weakFrame(this); + ScrollToIndex(aClickedIndex); + if (!weakFrame.IsAlive()) { + return wasChanged; + } + +#ifdef ACCESSIBILITY + bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex; +#endif + mStartSelectionIndex = aClickedIndex; + mEndSelectionIndex = aClickedIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + if (isCurrentOptionChanged) { + FireMenuItemActiveEvent(); + } +#endif + + return wasChanged; +} + +void +nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) +{ + // + // If nothing is selected, set the start selection depending on where + // the user clicked and what the initial selection is: + // - if the user clicked *before* selectedIndex, set the start index to + // the end of the first contiguous selection. + // - if the user clicked *after* the end of the first contiguous + // selection, set the start index to selectedIndex. + // - if the user clicked *within* the first contiguous selection, set the + // start index to selectedIndex. + // The last two rules, of course, boil down to the same thing: if the user + // clicked >= selectedIndex, return selectedIndex. + // + // This makes it so that shift click works properly when you first click + // in a multiple select. + // + int32_t selectedIndex = GetSelectedIndex(); + if (selectedIndex >= 0) { + // Get the end of the contiguous selection + RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); + NS_ASSERTION(options, "Collection of options is null!"); + uint32_t numOptions = options->Length(); + // Push i to one past the last selected index in the group. + uint32_t i; + for (i = selectedIndex + 1; i < numOptions; i++) { + if (!options->ItemAsOption(i)->Selected()) { + break; + } + } + + if (aClickedIndex < selectedIndex) { + // User clicked before selection, so start selection at end of + // contiguous selection + mStartSelectionIndex = i-1; + mEndSelectionIndex = selectedIndex; + } else { + // User clicked after selection, so start selection at start of + // contiguous selection + mStartSelectionIndex = selectedIndex; + mEndSelectionIndex = i-1; + } + } +} + +static uint32_t +CountOptionsAndOptgroups(nsIFrame* aFrame) +{ + uint32_t count = 0; + nsFrameList::Enumerator e(aFrame->PrincipalChildList()); + for (; !e.AtEnd(); e.Next()) { + nsIFrame* child = e.get(); + nsIContent* content = child->GetContent(); + if (content) { + if (content->IsHTMLElement(nsGkAtoms::option)) { + ++count; + } else { + nsCOMPtr<nsIDOMHTMLOptGroupElement> optgroup = do_QueryInterface(content); + if (optgroup) { + nsAutoString label; + optgroup->GetLabel(label); + if (label.Length() > 0) { + ++count; + } + count += CountOptionsAndOptgroups(child); + } + } + } + } + return count; +} + +uint32_t +nsListControlFrame::GetNumberOfRows() +{ + return ::CountOptionsAndOptgroups(GetContentInsertionFrame()); +} + +//--------------------------------------------------------- +bool +nsListControlFrame::PerformSelection(int32_t aClickedIndex, + bool aIsShift, + bool aIsControl) +{ + bool wasChanged = false; + + if (aClickedIndex == kNothingSelected && !mForceSelection) { + // Ignore kNothingSelected unless the selection is forced + } else if (GetMultiple()) { + if (aIsShift) { + // Make sure shift+click actually does something expected when + // the user has never clicked on the select + if (mStartSelectionIndex == kNothingSelected) { + InitSelectionRange(aClickedIndex); + } + + // Get the range from beginning (low) to end (high) + // Shift *always* works, even if the current option is disabled + int32_t startIndex; + int32_t endIndex; + if (mStartSelectionIndex == kNothingSelected) { + startIndex = aClickedIndex; + endIndex = aClickedIndex; + } else if (mStartSelectionIndex <= aClickedIndex) { + startIndex = mStartSelectionIndex; + endIndex = aClickedIndex; + } else { + startIndex = aClickedIndex; + endIndex = mStartSelectionIndex; + } + + // Clear only if control was not pressed + wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl); + nsWeakFrame weakFrame(this); + ScrollToIndex(aClickedIndex); + if (!weakFrame.IsAlive()) { + return wasChanged; + } + + if (mStartSelectionIndex == kNothingSelected) { + mStartSelectionIndex = aClickedIndex; + } +#ifdef ACCESSIBILITY + bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex; +#endif + mEndSelectionIndex = aClickedIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + if (isCurrentOptionChanged) { + FireMenuItemActiveEvent(); + } +#endif + } else if (aIsControl) { + wasChanged = SingleSelection(aClickedIndex, true); // might destroy us + } else { + wasChanged = SingleSelection(aClickedIndex, false); // might destroy us + } + } else { + wasChanged = SingleSelection(aClickedIndex, false); // might destroy us + } + + return wasChanged; +} + +//--------------------------------------------------------- +bool +nsListControlFrame::HandleListSelection(nsIDOMEvent* aEvent, + int32_t aClickedIndex) +{ + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent); + bool isShift; + bool isControl; +#ifdef XP_MACOSX + mouseEvent->GetMetaKey(&isControl); +#else + mouseEvent->GetCtrlKey(&isControl); +#endif + mouseEvent->GetShiftKey(&isShift); + return PerformSelection(aClickedIndex, isShift, isControl); // might destroy us +} + +//--------------------------------------------------------- +void +nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) +{ + // Currently cocoa widgets use a native popup widget which tracks clicks synchronously, + // so we never want to do mouse capturing. Note that we only bail if the list + // is in drop-down mode, and the caller is requesting capture (we let release capture + // requests go through to ensure that we can release capture requested via other + // code paths, if any exist). + if (aGrabMouseEvents && IsInDropDownMode() && nsComboboxControlFrame::ToolkitHasNativePopup()) + return; + + if (aGrabMouseEvents) { + nsIPresShell::SetCapturingContent(mContent, CAPTURE_IGNOREALLOWED); + } else { + nsIContent* capturingContent = nsIPresShell::GetCapturingContent(); + + bool dropDownIsHidden = false; + if (IsInDropDownMode()) { + dropDownIsHidden = !mComboboxFrame->IsDroppedDown(); + } + if (capturingContent == mContent || dropDownIsHidden) { + // only clear the capturing content if *we* are the ones doing the + // capturing (or if the dropdown is hidden, in which case NO-ONE should + // be capturing anything - it could be a scrollbar inside this listbox + // which is actually grabbing + // This shouldn't be necessary. We should simply ensure that events targeting + // scrollbars are never visible to DOM consumers. + nsIPresShell::SetCapturingContent(nullptr, 0); + } + } +} + +//--------------------------------------------------------- +nsresult +nsListControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) +{ + NS_ENSURE_ARG_POINTER(aEventStatus); + + /*const char * desc[] = {"eMouseMove", + "NS_MOUSE_LEFT_BUTTON_UP", + "NS_MOUSE_LEFT_BUTTON_DOWN", + "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", + "NS_MOUSE_MIDDLE_BUTTON_UP", + "NS_MOUSE_MIDDLE_BUTTON_DOWN", + "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", + "NS_MOUSE_RIGHT_BUTTON_UP", + "NS_MOUSE_RIGHT_BUTTON_DOWN", + "eMouseOver", + "eMouseOut", + "NS_MOUSE_LEFT_DOUBLECLICK", + "NS_MOUSE_MIDDLE_DOUBLECLICK", + "NS_MOUSE_RIGHT_DOUBLECLICK", + "NS_MOUSE_LEFT_CLICK", + "NS_MOUSE_MIDDLE_CLICK", + "NS_MOUSE_RIGHT_CLICK"}; + int inx = aEvent->mMessage - eMouseEventFirst; + if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) { + printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage); + } else { + printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage); + }*/ + + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) + return NS_OK; + + // do we have style that affects how we are selected? + // do we have user-input style? + const nsStyleUserInterface* uiStyle = StyleUserInterface(); + if (uiStyle->mUserInput == StyleUserInput::None || + uiStyle->mUserInput == StyleUserInput::Disabled) { + return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) + return NS_OK; + + return nsHTMLScrollFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + + +//--------------------------------------------------------- +void +nsListControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) +{ + if (aListID == kPrincipalList) { + // First check to see if all the content has been added + mIsAllContentHere = mContent->IsDoneAddingChildren(); + if (!mIsAllContentHere) { + mIsAllFramesHere = false; + mHasBeenInitialized = false; + } + } + nsHTMLScrollFrame::SetInitialChildList(aListID, aChildList); + + // If all the content is here now check + // to see if all the frames have been created + /*if (mIsAllContentHere) { + // If all content and frames are here + // the reset/initialize + if (CheckIfAllFramesHere()) { + ResetList(aPresContext); + mHasBeenInitialized = true; + } + }*/ +} + +//--------------------------------------------------------- +void +nsListControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + nsHTMLScrollFrame::Init(aContent, aParent, aPrevInFlow); + + // we shouldn't have to unregister this listener because when + // our frame goes away all these content node go away as well + // because our frame is the only one who references them. + // we need to hook up our listeners before the editor is initialized + mEventListener = new nsListEventListener(this); + + mContent->AddSystemEventListener(NS_LITERAL_STRING("keydown"), + mEventListener, false, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("keypress"), + mEventListener, false, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("mousedown"), + mEventListener, false, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("mouseup"), + mEventListener, false, false); + mContent->AddSystemEventListener(NS_LITERAL_STRING("mousemove"), + mEventListener, false, false); + + mStartSelectionIndex = kNothingSelected; + mEndSelectionIndex = kNothingSelected; + + mLastDropdownBackstopColor = PresContext()->DefaultBackgroundColor(); + + if (IsInDropDownMode()) { + AddStateBits(NS_FRAME_IN_POPUP); + } +} + +dom::HTMLOptionsCollection* +nsListControlFrame::GetOptions() const +{ + dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromContentOrNull(mContent); + NS_ENSURE_TRUE(select, nullptr); + + return select->Options(); +} + +dom::HTMLOptionElement* +nsListControlFrame::GetOption(uint32_t aIndex) const +{ + dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromContentOrNull(mContent); + NS_ENSURE_TRUE(select, nullptr); + + return select->Item(aIndex); +} + +NS_IMETHODIMP +nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) +{ + if (aSelected) { + ScrollToIndex(aIndex); + } + return NS_OK; +} + +void +nsListControlFrame::OnContentReset() +{ + ResetList(true); +} + +void +nsListControlFrame::ResetList(bool aAllowScrolling) +{ + // if all the frames aren't here + // don't bother reseting + if (!mIsAllFramesHere) { + return; + } + + if (aAllowScrolling) { + mPostChildrenLoadedReset = true; + + // Scroll to the selected index + int32_t indexToSelect = kNothingSelected; + + nsCOMPtr<nsIDOMHTMLSelectElement> selectElement(do_QueryInterface(mContent)); + NS_ASSERTION(selectElement, "No select element!"); + if (selectElement) { + selectElement->GetSelectedIndex(&indexToSelect); + nsWeakFrame weakFrame(this); + ScrollToIndex(indexToSelect); + if (!weakFrame.IsAlive()) { + return; + } + } + } + + mStartSelectionIndex = kNothingSelected; + mEndSelectionIndex = kNothingSelected; + InvalidateFocus(); + // Combobox will redisplay itself with the OnOptionSelected event +} + +void +nsListControlFrame::SetFocus(bool aOn, bool aRepaint) +{ + InvalidateFocus(); + + if (aOn) { + ComboboxFocusSet(); + mFocused = this; + } else { + mFocused = nullptr; + } + + InvalidateFocus(); +} + +void nsListControlFrame::ComboboxFocusSet() +{ + gLastKeyTime = 0; +} + +void +nsListControlFrame::SetComboboxFrame(nsIFrame* aComboboxFrame) +{ + if (nullptr != aComboboxFrame) { + mComboboxFrame = do_QueryFrame(aComboboxFrame); + } +} + +void +nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) +{ + aStr.Truncate(); + if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) { + optionElement->GetText(aStr); + } +} + +int32_t +nsListControlFrame::GetSelectedIndex() +{ + dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromContentOrNull(mContent); + return select->SelectedIndex(); +} + +dom::HTMLOptionElement* +nsListControlFrame::GetCurrentOption() +{ + // The mEndSelectionIndex is what is currently being selected. Use + // the selected index if this is kNothingSelected. + int32_t focusedIndex = (mEndSelectionIndex == kNothingSelected) ? + GetSelectedIndex() : mEndSelectionIndex; + + if (focusedIndex != kNothingSelected) { + return GetOption(AssertedCast<uint32_t>(focusedIndex)); + } + + // There is no selected item. Return the first non-disabled item. + RefPtr<dom::HTMLSelectElement> selectElement = + dom::HTMLSelectElement::FromContent(mContent); + + for (uint32_t i = 0, length = selectElement->Length(); i < length; ++i) { + dom::HTMLOptionElement* node = selectElement->Item(i); + if (!node) { + return nullptr; + } + + if (!selectElement->IsOptionDisabled(node)) { + return node; + } + } + + return nullptr; +} + +bool +nsListControlFrame::IsInDropDownMode() const +{ + return (mComboboxFrame != nullptr); +} + +uint32_t +nsListControlFrame::GetNumberOfOptions() +{ + dom::HTMLOptionsCollection* options = GetOptions(); + if (!options) { + return 0; + } + + return options->Length(); +} + +//---------------------------------------------------------------------- +// nsISelectControlFrame +//---------------------------------------------------------------------- +bool nsListControlFrame::CheckIfAllFramesHere() +{ + // Get the number of optgroups and options + //int32_t numContentItems = 0; + nsCOMPtr<nsIDOMNode> node(do_QueryInterface(mContent)); + if (node) { + // XXX Need to find a fail proff way to determine that + // all the frames are there + mIsAllFramesHere = true;//NS_OK == CountAllChild(node, numContentItems); + } + // now make sure we have a frame each piece of content + + return mIsAllFramesHere; +} + +NS_IMETHODIMP +nsListControlFrame::DoneAddingChildren(bool aIsDone) +{ + mIsAllContentHere = aIsDone; + if (mIsAllContentHere) { + // Here we check to see if all the frames have been created + // for all the content. + // If so, then we can initialize; + if (!mIsAllFramesHere) { + // if all the frames are now present we can initialize + if (CheckIfAllFramesHere()) { + mHasBeenInitialized = true; + ResetList(true); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsListControlFrame::AddOption(int32_t aIndex) +{ +#ifdef DO_REFLOW_DEBUG + printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex); +#endif + + if (!mIsAllContentHere) { + mIsAllContentHere = mContent->IsDoneAddingChildren(); + if (!mIsAllContentHere) { + mIsAllFramesHere = false; + mHasBeenInitialized = false; + } else { + mIsAllFramesHere = (aIndex == static_cast<int32_t>(GetNumberOfOptions()-1)); + } + } + + // Make sure we scroll to the selected option as needed + mNeedToReset = true; + + if (!mHasBeenInitialized) { + return NS_OK; + } + + mPostChildrenLoadedReset = mIsAllContentHere; + return NS_OK; +} + +static int32_t +DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) +{ + return aLength == 0 ? kNothingSelected : std::max(0, aSelectionIndex - 1); +} + +NS_IMETHODIMP +nsListControlFrame::RemoveOption(int32_t aIndex) +{ + NS_PRECONDITION(aIndex >= 0, "negative <option> index"); + + // Need to reset if we're a dropdown + if (IsInDropDownMode()) { + mNeedToReset = true; + mPostChildrenLoadedReset = mIsAllContentHere; + } + + if (mStartSelectionIndex != kNothingSelected) { + NS_ASSERTION(mEndSelectionIndex != kNothingSelected, ""); + int32_t numOptions = GetNumberOfOptions(); + // NOTE: numOptions is the new number of options whereas aIndex is the + // unadjusted index of the removed option (hence the <= below). + NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index"); + + int32_t forward = mEndSelectionIndex - mStartSelectionIndex; + int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex; + int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex; + if (aIndex < *low) + *low = ::DecrementAndClamp(*low, numOptions); + if (aIndex <= *high) + *high = ::DecrementAndClamp(*high, numOptions); + if (forward == 0) + *low = *high; + } + else + NS_ASSERTION(mEndSelectionIndex == kNothingSelected, ""); + + InvalidateFocus(); + return NS_OK; +} + +//--------------------------------------------------------- +// Set the option selected in the DOM. This method is named +// as it is because it indicates that the frame is the source +// of this event rather than the receiver. +bool +nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex, + int32_t aEndIndex, + bool aValue, + bool aClearAll) +{ + RefPtr<dom::HTMLSelectElement> selectElement = + dom::HTMLSelectElement::FromContent(mContent); + + uint32_t mask = dom::HTMLSelectElement::NOTIFY; + if (mForceSelection) { + mask |= dom::HTMLSelectElement::SET_DISABLED; + } + if (aValue) { + mask |= dom::HTMLSelectElement::IS_SELECTED; + } + if (aClearAll) { + mask |= dom::HTMLSelectElement::CLEAR_ALL; + } + + return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask); +} + +bool +nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) +{ + RefPtr<dom::HTMLOptionElement> option = + GetOption(static_cast<uint32_t>(aIndex)); + NS_ENSURE_TRUE(option, false); + + RefPtr<dom::HTMLSelectElement> selectElement = + dom::HTMLSelectElement::FromContent(mContent); + + uint32_t mask = dom::HTMLSelectElement::NOTIFY; + if (!option->Selected()) { + mask |= dom::HTMLSelectElement::IS_SELECTED; + } + + return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask); +} + + +// Dispatch event and such +bool +nsListControlFrame::UpdateSelection() +{ + if (mIsAllFramesHere) { + // if it's a combobox, display the new text + nsWeakFrame weakFrame(this); + if (mComboboxFrame) { + mComboboxFrame->RedisplaySelectedText(); + + // When dropdown list is open, onchange event will be fired when Enter key + // is hit or when dropdown list is dismissed. + if (mComboboxFrame->IsDroppedDown()) { + return weakFrame.IsAlive(); + } + } + if (mIsAllContentHere) { + FireOnInputAndOnChange(); + } + return weakFrame.IsAlive(); + } + return true; +} + +void +nsListControlFrame::ComboboxFinish(int32_t aIndex) +{ + gLastKeyTime = 0; + + if (mComboboxFrame) { + int32_t displayIndex = mComboboxFrame->GetIndexOfDisplayArea(); + // Make sure we can always reset to the displayed index + mForceSelection = displayIndex == aIndex; + + nsWeakFrame weakFrame(this); + PerformSelection(aIndex, false, false); // might destroy us + if (!weakFrame.IsAlive() || !mComboboxFrame) { + return; + } + + if (displayIndex != aIndex) { + mComboboxFrame->RedisplaySelectedText(); // might destroy us + } + + if (weakFrame.IsAlive() && mComboboxFrame) { + mComboboxFrame->RollupFromList(); // might destroy us + } + } +} + +// Send out an onInput and onChange notification. +void +nsListControlFrame::FireOnInputAndOnChange() +{ + if (mComboboxFrame) { + // Return hit without changing anything + int32_t index = mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX); + if (index == NS_SKIP_NOTIFY_INDEX) { + return; + } + + // See if the selection actually changed + if (index == GetSelectedIndex()) { + return; + } + } + + nsCOMPtr<nsIContent> content = mContent; + // Dispatch the input event. + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("input"), true, + false); + // Dispatch the change event. + nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content, + NS_LITERAL_STRING("change"), true, + false); +} + +NS_IMETHODIMP +nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) +{ + if (mComboboxFrame) { + // UpdateRecentIndex with NS_SKIP_NOTIFY_INDEX, so that we won't fire an onchange + // event for this setting of selectedIndex. + mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX); + } + + nsWeakFrame weakFrame(this); + ScrollToIndex(aNewIndex); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + mStartSelectionIndex = aNewIndex; + mEndSelectionIndex = aNewIndex; + InvalidateFocus(); + +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(); +#endif + + return NS_OK; +} + +//---------------------------------------------------------------------- +// End nsISelectControlFrame +//---------------------------------------------------------------------- + +nsresult +nsListControlFrame::SetFormProperty(nsIAtom* aName, + const nsAString& aValue) +{ + if (nsGkAtoms::selected == aName) { + return NS_ERROR_INVALID_ARG; // Selected is readonly according to spec. + } else if (nsGkAtoms::selectedindex == aName) { + // You shouldn't be calling me for this!!! + return NS_ERROR_INVALID_ARG; + } + + // We should be told about selectedIndex by the DOM element through + // OnOptionSelected + + return NS_OK; +} + +void +nsListControlFrame::AboutToDropDown() +{ + NS_ASSERTION(IsInDropDownMode(), + "AboutToDropDown called without being in dropdown mode"); + + // Our widget doesn't get invalidated on changes to the rest of the document, + // so compute and store this color at the start of a dropdown so we don't + // get weird painting behaviour. + // We start looking for backgrounds above the combobox frame to avoid + // duplicating the combobox frame's background and compose each background + // color we find underneath until we have an opaque color, or run out of + // backgrounds. We compose with the PresContext default background color, + // which is always opaque, in case we don't end up with an opaque color. + // This gives us a very poor approximation of translucency. + nsIFrame* comboboxFrame = do_QueryFrame(mComboboxFrame); + nsStyleContext* context = comboboxFrame->StyleContext()->GetParent(); + mLastDropdownBackstopColor = NS_RGBA(0,0,0,0); + while (NS_GET_A(mLastDropdownBackstopColor) < 255 && context) { + mLastDropdownBackstopColor = + NS_ComposeColors(context->StyleBackground()->mBackgroundColor, + mLastDropdownBackstopColor); + context = context->GetParent(); + } + mLastDropdownBackstopColor = + NS_ComposeColors(PresContext()->DefaultBackgroundColor(), + mLastDropdownBackstopColor); + + if (mIsAllContentHere && mIsAllFramesHere && mHasBeenInitialized) { + nsWeakFrame weakFrame(this); + ScrollToIndex(GetSelectedIndex()); + if (!weakFrame.IsAlive()) { + return; + } +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(); // Inform assistive tech what got focus +#endif + } + mItemSelectionStarted = false; + mForceSelection = false; +} + +// We are about to be rolledup from the outside (ComboboxFrame) +void +nsListControlFrame::AboutToRollup() +{ + // We've been updating the combobox with the keyboard up until now, but not + // with the mouse. The problem is, even with mouse selection, we are + // updating the <select>. So if the mouse goes over an option just before + // he leaves the box and clicks, that's what the <select> will show. + // + // To deal with this we say "whatever is in the combobox is canonical." + // - IF the combobox is different from the current selected index, we + // reset the index. + + if (IsInDropDownMode()) { + ComboboxFinish(mComboboxFrame->GetIndexOfDisplayArea()); // might destroy us + } +} + +void +nsListControlFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput, + nsDidReflowStatus aStatus) +{ + bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow && + aPresContext->HasPendingInterrupt(); + + nsHTMLScrollFrame::DidReflow(aPresContext, aReflowInput, aStatus); + + if (mNeedToReset && !wasInterrupted) { + mNeedToReset = false; + // Suppress scrolling to the selected element if we restored + // scroll history state AND the list contents have not changed + // since we loaded all the children AND nothing else forced us + // to scroll by calling ResetList(true). The latter two conditions + // are folded into mPostChildrenLoadedReset. + // + // The idea is that we want scroll history restoration to trump ResetList + // scrolling to the selected element, when the ResetList was probably only + // caused by content loading normally. + ResetList(!DidHistoryRestore() || mPostChildrenLoadedReset); + } + + mHasPendingInterruptAtStartOfReflow = false; +} + +nsIAtom* +nsListControlFrame::GetType() const +{ + return nsGkAtoms::listControlFrame; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsListControlFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("ListControl"), aResult); +} +#endif + +nscoord +nsListControlFrame::GetBSizeOfARow() +{ + return BSizeOfARow(); +} + +nsresult +nsListControlFrame::IsOptionDisabled(int32_t anIndex, bool &aIsDisabled) +{ + RefPtr<dom::HTMLSelectElement> sel = + dom::HTMLSelectElement::FromContent(mContent); + if (sel) { + sel->IsOptionDisabled(anIndex, &aIsDisabled); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +//---------------------------------------------------------------------- +// helper +//---------------------------------------------------------------------- +bool +nsListControlFrame::IsLeftButton(nsIDOMEvent* aMouseEvent) +{ + // only allow selection with the left button + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent); + if (mouseEvent) { + int16_t whichButton; + if (NS_SUCCEEDED(mouseEvent->GetButton(&whichButton))) { + return whichButton != 0?false:true; + } + } + return false; +} + +nscoord +nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) +{ + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + return fontMet->MaxHeight(); +} + +nscoord +nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow, + int32_t aNumberOfOptions) +{ + NS_PRECONDITION(!IsInDropDownMode(), + "Shouldn't be in dropdown mode when we call this"); + + dom::HTMLSelectElement* select = + dom::HTMLSelectElement::FromContentOrNull(mContent); + if (select) { + mNumDisplayRows = select->Size(); + } else { + mNumDisplayRows = 1; + } + + if (mNumDisplayRows < 1) { + mNumDisplayRows = 4; + } + + return mNumDisplayRows * aBSizeOfARow; +} + +//---------------------------------------------------------------------- +// nsIDOMMouseListener +//---------------------------------------------------------------------- +nsresult +nsListControlFrame::MouseUp(nsIDOMEvent* aMouseEvent) +{ + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + UpdateInListState(aMouseEvent); + + mButtonDown = false; + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return NS_OK; + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + if (!IsLeftButton(aMouseEvent)) { + if (IsInDropDownMode()) { + if (!IgnoreMouseEventForSelection(aMouseEvent)) { + aMouseEvent->PreventDefault(); + aMouseEvent->StopPropagation(); + } else { + CaptureMouseEvents(false); + return NS_OK; + } + CaptureMouseEvents(false); + return NS_ERROR_FAILURE; // means consume event + } else { + CaptureMouseEvents(false); + return NS_OK; + } + } + + const nsStyleVisibility* vis = StyleVisibility(); + + if (!vis->IsVisible()) { + return NS_OK; + } + + if (IsInDropDownMode()) { + // XXX This is a bit of a hack, but..... + // But the idea here is to make sure you get an "onclick" event when you mouse + // down on the select and the drag over an option and let go + // And then NOT get an "onclick" event when when you click down on the select + // and then up outside of the select + // the EventStateManager tracks the content of the mouse down and the mouse up + // to make sure they are the same, and the onclick is sent in the PostHandleEvent + // depeneding on whether the clickCount is non-zero. + // So we cheat here by either setting or unsetting the clcikCount in the native event + // so the right thing happens for the onclick event + WidgetMouseEvent* mouseEvent = + aMouseEvent->WidgetEventPtr()->AsMouseEvent(); + + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + // If it's disabled, disallow the click and leave. + bool isDisabled = false; + IsOptionDisabled(selectedIndex, isDisabled); + if (isDisabled) { + aMouseEvent->PreventDefault(); + aMouseEvent->StopPropagation(); + CaptureMouseEvents(false); + return NS_ERROR_FAILURE; + } + + if (kNothingSelected != selectedIndex) { + nsWeakFrame weakFrame(this); + ComboboxFinish(selectedIndex); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + + FireOnInputAndOnChange(); + } + + mouseEvent->mClickCount = 1; + } else { + // the click was out side of the select or its dropdown + mouseEvent->mClickCount = + IgnoreMouseEventForSelection(aMouseEvent) ? 1 : 0; + } + } else { + CaptureMouseEvents(false); + // Notify + if (mChangesSinceDragStart) { + // reset this so that future MouseUps without a prior MouseDown + // won't fire onchange + mChangesSinceDragStart = false; + FireOnInputAndOnChange(); + } + } + + return NS_OK; +} + +void +nsListControlFrame::UpdateInListState(nsIDOMEvent* aEvent) +{ + if (!mComboboxFrame || !mComboboxFrame->IsDroppedDown()) + return; + + nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aEvent, this); + nsRect borderInnerEdge = GetScrollPortRect(); + if (pt.y >= borderInnerEdge.y && pt.y < borderInnerEdge.YMost()) { + mItemSelectionStarted = true; + } +} + +bool nsListControlFrame::IgnoreMouseEventForSelection(nsIDOMEvent* aEvent) +{ + if (!mComboboxFrame) + return false; + + // Our DOM listener does get called when the dropdown is not + // showing, because it listens to events on the SELECT element + if (!mComboboxFrame->IsDroppedDown()) + return true; + + return !mItemSelectionStarted; +} + +#ifdef ACCESSIBILITY +void +nsListControlFrame::FireMenuItemActiveEvent() +{ + if (mFocused != this && !IsInDropDownMode()) { + return; + } + + nsCOMPtr<nsIContent> optionContent = GetCurrentOption(); + if (!optionContent) { + return; + } + + FireDOMEvent(NS_LITERAL_STRING("DOMMenuItemActive"), optionContent); +} +#endif + +nsresult +nsListControlFrame::GetIndexFromDOMEvent(nsIDOMEvent* aMouseEvent, + int32_t& aCurIndex) +{ + if (IgnoreMouseEventForSelection(aMouseEvent)) + return NS_ERROR_FAILURE; + + if (nsIPresShell::GetCapturingContent() != mContent) { + // If we're not capturing, then ignore movement in the border + nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this); + nsRect borderInnerEdge = GetScrollPortRect(); + if (!borderInnerEdge.Contains(pt)) { + return NS_ERROR_FAILURE; + } + } + + RefPtr<dom::HTMLOptionElement> option; + for (nsCOMPtr<nsIContent> content = + PresContext()->EventStateManager()->GetEventTargetContent(nullptr); + content && !option; + content = content->GetParent()) { + option = dom::HTMLOptionElement::FromContent(content); + } + + if (option) { + aCurIndex = option->Index(); + MOZ_ASSERT(aCurIndex >= 0); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +static bool +FireShowDropDownEvent(nsIContent* aContent, bool aShow, bool aIsSourceTouchEvent) +{ + if (XRE_IsContentProcess() && + Preferences::GetBool("browser.tabs.remote.desktopbehavior", false)) { + nsString eventName; + if (aShow) { + eventName = aIsSourceTouchEvent ? NS_LITERAL_STRING("mozshowdropdown-sourcetouch") : + NS_LITERAL_STRING("mozshowdropdown"); + } else { + eventName = NS_LITERAL_STRING("mozhidedropdown"); + } + nsContentUtils::DispatchChromeEvent(aContent->OwnerDoc(), aContent, + eventName, true, false); + return true; + } + + return false; +} + +nsresult +nsListControlFrame::MouseDown(nsIDOMEvent* aMouseEvent) +{ + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + UpdateInListState(aMouseEvent); + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return NS_OK; + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + if (!IsLeftButton(aMouseEvent)) { + if (IsInDropDownMode()) { + if (!IgnoreMouseEventForSelection(aMouseEvent)) { + aMouseEvent->PreventDefault(); + aMouseEvent->StopPropagation(); + } else { + return NS_OK; + } + return NS_ERROR_FAILURE; // means consume event + } else { + return NS_OK; + } + } + + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + // Handle Like List + mButtonDown = true; + CaptureMouseEvents(true); + nsWeakFrame weakFrame(this); + bool change = + HandleListSelection(aMouseEvent, selectedIndex); // might destroy us + if (!weakFrame.IsAlive()) { + return NS_OK; + } + mChangesSinceDragStart = change; + } else { + // NOTE: the combo box is responsible for dropping it down + if (mComboboxFrame) { + // Ignore the click that occurs on the option element when one is + // selected from the parent process popup. + if (mComboboxFrame->IsOpenInParentProcess()) { + nsCOMPtr<nsIDOMEventTarget> etarget; + aMouseEvent->GetTarget(getter_AddRefs(etarget)); + nsCOMPtr<nsIDOMHTMLOptionElement> option = do_QueryInterface(etarget); + if (option) { + return NS_OK; + } + } + + uint16_t inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN; + if (NS_FAILED(mouseEvent->GetMozInputSource(&inputSource))) { + return NS_ERROR_FAILURE; + } + bool isSourceTouchEvent = inputSource == nsIDOMMouseEvent::MOZ_SOURCE_TOUCH; + if (FireShowDropDownEvent(mContent, !mComboboxFrame->IsDroppedDownOrHasParentPopup(), + isSourceTouchEvent)) { + return NS_OK; + } + + if (!IgnoreMouseEventForSelection(aMouseEvent)) { + return NS_OK; + } + + if (!nsComboboxControlFrame::ToolkitHasNativePopup()) + { + bool isDroppedDown = mComboboxFrame->IsDroppedDown(); + nsIFrame* comboFrame = do_QueryFrame(mComboboxFrame); + nsWeakFrame weakFrame(comboFrame); + mComboboxFrame->ShowDropDown(!isDroppedDown); + if (!weakFrame.IsAlive()) + return NS_OK; + if (isDroppedDown) { + CaptureMouseEvents(false); + } + } + } + } + + return NS_OK; +} + +//---------------------------------------------------------------------- +// nsIDOMMouseMotionListener +//---------------------------------------------------------------------- +nsresult +nsListControlFrame::MouseMove(nsIDOMEvent* aMouseEvent) +{ + NS_ASSERTION(aMouseEvent, "aMouseEvent is null."); + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + UpdateInListState(aMouseEvent); + + if (IsInDropDownMode()) { + if (mComboboxFrame->IsDroppedDown()) { + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + PerformSelection(selectedIndex, false, false); // might destroy us + } + } + } else {// XXX - temporary until we get drag events + if (mButtonDown) { + return DragMove(aMouseEvent); // might destroy us + } + } + return NS_OK; +} + +nsresult +nsListControlFrame::DragMove(nsIDOMEvent* aMouseEvent) +{ + NS_ASSERTION(aMouseEvent, "aMouseEvent is null."); + + UpdateInListState(aMouseEvent); + + if (!IsInDropDownMode()) { + int32_t selectedIndex; + if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { + // Don't waste cycles if we already dragged over this item + if (selectedIndex == mEndSelectionIndex) { + return NS_OK; + } + nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent); + NS_ASSERTION(mouseEvent, "aMouseEvent is not an nsIDOMMouseEvent!"); + bool isControl; +#ifdef XP_MACOSX + mouseEvent->GetMetaKey(&isControl); +#else + mouseEvent->GetCtrlKey(&isControl); +#endif + nsWeakFrame weakFrame(this); + // Turn SHIFT on when you are dragging, unless control is on. + bool wasChanged = PerformSelection(selectedIndex, + !isControl, isControl); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + mChangesSinceDragStart = mChangesSinceDragStart || wasChanged; + } + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Scroll helpers. +//---------------------------------------------------------------------- +void +nsListControlFrame::ScrollToIndex(int32_t aIndex) +{ + if (aIndex < 0) { + // XXX shouldn't we just do nothing if we're asked to scroll to + // kNothingSelected? + ScrollTo(nsPoint(0, 0), nsIScrollableFrame::INSTANT); + } else { + RefPtr<dom::HTMLOptionElement> option = + GetOption(AssertedCast<uint32_t>(aIndex)); + if (option) { + ScrollToFrame(*option); + } + } +} + +void +nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) +{ + // otherwise we find the content's frame and scroll to it + nsIFrame* childFrame = aOptElement.GetPrimaryFrame(); + if (childFrame) { + PresContext()->PresShell()-> + ScrollFrameRectIntoView(childFrame, + nsRect(nsPoint(0, 0), childFrame->GetSize()), + nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis(), + nsIPresShell::SCROLL_OVERFLOW_HIDDEN | + nsIPresShell::SCROLL_FIRST_ANCESTOR_ONLY); + } +} + +//--------------------------------------------------------------------- +// Ok, the entire idea of this routine is to move to the next item that +// is suppose to be selected. If the item is disabled then we search in +// the same direction looking for the next item to select. If we run off +// the end of the list then we start at the end of the list and search +// backwards until we get back to the original item or an enabled option +// +// aStartIndex - the index to start searching from +// aNewIndex - will get set to the new index if it finds one +// aNumOptions - the total number of options in the list +// aDoAdjustInc - the initial increment 1-n +// aDoAdjustIncNext - the increment used to search for the next enabled option +// +// the aDoAdjustInc could be a "1" for a single item or +// any number greater representing a page of items +// +void +nsListControlFrame::AdjustIndexForDisabledOpt(int32_t aStartIndex, + int32_t &aNewIndex, + int32_t aNumOptions, + int32_t aDoAdjustInc, + int32_t aDoAdjustIncNext) +{ + // Cannot select anything if there is nothing to select + if (aNumOptions == 0) { + aNewIndex = kNothingSelected; + return; + } + + // means we reached the end of the list and now we are searching backwards + bool doingReverse = false; + // lowest index in the search range + int32_t bottom = 0; + // highest index in the search range + int32_t top = aNumOptions; + + // Start off keyboard options at selectedIndex if nothing else is defaulted to + // + // XXX Perhaps this should happen for mouse too, to start off shift click + // automatically in multiple ... to do this, we'd need to override + // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not + // sure of the effects, though, so I'm not doing it just yet. + int32_t startIndex = aStartIndex; + if (startIndex < bottom) { + startIndex = GetSelectedIndex(); + } + int32_t newIndex = startIndex + aDoAdjustInc; + + // make sure we start off in the range + if (newIndex < bottom) { + newIndex = 0; + } else if (newIndex >= top) { + newIndex = aNumOptions-1; + } + + while (1) { + // if the newIndex isn't disabled, we are golden, bail out + bool isDisabled = true; + if (NS_SUCCEEDED(IsOptionDisabled(newIndex, isDisabled)) && !isDisabled) { + break; + } + + // it WAS disabled, so sart looking ahead for the next enabled option + newIndex += aDoAdjustIncNext; + + // well, if we reach end reverse the search + if (newIndex < bottom) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } else { + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = bottom; + aDoAdjustIncNext = 1; + doingReverse = true; + top = startIndex; + } + } else if (newIndex >= top) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } else { + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = top - 1; + aDoAdjustIncNext = -1; + doingReverse = true; + bottom = startIndex; + } + } + } + + // Looks like we found one + aNewIndex = newIndex; +} + +nsAString& +nsListControlFrame::GetIncrementalString() +{ + if (sIncrementalString == nullptr) + sIncrementalString = new nsString(); + + return *sIncrementalString; +} + +void +nsListControlFrame::Shutdown() +{ + delete sIncrementalString; + sIncrementalString = nullptr; +} + +void +nsListControlFrame::DropDownToggleKey(nsIDOMEvent* aKeyEvent) +{ + // Cocoa widgets do native popups, so don't try to show + // dropdowns there. + if (IsInDropDownMode() && !nsComboboxControlFrame::ToolkitHasNativePopup()) { + aKeyEvent->PreventDefault(); + if (!mComboboxFrame->IsDroppedDown()) { + if (!FireShowDropDownEvent(mContent, true, false)) { + mComboboxFrame->ShowDropDown(true); + } + } else { + nsWeakFrame weakFrame(this); + // mEndSelectionIndex is the last item that got selected. + ComboboxFinish(mEndSelectionIndex); + if (weakFrame.IsAlive()) { + FireOnInputAndOnChange(); + } + } + } +} + +nsresult +nsListControlFrame::KeyDown(nsIDOMEvent* aKeyEvent) +{ + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchResetter incrementalSearchResetter; + + // Don't check defaultPrevented value because other browsers don't prevent + // the key navigation of list control even if preventDefault() is called. + // XXXmats 2015-04-16: the above is not true anymore, Chrome prevents all + // XXXmats keyboard events, even tabbing, when preventDefault() is called + // XXXmats in onkeydown. That seems sub-optimal though. + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + bool dropDownMenuOnUpDown; + bool dropDownMenuOnSpace; +#ifdef XP_MACOSX + dropDownMenuOnUpDown = IsInDropDownMode() && !mComboboxFrame->IsDroppedDown(); + dropDownMenuOnSpace = !keyEvent->IsAlt() && !keyEvent->IsControl() && + !keyEvent->IsMeta(); +#else + dropDownMenuOnUpDown = keyEvent->IsAlt(); + dropDownMenuOnSpace = IsInDropDownMode() && !mComboboxFrame->IsDroppedDown(); +#endif + bool withinIncrementalSearchTime = + keyEvent->mTime - gLastKeyTime <= INCREMENTAL_SEARCH_KEYPRESS_TIME; + if ((dropDownMenuOnUpDown && + (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) || + (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE && + !withinIncrementalSearchTime)) { + DropDownToggleKey(aKeyEvent); + if (keyEvent->DefaultPrevented()) { + return NS_OK; + } + } + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // now make sure there are options or we are wasting our time + RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); + NS_ENSURE_TRUE(options, NS_ERROR_FAILURE); + + uint32_t numOptions = options->Length(); + + // this is the new index to set + int32_t newIndex = kNothingSelected; + + bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta()); + // Don't try to handle multiple-select pgUp/pgDown in single-select lists. + if (isControlOrMeta && !GetMultiple() && + (keyEvent->mKeyCode == NS_VK_PAGE_UP || + keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) { + return NS_OK; + } + if (isControlOrMeta && (keyEvent->mKeyCode == NS_VK_UP || + keyEvent->mKeyCode == NS_VK_LEFT || + keyEvent->mKeyCode == NS_VK_DOWN || + keyEvent->mKeyCode == NS_VK_RIGHT || + keyEvent->mKeyCode == NS_VK_HOME || + keyEvent->mKeyCode == NS_VK_END)) { + // Don't go into multiple-select mode unless this list can handle it. + isControlOrMeta = mControlSelectMode = GetMultiple(); + } else if (keyEvent->mKeyCode != NS_VK_SPACE) { + mControlSelectMode = false; + } + + switch (keyEvent->mKeyCode) { + case NS_VK_UP: + case NS_VK_LEFT: + AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex, + static_cast<int32_t>(numOptions), + -1, -1); + break; + case NS_VK_DOWN: + case NS_VK_RIGHT: + AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex, + static_cast<int32_t>(numOptions), + 1, 1); + break; + case NS_VK_RETURN: + if (IsInDropDownMode()) { + if (mComboboxFrame->IsDroppedDown()) { + // If the select element is a dropdown style, Enter key should be + // consumed while the dropdown is open for security. + aKeyEvent->PreventDefault(); + + nsWeakFrame weakFrame(this); + ComboboxFinish(mEndSelectionIndex); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + } + FireOnInputAndOnChange(); + return NS_OK; + } + + // If this is single select listbox, Enter key doesn't cause anything. + if (!GetMultiple()) { + return NS_OK; + } + + newIndex = mEndSelectionIndex; + break; + case NS_VK_ESCAPE: { + // If the select element is a listbox style, Escape key causes nothing. + if (!IsInDropDownMode()) { + return NS_OK; + } + + AboutToRollup(); + // If the select element is a dropdown style, Enter key should be + // consumed everytime since Escape key may be pressed accidentally after + // the dropdown is closed by Escepe key. + aKeyEvent->PreventDefault(); + return NS_OK; + } + case NS_VK_PAGE_UP: { + int32_t itemsPerPage = + std::max(1, static_cast<int32_t>(mNumDisplayRows - 1)); + AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex, + static_cast<int32_t>(numOptions), + -itemsPerPage, -1); + break; + } + case NS_VK_PAGE_DOWN: { + int32_t itemsPerPage = + std::max(1, static_cast<int32_t>(mNumDisplayRows - 1)); + AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex, + static_cast<int32_t>(numOptions), + itemsPerPage, 1); + break; + } + case NS_VK_HOME: + AdjustIndexForDisabledOpt(0, newIndex, + static_cast<int32_t>(numOptions), + 0, 1); + break; + case NS_VK_END: + AdjustIndexForDisabledOpt(static_cast<int32_t>(numOptions) - 1, newIndex, + static_cast<int32_t>(numOptions), + 0, -1); + break; + +#if defined(XP_WIN) + case NS_VK_F4: + if (!isControlOrMeta) { + DropDownToggleKey(aKeyEvent); + } + return NS_OK; +#endif + + default: // printable key will be handled by keypress event. + incrementalSearchResetter.Cancel(); + return NS_OK; + } + + aKeyEvent->PreventDefault(); + + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta); + return NS_OK; +} + +nsresult +nsListControlFrame::KeyPress(nsIDOMEvent* aKeyEvent) +{ + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchResetter incrementalSearchResetter; + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + // Select option with this as the first character + // XXX Not I18N compliant + + // Don't do incremental search if the key event has already consumed. + if (keyEvent->DefaultPrevented()) { + return NS_OK; + } + + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // With some keyboard layout, space key causes non-ASCII space. + // So, the check in keydown event handler isn't enough, we need to check it + // again with keypress event. + if (keyEvent->mCharCode != ' ') { + mControlSelectMode = false; + } + + bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta()); + if (isControlOrMeta && keyEvent->mCharCode != ' ') { + return NS_OK; + } + + // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0. + // Therefore, all non-printable keys are not handled after this block. + if (!keyEvent->mCharCode) { + // Backspace key will delete the last char in the string. Otherwise, + // non-printable keypress should reset incremental search. + if (keyEvent->mKeyCode == NS_VK_BACK) { + incrementalSearchResetter.Cancel(); + if (!GetIncrementalString().IsEmpty()) { + GetIncrementalString().Truncate(GetIncrementalString().Length() - 1); + } + aKeyEvent->PreventDefault(); + } else { + // XXX When a select element has focus, even if the key causes nothing, + // it might be better to call preventDefault() here because nobody + // should expect one of other elements including chrome handles the + // key event. + } + return NS_OK; + } + + incrementalSearchResetter.Cancel(); + + // We ate the key if we got this far. + aKeyEvent->PreventDefault(); + + // XXX Why don't we check/modify timestamp first? + + // Incremental Search: if time elapsed is below + // INCREMENTAL_SEARCH_KEYPRESS_TIME, append this keystroke to the search + // string we will use to find options and start searching at the current + // keystroke. Otherwise, Truncate the string if it's been a long time + // since our last keypress. + if (keyEvent->mTime - gLastKeyTime > INCREMENTAL_SEARCH_KEYPRESS_TIME) { + // If this is ' ' and we are at the beginning of the string, treat it as + // "select this option" (bug 191543) + if (keyEvent->mCharCode == ' ') { + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(mEndSelectionIndex, keyEvent->mCharCode, + keyEvent->IsShift(), isControlOrMeta); + + return NS_OK; + } + + GetIncrementalString().Truncate(); + } + + gLastKeyTime = keyEvent->mTime; + + // Append this keystroke to the search string. + char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->mCharCode)); + GetIncrementalString().Append(uniChar); + + // See bug 188199, if all letters in incremental string are same, just try to + // match the first one + nsAutoString incrementalString(GetIncrementalString()); + uint32_t charIndex = 1, stringLength = incrementalString.Length(); + while (charIndex < stringLength && + incrementalString[charIndex] == incrementalString[charIndex - 1]) { + charIndex++; + } + if (charIndex == stringLength) { + incrementalString.Truncate(1); + stringLength = 1; + } + + // Determine where we're going to start reading the string + // If we have multiple characters to look for, we start looking *at* the + // current option. If we have only one character to look for, we start + // looking *after* the current option. + // Exception: if there is no option selected to start at, we always start + // *at* 0. + int32_t startIndex = GetSelectedIndex(); + if (startIndex == kNothingSelected) { + startIndex = 0; + } else if (stringLength == 1) { + startIndex++; + } + + // now make sure there are options or we are wasting our time + RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); + NS_ENSURE_TRUE(options, NS_ERROR_FAILURE); + + uint32_t numOptions = options->Length(); + + nsWeakFrame weakFrame(this); + for (uint32_t i = 0; i < numOptions; ++i) { + uint32_t index = (i + startIndex) % numOptions; + RefPtr<dom::HTMLOptionElement> optionElement = + options->ItemAsOption(index); + if (!optionElement || !optionElement->GetPrimaryFrame()) { + continue; + } + + nsAutoString text; + if (NS_FAILED(optionElement->GetText(text)) || + !StringBeginsWith( + nsContentUtils::TrimWhitespace< + nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false), + incrementalString, nsCaseInsensitiveStringComparator())) { + continue; + } + + bool wasChanged = PerformSelection(index, keyEvent->IsShift(), isControlOrMeta); + if (!weakFrame.IsAlive()) { + return NS_OK; + } + if (!wasChanged) { + break; + } + + // If UpdateSelection() returns false, that means the frame is no longer + // alive. We should stop doing anything. + if (!UpdateSelection()) { + return NS_OK; + } + break; + } + + return NS_OK; +} + +void +nsListControlFrame::PostHandleKeyEvent(int32_t aNewIndex, + uint32_t aCharCode, + bool aIsShift, + bool aIsControlOrMeta) +{ + if (aNewIndex == kNothingSelected) { + return; + } + + // If you hold control, but not shift, no key will actually do anything + // except space. + nsWeakFrame weakFrame(this); + bool wasChanged = false; + if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') { + mStartSelectionIndex = aNewIndex; + mEndSelectionIndex = aNewIndex; + InvalidateFocus(); + ScrollToIndex(aNewIndex); + if (!weakFrame.IsAlive()) { + return; + } + +#ifdef ACCESSIBILITY + FireMenuItemActiveEvent(); +#endif + } else if (mControlSelectMode && aCharCode == ' ') { + wasChanged = SingleSelection(aNewIndex, true); + } else { + wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta); + } + if (wasChanged && weakFrame.IsAlive()) { + // dispatch event, update combobox, etc. + UpdateSelection(); + } +} + + +/****************************************************************************** + * nsListEventListener + *****************************************************************************/ + +NS_IMPL_ISUPPORTS(nsListEventListener, nsIDOMEventListener) + +NS_IMETHODIMP +nsListEventListener::HandleEvent(nsIDOMEvent* aEvent) +{ + if (!mFrame) + return NS_OK; + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("keydown")) { + return mFrame->nsListControlFrame::KeyDown(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return mFrame->nsListControlFrame::KeyPress(aEvent); + } + if (eventType.EqualsLiteral("mousedown")) { + bool defaultPrevented = false; + aEvent->GetDefaultPrevented(&defaultPrevented); + if (defaultPrevented) { + return NS_OK; + } + return mFrame->nsListControlFrame::MouseDown(aEvent); + } + if (eventType.EqualsLiteral("mouseup")) { + // Don't try to honor defaultPrevented here - it's not web compatible. + // (bug 1194733) + return mFrame->nsListControlFrame::MouseUp(aEvent); + } + if (eventType.EqualsLiteral("mousemove")) { + // I don't think we want to honor defaultPrevented on mousemove + // in general, and it would only prevent highlighting here. + return mFrame->nsListControlFrame::MouseMove(aEvent); + } + + NS_ABORT(); + return NS_OK; +} diff --git a/layout/forms/nsListControlFrame.h b/layout/forms/nsListControlFrame.h new file mode 100644 index 000000000..47f033a9b --- /dev/null +++ b/layout/forms/nsListControlFrame.h @@ -0,0 +1,477 @@ +/* -*- 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/. */ +#ifndef nsListControlFrame_h___ +#define nsListControlFrame_h___ + +#ifdef DEBUG_evaughan +//#define DEBUG_rods +#endif + +#ifdef DEBUG_rods +//#define DO_REFLOW_DEBUG +//#define DO_REFLOW_COUNTER +//#define DO_UNCONSTRAINED_CHECK +//#define DO_PIXELS +#endif + +#include "mozilla/Attributes.h" +#include "nsGfxScrollFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIListControlFrame.h" +#include "nsISelectControlFrame.h" +#include "nsSelectsAreaFrame.h" + +// X.h defines KeyPress +#ifdef KeyPress +#undef KeyPress +#endif + +class nsIComboboxControlFrame; +class nsPresContext; +class nsListEventListener; + +namespace mozilla { +namespace dom { +class HTMLOptionElement; +class HTMLOptionsCollection; +} // namespace dom +} // namespace mozilla + +/** + * Frame-based listbox. + */ + +class nsListControlFrame final : public nsHTMLScrollFrame, + public nsIFormControlFrame, + public nsIListControlFrame, + public nsISelectControlFrame +{ +public: + friend nsContainerFrame* NS_NewListControlFrame(nsIPresShell* aPresShell, + nsStyleContext* aContext); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + // nsIFrame + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + virtual void SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) override; + + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual void Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual void DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput, + nsDidReflowStatus aStatus) override; + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + virtual nsContainerFrame* GetContentInsertionFrame() override; + + /** + * Get the "type" of the frame + * + * @see nsGkAtoms::scrollFrame + */ + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsHTMLScrollFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + // nsIFormControlFrame + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + virtual void SetFocus(bool aOn = true, bool aRepaint = false) override; + + virtual mozilla::ScrollbarStyles GetScrollbarStyles() const override; + virtual bool ShouldPropagateComputedBSizeToScrolledContent() const override; + + // for accessibility purposes +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIListControlFrame + virtual void SetComboboxFrame(nsIFrame* aComboboxFrame) override; + virtual int32_t GetSelectedIndex() override; + virtual mozilla::dom::HTMLOptionElement* GetCurrentOption() override; + + /** + * Gets the text of the currently selected item. + * If the there are zero items then an empty string is returned + * If there is nothing selected, then the 0th item's text is returned. + */ + virtual void GetOptionText(uint32_t aIndex, nsAString& aStr) override; + + virtual void CaptureMouseEvents(bool aGrabMouseEvents) override; + virtual nscoord GetBSizeOfARow() override; + virtual uint32_t GetNumberOfOptions() override; + virtual void AboutToDropDown() override; + + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + virtual void AboutToRollup() override; + + /** + * Dispatch a DOM oninput and onchange event synchroniously. + * @note This method might destroy the frame, pres shell and other objects. + */ + virtual void FireOnInputAndOnChange() override; + + /** + * Makes aIndex the selected option of a combobox list. + * @note This method might destroy the frame, pres shell and other objects. + */ + virtual void ComboboxFinish(int32_t aIndex) override; + virtual void OnContentReset() override; + + // nsISelectControlFrame + NS_IMETHOD AddOption(int32_t index) override; + NS_IMETHOD RemoveOption(int32_t index) override; + NS_IMETHOD DoneAddingChildren(bool aIsDone) override; + + /** + * Gets the content (an option) by index and then set it as + * being selected or not selected. + */ + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) override; + NS_IMETHOD OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) override; + + /** + * Mouse event listeners. + * @note These methods might destroy the frame, pres shell and other objects. + */ + nsresult MouseDown(nsIDOMEvent* aMouseEvent); + nsresult MouseUp(nsIDOMEvent* aMouseEvent); + nsresult MouseMove(nsIDOMEvent* aMouseEvent); + nsresult DragMove(nsIDOMEvent* aMouseEvent); + nsresult KeyDown(nsIDOMEvent* aKeyEvent); + nsresult KeyPress(nsIDOMEvent* aKeyEvent); + + /** + * Returns the options collection for mContent, if any. + */ + mozilla::dom::HTMLOptionsCollection* GetOptions() const; + /** + * Returns the HTMLOptionElement for a given index in mContent's collection. + */ + mozilla::dom::HTMLOptionElement* GetOption(uint32_t aIndex) const; + + static void ComboboxFocusSet(); + + // Helper + bool IsFocused() { return this == mFocused; } + + /** + * Function to paint the focus rect when our nsSelectsAreaFrame is painting. + * @param aPt the offset of this frame, relative to the rendering reference + * frame + */ + void PaintFocus(mozilla::gfx::DrawTarget* aDrawTarget, nsPoint aPt); + + /** + * If this frame IsFocused(), invalidates an area that includes anything + * that PaintFocus will or could have painted --- basically the whole + * GetOptionsContainer, plus some extra stuff if there are no options. This + * must be called every time mEndSelectionIndex changes. + */ + void InvalidateFocus(); + + /** + * Function to calculate the block size of a row, for use with the + * "size" attribute. + * Can't be const because GetNumberOfOptions() isn't const. + */ + nscoord CalcBSizeOfARow(); + + /** + * Function to ask whether we're currently in what might be the + * first pass of a two-pass reflow. + */ + bool MightNeedSecondPass() const { + return mMightNeedSecondPass; + } + + void SetSuppressScrollbarUpdate(bool aSuppress) { + nsHTMLScrollFrame::SetSuppressScrollbarUpdate(aSuppress); + } + + /** + * Return whether the list is in dropdown mode. + */ + bool IsInDropDownMode() const; + + /** + * Return the number of displayed rows in the list. + */ + uint32_t GetNumDisplayRows() const { return mNumDisplayRows; } + + /** + * Return true if the drop-down list can display more rows. + * (always false if not in drop-down mode) + */ + bool GetDropdownCanGrow() const { return mDropdownCanGrow; } + + /** + * Dropdowns need views + */ + virtual bool NeedsView() override { return IsInDropDownMode(); } + + /** + * Frees statics owned by this class. + */ + static void Shutdown(); + +#ifdef ACCESSIBILITY + /** + * Post a custom DOM event for the change, so that accessibility can + * fire a native focus event for accessibility + * (Some 3rd party products need to track our focus) + */ + void FireMenuItemActiveEvent(); // Inform assistive tech what got focused +#endif + +protected: + /** + * Updates the selected text in a combobox and then calls FireOnChange(). + * @note This method might destroy the frame, pres shell and other objects. + * Returns false if calling it destroyed |this|. + */ + bool UpdateSelection(); + + /** + * Returns whether mContent supports multiple selection. + */ + bool GetMultiple() const { + return mContent->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple); + } + + + /** + * Toggles (show/hide) the combobox dropdown menu. + * @note This method might destroy the frame, pres shell and other objects. + */ + void DropDownToggleKey(nsIDOMEvent* aKeyEvent); + + nsresult IsOptionDisabled(int32_t anIndex, bool &aIsDisabled); + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + void ScrollToFrame(mozilla::dom::HTMLOptionElement& aOptElement); + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + void ScrollToIndex(int32_t anIndex); + + /** + * When the user clicks on the comboboxframe to show the dropdown + * listbox, they then have to move the mouse into the list. We don't + * want to process those mouse events as selection events (i.e., to + * scroll list items into view). So we ignore the events until + * the mouse moves below our border-inner-edge, when + * mItemSelectionStarted is set. + * + * @param aPoint relative to this frame + */ + bool IgnoreMouseEventForSelection(nsIDOMEvent* aEvent); + + /** + * If the dropdown is showing and the mouse has moved below our + * border-inner-edge, then set mItemSelectionStarted. + */ + void UpdateInListState(nsIDOMEvent* aEvent); + void AdjustIndexForDisabledOpt(int32_t aStartIndex, int32_t &anNewIndex, + int32_t aNumOptions, int32_t aDoAdjustInc, int32_t aDoAdjustIncNext); + + /** + * Resets the select back to it's original default values; + * those values as determined by the original HTML + */ + virtual void ResetList(bool aAllowScrolling); + + explicit nsListControlFrame(nsStyleContext* aContext); + virtual ~nsListControlFrame(); + + /** + * Sets the mSelectedIndex and mOldSelectedIndex from figuring out what + * item was selected using content + * @param aPoint the event point, in listcontrolframe coordinates + * @return NS_OK if it successfully found the selection + */ + nsresult GetIndexFromDOMEvent(nsIDOMEvent* aMouseEvent, int32_t& aCurIndex); + + bool CheckIfAllFramesHere(); + bool IsLeftButton(nsIDOMEvent* aMouseEvent); + + // guess at a row block size based on our own style. + nscoord CalcFallbackRowBSize(float aFontSizeInflation); + + // CalcIntrinsicBSize computes our intrinsic block size (taking the + // "size" attribute into account). This should only be called in + // non-dropdown mode. + nscoord CalcIntrinsicBSize(nscoord aBSizeOfARow, int32_t aNumberOfOptions); + + // Dropped down stuff + void SetComboboxItem(int32_t aIndex); + + /** + * Method to reflow ourselves as a dropdown list. This differs from + * reflow as a listbox because the criteria for needing a second + * pass are different. This will be called from Reflow() as needed. + */ + void ReflowAsDropdown(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus); + + // Selection + bool SetOptionsSelectedFromFrame(int32_t aStartIndex, + int32_t aEndIndex, + bool aValue, + bool aClearAll); + bool ToggleOptionSelectedFromFrame(int32_t aIndex); + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + bool SingleSelection(int32_t aClickedIndex, bool aDoToggle); + bool ExtendedSelection(int32_t aStartIndex, int32_t aEndIndex, + bool aClearAll); + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + bool PerformSelection(int32_t aClickedIndex, bool aIsShift, + bool aIsControl); + /** + * @note This method might destroy the frame, pres shell and other objects. + */ + bool HandleListSelection(nsIDOMEvent * aDOMEvent, int32_t selectedIndex); + void InitSelectionRange(int32_t aClickedIndex); + void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode, + bool aIsShift, bool aIsControlOrMeta); + +public: + nsSelectsAreaFrame* GetOptionsContainer() const { + return static_cast<nsSelectsAreaFrame*>(GetScrolledFrame()); + } + +protected: + nscoord BSizeOfARow() { + return GetOptionsContainer()->BSizeOfARow(); + } + + /** + * @return how many displayable options/optgroups this frame has. + */ + uint32_t GetNumberOfRows(); + + // Data Members + int32_t mStartSelectionIndex; + int32_t mEndSelectionIndex; + + nsIComboboxControlFrame *mComboboxFrame; + uint32_t mNumDisplayRows; + bool mChangesSinceDragStart:1; + bool mButtonDown:1; + // Has the user selected a visible item since we showed the + // dropdown? + bool mItemSelectionStarted:1; + + bool mIsAllContentHere:1; + bool mIsAllFramesHere:1; + bool mHasBeenInitialized:1; + bool mNeedToReset:1; + bool mPostChildrenLoadedReset:1; + + //bool value for multiple discontiguous selection + bool mControlSelectMode:1; + + // True if we're in the middle of a reflow and might need a second + // pass. This only happens for auto heights. + bool mMightNeedSecondPass:1; + + /** + * Set to aPresContext->HasPendingInterrupt() at the start of Reflow. + * Set to false at the end of DidReflow. + */ + bool mHasPendingInterruptAtStartOfReflow:1; + + // True if the drop-down can show more rows. Always false if this list + // is not in drop-down mode. + bool mDropdownCanGrow:1; + + // True if the selection can be set to nothing or disabled options. + bool mForceSelection:1; + + // The last computed block size we reflowed at if we're a combobox + // dropdown. + // XXXbz should we be using a subclass here? Or just not worry + // about the extra member on listboxes? + nscoord mLastDropdownComputedBSize; + + // At the time of our last dropdown, the backstop color to draw in case we + // are translucent. + nscolor mLastDropdownBackstopColor; + + RefPtr<nsListEventListener> mEventListener; + + static nsListControlFrame * mFocused; + static nsString * sIncrementalString; + +#ifdef DO_REFLOW_COUNTER + int32_t mReflowId; +#endif + +private: + // for incremental typing navigation + static nsAString& GetIncrementalString (); + static DOMTimeStamp gLastKeyTime; + + class MOZ_RAII AutoIncrementalSearchResetter + { + public: + AutoIncrementalSearchResetter() : + mCancelled(false) + { + } + ~AutoIncrementalSearchResetter() + { + if (!mCancelled) { + nsListControlFrame::GetIncrementalString().Truncate(); + } + } + void Cancel() + { + mCancelled = true; + } + private: + bool mCancelled; + }; +}; + +#endif /* nsListControlFrame_h___ */ + diff --git a/layout/forms/nsMeterFrame.cpp b/layout/forms/nsMeterFrame.cpp new file mode 100644 index 000000000..1f3bd8022 --- /dev/null +++ b/layout/forms/nsMeterFrame.cpp @@ -0,0 +1,297 @@ +/* -*- 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 "nsMeterFrame.h" + +#include "nsIContent.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsFormControlFrame.h" +#include "nsFontMetrics.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMeterElement.h" +#include "nsContentList.h" +#include "nsCSSPseudoElements.h" +#include "nsStyleSet.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsThemeConstants.h" +#include <algorithm> + +using namespace mozilla; +using mozilla::dom::Element; +using mozilla::dom::HTMLMeterElement; + +nsIFrame* +NS_NewMeterFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsMeterFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsMeterFrame) + +nsMeterFrame::nsMeterFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) + , mBarDiv(nullptr) +{ +} + +nsMeterFrame::~nsMeterFrame() +{ +} + +void +nsMeterFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + NS_ASSERTION(!GetPrevContinuation(), + "nsMeterFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContentUtils::DestroyAnonymousContent(&mBarDiv); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +nsIAtom* +nsMeterFrame::GetType() const +{ + return nsGkAtoms::meterFrame; +} + +nsresult +nsMeterFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + // Get the NodeInfoManager and tag necessary to create the meter bar div. + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + + // Create the div. + mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate ::-moz-meter-bar pseudo-element to the anonymous child. + CSSPseudoElementType pseudoType = CSSPseudoElementType::mozMeterBar; + RefPtr<nsStyleContext> newStyleContext = PresContext()->StyleSet()-> + ResolvePseudoElementStyle(mContent->AsElement(), pseudoType, + StyleContext(), mBarDiv->AsElement()); + + if (!aElements.AppendElement(ContentInfo(mBarDiv, newStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void +nsMeterFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mBarDiv) { + aElements.AppendElement(mBarDiv); + } +} + +NS_QUERYFRAME_HEAD(nsMeterFrame) + NS_QUERYFRAME_ENTRY(nsMeterFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + + +void +nsMeterFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsMeterFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_ASSERTION(mBarDiv, "Meter bar div must exist!"); + NS_ASSERTION(!GetPrevContinuation(), + "nsMeterFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!"); + + ReflowBarFrame(barFrame, aPresContext, aReflowInput, aStatus); + + aDesiredSize.SetSize(aReflowInput.GetWritingMode(), + aReflowInput.ComputedSizeWithBorderPadding()); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, barFrame); + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +void +nsMeterFrame::ReflowBarFrame(nsIFrame* aBarFrame, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + bool vertical = ResolvedOrientationIsVertical(); + WritingMode wm = aBarFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput reflowInput(aPresContext, aReflowInput, + aBarFrame, availSize); + nscoord size = vertical ? aReflowInput.ComputedHeight() + : aReflowInput.ComputedWidth(); + nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left; + nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top; + + // NOTE: Introduce a new function getPosition in the content part ? + HTMLMeterElement* meterElement = static_cast<HTMLMeterElement*>(mContent); + + double max = meterElement->Max(); + double min = meterElement->Min(); + double value = meterElement->Value(); + + double position = max - min; + position = position != 0 ? (value - min) / position : 1; + + size = NSToCoordRound(size * position); + + if (!vertical && (wm.IsVertical() ? wm.IsVerticalRL() : !wm.IsBidiLTR())) { + xoffset += aReflowInput.ComputedWidth() - size; + } + + // The bar position is *always* constrained. + if (vertical) { + // We want the bar to begin at the bottom. + yoffset += aReflowInput.ComputedHeight() - size; + + size -= reflowInput.ComputedPhysicalMargin().TopBottom() + + reflowInput.ComputedPhysicalBorderPadding().TopBottom(); + size = std::max(size, 0); + reflowInput.SetComputedHeight(size); + } else { + size -= reflowInput.ComputedPhysicalMargin().LeftRight() + + reflowInput.ComputedPhysicalBorderPadding().LeftRight(); + size = std::max(size, 0); + reflowInput.SetComputedWidth(size); + } + + xoffset += reflowInput.ComputedPhysicalMargin().left; + yoffset += reflowInput.ComputedPhysicalMargin().top; + + ReflowOutput barDesiredSize(reflowInput); + ReflowChild(aBarFrame, aPresContext, barDesiredSize, reflowInput, xoffset, + yoffset, 0, aStatus); + FinishReflowChild(aBarFrame, aPresContext, barDesiredSize, &reflowInput, + xoffset, yoffset, 0); +} + +nsresult +nsMeterFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + NS_ASSERTION(mBarDiv, "Meter bar div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::value || + aAttribute == nsGkAtoms::max || + aAttribute == nsGkAtoms::min )) { + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + NS_ASSERTION(barFrame, "The meter frame should have a child with a frame!"); + PresContext()->PresShell()->FrameNeedsReflow(barFrame, + nsIPresShell::eResize, + NS_FRAME_IS_DIRTY); + InvalidateFrame(); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +LogicalSize +nsMeterFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + autoSize.BSize(wm) = autoSize.ISize(wm) = fontMet->Font().size; // 1em + + if (ResolvedOrientationIsVertical() == wm.IsVertical()) { + autoSize.ISize(wm) *= 5; // 5em + } else { + autoSize.BSize(wm) *= 5; // 5em + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord +nsMeterFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + nscoord minISize = fontMet->Font().size; // 1em + + if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) { + // The orientation is inline + minISize *= 5; // 5em + } + + return minISize; +} + +nscoord +nsMeterFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + return GetMinISize(aRenderingContext); +} + +bool +nsMeterFrame::ShouldUseNativeStyle() const +{ + nsIFrame* barFrame = mBarDiv->GetPrimaryFrame(); + + // Use the native style if these conditions are satisfied: + // - both frames use the native appearance; + // - neither frame has author specified rules setting the border or the + // background. + return StyleDisplay()->mAppearance == NS_THEME_METERBAR && + !PresContext()->HasAuthorSpecifiedRules(this, + NS_AUTHOR_SPECIFIED_BORDER | NS_AUTHOR_SPECIFIED_BACKGROUND) && + barFrame && + barFrame->StyleDisplay()->mAppearance == NS_THEME_METERCHUNK && + !PresContext()->HasAuthorSpecifiedRules(barFrame, + NS_AUTHOR_SPECIFIED_BORDER | NS_AUTHOR_SPECIFIED_BACKGROUND); +} + +Element* +nsMeterFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::mozMeterBar) { + return mBarDiv; + } + + return nsContainerFrame::GetPseudoElement(aType); +} diff --git a/layout/forms/nsMeterFrame.h b/layout/forms/nsMeterFrame.h new file mode 100644 index 000000000..9f24b7656 --- /dev/null +++ b/layout/forms/nsMeterFrame.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef nsMeterFrame_h___ +#define nsMeterFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" +#include "nsCSSPseudoElements.h" + +class nsMeterFrame : public nsContainerFrame, + public nsIAnonymousContentCreator + +{ + typedef mozilla::dom::Element Element; + +public: + NS_DECL_QUERYFRAME_TARGET(nsMeterFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + explicit nsMeterFrame(nsStyleContext* aContext); + virtual ~nsMeterFrame(); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("Meter"), aResult); + } +#endif + + virtual bool IsLeaf() const override { return true; } + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize + ComputeAutoSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + virtual Element* GetPseudoElement(mozilla::CSSPseudoElementType aType) override; + +protected: + // Helper function which reflow the anonymous div frame. + void ReflowBarFrame(nsIFrame* aBarFrame, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus); + /** + * The div used to show the meter bar. + * @see nsMeterFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mBarDiv; +}; + +#endif diff --git a/layout/forms/nsNumberControlFrame.cpp b/layout/forms/nsNumberControlFrame.cpp new file mode 100644 index 000000000..58421ecb9 --- /dev/null +++ b/layout/forms/nsNumberControlFrame.cpp @@ -0,0 +1,799 @@ +/* -*- 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 "nsNumberControlFrame.h" + +#include "HTMLInputElement.h" +#include "ICUUtils.h" +#include "nsIFocusManager.h" +#include "nsIPresShell.h" +#include "nsFocusManager.h" +#include "nsFontMetrics.h" +#include "nsFormControlFrame.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsThemeConstants.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventStates.h" +#include "nsContentUtils.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentList.h" +#include "nsCSSPseudoElements.h" +#include "nsStyleSet.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsIDOMMutationEvent.h" +#include "nsThreadUtils.h" +#include "mozilla/FloatingPoint.h" + +#ifdef ACCESSIBILITY +#include "mozilla/a11y/AccTypes.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* +NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsNumberControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsNumberControlFrame) + +NS_QUERYFRAME_HEAD(nsNumberControlFrame) + NS_QUERYFRAME_ENTRY(nsNumberControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsNumberControlFrame::nsNumberControlFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) + , mHandlingInputEvent(false) +{ +} + +void +nsNumberControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsNumberControlFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first"); + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContentUtils::DestroyAnonymousContent(&mOuterWrapper); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +nscoord +nsNumberControlFrame::GetMinISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::MIN_ISIZE); + } else { + result = 0; + } + + return result; +} + +nscoord +nsNumberControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext) +{ + nscoord result; + DISPLAY_PREF_WIDTH(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, + kid, + nsLayoutUtils::PREF_ISIZE); + } else { + result = 0; + } + + return result; +} + +void +nsNumberControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsNumberControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_ASSERTION(mOuterWrapper, "Outer wrapper div must exist!"); + + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsNumberControlFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first"); + + NS_ASSERTION(!mFrames.FirstChild() || + !mFrames.FirstChild()->GetNextSibling(), + "We expect at most one direct child frame"); + + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + const WritingMode myWM = aReflowInput.GetWritingMode(); + + // The ISize of our content box, which is the available ISize + // for our anonymous content: + const nscoord contentBoxISize = aReflowInput.ComputedISize(); + nscoord contentBoxBSize = aReflowInput.ComputedBSize(); + + // Figure out our border-box sizes as well (by adding borderPadding to + // content-box sizes): + const nscoord borderBoxISize = contentBoxISize + + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(myWM); + + nscoord borderBoxBSize; + if (contentBoxBSize != NS_INTRINSICSIZE) { + borderBoxBSize = contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize. + + nsIFrame* outerWrapperFrame = mOuterWrapper->GetPrimaryFrame(); + + if (!outerWrapperFrame) { // display:none? + if (contentBoxBSize == NS_INTRINSICSIZE) { + contentBoxBSize = 0; + borderBoxBSize = + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } + } else { + NS_ASSERTION(outerWrapperFrame == mFrames.FirstChild(), "huh?"); + + ReflowOutput wrappersDesiredSize(aReflowInput); + + WritingMode wrapperWM = outerWrapperFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wrapperWM); + availSize.BSize(wrapperWM) = NS_UNCONSTRAINEDSIZE; + + ReflowInput wrapperReflowInput(aPresContext, aReflowInput, + outerWrapperFrame, availSize); + + // Convert wrapper margin into my own writing-mode (in case it differs): + LogicalMargin wrapperMargin = + wrapperReflowInput.ComputedLogicalMargin().ConvertTo(myWM, wrapperWM); + + // offsets of wrapper frame within this frame: + LogicalPoint + wrapperOffset(myWM, + aReflowInput.ComputedLogicalBorderPadding().IStart(myWM) + + wrapperMargin.IStart(myWM), + aReflowInput.ComputedLogicalBorderPadding().BStart(myWM) + + wrapperMargin.BStart(myWM)); + + nsReflowStatus childStatus; + // We initially reflow the child with a dummy containerSize; positioning + // will be fixed later. + const nsSize dummyContainerSize; + ReflowChild(outerWrapperFrame, aPresContext, wrappersDesiredSize, + wrapperReflowInput, myWM, wrapperOffset, dummyContainerSize, 0, + childStatus); + MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(childStatus), + "We gave our child unconstrained available block-size, " + "so it should be complete"); + + nscoord wrappersMarginBoxBSize = + wrappersDesiredSize.BSize(myWM) + wrapperMargin.BStartEnd(myWM); + + if (contentBoxBSize == NS_INTRINSICSIZE) { + // We are intrinsically sized -- we should shrinkwrap the outer wrapper's + // block-size: + contentBoxBSize = wrappersMarginBoxBSize; + + // Make sure we obey min/max-bsize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since ComputedMaxBSize and + // ComputedMinBSize are content heights. + contentBoxBSize = + NS_CSS_MINMAX(contentBoxBSize, + aReflowInput.ComputedMinBSize(), + aReflowInput.ComputedMaxBSize()); + + borderBoxBSize = contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM); + } + + // Center child in block axis + nscoord extraSpace = contentBoxBSize - wrappersMarginBoxBSize; + wrapperOffset.B(myWM) += std::max(0, extraSpace / 2); + + // Needed in FinishReflowChild, for logical-to-physical conversion: + nsSize borderBoxSize = LogicalSize(myWM, borderBoxISize, borderBoxBSize). + GetPhysicalSize(myWM); + + // Place the child + FinishReflowChild(outerWrapperFrame, aPresContext, wrappersDesiredSize, + &wrapperReflowInput, myWM, wrapperOffset, + borderBoxSize, 0); + + nsSize contentBoxSize = + LogicalSize(myWM, contentBoxISize, contentBoxBSize). + GetPhysicalSize(myWM); + aDesiredSize.SetBlockStartAscent( + wrappersDesiredSize.BlockStartAscent() + + outerWrapperFrame->BStart(aReflowInput.GetWritingMode(), + contentBoxSize)); + } + + LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize); + aDesiredSize.SetSize(myWM, logicalDesiredSize); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (outerWrapperFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, outerWrapperFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +void +nsNumberControlFrame::SyncDisabledState() +{ + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) { + mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, EmptyString(), + true); + } else { + mTextField->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +nsresult +nsNumberControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + // nsGkAtoms::disabled is handled by SyncDisabledState + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::placeholder || + aAttribute == nsGkAtoms::readonly || + aAttribute == nsGkAtoms::tabindex) { + if (aModType == nsIDOMMutationEvent::REMOVAL) { + mTextField->UnsetAttr(aNameSpaceID, aAttribute, true); + } else { + MOZ_ASSERT(aModType == nsIDOMMutationEvent::ADDITION || + aModType == nsIDOMMutationEvent::MODIFICATION); + nsAutoString value; + mContent->GetAttr(aNameSpaceID, aAttribute, value); + mTextField->SetAttr(aNameSpaceID, aAttribute, value, true); + } + } + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +void +nsNumberControlFrame::ContentStatesChanged(EventStates aStates) +{ + if (aStates.HasState(NS_EVENT_STATE_DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +nsITextControlFrame* +nsNumberControlFrame::GetTextFieldFrame() +{ + return do_QueryFrame(GetAnonTextControl()->GetPrimaryFrame()); +} + +class FocusTextField : public Runnable +{ +public: + FocusTextField(nsIContent* aNumber, nsIContent* aTextField) + : mNumber(aNumber), + mTextField(aTextField) + {} + + NS_IMETHOD Run() override + { + if (mNumber->AsElement()->State().HasState(NS_EVENT_STATE_FOCUS)) { + HTMLInputElement::FromContent(mTextField)->Focus(); + } + + return NS_OK; + } + +private: + nsCOMPtr<nsIContent> mNumber; + nsCOMPtr<nsIContent> mTextField; +}; + +nsresult +nsNumberControlFrame::MakeAnonymousElement(Element** aResult, + nsTArray<ContentInfo>& aElements, + nsIAtom* aTagName, + CSSPseudoElementType aPseudoType, + nsStyleContext* aParentContext) +{ + // Get the NodeInfoManager and tag necessary to create the anonymous divs. + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + RefPtr<Element> resultElement = doc->CreateHTMLElement(aTagName); + + // If we legitimately fail this assertion and need to allow + // non-pseudo-element anonymous children, then we'll need to add a branch + // that calls ResolveStyleFor((*aResult)->AsElement(), aParentContext)") to + // set newStyleContext. + NS_ASSERTION(aPseudoType != CSSPseudoElementType::NotPseudo, + "Expecting anonymous children to all be pseudo-elements"); + // Associate the pseudo-element with the anonymous child + RefPtr<nsStyleContext> newStyleContext = + PresContext()->StyleSet()->ResolvePseudoElementStyle(mContent->AsElement(), + aPseudoType, + aParentContext, + resultElement); + + if (!aElements.AppendElement(ContentInfo(resultElement, newStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (aPseudoType == CSSPseudoElementType::mozNumberSpinDown || + aPseudoType == CSSPseudoElementType::mozNumberSpinUp) { + resultElement->SetAttr(kNameSpaceID_None, nsGkAtoms::role, + NS_LITERAL_STRING("button"), false); + } + + resultElement.forget(aResult); + return NS_OK; +} + +nsresult +nsNumberControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsresult rv; + + // We create an anonymous tree for our input element that is structured as + // follows: + // + // input + // div - outer wrapper with "display:flex" by default + // input - text input field + // div - spin box wrapping up/down arrow buttons + // div - spin up (up arrow button) + // div - spin down (down arrow button) + // + // If you change this, be careful to change the destruction order in + // nsNumberControlFrame::DestroyFrom. + + + // Create the anonymous outer wrapper: + rv = MakeAnonymousElement(getter_AddRefs(mOuterWrapper), + aElements, + nsGkAtoms::div, + CSSPseudoElementType::mozNumberWrapper, + mStyleContext); + NS_ENSURE_SUCCESS(rv, rv); + + ContentInfo& outerWrapperCI = aElements.LastElement(); + + // Create the ::-moz-number-text pseudo-element: + rv = MakeAnonymousElement(getter_AddRefs(mTextField), + outerWrapperCI.mChildren, + nsGkAtoms::input, + CSSPseudoElementType::mozNumberText, + outerWrapperCI.mStyleContext); + NS_ENSURE_SUCCESS(rv, rv); + + mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::type, + NS_LITERAL_STRING("text"), PR_FALSE); + + HTMLInputElement* content = HTMLInputElement::FromContent(mContent); + HTMLInputElement* textField = HTMLInputElement::FromContent(mTextField); + + // Initialize the text field value: + nsAutoString value; + content->GetValue(value); + SetValueOfAnonTextControl(value); + + // If we're readonly, make sure our anonymous text control is too: + nsAutoString readonly; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly)) { + mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly, false); + } + + // Propogate our tabindex: + int32_t tabIndex; + content->GetTabIndex(&tabIndex); + textField->SetTabIndex(tabIndex); + + // Initialize the text field's placeholder, if ours is set: + nsAutoString placeholder; + if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::placeholder, placeholder)) { + mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::placeholder, placeholder, false); + } + + if (mContent->AsElement()->State().HasState(NS_EVENT_STATE_FOCUS)) { + // We don't want to focus the frame but the text field. + RefPtr<FocusTextField> focusJob = new FocusTextField(mContent, mTextField); + nsContentUtils::AddScriptRunner(focusJob); + } + + if (StyleDisplay()->mAppearance == NS_THEME_TEXTFIELD) { + // The author has elected to hide the spinner by setting this + // -moz-appearance. We will reframe if it changes. + return rv; + } + + // Create the ::-moz-number-spin-box pseudo-element: + rv = MakeAnonymousElement(getter_AddRefs(mSpinBox), + outerWrapperCI.mChildren, + nsGkAtoms::div, + CSSPseudoElementType::mozNumberSpinBox, + outerWrapperCI.mStyleContext); + NS_ENSURE_SUCCESS(rv, rv); + + ContentInfo& spinBoxCI = outerWrapperCI.mChildren.LastElement(); + + // Create the ::-moz-number-spin-up pseudo-element: + rv = MakeAnonymousElement(getter_AddRefs(mSpinUp), + spinBoxCI.mChildren, + nsGkAtoms::div, + CSSPseudoElementType::mozNumberSpinUp, + spinBoxCI.mStyleContext); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the ::-moz-number-spin-down pseudo-element: + rv = MakeAnonymousElement(getter_AddRefs(mSpinDown), + spinBoxCI.mChildren, + nsGkAtoms::div, + CSSPseudoElementType::mozNumberSpinDown, + spinBoxCI.mStyleContext); + + SyncDisabledState(); + + return rv; +} + +nsIAtom* +nsNumberControlFrame::GetType() const +{ + return nsGkAtoms::numberControlFrame; +} + +void +nsNumberControlFrame::SetFocus(bool aOn, bool aRepaint) +{ + GetTextFieldFrame()->SetFocus(aOn, aRepaint); +} + +nsresult +nsNumberControlFrame::SetFormProperty(nsIAtom* aName, const nsAString& aValue) +{ + return GetTextFieldFrame()->SetFormProperty(aName, aValue); +} + +HTMLInputElement* +nsNumberControlFrame::GetAnonTextControl() +{ + return mTextField ? HTMLInputElement::FromContent(mTextField) : nullptr; +} + +/* static */ nsNumberControlFrame* +nsNumberControlFrame::GetNumberControlFrameForTextField(nsIFrame* aFrame) +{ + // If aFrame is the anon text field for an <input type=number> then we expect + // the frame of its mContent's grandparent to be that input's frame. We + // have to check for this via the content tree because we don't know whether + // extra frames will be wrapped around any of the elements between aFrame and + // the nsNumberControlFrame that we're looking for (e.g. flex wrappers). + nsIContent* content = aFrame->GetContent(); + if (content->IsInNativeAnonymousSubtree() && + content->GetParent() && content->GetParent()->GetParent()) { + nsIContent* grandparent = content->GetParent()->GetParent(); + if (grandparent->IsHTMLElement(nsGkAtoms::input) && + grandparent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::number, eCaseMatters)) { + return do_QueryFrame(grandparent->GetPrimaryFrame()); + } + } + return nullptr; +} + +/* static */ nsNumberControlFrame* +nsNumberControlFrame::GetNumberControlFrameForSpinButton(nsIFrame* aFrame) +{ + // If aFrame is a spin button for an <input type=number> then we expect the + // frame of its mContent's great-grandparent to be that input's frame. We + // have to check for this via the content tree because we don't know whether + // extra frames will be wrapped around any of the elements between aFrame and + // the nsNumberControlFrame that we're looking for (e.g. flex wrappers). + nsIContent* content = aFrame->GetContent(); + if (content->IsInNativeAnonymousSubtree() && + content->GetParent() && content->GetParent()->GetParent() && + content->GetParent()->GetParent()->GetParent()) { + nsIContent* greatgrandparent = content->GetParent()->GetParent()->GetParent(); + if (greatgrandparent->IsHTMLElement(nsGkAtoms::input) && + greatgrandparent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::number, eCaseMatters)) { + return do_QueryFrame(greatgrandparent->GetPrimaryFrame()); + } + } + return nullptr; +} + +int32_t +nsNumberControlFrame::GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const +{ + MOZ_ASSERT(aEvent->mClass == eMouseEventClass, "Unexpected event type"); + + if (!mSpinBox) { + // we don't have a spinner + return eSpinButtonNone; + } + if (aEvent->mOriginalTarget == mSpinUp) { + return eSpinButtonUp; + } + if (aEvent->mOriginalTarget == mSpinDown) { + return eSpinButtonDown; + } + if (aEvent->mOriginalTarget == mSpinBox) { + // In the case that the up/down buttons are hidden (display:none) we use + // just the spin box element, spinning up if the pointer is over the top + // half of the element, or down if it's over the bottom half. This is + // important to handle since this is the state things are in for the + // default UA style sheet. See the comment in forms.css for why. + LayoutDeviceIntPoint absPoint = aEvent->mRefPoint; + nsPoint point = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, + absPoint, mSpinBox->GetPrimaryFrame()); + if (point != nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + if (point.y < mSpinBox->GetPrimaryFrame()->GetSize().height / 2) { + return eSpinButtonUp; + } + return eSpinButtonDown; + } + } + return eSpinButtonNone; +} + +void +nsNumberControlFrame::SpinnerStateChanged() const +{ + MOZ_ASSERT(mSpinUp && mSpinDown, + "We should not be called when we have no spinner"); + + nsIFrame* spinUpFrame = mSpinUp->GetPrimaryFrame(); + if (spinUpFrame && spinUpFrame->IsThemed()) { + spinUpFrame->InvalidateFrame(); + } + nsIFrame* spinDownFrame = mSpinDown->GetPrimaryFrame(); + if (spinDownFrame && spinDownFrame->IsThemed()) { + spinDownFrame->InvalidateFrame(); + } +} + +bool +nsNumberControlFrame::SpinnerUpButtonIsDepressed() const +{ + return HTMLInputElement::FromContent(mContent)-> + NumberSpinnerUpButtonIsDepressed(); +} + +bool +nsNumberControlFrame::SpinnerDownButtonIsDepressed() const +{ + return HTMLInputElement::FromContent(mContent)-> + NumberSpinnerDownButtonIsDepressed(); +} + +bool +nsNumberControlFrame::IsFocused() const +{ + // Normally this depends on the state of our anonymous text control (which + // takes focus for us), but in the case that it does not have a frame we will + // have focus ourself. + return mTextField->AsElement()->State().HasState(NS_EVENT_STATE_FOCUS) || + mContent->AsElement()->State().HasState(NS_EVENT_STATE_FOCUS); +} + +void +nsNumberControlFrame::HandleFocusEvent(WidgetEvent* aEvent) +{ + if (aEvent->mOriginalTarget != mTextField) { + // Move focus to our text field + RefPtr<HTMLInputElement> textField = HTMLInputElement::FromContent(mTextField); + textField->Focus(); + } +} + +nsresult +nsNumberControlFrame::HandleSelectCall() +{ + RefPtr<HTMLInputElement> textField = HTMLInputElement::FromContent(mTextField); + return textField->Select(); +} + +#define STYLES_DISABLING_NATIVE_THEMING \ + NS_AUTHOR_SPECIFIED_BACKGROUND | \ + NS_AUTHOR_SPECIFIED_PADDING | \ + NS_AUTHOR_SPECIFIED_BORDER + +bool +nsNumberControlFrame::ShouldUseNativeStyleForSpinner() const +{ + MOZ_ASSERT(mSpinUp && mSpinDown, + "We should not be called when we have no spinner"); + + nsIFrame* spinUpFrame = mSpinUp->GetPrimaryFrame(); + nsIFrame* spinDownFrame = mSpinDown->GetPrimaryFrame(); + + return spinUpFrame && + spinUpFrame->StyleDisplay()->mAppearance == NS_THEME_SPINNER_UPBUTTON && + !PresContext()->HasAuthorSpecifiedRules(spinUpFrame, + STYLES_DISABLING_NATIVE_THEMING) && + spinDownFrame && + spinDownFrame->StyleDisplay()->mAppearance == NS_THEME_SPINNER_DOWNBUTTON && + !PresContext()->HasAuthorSpecifiedRules(spinDownFrame, + STYLES_DISABLING_NATIVE_THEMING); +} + +void +nsNumberControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + // Only one direct anonymous child: + if (mOuterWrapper) { + aElements.AppendElement(mOuterWrapper); + } +} + +void +nsNumberControlFrame::SetValueOfAnonTextControl(const nsAString& aValue) +{ + if (mHandlingInputEvent) { + // We have been called while our HTMLInputElement is processing a DOM + // 'input' event targeted at our anonymous text control. Our + // HTMLInputElement has taken the value of our anon text control and + // called SetValueInternal on itself to keep its own value in sync. As a + // result SetValueInternal has called us. In this one case we do not want + // to update our anon text control, especially since aValue will be the + // sanitized value, and only the internal value should be sanitized (not + // the value shown to the user, and certainly we shouldn't change it as + // they type). + return; + } + + // Init to aValue so that we set aValue as the value of our text control if + // aValue isn't a valid number (in which case the HTMLInputElement's validity + // state will be set to invalid) or if aValue can't be localized: + nsAutoString localizedValue(aValue); + +#ifdef ENABLE_INTL_API + // Try and localize the value we will set: + Decimal val = HTMLInputElement::StringToDecimal(aValue); + if (val.isFinite()) { + ICUUtils::LanguageTagIterForContent langTagIter(mContent); + ICUUtils::LocalizeNumber(val.toDouble(), langTagIter, localizedValue); + } +#endif + + // We need to update the value of our anonymous text control here. Note that + // this must be its value, and not its 'value' attribute (the default value), + // since the default value is ignored once a user types into the text + // control. + HTMLInputElement::FromContent(mTextField)->SetValue(localizedValue); +} + +void +nsNumberControlFrame::GetValueOfAnonTextControl(nsAString& aValue) +{ + if (!mTextField) { + aValue.Truncate(); + return; + } + + HTMLInputElement::FromContent(mTextField)->GetValue(aValue); + +#ifdef ENABLE_INTL_API + // Here we need to de-localize any number typed in by the user. That is, we + // need to convert it from the number format of the user's language, region, + // etc. to the format that the HTML 5 spec defines to be a "valid + // floating-point number": + // + // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers + // + // This is necessary to allow the number that we return to be parsed by + // functions like HTMLInputElement::StringToDecimal (the HTML-5-conforming + // parsing function) which don't know how to handle numbers that are + // formatted differently (for example, with non-ASCII digits, with grouping + // separator characters or with a decimal separator character other than + // '.'). + + ICUUtils::LanguageTagIterForContent langTagIter(mContent); + double value = ICUUtils::ParseNumber(aValue, langTagIter); + if (!IsFinite(value)) { + aValue.Truncate(); + return; + } + if (value == HTMLInputElement::StringToDecimal(aValue).toDouble()) { + // We want to preserve the formatting of the number as typed in by the user + // whenever possible. Since the localized serialization parses to the same + // number as the de-localized serialization, we can do that. This helps + // prevent normalization of input such as "2e2" (which would otherwise be + // converted to "200"). Content relies on this. + // + // Typically we will only get here for locales in which numbers are + // formatted in the same way as they are for HTML5's "valid floating-point + // number" format. + return; + } + // We can't preserve the formatting, otherwise functions such as + // HTMLInputElement::StringToDecimal would incorrectly process the number + // input by the user. For example, "12.345" with lang=de de-localizes as + // 12345, but HTMLInputElement::StringToDecimal would mistakenly parse it as + // 12.345. Another example would be "12,345" with lang=de which de-localizes + // as 12.345, but HTMLInputElement::StringToDecimal would parse it to NaN. + aValue.Truncate(); + aValue.AppendFloat(value); +#endif +} + +bool +nsNumberControlFrame::AnonTextControlIsEmpty() +{ + if (!mTextField) { + return true; + } + nsAutoString value; + HTMLInputElement::FromContent(mTextField)->GetValue(value); + return value.IsEmpty(); +} + +Element* +nsNumberControlFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::mozNumberWrapper) { + return mOuterWrapper; + } + + if (aType == CSSPseudoElementType::mozNumberText) { + return mTextField; + } + + if (aType == CSSPseudoElementType::mozNumberSpinBox) { + // Might be null. + return mSpinBox; + } + + if (aType == CSSPseudoElementType::mozNumberSpinUp) { + // Might be null. + return mSpinUp; + } + + if (aType == CSSPseudoElementType::mozNumberSpinDown) { + // Might be null. + return mSpinDown; + } + + return nsContainerFrame::GetPseudoElement(aType); +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsNumberControlFrame::AccessibleType() +{ + return a11y::eHTMLSpinnerType; +} +#endif diff --git a/layout/forms/nsNumberControlFrame.h b/layout/forms/nsNumberControlFrame.h new file mode 100644 index 000000000..47e32ad65 --- /dev/null +++ b/layout/forms/nsNumberControlFrame.h @@ -0,0 +1,216 @@ +/* -*- 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/. */ + +#ifndef nsNumberControlFrame_h__ +#define nsNumberControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +class nsITextControlFrame; +class nsPresContext; + +namespace mozilla { +enum class CSSPseudoElementType : uint8_t; +class WidgetEvent; +class WidgetGUIEvent; +namespace dom { +class HTMLInputElement; +} // namespace dom +} // namespace mozilla + +/** + * This frame type is used for <input type=number>. + */ +class nsNumberControlFrame final : public nsContainerFrame + , public nsIAnonymousContentCreator + , public nsIFormControlFrame +{ + friend nsIFrame* + NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext); + + typedef mozilla::CSSPseudoElementType CSSPseudoElementType; + typedef mozilla::dom::Element Element; + typedef mozilla::dom::HTMLInputElement HTMLInputElement; + typedef mozilla::WidgetEvent WidgetEvent; + typedef mozilla::WidgetGUIEvent WidgetGUIEvent; + + explicit nsNumberControlFrame(nsStyleContext* aContext); + +public: + NS_DECL_QUERYFRAME_TARGET(nsNumberControlFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + virtual void ContentStatesChanged(mozilla::EventStates aStates) override; + virtual bool IsLeaf() const override { return true; } + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + virtual nscoord GetMinISize(nsRenderingContext* aRenderingContext) override; + + virtual nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override; + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("NumberControl"), aResult); + } +#endif + + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // nsIFormControlFrame + virtual void SetFocus(bool aOn, bool aRepaint) override; + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + + /** + * This method attempts to localizes aValue and then sets the result as the + * value of our anonymous text control. It's called when our + * HTMLInputElement's value changes, when we need to sync up the value + * displayed in our anonymous text control. + */ + void SetValueOfAnonTextControl(const nsAString& aValue); + + /** + * This method gets the string value of our anonymous text control, + * attempts to normalizes (de-localizes) it, then sets the outparam aValue to + * the result. It's called when user input changes the text value of our + * anonymous text control so that we can sync up the internal value of our + * HTMLInputElement. + */ + void GetValueOfAnonTextControl(nsAString& aValue); + + bool AnonTextControlIsEmpty(); + + /** + * Called to notify this frame that its HTMLInputElement is currently + * processing a DOM 'input' event. + */ + void HandlingInputEvent(bool aHandlingEvent) + { + mHandlingInputEvent = aHandlingEvent; + } + + HTMLInputElement* GetAnonTextControl(); + + /** + * If the frame is the frame for an nsNumberControlFrame's anonymous text + * field, returns the nsNumberControlFrame. Else returns nullptr. + */ + static nsNumberControlFrame* GetNumberControlFrameForTextField(nsIFrame* aFrame); + + /** + * If the frame is the frame for an nsNumberControlFrame's up or down spin + * button, returns the nsNumberControlFrame. Else returns nullptr. + */ + static nsNumberControlFrame* GetNumberControlFrameForSpinButton(nsIFrame* aFrame); + + enum SpinButtonEnum { + eSpinButtonNone, + eSpinButtonUp, + eSpinButtonDown + }; + + /** + * Returns one of the SpinButtonEnum values to depending on whether the + * pointer event is over the spin-up button, the spin-down button, or + * neither. + */ + int32_t GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const; + + void SpinnerStateChanged() const; + + bool SpinnerUpButtonIsDepressed() const; + bool SpinnerDownButtonIsDepressed() const; + + bool IsFocused() const; + + void HandleFocusEvent(WidgetEvent* aEvent); + + /** + * Our element had HTMLInputElement::Select() called on it. + */ + nsresult HandleSelectCall(); + + virtual Element* GetPseudoElement(CSSPseudoElementType aType) override; + + bool ShouldUseNativeStyleForSpinner() const; + +private: + + nsITextControlFrame* GetTextFieldFrame(); + nsresult MakeAnonymousElement(Element** aResult, + nsTArray<ContentInfo>& aElements, + nsIAtom* aTagName, + CSSPseudoElementType aPseudoType, + nsStyleContext* aParentContext); + + class SyncDisabledStateEvent; + friend class SyncDisabledStateEvent; + class SyncDisabledStateEvent : public mozilla::Runnable + { + public: + explicit SyncDisabledStateEvent(nsNumberControlFrame* aFrame) + : mFrame(aFrame) + {} + + NS_IMETHOD Run() override + { + nsNumberControlFrame* frame = + static_cast<nsNumberControlFrame*>(mFrame.GetFrame()); + NS_ENSURE_STATE(frame); + + frame->SyncDisabledState(); + return NS_OK; + } + + private: + nsWeakFrame mFrame; + }; + + /** + * Sync the disabled state of the anonymous children up with our content's. + */ + void SyncDisabledState(); + + /** + * The text field used to edit and show the number. + * @see nsNumberControlFrame::CreateAnonymousContent. + */ + nsCOMPtr<Element> mOuterWrapper; + nsCOMPtr<Element> mTextField; + nsCOMPtr<Element> mSpinBox; + nsCOMPtr<Element> mSpinUp; + nsCOMPtr<Element> mSpinDown; + bool mHandlingInputEvent; +}; + +#endif // nsNumberControlFrame_h__ diff --git a/layout/forms/nsProgressFrame.cpp b/layout/forms/nsProgressFrame.cpp new file mode 100644 index 000000000..2445defd3 --- /dev/null +++ b/layout/forms/nsProgressFrame.cpp @@ -0,0 +1,308 @@ +/* -*- 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 "nsProgressFrame.h" + +#include "nsIContent.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsFormControlFrame.h" +#include "nsFontMetrics.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLProgressElement.h" +#include "nsContentList.h" +#include "nsCSSPseudoElements.h" +#include "nsStyleSet.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsThemeConstants.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* +NS_NewProgressFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsProgressFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsProgressFrame) + +nsProgressFrame::nsProgressFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) + , mBarDiv(nullptr) +{ +} + +nsProgressFrame::~nsProgressFrame() +{ +} + +void +nsProgressFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContentUtils::DestroyAnonymousContent(&mBarDiv); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +nsIAtom* +nsProgressFrame::GetType() const +{ + return nsGkAtoms::progressFrame; +} + +nsresult +nsProgressFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + // Create the progress bar div. + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate ::-moz-progress-bar pseudo-element to the anonymous child. + CSSPseudoElementType pseudoType = CSSPseudoElementType::mozProgressBar; + RefPtr<nsStyleContext> newStyleContext = PresContext()->StyleSet()-> + ResolvePseudoElementStyle(mContent->AsElement(), pseudoType, + StyleContext(), mBarDiv->AsElement()); + + if (!aElements.AppendElement(ContentInfo(mBarDiv, newStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void +nsProgressFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mBarDiv) { + aElements.AppendElement(mBarDiv); + } +} + +NS_QUERYFRAME_HEAD(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + + +void +nsProgressFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + BuildDisplayListForInline(aBuilder, aDirtyRect, aLists); +} + +void +nsProgressFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsProgressFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + NS_ASSERTION(PrincipalChildList().GetLength() == 1 && + PrincipalChildList().FirstChild() == mBarDiv->GetPrimaryFrame(), + "unexpected child frames"); + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + aDesiredSize.SetSize(aReflowInput.GetWritingMode(), + aReflowInput.ComputedSizeWithBorderPadding()); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + for (auto childFrame : PrincipalChildList()) { + ReflowChildFrame(childFrame, aPresContext, aReflowInput, aStatus); + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, childFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +void +nsProgressFrame::ReflowChildFrame(nsIFrame* aChild, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + bool vertical = ResolvedOrientationIsVertical(); + WritingMode wm = aChild->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput reflowInput(aPresContext, aReflowInput, aChild, availSize); + nscoord size = vertical ? aReflowInput.ComputedHeight() + : aReflowInput.ComputedWidth(); + nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left; + nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top; + + double position = static_cast<HTMLProgressElement*>(mContent)->Position(); + + // Force the bar's size to match the current progress. + // When indeterminate, the progress' size will be 100%. + if (position >= 0.0) { + size *= position; + } + + if (!vertical && (wm.IsVertical() ? wm.IsVerticalRL() : !wm.IsBidiLTR())) { + xoffset += aReflowInput.ComputedWidth() - size; + } + + // The bar size is fixed in these cases: + // - the progress position is determined: the bar size is fixed according + // to it's value. + // - the progress position is indeterminate and the bar appearance should be + // shown as native: the bar size is forced to 100%. + // Otherwise (when the progress is indeterminate and the bar appearance isn't + // native), the bar size isn't fixed and can be set by the author. + if (position != -1 || ShouldUseNativeStyle()) { + if (vertical) { + // We want the bar to begin at the bottom. + yoffset += aReflowInput.ComputedHeight() - size; + + size -= reflowInput.ComputedPhysicalMargin().TopBottom() + + reflowInput.ComputedPhysicalBorderPadding().TopBottom(); + size = std::max(size, 0); + reflowInput.SetComputedHeight(size); + } else { + size -= reflowInput.ComputedPhysicalMargin().LeftRight() + + reflowInput.ComputedPhysicalBorderPadding().LeftRight(); + size = std::max(size, 0); + reflowInput.SetComputedWidth(size); + } + } else if (vertical) { + // For vertical progress bars, we need to position the bar specificly when + // the width isn't constrained (position == -1 and !ShouldUseNativeStyle()) + // because aReflowInput.ComputedHeight() - size == 0. + yoffset += aReflowInput.ComputedHeight() - reflowInput.ComputedHeight(); + } + + xoffset += reflowInput.ComputedPhysicalMargin().left; + yoffset += reflowInput.ComputedPhysicalMargin().top; + + ReflowOutput barDesiredSize(aReflowInput); + ReflowChild(aChild, aPresContext, barDesiredSize, reflowInput, xoffset, + yoffset, 0, aStatus); + FinishReflowChild(aChild, aPresContext, barDesiredSize, &reflowInput, + xoffset, yoffset, 0); +} + +nsresult +nsProgressFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max)) { + auto shell = PresContext()->PresShell(); + for (auto childFrame : PrincipalChildList()) { + shell->FrameNeedsReflow(childFrame, nsIPresShell::eResize, + NS_FRAME_IS_DIRTY); + } + InvalidateFrame(); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +LogicalSize +nsProgressFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + autoSize.BSize(wm) = autoSize.ISize(wm) = + NSToCoordRound(StyleFont()->mFont.size * + nsLayoutUtils::FontSizeInflationFor(this)); // 1em + + if (ResolvedOrientationIsVertical() == wm.IsVertical()) { + autoSize.ISize(wm) *= 10; // 10em + } else { + autoSize.BSize(wm) *= 10; // 10em + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord +nsProgressFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + nscoord minISize = fontMet->Font().size; // 1em + + if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) { + // The orientation is inline + minISize *= 10; // 10em + } + + return minISize; +} + +nscoord +nsProgressFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + return GetMinISize(aRenderingContext); +} + +bool +nsProgressFrame::ShouldUseNativeStyle() const +{ + nsIFrame* barFrame = PrincipalChildList().FirstChild(); + + // Use the native style if these conditions are satisfied: + // - both frames use the native appearance; + // - neither frame has author specified rules setting the border or the + // background. + return StyleDisplay()->mAppearance == NS_THEME_PROGRESSBAR && + !PresContext()->HasAuthorSpecifiedRules(this, + NS_AUTHOR_SPECIFIED_BORDER | NS_AUTHOR_SPECIFIED_BACKGROUND) && + barFrame && + barFrame->StyleDisplay()->mAppearance == NS_THEME_PROGRESSCHUNK && + !PresContext()->HasAuthorSpecifiedRules(barFrame, + NS_AUTHOR_SPECIFIED_BORDER | NS_AUTHOR_SPECIFIED_BACKGROUND); +} + +Element* +nsProgressFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::mozProgressBar) { + return mBarDiv; + } + + return nsContainerFrame::GetPseudoElement(aType); +} diff --git a/layout/forms/nsProgressFrame.h b/layout/forms/nsProgressFrame.h new file mode 100644 index 000000000..01465ff7c --- /dev/null +++ b/layout/forms/nsProgressFrame.h @@ -0,0 +1,102 @@ +/* -*- 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/. */ + +#ifndef nsProgressFrame_h___ +#define nsProgressFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +enum class CSSPseudoElementType : uint8_t; +} // namespace mozilla + +class nsProgressFrame : public nsContainerFrame, + public nsIAnonymousContentCreator +{ + typedef mozilla::CSSPseudoElementType CSSPseudoElementType; + typedef mozilla::dom::Element Element; + +public: + NS_DECL_QUERYFRAME_TARGET(nsProgressFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + explicit nsProgressFrame(nsStyleContext* aContext); + virtual ~nsProgressFrame(); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsIAtom* GetType() const override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("Progress"), aResult); + } +#endif + + virtual bool IsLeaf() const override { return true; } + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize + ComputeAutoSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + virtual Element* GetPseudoElement(CSSPseudoElementType aType) override; + +protected: + // Helper function to reflow a child frame. + void ReflowChildFrame(nsIFrame* aChild, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus); + + /** + * The div used to show the progress bar. + * @see nsProgressFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mBarDiv; +}; + +#endif diff --git a/layout/forms/nsRangeFrame.cpp b/layout/forms/nsRangeFrame.cpp new file mode 100644 index 000000000..7590da066 --- /dev/null +++ b/layout/forms/nsRangeFrame.cpp @@ -0,0 +1,949 @@ +/* -*- 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 "nsRangeFrame.h" + +#include "mozilla/EventStates.h" +#include "mozilla/TouchEvents.h" + +#include "nsContentCreatorFunctions.h" +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsCSSPseudoElements.h" +#include "nsCSSRendering.h" +#include "nsFormControlFrame.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsNameSpaceManager.h" +#include "nsIPresShell.h" +#include "nsGkAtoms.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsPresContext.h" +#include "nsNodeInfoManager.h" +#include "nsRenderingContext.h" +#include "mozilla/dom/Element.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "nsThemeConstants.h" + +#ifdef ACCESSIBILITY +#include "nsAccessibilityService.h" +#endif + +#define LONG_SIDE_TO_SHORT_SIDE_RATIO 10 + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::image; + +NS_IMPL_ISUPPORTS(nsRangeFrame::DummyTouchListener, nsIDOMEventListener) + +nsIFrame* +NS_NewRangeFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsRangeFrame(aContext); +} + +nsRangeFrame::nsRangeFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) +{ +} + +nsRangeFrame::~nsRangeFrame() +{ +#ifdef DEBUG + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameRelease(); + } +#endif +} + +NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame) + +NS_QUERYFRAME_HEAD(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void +nsRangeFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) +{ + // With APZ enabled, touch events may be handled directly by the APZC code + // if the APZ knows that there is no content interested in the touch event. + // The range input element *is* interested in touch events, but doesn't use + // the usual mechanism (i.e. registering an event listener) to handle touch + // input. Instead, we do it here so that the APZ finds out about it, and + // makes sure to wait for content to run handlers before handling the touch + // input itself. + if (!mDummyTouchListener) { + mDummyTouchListener = new DummyTouchListener(); + } + aContent->AddEventListener(NS_LITERAL_STRING("touchstart"), mDummyTouchListener, false); + + StyleSetHandle styleSet = PresContext()->StyleSet(); + + mOuterFocusStyle = + styleSet->ProbePseudoElementStyle(aContent->AsElement(), + CSSPseudoElementType::mozFocusOuter, + StyleContext()); + + return nsContainerFrame::Init(aContent, aParent, aPrevInFlow); +} + +void +nsRangeFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + mContent->RemoveEventListener(NS_LITERAL_STRING("touchstart"), mDummyTouchListener, false); + + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + nsContentUtils::DestroyAnonymousContent(&mTrackDiv); + nsContentUtils::DestroyAnonymousContent(&mProgressDiv); + nsContentUtils::DestroyAnonymousContent(&mThumbDiv); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +nsresult +nsRangeFrame::MakeAnonymousDiv(Element** aResult, + CSSPseudoElementType aPseudoType, + nsTArray<ContentInfo>& aElements) +{ + nsCOMPtr<nsIDocument> doc = mContent->GetComposedDoc(); + RefPtr<Element> resultElement = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate the pseudo-element with the anonymous child. + RefPtr<nsStyleContext> newStyleContext = + PresContext()->StyleSet()->ResolvePseudoElementStyle(mContent->AsElement(), + aPseudoType, + StyleContext(), + resultElement); + + if (!aElements.AppendElement(ContentInfo(resultElement, newStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + resultElement.forget(aResult); + return NS_OK; +} + +nsresult +nsRangeFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + nsresult rv; + + // Create the ::-moz-range-track pseuto-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mTrackDiv), + CSSPseudoElementType::mozRangeTrack, + aElements); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the ::-moz-range-progress pseudo-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mProgressDiv), + CSSPseudoElementType::mozRangeProgress, + aElements); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the ::-moz-range-thumb pseudo-element (a div): + rv = MakeAnonymousDiv(getter_AddRefs(mThumbDiv), + CSSPseudoElementType::mozRangeThumb, + aElements); + return rv; +} + +void +nsRangeFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mTrackDiv) { + aElements.AppendElement(mTrackDiv); + } + + if (mProgressDiv) { + aElements.AppendElement(mProgressDiv); + } + + if (mThumbDiv) { + aElements.AppendElement(mThumbDiv); + } +} + +class nsDisplayRangeFocusRing : public nsDisplayItem +{ +public: + nsDisplayRangeFocusRing(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayRangeFocusRing); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayRangeFocusRing() { + MOZ_COUNT_DTOR(nsDisplayRangeFocusRing); + } +#endif + + nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override; + void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion *aInvalidRegion) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) override; + virtual void Paint(nsDisplayListBuilder* aBuilder, nsRenderingContext* aCtx) override; + NS_DISPLAY_DECL_NAME("RangeFocusRing", TYPE_RANGE_FOCUS_RING) +}; + +nsDisplayItemGeometry* +nsDisplayRangeFocusRing::AllocateGeometry(nsDisplayListBuilder* aBuilder) +{ + return new nsDisplayItemGenericImageGeometry(this, aBuilder); +} + +void +nsDisplayRangeFocusRing::ComputeInvalidationRegion( + nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion* aInvalidRegion) +{ + auto geometry = + static_cast<const nsDisplayItemGenericImageGeometry*>(aGeometry); + + if (aBuilder->ShouldSyncDecodeImages() && + geometry->ShouldInvalidateToSyncDecodeImages()) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, aInvalidRegion); +} + +nsRect +nsDisplayRangeFocusRing::GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) +{ + *aSnap = false; + nsRect rect(ToReferenceFrame(), Frame()->GetSize()); + + // We want to paint as if specifying a border for ::-moz-focus-outer + // specifies an outline for our frame, so inflate by the border widths: + nsStyleContext* styleContext = + static_cast<nsRangeFrame*>(mFrame)->mOuterFocusStyle; + MOZ_ASSERT(styleContext, "We only exist if mOuterFocusStyle is non-null"); + rect.Inflate(styleContext->StyleBorder()->GetComputedBorder()); + + return rect; +} + +void +nsDisplayRangeFocusRing::Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) +{ + bool unused; + nsStyleContext* styleContext = + static_cast<nsRangeFrame*>(mFrame)->mOuterFocusStyle; + MOZ_ASSERT(styleContext, "We only exist if mOuterFocusStyle is non-null"); + + PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SYNC_DECODE_IMAGES + : PaintBorderFlags(); + + DrawResult result = + nsCSSRendering::PaintBorder(mFrame->PresContext(), *aCtx, mFrame, + mVisibleRect, GetBounds(aBuilder, &unused), + styleContext, flags); + + nsDisplayItemGenericImageGeometry::UpdateDrawResult(this, result); +} + +void +nsRangeFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + if (IsThemed()) { + DisplayBorderBackgroundOutline(aBuilder, aLists); + // Only create items for the thumb. Specifically, we do not want + // the track to paint, since *our* background is used to paint + // the track, and we don't want the unthemed track painting over + // the top of the themed track. + // This logic is copied from + // nsContainerFrame::BuildDisplayListForNonBlockChildren as + // called by BuildDisplayListForInline. + nsIFrame* thumb = mThumbDiv->GetPrimaryFrame(); + if (thumb) { + nsDisplayListSet set(aLists, aLists.Content()); + BuildDisplayListForChild(aBuilder, thumb, aDirtyRect, set, DISPLAY_CHILD_INLINE); + } + } else { + BuildDisplayListForInline(aBuilder, aDirtyRect, aLists); + } + + // Draw a focus outline if appropriate: + + if (!aBuilder->IsForPainting() || + !IsVisibleForPainting(aBuilder)) { + // we don't want the focus ring item for hit-testing or if the item isn't + // in the area being [re]painted + return; + } + + EventStates eventStates = mContent->AsElement()->State(); + if (eventStates.HasState(NS_EVENT_STATE_DISABLED) || + !eventStates.HasState(NS_EVENT_STATE_FOCUSRING)) { + return; // can't have focus or doesn't match :-moz-focusring + } + + if (!mOuterFocusStyle || + !mOuterFocusStyle->StyleBorder()->HasBorder()) { + // no ::-moz-focus-outer specified border (how style specifies a focus ring + // for range) + return; + } + + const nsStyleDisplay *disp = StyleDisplay(); + if (IsThemed(disp) && + PresContext()->GetTheme()->ThemeDrawsFocusForWidget(disp->mAppearance)) { + return; // the native theme displays its own visual indication of focus + } + + aLists.Content()->AppendNewToTop( + new (aBuilder) nsDisplayRangeFocusRing(aBuilder, this)); +} + +void +nsRangeFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsRangeFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + NS_ASSERTION(mTrackDiv, "::-moz-range-track div must exist!"); + NS_ASSERTION(mProgressDiv, "::-moz-range-progress div must exist!"); + NS_ASSERTION(mThumbDiv, "::-moz-range-thumb div must exist!"); + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord computedBSize = aReflowInput.ComputedBSize(); + if (computedBSize == NS_AUTOHEIGHT) { + computedBSize = 0; + } + LogicalSize + finalSize(wm, + aReflowInput.ComputedISize() + + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(wm), + computedBSize + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(wm)); + aDesiredSize.SetSize(wm, finalSize); + + ReflowAnonymousContent(aPresContext, aDesiredSize, aReflowInput); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + if (trackFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, trackFrame); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + if (rangeProgressFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, rangeProgressFrame); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, thumbFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +void +nsRangeFrame::ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput) +{ + // The width/height of our content box, which is the available width/height + // for our anonymous content: + nscoord rangeFrameContentBoxWidth = aReflowInput.ComputedWidth(); + nscoord rangeFrameContentBoxHeight = aReflowInput.ComputedHeight(); + if (rangeFrameContentBoxHeight == NS_AUTOHEIGHT) { + rangeFrameContentBoxHeight = 0; + } + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + + if (trackFrame) { // display:none? + + // Position the track: + // The idea here is that we allow content authors to style the width, + // height, border and padding of the track, but we ignore margin and + // positioning properties and do the positioning ourself to keep the center + // of the track's border box on the center of the nsRangeFrame's content + // box. + + WritingMode wm = trackFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput trackReflowInput(aPresContext, aReflowInput, + trackFrame, availSize); + + // Find the x/y position of the track frame such that it will be positioned + // as described above. These coordinates are with respect to the + // nsRangeFrame's border-box. + nscoord trackX = rangeFrameContentBoxWidth / 2; + nscoord trackY = rangeFrameContentBoxHeight / 2; + + // Account for the track's border and padding (we ignore its margin): + trackX -= trackReflowInput.ComputedPhysicalBorderPadding().left + + trackReflowInput.ComputedWidth() / 2; + trackY -= trackReflowInput.ComputedPhysicalBorderPadding().top + + trackReflowInput.ComputedHeight() / 2; + + // Make relative to our border box instead of our content box: + trackX += aReflowInput.ComputedPhysicalBorderPadding().left; + trackY += aReflowInput.ComputedPhysicalBorderPadding().top; + + nsReflowStatus frameStatus; + ReflowOutput trackDesiredSize(aReflowInput); + ReflowChild(trackFrame, aPresContext, trackDesiredSize, + trackReflowInput, trackX, trackY, 0, frameStatus); + MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(frameStatus), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(trackFrame, aPresContext, trackDesiredSize, + &trackReflowInput, trackX, trackY, 0); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + if (thumbFrame) { // display:none? + WritingMode wm = thumbFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput thumbReflowInput(aPresContext, aReflowInput, + thumbFrame, availSize); + + // Where we position the thumb depends on its size, so we first reflow + // the thumb at {0,0} to obtain its size, then position it afterwards. + + nsReflowStatus frameStatus; + ReflowOutput thumbDesiredSize(aReflowInput); + ReflowChild(thumbFrame, aPresContext, thumbDesiredSize, + thumbReflowInput, 0, 0, 0, frameStatus); + MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(frameStatus), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(thumbFrame, aPresContext, thumbDesiredSize, + &thumbReflowInput, 0, 0, 0); + DoUpdateThumbPosition(thumbFrame, nsSize(aDesiredSize.Width(), + aDesiredSize.Height())); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + + if (rangeProgressFrame) { // display:none? + WritingMode wm = rangeProgressFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput progressReflowInput(aPresContext, aReflowInput, + rangeProgressFrame, availSize); + + // We first reflow the range-progress frame at {0,0} to obtain its + // unadjusted dimensions, then we adjust it to so that the appropriate edge + // ends at the thumb. + + nsReflowStatus frameStatus; + ReflowOutput progressDesiredSize(aReflowInput); + ReflowChild(rangeProgressFrame, aPresContext, + progressDesiredSize, progressReflowInput, 0, 0, + 0, frameStatus); + MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(frameStatus), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(rangeProgressFrame, aPresContext, + progressDesiredSize, &progressReflowInput, 0, 0, 0); + DoUpdateRangeProgressFrame(rangeProgressFrame, nsSize(aDesiredSize.Width(), + aDesiredSize.Height())); + } +} + +#ifdef ACCESSIBILITY +a11y::AccType +nsRangeFrame::AccessibleType() +{ + return a11y::eHTMLRangeType; +} +#endif + +double +nsRangeFrame::GetValueAsFractionOfRange() +{ + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + dom::HTMLInputElement* input = static_cast<dom::HTMLInputElement*>(mContent); + + MOZ_ASSERT(input->GetType() == NS_FORM_INPUT_RANGE); + + Decimal value = input->GetValueAsDecimal(); + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); + + MOZ_ASSERT(value.isFinite() && minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + + if (maximum <= minimum) { + MOZ_ASSERT(value == minimum, "Unsanitized value"); + return 0.0; + } + + MOZ_ASSERT(value >= minimum && value <= maximum, "Unsanitized value"); + + return ((value - minimum) / (maximum - minimum)).toDouble(); +} + +Decimal +nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent* aEvent) +{ + MOZ_ASSERT(aEvent->mClass == eMouseEventClass || + aEvent->mClass == eTouchEventClass, + "Unexpected event type - aEvent->mRefPoint may be meaningless"); + + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + dom::HTMLInputElement* input = static_cast<dom::HTMLInputElement*>(mContent); + + MOZ_ASSERT(input->GetType() == NS_FORM_INPUT_RANGE); + + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + if (maximum <= minimum) { + return minimum; + } + Decimal range = maximum - minimum; + + LayoutDeviceIntPoint absPoint; + if (aEvent->mClass == eTouchEventClass) { + MOZ_ASSERT(aEvent->AsTouchEvent()->mTouches.Length() == 1, + "Unexpected number of mTouches"); + absPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint; + } else { + absPoint = aEvent->mRefPoint; + } + nsPoint point = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, absPoint, this); + + if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + // We don't want to change the current value for this error state. + return static_cast<dom::HTMLInputElement*>(mContent)->GetValueAsDecimal(); + } + + nsRect rangeContentRect = GetContentRectRelativeToSelf(); + nsSize thumbSize; + + if (IsThemed()) { + // We need to get the size of the thumb from the theme. + nsPresContext *presContext = PresContext(); + bool notUsedCanOverride; + LayoutDeviceIntSize size; + presContext->GetTheme()-> + GetMinimumWidgetSize(presContext, this, NS_THEME_RANGE_THUMB, &size, + ¬UsedCanOverride); + thumbSize.width = presContext->DevPixelsToAppUnits(size.width); + thumbSize.height = presContext->DevPixelsToAppUnits(size.height); + MOZ_ASSERT(thumbSize.width > 0 && thumbSize.height > 0); + } else { + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { // diplay:none? + thumbSize = thumbFrame->GetSize(); + } + } + + Decimal fraction; + if (IsHorizontal()) { + nscoord traversableDistance = rangeContentRect.width - thumbSize.width; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.x + thumbSize.width/2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.x, posAtStart, posAtEnd); + fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + if (IsRightToLeft()) { + fraction = Decimal(1) - fraction; + } + } else { + nscoord traversableDistance = rangeContentRect.height - thumbSize.height; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.y + thumbSize.height/2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.y, posAtStart, posAtEnd); + // For a vertical range, the top (posAtStart) is the highest value, so we + // subtract the fraction from 1.0 to get that polarity correct. + fraction = Decimal(1) - Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + } + + MOZ_ASSERT(fraction >= Decimal(0) && fraction <= Decimal(1)); + return minimum + fraction * range; +} + +void +nsRangeFrame::UpdateForValueChange() +{ + if (NS_SUBTREE_DIRTY(this)) { + return; // we're going to be updated when we reflow + } + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (!rangeProgressFrame && !thumbFrame) { + return; // diplay:none? + } + if (rangeProgressFrame) { + DoUpdateRangeProgressFrame(rangeProgressFrame, GetSize()); + } + if (thumbFrame) { + DoUpdateThumbPosition(thumbFrame, GetSize()); + } + if (IsThemed()) { + // We don't know the exact dimensions or location of the thumb when native + // theming is applied, so we just repaint the entire range. + InvalidateFrame(); + } + +#ifdef ACCESSIBILITY + nsAccessibilityService* accService = nsIPresShell::AccService(); + if (accService) { + accService->RangeValueChanged(PresContext()->PresShell(), mContent); + } +#endif + + SchedulePaint(); +} + +void +nsRangeFrame::DoUpdateThumbPosition(nsIFrame* aThumbFrame, + const nsSize& aRangeSize) +{ + MOZ_ASSERT(aThumbFrame); + + // The idea here is that we want to position the thumb so that the center + // of the thumb is on an imaginary line drawn from the middle of one edge + // of the range frame's content box to the middle of the opposite edge of + // its content box (the opposite edges being the left/right edge if the + // range is horizontal, or else the top/bottom edges if the range is + // vertical). How far along this line the center of the thumb is placed + // depends on the value of the range. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsPoint newPosition(borderAndPadding.left, borderAndPadding.top); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + nsSize thumbSize = aThumbFrame->GetSize(); + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + if (thumbSize.width < rangeContentBoxSize.width) { + nscoord traversableDistance = + rangeContentBoxSize.width - thumbSize.width; + if (IsRightToLeft()) { + newPosition.x += NSToCoordRound((1.0 - fraction) * traversableDistance); + } else { + newPosition.x += NSToCoordRound(fraction * traversableDistance); + } + newPosition.y += (rangeContentBoxSize.height - thumbSize.height)/2; + } + } else { + if (thumbSize.height < rangeContentBoxSize.height) { + nscoord traversableDistance = + rangeContentBoxSize.height - thumbSize.height; + newPosition.x += (rangeContentBoxSize.width - thumbSize.width)/2; + newPosition.y += NSToCoordRound((1.0 - fraction) * traversableDistance); + } + } + aThumbFrame->SetPosition(newPosition); +} + +void +nsRangeFrame::DoUpdateRangeProgressFrame(nsIFrame* aRangeProgressFrame, + const nsSize& aRangeSize) +{ + MOZ_ASSERT(aRangeProgressFrame); + + // The idea here is that we want to position the ::-moz-range-progress + // pseudo-element so that the center line running along its length is on the + // corresponding center line of the nsRangeFrame's content box. In the other + // dimension, we align the "start" edge of the ::-moz-range-progress + // pseudo-element's border-box with the corresponding edge of the + // nsRangeFrame's content box, and we size the progress element's border-box + // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's + // content-box size. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsSize progSize = aRangeProgressFrame->GetSize(); + nsRect progRect(borderAndPadding.left, borderAndPadding.top, + progSize.width, progSize.height); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.width); + if (IsRightToLeft()) { + progRect.x += rangeContentBoxSize.width - progLength; + } + progRect.y += (rangeContentBoxSize.height - progSize.height)/2; + progRect.width = progLength; + } else { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.height); + progRect.x += (rangeContentBoxSize.width - progSize.width)/2; + progRect.y += rangeContentBoxSize.height - progLength; + progRect.height = progLength; + } + aRangeProgressFrame->SetRect(progRect); +} + +nsresult +nsRangeFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + NS_ASSERTION(mTrackDiv, "The track div must exist!"); + NS_ASSERTION(mThumbDiv, "The thumb div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::value || + aAttribute == nsGkAtoms::min || + aAttribute == nsGkAtoms::max || + aAttribute == nsGkAtoms::step) { + // We want to update the position of the thumb, except in one special + // case: If the value attribute is being set, it is possible that we are + // in the middle of a type change away from type=range, under the + // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement:: + // HandleTypeChange. In that case the HTMLInputElement's type will + // already have changed, and if we call UpdateForValueChange() + // we'll fail the asserts under that call that check the type of our + // HTMLInputElement. Given that we're changing away from being a range + // and this frame will shortly be destroyed, there's no point in calling + // UpdateForValueChange() anyway. + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + bool typeIsRange = static_cast<dom::HTMLInputElement*>(mContent)->GetType() == + NS_FORM_INPUT_RANGE; + // If script changed the <input>'s type before setting these attributes + // then we don't need to do anything since we are going to be reframed. + if (typeIsRange) { + UpdateForValueChange(); + } + } else if (aAttribute == nsGkAtoms::orient) { + PresContext()->PresShell()->FrameNeedsReflow(this, nsIPresShell::eResize, + NS_FRAME_IS_DIRTY); + } + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +LogicalSize +nsRangeFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + nscoord oneEm = NSToCoordRound(StyleFont()->mFont.size * + nsLayoutUtils::FontSizeInflationFor(this)); // 1em + + bool isInlineOriented = IsInlineOriented(); + + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + + // nsFrame::ComputeSize calls GetMinimumWidgetSize to prevent us from being + // given too small a size when we're natively themed. If we're themed, we set + // our "thickness" dimension to zero below and rely on that + // GetMinimumWidgetSize check to correct that dimension to the natural + // thickness of a slider in the current theme. + + if (isInlineOriented) { + autoSize.ISize(wm) = LONG_SIDE_TO_SHORT_SIDE_RATIO * oneEm; + autoSize.BSize(wm) = IsThemed() ? 0 : oneEm; + } else { + autoSize.ISize(wm) = IsThemed() ? 0 : oneEm; + autoSize.BSize(wm) = LONG_SIDE_TO_SHORT_SIDE_RATIO * oneEm; + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord +nsRangeFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + // nsFrame::ComputeSize calls GetMinimumWidgetSize to prevent us from being + // given too small a size when we're natively themed. If we aren't native + // themed, we don't mind how small we're sized. + return nscoord(0); +} + +nscoord +nsRangeFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + bool isInline = IsInlineOriented(); + + if (!isInline && IsThemed()) { + // nsFrame::ComputeSize calls GetMinimumWidgetSize to prevent us from being + // given too small a size when we're natively themed. We return zero and + // depend on that correction to get our "natural" width when we're a + // vertical slider. + return 0; + } + + nscoord prefISize = NSToCoordRound(StyleFont()->mFont.size * + nsLayoutUtils::FontSizeInflationFor(this)); // 1em + + if (isInline) { + prefISize *= LONG_SIDE_TO_SHORT_SIDE_RATIO; + } + + return prefISize; +} + +bool +nsRangeFrame::IsHorizontal() const +{ + dom::HTMLInputElement* element = + static_cast<dom::HTMLInputElement*>(mContent); + return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::horizontal, eCaseMatters) || + (!element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::vertical, eCaseMatters) && + GetWritingMode().IsVertical() == + element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::block, eCaseMatters)); +} + +double +nsRangeFrame::GetMin() const +{ + return static_cast<dom::HTMLInputElement*>(mContent)->GetMinimum().toDouble(); +} + +double +nsRangeFrame::GetMax() const +{ + return static_cast<dom::HTMLInputElement*>(mContent)->GetMaximum().toDouble(); +} + +double +nsRangeFrame::GetValue() const +{ + return static_cast<dom::HTMLInputElement*>(mContent)->GetValueAsDecimal().toDouble(); +} + +nsIAtom* +nsRangeFrame::GetType() const +{ + return nsGkAtoms::rangeFrame; +} + +#define STYLES_DISABLING_NATIVE_THEMING \ + NS_AUTHOR_SPECIFIED_BACKGROUND | \ + NS_AUTHOR_SPECIFIED_PADDING | \ + NS_AUTHOR_SPECIFIED_BORDER + +bool +nsRangeFrame::ShouldUseNativeStyle() const +{ + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + nsIFrame* progressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + return (StyleDisplay()->mAppearance == NS_THEME_RANGE) && + !PresContext()->HasAuthorSpecifiedRules(this, + (NS_AUTHOR_SPECIFIED_BORDER | + NS_AUTHOR_SPECIFIED_BACKGROUND)) && + trackFrame && + !PresContext()->HasAuthorSpecifiedRules(trackFrame, + STYLES_DISABLING_NATIVE_THEMING) && + progressFrame && + !PresContext()->HasAuthorSpecifiedRules(progressFrame, + STYLES_DISABLING_NATIVE_THEMING) && + thumbFrame && + !PresContext()->HasAuthorSpecifiedRules(thumbFrame, + STYLES_DISABLING_NATIVE_THEMING); +} + +Element* +nsRangeFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::mozRangeTrack) { + return mTrackDiv; + } + + if (aType == CSSPseudoElementType::mozRangeThumb) { + return mThumbDiv; + } + + if (aType == CSSPseudoElementType::mozRangeProgress) { + return mProgressDiv; + } + + return nsContainerFrame::GetPseudoElement(aType); +} + +nsStyleContext* +nsRangeFrame::GetAdditionalStyleContext(int32_t aIndex) const +{ + // We only implement this so that SetAdditionalStyleContext will be + // called if style changes that would change the -moz-focus-outer + // pseudo-element have occurred. + if (aIndex != 0) { + return nullptr; + } + return mOuterFocusStyle; +} + +void +nsRangeFrame::SetAdditionalStyleContext(int32_t aIndex, + nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aIndex == 0, + "GetAdditionalStyleContext is handling other indexes?"); + +#ifdef DEBUG + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameRelease(); + } +#endif + + // The -moz-focus-outer pseudo-element's style has changed. + mOuterFocusStyle = aStyleContext; + +#ifdef DEBUG + if (mOuterFocusStyle) { + mOuterFocusStyle->FrameAddRef(); + } +#endif +} diff --git a/layout/forms/nsRangeFrame.h b/layout/forms/nsRangeFrame.h new file mode 100644 index 000000000..8a2d34a40 --- /dev/null +++ b/layout/forms/nsRangeFrame.h @@ -0,0 +1,215 @@ +/* -*- 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/. */ + +#ifndef nsRangeFrame_h___ +#define nsRangeFrame_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/Decimal.h" +#include "mozilla/EventForwards.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsIDOMEventListener.h" +#include "nsCOMPtr.h" + +class nsDisplayRangeFocusRing; + +class nsRangeFrame : public nsContainerFrame, + public nsIAnonymousContentCreator +{ + friend nsIFrame* + NS_NewRangeFrame(nsIPresShell* aPresShell, nsStyleContext* aContext); + + friend class nsDisplayRangeFocusRing; + + explicit nsRangeFrame(nsStyleContext* aContext); + virtual ~nsRangeFrame(); + + typedef mozilla::CSSPseudoElementType CSSPseudoElementType; + typedef mozilla::dom::Element Element; + +public: + NS_DECL_QUERYFRAME_TARGET(nsRangeFrame) + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS + + // nsIFrame overrides + virtual void Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(NS_LITERAL_STRING("Range"), aResult); + } +#endif + + virtual bool IsLeaf() const override { return true; } + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize + ComputeAutoSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(nsRenderingContext *aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext *aRenderingContext) override; + + virtual nsIAtom* GetType() const override; + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + nsStyleContext* GetAdditionalStyleContext(int32_t aIndex) const override; + void SetAdditionalStyleContext(int32_t aIndex, + nsStyleContext* aStyleContext) override; + + /** + * Returns true if the slider's thumb moves horizontally, or else false if it + * moves vertically. + */ + bool IsHorizontal() const; + + /** + * Returns true if the slider is oriented along the inline axis. + */ + bool IsInlineOriented() const { + return IsHorizontal() != GetWritingMode().IsVertical(); + } + + /** + * Returns true if the slider's thumb moves right-to-left for increasing + * values; only relevant when IsHorizontal() is true. + */ + bool IsRightToLeft() const { + MOZ_ASSERT(IsHorizontal()); + mozilla::WritingMode wm = GetWritingMode(); + return wm.IsVertical() ? wm.IsVerticalRL() : !wm.IsBidiLTR(); + } + + double GetMin() const; + double GetMax() const; + double GetValue() const; + + /** + * Returns the input element's value as a fraction of the difference between + * the input's minimum and its maximum (i.e. returns 0.0 when the value is + * the same as the minimum, and returns 1.0 when the value is the same as the + * maximum). + */ + double GetValueAsFractionOfRange(); + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + mozilla::Decimal GetValueAtEventPoint(mozilla::WidgetGUIEvent* aEvent); + + /** + * Helper that's used when the value of the range changes to reposition the + * thumb, resize the range-progress element, and schedule a repaint. (This + * does not reflow, since the position and size of the thumb and + * range-progress element do not affect the position or size of any other + * frames.) + */ + void UpdateForValueChange(); + + virtual Element* GetPseudoElement(CSSPseudoElementType aType) override; + +private: + + nsresult MakeAnonymousDiv(Element** aResult, + CSSPseudoElementType aPseudoType, + nsTArray<ContentInfo>& aElements); + + // Helper function which reflows the anonymous div frames. + void ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput); + + void DoUpdateThumbPosition(nsIFrame* aThumbFrame, + const nsSize& aRangeSize); + + void DoUpdateRangeProgressFrame(nsIFrame* aProgressFrame, + const nsSize& aRangeSize); + + /** + * The div used to show the ::-moz-range-track pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mTrackDiv; + + /** + * The div used to show the ::-moz-range-progress pseudo-element, which is + * used to (optionally) style the specific chunk of track leading up to the + * thumb's current position. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mProgressDiv; + + /** + * The div used to show the ::-moz-range-thumb pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr<Element> mThumbDiv; + + /** + * Cached style context for -moz-focus-outer CSS pseudo-element style. + */ + RefPtr<nsStyleContext> mOuterFocusStyle; + + class DummyTouchListener final : public nsIDOMEventListener + { + private: + ~DummyTouchListener() {} + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) override + { + return NS_OK; + } + }; + + /** + * A no-op touch-listener used for APZ purposes (see nsRangeFrame::Init). + */ + RefPtr<DummyTouchListener> mDummyTouchListener; +}; + +#endif diff --git a/layout/forms/nsSelectsAreaFrame.cpp b/layout/forms/nsSelectsAreaFrame.cpp new file mode 100644 index 000000000..dd613ae9f --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.cpp @@ -0,0 +1,206 @@ +/* -*- 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 "nsSelectsAreaFrame.h" +#include "nsIContent.h" +#include "nsListControlFrame.h" +#include "nsDisplayList.h" +#include "WritingModes.h" + +using namespace mozilla; + +nsContainerFrame* +NS_NewSelectsAreaFrame(nsIPresShell* aShell, nsStyleContext* aContext, nsFrameState aFlags) +{ + nsSelectsAreaFrame* it = new (aShell) nsSelectsAreaFrame(aContext); + + // We need NS_BLOCK_FLOAT_MGR to ensure that the options inside the select + // aren't expanded by right floats outside the select. + it->AddStateBits(aFlags | NS_BLOCK_FLOAT_MGR); + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsSelectsAreaFrame) + +//--------------------------------------------------------- +/** + * This wrapper class lets us redirect mouse hits from the child frame of + * an option element to the element's own frame. + * REVIEW: This is what nsSelectsAreaFrame::GetFrameForPoint used to do + */ +class nsDisplayOptionEventGrabber : public nsDisplayWrapList { +public: + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, nsDisplayItem* aItem) + : nsDisplayWrapList(aBuilder, aFrame, aItem) {} + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, nsDisplayList* aList) + : nsDisplayWrapList(aBuilder, aFrame, aList) {} + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, nsTArray<nsIFrame*> *aOutFrames) override; + virtual bool ShouldFlattenAway(nsDisplayListBuilder* aBuilder) override { + return false; + } + NS_DISPLAY_DECL_NAME("OptionEventGrabber", TYPE_OPTION_EVENT_GRABBER) +}; + +void nsDisplayOptionEventGrabber::HitTest(nsDisplayListBuilder* aBuilder, + const nsRect& aRect, HitTestState* aState, nsTArray<nsIFrame*> *aOutFrames) +{ + nsTArray<nsIFrame*> outFrames; + mList.HitTest(aBuilder, aRect, aState, &outFrames); + + for (uint32_t i = 0; i < outFrames.Length(); i++) { + nsIFrame* selectedFrame = outFrames.ElementAt(i); + while (selectedFrame && + !(selectedFrame->GetContent() && + selectedFrame->GetContent()->IsHTMLElement(nsGkAtoms::option))) { + selectedFrame = selectedFrame->GetParent(); + } + if (selectedFrame) { + aOutFrames->AppendElement(selectedFrame); + } else { + // keep the original result, which could be this frame + aOutFrames->AppendElement(outFrames.ElementAt(i)); + } + } +} + +class nsOptionEventGrabberWrapper : public nsDisplayWrapper +{ +public: + nsOptionEventGrabberWrapper() {} + virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, nsDisplayList* aList) { + return new (aBuilder) nsDisplayOptionEventGrabber(aBuilder, aFrame, aList); + } + virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder, + nsDisplayItem* aItem) { + return new (aBuilder) nsDisplayOptionEventGrabber(aBuilder, aItem->Frame(), aItem); + } +}; + +static nsListControlFrame* GetEnclosingListFrame(nsIFrame* aSelectsAreaFrame) +{ + nsIFrame* frame = aSelectsAreaFrame->GetParent(); + while (frame) { + if (frame->GetType() == nsGkAtoms::listControlFrame) + return static_cast<nsListControlFrame*>(frame); + frame = frame->GetParent(); + } + return nullptr; +} + +class nsDisplayListFocus : public nsDisplayItem { +public: + nsDisplayListFocus(nsDisplayListBuilder* aBuilder, + nsSelectsAreaFrame* aFrame) : + nsDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayListFocus); + } +#ifdef NS_BUILD_REFCNT_LOGGING + virtual ~nsDisplayListFocus() { + MOZ_COUNT_DTOR(nsDisplayListFocus); + } +#endif + + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) override { + *aSnap = false; + // override bounds because the list item focus ring may extend outside + // the nsSelectsAreaFrame + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + return listFrame->GetVisualOverflowRectRelativeToSelf() + + listFrame->GetOffsetToCrossDoc(ReferenceFrame()); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, + nsRenderingContext* aCtx) override { + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + // listFrame must be non-null or we wouldn't get called. + listFrame->PaintFocus(aCtx->GetDrawTarget(), + aBuilder->ToReferenceFrame(listFrame)); + } + NS_DISPLAY_DECL_NAME("ListFocus", TYPE_LIST_FOCUS) +}; + +void +nsSelectsAreaFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + if (!aBuilder->IsForEventDelivery()) { + BuildDisplayListInternal(aBuilder, aDirtyRect, aLists); + return; + } + + nsDisplayListCollection set; + BuildDisplayListInternal(aBuilder, aDirtyRect, set); + + nsOptionEventGrabberWrapper wrapper; + wrapper.WrapLists(aBuilder, this, set, aLists); +} + +void +nsSelectsAreaFrame::BuildDisplayListInternal(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + nsBlockFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists); + + nsListControlFrame* listFrame = GetEnclosingListFrame(this); + if (listFrame && listFrame->IsFocused()) { + // we can't just associate the display item with the list frame, + // because then the list's scrollframe won't clip it (the scrollframe + // only clips contained descendants). + aLists.Outlines()->AppendNewToTop(new (aBuilder) + nsDisplayListFocus(aBuilder, this)); + } +} + +void +nsSelectsAreaFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + nsListControlFrame* list = GetEnclosingListFrame(this); + NS_ASSERTION(list, + "Must have an nsListControlFrame! Frame constructor is " + "broken"); + + bool isInDropdownMode = list->IsInDropDownMode(); + + // See similar logic in nsListControlFrame::Reflow and + // nsListControlFrame::ReflowAsDropdown. We need to match it here. + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord oldBSize; + if (isInDropdownMode) { + // Store the block size now in case it changes during + // nsBlockFrame::Reflow for some odd reason. + if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW)) { + oldBSize = BSize(wm); + } else { + oldBSize = NS_UNCONSTRAINEDSIZE; + } + } + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // Check whether we need to suppress scrollbar updates. We want to do + // that if we're in a possible first pass and our block size of a row + // has changed. + if (list->MightNeedSecondPass()) { + nscoord newBSizeOfARow = list->CalcBSizeOfARow(); + // We'll need a second pass if our block size of a row changed. For + // comboboxes, we'll also need it if our block size changed. If + // we're going to do a second pass, suppress scrollbar updates for + // this pass. + if (newBSizeOfARow != mBSizeOfARow || + (isInDropdownMode && (oldBSize != aDesiredSize.BSize(wm) || + oldBSize != BSize(wm)))) { + mBSizeOfARow = newBSizeOfARow; + list->SetSuppressScrollbarUpdate(true); + } + } +} diff --git a/layout/forms/nsSelectsAreaFrame.h b/layout/forms/nsSelectsAreaFrame.h new file mode 100644 index 000000000..3aac8a837 --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.h @@ -0,0 +1,49 @@ +/* -*- 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/. */ +#ifndef nsSelectsAreaFrame_h___ +#define nsSelectsAreaFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" + +class nsSelectsAreaFrame : public nsBlockFrame +{ +public: + NS_DECL_FRAMEARENA_HELPERS + + friend nsContainerFrame* NS_NewSelectsAreaFrame(nsIPresShell* aShell, + nsStyleContext* aContext, + nsFrameState aFlags); + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + void BuildDisplayListInternal(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists); + + virtual void Reflow(nsPresContext* aCX, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nscoord BSizeOfARow() const { return mBSizeOfARow; } + +protected: + explicit nsSelectsAreaFrame(nsStyleContext* aContext) : + nsBlockFrame(aContext), + mBSizeOfARow(0) + {} + + // We cache the block size of a single row so that changes to the + // "size" attribute, padding, etc. can all be handled with only one + // reflow. We'll have to reflow twice if someone changes our font + // size or something like that, so that the block size of our options + // will change. + nscoord mBSizeOfARow; +}; + +#endif /* nsSelectsAreaFrame_h___ */ diff --git a/layout/forms/nsTextControlFrame.cpp b/layout/forms/nsTextControlFrame.cpp new file mode 100644 index 000000000..f85bc2a80 --- /dev/null +++ b/layout/forms/nsTextControlFrame.cpp @@ -0,0 +1,1506 @@ +/* -*- 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 "mozilla/DebugOnly.h" + +#include "nsCOMPtr.h" +#include "nsFontMetrics.h" +#include "nsTextControlFrame.h" +#include "nsIPlaintextEditor.h" +#include "nsCaret.h" +#include "nsCSSPseudoElements.h" +#include "nsGenericHTMLElement.h" +#include "nsIEditor.h" +#include "nsIEditorIMESupport.h" +#include "nsIPhonetic.h" +#include "nsTextFragment.h" +#include "nsIDOMHTMLTextAreaElement.h" +#include "nsNameSpaceManager.h" +#include "nsFormControlFrame.h" //for registering accesskeys + +#include "nsIContent.h" +#include "nsPresContext.h" +#include "nsRenderingContext.h" +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" +#include "nsIDOMElement.h" +#include "nsIDOMHTMLElement.h" +#include "nsIPresShell.h" + +#include <algorithm> +#include "nsIDOMNodeList.h" //for selection setting helper func +#include "nsIDOMRange.h" //for selection setting helper func +#include "nsPIDOMWindow.h" //needed for notify selection changed to update the menus ect. +#include "nsIDOMNode.h" + +#include "nsIDOMText.h" //for multiline getselection +#include "nsFocusManager.h" +#include "nsPresState.h" +#include "nsContentList.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/TextEditRules.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/MathAlgorithms.h" +#include "nsFrameSelection.h" + +#define DEFAULT_COLUMN_WIDTH 20 + +using namespace mozilla; + +nsIFrame* +NS_NewTextControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) +{ + return new (aPresShell) nsTextControlFrame(aContext); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame) + +NS_QUERYFRAME_HEAD(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsITextControlFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType +nsTextControlFrame::AccessibleType() +{ + return a11y::eHTMLTextFieldType; +} +#endif + +#ifdef DEBUG +class EditorInitializerEntryTracker { +public: + explicit EditorInitializerEntryTracker(nsTextControlFrame &frame) + : mFrame(frame) + , mFirstEntry(false) + { + if (!mFrame.mInEditorInitialization) { + mFrame.mInEditorInitialization = true; + mFirstEntry = true; + } + } + ~EditorInitializerEntryTracker() + { + if (mFirstEntry) { + mFrame.mInEditorInitialization = false; + } + } + bool EnteredMoreThanOnce() const { return !mFirstEntry; } +private: + nsTextControlFrame &mFrame; + bool mFirstEntry; +}; +#endif + +nsTextControlFrame::nsTextControlFrame(nsStyleContext* aContext) + : nsContainerFrame(aContext) + , mEditorHasBeenInitialized(false) + , mIsProcessing(false) +#ifdef DEBUG + , mInEditorInitialization(false) +#endif +{ +} + +nsTextControlFrame::~nsTextControlFrame() +{ +} + +void +nsTextControlFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + mScrollEvent.Revoke(); + + EditorInitializer* initializer = Properties().Get(TextControlInitializer()); + if (initializer) { + initializer->Revoke(); + Properties().Delete(TextControlInitializer()); + } + + // Unbind the text editor state object from the frame. The editor will live + // on, but things like controllers will be released. + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + txtCtrl->UnbindFromFrame(this); + + nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false); + + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +nsIAtom* +nsTextControlFrame::GetType() const +{ + return nsGkAtoms::textInputFrame; +} + +LogicalSize +nsTextControlFrame::CalcIntrinsicSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + float aFontSizeInflation) const +{ + LogicalSize intrinsicSize(aWM); + // Get leading and the Average/MaxAdvance char width + nscoord lineHeight = 0; + nscoord charWidth = 0; + nscoord charMaxAdvance = 0; + + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + + lineHeight = + ReflowInput::CalcLineHeight(GetContent(), StyleContext(), + NS_AUTOHEIGHT, aFontSizeInflation); + charWidth = fontMet->AveCharWidth(); + charMaxAdvance = fontMet->MaxAdvance(); + + // Set the width equal to the width in characters + int32_t cols = GetCols(); + intrinsicSize.ISize(aWM) = cols * charWidth; + + // To better match IE, take the maximum character width(in twips) and remove + // 4 pixels add this on as additional padding(internalPadding). But only do + // this if we think we have a fixed-width font. + if (mozilla::Abs(charWidth - charMaxAdvance) > (unsigned)nsPresContext::CSSPixelsToAppUnits(1)) { + nscoord internalPadding = + std::max(0, charMaxAdvance - nsPresContext::CSSPixelsToAppUnits(4)); + nscoord t = nsPresContext::CSSPixelsToAppUnits(1); + // Round to a multiple of t + nscoord rest = internalPadding % t; + if (rest < t - rest) { + internalPadding -= rest; + } else { + internalPadding += t - rest; + } + // Now add the extra padding on (so that small input sizes work well) + intrinsicSize.ISize(aWM) += internalPadding; + } else { + // This is to account for the anonymous <br> having a 1 twip width + // in Full Standards mode, see BRFrame::Reflow and bug 228752. + if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) { + intrinsicSize.ISize(aWM) += 1; + } + } + + // Increment width with cols * letter-spacing. + { + const nsStyleCoord& lsCoord = StyleText()->mLetterSpacing; + if (eStyleUnit_Coord == lsCoord.GetUnit()) { + nscoord letterSpacing = lsCoord.GetCoordValue(); + if (letterSpacing != 0) { + intrinsicSize.ISize(aWM) += cols * letterSpacing; + } + } + } + + // Set the height equal to total number of rows (times the height of each + // line, of course) + intrinsicSize.BSize(aWM) = lineHeight * GetRows(); + + // Add in the size of the scrollbars for textarea + if (IsTextArea()) { + nsIFrame* first = PrincipalChildList().FirstChild(); + + nsIScrollableFrame *scrollableFrame = do_QueryFrame(first); + NS_ASSERTION(scrollableFrame, "Child must be scrollable"); + + if (scrollableFrame) { + LogicalMargin scrollbarSizes(aWM, + scrollableFrame->GetDesiredScrollbarSizes(PresContext(), + aRenderingContext)); + + intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM); + intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM); + } + } + return intrinsicSize; +} + +nsresult +nsTextControlFrame::EnsureEditorInitialized() +{ + // This method initializes our editor, if needed. + + // This code used to be called from CreateAnonymousContent(), but + // when the editor set the initial string, it would trigger a + // PresShell listener which called FlushPendingNotifications() + // during frame construction. This was causing other form controls + // to display wrong values. Additionally, calling this every time + // a text frame control is instantiated means that we're effectively + // instantiating the editor for all text fields, even if they + // never get used. So, now this method is being called lazily only + // when we actually need an editor. + + if (mEditorHasBeenInitialized) + return NS_OK; + + nsIDocument* doc = mContent->GetComposedDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + nsWeakFrame weakFrame(this); + + // Flush out content on our document. Have to do this, because script + // blockers don't prevent the sink flushing out content and notifying in the + // process, which can destroy frames. + doc->FlushPendingNotifications(Flush_ContentAndNotify); + NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE); + + // Make sure that editor init doesn't do things that would kill us off + // (especially off the script blockers it'll create for its DOM mutations). + { + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + MOZ_ASSERT(txtCtrl, "Content not a text control element"); + + // Hide selection changes during the initialization, as webpages should not + // be aware of these initializations + AutoHideSelectionChanges hideSelectionChanges(txtCtrl->GetConstFrameSelection()); + + nsAutoScriptBlocker scriptBlocker; + + // Time to mess with our security context... See comments in GetValue() + // for why this is needed. + mozilla::dom::AutoNoJSAPI nojsapi; + + // Make sure that we try to focus the content even if the method fails + class EnsureSetFocus { + public: + explicit EnsureSetFocus(nsTextControlFrame* aFrame) + : mFrame(aFrame) {} + ~EnsureSetFocus() { + if (nsContentUtils::IsFocusedContent(mFrame->GetContent())) + mFrame->SetFocus(true, false); + } + private: + nsTextControlFrame *mFrame; + }; + EnsureSetFocus makeSureSetFocusHappens(this); + +#ifdef DEBUG + // Make sure we are not being called again until we're finished. + // If reentrancy happens, just pretend that we don't have an editor. + const EditorInitializerEntryTracker tracker(*this); + NS_ASSERTION(!tracker.EnteredMoreThanOnce(), + "EnsureEditorInitialized has been called while a previous call was in progress"); +#endif + + // Create an editor for the frame, if one doesn't already exist + nsresult rv = txtCtrl->CreateEditor(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Set mEditorHasBeenInitialized so that subsequent calls will use the + // editor. + mEditorHasBeenInitialized = true; + + if (weakFrame.IsAlive()) { + int32_t position = 0; + + // Set the selection to the end of the text field (bug 1287655), + // but only if the contents has changed (bug 1337392). + if (txtCtrl->ValueChanged()) { + nsAutoString val; + txtCtrl->GetTextEditorValue(val, true); + position = val.Length(); + } + + SetSelectionEndPoints(position, position); + } + } + NS_ENSURE_STATE(weakFrame.IsAlive()); + return NS_OK; +} + +nsresult +nsTextControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + NS_ASSERTION(mContent, "We should have a content!"); + + mState |= NS_FRAME_INDEPENDENT_SELECTION; + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + // Bind the frame to its text control + nsresult rv = txtCtrl->BindToFrame(this); + NS_ENSURE_SUCCESS(rv, rv); + + nsIContent* rootNode = txtCtrl->GetRootEditorNode(); + NS_ENSURE_TRUE(rootNode, NS_ERROR_OUT_OF_MEMORY); + + if (!aElements.AppendElement(rootNode)) + return NS_ERROR_OUT_OF_MEMORY; + + // Do we need a placeholder node? + nsAutoString placeholderTxt; + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::placeholder, + placeholderTxt); + nsContentUtils::RemoveNewlines(placeholderTxt); + mUsePlaceholder = !placeholderTxt.IsEmpty(); + + // Create the placeholder anonymous content if needed. + if (mUsePlaceholder) { + nsIContent* placeholderNode = txtCtrl->CreatePlaceholderNode(); + NS_ENSURE_TRUE(placeholderNode, NS_ERROR_OUT_OF_MEMORY); + + // Associate ::placeholder pseudo-element with the placeholder node. + CSSPseudoElementType pseudoType = CSSPseudoElementType::placeholder; + + // If this is a text input inside a number input then we want to use the + // main number input as the source of style for the placeholder frame. + nsIFrame* mainInputFrame = this; + if (StyleContext()->GetPseudoType() == CSSPseudoElementType::mozNumberText) { + do { + mainInputFrame = mainInputFrame->GetParent(); + } while (mainInputFrame && + mainInputFrame->GetType() != nsGkAtoms::numberControlFrame); + MOZ_ASSERT(mainInputFrame); + } + + RefPtr<nsStyleContext> placeholderStyleContext = + PresContext()->StyleSet()->ResolvePseudoElementStyle( + mainInputFrame->GetContent()->AsElement(), pseudoType, StyleContext(), + placeholderNode->AsElement()); + + if (!aElements.AppendElement(ContentInfo(placeholderNode, + placeholderStyleContext))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!IsSingleLineTextControl()) { + // For textareas, UpdateValueDisplay doesn't initialize the visibility + // status of the placeholder because it returns early, so we have to + // do that manually here. + txtCtrl->UpdatePlaceholderVisibility(true); + } + } + + rv = UpdateValueDisplay(false); + NS_ENSURE_SUCCESS(rv, rv); + + // textareas are eagerly initialized + bool initEagerly = !IsSingleLineTextControl(); + if (!initEagerly) { + // Also, input elements which have a cached selection should get eager + // editor initialization. + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + initEagerly = txtCtrl->HasCachedSelection(); + } + if (!initEagerly) { + nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(txtCtrl); + if (element) { + // so are input text controls with spellcheck=true + element->GetSpellcheck(&initEagerly); + } + } + + if (initEagerly) { + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "Someone forgot a script blocker?"); + EditorInitializer* initializer = Properties().Get(TextControlInitializer()); + if (initializer) { + initializer->Revoke(); + } + initializer = new EditorInitializer(this); + Properties().Set(TextControlInitializer(),initializer); + nsContentUtils::AddScriptRunner(initializer); + } + + return NS_OK; +} + +void +nsTextControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + // This can be called off-main-thread during Servo traversal, so we take care + // to avoid QI-ing the DOM node. + nsITextControlElement* txtCtrl = nullptr; + nsIContent* content = GetContent(); + if (content->IsHTMLElement(nsGkAtoms::input)) { + txtCtrl = static_cast<HTMLInputElement*>(content); + } else if (content->IsHTMLElement(nsGkAtoms::textarea)) { + txtCtrl = static_cast<HTMLTextAreaElement*>(content); + } else { + MOZ_CRASH("Unexpected content type for nsTextControlFrame"); + } + + nsIContent* root = txtCtrl->GetRootEditorNode(); + if (root) { + aElements.AppendElement(root); + } + + nsIContent* placeholder = txtCtrl->GetPlaceholderNode(); + if (placeholder && !(aFilter & nsIContent::eSkipPlaceholderContent)) + aElements.AppendElement(placeholder); + +} + +nscoord +nsTextControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext) +{ + nscoord result = 0; + DISPLAY_PREF_WIDTH(this, result); + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + WritingMode wm = GetWritingMode(); + result = CalcIntrinsicSize(aRenderingContext, wm, inflation).ISize(wm); + return result; +} + +nscoord +nsTextControlFrame::GetMinISize(nsRenderingContext* aRenderingContext) +{ + // Our min width is just our preferred width if we have auto width. + nscoord result; + DISPLAY_MIN_WIDTH(this, result); + result = GetPrefISize(aRenderingContext); + return result; +} + +LogicalSize +nsTextControlFrame::ComputeAutoSize(nsRenderingContext* aRenderingContext, + WritingMode aWM, + const LogicalSize& aCBSize, + nscoord aAvailableISize, + const LogicalSize& aMargin, + const LogicalSize& aBorder, + const LogicalSize& aPadding, + ComputeSizeFlags aFlags) +{ + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + LogicalSize autoSize = CalcIntrinsicSize(aRenderingContext, aWM, inflation); + + // Note: nsContainerFrame::ComputeAutoSize only computes the inline-size (and + // only for 'auto'), the block-size it returns is always NS_UNCONSTRAINEDSIZE. + const nsStyleCoord& iSizeCoord = StylePosition()->ISize(aWM); + if (iSizeCoord.GetUnit() == eStyleUnit_Auto) { + if (aFlags & ComputeSizeFlags::eIClampMarginBoxMinSize) { + // CalcIntrinsicSize isn't aware of grid-item margin-box clamping, so we + // fall back to nsContainerFrame's ComputeAutoSize to handle that. + // XXX maybe a font-inflation issue here? (per the assertion below). + autoSize.ISize(aWM) = + nsContainerFrame::ComputeAutoSize(aRenderingContext, aWM, + aCBSize, aAvailableISize, + aMargin, aBorder, + aPadding, aFlags).ISize(aWM); + } +#ifdef DEBUG + else { + LogicalSize ancestorAutoSize = + nsContainerFrame::ComputeAutoSize(aRenderingContext, aWM, + aCBSize, aAvailableISize, + aMargin, aBorder, + aPadding, aFlags); + // Disabled when there's inflation; see comment in GetXULPrefSize. + MOZ_ASSERT(inflation != 1.0f || + ancestorAutoSize.ISize(aWM) == autoSize.ISize(aWM), + "Incorrect size computed by ComputeAutoSize?"); + } +#endif + } + return autoSize; +} + +void +nsTextControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) +{ + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsTextControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + + // make sure that the form registers itself on the initial/first reflow + if (mState & NS_FRAME_FIRST_REFLOW) { + nsFormControlFrame::RegUnRegAccessKey(this, true); + } + + // set values of reflow's out parameters + WritingMode wm = aReflowInput.GetWritingMode(); + LogicalSize + finalSize(wm, + aReflowInput.ComputedISize() + + aReflowInput.ComputedLogicalBorderPadding().IStartEnd(wm), + aReflowInput.ComputedBSize() + + aReflowInput.ComputedLogicalBorderPadding().BStartEnd(wm)); + aDesiredSize.SetSize(wm, finalSize); + + // computation of the ascent wrt the input height + nscoord lineHeight = aReflowInput.ComputedBSize(); + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + if (!IsSingleLineTextControl()) { + lineHeight = ReflowInput::CalcLineHeight(GetContent(), StyleContext(), + NS_AUTOHEIGHT, inflation); + } + RefPtr<nsFontMetrics> fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, inflation); + // now adjust for our borders and padding + aDesiredSize.SetBlockStartAscent( + nsLayoutUtils::GetCenteredFontBaseline(fontMet, lineHeight, + wm.IsLineInverted()) + + aReflowInput.ComputedLogicalBorderPadding().BStart(wm)); + + // overflow handling + aDesiredSize.SetOverflowAreasToDesiredBounds(); + // perform reflow on all kids + nsIFrame* kid = mFrames.FirstChild(); + while (kid) { + ReflowTextControlChild(kid, aPresContext, aReflowInput, aStatus, aDesiredSize); + kid = kid->GetNextSibling(); + } + + // take into account css properties that affect overflow handling + FinishAndStoreOverflow(&aDesiredSize); + + aStatus = NS_FRAME_COMPLETE; + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); +} + +void +nsTextControlFrame::ReflowTextControlChild(nsIFrame* aKid, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus, + ReflowOutput& aParentDesiredSize) +{ + // compute available size and frame offsets for child + WritingMode wm = aKid->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSizeWithPadding(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + ReflowInput kidReflowInput(aPresContext, aReflowInput, + aKid, availSize, nullptr, + ReflowInput::CALLER_WILL_INIT); + // Override padding with our computed padding in case we got it from theming or percentage + kidReflowInput.Init(aPresContext, nullptr, nullptr, &aReflowInput.ComputedPhysicalPadding()); + + // Set computed width and computed height for the child + kidReflowInput.SetComputedWidth(aReflowInput.ComputedWidth()); + kidReflowInput.SetComputedHeight(aReflowInput.ComputedHeight()); + + // Offset the frame by the size of the parent's border + nscoord xOffset = aReflowInput.ComputedPhysicalBorderPadding().left - + aReflowInput.ComputedPhysicalPadding().left; + nscoord yOffset = aReflowInput.ComputedPhysicalBorderPadding().top - + aReflowInput.ComputedPhysicalPadding().top; + + // reflow the child + ReflowOutput desiredSize(aReflowInput); + ReflowChild(aKid, aPresContext, desiredSize, kidReflowInput, + xOffset, yOffset, 0, aStatus); + + // place the child + FinishReflowChild(aKid, aPresContext, desiredSize, + &kidReflowInput, xOffset, yOffset, 0); + + // consider the overflow + aParentDesiredSize.mOverflowAreas.UnionWith(desiredSize.mOverflowAreas); +} + +nsSize +nsTextControlFrame::GetXULMinSize(nsBoxLayoutState& aState) +{ + // XXXbz why? Why not the nsBoxFrame sizes? + return nsBox::GetXULMinSize(aState); +} + +bool +nsTextControlFrame::IsXULCollapsed() +{ + // We're never collapsed in the box sense. + return false; +} + +bool +nsTextControlFrame::IsLeaf() const +{ + return true; +} + +NS_IMETHODIMP +nsTextControlFrame::ScrollOnFocusEvent::Run() +{ + if (mFrame) { + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(mFrame->GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + if (selCon) { + mFrame->mScrollEvent.Forget(); + selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, + nsISelectionController::SCROLL_SYNCHRONOUS); + } + } + return NS_OK; +} + +//IMPLEMENTING NS_IFORMCONTROLFRAME +void nsTextControlFrame::SetFocus(bool aOn, bool aRepaint) +{ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + // Revoke the previous scroll event if one exists + mScrollEvent.Revoke(); + + // If 'dom.placeholeder.show_on_focus' preference is 'false', focusing or + // blurring the frame can have an impact on the placeholder visibility. + if (mUsePlaceholder) { + txtCtrl->UpdatePlaceholderVisibility(true); + } + + if (!aOn) { + return; + } + + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + if (!selCon) + return; + + nsCOMPtr<nsISelection> ourSel; + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(ourSel)); + if (!ourSel) return; + + nsIPresShell* presShell = PresContext()->GetPresShell(); + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) return; + + // Scroll the current selection into view + nsISelection *caretSelection = caret->GetSelection(); + const bool isFocusedRightNow = ourSel == caretSelection; + if (!isFocusedRightNow) { + // Don't scroll the current selection if we've been focused using the mouse. + uint32_t lastFocusMethod = 0; + nsIDocument* doc = GetContent()->GetComposedDoc(); + if (doc) { + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) { + fm->GetLastFocusMethod(doc->GetWindow(), &lastFocusMethod); + } + } + if (!(lastFocusMethod & nsIFocusManager::FLAG_BYMOUSE)) { + RefPtr<ScrollOnFocusEvent> event = new ScrollOnFocusEvent(this); + nsresult rv = NS_DispatchToCurrentThread(event); + if (NS_SUCCEEDED(rv)) { + mScrollEvent = event; + } + } + } + + // tell the caret to use our selection + caret->SetSelection(ourSel); + + // mutual-exclusion: the selection is either controlled by the + // document or by the text input/area. Clear any selection in the + // document since the focus is now on our independent selection. + + nsCOMPtr<nsISelectionController> selcon = do_QueryInterface(presShell); + nsCOMPtr<nsISelection> docSel; + selcon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(docSel)); + if (!docSel) return; + + bool isCollapsed = false; + docSel->GetIsCollapsed(&isCollapsed); + if (!isCollapsed) + docSel->RemoveAllRanges(); +} + +nsresult nsTextControlFrame::SetFormProperty(nsIAtom* aName, const nsAString& aValue) +{ + if (!mIsProcessing)//some kind of lock. + { + mIsProcessing = true; + if (nsGkAtoms::select == aName) + { + // Select all the text. + // + // XXX: This is lame, we can't call editor's SelectAll method + // because that triggers AutoCopies in unix builds. + // Instead, we have to call our own homegrown version + // of select all which merely builds a range that selects + // all of the content and adds that to the selection. + + nsWeakFrame weakThis = this; + SelectAllOrCollapseToEndOfText(true); // NOTE: can destroy the world + if (!weakThis.IsAlive()) { + return NS_OK; + } + } + mIsProcessing = false; + } + return NS_OK; +} + +NS_IMETHODIMP +nsTextControlFrame::GetEditor(nsIEditor **aEditor) +{ + NS_ENSURE_ARG_POINTER(aEditor); + + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + *aEditor = txtCtrl->GetTextEditor(); + NS_IF_ADDREF(*aEditor); + return NS_OK; +} + +nsresult +nsTextControlFrame::SetSelectionInternal(nsIDOMNode *aStartNode, + int32_t aStartOffset, + nsIDOMNode *aEndNode, + int32_t aEndOffset, + nsITextControlFrame::SelectionDirection aDirection) +{ + // Create a new range to represent the new selection. + // Note that we use a new range to avoid having to do + // isIncreasing checks to avoid possible errors. + + RefPtr<nsRange> range = new nsRange(mContent); + // Be careful to use internal nsRange methods which do not check to make sure + // we have access to the node. + nsCOMPtr<nsINode> start = do_QueryInterface(aStartNode); + nsCOMPtr<nsINode> end = do_QueryInterface(aEndNode); + nsresult rv = range->Set(start, aStartOffset, end, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the selection, clear it and add the new range to it! + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + + nsCOMPtr<nsISelection> selection; + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + nsCOMPtr<nsISelectionPrivate> selPriv = do_QueryInterface(selection, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsDirection direction; + if (aDirection == eNone) { + // Preserve the direction + direction = selPriv->GetSelectionDirection(); + } else { + direction = (aDirection == eBackward) ? eDirPrevious : eDirNext; + } + + rv = selection->RemoveAllRanges(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = selection->AddRange(range); // NOTE: can destroy the world + NS_ENSURE_SUCCESS(rv, rv); + + selPriv->SetSelectionDirection(direction); + return rv; +} + +nsresult +nsTextControlFrame::ScrollSelectionIntoView() +{ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + if (selCon) { + // Scroll the selection into view (see bug 231389). + return selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, + nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY); + } + + return NS_ERROR_FAILURE; +} + +mozilla::dom::Element* +nsTextControlFrame::GetRootNodeAndInitializeEditor() +{ + nsCOMPtr<nsIDOMElement> root; + GetRootNodeAndInitializeEditor(getter_AddRefs(root)); + nsCOMPtr<mozilla::dom::Element> rootElem = do_QueryInterface(root); + return rootElem; +} + +nsresult +nsTextControlFrame::GetRootNodeAndInitializeEditor(nsIDOMElement **aRootElement) +{ + NS_ENSURE_ARG_POINTER(aRootElement); + + nsCOMPtr<nsIEditor> editor; + GetEditor(getter_AddRefs(editor)); + if (!editor) + return NS_OK; + + return editor->GetRootElement(aRootElement); +} + +nsresult +nsTextControlFrame::SelectAllOrCollapseToEndOfText(bool aSelect) +{ + nsCOMPtr<nsIDOMElement> rootElement; + nsresult rv = GetRootNodeAndInitializeEditor(getter_AddRefs(rootElement)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContent> rootContent = do_QueryInterface(rootElement); + nsCOMPtr<nsIDOMNode> rootNode(do_QueryInterface(rootElement)); + + NS_ENSURE_TRUE(rootNode && rootContent, NS_ERROR_FAILURE); + + int32_t numChildren = rootContent->GetChildCount(); + + if (numChildren > 0) { + // We never want to place the selection after the last + // br under the root node! + nsIContent *child = rootContent->GetChildAt(numChildren - 1); + if (child) { + if (child->IsHTMLElement(nsGkAtoms::br)) + --numChildren; + } + if (!aSelect && numChildren) { + child = rootContent->GetChildAt(numChildren - 1); + if (child && child->IsNodeOfType(nsINode::eTEXT)) { + rootNode = do_QueryInterface(child); + const nsTextFragment* fragment = child->GetText(); + numChildren = fragment ? fragment->GetLength() : 0; + } + } + } + + rv = SetSelectionInternal(rootNode, aSelect ? 0 : numChildren, + rootNode, numChildren); + NS_ENSURE_SUCCESS(rv, rv); + + return ScrollSelectionIntoView(); +} + +nsresult +nsTextControlFrame::SetSelectionEndPoints(int32_t aSelStart, int32_t aSelEnd, + nsITextControlFrame::SelectionDirection aDirection) +{ + NS_ASSERTION(aSelStart <= aSelEnd, "Invalid selection offsets!"); + + if (aSelStart > aSelEnd) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIDOMNode> startNode, endNode; + int32_t startOffset, endOffset; + + // Calculate the selection start point. + + nsresult rv = OffsetToDOMPoint(aSelStart, getter_AddRefs(startNode), &startOffset); + + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelStart == aSelEnd) { + // Collapsed selection, so start and end are the same! + endNode = startNode; + endOffset = startOffset; + } + else { + // Selection isn't collapsed so we have to calculate + // the end point too. + + rv = OffsetToDOMPoint(aSelEnd, getter_AddRefs(endNode), &endOffset); + + NS_ENSURE_SUCCESS(rv, rv); + } + + return SetSelectionInternal(startNode, startOffset, endNode, endOffset, aDirection); +} + +NS_IMETHODIMP +nsTextControlFrame::SetSelectionRange(int32_t aSelStart, int32_t aSelEnd, + nsITextControlFrame::SelectionDirection aDirection) +{ + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelStart > aSelEnd) { + // Simulate what we'd see SetSelectionStart() was called, followed + // by a SetSelectionEnd(). + + aSelStart = aSelEnd; + } + + return SetSelectionEndPoints(aSelStart, aSelEnd, aDirection); +} + + +NS_IMETHODIMP +nsTextControlFrame::SetSelectionStart(int32_t aSelectionStart) +{ + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t selStart = 0, selEnd = 0; + + rv = GetSelectionRange(&selStart, &selEnd); + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelectionStart > selEnd) { + // Collapse to the new start point. + selEnd = aSelectionStart; + } + + selStart = aSelectionStart; + + return SetSelectionEndPoints(selStart, selEnd); +} + +NS_IMETHODIMP +nsTextControlFrame::SetSelectionEnd(int32_t aSelectionEnd) +{ + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t selStart = 0, selEnd = 0; + + rv = GetSelectionRange(&selStart, &selEnd); + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelectionEnd < selStart) { + // Collapse to the new end point. + selStart = aSelectionEnd; + } + + selEnd = aSelectionEnd; + + return SetSelectionEndPoints(selStart, selEnd); +} + +nsresult +nsTextControlFrame::OffsetToDOMPoint(int32_t aOffset, + nsIDOMNode** aResult, + int32_t* aPosition) +{ + NS_ENSURE_ARG_POINTER(aResult && aPosition); + + *aResult = nullptr; + *aPosition = 0; + + nsCOMPtr<nsIDOMElement> rootElement; + nsresult rv = GetRootNodeAndInitializeEditor(getter_AddRefs(rootElement)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDOMNode> rootNode(do_QueryInterface(rootElement)); + + NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMNodeList> nodeList; + + rv = rootNode->GetChildNodes(getter_AddRefs(nodeList)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(nodeList, NS_ERROR_FAILURE); + + uint32_t length = 0; + + rv = nodeList->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(length <= 2, "We should have one text node and one mozBR at most"); + + nsCOMPtr<nsIDOMNode> firstNode; + rv = nodeList->Item(0, getter_AddRefs(firstNode)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(firstNode); + + if (length == 0 || aOffset < 0) { + NS_IF_ADDREF(*aResult = rootNode); + *aPosition = 0; + } else if (textNode) { + uint32_t textLength = 0; + textNode->GetLength(&textLength); + if (length == 2 && uint32_t(aOffset) == textLength) { + // If we're at the end of the text node and we have a trailing BR node, + // set the selection on the BR node. + NS_IF_ADDREF(*aResult = rootNode); + *aPosition = 1; + } else { + // Otherwise, set the selection on the textnode itself. + NS_IF_ADDREF(*aResult = firstNode); + *aPosition = std::min(aOffset, int32_t(textLength)); + } + } else { + NS_IF_ADDREF(*aResult = rootNode); + *aPosition = 0; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsTextControlFrame::GetSelectionRange(int32_t* aSelectionStart, + int32_t* aSelectionEnd, + SelectionDirection* aDirection) +{ + // make sure we have an editor + nsresult rv = EnsureEditorInitialized(); + NS_ENSURE_SUCCESS(rv, rv); + + if (aSelectionStart) { + *aSelectionStart = 0; + } + if (aSelectionEnd) { + *aSelectionEnd = 0; + } + if (aDirection) { + *aDirection = eNone; + } + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + nsCOMPtr<nsISelection> selection; + rv = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + dom::Selection* sel = selection->AsSelection(); + if (aDirection) { + nsDirection direction = sel->GetSelectionDirection(); + if (direction == eDirNext) { + *aDirection = eForward; + } else if (direction == eDirPrevious) { + *aDirection = eBackward; + } else { + NS_NOTREACHED("Invalid nsDirection enum value"); + } + } + + if (!aSelectionStart || !aSelectionEnd) { + return NS_OK; + } + + mozilla::dom::Element* root = GetRootNodeAndInitializeEditor(); + NS_ENSURE_STATE(root); + nsContentUtils::GetSelectionInTextControl(sel, root, + *aSelectionStart, *aSelectionEnd); + + return NS_OK; +} + +/////END INTERFACE IMPLEMENTATIONS + +////NSIFRAME +nsresult +nsTextControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) +{ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsISelectionController* selCon = txtCtrl->GetSelectionController(); + const bool needEditor = nsGkAtoms::maxlength == aAttribute || + nsGkAtoms::readonly == aAttribute || + nsGkAtoms::disabled == aAttribute || + nsGkAtoms::spellcheck == aAttribute; + nsCOMPtr<nsIEditor> editor; + if (needEditor) { + GetEditor(getter_AddRefs(editor)); + } + if ((needEditor && !editor) || !selCon) { + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); + } + + if (nsGkAtoms::maxlength == aAttribute) { + int32_t maxLength; + bool maxDefined = GetMaxLength(&maxLength); + nsCOMPtr<nsIPlaintextEditor> textEditor = do_QueryInterface(editor); + if (textEditor) { + if (maxDefined) { // set the maxLength attribute + textEditor->SetMaxTextLength(maxLength); + // if maxLength>docLength, we need to truncate the doc content + } else { // unset the maxLength attribute + textEditor->SetMaxTextLength(-1); + } + } + return NS_OK; + } + + if (nsGkAtoms::readonly == aAttribute) { + uint32_t flags; + editor->GetFlags(&flags); + if (AttributeExists(nsGkAtoms::readonly)) { // set readonly + flags |= nsIPlaintextEditor::eEditorReadonlyMask; + if (nsContentUtils::IsFocusedContent(mContent)) { + selCon->SetCaretEnabled(false); + } + } else { // unset readonly + flags &= ~(nsIPlaintextEditor::eEditorReadonlyMask); + if (!(flags & nsIPlaintextEditor::eEditorDisabledMask) && + nsContentUtils::IsFocusedContent(mContent)) { + selCon->SetCaretEnabled(true); + } + } + editor->SetFlags(flags); + return NS_OK; + } + + if (nsGkAtoms::disabled == aAttribute) { + uint32_t flags; + editor->GetFlags(&flags); + int16_t displaySelection = nsISelectionController::SELECTION_OFF; + const bool focused = nsContentUtils::IsFocusedContent(mContent); + const bool hasAttr = AttributeExists(nsGkAtoms::disabled); + if (hasAttr) { // set disabled + flags |= nsIPlaintextEditor::eEditorDisabledMask; + } else { // unset disabled + flags &= ~(nsIPlaintextEditor::eEditorDisabledMask); + displaySelection = focused ? nsISelectionController::SELECTION_ON + : nsISelectionController::SELECTION_HIDDEN; + } + selCon->SetDisplaySelection(displaySelection); + if (focused) { + selCon->SetCaretEnabled(!hasAttr); + } + editor->SetFlags(flags); + return NS_OK; + } + + if (!mEditorHasBeenInitialized && nsGkAtoms::value == aAttribute) { + UpdateValueDisplay(true); + return NS_OK; + } + + // Allow the base class to handle common attributes supported by all form + // elements... + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + + +nsresult +nsTextControlFrame::GetText(nsString& aText) +{ + nsresult rv = NS_OK; + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + if (IsSingleLineTextControl()) { + // There will be no line breaks so we can ignore the wrap property. + txtCtrl->GetTextEditorValue(aText, true); + } else { + nsCOMPtr<nsIDOMHTMLTextAreaElement> textArea = do_QueryInterface(mContent); + if (textArea) { + rv = textArea->GetValue(aText); + } + } + return rv; +} + + +nsresult +nsTextControlFrame::GetPhonetic(nsAString& aPhonetic) +{ + aPhonetic.Truncate(0); + + nsCOMPtr<nsIEditor> editor; + nsresult rv = GetEditor(getter_AddRefs(editor)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEditorIMESupport> imeSupport = do_QueryInterface(editor); + if (imeSupport) { + nsCOMPtr<nsIPhonetic> phonetic = do_QueryInterface(imeSupport); + if (phonetic) + phonetic->GetPhonetic(aPhonetic); + } + return NS_OK; +} + +///END NSIFRAME OVERLOADS +/////BEGIN PROTECTED METHODS + +bool +nsTextControlFrame::GetMaxLength(int32_t* aSize) +{ + *aSize = -1; + + nsGenericHTMLElement *content = nsGenericHTMLElement::FromContent(mContent); + if (content) { + const nsAttrValue* attr = content->GetParsedAttr(nsGkAtoms::maxlength); + if (attr && attr->Type() == nsAttrValue::eInteger) { + *aSize = attr->GetIntegerValue(); + + return true; + } + } + return false; +} + +// END IMPLEMENTING NS_IFORMCONTROLFRAME + +void +nsTextControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) +{ + nsContainerFrame::SetInitialChildList(aListID, aChildList); + if (aListID != kPrincipalList) { + return; + } + + // Mark the scroll frame as being a reflow root. This will allow + // incremental reflows to be initiated at the scroll frame, rather + // than descending from the root frame of the frame hierarchy. + if (nsIFrame* first = PrincipalChildList().FirstChild()) { + first->AddStateBits(NS_FRAME_REFLOW_ROOT); + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + txtCtrl->InitializeKeyboardEventListeners(); + + nsPoint* contentScrollPos = Properties().Get(ContentScrollPos()); + if (contentScrollPos) { + // If we have a scroll pos stored to be passed to our anonymous + // div, do it here! + nsIStatefulFrame* statefulFrame = do_QueryFrame(first); + NS_ASSERTION(statefulFrame, "unexpected type of frame for the anonymous div"); + nsPresState fakePresState; + fakePresState.SetScrollState(*contentScrollPos); + statefulFrame->RestoreState(&fakePresState); + Properties().Remove(ContentScrollPos()); + delete contentScrollPos; + } + } +} + +void +nsTextControlFrame::SetValueChanged(bool aValueChanged) +{ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + if (mUsePlaceholder) { + nsWeakFrame weakFrame(this); + txtCtrl->UpdatePlaceholderVisibility(true); + if (!weakFrame.IsAlive()) { + return; + } + } + + txtCtrl->SetValueChanged(aValueChanged); +} + + +nsresult +nsTextControlFrame::UpdateValueDisplay(bool aNotify, + bool aBeforeEditorInit, + const nsAString *aValue) +{ + if (!IsSingleLineTextControl()) // textareas don't use this + return NS_OK; + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + nsIContent* rootNode = txtCtrl->GetRootEditorNode(); + + NS_PRECONDITION(rootNode, "Must have a div content\n"); + NS_PRECONDITION(!mEditorHasBeenInitialized, + "Do not call this after editor has been initialized"); + NS_ASSERTION(!mUsePlaceholder || txtCtrl->GetPlaceholderNode(), + "A placeholder div must exist"); + + nsIContent *textContent = rootNode->GetChildAt(0); + if (!textContent) { + // Set up a textnode with our value + RefPtr<nsTextNode> textNode = + new nsTextNode(mContent->NodeInfo()->NodeInfoManager()); + + NS_ASSERTION(textNode, "Must have textcontent!\n"); + + rootNode->AppendChildTo(textNode, aNotify); + textContent = textNode; + } + + NS_ENSURE_TRUE(textContent, NS_ERROR_UNEXPECTED); + + // Get the current value of the textfield from the content. + nsAutoString value; + if (aValue) { + value = *aValue; + } else { + txtCtrl->GetTextEditorValue(value, true); + } + + // Update the display of the placeholder value if needed. + // We don't need to do this if we're about to initialize the + // editor, since EnsureEditorInitialized takes care of this. + if (mUsePlaceholder && !aBeforeEditorInit) + { + nsWeakFrame weakFrame(this); + txtCtrl->UpdatePlaceholderVisibility(aNotify); + NS_ENSURE_STATE(weakFrame.IsAlive()); + } + + if (aBeforeEditorInit && value.IsEmpty()) { + rootNode->RemoveChildAt(0, true); + return NS_OK; + } + + if (!value.IsEmpty() && IsPasswordTextControl()) { + TextEditRules::FillBufWithPWChars(&value, value.Length()); + } + return textContent->SetText(value, aNotify); +} + +NS_IMETHODIMP +nsTextControlFrame::GetOwnedSelectionController(nsISelectionController** aSelCon) +{ + NS_ENSURE_ARG_POINTER(aSelCon); + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + *aSelCon = txtCtrl->GetSelectionController(); + NS_IF_ADDREF(*aSelCon); + + return NS_OK; +} + +nsFrameSelection* +nsTextControlFrame::GetOwnedFrameSelection() +{ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + return txtCtrl->GetConstFrameSelection(); +} + +NS_IMETHODIMP +nsTextControlFrame::SaveState(nsPresState** aState) +{ + NS_ENSURE_ARG_POINTER(aState); + + *aState = nullptr; + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + nsIContent* rootNode = txtCtrl->GetRootEditorNode(); + if (rootNode) { + // Query the nsIStatefulFrame from the HTMLScrollFrame + nsIStatefulFrame* scrollStateFrame = do_QueryFrame(rootNode->GetPrimaryFrame()); + if (scrollStateFrame) { + return scrollStateFrame->SaveState(aState); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsTextControlFrame::RestoreState(nsPresState* aState) +{ + NS_ENSURE_ARG_POINTER(aState); + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element"); + + nsIContent* rootNode = txtCtrl->GetRootEditorNode(); + if (rootNode) { + // Query the nsIStatefulFrame from the HTMLScrollFrame + nsIStatefulFrame* scrollStateFrame = do_QueryFrame(rootNode->GetPrimaryFrame()); + if (scrollStateFrame) { + return scrollStateFrame->RestoreState(aState); + } + } + + // Most likely, we don't have our anonymous content constructed yet, which + // would cause us to end up here. In this case, we'll just store the scroll + // pos ourselves, and forward it to the scroll frame later when it's created. + Properties().Set(ContentScrollPos(), new nsPoint(aState->GetScrollPosition())); + return NS_OK; +} + +nsresult +nsTextControlFrame::PeekOffset(nsPeekOffsetStruct *aPos) +{ + return NS_ERROR_FAILURE; +} + +void +nsTextControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + /* + * The implementation of this method is equivalent as: + * nsContainerFrame::BuildDisplayList() + * with the difference that we filter-out the placeholder frame when it + * should not be visible. + */ + DO_GLOBAL_REFLOW_COUNT_DSP("nsTextControlFrame"); + + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + NS_ASSERTION(txtCtrl, "Content not a text control element!"); + + DisplayBorderBackgroundOutline(aBuilder, aLists); + + nsIFrame* kid = mFrames.FirstChild(); + // Redirect all lists to the Content list so that nothing can escape, ie + // opacity creating stacking contexts that then get sorted with stacking + // contexts external to us. + nsDisplayList* content = aLists.Content(); + nsDisplayListSet set(content, content, content, content, content, content); + + while (kid) { + // If the frame is the placeholder frame, we should only show it if the + // placeholder has to be visible. + if (kid->GetContent() != txtCtrl->GetPlaceholderNode() || + txtCtrl->GetPlaceholderVisibility()) { + BuildDisplayListForChild(aBuilder, kid, aDirtyRect, set, 0); + } + kid = kid->GetNextSibling(); + } +} + +mozilla::dom::Element* +nsTextControlFrame::GetPseudoElement(CSSPseudoElementType aType) +{ + if (aType == CSSPseudoElementType::placeholder) { + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); + return txtCtrl->GetPlaceholderNode(); + } + + return nsContainerFrame::GetPseudoElement(aType); +} + +NS_IMETHODIMP +nsTextControlFrame::EditorInitializer::Run() +{ + if (!mFrame) { + return NS_OK; + } + + // Need to block script to avoid bug 669767. + nsAutoScriptBlocker scriptBlocker; + + nsCOMPtr<nsIPresShell> shell = + mFrame->PresContext()->GetPresShell(); + bool observes = shell->ObservesNativeAnonMutationsForPrint(); + shell->ObserveNativeAnonMutationsForPrint(true); + // This can cause the frame to be destroyed (and call Revoke()). + mFrame->EnsureEditorInitialized(); + shell->ObserveNativeAnonMutationsForPrint(observes); + + // The frame can *still* be destroyed even though we have a scriptblocker, + // bug 682684. + if (!mFrame) { + return NS_ERROR_FAILURE; + } + + mFrame->FinishedInitializer(); + return NS_OK; +} diff --git a/layout/forms/nsTextControlFrame.h b/layout/forms/nsTextControlFrame.h new file mode 100644 index 000000000..a76cba514 --- /dev/null +++ b/layout/forms/nsTextControlFrame.h @@ -0,0 +1,319 @@ +/* -*- 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/. */ + +#ifndef nsTextControlFrame_h___ +#define nsTextControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsIContent.h" +#include "nsITextControlFrame.h" +#include "nsITextControlElement.h" +#include "nsIStatefulFrame.h" + +class nsISelectionController; +class EditorInitializerEntryTracker; +class nsTextEditorState; +class nsIEditor; +namespace mozilla { +enum class CSSPseudoElementType : uint8_t; +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +class nsTextControlFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator, + public nsITextControlFrame, + public nsIStatefulFrame +{ +public: + NS_DECL_FRAMEARENA_HELPERS + + NS_DECLARE_FRAME_PROPERTY_DELETABLE(ContentScrollPos, nsPoint) + + explicit nsTextControlFrame(nsStyleContext* aContext); + virtual ~nsTextControlFrame(); + + virtual void DestroyFrom(nsIFrame* aDestructRoot) override; + + virtual nsIScrollableFrame* GetScrollTargetFrame() override { + return do_QueryFrame(PrincipalChildList().FirstChild()); + } + + virtual nscoord GetMinISize(nsRenderingContext* aRenderingContext) override; + virtual nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override; + + virtual mozilla::LogicalSize + ComputeAutoSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, + nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorder, + const mozilla::LogicalSize& aPadding, + ComputeSizeFlags aFlags) override; + + virtual void Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override; + virtual bool IsXULCollapsed() override; + + virtual bool IsLeaf() const override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override + { + aResult.AssignLiteral("nsTextControlFrame"); + return NS_OK; + } +#endif + + virtual bool IsFrameOfType(uint32_t aFlags) const override + { + // nsStackFrame is already both of these, but that's somewhat bogus, + // and we really mean it. + return nsContainerFrame::IsFrameOfType(aFlags & + ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock)); + } + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) override; + + virtual void SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) override; + + virtual mozilla::dom::Element* + GetPseudoElement(mozilla::CSSPseudoElementType aType) override; + +//==== BEGIN NSIFORMCONTROLFRAME + virtual void SetFocus(bool aOn , bool aRepaint) override; + virtual nsresult SetFormProperty(nsIAtom* aName, const nsAString& aValue) override; + +//==== END NSIFORMCONTROLFRAME + +//==== NSITEXTCONTROLFRAME + + NS_IMETHOD GetEditor(nsIEditor **aEditor) override; + NS_IMETHOD SetSelectionStart(int32_t aSelectionStart) override; + NS_IMETHOD SetSelectionEnd(int32_t aSelectionEnd) override; + NS_IMETHOD SetSelectionRange(int32_t aSelectionStart, + int32_t aSelectionEnd, + SelectionDirection aDirection = eNone) override; + NS_IMETHOD GetSelectionRange(int32_t* aSelectionStart, + int32_t* aSelectionEnd, + SelectionDirection* aDirection = nullptr) override; + NS_IMETHOD GetOwnedSelectionController(nsISelectionController** aSelCon) override; + virtual nsFrameSelection* GetOwnedFrameSelection() override; + + nsresult GetPhonetic(nsAString& aPhonetic) override; + + /** + * Ensure mEditor is initialized with the proper flags and the default value. + * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created + * @throws various and sundry other things + */ + virtual nsresult EnsureEditorInitialized() override; + +//==== END NSITEXTCONTROLFRAME + +//==== NSISTATEFULFRAME + + NS_IMETHOD SaveState(nsPresState** aState) override; + NS_IMETHOD RestoreState(nsPresState* aState) override; + +//=== END NSISTATEFULFRAME + +//==== OVERLOAD of nsIFrame + virtual nsIAtom* GetType() const override; + + /** handler for attribute changes to mContent */ + virtual nsresult AttributeChanged(int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType) override; + + nsresult GetText(nsString& aText); + + virtual nsresult PeekOffset(nsPeekOffsetStruct *aPos) override; + + NS_DECL_QUERYFRAME + +protected: + /** + * Launch the reflow on the child frames - see nsTextControlFrame::Reflow() + */ + void ReflowTextControlChild(nsIFrame* aFrame, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus, + ReflowOutput& aParentDesiredSize); + +public: //for methods who access nsTextControlFrame directly + void SetValueChanged(bool aValueChanged); + + // called by the focus listener + nsresult MaybeBeginSecureKeyboardInput(); + void MaybeEndSecureKeyboardInput(); + +#define DEFINE_TEXTCTRL_FORWARDER(type, name) \ + type name() { \ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); \ + NS_ASSERTION(txtCtrl, "Content not a text control element"); \ + return txtCtrl->name(); \ + } +#define DEFINE_TEXTCTRL_CONST_FORWARDER(type, name) \ + type name() const { \ + nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent()); \ + NS_ASSERTION(txtCtrl, "Content not a text control element"); \ + return txtCtrl->name(); \ + } + + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsSingleLineTextControl) + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsTextArea) + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsPlainTextControl) + DEFINE_TEXTCTRL_CONST_FORWARDER(bool, IsPasswordTextControl) + DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetCols) + DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetWrapCols) + DEFINE_TEXTCTRL_CONST_FORWARDER(int32_t, GetRows) + +#undef DEFINE_TEXTCTRL_CONST_FORWARDER +#undef DEFINE_TEXTCTRL_FORWARDER + +protected: + class EditorInitializer; + friend class EditorInitializer; + friend class nsTextEditorState; // needs access to UpdateValueDisplay + + // Temp reference to scriptrunner + // We could make these auto-Revoking via the "delete" entry for safety + NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(TextControlInitializer, + EditorInitializer) + + class EditorInitializer : public mozilla::Runnable { + public: + explicit EditorInitializer(nsTextControlFrame* aFrame) : + mFrame(aFrame) {} + + NS_IMETHOD Run() override; + + // avoids use of nsWeakFrame + void Revoke() { + mFrame = nullptr; + } + + private: + nsTextControlFrame* mFrame; + }; + + class ScrollOnFocusEvent; + friend class ScrollOnFocusEvent; + + class ScrollOnFocusEvent : public mozilla::Runnable { + public: + explicit ScrollOnFocusEvent(nsTextControlFrame* aFrame) : + mFrame(aFrame) {} + + NS_DECL_NSIRUNNABLE + + void Revoke() { + mFrame = nullptr; + } + + private: + nsTextControlFrame* mFrame; + }; + + nsresult OffsetToDOMPoint(int32_t aOffset, nsIDOMNode** aResult, int32_t* aPosition); + + /** + * Update the textnode under our anonymous div to show the new + * value. This should only be called when we have no editor yet. + * @throws NS_ERROR_UNEXPECTED if the div has no text content + */ + nsresult UpdateValueDisplay(bool aNotify, + bool aBeforeEditorInit = false, + const nsAString *aValue = nullptr); + + /** + * Get the maxlength attribute + * @param aMaxLength the value of the max length attr + * @returns false if attr not defined + */ + bool GetMaxLength(int32_t* aMaxLength); + + /** + * Find out whether an attribute exists on the content or not. + * @param aAtt the attribute to determine the existence of + * @returns false if it does not exist + */ + bool AttributeExists(nsIAtom *aAtt) const + { return mContent && mContent->HasAttr(kNameSpaceID_None, aAtt); } + + /** + * We call this when we are being destroyed or removed from the PFM. + * @param aPresContext the current pres context + */ + void PreDestroy(); + + // Compute our intrinsic size. This does not include any borders, paddings, + // etc. Just the size of our actual area for the text (and the scrollbars, + // for <textarea>). + mozilla::LogicalSize CalcIntrinsicSize(nsRenderingContext* aRenderingContext, + mozilla::WritingMode aWM, + float aFontSizeInflation) const; + + nsresult ScrollSelectionIntoView() override; + +private: + //helper methods + nsresult SetSelectionInternal(nsIDOMNode *aStartNode, int32_t aStartOffset, + nsIDOMNode *aEndNode, int32_t aEndOffset, + SelectionDirection aDirection = eNone); + nsresult SelectAllOrCollapseToEndOfText(bool aSelect); + nsresult SetSelectionEndPoints(int32_t aSelStart, int32_t aSelEnd, + SelectionDirection aDirection = eNone); + + /** + * Return the root DOM element, and implicitly initialize the editor if needed. + */ + mozilla::dom::Element* GetRootNodeAndInitializeEditor(); + nsresult GetRootNodeAndInitializeEditor(nsIDOMElement **aRootElement); + + void FinishedInitializer() { + Properties().Delete(TextControlInitializer()); + } + +private: + // these packed bools could instead use the high order bits on mState, saving 4 bytes + bool mEditorHasBeenInitialized; + bool mIsProcessing; + // Keep track if we have asked a placeholder node creation. + bool mUsePlaceholder; + +#ifdef DEBUG + bool mInEditorInitialization; + friend class EditorInitializerEntryTracker; +#endif + + nsRevocableEventPtr<ScrollOnFocusEvent> mScrollEvent; +}; + +#endif + + diff --git a/layout/forms/test/bug287446_subframe.html b/layout/forms/test/bug287446_subframe.html new file mode 100644 index 000000000..626430268 --- /dev/null +++ b/layout/forms/test/bug287446_subframe.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> + <head> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script> + function doIs(arg1, arg2, arg3) { + window.parent.postMessage("t " + encodeURIComponent(arg1) + " " + + encodeURIComponent(arg2) + " " + + encodeURIComponent(arg3), "*"); + } + + function $(arg) { return document.getElementById(arg); } + + window.addEventListener("message", + function(evt) { + var t = $("target"); + if (evt.data == "start") { + doIs(t.value, "Test", "Shouldn't have lost our initial value"); + t.focus(); + sendString("Foo"); + doIs(t.value, "FooTest", "Typing should work"); + window.parent.postMessage("c", "*"); + } else { + doIs(evt.data, "continue", "Unexpected message"); + doIs(t.value, "FooTest", "Shouldn't have lost our typed value"); + sendString("Bar"); + doIs(t.value, "FooBarTest", "Typing should still work"); + window.parent.postMessage("f", "*"); + } + }, + "false"); + + </script> + </head> + <body> + <input id="target" value="Test"> + </body> +</html> diff --git a/layout/forms/test/bug477700_subframe.html b/layout/forms/test/bug477700_subframe.html new file mode 100644 index 000000000..067ce68c7 --- /dev/null +++ b/layout/forms/test/bug477700_subframe.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> + <head> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script> + function doIs(arg1, arg2, arg3) { + window.parent.postMessage("t " + encodeURIComponent(arg1) + " " + + encodeURIComponent(arg2) + " " + + encodeURIComponent(arg3), "*"); + } + + function $(arg) { return document.getElementById(arg); } + + window.addEventListener("message", + function(evt) { + doIs(evt.data, "start", "Unexpected message"); + $("target").focus(); + sendString("Test"); + var t = $("target"); + doIs(t.value, "Test", "Typing should work"); + (function() { + SpecialPowers.wrap(t).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor.undo(1); + })() + doIs(t.value, "", "Undo should work"); + (function() { + SpecialPowers.wrap(t).QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement).editor.redo(1); + })() + doIs(t.value, "Test", "Redo should work"); + window.parent.postMessage("f", "*"); + }, + "false"); + + </script> + </head> + <body> + <input id="target"> + </body> +</html> + diff --git a/layout/forms/test/bug536567_iframe.html b/layout/forms/test/bug536567_iframe.html new file mode 100644 index 000000000..b2b2ca60a --- /dev/null +++ b/layout/forms/test/bug536567_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe id="content"></iframe>
+ </body>
+</html>
+
diff --git a/layout/forms/test/bug536567_subframe.html b/layout/forms/test/bug536567_subframe.html new file mode 100644 index 000000000..c1271becf --- /dev/null +++ b/layout/forms/test/bug536567_subframe.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<body> +<input id="target" type="file" /> +<script type="text/javascript"> + +window.onload = function() { + var fileInput = document.getElementById("target"); + fileInput.click(); +}; + +</script> +</body> +</html> diff --git a/layout/forms/test/bug564115_window.html b/layout/forms/test/bug564115_window.html new file mode 100644 index 000000000..b55cc0400 --- /dev/null +++ b/layout/forms/test/bug564115_window.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Window for Bug 564115</title> +</head> +<body> +<input type="text"> +<div style="height: 10000px;"></div> +</body> +</html> diff --git a/layout/forms/test/bug665540_window.xul b/layout/forms/test/bug665540_window.xul new file mode 100644 index 000000000..f0225ef93 --- /dev/null +++ b/layout/forms/test/bug665540_window.xul @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test Select Dropdown Positioning in Fullscreen Window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + sizemode="fullscreen"> + + <script> + opener.SimpleTest.waitForFocus(opener.childFocused, window); + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <select id="select" style="-moz-appearance:none"> + <option id="optiona">a</option> + <option>b</option> + </select> + </body> +</window> diff --git a/layout/forms/test/chrome.ini b/layout/forms/test/chrome.ini new file mode 100644 index 000000000..32d862eec --- /dev/null +++ b/layout/forms/test/chrome.ini @@ -0,0 +1,11 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + bug536567_iframe.html + bug536567_subframe.html + bug665540_window.xul + +[test_bug536567_perwindowpb.html] +[test_bug665540.html] +tags = fullscreen +skip-if = (os == 'linux' && bits == 64) # Bug 888164 diff --git a/layout/forms/test/mochitest.ini b/layout/forms/test/mochitest.ini new file mode 100644 index 000000000..cc8519db7 --- /dev/null +++ b/layout/forms/test/mochitest.ini @@ -0,0 +1,66 @@ +[DEFAULT] +support-files = + bug287446_subframe.html + bug477700_subframe.html + bug564115_window.html + +[test_bug231389.html] +[test_bug287446.html] +[test_bug345267.html] +[test_bug346043.html] +[test_bug348236.html] +skip-if = toolkit == 'android' || e10s || os == 'mac' # mac(select form control popup behavior is different) +[test_bug353539.html] +[test_bug365410.html] +[test_bug378670.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug402198.html] +[test_bug411236.html] +[test_bug446663.html] +skip-if = toolkit == 'android' +[test_bug476308.html] +[test_bug477531.html] +[test_bug477700.html] +[test_bug478219.xhtml] +skip-if = toolkit == 'android' +[test_bug534785.html] +[test_bug542914.html] +[test_bug549170.html] +[test_bug562447.html] +[test_bug563642.html] +[test_bug564115.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug571352.html] +skip-if = (os == 'mac' && os_version == '10.10') || toolkit == 'android' #TIMED_OUT # OS X 10.10 - bug 947690 +[test_bug572406.html] +[test_bug572649.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug595310.html] +[test_bug620936.html] +[test_bug644542.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug672810.html] +skip-if = toolkit == 'android' +[test_bug704049.html] +[test_bug717878_input_scroll.html] +[test_bug869314.html] +[test_bug903715.html] +skip-if = toolkit == 'android' || e10s #select elements don't use an in-page popup on Android +[test_bug935876.html] +# Bug 1023472 - Fails when pushed into a different chunk on Android +skip-if = toolkit == 'android' +[test_bug957562.html] +[test_bug960277.html] +[test_bug961363.html] +skip-if = toolkit == 'android' # Bug 1021644 - Fails when pushed into a different chunk on Android +[test_bug1111995.html] +[test_bug1301290.html] +skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' # b2g(resizing textarea not available in b2g) b2g-debug(resizing textarea not available in b2g) b2g-desktop(resizing textarea not available in b2g) +[test_bug1305282.html] +[test_listcontrol_search.html] +skip-if = toolkit == 'android' #select elements don't use an in-page popup on Android +[test_select_prevent_default.html] +[test_select_vertical.html] +skip-if = e10s || toolkit == 'android' # Bug 1170129 - vertical <select> popup not implemented for e10s # <select> elements don't use an in-page popup on Android +[test_textarea_resize.html] +skip-if = toolkit == 'android' diff --git a/layout/forms/test/test_bug1111995.html b/layout/forms/test/test_bug1111995.html new file mode 100644 index 000000000..74ab76b06 --- /dev/null +++ b/layout/forms/test/test_bug1111995.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1111995 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1111995</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1111995 **/ + + function doTest() { + SimpleTest.waitForExplicitFinish(); + + var clicks = 0; + var elms = document.querySelectorAll('.click'); + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + e.addEventListener('click', function(event) { + ++clicks; + }, false); + } + + for (var i = 0; i < elms.length; ++i) { + var e = elms[i]; + synthesizeMouse(e, 3, 3, {}); + } + is(clicks, 0, "click events outside border with radius"); + + clicks = 0; + synthesizeMouse($("t3"), 17, 17, {}); + synthesizeMouse($("t4"), 17, 17, {}); + is(clicks, 2, "click events on border with radius"); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="SimpleTest.waitForFocus(doTest, window)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1111995">Mozilla Bug 1111995</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<input class="click" id="t1" type=button style="width:100px; height:100px; padding:20px; border-radius:50%" value="Round button"> +<button class="click" id="t2" style="width:100px; height:100px; padding:20px; border-radius:50%">Round button</button> +<input class="click" id="t3" type=button style="width:100px; height:100px; border-width:20px; border-radius:50%" value="Round button"> +<button class="click" id="t4" style="width:100px; height:100px; border-width:20px; border-radius:50%">Round button</button> +<input class="click" id="t5" type=button style="width:100px; height:100px; border-radius:50%;overflow:hidden" value="Round button"> +<button class="click" id="t6" style="width:100px; height:100px; border-radius:50%;overflow:hidden">Round button</button> + +</body> +</html> diff --git a/layout/forms/test/test_bug1301290.html b/layout/forms/test/test_bug1301290.html new file mode 100644 index 000000000..5f226c453 --- /dev/null +++ b/layout/forms/test/test_bug1301290.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for Bug 1301290</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + .blue, .green { + border: none; + box-sizing: border-box; + display: block; + width: 200px; + height: 100px; + overflow: scroll; + resize: both; + } + + .blue { + background: blue; + } + + .green { + background: green; + margin-top: -100px; + } + </style> + </head> + <body> + <div class="blue"></div> + <textarea class="green" id="textarea"></textarea> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + addLoadEvent(() => SimpleTest.executeSoon(function() { + var textarea = $("textarea"); + var rect = textarea.getBoundingClientRect(); + + synthesizeMouse(textarea, rect.width - 10, rect.height - 10, { type: "mousedown" }); + synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mousemove" }); + synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mouseup" }); + + var newrect = textarea.getBoundingClientRect(); + ok(newrect.width > rect.width, "width did not increase"); + ok(newrect.height > rect.height, "height did not increase"); + SimpleTest.finish(); + })); + </script> + </body> +</html> diff --git a/layout/forms/test/test_bug1305282.html b/layout/forms/test/test_bug1305282.html new file mode 100644 index 000000000..b43901e6c --- /dev/null +++ b/layout/forms/test/test_bug1305282.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=615697 +--> +<head> + <title>Test for Bug 1305282</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1305282">Mozilla Bug 1305282</a> +<p id="display"></p> +<div id="content"> + <select> + <option>f o o</option> + <option>b a r</option> + <option>b o o</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1305282 **/ + +var select = document.getElementsByTagName('select')[0]; + +select.addEventListener("change", function(aEvent) { + select.removeEventListener("change", arguments.callee, false); + is(select.selectedIndex, 1, "'b a r' option is selected"); + SimpleTest.finish(); +}, false); + +select.addEventListener("focus", function() { + select.removeEventListener("focus", arguments.callee, false); + SimpleTest.executeSoon(function () { + synthesizeKey("VK_DOWN", {}); + SimpleTest.executeSoon(function () { + synthesizeKey('b', {}); + SimpleTest.executeSoon(function () { + synthesizeKey(' ', {}); + SimpleTest.executeSoon(function () { + synthesizeKey("VK_RETURN", {}); + }); + }); + }); + }); +}, false); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + select.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug231389.html b/layout/forms/test/test_bug231389.html new file mode 100644 index 000000000..6a6cc9e50 --- /dev/null +++ b/layout/forms/test/test_bug231389.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=231389 +--> +<head> + <title>Test for Bug 231389</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=231389">Mozilla Bug 231389</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 231389 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var area = document.getElementById("area"); + var val = area.value; + var pos = val.indexOf("purposes"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.selectionStart = pos; + area.selectionEnd = pos; + setTimeout(function() { + isnot(area.scrollTop, 0, "The textarea's insertion point should be scrolled into view"); + + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug287446.html b/layout/forms/test/test_bug287446.html new file mode 100644 index 000000000..e551ba998 --- /dev/null +++ b/layout/forms/test/test_bug287446.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=287446 +--> +<head> + <title>Test for Bug 287446</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287446">Mozilla Bug 287446</a> +<p id="display"> + <iframe id="i" + src="http://example.com/tests/layout/forms/test/bug287446_subframe.html"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 287446 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + isnot(window.location.host, "example.com", "test is not testing cross-site"); + var accessed = false; + try { + $("i").contentDocument.documentElement; + accessed = true; + } catch(e) {} + is(accessed, false, "Shouldn't be able to access cross-site"); + + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "none", + "toggling display failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "inline", + "toggling display back failed"); + + $("i").contentWindow.postMessage("start", "*"); +}); + +function continueTest() { + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "none", + "toggling display second time failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "inline", + "toggling display back second time failed"); + +$("i").contentWindow.postMessage("continue", "*"); +} + +window.addEventListener("message", + function(evt) { + var arr = evt.data.split(/ /).map(decodeURIComponent); + if (arr[0] == 't') { + is(arr[1], arr[2], arr[3]); + } else if (arr[0] == 'c') { + continueTest(); + } else if (arr[0] == 'f') { + SimpleTest.finish(); + } + }, + false); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug345267.html b/layout/forms/test/test_bug345267.html new file mode 100644 index 000000000..0c1c4c9a4 --- /dev/null +++ b/layout/forms/test/test_bug345267.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345267 +--> +<head> + <title>Test for Bug 345267</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345267">Mozilla Bug 345267</a> +<p id="display"> + <input id="d1" maxlength="3" value="abcde"> + <input id="d2" maxlength="3"> + <input id="d3" maxlength="3"> + <input id="d4" value="abcdefghijk"> + <input id="target" value="abcdefghijklm" maxlength="3"> +</p> +<div id="content" style="display: none"> + <input id="u1" maxlength="3" value="abcdef"> + <input id="u2" maxlength="3"> + <input id="u3" maxlength="3"> + <input id="u4" value="abcdefghijkl"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 345267 **/ +SimpleTest.waitForExplicitFinish(); +// Turn off Spatial Navigation to stop if from hijacking "left" keypress event. +SpecialPowers.pushPrefEnv({"set":[['snav.enabled', false]]}, runTest); + +function runTest() { + is($("d1").value, "abcde", + "Displayed initial value should not be truncated by maxlength"); + is($("u1").value, "abcdef", + "Undisplayed initial value should not be truncated by maxlength"); + + $("d2").value = "abcdefg"; + is($("d2").value, "abcdefg", + "Displayed set value should not be truncated by maxlength"); + + $("u2").value = "abcdefgh"; + is($("u2").value, "abcdefgh", + "Undisplayed set value should not be truncated by maxlength"); + + $("d3").defaultValue = "abcdefghi"; + is($("d3").value, "abcdefghi", + "Displayed set defaultValue should not be truncated by maxlength"); + + $("u3").defaultValue = "abcdefghij"; + is($("u3").value, "abcdefghij", + "Undisplayed set defaultValue should not be truncated by maxlength"); + + $("d4").maxLength = "3"; + is($("d4").value, "abcdefghijk", + "Displayed: setting maxLength should not truncate existing value"); + + $("u4").maxLength = "3"; + is($("u4").value, "abcdefghijkl", + "Undisplayed: setting maxLength should not truncate existing value"); + + // Now start the editing tests + is($("target").value, "abcdefghijklm", "Test starting state incorrect"); + $("target").focus(); + $("target").selectionStart = $("target").selectionEnd = 13; + sendKey("back_space"); + is($("target").value, "abcdefghijkl", "Should only delete one char"); + sendKey("back_space"); + is($("target").value, "abcdefghijk", "Should only delete one char again"); + (function () { + SpecialPowers.wrap($("target")).controllers.getControllerForCommand('cmd_undo') + .doCommand('cmd_undo'); + })(); + is($("target").value, "abcdefghijklm", + "Should be able to undo deletion in the face of maxlength"); + sendString("nopq"); + is($("target").value, "abcdefghijklm", + "Typing should have no effect when already past maxlength"); + + $("target").value = ""; + sendString("abcde"); + is($("target").value, "abc", "Typing should be limited by maxlength"); + + $("target").value = ""; + sendString("ad"); + sendKey("left"); + sendString("bc"); + is($("target").value, "abd", "Typing should be limited by maxlength again"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug346043.html b/layout/forms/test/test_bug346043.html new file mode 100644 index 000000000..f509ae1fc --- /dev/null +++ b/layout/forms/test/test_bug346043.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=346043 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 346043</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript"> + /** Test for Bug 346043 **/ + function test(select, index, useEscape) { + select.selectedIndex = index; + is(select.selectedIndex, index, "Selected index is broken"); + + // Open the select dropdown. + // + // Using Alt+Down (instead of a click) seems necessary to make subsequent + // mouse events work with the dropdown itself, instead of the window below + // it. + select.focus(); + synthesizeKey("VK_DOWN", {altKey: true}); + + var options = select.getElementsByTagName("option"); + synthesizeMouseAtCenter(options[1], + {type: "mousemove", clickCount: 0}); + + // Close the select dropdown. + if (useEscape) + synthesizeKey("VK_ESCAPE", {}); // Tests a different code path. + select.blur(); + is(select.selectedIndex, index, "Selected index shouldn't change"); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + test(document.getElementById("test-unselected"), -1, true); + test(document.getElementById("test-unselected"), -1, false); + test(document.getElementById("test-disabled"), 0, true); + test(document.getElementById("test-disabled"), 0, false); + SimpleTest.finish(); + }); + </script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346043">Mozilla Bug 346043</a> + <p id="display"></p> + <div id="content"> + <select id="test-unselected"> + <option>A</option> + <option>B</option> + <option>C</option> + </select> + + <select id="test-disabled"> + <option disabled>A</option> + <option>B</option> + <option>C</option> + </select> + </div> + <pre id="test"></pre> +</body> +</html> diff --git a/layout/forms/test/test_bug348236.html b/layout/forms/test/test_bug348236.html new file mode 100644 index 000000000..ff89c3bd9 --- /dev/null +++ b/layout/forms/test/test_bug348236.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=348236 +--> +<head> + + <title>Test for Bug 348236</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <style type="text/css"> + #eSelect { + position: fixed; top:0; left: 350px; font-size: 24px; width: 100px + } + #eSelect option { + margin: 0; padding: 0; height: 24px + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=348236">Mozilla Bug 348236</a> +<p id="display"></p> +<div id="content"> + + <select id="eSelect" size="1" onchange="++this.onchangeCount"> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> +</div> +<pre id="test"> +<script type="text/javascript"> + + /** Test for Bug 348236 **/ + +SimpleTest.waitForExplicitFinish() +addLoadEvent(function test() { + + var + CI = SpecialPowers.Components.interfaces, + WinUtils = SpecialPowers.getDOMWindowUtils(window), + sec = netscape.security, + eSelect = $("eSelect"), + IDOMEvent = CI.nsIDOMEvent, + IDOMKeyEvent = CI.nsIDOMKeyEvent, + timeout = 0 // Choose a larger value like 500 ms if you want to see what's happening. + + function keypressOnSelect(key, modifiers) { + WinUtils.focus(eSelect) + WinUtils.sendKeyEvent("keyup", key, 0, modifiers) + WinUtils.sendKeyEvent("keypress", key, 0, modifiers) + WinUtils.sendKeyEvent("keydown", key, 0, modifiers) + } + + function testKey(key, modifiers, keyString, functionToContinue) { + var selectGotClick + function clickListener() { selectGotClick = true } + eSelect.selectedIndex = 0 + eSelect.onchangeCount = 0 + + // Drop the SELECT down. + keypressOnSelect(key, modifiers) + // This timeout and the following are necessary to let the sent events take effect. + setTimeout(cont1, timeout) + function cont1() { + // Move the mouse over option 3 (90 = 3 (rows) * 24 (row height) + 18). + WinUtils.sendMouseEvent("mousemove", 355, 90, 0, 0, 0, true) + setTimeout(cont2, timeout) + } + function cont2() { + // Close the select. + keypressOnSelect(key, modifiers) + setTimeout(cont3, timeout) + } + function cont3() { + is(eSelect.value, "3", "Select's value should be 3 after hovering over option 3 and pressing " + keyString + ".") + is(eSelect.onchangeCount, 1, "Onchange should have fired once.") + + // Simulate click on area to the left of the select. + eSelect.addEventListener("click", clickListener, true) + selectGotClick = false + WinUtils.sendMouseEvent("mousedown", 320, 0, 0, 0, 0, true) + WinUtils.sendMouseEvent("mouseup", 320, 0, 0, 0, 0, true) + setTimeout(cont4, timeout) + } + function cont4() { + eSelect.removeEventListener("click", clickListener, true) + ok(!selectGotClick, "SELECT must not capture mouse events after closing it with " + keyString + ".") + functionToContinue() + } + } + + + // Quick sanity checks. + is(eSelect.value, "1", "SELECT value should be 1 after load.") + is(eSelect.selectedIndex, 0, "SELECT selectedIndex should be 0 after load.") + + // Check if sending key events works. + keypressOnSelect(IDOMKeyEvent.DOM_VK_DOWN, 0) + is(eSelect.value, "2", "SELECT value should be 2 after pressing Down.") + + // Test ALT-Down. + testKey(IDOMKeyEvent.DOM_VK_DOWN, IDOMEvent.ALT_MASK, "ALT-Down", nextKey1) + function nextKey1() { + // Test ALT-Up. + testKey(IDOMKeyEvent.DOM_VK_UP, IDOMEvent.ALT_MASK, "ALT-Up", nextKey2) + } + function nextKey2() { + // Test the F4 key on OS/2 and Windows. + if (/OS\/2|Win/i.test(navigator.platform)) + testKey(IDOMKeyEvent.DOM_VK_F4, 0, "F4", finished) + else + finished() + } + function finished() { + // Reset value to get the expected value if we reload the page. + eSelect.selectedIndex = 0 + SimpleTest.finish() + } +}) + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug353539.html b/layout/forms/test/test_bug353539.html new file mode 100644 index 000000000..6ada2b115 --- /dev/null +++ b/layout/forms/test/test_bug353539.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=353539 +--> +<head> + <title>Test for Bug 353539</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=353539">Mozilla Bug 353539</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 353539 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var area = document.getElementById("area"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.focus(); + setTimeout(function() { + is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view"); + + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug365410.html b/layout/forms/test/test_bug365410.html new file mode 100644 index 000000000..93a55eac6 --- /dev/null +++ b/layout/forms/test/test_bug365410.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=365410 +--> +<head> + <title>Test for Bug 365410</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365410">Mozilla Bug 365410</a> +<p id="display"> +<select id="test0" multiple="multiple"> + <option id="option">Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test1" multiple="multiple" size="1"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test2" multiple="multiple" size="1" style="height:0.9em"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> + <option>Item 6</option> + <option>Item 7</option> + <option>Item 8</option> + <option>Item 9</option> + <option>Item 10</option> + <option>Item 11</option> + <option>Item 12</option> + <option>Item 13</option> + <option>Item 14</option> + <option>Item 15</option> +</select> +<select id="test3" multiple="multiple" size="1"></select> +<select id="test4" multiple="multiple" size="1" style="height:0.9em"></select> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 365410 **/ + +function pageUpDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_up"); + sendKey("page_down"); + is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index); +} + +function upDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("down"); + sendKey("down"); + sendKey("up"); + sendKey("down"); + is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index); +} + +function setHeight(id, h) { + var elm = document.getElementById(id); + elm.style.height = h + 'px'; +} + +function runTest() { + var h = document.getElementById("option").clientHeight; + var list5itemsHeight = h * 5.5; + setHeight("test0", list5itemsHeight); + setHeight("test1", list5itemsHeight); + setHeight("test3", list5itemsHeight); + + pageUpDownTest("test0",8); + pageUpDownTest("test1",8); + pageUpDownTest("test2",2); + pageUpDownTest("test3",-1); + pageUpDownTest("test4",-1); + upDownTest("test0",2); + upDownTest("test1",2); + upDownTest("test2",2); + upDownTest("test3",-1); + upDownTest("test4",-1); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +// Turn off spatial nav so that it does not hijack the up and down events. +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, runTest); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug378670.html b/layout/forms/test/test_bug378670.html new file mode 100644 index 000000000..098ff75be --- /dev/null +++ b/layout/forms/test/test_bug378670.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=378670 +--> +<head> + <title>Test for Bug 378670</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=378670">Mozilla Bug 378670</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +Clicking on the select should not crash Mozilla +<select id="select"> +<option>1</option> +<option>2</option> +</select> + +<pre id="test"> +<script> +document.body.addEventListener('popupshowing', function(e) {e.target.parentNode.removeChild(e.target) }, true); +</script> +<script type="application/javascript"> + +/** Test for Bug 378670 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function clickit() { + var select = document.getElementById('select'); + + sendMouseEvent({type:'mousedown'}, select); + sendMouseEvent({type:'mouseup'}, select); + sendMouseEvent({type:'click'}, select); + + setTimeout(finish, 200); +} + +window.addEventListener('load', clickit, false); + +function finish() +{ + ok(true, "This is a mochikit version of a crash test. To complete is to pass."); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug402198.html b/layout/forms/test/test_bug402198.html new file mode 100644 index 000000000..647a892fb --- /dev/null +++ b/layout/forms/test/test_bug402198.html @@ -0,0 +1,77 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402198 +--> +<head> + <title>Test for Bug 402198</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402198">Mozilla Bug 402198</a> +<p id="display"> + <select></select> + <select><optgroup></optgroup></select> + <legend style="overflow: scroll;"> + <select></select> + </legend> + <span></span> + <span style="display: -moz-box;"> + <select></select> + </span> + <legend style=" "> + <label style="overflow: scroll; display: -moz-box;"> + <select></select> + </label> + </legend> + <legend> + <label style=" display: table;"> + <select id="a"> + <option>High Grade</option> + <option>Medium Grade</option> + </select> + </label> + </legend> + + <input> + <select multiple="multiple"></select> + <select style="overflow: scroll; display: -moz-box;"> + <optgroup></optgroup> + <optgroup style="display: table-cell;"></optgroup> + </select> +</p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +function doe3() { + document.documentElement.style.display = 'none'; + document.body.offsetHeight; + document.documentElement.style.display = ''; + document.body.offsetHeight; + + document.getElementById('a').focus(); + document.body.style.display = 'none'; + + synthesizeKey('KEY_Tab', { code: "Tab", shiftKey: true }); + + is(0, 0, "this is a crash/assertion test, so we're ok if we survived this far"); + setTimeout(function() {document.body.style.display = ''; SimpleTest.finish();}, 0); +} + +function do_test() { + setTimeout(doe3,300); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(do_test); +</script> +</pre> + +<style> +* {quotes: "quote" "quote" !important;} +</style> + +</body> +</html> diff --git a/layout/forms/test/test_bug411236.html b/layout/forms/test/test_bug411236.html new file mode 100644 index 000000000..a5369b832 --- /dev/null +++ b/layout/forms/test/test_bug411236.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=411236 +--> +<head> + <title>Test for Bug 411236</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=411236">Mozilla Bug 411236</a> +<p id="display"></p> +<div id="content"> + <input type="file" onfocus="window.oTarget = event.originalTarget;" + onclick="window.fileInputGotClick = true; return false;" + id="fileinput"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 411236 **/ + +window.oTarget = null; +window.fileInputGotClick = false; + +function tab() { + var utils = SpecialPowers.DOMWindowUtils; + // Send tab key events. + var key = SpecialPowers.Ci.nsIDOMKeyEvent.DOM_VK_TAB; + utils.sendKeyEvent("keydown", key, 0, 0); + utils.sendKeyEvent("keypress", key, 0, 0); + utils.sendKeyEvent("keyup", key, 0, 0); +} + +function test() { + // Try to find the 'Browse...' using tabbing. + var i = 0; + while (!window.oTarget && i < 100) { + ++i; + tab(); + } + + if (i >= 100) { + ok(false, "Couldn't find an input element!"); + SimpleTest.finish(); + return; + } + + ok(window.oTarget instanceof HTMLButtonElement, "Should have focused an input element!") + ok(SpecialPowers.wrap(window.oTarget).type == "button", "Should have focused 'Browse...' button!"); + var e = document.createEvent("mouseevents"); + e.initMouseEvent("click", true, true, window, 0, 1, 1, 1, 1, + false, false, false, false, 0, null); + SpecialPowers.wrap(window.oTarget).dispatchEvent(e); + ok(window.fileInputGotClick, + "File input should have got a click event, but not open the file dialog."); + SimpleTest.finish(); +} + +function beginTest() { + // accessibility.tabfocus must be set to value 7 before running test also + // on a mac. + SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}, do_test); +} + +function do_test() { + window.focus(); + document.getElementById('fileinput').focus(); + setTimeout(test, 100); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(beginTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug446663.html b/layout/forms/test/test_bug446663.html new file mode 100644 index 000000000..f12a47248 --- /dev/null +++ b/layout/forms/test/test_bug446663.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=446663 +--> +<head> + <title>Test for Bug 446663</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446663">Mozilla Bug 446663</a> +<p id="display"> +<style>#bug446663_a:focus{overflow:hidden}</style> +<input id="bug446663_a"><input id="bug446663_b"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 446663 **/ + +function test_edit_cmds(id) { + + var elm = document.getElementById(id); + elm.focus(); + elm.select(); + SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_cut') + .doCommand('cmd_cut'); + is(elm.value, '', id + " cut"); + + SpecialPowers.wrap(elm).controllers.getControllerForCommand('cmd_undo') + .doCommand('cmd_undo'); + is(elm.value, '123', id + " undo"); +} + +var inputHappened = false; +function inputListener() { + inputHappened = true; + $(id).removeEventListener("input", inputListener, false); +} + +var id = 'bug446663_a' +var elm = document.getElementById(id); +elm.focus(); +var x = document.body.offsetHeight; +$(id).addEventListener("input", inputListener, false); +sendChar('1'); +is(inputHappened, true, "How come no input?"); +sendChar('3'); +sendKey('LEFT') +sendChar('2'); +elm.blur(); +x = document.body.offsetHeight; +is(elm.value, '123', id + " edit"); +test_edit_cmds(id) + +id = 'bug446663_b' +elm = document.getElementById(id); +elm.focus(); +sendChar('1'); +elm.style.display = 'none' +var x = document.body.offsetHeight; +elm.style.display = 'inline' +x = document.body.offsetHeight; +sendChar('3'); +sendKey('LEFT') +sendChar('2'); +elm.blur(); +x = document.body.offsetHeight; +is(elm.value, '123', id + " edit"); +test_edit_cmds(id) + +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug476308.html b/layout/forms/test/test_bug476308.html new file mode 100644 index 000000000..d8f572803 --- /dev/null +++ b/layout/forms/test/test_bug476308.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=476308 +--> +<head> + <title>Test for Bug 345267</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<button style="-moz-appearance: none; width: 100px; height: 60px; background-color: red; border: 2px solid green; box-shadow: 30px 0px 3.5px black; position: absolute; top: 300px; left: 20px;" + id="button1">1</button> + +<br /> +<div style="width: 100px; height: 100px; background-color: green; border: 3px dotted blue; box-shadow: -30px -20px 0px black; position: absolute; top: 500px; left: 70px;" + id="div1">2</div> + +<script type="text/javascript"> + var elem = document.elementFromPoint(130, 310); + isnot(elem, document.getElementById("button1"), "button1's box-shadow is receiving events when it shouldn't"); + + elem = document.elementFromPoint(50, 500); + isnot(elem, document.getElementById("div1"), "div1's box-shadow is receiving events when it shouldn't"); +</script> + +</body> +</html> + diff --git a/layout/forms/test/test_bug477531.html b/layout/forms/test/test_bug477531.html new file mode 100644 index 000000000..d766d3f7d --- /dev/null +++ b/layout/forms/test/test_bug477531.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=477531 +--> +<head> + <title>Test for Bug 477531</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style type="text/css"> + #s { + margin-left: 10px; + } + + #s:indeterminate { + margin-left: 30px; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477531">Mozilla Bug 477531</a> +<p id="display"></p> +<div id="content"> + +<input type="checkbox" id="s" /> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 477531 **/ +is(document.defaultView.getComputedStyle($("s"), null).getPropertyValue("margin-left"), + "10px", + "Non-indeterminate checkbox should have a margin of 10px"); + +$("s").indeterminate = true; + +is(document.defaultView.getComputedStyle($("s"), null).getPropertyValue("margin-left"), + "30px", + "Indeterminate checkbox should have a margin of 30px"); + +$("s").setAttribute("type", "radio"); + +is(document.defaultView.getComputedStyle($("s"), null).getPropertyValue("margin-left"), + "30px", + "Setting an indeterminate element to type radio should give it indeterminate styles"); + +$("s").setAttribute("type", "checkbox"); + +is(document.defaultView.getComputedStyle($("s"), null).getPropertyValue("margin-left"), + "30px", + "Setting an indeterminate element to type checkbox should give it indeterminate styles"); + +$("s").indeterminate = false; + +is(document.defaultView.getComputedStyle($("s"), null).getPropertyValue("margin-left"), + "10px", + "Newly non-indeterminate checkbox should have a margin of 10px"); + +</script> +</pre> +</body> +</html> + diff --git a/layout/forms/test/test_bug477700.html b/layout/forms/test/test_bug477700.html new file mode 100644 index 000000000..45537257b --- /dev/null +++ b/layout/forms/test/test_bug477700.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=477700 +--> +<head> + <title>Test for Bug 477700</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477700">Mozilla Bug 477700</a> +<p id="display"> + <iframe id="i" + src="http://example.com/tests/layout/forms/test/bug477700_subframe.html"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 477700 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + isnot(window.location.host, "example.com", "test is not testing cross-site"); + var accessed = false; + try { + $("i").contentDocument.documentElement; + accessed = true; + } catch(e) {} + is(accessed, false, "Shouldn't be able to access cross-site"); + + $("i").style.display = "none"; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "none", + "toggling display failed"); + $("i").style.display = ""; + document.body.offsetWidth; + is(document.defaultView.getComputedStyle($("i"), "").display, "inline", + "toggling display back failed"); + + $("i").contentWindow.postMessage("start", "*"); +}); + +window.addEventListener("message", + function(evt) { + var arr = evt.data.split(/ /).map(decodeURIComponent); + if (arr[0] == 't') { + is(arr[1], arr[2], arr[3]); + } else if (arr[0] == 'f') { + SimpleTest.finish(); + } + }, + false); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug478219.xhtml b/layout/forms/test/test_bug478219.xhtml new file mode 100644 index 000000000..42e5f07b7 --- /dev/null +++ b/layout/forms/test/test_bug478219.xhtml @@ -0,0 +1,38 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=478219 +--> +<head> + <title>Test for Bug 478219</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <style type="text/css"> + + </style> +</head> + +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478219">Mozilla Bug 478219</a> +<input style="overflow: scroll;-moz-binding:url(#xbl);" type="image"/> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var win = window.open('', '', 'width=100,height=100'); + win.close(); + is(win.closed, true, "Window should have been opened"); + is(win.opener, window, "Opened window should have as opener this window"); +</script> +</pre> + +<bindings xmlns="http://www.mozilla.org/xbl"> +<binding id="xbl" inheritstyle="false"> +<content> + +<div xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"> +<style>style {-moz-binding:url(#xbl);</style> +</div> +<textarea xmlns="http://www.w3.org/1999/xhtml"></textarea> +</content> +</binding> +</bindings> +</body> +</html> diff --git a/layout/forms/test/test_bug534785.html b/layout/forms/test/test_bug534785.html new file mode 100644 index 000000000..76590798f --- /dev/null +++ b/layout/forms/test/test_bug534785.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=534785 +--> +<head> + <title>Test for Bug 534785</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534785">Mozilla Bug 534785</a> +<p id="display"></p> +<input type="text" value="test"> +<div id="reframe"> +<textarea></textarea> +<textarea>test</textarea> +<input type="text"> +<input type="text" value="test"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 534785 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.addEventListener("focus", function() { + is(i.value, "test", "Sanity check"); + + is(document.activeElement, i, "Should be focused before frame reconstruction"); + synthesizeKey("1", {}); + is(i.value, "1test", "Can accept keyboard events before frame reconstruction"); + + // force frame reconstruction + i.style.display = "none"; + document.offsetHeight; + i.style.display = ""; + document.offsetHeight; + + is(document.activeElement, i, "Should be focused after frame reconstruction"); + synthesizeKey("2", {}); + is(i.value, "12test", "Can accept keyboard events after frame reconstruction"); + + // Make sure reframing happens gracefully + var reframeDiv = document.getElementById("reframe"); + var textAreaWithoutValue = reframeDiv.querySelectorAll("textarea")[0]; + var textAreaWithValue = reframeDiv.querySelectorAll("textarea")[1]; + var inputWithoutValue = reframeDiv.querySelectorAll("input")[0]; + var inputWithValue = reframeDiv.querySelectorAll("input")[1]; + reframeDiv.style.display = "none"; + document.body.offsetWidth; + reframeDiv.style.display = ""; + document.body.offsetWidth; + [textAreaWithoutValue, inputWithoutValue].forEach(function (elem) { + is(elem.value, "", "Value should persist correctly"); + }); + [textAreaWithValue, inputWithValue].forEach(function (elem) { + is(elem.value, "test", "Value should persist correctly"); + }); + [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "submit"); + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly"); + is(inputWithValue.value, "test", "Value should persist correctly"); + [inputWithoutValue, inputWithValue].forEach(elem => elem.type = "text"); + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly"); + is(inputWithValue.value, "test", "Value should persist correctly"); + [inputWithoutValue, inputWithValue].forEach(elem => elem.focus()); // initialze the editor + reframeDiv.style.display = "none"; + document.body.offsetWidth; + reframeDiv.style.display = ""; + document.body.offsetWidth; + is(inputWithoutValue.value, "", "Value should persist correctly with editor"); + is(inputWithValue.value, "test", "Value should persist correctly with editor"); + + SimpleTest.finish(); + }, false); + i.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug536567_perwindowpb.html b/layout/forms/test/test_bug536567_perwindowpb.html new file mode 100644 index 000000000..8bb2f68ce --- /dev/null +++ b/layout/forms/test/test_bug536567_perwindowpb.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=536567 +--> +<head> + <title>Test for Bug 536567</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536567">Mozilla Bug 536567</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 536567 **/ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cm = Components.manager; + +Cu.import("resource://gre/modules/Services.jsm"); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +var tmpDir = Services.dirsvc.get("TmpD", Ci.nsILocalFile); +var homeDir = Services.dirsvc.get("Desk", Ci.nsILocalFile); + +function newDir() { + var dir = tmpDir.clone(); + dir.append("testdir" + Math.floor(Math.random() * 10000)); + dir.QueryInterface(Ci.nsILocalFile); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0700); + return dir; +} + +var dirs = []; +for(var i = 0; i < 6; i++) { + dirs.push(newDir()); +} +dirs.push(homeDir); +var domains = ['http://mochi.test:8888', 'http://example.org:80', 'http://example.com:80']; +/* + * These tests take 3 args each: + * - which domain to load + * - the filePicker displayDirectory we expect to be set + * - the file to pick (in most cases this will show up in the next test, + * as indicated by the comments) + */ +var tests = [ + "clear history", + [0, 6, 0], // 0 -> 3 + [1, 6, 1], // 1 -> 4 + [2, 6, 2], // 2 -> 5 + [0, 0, 3], // 3 -> 6 + [1, 1, 1], // 4 -> 8 + [2, 2, 2], // 5 -> 9 + [0, 3, 1], // 6 -> 7 + [0, 1, 0], // 7 -> x + [1, 1, 1], // 8 -> x + [2, 2, 2], // 9 -> x + "clear history", + [0, 6, 0], // 11 -> 15 + [1, 6, 1], // 12 -> 16 + [2, 6, 2], // 13 -> 17 + "pb on", + [0, 0, 3], // 15 -> 18 + [1, 1, 4], // 16 -> 19 + [2, 2, 5], // 17 -> 20 + [0, 3, 3], // 18 -> x + [1, 4, 4], // 19 -> x + [2, 5, 5], // 20 -> x + "pb off", + [0, 0, 5], // 22 -> 26 + [1, 1, 4], // 23 -> 27 + [2, 2, 3], // 24 -> 28 + "pb on", + [0, 3, 5], // 26 -> x + [1, 4, 4], // 27 -> x + [2, 5, 3], // 28 -> x + "clear history", + // Not checking after clear history because browser.download.lastDir content + // pref is not being clear properly in private windows. + //[0, 6, 0], // 30 -> x + //[1, 6, 1], // 31 -> x + //[2, 6, 2], // 32 -> x + "pb off" +]; + +var testIndex = 0; +var content; +var normalWindow; +var privateWindow; +var normalWindowIframe; +var privateWindowIframe; + +function runTest() { + var test = tests[testIndex]; + if (test == undefined) { + endTest(); + } else if (test == "pb on") { + content = privateWindowIframe; + testIndex++; + runTest(); + } else if (test == "pb off") { + content = normalWindowIframe; + testIndex++; + runTest(); + } else if (test == "clear history") { + Services.obs.notifyObservers(null, "browser:purge-session-history", ""); + testIndex++; + runTest(); + } else { + var file = dirs[test[2]].clone(); + file.append("file.file"); + MockFilePicker.returnFiles = [file]; + content.setAttribute('src', domains[test[0]] + '/chrome/layout/forms/test/bug536567_subframe.html'); + } +} + +function endTest() { + for(var i = 0; i < dirs.length - 1; i++) { + dirs[i].remove(true); + } + + normalWindow.close(); + privateWindow.close(); + MockFilePicker.cleanup(); + SimpleTest.finish(); +} + +var mainWindow = + window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShellTreeItem). + rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindow); +var contentPage = "http://mochi.test:8888/chrome/layout/forms/test/bug536567_iframe.html"; + +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + setTimeout(aCallback, 0); + } + }, "browser-delayed-startup-finished", false); +} + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + whenDelayedStartupFinished(win, function() { + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + win.gBrowser.loadURI(contentPage); + return; + } + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + win.gBrowser.selectedBrowser.focus(); + SimpleTest.info("DOMContentLoaded's window: " + win.location + " vs. " + window.location); + win.setTimeout(function() { aCallback(win); }, 0); + }, true); + SimpleTest.info("load's window: " + win.location + " vs. " + window.location); + win.setTimeout(function() { win.gBrowser.loadURI(contentPage); }, 0); + }); +} + +MockFilePicker.showCallback = function(filepicker) { + var test = tests[testIndex]; + var returned = -1; + for (var i = 0; i < dirs.length; i++) { + if (dirs[i].path == MockFilePicker.displayDirectory.path) { + returned = i; + break; + } + } + if (test[1] == -1) { + ok(false, "We should never get an unknown directory back"); + } else { + is(returned, test[1], 'test ' + testIndex); + } + + filepicker.window.setTimeout(function() { + testIndex++; + runTest(); + }, 0); +}; + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + testOnWindow(false, function(aWin) { + var selectedBrowser = aWin.gBrowser.selectedBrowser; + + normalWindow = aWin; + normalWindowIframe = + selectedBrowser.contentDocument.getElementById("content"); + + testOnWindow(true, function(aPrivateWin) { + selectedBrowser = aPrivateWin.gBrowser.selectedBrowser; + + privateWindow = aPrivateWin; + privateWindowIframe = + selectedBrowser.contentDocument.getElementById("content"); + + content = normalWindowIframe; + selectedBrowser.contentWindow.setTimeout(runTest, 0); + }); + }); +}; + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug542914.html b/layout/forms/test/test_bug542914.html new file mode 100644 index 000000000..dc2fadb3a --- /dev/null +++ b/layout/forms/test/test_bug542914.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=542914 +--> +<head> + <title>Test for Bug 542914</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=542914">Mozilla Bug 542914</a> +<p id="display"> + <input type="text" id="a" value="test"> + <input type="text" id="b"> + <input type="text" id="c"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 542914 **/ +SimpleTest.waitForExplicitFinish(); +function runTests(callback, type) { + var a = $("a"); + + // Test that the initial value of the control is available to script + // without initilization of the editor + is(a.value, "test", "The value is available before initialization"); + // Initialize the editor + a.focus(); + // Test that the value does not change after initialization + is(a.value, "test", "The value does not change after initializtion"); + + var b = $("b"); + + // Test that the initial value is empty before initialization. + is(b.value, "", "The value is empty before initialization"); + // Make sure that the value can be changed before initialization + b.value ="some value"; + is(b.value, "some value", "The value can be changed before initialization"); + // Initialize the editor + b.focus(); + // Make sure that the value does not change after initialization + is(b.value, "some value", "The value does not change after initialization"); + // Make sure that the value does not change if the element is hidden + b.style.display = "none"; + document.body.offsetHeight; + is(b.value, "some value", "The value does not change while hidden"); + b.style.display = ""; + document.body.offsetHeight; + b.focus(); + is(b.value, "some value", "The value does not change after being shown"); + + var c = $("c"); + + // Make sure that the control accepts input events without explicit initialization + is(c.value, "", "Control is empty initially"); + c.focus(); + sendChar("a"); + is(c.value, "a", "Control accepts input without explicit initialization"); + // Make sure that the control retains its caret position + c.focus(); + c.blur(); + c.focus(); + sendChar("b"); + is(c.value, "ab", "Control retains caret position after being re-focused"); + + var d = document.createElement("input"); + d.setAttribute("type", type); + $("display").appendChild(d); + document.body.offsetHeight; + + // Make sure dynamically injected inputs work as expected + is(d.value, "", "Dynamic control's initial value should be empty"); + d.value = "new"; + d.focus(); + is(d.value, "new", "Dynamic control's value can be set before initialization"); + sendChar("x"); + is(d.value, "newx", "Dynamic control accepts keyboard input without explicit initialization"); + $("display").removeChild(d); + is(d.value, "newx", "Dynamic control retains value after being removed from the document"); + + callback(); +} + +var gPreviousType = "text"; +function setTypes(aType) { + var content = document.getElementById("display"); + content.innerHTML = content.innerHTML.replace(gPreviousType, aType); + gPreviousType = aType; +} + +addLoadEvent(function() { + ok(true, "Running tests on <input type=text>"); + runTests(function() { + ok(true, "Running tests on <input type=password>"); + setTypes("password"); + runTests(function() { + ok(true, "Running tests on <input type=tel>"); + setTypes("tel"); + runTests(function() { + SimpleTest.finish(); + }, "tel"); + }, "password"); + }, "text"); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug549170.html b/layout/forms/test/test_bug549170.html new file mode 100644 index 000000000..37c5bb336 --- /dev/null +++ b/layout/forms/test/test_bug549170.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=549170 +--> +<head> + <title>Test for Bug 549170</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="window.setTimeout(runTests, 0);"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549170">Mozilla Bug 549170</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id='i' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"> +<textarea id='t' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"></textarea><br> +<input id='ip' placeholder='foo' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"> +<textarea id='tp' placeholder='foo' + onmouseup="mouseHandler(event);" + onmousedown="mouseHandler(event);"></textarea> +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 549170 **/ + +var gNumberOfMouseEventsCatched = 0; + +SimpleTest.waitForExplicitFinish(); + +function mouseHandler(aEvent) +{ + gNumberOfMouseEventsCatched++; + is(SpecialPowers.wrap(aEvent).originalTarget.nodeName, "DIV", "An inner div should be the target of the event"); + ok(SpecialPowers.wrap(aEvent).originalTarget.classList.contains("anonymous-div"), "the target div should be the editor div"); +} + +function checkMouseEvents(element) +{ + gNumberOfMouseEventsCatched = 0; + + synthesizeMouse(element, 5, 5, {type: "mousedown", button: 0}); + synthesizeMouse(element, 5, 5, {type: "mouseup", button: 0}); + synthesizeMouse(element, 5, 5, {type: "mousedown", button: 1}); + // NOTE: this event is going to copy the buffer on linux, this should not be a problem + synthesizeMouse(element, 5, 5, {type: "mouseup", button: 1}); + synthesizeMouse(element, 5, 5, {type: "mousedown", button: 2}); + synthesizeMouse(element, 5, 5, {type: "mouseup", button: 2}); + + is(gNumberOfMouseEventsCatched, 6, "Some mouse events have not been catched"); +} + +function runTests() +{ + checkMouseEvents(document.getElementById('i')); + checkMouseEvents(document.getElementById('t')); + checkMouseEvents(document.getElementById('ip')); + checkMouseEvents(document.getElementById('tp')); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug562447.html b/layout/forms/test/test_bug562447.html new file mode 100644 index 000000000..53f84428e --- /dev/null +++ b/layout/forms/test/test_bug562447.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=562447 +--> +<head> + <title>Test for Bug 562447</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<body> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=562447">Mozilla Bug 562447</a> + +<input id="WhyDoYouFocusMe" + style="position: absolute; left: -50px; top: 10000px;"> + +<pre id="test"> +<script> +addLoadEvent(function() { + // Scroll down a bit + window.scrollTo(0, 5000); + + setTimeout(function() { + // Make sure that we're scrolled by 5000px + is(window.pageYOffset, 5000, "Make sure we're scrolled correctly"); + + // Scroll back up, and mess with the input box along the way + var input = document.getElementById("WhyDoYouFocusMe"); + input.focus(); + input.blur(); + window.scrollTo(0, 0); + + setTimeout(function() { + is(window.pageYOffset, 0, "Make sure we're scrolled back up correctly"); + + // Scroll back up + window.scrollTo(0, 5000); + + setTimeout(function() { + is(window.pageYOffset, 5000, "Sanity check"); + + window.scrollTo(0, 0); + input.focus(); + input.blur(); + + setTimeout(function() { + isnot(window.pageYOffset, 0, "This time we shouldn't be scrolled up"); + + SimpleTest.finish(); + }, 0); + }, 0); + }, 0); + }, 0); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> + +</body> +</html> diff --git a/layout/forms/test/test_bug563642.html b/layout/forms/test/test_bug563642.html new file mode 100644 index 000000000..9c5171d70 --- /dev/null +++ b/layout/forms/test/test_bug563642.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=563642 +--> +<head> + <title>Test for Bug 563642</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563642">Mozilla Bug 563642</a> +<p id="display"> +<select id="test1" multiple="multiple" size="1"> + <option>Item 1</option> + <option>Item 2</option> + <option>Item 3</option> + <option>Item 4</option> + <option>Item 5</option> +</select> +<select id="test2" multiple="multiple" size="1"> + <option>Item 1</option> + <option disabled>Item 2</option> + <option>Item 3</option> + <option disabled>Item 4</option> + <option>Item 5</option> +</select> +<select id="test3" multiple="multiple"></select> +<select id="test4" multiple="multiple" size="1"></select> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 563642 **/ + +function pageUpDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_down"); + sendKey("page_up"); + sendKey("page_down"); + is(elm.selectedIndex, index, "pageUpDownTest: selectedIndex for " + id + " is " + index); +} + +function upDownTest(id,index) { + var elm = document.getElementById(id); + elm.focus(); + elm.selectedIndex = 0; + sendKey("down"); + sendKey("down"); + sendKey("down"); + sendKey("up"); + sendKey("down"); + is(elm.selectedIndex, index, "upDownTest: selectedIndex for " + id + " is " + index); +} + +function runTest() { + pageUpDownTest("test1",3); + pageUpDownTest("test2",4); + pageUpDownTest("test3",-1); + pageUpDownTest("test4",-1); + upDownTest("test1",3); + upDownTest("test2",4); + upDownTest("test3",-1); + upDownTest("test4",-1); + + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +// Turn off Spatial Navigation because it hijacks down and up key events. +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, runTest); +}) + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug564115.html b/layout/forms/test/test_bug564115.html new file mode 100644 index 000000000..5723b55d5 --- /dev/null +++ b/layout/forms/test/test_bug564115.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=564115 +--> +<head> + <title>Test for Bug 564115</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<body> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug?id=564115">Mozilla Bug 564115</a> + +<pre id="test"> +<script> + +const TEST_URL = "/tests/layout/forms/test/bug564115_window.html"; + +addLoadEvent(function() { + var win = open(TEST_URL, "", "width=600,height=600"); + SimpleTest.waitForFocus(function() { + var doc = win.document; + var input = doc.querySelector("input"); + + // Focus the input box, and wait for the focus to actually happen + input.focus(); + setTimeout(function() { + // Scroll down a bit + win.scrollTo(0, 5000); + + setTimeout(function() { + is(win.pageYOffset, 5000, "Page should be scrolled correctly"); + + // Refocus the window + SimpleTest.waitForFocus(function() { + SimpleTest.waitForFocus(function() { + is(win.pageYOffset, 5000, + "The page's scroll offset should not have been changed"); + + win.close(); + SimpleTest.finish(); + }, win); + }); + }, 0); + }, 0); + }, win); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> + +</body> +</html> diff --git a/layout/forms/test/test_bug571352.html b/layout/forms/test/test_bug571352.html new file mode 100644 index 000000000..547ab4007 --- /dev/null +++ b/layout/forms/test/test_bug571352.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=571352 +--> +<head> + <title>Test for Bug 571352</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=571352">Mozilla Bug 571352</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 571352 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function test() { + function createList() { + $('display').innerHTML = '<select multiple><option>0<option>1<option>2<option>3<option>4<option>5</select>'; + $('display').firstChild.focus(); + } + function option(index) { + return $('display').firstChild.childNodes[index]; + } + function remove(index) { + var sel = $('display').firstChild; + sel.removeChild(sel.childNodes[index]); + } + function up() { synthesizeKey("VK_UP", {}); } + function shiftUp() { synthesizeKey("VK_UP", {shiftKey:true}); } + function down() { synthesizeKey("VK_DOWN", {}); } + function shiftDown() { synthesizeKey("VK_DOWN", {shiftKey:true}); } + function mouseEvent(index,event) { + synthesizeMouse(option(index), 5, 5, event); + } + + const click = {}; + const shiftClick = {shiftKey:true}; + const CtrlClick = {CtrlKey:true}; + createList(); + mouseEvent(0,click) + is(document.activeElement,$('display').firstChild,"<select> is focused"); + mouseEvent(2,shiftClick) + remove(0); + ok(option(0).selected && option(1).selected,"first two options are selected"); + mouseEvent(2,shiftClick) + ok(option(0).selected && option(1).selected && option(2).selected,"first three options are selected"); + shiftUp(); + ok(option(0).selected && option(1).selected,"first two options are selected"); + remove(1); + ok(option(0).selected,"first option is selected"); + shiftDown(); + ok(option(0).selected && option(1).selected,"first two options are selected"); + down(); + ok(option(2).selected,"third option is selected"); + shiftDown(); + ok(option(2).selected && option(3).selected,"third & fourth option are selected"); + remove(2); + shiftUp(); + ok(option(1).selected && option(2).selected,"2nd & third option are selected"); + remove(0); + mouseEvent(0,shiftClick) + ok(option(0).selected && option(1).selected,"all remaining 2 options are selected"); + shiftDown(); + remove(1); + ok(!option(0).selected,"first option is unselected"); + remove(0); // select is now empty + ok($('display').firstChild.firstChild==null,"all options were removed"); + + SimpleTest.finish(); +}); + + + + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug572406.html b/layout/forms/test/test_bug572406.html new file mode 100644 index 000000000..3f2693c75 --- /dev/null +++ b/layout/forms/test/test_bug572406.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572406 +--> +<head> + <title>Test for Bug 572406</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload='runTests();'> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572406">Mozilla Bug 572406</a> +<p id="display"></p> +<div id='content'> + <textarea id='i'>foo</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572406 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var textarea = document.getElementById('i'); + + textarea.firstChild.nodeValue = "bar"; + is(textarea.value, textarea.firstChild.nodeValue, + "textarea value should be firstChild.nodeValue"); + is(textarea.defaultValue, textarea.firstChild.nodeValue, + "textarea defaultValue should be firstChild.nodeValue"); + + textarea.style.display = 'none'; + SimpleTest.executeSoon(function() { + textarea.firstChild.nodeValue = "tulip"; + is(textarea.value, textarea.firstChild.nodeValue, + "textarea value should be firstChild.nodeValue"); + is(textarea.defaultValue, textarea.firstChild.nodeValue, + "textarea defaultValue should be firstChild.nodeValue"); + SimpleTest.finish(); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug572649.html b/layout/forms/test/test_bug572649.html new file mode 100644 index 000000000..c6f00f3bb --- /dev/null +++ b/layout/forms/test/test_bug572649.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572649 +--> +<head> + <title>Test for Bug 572649</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572649">Mozilla Bug 572649</a> +<p id="display"> + <textarea id="area" rows="5"> + Here + is + some + very + long + text + that + we're + using + for + testing + purposes + </textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572649 **/ +SimpleTest.waitForExplicitFinish(); + +// We intermittently trigger two "Wrong parent style context" assertions +// on B2G emulator builds (bug XXXXXXX). The two frames that get incorrect +// style context parents are scroll bar parts in the <textarea>. +SimpleTest.expectAssertions(0, 2); + +addLoadEvent(function() { + var area = document.getElementById("area"); + + is(area.scrollTop, 0, "The textarea should not be scrolled initially"); + area.addEventListener("focus", function() { + area.removeEventListener("focus", arguments.callee, false); + setTimeout(function() { + is(area.scrollTop, 0, "The textarea's insertion point should not be scrolled into view"); + + SimpleTest.finish(); + }, 0); + }, false); + setTimeout(function() { + var rect = area.getBoundingClientRect(); + synthesizeMouse(area, rect.width - 5, 5, {}); + }, 0); +}); +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug595310.html b/layout/forms/test/test_bug595310.html new file mode 100644 index 000000000..295da1170 --- /dev/null +++ b/layout/forms/test/test_bug595310.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=595310 +--> +<head> + <title>Test for Bug 595310</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595310">Mozilla Bug 595310</a> +<p id="display"></p> +<div id="content"> + <input id='i' value="bar"> + <textarea id='t'>bar</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 595310 **/ + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // We want to compare to value="bar" and no placeholder shown. + // That is what is currently showed. + var s1 = snapshotWindow(window, false); + + var content = document.getElementById('content'); + var i = document.getElementById('i'); + var t = SpecialPowers.wrap(document.getElementById('t')); + i.value = ""; i.placeholder = "foo"; + t.value = ""; t.placeholder = "foo"; + + // Flushing. + // Note: one call would have been enough actually but I didn't want to favour + // one element... ;) + i.getBoundingClientRect(); + t.getBoundingClientRect(); + + function synthesizeDropText(aElement, aText) + { + var editor = SpecialPowers.wrap(aElement) + .QueryInterface(SpecialPowers.Ci.nsIDOMNSEditableElement) + .editor + .QueryInterface(SpecialPowers.Ci.nsIPlaintextEditor); + + editor.insertText(aText); + } + + // We insert "bar" and we should only see "bar" now. + synthesizeDropText(i, "bar"); + synthesizeDropText(t, "bar"); + + var s2 = snapshotWindow(window, false); + + ok(compareSnapshots(s1, s2, true)[0], + "When setting the value, the placeholder should disappear."); + + SimpleTest.finish(); +}); + + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug620936.html b/layout/forms/test/test_bug620936.html new file mode 100644 index 000000000..5f02129ac --- /dev/null +++ b/layout/forms/test/test_bug620936.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=620936 +--> +<head> + <title>Test for Bug 620936</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620936">Mozilla Bug 620936</a> +<p id="display"></p> +<div id="content"> + <input value="foo"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 620936 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("input"); + i.focus(); + i.setSelectionRange(100, 100); + is(i.selectionStart, 3, "The selection should be set to the end of the text"); + is(i.selectionEnd, 3, "The selection should be set to the end of the text"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug644542.html b/layout/forms/test/test_bug644542.html new file mode 100644 index 000000000..dca37c390 --- /dev/null +++ b/layout/forms/test/test_bug644542.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=644542 +--> +<head> + <title>Test for Bug 644542</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + select:after { + content: ' appended string'; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=644542">Mozilla Bug 644542</a> +<p id="display"> + <form method="post" action=""> + <select id="select"> + <option value="1">1</option> + <option value="2">2</option> + </select> + </form> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 644542 **/ + +var select = document.getElementById("select"); + +var clicks = 0; + +function click() { + synthesizeMouseAtCenter(select, { }); + ++clicks; + + // At least two clicks were required for bug 644542, sometimes more; + // delay is long enough that this doesn't look like a double + // click, and also allows time for popup to show and for painting. + setTimeout(clicks < 4 ? click : done, 500); +} + +function done() { + ok(true, "No crash on opening dropdown"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +// waitForFocus is most likely not the right thing to wait for, but +// without this the first click is ineffective (even with a reflow forced +// before synthesizeMouse). +SimpleTest.waitForFocus(click); +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug665540.html b/layout/forms/test/test_bug665540.html new file mode 100644 index 000000000..51f8f568a --- /dev/null +++ b/layout/forms/test/test_bug665540.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=665540 +--> +<head> + <title>Test for Bug 665540 Select dropdown position in fullscreen window</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body onload="openFullscreenWindow()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=665540">Mozilla Bug 665540</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 665540 **/ + +SimpleTest.waitForExplicitFinish(); + +var win; +var select; +var optiona; +var eventType = "mouseover"; +var timeoutID; +var eventOffsetX = 2; +var eventOffsetY = 2; + +function openFullscreenWindow() { + win = open("bug665540_window.xul", "_blank", "resizable=yes,chrome"); + win.addEventListener("sizemodechange", + function() { + info("sizemodechange. windowState = " + win.windowState + " fullScreen = " + win.fullScreen); + }, false); + win.addEventListener("fullscreen", + function() { + info("fullscreen event. windowState = " + win.windowState + " fullScreen = " + win.fullScreen); + }, false); + info("win.windowState = " + win.windowState); + info("win.fullScreen = " + win.fullScreen); + + // Close our window if the test times out so that it doesn't interfere + // with later tests. + timeoutID = setTimeout(function () { + ok(false, "Test timed out."); + // Provide some time for a screenshot + setTimeout(finish, 1000); + }, 20000); +} + +function childFocused() { + ok(win.fullScreen, "window should be fullscreen"); + is(win.windowState, win.STATE_FULLSCREEN, + "window state should be fullscreen"); + + // The select doesn't open if the mouse click is fired too soon + // (on X11 at least). + setTimeout(openSelect, 1000); +} + +function openSelect() { + select = win.document.getElementById("select"); + synthesizeMouseAtCenter(select, {}, win); + // A yield was required on X11 tinderbox machines. + // (Wasn't required on other platforms nor on an X11 system with kwin.) + setTimeout(checkPosition, 1000); +} + +function checkPosition() { + optiona = win.document.getElementById("optiona"); + optiona.addEventListener(eventType, eventReceived, false); + + // If the select dropdown is opened in the position where + // getBoundingClientRect() predicts, then optiona will receive the event. + // The event is received asynchronously (I don't know why), so the handler + // is removed later. + synthesizeMouse(optiona, eventOffsetX, eventOffsetY, + { type: eventType }, win); +} + +function eventReceived(event) { + clearTimeout(timeoutID); + optiona.removeEventListener(eventType, eventReceived, false); + + var rect = optiona.getBoundingClientRect(); + + // Note that fullscreen only fully covers one monitor, so win.screenX can + // be non-zero. + is(event.screenX, win.screenX + rect.left + eventOffsetX, + "event.screenX should match sent event"); + is(event.screenY, win.screenY + rect.top + eventOffsetY, + "event.screenY should match sent event"); + + finish(); +} + +function finish() { + if (select && navigator.platform.indexOf("Win") >= 0) { + todo(false, + "Should not have to close select before closing its window"); + // This avoids mochitest "Unable to restore focus" errors (bug 670053). + synthesizeMouseAtCenter(select, {}, win); + } + + is(win.windowState, win.STATE_FULLSCREEN, + "window state should still be fullscreen"); + + win.close(); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug672810.html b/layout/forms/test/test_bug672810.html new file mode 100644 index 000000000..4a3f74134 --- /dev/null +++ b/layout/forms/test/test_bug672810.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=672810 +--> +<head> + <title>Test for Bug 672810</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=672810">Mozilla Bug 672810</a> +<p id="display"></p> +<div id="content"> + <select id="s1" multiple size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> + <select id="s2" size="10"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> + <select id="s3" size="1"><option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x<option>x</select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 672810 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var sel = document.getElementsByTagName('select'); + + sel[0].addEventListener('focus', function() { + s = sel[0]; + s.removeEventListener('focus', arguments.callee, false); + synthesizeKey('VK_DOWN', {}); + is(s.selectedIndex,0, s.id + ": initial DOWN selects first option"); + synthesizeKey('VK_DOWN', {ctrlKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": CTRL+DOWN did not select 2nd option"); + synthesizeKey('VK_DOWN', {ctrlKey: true}); + synthesizeKey('VK_DOWN', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is selected"); + ok(s[3].selected,s.id + ": 4th option is selected"); + ok(!s[4].selected,s.id + ": 5th option is unselected"); + synthesizeKey('VK_DOWN', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is still selected"); + ok(s[4].selected,s.id + ": 5th option is selected"); + ok(!s[5].selected,s.id + ": 6th option is unselected"); + synthesizeKey('VK_UP', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is still selected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(!s[3].selected,s.id + ": 4th option is unselected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,0, s.id + ": first option is still selected"); + ok(!s[1].selected,s.id + ": 2nd option is still unselected"); + ok(s[2].selected,s.id + ": 3rd option is still selected"); + ok(s[3].selected,s.id + ": 4th option is selected"); + ok(s[4].selected,s.id + ": 5th option is still selected"); + ok(!s[5].selected,s.id + ": 6th option is still unselected"); + setTimeout(function(){sel[1].focus()},0); + }, false); + sel[1].addEventListener('focus', function() { + s = sel[1]; + s.removeEventListener('focus', arguments.callee, false); + synthesizeKey('VK_DOWN', {}); + is(s.selectedIndex,0, s.id + ": initial DOWN selects first option"); + synthesizeKey('VK_DOWN', {ctrlKey: true}); + is(s.selectedIndex,1, s.id + ": 2nd option is selected"); + ok(!s[0].selected,s.id + ": CTRL+DOWN deselected first option"); + synthesizeKey('VK_DOWN', {ctrlKey: true}); + synthesizeKey('VK_DOWN', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is selected"); + ok(!s[1].selected,s.id + ": CTRL+SHIFT+DOWN deselected 2nd option"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + setTimeout(function(){sel[2].focus()},0); + }, false); + sel[2].addEventListener('focus', function() { + if (navigator.platform.indexOf("Mac") == -1) { + s = sel[2]; + s.removeEventListener('focus', arguments.callee, false); + synthesizeKey('VK_DOWN', {}); + is(s.selectedIndex,1, s.id + ": initial DOWN selects 2nd option"); + synthesizeKey('VK_DOWN', {ctrlKey: true}); + is(s.selectedIndex,2, s.id + ": 3rd option is selected"); + ok(!s[1].selected,s.id + ": CTRL+DOWN deselected 2nd option"); + synthesizeKey('VK_DOWN', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is selected"); + ok(!s[2].selected,s.id + ": CTRL+SHIFT+DOWN deselected 3rd option"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + synthesizeKey(' ', {ctrlKey: true, shiftKey: true}); + is(s.selectedIndex,3, s.id + ": 4th option is still selected"); + } else { + todo(false, "Make this test work on OSX"); + } + setTimeout(function(){SimpleTest.finish()},0); + }, false); + sel[0].focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug704049.html b/layout/forms/test/test_bug704049.html new file mode 100644 index 000000000..a779abfcd --- /dev/null +++ b/layout/forms/test/test_bug704049.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=704049 +--> +<head> + <title>Test for Bug 704049</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=704049">Mozilla Bug 704049</a> +<p id="display"></p> +<input type="radio" id="radio11" name="group1"> +<input type="radio" id="radio12" name="group1"> +<input type="radio" id="radio21" name="group2" checked> +<input type="radio" id="radio22" name="group2"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 704049 **/ + +window.addEventListener("click", function (e) { e.preventDefault(); }, false); + +function doTest() +{ + var target = document.getElementById("radio11"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio11 is checked"); + target = document.getElementById("radio12"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio12 is checked"); + target = document.getElementById("radio21"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, true, "radio21 is not checked"); + target = document.getElementById("radio22"); + synthesizeMouseAtCenter(target, {}); + is(target.checked, false, "radio22 is checked"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(doTest); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug717878_input_scroll.html b/layout/forms/test/test_bug717878_input_scroll.html new file mode 100644 index 000000000..89b431b34 --- /dev/null +++ b/layout/forms/test/test_bug717878_input_scroll.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717878 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 717878</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717878">Mozilla Bug 717878</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<!-- size=10 and monospace font ensure there's no overflow in either direction --> +<input id="no-overflow" type="text" + size="10" + style=" + font-family: monospace; + font-size: 1em;" + value="Short"> +<!-- size=10, monospace font, and height=0.5em ensure overflow in both directions --> +<input id="overflow" type="text" + size="10" + style=" + font-family: monospace; + font-size: 1em; + height: 0.5em;" + value="This is a long string"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 717878 **/ + +/** + * Test an element's scroll properties for correctness + * + * @param element Element to test + * @param prop Specify the property to test, + * i.e. "scrollLeft" or "scrollTop" + * @param propMax Specify the scrollMax property to test, + * i.e. "scrollLeftMax" or "scrollTopMax" + * @param is_overflow Specify whether the element is + * scrollable in the above direction + */ +function test_scroll(element, scroll, scrollMax, is_overflow) { + + is(element[scroll], 0, element.id + " initial " + scroll + " != 0"); + if (is_overflow) { + isnot(element[scrollMax], 0, element.id + " " + scrollMax + " == 0"); + } else { + is(element[scrollMax], 0, element.id + " " + scrollMax + " != 0"); + } + + element[scroll] = 10; + if (is_overflow) { + isnot(element[scroll], 0, element.id + " unable to scroll " + scroll); + } else { + is(element[scroll], 0, element.id + " able to scroll " + scroll); + } + + element[scroll] = element[scrollMax]; + is(element[scroll], element[scrollMax], element.id + " did not scroll to " + scrollMax); + + element[scroll] = element[scrollMax] + 10; + is(element[scroll], element[scrollMax], element.id + " scrolled past " + scrollMax); +} + +var no_overflow = document.getElementById("no-overflow"); +test_scroll(no_overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ false); +test_scroll(no_overflow, "scrollTop", "scrollTopMax", /* is_overflow */ false); + +var overflow = document.getElementById("overflow"); +test_scroll(overflow, "scrollLeft", "scrollLeftMax", /* is_overflow */ true); +test_scroll(overflow, "scrollTop", "scrollTopMax", /* is_overflow */ true); + +</script> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug869314.html b/layout/forms/test/test_bug869314.html new file mode 100644 index 000000000..9116884d6 --- /dev/null +++ b/layout/forms/test/test_bug869314.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=869314 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 869314</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <style type="text/css"> + .selectbox { + background-color: #00FF00; + } + </style> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=869314">Mozilla Bug 869314</a> +<p id="display"></p> +<div id="content"> + + <select id="selectbox1" name="non-native selectbox" class="selectbox"> + <option value="item">test item</option> + </select> + + <select id="selectbox2" name="native selectbox"> + <option value="item">test item</option> + </select> + + <script type="application/javascript"> + var Cc = SpecialPowers.Cc; + var Ci = SpecialPowers.Ci; + var sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + var osName = sysInfo.getProperty("name"); + if (osName == "Darwin") { // Mac OS X. + // This test is for Mac only. See bug for more info. + ok(document.getElementById("selectbox1").clientWidth > + document.getElementById("selectbox2").clientWidth, + "Non-native styled combobox does not have enough space for a " + + "dropmarker!"); + } else { + // We need to call at least one test function to make the test harness + // happy. + ok(true, "Test wasn't ignored but should have been."); + } + </script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_bug903715.html b/layout/forms/test/test_bug903715.html new file mode 100644 index 000000000..37d49762e --- /dev/null +++ b/layout/forms/test/test_bug903715.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=903715 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=903715">Mozilla Bug 903715</a> +<p id="display"></p> +<div id="content"> + <form id="form" action="/"> + <select id="select" name="select"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> + <option>8</option> + <option>9</option> + </select> + <input id="input-text" name="text" value="some text"> + <input id="input-submit" type="submit"> + </form> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +SimpleTest.waitForFocus(runTests, window); + +function runTests() +{ + var form = document.getElementById("form"); + form.addEventListener("keypress", function (aEvent) { + ok(false, "keypress event shouldn't be fired when the preceding keydown event caused closing the dropdown of the select element"); + }, true); + form.addEventListener("submit", function (aEvent) { + ok(false, "submit shouldn't be performed by the Enter key press on the select element"); + aEvent.preventDefault(); + }, true); + var select = document.getElementById("select"); + select.addEventListener("change", function (aEvent) { + var input = document.getElementById("input-text"); + input.focus(); + input.select(); + }, false); + + select.focus(); + + select.addEventListener("popupshowing", function (aEvent) { + setTimeout(function () { + synthesizeKey("VK_DOWN", { }); + select.addEventListener("popuphiding", function (aEvent) { + setTimeout(function () { + // Enter key should cause closing the dropdown of the select element + // and keypress event shouldn't be fired on the input element because + // which shouldn't cause sumbmitting the form contents. + ok(true, "Test passes if there is no error"); + SimpleTest.finish(); + }, 100); + }, false); + // Close dropdown. + synthesizeKey("VK_RETURN", { }); + }, 100); + }, false); + + // Open dropdown. + synthesizeKey("VK_DOWN", { altKey: true }); +} +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug935876.html b/layout/forms/test/test_bug935876.html new file mode 100644 index 000000000..bb4090211 --- /dev/null +++ b/layout/forms/test/test_bug935876.html @@ -0,0 +1,495 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935876 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 935876</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935876">Mozilla Bug 935876</a> +<p id="display"></p> +<div> +<select id="listbox" size="3"> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +<select id="multipleListbox" size="3" multiple> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +<select id="combobox"> + <option selected>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + <option>5</option> + <option>6</option> + <option>7</option> +</select> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +const kIsWin = navigator.platform.indexOf("Win") == 0; +const kIsMac = navigator.platform.indexOf("Mac") == 0; +const kIsAndroid = navigator.appVersion.indexOf("Android") != 0; + +function runTests() +{ + var doPreventDefault = false; + function onKeydown(aEvent) + { + if (doPreventDefault) { + aEvent.preventDefault(); + } + } + + var keyPressEventFired = false; + function onKeypress(aEvent) + { + keyPressEventFired = true; + } + + var keyDownEventConsumedByJS = false; + var keyDownEventConsumed = false; + function onkeydownInSystemEventGroup(aEvent) + { + keyDownEventConsumedByJS = aEvent.defaultPrevented; + keyDownEventConsumed = aEvent.getPreventDefault(); + } + + function reset() + { + keyPressEventFired = false; + keyDownEventConsumedByJS = false; + keyDownEventConsumed = false; + } + + function check(aExpectingKeydownConsumed, aDescription) + { + if (doPreventDefault) { + ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription + + " if preventDefault() of keydown event was called"); + ok(keyDownEventConsumedByJS, "keydown event of " + aDescription + + " should be consumed in content level if preventDefault() of keydown event is called"); + ok(keyDownEventConsumed, "keydown event of " + aDescription + + " should be consumed in system level if preventDefault() of keydown event is called"); + } else if (aExpectingKeydownConsumed) { + ok(!keyPressEventFired, "keypress event shouldn't be fired for " + aDescription); + ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level"); + ok(keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level"); + } else { + ok(keyPressEventFired, "keypress event should be fired for " + aDescription); + ok(!keyDownEventConsumedByJS, "keydown event of " + aDescription + " shouldn't be consumed in content level"); + ok(!keyDownEventConsumed, "keydown event of " + aDescription + " should be consumed in system level"); + } + } + + var listbox = document.getElementById("listbox"); + listbox.addEventListener("keydown", onKeydown, false); + listbox.addEventListener("keypress", onKeypress, false); + SpecialPowers.addSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false); + + listbox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_DOWN", {}); + check(true, "DownArrow key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_UP", {}); + check(true, "UpArrow key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_RIGHT", {}); + check(true, "RightArrow key on listbox #" + i); + } + + for (var i = 0; i < listbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_LEFT", {}); + check(true, "LeftArrow key on listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_DOWN", {}); + check(true, "PageDown key on listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_UP", {}); + check(true, "PageUp key on listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_END", {}); + check(true, "End key on listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_HOME", {}); + check(true, "Home key on listbox #" + i); + } + + reset() + synthesizeKey("VK_RETURN", {}); + check(false, "Enter key on listbox"); + + reset() + synthesizeKey("VK_ESCAPE", {}); + check(false, "Esc key on listbox"); + + reset() + synthesizeKey("VK_F4", {}); + check(false, "F4 key on listbox"); + + reset() + synthesizeKey("a", {}); + check(false, "'A' key on listbox"); + }); + + listbox.removeEventListener("keydown", onKeydown, false); + listbox.removeEventListener("keypress", onKeypress, false); + SpecialPowers.removeSystemEventListener(listbox, "keydown", onkeydownInSystemEventGroup, false); + + + + var multipleListbox = document.getElementById("multipleListbox"); + multipleListbox.addEventListener("keydown", onKeydown, false); + multipleListbox.addEventListener("keypress", onKeypress, false); + SpecialPowers.addSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false); + + multipleListbox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_DOWN", {}); + check(true, "DownArrow key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_UP", {}); + check(true, "UpArrow key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_RIGHT", {}); + check(true, "RightArrow key on multiple listbox #" + i); + } + + for (var i = 0; i < multipleListbox.options.length + 1; i++) { + reset() + synthesizeKey("VK_LEFT", {}); + check(true, "LeftArrow key on multiple listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_DOWN", {}); + check(true, "PageDown key on multiple listbox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_UP", {}); + check(true, "PageUp key on multiple listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_END", {}); + check(true, "End key on multiple listbox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_HOME", {}); + check(true, "Home key on multiple listbox #" + i); + } + + reset() + synthesizeKey("VK_RETURN", {}); + check(true, "Enter key on multiple listbox"); + + reset() + synthesizeKey("VK_ESCAPE", {}); + check(false, "Esc key on multiple listbox"); + + reset() + synthesizeKey("VK_F4", {}); + check(false, "F4 key on multiple listbox"); + + reset() + synthesizeKey("a", {}); + check(false, "'A' key on multiple listbox"); + }); + + multipleListbox.removeEventListener("keydown", onKeydown, false); + multipleListbox.removeEventListener("keypress", onKeypress, false); + SpecialPowers.removeSystemEventListener(multipleListbox, "keydown", onkeydownInSystemEventGroup, false); + + + + var combobox = document.getElementById("combobox"); + combobox.addEventListener("keydown", onKeydown, false); + combobox.addEventListener("keypress", onKeypress, false); + SpecialPowers.addSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false); + + combobox.focus(); + + [ false, true ].forEach(function (consume) { + doPreventDefault = consume; + if (!kIsMac) { + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_DOWN", {}); + check(true, "DownArrow key on combobox #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_UP", {}); + check(true, "UpArrow key on combobox #" + i); + } + } else { + todo(false, "Make this test work on OSX"); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_RIGHT", {}); + check(true, "RightArrow key on combobox #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_LEFT", {}); + check(true, "LeftArrow key on combobox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_DOWN", {}); + check(true, "PageDown key on combobox #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_UP", {}); + check(true, "PageUp key on combobox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_END", {}); + check(true, "End key on combobox #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_HOME", {}); + check(true, "Home key on combobox #" + i); + } + + reset() + synthesizeKey("VK_RETURN", {}); + check(false, "Enter key on combobox"); + + reset() + synthesizeKey("VK_ESCAPE", {}); + check(true, "Esc key on combobox"); + + if (!kIsWin) { + reset() + synthesizeKey("VK_F4", {}); + check(false, "F4 key on combobox"); + } + + reset() + synthesizeKey("a", {}); + check(false, "'A' key on combobox"); + }); + + function finish() + { + combobox.removeEventListener("keydown", onKeydown, false); + combobox.removeEventListener("keypress", onKeypress, false); + SpecialPowers.removeSystemEventListener(combobox, "keydown", onkeydownInSystemEventGroup, false); + SimpleTest.finish(); + } + + // Mac uses native popup for dropdown. Let's skip the tests for popup + // since it's not handled in nsListControlFrame. + // Similarly, Android doesn't use popup for dropdown. + if (kIsMac || kIsAndroid) { + finish(); + return; + } + + function testDropDown(aCallback) + { + testOpenDropDown(function () { + reset() + synthesizeKey("VK_DOWN", { altKey: true }); + }, function () { + check(true, "Alt + DownArrow key on combobox at opening dropdown"); + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_DOWN", {}); + check(true, "DownArrow key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_UP", {}); + check(true, "UpArrow key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_RIGHT", {}); + check(true, "RightArrow key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < combobox.options.length + 1; i++) { + reset() + synthesizeKey("VK_LEFT", {}); + check(true, "LeftArrow key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_DOWN", {}); + check(true, "PageDown key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 4; i++) { + reset() + synthesizeKey("VK_PAGE_UP", {}); + check(true, "PageUp key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_END", {}); + check(true, "End key on combobox during dropdown open #" + i); + } + + for (var i = 0; i < 2; i++) { + reset() + synthesizeKey("VK_HOME", {}); + check(true, "Home key on combobox during dropdown open #" + i); + } + + testCloseDropDown(function () { + reset() + synthesizeKey("VK_RETURN", {}); + }, function () { + testOpenDropDown(function () { + check(true, "Enter key on combobox at closing dropdown"); + + synthesizeKey("VK_UP", { altKey: true }); + }, function () { + check(true, "Alt + UpArrow key on combobox at opening dropdown"); + + testCloseDropDown(function () { + reset() + synthesizeKey("VK_ESCAPE", {}); + }, function () { + check(true, "Esc key on combobox at closing dropdown"); + + // F4 key opens/closes dropdown only on Windows. So, other platforms + // don't need to do anymore. + if (!kIsWin) { + aCallback(); + return; + } + + testOpenDropDown(function () { + reset() + synthesizeKey("VK_F4", {}); + }, function () { + check(true, "F4 key on combobox at opening dropdown on Windows"); + + testCloseDropDown(function () { + reset() + synthesizeKey("VK_F4", {}); + }, function () { + check(true, "F4 key on combobox at closing dropdown on Windows"); + + aCallback(); + return; + }); + }); + }); + }); + }); + }); + } + + doPreventDefault = false; + testDropDown(function () { + // Even if keydown event is consumed by JS, opening/closing dropdown + // should work for a11y and security (e.g., cannot close dropdown causes + // staying top-most window on the screen). If it's blocked by JS, this + // test would cause permanent timeout. + doPreventDefault = true; + testDropDown(finish); + }); +} + +function testOpenDropDown(aTest, aOnOpenDropDown) +{ + document.addEventListener("popupshowing", function (aEvent) { + document.removeEventListener(aEvent.type, arguments.callee, false); + setTimeout(aOnOpenDropDown, 0); + }, false); + aTest(); +} + +function testCloseDropDown(aTest, aOnCloseDropDown) +{ + document.addEventListener("popuphiding", function (aEvent) { + document.removeEventListener(aEvent.type, arguments.callee, false); + setTimeout(aOnCloseDropDown, 0) + }, false); + aTest(); +} + +SimpleTest.waitForFocus(runTests); +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug957562.html b/layout/forms/test/test_bug957562.html new file mode 100644 index 000000000..6c52dc5e0 --- /dev/null +++ b/layout/forms/test/test_bug957562.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=957562 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957562">Mozilla Bug 957562</a> +<p id="display"></p> +<input id="n" onfocus="kill()" type="number" style="border:20px solid black"> +<pre id="test"> +</pre> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var killed = false; +function kill() { + if (killed) { + return; + } + killed = true; + n.style.display = 'none'; + r = n.getBoundingClientRect(); + setTimeout(function() { + ok(true, "Didn't crash"); + SimpleTest.finish(); + }, 0); +} + +function runTests() +{ + synthesizeMouse(n, 2, 2, {}); +} +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug960277.html b/layout/forms/test/test_bug960277.html new file mode 100644 index 000000000..d95e0f7cb --- /dev/null +++ b/layout/forms/test/test_bug960277.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=960277 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 903715</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=960277">Mozilla Bug 960277</a> +<p id="display"></p> +<fieldset style="position:relative; height:100px; margin:50px; background:blue;"> +<legend style="background:purple"> + <div id="d" style="position:absolute; background:yellow; top:0; left:0; width:50px; height:50px;"></div> +</legend> +</fieldset> +<pre id="test"> +</pre> +<script type="application/javascript"> +var rect = d.getBoundingClientRect(); +is(document.elementFromPoint(rect.left + 10, rect.top + 10), d, + "Hit testing yellow div"); +</script> +</body> +</html> diff --git a/layout/forms/test/test_bug961363.html b/layout/forms/test/test_bug961363.html new file mode 100644 index 000000000..a3c45cd9a --- /dev/null +++ b/layout/forms/test/test_bug961363.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=961363 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 961363</title> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + /** Test for Bug 961363 **/ + + SimpleTest.waitForExplicitFinish(); + + function test() { + + SimpleTest.waitForFocus(function() { + var one = [{k:"DOWN",s:[false,true,true,false]}, {k:"UP",s:[false,false,true,false]}, + {k:"RIGHT",s:[false,false,false,false]}, {k:"LEFT",s:[false,true,false,false]}, + {k:"PAGE_DOWN",s:[false,true,false,true]}, {k:"PAGE_UP",s:[false,false,false,true]}, + {k:"END",s:[false,false,false,false]}, {k:"HOME",s:[true,false,false,false]} ]; + var two_1 = [{k:"DOWN",s:[false,false,true,false]}, {k:"UP",s:[false,true,false,false]}, + {k:"RIGHT",s:[false,false,true,false]}, {k:"LEFT",s:[false,true,false,false]}, + {k:"END",s:[false,false,false,true]}, {k:"HOME",s:[true,false,false,false]} ]; + var two_2 = [{k:"PAGE_DOWN",s:[true,false,false,false]}, {k:"PAGE_UP",s:[true,false,false,false]}]; + var three_1 = [{k:"DOWN",s:[false,false,true,false]}, {k:"UP",s:[false,true,false,false]}, + {k:"RIGHT",s:[false,false,true,false]}, {k:"LEFT",s:[false,true,false,false]}, + {k:"END",s:[false,false,false,true]}, {k:"HOME",s:[true,false,false,false]} ]; + var three_2 = [{k:"PAGE_DOWN",s:[true,false,false,false]}, {k:"PAGE_UP",s:[true,false,false,false]} ]; + + function select_test(id, tests, ctrl_change) { + var element = document.getElementById(id); + element.focus(); + var previousValue = element.value; + tests.forEach(function(data) { + var key = data.k; + synthesizeKey("VK_"+key, { shiftKey:false, metaKey:false, ctrlKey:true }); + (ctrl_change ? isnot : is)(element.value, previousValue, "value should " + + (ctrl_change?"":"not ") + "have changed while testing CTRL+key " + key + " (id: " + id + ")"); + previousValue = element.value; + synthesizeKey(" ", { shiftKey:false, metaKey:false, ctrlKey:true }); + var sel = Array.prototype.slice.call(element.options).map(function(o){return o.selected}) + is(""+sel, ""+data.s, "selected options match after CTRL+SPACE (after testing CTRL+key " + key + ") for (id: " + id + ")"); + previousValue = element.value; + }); + }; + select_test("one", one, false); + select_test("two", two_1, true); + select_test("two", two_2, false); + if (navigator.platform.indexOf("Mac") == -1) { + select_test("three", three_1, true); + select_test("three", three_2, false); + } else { + todo(false, "Make these tests work on OSX"); + } + SimpleTest.finish(); + }); + } +</script> +</head> +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=961363">Mozilla Bug 961363</a> +<div> + <ul> + <li> + <select id="one" multiple size="3"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + <li> + <select id="two" size="3"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + <li> + <select id="three" size="1"> + <option>0</option> + <option selected>1</option> + <option>2</option> + <option>3</option> + </select> + </li> + </ul> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_listcontrol_search.html b/layout/forms/test/test_listcontrol_search.html new file mode 100644 index 000000000..7f73c5f75 --- /dev/null +++ b/layout/forms/test/test_listcontrol_search.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=849438 +--> +<head> + <meta charset="utf-8"> + <title>Test for <select> list control search</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** This test will focus a select element and press a key that matches the + first non-space character of an entry. **/ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var select = document.getElementsByTagName('select')[0]; + select.focus(); + synthesizeKey('a', {}); + + is(select.options[0].selected, false, "the first option isn't selected"); + is(select.options[1].selected, true, "the second option is selected"); + + select.blur(); + + SimpleTest.finish(); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=849438">Mozilla Bug 849438</a> +<p id="display"></p> +<div id="content"> + <select> + <option>Please select an entry</option> + <option> a</option> + <option> b</option> + </select> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_prevent_default.html b/layout/forms/test/test_select_prevent_default.html new file mode 100644 index 000000000..85066e3c2 --- /dev/null +++ b/layout/forms/test/test_select_prevent_default.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=291082 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 291082</title> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + /** Test for Bug 291082 **/ + + + SimpleTest.waitForExplicitFinish(); + + function preventDefault(event) { + event.preventDefault(); + } + + function test() { + // Turn off Spatial Navigation because it hijacks arrow keydown events. + SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, runTest); + } + + function runTest() { + document.getElementById("keydown").addEventListener("keydown", preventDefault); + document.getElementById("keypress").addEventListener("keypress", preventDefault); + + SimpleTest.waitForFocus(function() { + if (navigator.platform.indexOf("Mac") == 0) { + todo(false, "Make this test work on OSX"); + SimpleTest.finish(); + return; + } + var testData = [ "one", "two", "three", "four", "keydown", "keypress" ]; + + // The order of the keys in otherKeys is important for the test to function properly. + var otherKeys = [ "DOWN", "UP", "RIGHT", "LEFT", "PAGE_DOWN", "PAGE_UP", + "END", "HOME" ]; + + testData.forEach(function(id) { + var element = document.getElementById(id); + element.focus(); + var previousValue = element.value; + sendChar('2'); + is(element.value, previousValue, "value should not have changed (id: " + id + ")"); + previousValue = element.value; + otherKeys.forEach(function(key) { + sendKey(key); + isnot(element.value, previousValue, "value should have changed while testing key " + key + " (id: " + id + ")"); + previousValue = element.value; + }); + }); + SimpleTest.finish(); + }); + } +</script> +</head> +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291082">Mozilla Bug 291082</a> +<div> + <ul> + <li> + <select id="one" onkeydown="event.preventDefault();"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select onkeydown="event.preventDefault();" + </li> + <li> + <select id="two" onkeypress="event.preventDefault();"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select onkeypress="event.preventDefault();" + </li> + <li onkeydown="event.preventDefault();"> + <select id="three"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + li onkeydown="event.preventDefault();" + </li> + <li onkeypress="event.preventDefault();"> + <select id="four"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + li onkeypress="event.preventDefault();" + </li> + <li> + <select id="keydown"> + <option>0</option> + <option>1</option> + <option>2</option> + </select> + select.addEventListener("keydown", function(event) { event.preventDefault(); }); + </li> + <li> + <select id="keypress"> + <option>0</option> + <option>1</option> + <option>2</option> + <option>9</option> + </select> + select.addEventListener("keypress", function(event) { event.preventDefault(); }); + </li> + </ul> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/layout/forms/test/test_select_vertical.html b/layout/forms/test/test_select_vertical.html new file mode 100644 index 000000000..570735641 --- /dev/null +++ b/layout/forms/test/test_select_vertical.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for select popup in vertical writing mode</title> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +<script> +SimpleTest.waitForExplicitFinish(); + +function test() { + SimpleTest.waitForFocus(function() { + var vlr = document.getElementById("vlr"); + var selWidth = vlr.offsetWidth; + var optWidth = vlr.children[0].offsetWidth; + + // We should be able to choose options from the vertical-lr <select> + // at positions increasingly to the right of the element itself. + is(vlr.value, "1", "(vertical-lr) initial value should be 1"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 1.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "2", "(vertical-lr) new value should be 2"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 2.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "3", "(vertical-lr) new value should be 3"); + + synthesizeMouse(vlr, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vlr, selWidth + 0.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vlr.value, "1", "(vertical-lr) value should be back to 1"); + + var vrl = document.getElementById("vrl"); + + // We should be able to choose options from the vertical-rl <select> + // at positions increasingly to the left of the element itself. + is(vrl.value, "1", "(vertical-rl) initial value should be 1"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -1.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "2", "(vertical-rl) new value should be 2"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -2.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "3", "(vertical-rl) new value should be 3"); + + synthesizeMouse(vrl, 5, 5, { type : "mousedown", button: 0 }); + synthesizeMouse(vrl, -0.5 * optWidth, 5, { type : "mouseup", button: 0 }); + + is(vrl.value, "1", "(vertical-rl) value should be back to 1"); + + SimpleTest.finish(); + }); +} +</script> + +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1113206">Mozilla Bug 1113206</a> +<div> + <select id="vlr" style="writing-mode: vertical-lr; margin: 80px;"> + <option value="1">one + <option value="2">two + <option value="3">three + <option value="4">four + </select> + <select id="vrl" style="writing-mode: vertical-rl; margin: 80px;"> + <option value="1">one + <option value="2">two + <option value="3">three + <option value="4">four + </select> +</div> diff --git a/layout/forms/test/test_textarea_resize.html b/layout/forms/test/test_textarea_resize.html new file mode 100644 index 000000000..c71f554e0 --- /dev/null +++ b/layout/forms/test/test_textarea_resize.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 477700</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="content" style="display: none"> +</div> + +<textarea id="textarea" style="-moz-appearance: none; border: 2px solid black; padding: 3px; box-sizing: border-box; min-width: 15px; min-height: 15px;">Text</textarea> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for textbox resizing **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(() => SimpleTest.executeSoon(doTheTest)); + +// -1 means use the default value which is 'both', then test explicitly +// setting each possible value. +var currentResize = -1; +var currentBoxSizing = 0; +var currentPointer = 0; +var resizeTypes = [ "horizontal", "vertical", "none", "inherit", "both" ]; +var boxSizingTypes = [ "", "border-box" ]; +var pointerTypes = [ synthesizeMouse, synthesizeTouch] + +function doTheTest() { + runTest(pointerTypes[currentPointer]); +} + +function runTest(aPointerFunc) { + var boxSizingText = " with box sizing " + (currentBoxSizing ? boxSizingTypes[currentBoxSizing] : "content-box"); + + var textarea = $("textarea"); + var rect = textarea.getBoundingClientRect(); + var touch = aPointerFunc.name.match(/Touch/); + // -1 means use the default value of resize, i.e. "both" + var type = (currentResize == -1) ? "both" : resizeTypes[currentResize]; + // assume that the resizer is in the lower right corner + + aPointerFunc(textarea, rect.width - 10, rect.height - 10, { type: touch ? "touchstart" : "mousedown" }); + aPointerFunc(textarea, rect.width + 40, rect.height + 40, { type: touch ? "touchmove" : "mousemove" }); + + var newrect = textarea.getBoundingClientRect(); + var hchange = (type == "both" || type == "horizontal"); + var vchange = (type == "both" || type == "vertical"); + + is(Math.round(newrect.width), Math.round(rect.width + (hchange ? 50 : 0)), + type + " width has increased" + boxSizingText + " using " + aPointerFunc.name); + is(Math.round(newrect.height), Math.round(rect.height + (vchange ? 50 : 0)), + type + " height has increased" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 20, rect.height - 20, { type: touch ? "touchmove" : "mousemove" }); + + newrect = textarea.getBoundingClientRect(); + + is(Math.round(newrect.width), Math.round(rect.width - (hchange ? 10 : 0)), + type + " width has decreased" + boxSizingText + " using " + aPointerFunc.name); + is(Math.round(newrect.height), Math.round(rect.height - (vchange ? 10 : 0)), + type + " height has decreased" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 220, rect.height - 220, { type: touch ? "touchmove" : "mousemove" }); + + newrect = textarea.getBoundingClientRect(); + ok(hchange ? newrect.width >= 15 : Math.round(newrect.width) == Math.round(rect.width), + type + " width decreased below minimum" + boxSizingText + " using " + newrect.width); + ok(vchange ? newrect.height >= 15 : Math.round(newrect.height) == Math.round(rect.height), + type + " height decreased below minimum" + boxSizingText + " using " + aPointerFunc.name); + + aPointerFunc(textarea, rect.width - 8, rect.height - 8, { type: touch ? "touchend" : "mouseup" }); + + textarea.style.width = "auto"; + textarea.style.height = "auto"; + + if (currentBoxSizing++ <= boxSizingTypes.length) { + textarea.style.MozBoxSizing = boxSizingTypes[currentBoxSizing]; + SimpleTest.executeSoon(doTheTest); + } else { + currentBoxSizing = 0; + if (++currentResize < resizeTypes.length) { + textarea.style.resize = resizeTypes[currentResize]; + SimpleTest.executeSoon(doTheTest); + } else { + currentResize = -1; + textarea.style.resize = ""; + if (++currentPointer < pointerTypes.length) { + SimpleTest.executeSoon(doTheTest); + } else { + SimpleTest.finish(); + } + } + } +} + +</script> +</pre> +</body> +</html> |